diff --git a/.clang-tidy b/.clang-tidy index a16136785..19c7dc642 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,3 +1,33 @@ +# Style or vendor rule sets that do not fit a header-only library: +# altera-*, fuchsia-*, llvmlibc-*, android-cloexec-fopen, boost-use-ranges, llvm-header-guard, +# llvm-include-order, llvm-prefer-static-over-anonymous-namespace, llvm-use-ranges, +# misc-use-anonymous-namespace, google-runtime-float, google-runtime-int, hicpp-signed-bitwise, +# bugprone-signed-bitwise, hicpp-explicit-conversions, google-explicit-constructor, +# cppcoreguidelines-explicit-constructor, misc-explicit-constructor (implicit conversions are the +# API), readability-trailing-comma, +# hicpp-avoid-goto / cppcoreguidelines-avoid-goto, hicpp-no-array-decay, +# hicpp-uppercase-literal-suffix, readability-uppercase-literal-suffix, modernize-use-trailing-return-type. +# Constructs the library needs on purpose (the library supports C++11 and legacy compilers): +# cppcoreguidelines-macro-usage, cppcoreguidelines-avoid-do-while (macros), +# cppcoreguidelines-pro-bounds-*, cppcoreguidelines-pro-type-reinterpret-cast, +# cppcoreguidelines-pro-type-union-access (parsers, serializers, basic_json's value union), +# cppcoreguidelines-avoid-const-or-ref-data-members, cppcoreguidelines-rvalue-reference-param-not-moved, +# cppcoreguidelines-avoid-non-const-global-variables, misc-non-private-member-variables-in-classes, +# misc-no-recursion (value walks are recursive, bounded by detail::recursion_depth_limit()), +# modernize-type-traits, modernize-use-constraints, modernize-use-ranges, modernize-use-std-numbers, +# modernize-use-designated-initializers, modernize-use-nodiscard, modernize-concat-nested-namespaces, +# readability-redundant-typename, readability-redundant-parentheses, +# readability-redundant-access-specifiers, readability-simplify-boolean-expr, +# readability-use-concise-preprocessor-directives, modernize-use-string-view, +# modernize-use-integer-sign-comparison (needs std::cmp_less; the flagged +# static_cast(end - pos) < n comparisons are deliberate) +# (all need a newer standard or compiler than C++11, or would change macro-generated or +# deliberately explicit code). +# Too noisy to be useful (numeric parsing/formatting code, doctest, tests): +# bugprone-easily-swappable-parameters, cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers, +# readability-identifier-length, readability-function-cognitive-complexity, performance-enum-size, +# concurrency-mt-unsafe, misc-confusable-identifiers, misc-include-cleaner (run on its own for +# the consumer headers in cmake/ci.cmake). # portability-avoid-pragma-once: kept disabled on purpose. #pragma once is accepted # by every supported compiler, and tools/amalgamate/amalgamate.py strips it from # single_include, so there is nothing left to fix here. @@ -10,14 +40,14 @@ Checks: '*, -android-cloexec-fopen, -boost-use-ranges, -bugprone-easily-swappable-parameters, - -cert-err58-cpp, - -clang-analyzer-webkit.NoUncountedMemberChecker, + -bugprone-signed-bitwise, -concurrency-mt-unsafe, -cppcoreguidelines-avoid-const-or-ref-data-members, -cppcoreguidelines-avoid-do-while, -cppcoreguidelines-avoid-goto, -cppcoreguidelines-avoid-magic-numbers, -cppcoreguidelines-avoid-non-const-global-variables, + -cppcoreguidelines-explicit-constructor, -cppcoreguidelines-macro-usage, -cppcoreguidelines-pro-bounds-avoid-unchecked-container-access, -cppcoreguidelines-pro-bounds-array-to-pointer-decay, @@ -26,19 +56,15 @@ Checks: '*, -cppcoreguidelines-pro-type-reinterpret-cast, -cppcoreguidelines-pro-type-union-access, -cppcoreguidelines-rvalue-reference-param-not-moved, - -cppcoreguidelines-virtual-class-destructor, -fuchsia-default-arguments-calls, -fuchsia-default-arguments-declarations, -fuchsia-overloaded-operator, -google-explicit-constructor, - -google-readability-function-size, -google-runtime-float, -google-runtime-int, -hicpp-avoid-goto, -hicpp-explicit-conversions, - -hicpp-function-size, -hicpp-no-array-decay, - -hicpp-no-assembler, -hicpp-signed-bitwise, -hicpp-uppercase-literal-suffix, -llvm-header-guard, @@ -48,6 +74,7 @@ Checks: '*, -llvmlibc-*, -misc-use-anonymous-namespace, -misc-confusable-identifiers, + -misc-explicit-constructor, -misc-include-cleaner, -misc-no-recursion, -misc-non-private-member-variables-in-classes, @@ -55,20 +82,22 @@ Checks: '*, -modernize-type-traits, -modernize-use-constraints, -modernize-use-designated-initializers, + -modernize-use-integer-sign-comparison, -modernize-use-nodiscard, -modernize-use-ranges, + -modernize-use-string-view, -modernize-use-std-numbers, -modernize-use-trailing-return-type, -performance-enum-size, -portability-avoid-pragma-once, -readability-function-cognitive-complexity, - -readability-function-size, -readability-identifier-length, -readability-magic-numbers, -readability-redundant-access-specifiers, -readability-redundant-parentheses, -readability-redundant-typename, -readability-simplify-boolean-expr, + -readability-trailing-comma, -readability-uppercase-literal-suffix, -readability-use-concise-preprocessor-directives' diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..314c895f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# generated files are collapsed in diffs and excluded from language statistics +single_include/** linguist-generated +nlohmann_json.natvis linguist-generated diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e1e1040fc..a23b24c31 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,5 @@ # JSON for Modern C++ was originally written by Niels Lohmann. -# Since 2013, over 250 contributors have helped to improve the library. +# Since 2013, over 400 contributors have helped to improve the library. # This CODEOWNERS file is only to make sure that @nlohmann is requested # for a code review in case of a pull request. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 9c3bb2c04..52c8f3385 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -98,7 +98,7 @@ $ ctest --test-dir build -j 10 The test suite should report: ``` -100% tests passed, 0 tests failed out of 98 +100% tests passed, 0 tests failed out of ``` #### Add tests @@ -149,14 +149,17 @@ make build -C docs/mkdocs # strict build: fails on broken links, anchor make check_mermaid -C docs/mkdocs # checks the Mermaid diagrams (requires Node.js) ``` -A new API page also needs an entry in [`docs/docset/docSet.sql`](https://github.com/nlohmann/json/blob/develop/docs/docset/docSet.sql), -the search index of the docset; `make build` reports missing entries. +The search index of the docset is generated from [`mkdocs.yml`](https://github.com/nlohmann/json/blob/develop/docs/mkdocs/mkdocs.yml) +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); +`make build` reports API pages that cannot be classified. ### Amalgamate the source code The single-header files -[`single_include/nlohmann/json.hpp`](https://github.com/nlohmann/json/blob/develop/single_include/nlohmann/json.hpp) and -[`single_include/nlohmann/json_fwd.hpp`](https://github.com/nlohmann/json/blob/develop/single_include/nlohmann/json_fwd.hpp) +[`single_include/nlohmann/json.hpp`](https://github.com/nlohmann/json/blob/develop/single_include/nlohmann/json.hpp), +[`single_include/nlohmann/json_fwd.hpp`](https://github.com/nlohmann/json/blob/develop/single_include/nlohmann/json_fwd.hpp), and +[`single_include/nlohmann/json_literals.hpp`](https://github.com/nlohmann/json/blob/develop/single_include/nlohmann/json_literals.hpp) are **generated** from the source files in the [`include/nlohmann` directory](https://github.com/nlohmann/json/tree/develop/include/nlohmann). **Do not** edit the files directly; instead, modify the include/nlohmann sources and regenerate the files by executing: @@ -179,7 +182,7 @@ The amalgamation check in CI fails if any of these generated files is out of dat ## Recommended documentation -- The library’s [README file](https://github.com/nlohmann/json/blob/master/README.md) is an excellent starting point to +- The library’s [README file](https://github.com/nlohmann/json/blob/develop/README.md) is an excellent starting point to understand its functionality. - The [documentation page](https://json.nlohmann.me) is the reference documentation of the library. - [RFC 8259](https://datatracker.ietf.org/doc/html/rfc8259) is the reference for the JavaScript Object Notation (JSON) @@ -218,9 +221,9 @@ feature macros then allows users to test their code against the library in the n ### Break C++11 language conformance This library is designed to work with C++11 and later. This means that any -[supported C++11 compiler](https://github.com/nlohmann/json/blob/master/README.md#supported-compilers) should compile +[supported C++11 compiler](https://github.com/nlohmann/json/blob/develop/README.md#supported-compilers) should compile the library without problems. Some compilers like GCC 4.7 (and earlier), Clang 3.3 (and earlier), or Microsoft Visual -Studio 13.0 and earlier are known not to work due to missing or incomplete C++11 support. +C++ 2013 and earlier are known not to work due to missing or incomplete C++11 support. Please do not add features that do not work with the mentioned supported compilers. Please guard features from C++14 and later against the respective [`JSON_HAS_CPP_14`](https://json.nlohmann.me/api/macros/json_has_cpp_11/) macros. @@ -236,12 +239,11 @@ extension. The following areas really need contribution and are always welcomed: - Extending the **continuous integration** toward more exotic compilers such as Android NDK, Intel's Compiler, or the - bleeding-edge versions Clang. -- Improving the efficiency of the **JSON parser**. The current parser is implemented as a naive recursive descent parser - with hand-coded string handling. More sophisticated approaches like LALR parsers would be really appreciated. That - said, parser generators like Bison or ANTLR do not play nice with single-header files -- I really would like to keep - the parser inside the `json.hpp` header, and I am not aware of approaches similar to [`re2c`](http://re2c.org) for - parsing. + bleeding-edge versions of Clang. +- Improving the efficiency of the **JSON parser**. The current parser is a hand-written iterative parser that keeps an + explicit stack instead of recursing, with hand-coded string handling. Speeding up the lexer or the value construction + would be appreciated. Parser generators like Bison or ANTLR do not play nice with single-header files -- I really + would like to keep the parser inside the `json.hpp` header. - Extending and updating existing **benchmarks** to include (the most recent version of) this library. Though efficiency is not everything, speed and memory consumption are very important characteristics for C++ developers, so having proper comparisons would be interesting. diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml index ed426675d..d578e592f 100644 --- a/.github/ISSUE_TEMPLATE/bug.yaml +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -52,7 +52,7 @@ body: If possible, provide a small and self-contained example that triggers the bug. Please understand that we cannot analyze and debug large code bases. Please do not paste screenshots here. - render: Shell + render: cpp - type: textarea id: output attributes: @@ -79,7 +79,7 @@ body: description: > Which version of the library did you use? If it is a released version, please enter the version number (e.g., 3.12.0). Otherwise, please enter - the commit hash. If you got the library from another source as the + the commit hash. If you got the library from another source than the GitHub repository (e.g., via a package manager), please also state this. validations: diff --git a/.github/external_ci/appveyor.yml b/.github/external_ci/appveyor.yml index 21bb2e27a..f2194aef1 100644 --- a/.github/external_ci/appveyor.yml +++ b/.github/external_ci/appveyor.yml @@ -18,7 +18,8 @@ environment: matrix: # The Visual Studio 2017 jobs compile everything with /std:c++17, so they # only build the C++17 variant of each test, split into two jobs each to - # stay below AppVeyor's 60-minute limit per job. + # stay below AppVeyor's 60-minute limit per job. The Visual Studio 2019 + # job builds the tests for all standards and is split for the same reason. - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 configuration: Debug platform: x86 @@ -50,7 +51,13 @@ environment: configuration: Release platform: x86 CXX_FLAGS: "/W4 /WX" - CMAKE_OPTIONS: "-DJSON_ImplicitConversions=OFF" + CMAKE_OPTIONS: "-DJSON_ImplicitConversions=OFF -DJSON_TestShard=0/2" + GENERATOR: Visual Studio 16 2019 + - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 + configuration: Release + platform: x86 + CXX_FLAGS: "/W4 /WX" + CMAKE_OPTIONS: "-DJSON_ImplicitConversions=OFF -DJSON_TestShard=1/2" GENERATOR: Visual Studio 16 2019 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 diff --git a/.github/labeler.yml b/.github/labeler.yml index 5708f84f5..f9e81e2bf 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -21,6 +21,17 @@ labels: files: - "cmake/.*" +# other build systems +- label: "CMake" + files: + - "(meson\\.build|meson_options\\.txt|BUILD\\.bazel|MODULE\\.bazel|Package\\.swift)" + +- label: "documentation" + files: + - ".github/ISSUE_TEMPLATE/.*" + - ".github/config\\.yml" + - ".github/(CONTRIBUTING|SECURITY|CODE_OF_CONDUCT|PULL_REQUEST_TEMPLATE)\\.md" + - label: "CI" files: - ".github/workflows/.*" @@ -45,6 +56,25 @@ labels: - label: "aspect: binary formats" title: "(?i)(bson|cbor|msgpack|messagepack|ubjson|bjdata|bon8|binary format)" +- label: "aspect: json_view" + files: + - "include/nlohmann/json_view\\.hpp" + - "include/nlohmann/detail/view/.*" + - "single_include/nlohmann/json_view\\.hpp" + - "tests/src/unit-json_view.*" + - "tests/src/json_view_test_helpers\\.hpp" + - "tests/src/fuzzer-(parse_json_view|json_view_image)\\.cpp" + - "tests/benchmarks/json_view/.*" + - "tools/amalgamate/config_json_view\\.json" + - "docs/mkdocs/docs/features/json_view\\.md" + - "docs/mkdocs/docs/api/basic_json_(document|view)/.*" + - "docs/mkdocs/docs/api/(ordered_)?json_(editable_)?(document|view)\\.md" + - "docs/mkdocs/docs/examples/(basic_json_(document|view)__|(ordered_)?json_(editable_)?(document|view)).*" + - "tests/benchmarks/src/benchmarks_view\\.cpp" + +- label: "aspect: json_view" + title: "(?i)(json_view|json_document|zero-copy)" + - label: "python" files: - "\\.py$" diff --git a/.github/workflows/cancel_closed_pr_runs.yml b/.github/workflows/cancel_closed_pr_runs.yml index 4d8ee33e5..99010d81b 100644 --- a/.github/workflows/cancel_closed_pr_runs.yml +++ b/.github/workflows/cancel_closed_pr_runs.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 + uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1 with: egress-policy: audit diff --git a/.github/workflows/check_amalgamation.yml b/.github/workflows/check_amalgamation.yml index a497b4ce4..8104a20af 100644 --- a/.github/workflows/check_amalgamation.yml +++ b/.github/workflows/check_amalgamation.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 + uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1 with: egress-policy: audit @@ -31,13 +31,13 @@ jobs: mkdir -p ./pr echo ${{ github.event.number }} > ./pr/number echo ${{ github.event.pull_request.user.login }} > ./pr/author - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: pr path: pr/ check: - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 env: MAIN_DIR: ${{ github.workspace }}/main INCLUDE_DIR: ${{ github.workspace }}/main/single_include/nlohmann @@ -46,7 +46,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 + uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1 with: egress-policy: audit @@ -77,14 +77,12 @@ jobs: cd $MAIN_DIR # Built from this PR's own tools/macro_builder/main.cpp, not a - # develop checkout: unlike amalgamate.py and generate_natvis.py, - # this tool has no other source of truth to check against (its - # own README documents that it must reproduce macro_scope.hpp - # byte for byte), so there is nothing to gain from checking out a - # separate copy, and doing so would make this step fail on a PR - # that adds support for a new dispatch table until that PR itself - # merges to develop, the same way generate_natvis.py's --version - # requirement briefly did. + # develop checkout: unlike amalgamate.py, this tool has no other + # source of truth to check against (its own README documents that + # it must reproduce macro_scope.hpp byte for byte), so there is + # nothing to gain from checking out a separate copy, and doing so + # would make this step fail on a PR that adds support for a new + # dispatch table until that PR itself merges to develop. TMPDIR=$(mktemp -d ./macro_builder_check.XXXXXX) c++ -std=c++11 tools/macro_builder/main.cpp -o "$TMPDIR/macro_builder" "$TMPDIR/macro_builder" > "$TMPDIR/paste.hpp" @@ -117,13 +115,15 @@ jobs: python3 $TOOL_DIR/amalgamate.py -c $TOOL_DIR/config_json.json -s . python3 $TOOL_DIR/amalgamate.py -c $TOOL_DIR/config_json_fwd.json -s . - cp include/nlohmann/json_literals.hpp $INCLUDE_DIR/json_literals.hpp + # the configuration of json_view.hpp comes with the pull request until + # it is on develop; the tool itself is still develop's + python3 $TOOL_DIR/amalgamate.py -c $MAIN_DIR/tools/amalgamate/config_json_view.json -s . # the header list of the Bazel "json" target must match the files in include/ cmake -P cmake/scripts/gen_bazel_build_file.cmake ${{ github.workspace }}/venv/bin/astyle --project=tools/astyle/.astylerc --suffix=none --quiet \ - $INCLUDE_DIR/json.hpp $INCLUDE_DIR/json_fwd.hpp + $INCLUDE_DIR/json.hpp $INCLUDE_DIR/json_fwd.hpp $INCLUDE_DIR/json_view.hpp # fail loudly if a directory is renamed or removed: find would only warn # about the missing path and silently drop its files from the check @@ -138,25 +138,43 @@ jobs: ${{ github.workspace }}/venv/bin/astyle --project=tools/astyle/.astylerc --suffix=none --quiet \ $(find $SOURCE_DIRS -type f \( -name '*.hpp' -o -name '*.cpp' -o -name '*.cu' \) -not -path 'tests/thirdparty/*' -not -path 'tests/abi/include/nlohmann/*' | sort) + # json_literals.hpp is copied verbatim from the (now formatted) include/ + # directory, like `make amalgamate` does + cp include/nlohmann/json_literals.hpp $INCLUDE_DIR/json_literals.hpp + - name: Regenerate nlohmann_json.natvis run: | cd $MAIN_DIR - # Pass --version explicitly so this step also works with the tool - # copy from develop before this repository's own generate_natvis.py - # learns to derive the version itself: the older script requires - # --version, and the newer one accepts it as an explicit override. - ABI_MACROS=include/nlohmann/detail/abi_macros.hpp - VERSION_MAJOR=$(grep -m1 'define NLOHMANN_JSON_VERSION_MAJOR' $ABI_MACROS | grep -o '[0-9]\+') - VERSION_MINOR=$(grep -m1 'define NLOHMANN_JSON_VERSION_MINOR' $ABI_MACROS | grep -o '[0-9]\+') - VERSION_PATCH=$(grep -m1 'define NLOHMANN_JSON_VERSION_PATCH' $ABI_MACROS | grep -o '[0-9]\+') - # The template is what this check verifies the committed natvis - # file against, so take it from this PR rather than from the - # develop checkout: generate_natvis.py loads the template from its - # own directory, and with develop's copy a PR that changes the - # template could never pass. Like the macro_builder step above, - # this only renders PR content in a job without credentials. - cp tools/generate_natvis/nlohmann_json.natvis.j2 $NATVIS_TOOL_DIR/ - python3 $NATVIS_TOOL_DIR/generate_natvis.py --version "$VERSION_MAJOR.$VERSION_MINOR.$VERSION_PATCH" $MAIN_DIR + # The generator and its template are what this check verifies the + # committed natvis file against (which namespaces it lists, and + # what each entry looks like), so run this PR's copy rather than + # the develop checkout's: with develop's, a PR that changes either + # could never pass. generate_natvis.py loads the template from its + # own directory. Like the macro_builder step above, this only runs + # PR content in a job without credentials. + python3 tools/generate_natvis/generate_natvis.py $MAIN_DIR + + - name: Check the nlohmann_json.natvis generator options + run: | + cd $MAIN_DIR + # The options for configurations outside the checked-in file write + # to a temporary file, so they do not affect the diff below. + GENERATOR=tools/generate_natvis/generate_natvis.py + python3 $GENERATOR --list-tags + TAG_COUNT=$(python3 $GENERATOR --list-tags | wc -l) + + python3 $GENERATOR --abi-tags diag,_dp,_vns --abi-tags _diag --output $RUNNER_TEMP/abi_tags.natvis + grep -q '' $RUNNER_TEMP/abi_tags.natvis + grep -q '' $RUNNER_TEMP/abi_tags.natvis + test $(grep -c ' F{"just testing first?"} F -->|"yes"| G["contains() / find()"] ``` + +## Does this key exist? + +Use [`contains`](../../api/basic_json/contains.md) to test for a key; it does not modify the value and does not throw: + +```cpp +json j = {{"name", "Mary"}, {"address", {{"city", "Berlin"}}}}; + +j.contains("name"); // true +j.contains("age"); // false +j.contains("city"); // false: contains() only looks at the top level +j["address"].contains("city"); // true +j.contains(json::json_pointer("/address/city")); // true: a JSON pointer tests a nested path in one call +``` + +If the value is needed afterwards, [`find`](../../api/basic_json/find.md) tests and accesses with a single lookup: + +```cpp +if (auto it = j.find("name"); it != j.end()) +{ + std::cout << *it << '\n'; +} +``` + +`contains` also returns `true` for a key whose value is `null`; use `j.contains("k") && !j["k"].is_null()` if a +`null` should count as missing. To read a value with a fallback instead, see [`value`](default_value.md). + +!!! warning "`operator[]` inserts missing keys" + + Like `#!cpp std::map::operator[]`, the non-const [`operator[]`](unchecked_access.md) adds a `null` value if the key + is missing. Testing a key with it therefore *changes the object*: + + ```cpp + json j = {{"foo", 1}}; + bool absent = j["bar"].is_null(); // true, but j is now {"bar": null, "foo": 1} + ``` + + Use `contains`/`find` to test, and [`at`](checked_access.md) to access a value that must exist (it throws an + [`out_of_range`](../../home/exceptions.md#out-of-range) exception otherwise). + +## See also + +- [**operator[]**](../../api/basic_json/operator%5B%5D.md) - unchecked access +- [**at**](../../api/basic_json/at.md) - checked access +- [**value**](../../api/basic_json/value.md) - access with default value +- [**contains**](../../api/basic_json/contains.md) and [**find**](../../api/basic_json/find.md) - test for existence +- [**front**](../../api/basic_json/front.md) and [**back**](../../api/basic_json/back.md) - first and last element +- [**items**](../../api/basic_json/items.md) - iterate over keys and values diff --git a/docs/mkdocs/docs/features/index.md b/docs/mkdocs/docs/features/index.md index aaf1253a7..88443c796 100644 --- a/docs/mkdocs/docs/features/index.md +++ b/docs/mkdocs/docs/features/index.md @@ -12,6 +12,10 @@ C++ types, and finally serialize it again. [JSON Lines](parsing/json_lines.md), [callbacks](parsing/parser_callbacks.md), the [SAX interface](parsing/sax_interface.md), [error handling](parsing/parse_exceptions.md), and [parsing untrusted input](parsing/untrusted_input.md). +- [Zero-copy JSON views](json_view.md) — read a JSON text through a flat index instead of building a `json` tree; + strings and numbers stay in the input and are only decoded when needed. +- [Editable documents](json_view.md#editing-a-document) — modify a parsed document without building a `json` tree. +- [Images](json_view.md#images) — save a parsed document and load it again without parsing it. - [Comments](comments.md) and [trailing commas](trailing_commas.md) — opt-in relaxations of the JSON grammar. ## Accessing and modifying values @@ -55,3 +59,11 @@ C++ types, and finally serialize it again. This section gives conceptual overviews. For the precise signature, parameters, and return value of a function, see the [API Documentation](../api/basic_json/index.md). + +## See also + +- [API documentation](../api/basic_json/index.md) - reference of all classes and functions +- [FAQ](../home/faq.md) - answers to common questions +- [Exceptions](../home/exceptions.md) - all exceptions and their error codes +- [Design goals](../home/design_goals.md) +- [Integration](../integration/index.md) - how to add the library to a project diff --git a/docs/mkdocs/docs/features/json_pointer.md b/docs/mkdocs/docs/features/json_pointer.md index 786832c96..1bc5eabdb 100644 --- a/docs/mkdocs/docs/features/json_pointer.md +++ b/docs/mkdocs/docs/features/json_pointer.md @@ -77,6 +77,10 @@ auto val2 = j.at(json::json_pointer("/nested/three/1")); // false auto val3 = j.value(json::json_pointer("/nested/four"), 0); // 0 ``` +To read a value with a fallback, use [`value`](../api/basic_json/value.md) with a JSON Pointer; to test for existence, +use [`contains`](../api/basic_json/contains.md). Neither needs intermediate checks, see +[Nested values](element_access/default_value.md#nested-values). + !!! note "Creating intermediate levels that don't exist" See the [`operator[]` notes](../api/basic_json/operator%5B%5D.md#return-value) for how array vs. object is @@ -126,6 +130,8 @@ auto j_original = j_flat.unflatten(); ## See also - Class [`json_pointer`](../api/json_pointer/index.md) +- Functions [`value`](../api/basic_json/value.md), [`contains`](../api/basic_json/contains.md), and + [`at`](../api/basic_json/at.md) accept JSON Pointers; see [Nested values](element_access/default_value.md#nested-values) - Function [`flatten`](../api/basic_json/flatten.md) - Function [`unflatten`](../api/basic_json/unflatten.md) - [JSON Patch](json_patch.md) - paths inside a patch are JSON Pointers diff --git a/docs/mkdocs/docs/features/json_view.md b/docs/mkdocs/docs/features/json_view.md new file mode 100644 index 000000000..4f0938273 --- /dev/null +++ b/docs/mkdocs/docs/features/json_view.md @@ -0,0 +1,378 @@ +# Zero-copy JSON views + +`#!cpp ` adds a read-only, non-owning way to look at a parsed JSON text, as an alternative to +building a [`basic_json`](../api/basic_json/index.md) tree with [`parse()`](../api/basic_json/parse.md). + +## The problem + +[`basic_json::parse()`](../api/basic_json/parse.md) builds a tree of `basic_json` values: one allocation for every +array and object, and every string copied into its own `std::string`. That is the right trade-off when the program +goes on to read and write the value freely, but it does more work than necessary when only a small part of a large +JSON text is actually needed, or when the same text is parsed over and over (many small messages, for instance) and +most of the resulting tree is thrown away almost immediately. + +## The idea + +[`basic_json_document::parse()`](../api/basic_json_document/parse.md) parses the same JSON grammar, with the same +options, but instead of a tree it builds a flat index of the values it found: one +[16-byte entry](../home/architecture.md#node-index-of-json-views) per value (and one per object key), in document +order. Integers are converted while parsing and stored in their entry. Floating-point numbers and strings are +referenced in place in the source text: a float is converted when it is read, and only strings that contain escape +sequences are decoded, into one shared buffer owned by the document. + +[`basic_json_view`](../api/basic_json_view/index.md) is a small, trivially copyable handle (two pointers) into that +index. It gives you the read-only, type-inspection part of the `basic_json` interface -- +[`type()`](../api/basic_json_view/type.md) and the `is_*()` predicates, +[`size()`](../api/basic_json_view/size.md)/[`empty()`](../api/basic_json_view/empty.md) -- as well as element access +([`operator[]`](../api/basic_json_view/operator%5B%5D.md), [`at`](../api/basic_json_view/at.md), +[`front`](../api/basic_json_view/front.md)/[`back`](../api/basic_json_view/back.md)), lookup +([`find`](../api/basic_json_view/find.md), [`contains`](../api/basic_json_view/contains.md), +[`count`](../api/basic_json_view/count.md)), and iteration +([`begin`](../api/basic_json_view/begin.md)/[`end`](../api/basic_json_view/end.md), +[`items`](../api/basic_json_view/items.md)) -- without ever allocating a `basic_json` value. When you do need an +actual `basic_json` value for a subtree, [`materialize()`](../api/basic_json_view/materialize.md) builds exactly the +one [`parse()`](../api/basic_json/parse.md) would have produced for it. + +## How to use it + +Include `` in addition to (or instead of) ``. Parse into a +[`json_document`](../api/json_document.md), inspect its [`root()`](../api/basic_json_document/root.md), and +`materialize()` when you need a real value. `materialize()` works on any view, not just the root, so you can turn just +the subtree you need into a value: + +??? example "Example: parse a document, inspect its root, and materialize it" + + ```cpp + --8<-- "examples/json_document.cpp" + ``` + + Output: + + ```json + --8<-- "examples/json_document.output" + ``` + +[`ordered_json_document`](../api/ordered_json_document.md) is the equivalent for +[`ordered_json`](../api/ordered_json.md), just as [`ordered_json`](../api/ordered_json.md) is to +[`json`](../api/json.md). + +## Ownership and lifetime + +A document either **borrows** the text it was parsed from, or **owns** its own copy of it; call +[`owns_source()`](../api/basic_json_document/owns_source.md) to find out which happened. +[`parse()`](../api/basic_json_document/parse.md) decides this from the value category and type of its argument (an +lvalue `#!cpp std::string` is borrowed; an rvalue `#!cpp std::string` is moved in, owned without a copy; a stream is +read into an owned buffer; and so on -- see [`parse`'s Notes](../api/basic_json_document/parse.md#notes) for the full +table). [`parse_copy()`](../api/basic_json_document/parse_copy.md) always owns a copy, regardless of the input. + +!!! warning "A borrowed document depends on your buffer" + + If a document borrows its text, that text **must outlive the document** (and every view taken from it) and must + stay **unmodified** for as long as the document exists, since floating-point values and unescaped strings are + read from it lazily. Reading or writing through a view after the underlying buffer is gone is undefined behavior, + exactly as it would be for a dangling `#!cpp std::string_view`. + +A view is valid only while all of the following hold: + +- the document is alive, +- the document has not been re-parsed since the view was taken (with [`read()`](../api/basic_json_document/read.md) + or [`parse()`](../api/basic_json_document/parse.md) into it), +- the document has not had [`shrink_to_fit()`](../api/basic_json_document/shrink_to_fit.md) called on it since, and +- if the document borrows its source text, that text is still alive and unmodified. + +Moving the `basic_json_document<>` itself is fine and does **not** invalidate its views: the index is a separate heap +allocation that keeps its address across the move. Take a fresh view from [`root()`](../api/basic_json_document/root.md) +whenever any of the other conditions above was not met. + +A view can dangle, exactly like a `#!cpp std::string_view`: nothing stops you from using it after its document is +gone. This is deliberate. A view is two pointers and trivially copyable; making it safe (a `#!cpp std::shared_ptr` in +the document and a `#!cpp std::weak_ptr` in each view) would make it larger, add reference counting to every copy, +and a `lock()` to every access -- exactly the costs the view exists to avoid. What is caught is the obvious case: +because a view dies with its document, [`root()`](../api/basic_json_document/root.md) is not callable on a temporary +document: `#!cpp auto v = json_document::parse(text).root();` does not compile. Give the document a name first. + +??? example "Example: borrowed and owned documents, and when views become invalid" + + ```cpp + --8<-- "examples/json_view_ownership.cpp" + ``` + + Output: + + ```json + --8<-- "examples/json_view_ownership.output" + ``` + +## What is the same as `parse()` + +- **Accept/reject.** [`accept()`](../api/basic_json_document/accept.md) and + [`parse()`](../api/basic_json_document/parse.md) accept and reject exactly the same inputs as + [`basic_json::accept()`](../api/basic_json/accept.md)/[`basic_json::parse()`](../api/basic_json/parse.md), with the + same `ignore_comments` and `ignore_trailing_commas` options. +- **Errors.** A failing parse throws the same exception -- the same id, message, and position -- because on failure + the library's own parser is run on the same bytes to produce the diagnostic. Valid input never pays for this, and + the replay builds no values: it costs about a lexing pass up to the error. + `#!cpp allow_exceptions == false` skips the replay and gives a + [discarded](../api/basic_json_document/is_discarded.md) document instead of throwing, just as it gives a discarded + value for `#!cpp basic_json::parse()`. +- **Number classification.** An integer literal that does not fit into the 64-bit integer type becomes a + floating-point value, exactly as it does for `#!cpp basic_json::parse()`. +- **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) + behave the same way they do for ``. 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) + needs parent pointers in `basic_json` values) or only affect the values `materialize()` produces, through + `BasicJsonType`. + +## What is different + +- **Only 64-bit integers.** `basic_json_document` requires `BasicJsonType::number_integer_t` and + `number_unsigned_t` to both be 64 bits wide; this is a compile-time `#!cpp static_assert`. +- **A 4 GiB input limit.** An input of 4294967280 bytes (4 GiB minus 16 bytes) or more throws + [`out_of_range.416`](../home/exceptions.md#jsonexceptionout_of_range416), a limit + `#!cpp basic_json::parse()` does not have. It keeps an index entry at 16 bytes: offsets and counts are 32 bits + wide. The limit is 16 bytes below 2^32, because the parser reads in 16-byte steps, and a position one step past the + end must still fit. +- **A stream is always read to its end.** There is no partial/streaming read of an `#!cpp std::istream`: the stream + is read to its end into a buffer the document owns, and parsing starts after that. +- **No source positions on `materialize()`.** Even with + [`JSON_DIAGNOSTIC_POSITIONS`](../api/macros/json_diagnostic_positions.md) enabled, + [`materialize()`](../api/basic_json_view/materialize.md) does not set them. The index has the start offset of every + value, but not the end of arrays, objects, and escaped strings, which the positions also need. +- **Objects iterate in document order.** [`begin()`](../api/basic_json_view/begin.md)/ + [`end()`](../api/basic_json_view/end.md) and [`items()`](../api/basic_json_view/items.md) visit an object's + members in the order they appear in the source text. `basic_json`'s default `object_t` is a `std::map`, which + sorts by key, so iterating a [`materialize()`](../api/basic_json_view/materialize.md)d value can print members in + a different order than iterating the view they came from. +- **Chained access is safe.** [`operator[]`](../api/basic_json_view/operator%5B%5D.md) with a missing key, an index + out of range, or an unresolvable JSON pointer returns a [discarded](../api/basic_json_view/is_discarded.md) view, and + `operator[]` on a discarded view returns a discarded view without throwing: `#!cpp v["a"]["b"][0]` can be tested + once at the end. Type errors on values that exist (a key on an array, an index on an object) still throw, and + [`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) + 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<`. + [`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 + [`materialize()`](../api/basic_json_view/materialize.md) or [`parse()`](../api/basic_json/parse.md) would produce + equal values for them, without ever building a tree to do it. For ordering, too, + [`materialize()`](../api/basic_json_view/materialize.md) is the way to get a value you can compare. + +## Duplicate keys + +!!! warning "A lookup in a view and in the parsed value can give different answers" + + If an object in the source text repeats a key, [`operator[]`](../api/basic_json_view/operator%5B%5D.md), + [`at`](../api/basic_json_view/at.md), [`find`](../api/basic_json_view/find.md), + [`value`](../api/basic_json_view/value.md), [`contains`](../api/basic_json_view/contains.md), and JSON pointer + resolution all return the **first** member with that key. `basic_json::parse()` instead keeps the **last** value + of a repeated key, so for `#!cpp {"a":1,"a":2}`, `#!cpp view["a"]` is `#!cpp 1` while `#!cpp parse(text)["a"]` is + `#!cpp 2`. [`count`](../api/basic_json_view/count.md) counts every member with the key, so it is `#!cpp 2` here. + +[`begin()`](../api/basic_json_view/begin.md)/[`end()`](../api/basic_json_view/end.md) and +[`items()`](../api/basic_json_view/items.md) visit *every* occurrence, in document order, and +[`size()`](../api/basic_json_view/size.md) counts all of them, so `#!cpp view.size()` can be larger than +`#!cpp view.materialize().size()`. + +**Why the first?** A lookup can stop as soon as it finds a match. Returning the last member would force every lookup +to scan all members of the object, even when the key is found at the very first one: this made lookups in small +objects 1.6 to 3.4 times slower. The hash index of the keys of objects with 128 members or more leads to the first member of a +key as well. Other zero-copy parsers that index the source text, such as yyjson and simdjson, also return the first +member. RFC 8259 only says that names within an object SHOULD be unique and that the behavior of a receiver that sees +duplicates is unpredictable, so neither choice is wrong. + +**What stays the same as `parse()`?** [`materialize()`](../api/basic_json_view/materialize.md) and +[`get>()`](../api/basic_json_view/get.md) replay every member in order, so they keep the *last* value +exactly like `#!cpp basic_json::parse()` (at the position of the first occurrence of the key, for an +[`ordered_json`](../api/ordered_json.md)). + +**How do I get the value `parse()` would give?** Either call `#!cpp view.materialize()` and look the key up in the +result, or iterate the members and keep the last match: + +```cpp +// the last member with the key "a", as parse() would keep it +json_view last; +for (const auto item : view.items()) +{ + if (item.key() == "a") + { + last = item.value(); + } +} +``` + +If you cannot trust the source text to have unique keys, check [`size()`](../api/basic_json_view/size.md) against the +number of distinct keys, or reject duplicates when iterating. + +## Getting values out without copying + +[`get()`](../api/basic_json_view/get.md) converts many `T` directly from the flat index, without ever building a +`basic_json` value for the conversion: `#!cpp bool`, arithmetic types, `#!cpp std::nullptr_t`, +`#!cpp std::string`/other `#!cpp std::basic_string`s (copied once), `basic_json`/`ordered_json` (via +[`materialize()`](../api/basic_json_view/materialize.md)), `basic_json_view` itself, `#!cpp std::vector`, and +`#!cpp std::map`/`#!cpp std::unordered_map` with string-like keys. Every other type -- `#!cpp std::list`, +`#!cpp std::pair`, `#!cpp std::array`, enumerations, user types with a `from_json()` -- goes through +[`materialize()`](../api/basic_json_view/materialize.md)`.get()` instead: the subtree is built into a real +`basic_json` value first, exactly as [`parse()`](../api/basic_json/parse.md) would, and converted from there. + +Two conversions never copy at all: + +- [`get_string()`](../api/basic_json_view/get_string.md) (equivalently, `#!cpp get()`) returns a + string as a `string_view_t` pointing into the document's [`source()`](../api/basic_json_document/source.md) text -- + or, for a string that contains escape sequences, into the document's own buffer of decoded strings -- instead of + allocating a new `#!cpp std::string`. +- [`number_token()`](../api/basic_json_view/number_token.md) returns a number exactly as it was written in the + source, e.g. `#!cpp "1.50"`, `#!cpp "1E2"`, or an integer with more digits than any number type holds, instead of + rounding it into a `#!cpp double`/`#!cpp int64_t` the way `#!cpp get()` (and + [`basic_json::parse()`](../api/basic_json/parse.md)) would. + +Both results are only valid as long as the view -- and, for a string with no escapes, the borrowed source text -- is. + +## Writing a view back + +[`dump()`](../api/basic_json_view/dump.md) serializes a view directly from the flat index, without ever building a +`basic_json` value. An object's members are written in document order, not sorted by key, and *every* occurrence of a +repeated key is written, not only the last one -- the same two ways [iteration](#what-is-different) already differs +from a [`materialize()`](../api/basic_json_view/materialize.md)d value, see [Duplicate keys](#duplicate-keys). +`#!cpp materialize().dump()` gives a different result in both respects for a `json_view`. + +By default, numbers are written the way [`basic_json::dump()`](../api/basic_json/dump.md) would. +[`number_format::source`](../api/basic_json_view/number_format.md) instead copies every number exactly as it was +written in the source text -- a price like `#!cpp 19.90`, a long order or account ID with more digits than any number +type holds, or a high-precision coordinate -- something `basic_json` cannot do at all, since parsing already reduces +a number to its parsed `#!cpp double`/`#!cpp int64_t` value. + +[`operator<<`](../api/basic_json_view/operator_ltlt.md) writes a view to a stream the way `basic_json`'s does, using +the stream's `width`/`fill` for indentation. + +## Editing a document + +Everything above is read-only: a `json_document`/`json_view` lets you look at a parsed text without copying it, but +not change it. [`basic_json_document`](../api/basic_json_document/index.md) -- more conveniently +spelled [`json_editable_document`](../api/json_editable_document.md) or +[`ordered_json_editable_document`](../api/ordered_json_editable_document.md) -- also lets you +[`set`](../api/basic_json_document/set.md) a value, [`push_back`](../api/basic_json_document/push_back.md) onto or +[`insert`](../api/basic_json_document/insert.md) into an array, and [`erase`](../api/basic_json_document/erase.md) +an object member or an array element, still without ever building a `basic_json` tree for parts you do not touch. + +`#!cpp Editable` defaults to `#!cpp false`, so `json_document`/`ordered_json_document` are unaffected -- they carry +none of the bookkeeping edits need, and calling `set`/`push_back`/`insert`/`erase` on one is a compile error, not a +runtime one. + +### Why: editing without reformatting + +The `#!cpp 19.90` price from [above](#writing-a-view-back) is exactly the kind of value that makes editing a `json` +or `ordered_json` value in place lossy. Say you parse a configuration file, patch one field, and write it back: + +- **`json`** re-sorts every key on the way in (`object_t` is a `#!cpp std::map`) and rewrites every number to its + shortest round-trip form on the way out -- a one-field patch turns into a diff that reorders the whole file and + rewrites `#!cpp 19.90` to `#!cpp 19.9`. +- **`ordered_json`** keeps the key order, but still rewrites every number the same way: parsing has already reduced + it to a `#!cpp double`/`#!cpp int64_t`, and there is no way back to how it was spelled in the source text. + +An editable document keeps both. [`dump()`](../api/basic_json_view/dump.md) of an edited document writes members in +document order -- a member [`set`](../api/basic_json_document/set.md) added goes at the end, exactly where it was +inserted, and an [`erase`](../api/basic_json_document/erase.md)d member simply leaves a gap: everything around it +keeps its place -- and [`number_format::source`](../api/basic_json_view/number_format.md) keeps the exact spelling +of every number an edit did not itself touch; a number an edit *did* touch is written the way +[`BasicJsonType::dump()`](../api/basic_json/dump.md) would write it, since there is no source spelling for a brand +new value. + +??? example "Example: patch a configuration, keeping member order and number spellings" + + ```cpp + --8<-- "examples/json_editable_document.cpp" + ``` + + Output: + + ```json + --8<-- "examples/json_editable_document.output" + ``` + +### What stays valid, and what an edit costs + +The source text itself is **never written**, and the parsed index never moves -- a value keeps the node it was +parsed into for as long as it is not itself replaced. So every [view](../api/basic_json_view/index.md) taken before +an edit, including a previously obtained [`root()`](../api/basic_json_document/root.md), stays valid and, if it +still refers to the edited value, sees the edit; a view of a value a later edit drops or replaces just keeps showing +what it last held. New values go to storage the document allocates and owns on demand. The one thing an edit does +invalidate is the **iterators** taken over an edited array or object: the first time one of its elements is set, +appended to, inserted into, or erased, its elements move from the parsed, fixed layout to a growable block of links +so that [`push_back`](../api/basic_json_document/push_back.md) can later grow it in amortized constant time -- +existing elements are not touched, but an iterator that was walking the old layout no longer matches. A string +obtained with +[`get_string()`](../api/basic_json_view/get_string.md) is unaffected either way and stays valid across further +edits. See [`basic_json_document`'s Edits](../api/basic_json_document/index.md#edits) for the details, and +[`set`'s Exception safety](../api/basic_json_document/set.md#exception-safety) for what an edit guarantees if it +throws (the *basic* guarantee, not the strong one `dump()` and the read-only functions provide). How edits are kept in +the index is described in the [architecture overview](../home/architecture.md#node-index-of-json-views). + +## Images + +[`save()`](../api/basic_json_document/save.md) writes a document as an *image*: a byte buffer that +[`load()`](../api/basic_json_document/load.md) reads back into a document without parsing -- no lexing, no building +the node index, nothing but copying the nodes and pointing the text and the decoded strings at the image. Where +[`parse_copy()`](../api/basic_json_document/parse_copy.md) still has to scan the whole input, +[`load()`](../api/basic_json_document/load.md) turns that scan into a copy of the node index alone. + +**Why.** A document that is parsed once and then read many times -- a configuration loaded at startup, a template +rendered on every request, a large reference dataset a worker process needs in memory -- pays for parsing once but +can amortize [`save()`](../api/basic_json_document/save.md)'s cost across every later load. That makes images useful +for a cache: save a document the first time it is parsed (to a file, a shared-memory segment, an in-process cache), +and [`load()`](../api/basic_json_document/load.md) it on every later use instead of parsing the source text again. +They are just as useful for handing a parsed document to another process (or a forked worker) running the same build +of the library, since [`load()`](../api/basic_json_document/load.md) turns the transfer into a copy of the node index +plus pointers into the received bytes, not a re-parse. + +**Choosing a check.** [`load()`](../api/basic_json_document/load.md) takes an +[`image_check`](../api/basic_json_document/load.md#image_check) that trades validation against speed: +`image_check::full` (the default) checks everything the parser itself guarantees, so a checked image is exactly as +safe to read and serialize as a freshly parsed document -- the right choice whenever the image did not come straight +from this process's own [`save()`](../api/basic_json_document/save.md), such as a file or a network peer. +`image_check::bounds` only checks structure and bounds -- cheaper, since it skips scanning the text and the decoded +strings -- and fits a cache the process trusts, one it wrote and reads back itself. `image_check::none` skips +validation entirely, for an image trusted as much as the process's own memory. See +[`load()`'s Notes](../api/basic_json_document/load.md#notes) for exactly what each level does and does not guarantee. + +??? example "Example: cache a parsed configuration as an image" + + ```cpp + --8<-- "examples/basic_json_document__save.cpp" + ``` + + Output: + + ```json + --8<-- "examples/basic_json_document__save.output" + ``` + +!!! warning "Experimental" + + The image format is versioned but not yet stable, and may change in an incompatible way before it is declared + stable. It is little-endian only, and tied to the library build that wrote it -- use it to cache a document or to + hand one to another process running the *same* build, not as a long-term storage format; keep the original JSON + text if a saved document needs to be readable by a future library version. + +The idea of a document you can read without parsing comes from zero-copy formats such as +[FlatBuffers](https://github.com/google/flatbuffers) and [YaFF](https://github.com/yandex/yaff); the check +[`load()`](../api/basic_json_document/load.md) runs follows the idea of FlatBuffers' Verifier. No code is taken from +either. + +## Choosing between `json`, `ordered_json`, the SAX interface, and `json_view` + +| | [`json`](../api/json.md) / [`ordered_json`](../api/ordered_json.md) | [SAX interface](parsing/sax_interface.md) | [`json_document`](../api/json_document.md) / [`json_view`](../api/json_view.md) | [`json_editable_document`](../api/json_editable_document.md) / [`json_editable_view`](../api/json_editable_view.md) | +|---|---|---|---|---| +| **Ownership** | owns every value | owns nothing; you decide what to keep, in your handler | borrows or owns the *text*; the index is always owned by the document | same as `json_document`; edits go to storage the document owns | +| **Mutability** | freely mutable | not applicable (a one-shot event stream) | read-only | [`set`](../api/basic_json_document/set.md)/[`push_back`](../api/basic_json_document/push_back.md)/[`insert`](../api/basic_json_document/insert.md)/[`erase`](../api/basic_json_document/erase.md) edit in place; the source text is never rewritten | +| **What you get** | a full tree you can read, write, and keep as long as you like | a sequence of callbacks; whatever your handler builds from them | a flat index plus, on demand, [`materialize()`](../api/basic_json_view/materialize.md)d `json`/`ordered_json` values for the parts you actually use | the same, plus [`dump()`](../api/basic_json_view/dump.md) of an edited document that keeps the member order and, with [`number_format::source`](../api/basic_json_view/number_format.md), the spelling of every untouched number | +| **Typical use** | general-purpose JSON handling: config, request/response bodies you build or modify, anything you hold onto | validating or projecting a text into your own data structure without ever holding the whole thing as JSON | large or high-volume input where you only need part of it, or need it repeatedly, and can keep the source text (or a copy) alive for as long as the document lives | a document you read, patch a few fields of, and write back -- a configuration file, for instance -- where the rest of it should come back exactly as it was | +| **Caching/reload** | not applicable -- re-parse, or roll your own serialization | not applicable | [`save()`](../api/basic_json_document/save.md)/[`load()`](../api/basic_json_document/load.md): cache the parsed index as an image and reload it without parsing | same, saving the document's current -- possibly edited -- state | + +## Version history + +- Added in version 3.13.0. diff --git a/docs/mkdocs/docs/features/macros.md b/docs/mkdocs/docs/features/macros.md index ef41ce0d5..773ea5ac3 100644 --- a/docs/mkdocs/docs/features/macros.md +++ b/docs/mkdocs/docs/features/macros.md @@ -82,6 +82,45 @@ To override the built-in check, define `JSON_HAS_FILESYSTEM` or `JSON_HAS_EXPERI See [full documentation of `JSON_HAS_FILESYSTEM` and `JSON_HAS_EXPERIMENTAL_FILESYSTEM`](../api/macros/json_has_filesystem.md). +## `JSON_HAS_RANGES` + +The library uses `std::ranges` (and concepts) where available, for example, to parse from C++20 ranges and to +construct JSON arrays from range views. The library detects whether the standard library supports ranges and +disables the support on toolchains with an incomplete implementation. To override the built-in check, define +`JSON_HAS_RANGES` to `1` or `0`. + +See [full documentation of `JSON_HAS_RANGES`](../api/macros/json_has_ranges.md). + +## `JSON_HAS_RANGE_VIEW_CONVERSION` + +When `JSON_HAS_RANGES` is enabled (and the compiler is not MinGW), a JSON array can be constructed directly from a C++20 +range view such as `std::views::filter(...)`. To override the built-in check, define `JSON_HAS_RANGE_VIEW_CONVERSION` to +`1` or `0`. + +See [full documentation of `JSON_HAS_RANGE_VIEW_CONVERSION`](../api/macros/json_has_range_view_conversion.md). + +## `JSON_HAS_STATIC_RTTI` + +The library detects whether the compiler supports run time type information (RTTI), which it needs, for instance, to +exclude `std::any` from the candidate types of the implicit conversion on C++17. To override the built-in check, define +`JSON_HAS_STATIC_RTTI` to `1` or `0`. + +See [full documentation of `JSON_HAS_STATIC_RTTI`](../api/macros/json_has_static_rtti.md). + +## `JSON_HAS_STD_FORMAT` + +When compiling with C++20 and a standard library that provides ``, the library provides a `std::formatter` +specialization for JSON values. To override the built-in check, define `JSON_HAS_STD_FORMAT` to `1` or `0`. + +See [full documentation of `JSON_HAS_STD_FORMAT`](../api/macros/json_has_std_format.md). + +## `JSON_HAS_THREE_WAY_COMPARISON` + +When the compiler and standard library support 3-way comparison (the spaceship operator `<=>`), the library provides it +for JSON values. To override the built-in check, define `JSON_HAS_THREE_WAY_COMPARISON` to `1` or `0`. + +See [full documentation of `JSON_HAS_THREE_WAY_COMPARISON`](../api/macros/json_has_three_way_comparison.md). + ## `JSON_NOEXCEPTION` Exceptions can be switched off by defining the symbol `JSON_NOEXCEPTION`. @@ -94,6 +133,14 @@ When defined, default parse and serialize functions for enums are excluded and h See [full documentation of `JSON_DISABLE_ENUM_SERIALIZATION`](../api/macros/json_disable_enum_serialization.md). +## `JSON_DISABLE_STREAM_BULK_SCAN` + +When defined to `1`, parsing from a `#!cpp std::istream` reads one character at a time instead of scanning the +characters that the stream buffer holds in bulk. Results are the same either way; the macro is an escape hatch for +stream buffers that do not follow the usual `std::streambuf` contract. The default value is `0`. + +See [full documentation of `JSON_DISABLE_STREAM_BULK_SCAN`](../api/macros/json_disable_stream_bulk_scan.md). + ## `JSON_DISABLE_TUPLE_REFERENCE_CONVERSION` When defined to `1`, a JSON value can no longer be created from a one-element `std::tuple` holding a reference to a JSON @@ -102,6 +149,14 @@ is planned to become the default in version 4.0.0. See [full documentation of `JSON_DISABLE_TUPLE_REFERENCE_CONVERSION`](../api/macros/json_disable_tuple_reference_conversion.md). +## `JSON_DISABLE_UNLOCKED_FILE_IO` + +When defined to `1`, parsing from a `#!cpp FILE*` reads every character with `std::fgetc`, which locks the file each +time, instead of locking the file once and reading it with the unlocked stdio functions. Results are the same either +way. The default value is `0`. + +See [full documentation of `JSON_DISABLE_UNLOCKED_FILE_IO`](../api/macros/json_disable_unlocked_file_io.md). + ## `JSON_NO_AUTOMATIC_UDLS` When defined, `` does not include `` with the user-defined string literals @@ -119,6 +174,13 @@ security reasons (e.g., Intel Software Guard Extensions (SGX)). See [full documentation of `JSON_NO_IO`](../api/macros/json_no_io.md). +## `JSON_NO_SIMD` + +When defined, the library uses only portable code instead of vector instructions (SSE2, SSSE3, NEON) for scanning +strings and writing floating-point numbers. Results are the same either way. It is not defined by default. + +See [full documentation of `JSON_NO_SIMD`](../api/macros/json_no_simd.md). + ## `JSON_NO_THREAD_LOCAL` When defined, the library does not use `#!cpp thread_local` storage. Copying a value and comparing two values then @@ -204,9 +266,9 @@ See [full documentation of `JSON_USE_GLOBAL_UDLS`](../api/macros/json_use_global ## `JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON` -When defined to `1`, the library restores the legacy behavior in which a discarded value compared equal to itself. This -behavior is [deprecated](../integration/migration_guide.md#miscellaneous-functions) and switched off (`0`) by -default. +When defined to `1`, the library restores the legacy behavior in which comparing a discarded value with `<=` or `>=` +yields `#!cpp true` instead of `#!cpp false`. This behavior is +[deprecated](../integration/migration_guide.md#miscellaneous-functions) and switched off (`0`) by default. See [full documentation of `JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON`](../api/macros/json_use_legacy_discarded_value_comparison.md). @@ -225,6 +287,23 @@ external dependency and is not defined by default. See [full documentation of `JSON_USE_SIMDUTF`](../api/macros/json_use_simdutf.md). +## `JSON_VIEW_NO_SIMD` + +When defined, the parser of [`basic_json_document`](../api/basic_json_document/index.md) +(``) uses only portable C++ to scan strings, instead of vector instructions (SSE2 on x86-64, +NEON on AArch64). The same input is accepted or rejected either way; only the speed differs. It is not defined by +default. + +See [full documentation of `JSON_VIEW_NO_SIMD`](../api/macros/json_view_no_simd.md). + +## `JSON_VIEW_USE_SSSE3` + +When defined on x86-64, the parser of [`basic_json_document`](../api/basic_json_document/index.md) +(``) validates non-ASCII text in strings with SSSE3 without asking the CPU first. It is not +defined by default. + +See [full documentation of `JSON_VIEW_USE_SSSE3`](../api/macros/json_view_use_ssse3.md). + ## `NLOHMANN_DEFINE_TYPE_*(...)`, `NLOHMANN_DEFINE_DERIVED_TYPE_*(...)` The library defines 12 macros to simplify the serialization/deserialization of types. See the page on diff --git a/docs/mkdocs/docs/features/merge_patch.md b/docs/mkdocs/docs/features/merge_patch.md index 46b0c5f04..ac732c7bf 100644 --- a/docs/mkdocs/docs/features/merge_patch.md +++ b/docs/mkdocs/docs/features/merge_patch.md @@ -1,6 +1,6 @@ # JSON Merge Patch -The library supports JSON Merge Patch ([RFC 7386](https://tools.ietf.org/html/rfc7386)) as a patch format. +The library supports JSON Merge Patch ([RFC 7396](https://tools.ietf.org/html/rfc7396)) as a patch format. The merge patch format is primarily intended for use with the HTTP PATCH method as a means of describing a set of modifications to a target resource's content. This function applies a merge patch to the current JSON value. @@ -9,6 +9,16 @@ syntax that closely mimics the document being modified. Unlike [JSON Patch](json express every kind of change (e.g., it cannot reorder array elements or remove a specific array element), but it is easier to read and write for object-shaped documents. +The function [`merge_diff`](../api/basic_json/merge_diff.md) creates a merge patch that changes one JSON value into +another. + +!!! tip "Not a general deep merge" + + A merge patch is not a general deep merge: a `#!json null` value in the patch deletes the key from the target. + To merge two objects recursively (e.g., defaults and user settings), use + [`update`](../api/basic_json/update.md) with `merge_objects` set to `#!cpp true`; see + [Merging objects](modifying_values.md#merging-objects). + ??? example The following code shows how a JSON Merge Patch is applied to a JSON document. @@ -28,3 +38,5 @@ easier to read and write for object-shaped documents. - [JSON Patch and Diff](json_patch.md) - a more expressive alternative that describes a sequence of operations - [JSON Pointer](json_pointer.md) - the addressing scheme used by JSON Patch - Function [`merge_patch`](../api/basic_json/merge_patch.md) +- Function [`merge_diff`](../api/basic_json/merge_diff.md) +- Function [`update`](../api/basic_json/update.md) - merge objects, optionally recursively diff --git a/docs/mkdocs/docs/features/modifying_values.md b/docs/mkdocs/docs/features/modifying_values.md index 23c0feb53..1d6f2e27d 100644 --- a/docs/mkdocs/docs/features/modifying_values.md +++ b/docs/mkdocs/docs/features/modifying_values.md @@ -35,8 +35,12 @@ the insertion happened — useful for "add if absent" semantics. ## Merging objects -To merge one object into another, [`update`](../api/basic_json/update.md) copies all members from another object, -overwriting existing keys (similar to Python's `dict.update`). This is the idiomatic way to combine two objects. +To merge one object into another, [`update`](../api/basic_json/update.md) copies all members from another object +(similar to Python's `dict.update`). This is the idiomatic way to combine two objects. It has two modes: + +- By default, the merge is shallow: existing keys are overwritten, even if both values are objects. +- With `merge_objects = #!cpp true`, keys whose values are objects in both JSON values are merged recursively. + Everything else is overwritten. In particular, arrays are replaced, not concatenated. ??? example @@ -50,9 +54,22 @@ overwriting existing keys (similar to Python's `dict.update`). This is the idiom --8<-- "examples/update.output" ``` -For a recursive merge that follows [RFC 7386](https://tools.ietf.org/html/rfc7386), see -[JSON Merge Patch](merge_patch.md). To apply a sequence of well-defined edit operations, see -[JSON Patch](json_patch.md). +A common use of the recursive mode is combining defaults with user settings. Nested defaults that the user did not set +are kept: + +```cpp +json defaults = {{"log", {{"level", "info"}, {"file", "app.log"}}}, {"retries", 3}}; +json user_settings = {{"log", {{"level", "debug"}}}}; + +json config = defaults; +config.update(user_settings, true); +// {"log":{"file":"app.log","level":"debug"},"retries":3} +``` + +[JSON Merge Patch](merge_patch.md) ([RFC 7396](https://tools.ietf.org/html/rfc7396)) also merges objects recursively, +but it is a different tool: a `#!json null` in the patch means "remove this key". It is meant for applying merge patch +documents (e.g., received via HTTP PATCH). To merge configuration-like objects, use `#!cpp update(..., true)`. To apply +a sequence of well-defined edit operations, see [JSON Patch](json_patch.md). ## Removing elements @@ -68,10 +85,24 @@ json a = {1, 2, 3, 4}; a.erase(1); // [1,3,4] (erase by index) ``` +## Editing a document + +The operations above work on a `basic_json` value. A JSON text that was parsed into a +[document](json_view.md#editing-a-document) can also be changed in place, without building a `basic_json` tree for the +parts that are not touched: [`set`](../api/basic_json_document/set.md) replaces a value, adds or replaces an object +member, or replaces an array element (also through a JSON pointer), +[`push_back`](../api/basic_json_document/push_back.md) appends to an array, +[`insert`](../api/basic_json_document/insert.md) inserts into an array (unlike `basic_json::insert`, it has no object +overload; members are added with `set`), and [`erase`](../api/basic_json_document/erase.md) removes a member or an +element. These functions are available on the +editable document types, such as [`json_editable_document`](../api/json_editable_document.md). + ## See also - [`push_back`](../api/basic_json/push_back.md) / [`emplace_back`](../api/basic_json/emplace_back.md) - append to an array - [`emplace`](../api/basic_json/emplace.md) - insert into an object if the key is absent -- [`update`](../api/basic_json/update.md) - merge objects +- [`update`](../api/basic_json/update.md) - merge objects (shallow, or recursive with `merge_objects`) +- [`merge_patch`](../api/basic_json/merge_patch.md) - apply an RFC 7396 merge patch - [`erase`](../api/basic_json/erase.md) / [`clear`](../api/basic_json/clear.md) - remove elements - [JSON Patch and Diff](json_patch.md) and [JSON Merge Patch](merge_patch.md) - structured modifications +- [Editing a document](json_view.md#editing-a-document) - change a parsed document in place diff --git a/docs/mkdocs/docs/features/modules.md b/docs/mkdocs/docs/features/modules.md index 32e9ba7fd..b87cb8950 100644 --- a/docs/mkdocs/docs/features/modules.md +++ b/docs/mkdocs/docs/features/modules.md @@ -15,7 +15,7 @@ When using modules rather than headers, the previous example for creating a `jso import std; import nlohmann.json; -using json = nlohmann::json; +using nlohmann::json; // ... @@ -32,9 +32,15 @@ Only the following symbols are exported from `nlohmann.json`: - `nlohmann::adl_serializer` - `nlohmann::basic_json` +- `nlohmann::basic_json_document` +- `nlohmann::basic_json_view` - `nlohmann::json` +- `nlohmann::json_document` - `nlohmann::json_pointer` +- `nlohmann::json_view` - `nlohmann::ordered_json` +- `nlohmann::ordered_json_document` +- `nlohmann::ordered_json_view` - `nlohmann::ordered_map` - `nlohmann::to_string` - `nlohmann::literals::json_literals::operator""_json` diff --git a/docs/mkdocs/docs/features/namespace.md b/docs/mkdocs/docs/features/namespace.md index 26e89d09c..39cd58762 100644 --- a/docs/mkdocs/docs/features/namespace.md +++ b/docs/mkdocs/docs/features/namespace.md @@ -23,6 +23,13 @@ The complete default namespace name is derived as follows: - [`JSON_STRICT_BINARY_UTF8`](../api/macros/json_strict_binary_utf8.md) defined non-zero appends `_sbu8`. - [`JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS`](../api/macros/json_use_objects_for_enum_keyed_maps.md) defined non-zero appends `_ekmo`. + - [`JSON_DISABLE_STREAM_BULK_SCAN`](../api/macros/json_disable_stream_bulk_scan.md) defined non-zero appends + `_dsbs`. + - [`JSON_DISABLE_UNLOCKED_FILE_IO`](../api/macros/json_disable_unlocked_file_io.md) defined non-zero appends + `_dufio`. + - [`JSON_NO_SIMD`](../api/macros/json_no_simd.md) defined appends `_ns`. + - [`JSON_VIEW_NO_SIMD`](../api/macros/json_view_no_simd.md) defined appends `_vns`. + - [`JSON_VIEW_USE_SSSE3`](../api/macros/json_view_use_ssse3.md) defined appends `_vssse3`. - The inline namespace ends with the suffix `_v` followed by the 3 components of the version number separated by underscores. To omit the version component, see [Disabling the version component](#disabling-the-version-component) below. @@ -101,3 +108,14 @@ follows: - Introduced inline namespace (`json_v3_11_0[_abi-tag]*`) in version 3.11.0. - Changed structure of inline namespace in version 3.11.2. +- Added ABI tag `_dp` ([`JSON_DIAGNOSTIC_POSITIONS`](../api/macros/json_diagnostic_positions.md)) in version 3.12.0. +- Added ABI tags `_bics` ([`JSON_BRACE_INIT_COPY_SEMANTICS`](../api/macros/json_brace_init_copy_semantics.md)), `_psp` + ([`JSON_PRECISE_STREAM_POSITION`](../api/macros/json_precise_stream_position.md)), `_snul` + ([`JSON_STRICT_NUL_HANDLING`](../api/macros/json_strict_nul_handling.md)), `_sbu8` + ([`JSON_STRICT_BINARY_UTF8`](../api/macros/json_strict_binary_utf8.md)), `_ekmo` + ([`JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS`](../api/macros/json_use_objects_for_enum_keyed_maps.md)), `_dsbs` + ([`JSON_DISABLE_STREAM_BULK_SCAN`](../api/macros/json_disable_stream_bulk_scan.md)), `_dufio` + ([`JSON_DISABLE_UNLOCKED_FILE_IO`](../api/macros/json_disable_unlocked_file_io.md)), `_ns` + ([`JSON_NO_SIMD`](../api/macros/json_no_simd.md)), `_vns` + ([`JSON_VIEW_NO_SIMD`](../api/macros/json_view_no_simd.md)), and `_vssse3` + ([`JSON_VIEW_USE_SSSE3`](../api/macros/json_view_use_ssse3.md)) in version 3.13.0. diff --git a/docs/mkdocs/docs/features/no_exceptions.md b/docs/mkdocs/docs/features/no_exceptions.md new file mode 100644 index 000000000..cfb489876 --- /dev/null +++ b/docs/mkdocs/docs/features/no_exceptions.md @@ -0,0 +1,113 @@ +# Working Without Exceptions + +By default, the library reports errors by throwing exceptions (see [Exceptions](../home/exceptions.md)). This page +collects what to use when exceptions are unwanted or unavailable, for example in embedded systems, game engines, or +code compiled with `-fno-exceptions`. + +## Two different questions + +1. **Can my code avoid *catching* exceptions?** Yes. For parsing and for accessing values there are functions that + report failure through a return value. They work in a normal build and need no `#!cpp try`/`#!cpp catch`. +2. **Can the library be *compiled* without exceptions?** Yes, with `-fno-exceptions` or by defining + [`JSON_NOEXCEPTION`](../api/macros/json_noexception.md). Both have the same effect: every `#!cpp throw` in the + library becomes a call to `std::abort()`. The functions listed below never reach a `#!cpp throw` for the failures + they report, so they keep working in this configuration. Anything else that fails aborts the program. + +## Parsing + +| function | on invalid input | +|---------------------------------------------------------------------------------------------|-----------------------------------------------------| +| `#!cpp json::parse(input, nullptr, false)` | returns a *discarded* value; test with `is_discarded()` | +| [`accept`](../api/basic_json/accept.md) | returns `#!cpp false` | +| `#!cpp json::from_cbor(input, true, false)` (and the other `from_*` functions) | returns a *discarded* value | +| [`sax_parse`](../api/basic_json/sax_parse.md) with your own handler | returns `#!cpp false` after calling `parse_error` | + +??? example "Example: parse and validate without exceptions" + + ```cpp + --8<-- "examples/no_exceptions_parse.cpp" + ``` + + Output: + + ``` + --8<-- "examples/no_exceptions_parse.output" + ``` + +None of the calls in this example throws, so it also compiles and runs with `-fno-exceptions`. + +The discarded value carries no message. If you need the position or the reason, use the SAX interface; see +[Parsing and exceptions](parsing/parse_exceptions.md). + +## Accessing values + +The access functions are not equally safe. Only some of them never throw: + +| expression | if the key is missing or the type is wrong | +|-------------------------------------|-------------------------------------------------------------------------------| +| `#!cpp j.contains("k")` | returns `#!cpp false` for a missing key; never throws | +| `#!cpp j.find("k")` | returns `end()` for a missing key; never throws | +| `#!cpp j.is_string()`, ... | the `is_*` functions test the type; never throw | +| `#!cpp j.get_ptr()` | returns `#!cpp nullptr` for a different type; never throws | +| `#!cpp j.at("k")` | throws `out_of_range.403` for a missing key | +| `#!cpp j.get()` | throws `type_error.302` for a value that is not a number | +| `#!cpp j.value("k", 0)` | returns the default for a missing key, but **throws `type_error.302` if `k` exists with another type** | +| `#!cpp j["k"]` (non-const) | inserts `null` for a missing key | +| `#!cpp j["k"]` (const) | undefined behavior for a missing key | + +So reading a value that may be absent or of the wrong type without exceptions takes two steps: look up, then test the +type. + +??? example "Example: look up a key and test its type" + + ```cpp + --8<-- "examples/no_exceptions_access.cpp" + ``` + + Output: + + ``` + --8<-- "examples/no_exceptions_access.output" + ``` + +The last lines of the example show why `value` is not on the safe list: it returns the default only for a *missing* +key. + +For numbers, test with `is_number()` (or `is_number_integer()`, `is_number_unsigned()`, `is_number_float()`) before +calling `get()`, and see [number handling](types/number_handling.md) for range and precision. Whether a value +fits into the target type is not checked by `get`. + +## Converting to your own types + +`from_json` functions (including the ones generated by the +[`NLOHMANN_DEFINE_TYPE_*` macros](arbitrary_types.md#simplify-your-life-with-macros)) report problems by throwing. +There is no variant that returns an error code. Without exceptions you have two options: + +- Validate the JSON value yourself, as shown above, and fill your struct field by field. +- Convert in a function that is compiled **with** exceptions enabled and catches them at the boundary, returning + `std::optional` or an error code to the rest of your code: + +??? example "Example: convert to a struct and report failure as std::optional" + + ```cpp + --8<-- "examples/no_exceptions_convert.c++17.cpp" + ``` + + Output: + + ``` + --8<-- "examples/no_exceptions_convert.c++17.output" + ``` + +## Overriding what happens instead of an exception + +With exceptions off, you can replace the `abort()` by your own code, for example to log the message first. See +[Switch off exceptions](../home/exceptions.md#switch-off-exceptions) and +[`JSON_THROW_USER`](../api/macros/json_throw_user.md). The handler must not return normally. + +## See also + +- [Exceptions](../home/exceptions.md) - all exceptions and their ids +- [Parsing and exceptions](parsing/parse_exceptions.md) - the parsing alternatives in detail +- [Parsing untrusted input](parsing/untrusted_input.md) +- [`JSON_NOEXCEPTION`](../api/macros/json_noexception.md) diff --git a/docs/mkdocs/docs/features/object_order.md b/docs/mkdocs/docs/features/object_order.md index b90637711..b9997238e 100644 --- a/docs/mkdocs/docs/features/object_order.md +++ b/docs/mkdocs/docs/features/object_order.md @@ -12,7 +12,7 @@ The default type `nlohmann::json` uses a `std::map` to store JSON objects, and t #include #include - using json = nlohmann::json; + using nlohmann::json; int main() { diff --git a/docs/mkdocs/docs/features/parsing/index.md b/docs/mkdocs/docs/features/parsing/index.md index 9e5371d95..447b65ffc 100644 --- a/docs/mkdocs/docs/features/parsing/index.md +++ b/docs/mkdocs/docs/features/parsing/index.md @@ -18,6 +18,7 @@ flowchart LR The [`parse`](../../api/basic_json/parse.md) function reads a JSON value from an input. The input can be - a string (`#!cpp std::string`, C string, or string literal), +- a wide string (`#!cpp std::u16string`, `#!cpp std::u32string`, or `#!cpp std::wstring`, or a wide string literal), - a `#!cpp std::istream` (e.g., an `#!cpp std::ifstream` reading from a file), - a `#!cpp FILE*` pointer, - a pair of iterators over a contiguous range (e.g., a `#!cpp std::vector`), or @@ -32,12 +33,20 @@ std::ifstream f("example.json"); json data = json::parse(f); ``` -The input must be encoded in UTF-8; other encodings are not supported. A single input may contain only one JSON value. -Inputs consisting of multiple values separated by newlines are handled by the [JSON Lines](json_lines.md) format. +The input must be encoded in UTF-8; other encodings are not supported. The exception is wide-character input, which +is read as UTF-16 or UTF-32 -- depending on the character type -- and converted to UTF-8; see the +[`parse`](../../api/basic_json/parse.md) page for an example. A single input may contain only one JSON value. +Inputs consisting of multiple values separated by newlines are handled by the [JSON Lines](json_lines.md) format; input +that arrives in chunks or contains several values is covered in +[Large, streamed, and partial input](large_and_partial_input.md). By default, the library rejects comments and trailing commas. Both can be enabled with parameters of the `parse` function — see [comments](../comments.md) and [trailing commas](../trailing_commas.md). +[`basic_json_document::parse`](../../api/basic_json_document/parse.md) accepts the same inputs with the same options, +but keeps the result in a document rather than in a `basic_json` value; see +[Documents and views](#documents-and-views) below. + ## Strictness and trailing data [`parse`](../../api/basic_json/parse.md) reads a single JSON value and requires the whole input to be consumed: any @@ -55,9 +64,37 @@ whitespace, because `operator>>` consumes the character that terminates a number [`JSON_PRECISE_STREAM_POSITION`](../../api/macros/json_precise_stream_position.md) is defined to `1` — see the [`operator>>` notes](../../api/operator_gtgt.md#notes) for details and examples. +## Input sources and performance + +All inputs produce the same values and the same exceptions, but they are read in different ways, which makes some of +them faster than others: + +| input | how it is read | +|--------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------| +| strings, string views, string literals, contiguous containers, pointer or iterator ranges over contiguous memory | the whole input is scanned in bulk: runs of string characters, numbers, and indentation are recognized several bytes at a time | +| `#!cpp std::istream` (`#!cpp std::ifstream`, `#!cpp std::istringstream`, ...) | the characters the stream buffer holds are scanned in bulk like a string; the stream buffer is asked for more when they are used up | +| `#!cpp FILE*` | one character at a time, with the file locked once for the whole parse | +| other iterator ranges (e.g., `#!cpp std::list`, `#!cpp std::istreambuf_iterator`), wide strings | one character at a time | + +Parsing a file from a `#!cpp std::ifstream` is therefore only about 10 to 20 % slower than reading the whole file into a +`#!cpp std::string` first and parsing that, and it needs no memory for a copy of the file. The stream is never read +ahead: after parsing, it is positioned exactly as if every character had been read one at a time, so +[`operator>>`](../../api/operator_gtgt.md) can still read several values from one stream (see also +[JSON Lines](json_lines.md)). + +Two macros switch these optimizations off without changing any result: +[`JSON_DISABLE_STREAM_BULK_SCAN`](../../api/macros/json_disable_stream_bulk_scan.md) for stream buffers that do not +follow the usual `std::streambuf` contract, and +[`JSON_DISABLE_UNLOCKED_FILE_IO`](../../api/macros/json_disable_unlocked_file_io.md) for programs in which another +thread must use the same `#!cpp FILE` object while it is parsed. + +Where the target has them, strings are scanned with vector instructions: SSE2 on x86-64 (and SSSE3 for UTF-8 text, when +the CPU has it) and NEON on AArch64. [`JSON_NO_SIMD`](../../api/macros/json_no_simd.md) selects portable code instead, +again without changing any result. + ## SAX vs. DOM parsing -The library offers two parsing models: +The library offers two parsing models, and a third, read-only alternative: - **DOM parsing** (the default): the complete input is read and stored as an in-memory `basic_json` value that can be traversed and modified freely. This is what [`parse`](../../api/basic_json/parse.md) does, and it is the right choice @@ -67,9 +104,36 @@ The library offers two parsing models: inputs or when you only need to extract parts of the input. See the [SAX interface](sax_interface.md) for details and [`sax_parse`](../../api/basic_json/sax_parse.md) for the API. +- **Document and views** (`#!cpp `, added in version 3.13.0): [`json_document::parse`](../../api/basic_json_document/parse.md) + reads the same JSON grammar but builds a flat, read-only index of the values instead of a `basic_json` tree. Strings and + floating-point numbers are read from the text on demand, and only the parts you + [`materialize()`](../../api/basic_json_view/materialize.md) become `basic_json` values. This avoids most of the work when you + need only a few values from a large or frequently parsed text; see [Zero-copy JSON views](../json_view.md). It is read-only, so use + `parse` when you need to modify the value. + You can influence a DOM parse without switching to the SAX interface by passing a [parser callback](parser_callbacks.md), which is called during parsing and can, for example, discard parts of the input. +## Documents and views + +A third model sits between DOM and SAX: [`basic_json_document::parse`](../../api/basic_json_document/parse.md) parses +the input into a document that keeps the text and a flat index of the values in it, and +[`basic_json_view`](../../api/basic_json_view/index.md) gives read-only access to the values through that index. +The whole input is validated during parsing, but only integers and strings that contain escape sequences are converted +right away (the strings into a buffer owned by the document). Floating-point numbers and all other strings stay in the +source text and are only converted when they are read, which is faster and uses less memory than building a tree of `basic_json` values when only parts +of the document are needed. A document can also be +modified in place, and it can be saved and loaded again without parsing it. + +```cpp +#include + +json_document doc = json_document::parse(R"({"happy": true, "pi": 3.141})"); +std::cout << doc.root()["happy"].get() << '\n'; +``` + +See [Zero-copy JSON views](../json_view.md) for the concept and the trade-offs. + ## Exceptions When the input is not valid JSON, the `parse` function throws an exception by default. If exceptions are undesired or @@ -80,10 +144,14 @@ options. ## See also - [`parse`](../../api/basic_json/parse.md) - deserialize from a compatible input +- [`basic_json_document::parse`](../../api/basic_json_document/parse.md) - deserialize into a document instead of a value - [`accept`](../../api/basic_json/accept.md) - check if the input is valid JSON - [`sax_parse`](../../api/basic_json/sax_parse.md) - generate SAX events - [JSON Lines](json_lines.md) - parse newline-delimited JSON +- [Large, streamed, and partial input](large_and_partial_input.md) - chunked, concatenated, and very large input - [parser callbacks](parser_callbacks.md) - influence the parsing by a callback function - [SAX interface](sax_interface.md) - implement a custom SAX handler +- [Zero-copy JSON views](../json_view.md) - parse into a read-only document and read it through views - [parsing and exceptions](parse_exceptions.md) - control error handling - [parsing untrusted input](untrusted_input.md) - what to consider when parsing input from untrusted sources +- [Zero-copy JSON views](../json_view.md) - parse into a document and read it through a view diff --git a/docs/mkdocs/docs/features/parsing/large_and_partial_input.md b/docs/mkdocs/docs/features/parsing/large_and_partial_input.md new file mode 100644 index 000000000..d24b5fab1 --- /dev/null +++ b/docs/mkdocs/docs/features/parsing/large_and_partial_input.md @@ -0,0 +1,154 @@ +# Large, Streamed, and Partial Input + +[`parse`](../../api/basic_json/parse.md) reads exactly one complete JSON value and builds it in memory. It cannot be +paused and resumed, and it does not hand back a half-parsed value when the input ends early. This page shows what to do +when the input does not fit that model: it arrives in pieces, it contains several values, or it is too large to hold +as a `json` value. + +```mermaid +flowchart TD + A["my input is..."] --> B{"several JSON values in one stream?"} + B -->|"yes"| C["read them one at a time"] + A --> D{"arriving in chunks (socket, pipe)?"} + D -->|"yes"| E["frame the messages, or buffer until complete"] + A --> F{"too large to keep in memory?"} + F -->|"yes"| G["discard parts with a callback, or use SAX"] + A --> H{"only a small part is needed?"} + H -->|"yes"| I["stop early with SAX, or read through a view"] +``` + +## Several values in one stream + +For newline-delimited input, read line by line and parse each line, as described in [JSON Lines](json_lines.md). For +values that are simply concatenated, [`operator>>`](../../api/operator_gtgt.md) reads one value and leaves the stream +positioned after it. Check for the end of the stream before each read, because a read at the end throws: + +??? example "Example: read concatenated values" + + ```cpp + --8<-- "examples/partial_input_concatenated.cpp" + ``` + + Output: + + ``` + --8<-- "examples/partial_input_concatenated.output" + ``` + +The same works for binary formats. [`from_cbor`](../../api/basic_json/from_cbor.md) and its relatives accept +`#!cpp strict = false` to stop after the first value and leave the rest of the stream untouched; `tellg()` then reports +how many bytes the value took: + +??? example "Example: read one CBOR value and find out how many bytes it used" + + ```cpp + --8<-- "examples/partial_input_cbor.cpp" + ``` + + Output: + + ``` + --8<-- "examples/partial_input_cbor.output" + ``` + +## Input arriving in chunks + +A socket, pipe, or HTTP body hands over data in pieces of arbitrary size, and a piece can end in the middle of a +number, a string, or an object. The parser does not keep state between calls, so there are two ways to deal with this. + +**Frame the messages.** The most robust approach is a protocol that says where a value ends: one value per line +([JSON Lines](json_lines.md)), a length prefix, or a delimiter. Collect the bytes of one message and parse them when +the frame is complete. Prefer this whenever you control the protocol. + +**Buffer until the value is complete.** If the data has no framing, append every chunk to a buffer and try to parse +it. A parse error at the very end of the buffer means that the input ended prematurely, while an error anywhere else is +a real syntax error. The member `byte` of the [`parse_error`](../../home/exceptions.md#jsonexceptionparse_error101) +exception is one past the buffer size in the first case: + +??? example "Example: buffer chunks until the value is complete" + + ```cpp + --8<-- "examples/partial_input_buffering.cpp" + ``` + + Output: + + ``` + --8<-- "examples/partial_input_buffering.output" + ``` + +The example feeds a document in five chunks. The first four are reported as incomplete, and a syntax error is reported as +such, not as missing data. + +!!! warning "Limits of buffering" + + - The buffer must contain exactly one value. Data that belongs to the next value is a syntax error. Use framing if + values follow each other without a separator. + - A number at the end of the buffer is a complete value, even if more digits are still to come (`12` may be the + start of `123`). Buffering cannot detect this; use framing for top-level numbers. + - Every attempt parses the buffer from the beginning, so the total work grows quadratically with the size of the + value. Limit the size of the buffer for input you do not trust (see [untrusted input](untrusted_input.md)). + +## Input too large for memory + +The input does not have to become one huge `json` value. The two tools below process it piece by piece. + +### Handle and discard elements with a callback + +A [parser callback](parser_callbacks.md) is called for every finished value. If it returns `#!cpp false`, the value is +discarded and its memory is released at once. For a file that consists of one huge array (or one huge object) of +records, handle each record at depth 1 and discard it: + +??? example "Example: handle and discard each record of a large array" + + ```cpp + --8<-- "examples/partial_input_discard_records.cpp" + ``` + + Output: + + ``` + --8<-- "examples/partial_input_discard_records.output" + ``` + +Only one record is in memory at any time. The same recipe, with more detail, is in the +[parser callbacks](parser_callbacks.md#recipe-streaming-a-large-homogeneous-array) page. To skip entire members you do not +need, discard them when their key is read; see the [example](parser_callbacks.md) for `parse_event_t::key`. + +### Extract a few values and stop with SAX + +With the [SAX interface](sax_interface.md), no value is built at all, and **returning `#!cpp false` from any handler +function stops the parser**. [`sax_parse`](../../api/basic_json/sax_parse.md) then returns `#!cpp false`, and the rest +of the input is not read. The example finds the first `id` in a document without parsing the remainder: + +??? example "Example: stop parsing as soon as a value is found" + + ```cpp + --8<-- "examples/partial_input_sax_stop.cpp" + ``` + + Output: + + ``` + --8<-- "examples/partial_input_sax_stop.output" + ``` + +Note that `sax_parse` returns `#!cpp false` both when a handler stopped the parse and when the input is invalid, so +record the reason in the handler (here: `id`) if you need to tell the two apart. + +### Read parts of a large document with a view + +If the whole text is available and you only need to read some of it, a [`json_document`](../json_view.md) from +`` (added in version 3.13.0) avoids the `basic_json` tree altogether: it keeps the text, builds a flat index of the values, +and gives read-only [views](../../api/basic_json_view/index.md) of the parts you look at. Only what you +[`materialize()`](../../api/basic_json_view/materialize.md) becomes a `basic_json` value. See +[Zero-copy JSON views](../json_view.md) for details and for the lifetime rules. + +## See also + +- [Parsing](index.md) - overview of the parsing functions +- [JSON Lines](json_lines.md) - newline-delimited JSON +- [Parser callbacks](parser_callbacks.md) - discard values while parsing +- [SAX interface](sax_interface.md) - event-based parsing +- [Zero-copy JSON views](../json_view.md) - read parts of a large document without building a tree +- [Untrusted input](untrusted_input.md) - limits for input you do not control diff --git a/docs/mkdocs/docs/features/parsing/parse_exceptions.md b/docs/mkdocs/docs/features/parsing/parse_exceptions.md index f524b67d8..eb0ae5f58 100644 --- a/docs/mkdocs/docs/features/parsing/parse_exceptions.md +++ b/docs/mkdocs/docs/features/parsing/parse_exceptions.md @@ -21,6 +21,10 @@ catch (json::parse_error& ex) In case exceptions are undesired or not supported by the environment, there are different ways to proceed: +!!! tip + + [Working without exceptions](../no_exceptions.md) also covers accessing values and converting to your own types. + ## Switch off exceptions The [`parse()`](../../api/basic_json/parse.md) function accepts a `#!cpp bool` parameter `allow_exceptions` which @@ -81,3 +85,61 @@ The return value indicates whether the parsing should continue, so the function ``` --8<-- "examples/sax_no_exception.output" ``` + +## Where did the error occur? + +The message of a [`parse_error`](../../home/exceptions.md#jsonexceptionparse_error101) for JSON text contains the line +and column of the error: + +``` +[json.exception.parse_error.101] parse error at line 3, column 8: syntax error while parsing value - unexpected ']'; expected '[', '{', or a literal +``` + +The exception object itself only stores the **byte index** of the last read character in the member `byte` (the first +byte of the input has index 1). The same index is passed to `parse_error` in the [SAX interface](sax_interface.md). If +you need the line and column as numbers, for example to show them in an editor, compute them from the input: + +??? example "Example: compute line and column of a parse error" + + ```cpp + --8<-- "examples/parse_error_position.cpp" + ``` + + Output: + + ``` + --8<-- "examples/parse_error_position.output" + ``` + +Columns count bytes, so a line with multi-byte UTF-8 characters before the error may differ from the column your editor +shows. + +### Where is the value that caused a later error? + +A `parse_error` is raised while the text is read. Many mistakes show up later: the text is valid JSON, but a value has +the wrong type when you convert it (`type_error.302`), or a key is missing (`out_of_range.403`). The exception then says +what is wrong, but not where in the input the value was. Define +[`JSON_DIAGNOSTIC_POSITIONS`](../../api/macros/json_diagnostic_positions.md) before including the library to add the +byte range of the offending value to such messages (since version 3.12.0), and to query the range of any value with +[`start_pos()`](../../api/basic_json/start_pos.md) and [`end_pos()`](../../api/basic_json/end_pos.md): + +??? example "Example: locate a value with diagnostic positions" + + ```cpp + --8<-- "examples/parse_error_position_diagnostic.cpp" + ``` + + Output: + + ``` + --8<-- "examples/parse_error_position_diagnostic.output" + ``` + +Positions are only available for values created by [`parse`](../../api/basic_json/parse.md), they are not updated when a +value is changed, and the macro makes every value 16 bytes larger on 64-bit platforms, so use it where the cost is +acceptable, for example for configuration files. Note that `start_pos()` is a 0-based index into the input, whereas +`byte` of a `parse_error` starts at 1. + +For binary formats such as CBOR, there are no lines; `byte` is the position in the byte sequence. To find out how many +bytes a successfully parsed value used, see +[Several values in one stream](large_and_partial_input.md#several-values-in-one-stream). diff --git a/docs/mkdocs/docs/features/parsing/untrusted_input.md b/docs/mkdocs/docs/features/parsing/untrusted_input.md index d39a4179e..65f1bda25 100644 --- a/docs/mkdocs/docs/features/parsing/untrusted_input.md +++ b/docs/mkdocs/docs/features/parsing/untrusted_input.md @@ -26,9 +26,11 @@ several alternatives; see [Parsing and exceptions](parse_exceptions.md) for the [example](parse_exceptions.md#user-defined-sax-interface) that overrides it to print instead of throw. If exceptions are unavailable entirely (`-fno-exceptions`, or [`JSON_NOEXCEPTION`](../../api/macros/json_noexception.md) -defined), every `#!cpp throw` in the library becomes a call to `std::abort()` -- there is no way to recover from a -parse error of untrusted input in that configuration; see -[Switch off exceptions](../../home/exceptions.md#switch-off-exceptions) for the details and for overriding this with +defined), every `#!cpp throw` in the library becomes a call to `std::abort()`. The alternatives above do not throw, so +`parse()` with `allow_exceptions` set to `#!cpp false` (and likewise `accept()` and the `from_*` functions with +`allow_exceptions` set to `#!cpp false`) can still reject invalid input in that configuration; a plain `parse()` call +aborts on invalid input. See [Working without exceptions](../no_exceptions.md) and +[Switch off exceptions](../../home/exceptions.md#switch-off-exceptions) for the details and for overriding the abort with `JSON_THROW_USER`. ## Nesting depth @@ -50,19 +52,13 @@ stack, however deeply it is nested. Once a value has been parsed, operations that walk it recursively -- serializing it with [`dump`](../../api/basic_json/dump.md), hashing it, copying it, comparing two values with `#!cpp ==`, `#!cpp <`, or (in -C++20) `#!cpp <=>`, merging with [`update`](../../api/basic_json/update.md), and applying a -[`merge_patch`](../../api/basic_json/merge_patch.md) -- descend at most 128 levels on the call stack and continue +C++20) `#!cpp <=>`, merging with [`update`](../../api/basic_json/update.md), applying a +[`merge_patch`](../../api/basic_json/merge_patch.md), creating a patch with [`diff`](../../api/basic_json/diff.md) or +[`merge_diff`](../../api/basic_json/merge_diff.md), [`flatten`](../../api/basic_json/flatten.md), and the binary +writers (`to_cbor`, `to_msgpack`, ...) -- descend at most 128 levels on the call stack and continue below that with an explicit stack instead, so none of them can exhaust the stack either, however deeply the value is nested. Destroying a value (its destructor) never recurses at all, regardless of nesting depth, for the same reason. -!!! note "Not every operation is bounded yet" - - [`diff`](../../api/basic_json/diff.md), [`flatten`](../../api/basic_json/flatten.md), and the binary writers - (`to_cbor`, `to_msgpack`, ...) still recurse once per nesting level; this is called out as work in progress in the - [assurance case](../../community/assurance_case.md#secure-design). A value deep enough to matter for these - operations would typically first have to survive parsing without hitting a self-imposed depth limit, as described - above. - ## Input size The library does not limit the overall size of a JSON text; a value nested or wide enough will use memory @@ -87,6 +83,9 @@ Invalid UTF-8 is rejected while parsing, not just while serializing: - In JSON text, an ill-formed UTF-8 byte in a string is a [`parse_error.101`](../../home/exceptions.md#jsonexceptionparse_error101) ("invalid string: ill-formed UTF-8 byte"). +- In UTF-16 or UTF-32 input, an ill-formed sequence is a + [`parse_error.101`](../../home/exceptions.md#jsonexceptionparse_error101) as well: a lone surrogate, a high surrogate + that is not followed by its low surrogate, and a code point above `U+10FFFF` are all rejected. - In a binary format, a string that is not valid UTF-8 is a [`parse_error.113`](../../home/exceptions.md#jsonexceptionparse_error113). diff --git a/docs/mkdocs/docs/features/performance.md b/docs/mkdocs/docs/features/performance.md index d6e9f70fd..7ad7b187e 100644 --- a/docs/mkdocs/docs/features/performance.md +++ b/docs/mkdocs/docs/features/performance.md @@ -20,32 +20,39 @@ below apply on every supported standard. This adapter exposes two optimizations - it can reconstruct already-consumed input on demand for error messages, instead of copying every character as it is read, and -- the lexer can scan ordinary string characters directly out of the buffer, several bytes at a time, rather than one - character (and one function call) at a time. +- the lexer can scan ordinary string characters, numbers, and indentation directly out of the buffer, several bytes at + a time, rather than one character (and one function call) at a time. -A `#!cpp std::istream` (including `#!cpp std::ifstream`) or `#!cpp FILE*`, by contrast, is read through -`input_stream_adapter` or `file_input_adapter`, which read one character (or one block, for binary formats) at a time -and expose neither optimization -- the lexer falls back to the same byte-at-a-time path it uses for any -non-contiguous, general-purpose iterator range. An iterator pair over non-contiguous but random-access storage (e.g. -`#!cpp std::deque::iterator`) gets the first optimization but not the second, since the byte-scanning fast path -additionally requires contiguous storage. +A `#!cpp std::istream` (including `#!cpp std::ifstream`) gets the same bulk scanning: `input_stream_adapter` scans the +characters its stream buffer already holds (the *get area*) and asks the stream buffer for more only when they are used +up. Nothing is read ahead, so the stream is left exactly where it would be after reading one character at a time, and +[`operator>>`](../api/operator_gtgt.md) can still read several values from one stream. A stream buffer without a get +area is read one character at a time. A `#!cpp FILE*` is still read one character at a time, but `file_input_adapter` +locks the file once per parse instead of once per character. See +[input sources and performance](parsing/index.md#input-sources-and-performance) for the full table. According to the +description of [#5828](https://github.com/nlohmann/json/pull/5828), parsing from a stream is within about 10 to 20 % of +parsing an equivalent `#!cpp std::string`. -Practically: if the JSON text is already in memory, or small enough to read into memory, prefer passing a -`#!cpp std::string`, a `#!cpp std::vector`, or a pointer range to `parse` over a `#!cpp std::istream`. For a -file, that means reading it into a string first and then parsing the string, rather than passing a -`#!cpp std::ifstream` directly to `parse` -- the latter never benefits from either optimization: +Inputs that are still read one character at a time: iterator ranges that are not contiguous (e.g. +`#!cpp std::list` or `#!cpp std::istreambuf_iterator`), wide strings, and user-defined adapters. An +iterator pair over non-contiguous but random-access storage (e.g. `#!cpp std::deque::iterator`) gets the first +optimization but not the second, since the byte-scanning fast path requires contiguous storage. + +Practically: a file can be passed directly to `parse`, which also avoids the memory for a copy of the file. Reading it +into a string first is no longer necessary for speed: ```cpp -// gets the contiguous fast paths +// fast: the stream buffer's get area is scanned in bulk std::ifstream f("example.json"); -std::string contents((std::istreambuf_iterator(f)), std::istreambuf_iterator()); -json j = json::parse(contents); - -// does not: input_stream_adapter has no fast path -std::ifstream f2("example.json"); -json j2 = json::parse(f2); +json j = json::parse(f); ``` +Two macros switch the stream optimizations off without changing any result: +[`JSON_DISABLE_STREAM_BULK_SCAN`](../api/macros/json_disable_stream_bulk_scan.md) for stream buffers that do not follow +the usual `#!cpp std::streambuf` contract, and +[`JSON_DISABLE_UNLOCKED_FILE_IO`](../api/macros/json_disable_unlocked_file_io.md) for programs in which another thread +must use the same `#!cpp FILE` object while it is parsed. + For contiguous input with many non-ASCII characters, [`JSON_USE_SIMDUTF`](../api/macros/json_use_simdutf.md) can additionally speed up UTF-8 validation by using the [simdutf](https://github.com/simdutf/simdutf) library instead of the built-in scalar validator; streaming inputs (files, `#!cpp std::istream`, wide strings, user-defined adapters) @@ -55,7 +62,7 @@ always use the scalar path regardless of this macro. Parsing always produces SAX events internally; [`parse`](../api/basic_json/parse.md) simply feeds them to a consumer that builds a complete `basic_json` value tree (a DOM) in memory. For documents too large to comfortably hold as -a DOM, two alternatives avoid building it: +a DOM, three alternatives avoid building it: - Implement the [SAX interface](parsing/sax_interface.md) directly and pass it to [`sax_parse`](../api/basic_json/sax_parse.md); only the parts of the input you choose to keep ever become @@ -64,6 +71,12 @@ a DOM, two alternatives avoid building it: discard finished elements as soon as they are handled, so memory usage stays bounded by one element (plus the unparsed remainder of the input) instead of the whole document -- see the [recipe for streaming a large homogeneous array](parsing/parser_callbacks.md#recipe-streaming-a-large-homogeneous-array). +- Parse into a [`json_document`](json_view.md) (`#!cpp `) instead of a `basic_json`. It keeps + the input text and builds a flat index of 16 bytes per value; strings and floating-point numbers are not copied, but + read from the text when needed. Read-only [views](../api/basic_json_view/index.md) give the familiar element access, and only the + parts you [`materialize()`](../api/basic_json_view/materialize.md) become `basic_json` values. A document that + borrows the text instead of owning a copy needs the text to outlive it; see + [choosing between `json`, the SAX interface, and `json_view`](json_view.md#choosing-between-json-ordered_json-the-sax-interface-and-json_view). If the data is naturally record-oriented, consider [JSON Lines](parsing/json_lines.md) instead of one large JSON document: reading and parsing it line by line with `#!cpp std::getline` means only one line's value is ever in memory @@ -109,7 +122,9 @@ an adapter, since their template argument order does not match what `basic_json` [linear in its size](../api/basic_json/basic_json.md#complexity) for the copy constructor but [constant](../api/basic_json/basic_json.md#complexity) for the move constructor. The same applies to assigning a large `#!cpp std::string`, `#!cpp std::vector`, or other container into a value: pass it as `#!cpp std::move(x)` - rather than `x` whenever `x` is no longer needed afterwards. + rather than `x` whenever `x` is no longer needed afterwards. Since version 3.13.0, converting an rvalue container + (`#!cpp json j = std::move(vec);`) moves the elements instead of copying them; the source is then left in a valid but + unspecified state. - **Access without copying.** [`get()`](../api/basic_json/get.md) returns a copy of the stored value converted to `T`. When a reference or pointer to the value already stored inside the `basic_json` is enough, [`get_ref()`](../api/basic_json/get_ref.md) and [`get_ptr()`](../api/basic_json/get_ptr.md) access it directly: @@ -138,6 +153,29 @@ an adapter, since their template argument order does not match what `basic_json` ["reserving array capacity"](element_access/unchecked_access.md#performance-reserving-array-capacity) for the one-line recipe. +## Memory + +- **A value is small, its content is on the heap.** A `basic_json` is 16 bytes on a 64-bit platform (a type tag and a + union). Strings, arrays, objects, and binary values are allocated on the heap and owned by the value. Destroying the + value frees everything it owns. +- **Copies are deep.** After `#!cpp json b = a;`, changing `b` does not change `a`, and both own their memory. Copying a + large value therefore doubles its memory; see [Avoiding copies](#avoiding-copies). `#!cpp auto x = j["key"];` is a + copy, `#!cpp auto& x = j["key"];` is not. +- **A moved-from value is `null`.** After `#!cpp json c = std::move(a);` the value `a` is `#!json null` and owns nothing. +- **Reading does not require a tree.** If you only read the data, parse it into a [`json_document`](json_view.md) + (`#!cpp `, added in version 3.13.0) instead of a `basic_json`. It stores 16 bytes per value, does not copy floating-point + numbers or strings without escape sequences out of the text, and creates no `basic_json` value except for the parts + you [`materialize()`](../api/basic_json_view/materialize.md). A document that borrows the text needs the text to + outlive it; see [Zero-copy JSON views](json_view.md). +- **Containers keep their capacity.** `clear()` and `erase()` on an array remove the elements, but the underlying + `#!cpp std::vector` keeps its allocated capacity, like `#!cpp std::vector::clear` does. To give the memory back, + assign a new value (`#!cpp j = json::array();`) or destroy the value. +- **Freed memory does not always go back to the operating system.** The library returns memory to the allocator + (`#!cpp std::allocator` by default, or the [one you configured](types/template_parameters.md)). Whether the process + size shrinks then depends on the allocator and the platform, as for any C++ program. A process that parses a large + document and drops it is not leaking if its resident size stays high; run it under a tool such as + [Valgrind](https://valgrind.org) or the sanitizers to tell a leak from retained memory. + ## Serialization [`dump()`](../api/basic_json/dump.md) with the default `#!cpp indent = -1` selects "the most compact representation" @@ -209,6 +247,7 @@ those headers are then never processed by the compiler at all. - [Architecture](../home/architecture.md) - how input adapters, the lexer, and the serializer fit together - [Parsing](parsing/index.md) - the available parsing functions and inputs - [SAX interface](parsing/sax_interface.md) - parse without building a DOM +- [Zero-copy JSON views](json_view.md) - parse into a flat index of the text and read it without building a DOM - [Binary formats](binary_formats/index.md) - compact alternatives to JSON text - [Object Order](object_order.md) - `json` vs. `ordered_json` and other `ObjectType` choices - [Template Parameter Requirements](types/template_parameters.md) - custom container and allocator types diff --git a/docs/mkdocs/docs/features/serialization.md b/docs/mkdocs/docs/features/serialization.md index 1190c9005..5e1aedac0 100644 --- a/docs/mkdocs/docs/features/serialization.md +++ b/docs/mkdocs/docs/features/serialization.md @@ -52,7 +52,7 @@ with `\uXXXX` sequences, so that the output contains only ASCII characters: ```cpp json j = "苹果"; j.dump(); // "苹果" -j.dump(-1, ' ', true); // "苹果" +j.dump(-1, ' ', true); // "\u82f9\u679c" ``` ## Handling invalid UTF-8 diff --git a/docs/mkdocs/docs/features/type_recipes.md b/docs/mkdocs/docs/features/type_recipes.md new file mode 100644 index 000000000..314425dc6 --- /dev/null +++ b/docs/mkdocs/docs/features/type_recipes.md @@ -0,0 +1,124 @@ +# Recipes for Common Types + +The library converts the standard containers, strings, numbers, tuples, `std::optional`, and `std::filesystem::path` +(the last two since version 3.12.0, C++17) out of the box, and your own types once you write conversion functions or use +the macros (see [Arbitrary type conversions](arbitrary_types.md)). It deliberately does **not** convert smart pointers, +`std::variant`, `std::chrono` types, or class hierarchies, because there is more than one reasonable JSON +representation for each of them. This page shows a representation for each that you can copy and adapt. + +All recipes use a specialization of [`adl_serializer`](../api/adl_serializer/index.md), which you may provide for types +you do not own, or free `to_json`/`from_json` functions in the namespace of the type. Place them where they are +visible before any code that converts the type; see +[Why isn't my `to_json` found?](arbitrary_types.md#why-isnt-my-to_json-or-from_json-found). + +## Smart pointers + +A `std::shared_ptr` or `std::unique_ptr` is written as the value it points to, or as `#!json null` for an empty +pointer, and read back by allocating a new `T`: + +??? example "Example: serialize shared_ptr and unique_ptr" + + ```cpp + --8<-- "examples/type_recipes_smart_pointers.c++14.cpp" + ``` + + Output: + + ``` + --8<-- "examples/type_recipes_smart_pointers.c++14.output" + ``` + +With this, containers of smart pointers, pointers to `const`, and members of these types in your own structs convert as +usual. A pointer to a base class cannot be filled this way, because `T` would have +to be created as a derived type; see the next section. + +Reading creates a new object. Two `shared_ptr`s that pointed to the same object are written twice and read back as two +separate objects. + +## Class hierarchies + +!!! tip "Derived classes whose type is known at compile time" + + If you only need to serialize the data members of a derived class, and the code always knows the concrete type, you do + not need a type tag. The [`NLOHMANN_DEFINE_DERIVED_TYPE_*` macros](arbitrary_types.md#simplify-your-life-with-macros) + (since version 3.12.0) add the members of the derived class to those of its base class. This section is for the case + where the concrete type is only known at run time, such as a `std::unique_ptr` that holds a circle or a + rectangle. + +JSON has no notion of types, so a value in a hierarchy needs a **type tag** that says which derived class to create. +Write the tag in the object, and select the class from it when reading: + +??? example "Example: serialize a class hierarchy with a type tag" + + ```cpp + --8<-- "examples/type_recipes_class_hierarchy.c++14.cpp" + ``` + + Output: + + ``` + --8<-- "examples/type_recipes_class_hierarchy.c++14.output" + ``` + +A `#!cpp std::vector>` converts to an array of tagged objects and back, creating a `circle` +and a `rectangle`. The same works for `std::shared_ptr`. Remember the virtual destructor: without it, deleting a +derived object through the base pointer is undefined behavior. + +## `std::variant` + +A variant has the same problem: the alternative must be recorded. If the alternatives are your own types, write a type +tag next to their members, exactly as for a hierarchy: + +??? example "Example: serialize a std::variant with a type tag" + + ```cpp + --8<-- "examples/type_recipes_variant.c++17.cpp" + ``` + + Output: + + ``` + --8<-- "examples/type_recipes_variant.c++17.output" + ``` + +In the example, the functions are found +because `geo` is the namespace of the template arguments of `#!cpp std::variant`; for a variant of only +standard types (for example `#!cpp std::variant`) there is no such namespace, and you need an +`adl_serializer` specialization or a wrapper type of your own. + +If the alternatives are JSON-native (numbers, strings, ...), a variant such as `#!cpp std::variant` +can instead be written as the plain value, and read by testing the JSON type (`is_number_integer()`, `is_string()`) +to choose the alternative. + +## `std::chrono` + +A duration is written as an object with the keys `unit` and `value`, for example `{"unit": "ms", "value": 1500}`, so +the JSON value is self-describing. The serializer names the unit of the period (`ns`, `us`, `ms`, `s`, `min`, `h`; +extend the list for other periods), and the deserializer rejects a value whose unit differs from the requested +duration type instead of silently misreading it: + +??? example "Example: serialize durations and system_clock time points" + + ```cpp + --8<-- "examples/type_recipes_chrono.cpp" + ``` + + Output: + + ``` + --8<-- "examples/type_recipes_chrono.output" + ``` + +A `system_clock::time_point` with millisecond precision is written as the duration since the Unix epoch, that is, as an object with `unit` and `value`. +The time point serializer is limited to `system_clock`, +because the epoch of other clocks (for example `steady_clock`) has no meaning outside the running program. + +If you prefer a text representation such as ISO 8601 (`2023-11-14T22:13:20Z`), write the string with +`#!cpp std::format("{:%FT%TZ}", t)` and read it with `std::chrono::parse` (both C++20), or with a date library, in the +same two functions. + +## See also + +- [Arbitrary type conversions](arbitrary_types.md) - the conversion mechanism and macros +- [Why isn't my `to_json` found?](arbitrary_types.md#why-isnt-my-to_json-or-from_json-found) +- [Converting values](conversions.md) - `get`, implicit conversions, and `std::optional` diff --git a/docs/mkdocs/docs/features/types/index.md b/docs/mkdocs/docs/features/types/index.md index 354acd5ac..03e8c0bae 100644 --- a/docs/mkdocs/docs/features/types/index.md +++ b/docs/mkdocs/docs/features/types/index.md @@ -120,7 +120,7 @@ requirements are collected in [Template Parameter Requirements](template_paramet ### Default type -With the default values for *ObjectType* (`std::map`), *StringType* (`std::string`), and *AllocatorType* (`std::allocator`), the default value for `object_t` is: +With the default values for *ObjectType* (`std::map`), *StringType* (`std::string`), and *AllocatorType* (`std::allocator`), the default type for `object_t` is: ```cpp std::map< @@ -177,7 +177,7 @@ json j = m; ### Default type -With the default values for *ArrayType* (`std::vector`) and *AllocatorType* (`std::allocator`), the default value for `array_t` is: +With the default values for *ArrayType* (`std::vector`) and *AllocatorType* (`std::allocator`), the default type for `array_t` is: ```cpp std::vector< @@ -209,7 +209,7 @@ Unicode values are split by the JSON class into byte-sized characters during des ### Default type -With the default values for *StringType* (`std::string`), the default value for `string_t` is `#!cpp std::string`. +With the default values for *StringType* (`std::string`), the default type for `string_t` is `#!cpp std::string`. ### Encoding @@ -234,7 +234,7 @@ String values are stored as pointers in a `basic_json` type. That is, for any ac ### Default type -With the default values for *BooleanType* (`#!cpp bool`), the default value for `boolean_t` is `#!cpp bool`. +With the default values for *BooleanType* (`#!cpp bool`), the default type for `boolean_t` is `#!cpp bool`. ### Storage @@ -252,9 +252,9 @@ This description includes both integer and floating-point numbers. However, C++ ### Default types -With the default values for *NumberIntegerType* (`std::int64_t`), the default value for `number_integer_t` is `std::int64_t`. -With the default values for *NumberUnsignedType* (`std::uint64_t`), the default value for `number_unsigned_t` is `std::uint64_t`. -With the default values for *NumberFloatType* (`#!cpp double`), the default value for `number_float_t` is `#!cpp double`. +With the default values for *NumberIntegerType* (`std::int64_t`), the default type for `number_integer_t` is `std::int64_t`. +With the default values for *NumberUnsignedType* (`std::uint64_t`), the default type for `number_unsigned_t` is `std::uint64_t`. +With the default values for *NumberFloatType* (`#!cpp double`), the default type for `number_float_t` is `#!cpp double`. ### Default behavior @@ -281,8 +281,17 @@ As this range is a subrange of the exactly supported range [`INT64_MIN`, `INT64_ > This specification allows implementations to set limits on the range and precision of numbers accepted. Since software that implements IEEE 754-2008 binary64 (double precision) numbers is generally available and widely used, good interoperability can be achieved by implementations that expect no more precision or range than these provide, in the sense that implementations will approximate JSON numbers within the expected precision. -This implementation does exactly follow this approach, as it uses double precision floating-point numbers. Note values smaller than `#!c -1.79769313486232e+308` and values greater than `#!c 1.79769313486232e+308` will be stored as NaN internally and be serialized to `#!json null`. +This implementation does exactly follow this approach, as it uses double precision floating-point numbers by default (see *NumberFloatType* above). Note that with the default `#!cpp double`, numbers smaller than `#!c -1.79769313486232e+308` or greater than `#!c 1.79769313486232e+308` are rejected during parsing with [`out_of_range.406`](../../home/exceptions.md#jsonexceptionout_of_range406). Non-finite values (NaN and infinity) can only be stored by assigning them directly and are serialized to `#!json null`. ### Storage Integer number values, unsigned integer number values, and floating-point number values are stored directly inside a `basic_json` type. + +## See also + +- [Number handling](number_handling.md) - how numbers are stored and converted +- [Template parameter requirements](template_parameters.md) +- [**basic_json**](../../api/basic_json/index.md) - the class template and its member types +- [**type**](../../api/basic_json/type.md) and [**value_t**](../../api/basic_json/value_t.md) - query the type of a value +- [**object_t**](../../api/basic_json/object_t.md), [**array_t**](../../api/basic_json/array_t.md), [**string_t**](../../api/basic_json/string_t.md), [**number_integer_t**](../../api/basic_json/number_integer_t.md), [**number_float_t**](../../api/basic_json/number_float_t.md) - the underlying types +- [Object order](../object_order.md) diff --git a/docs/mkdocs/docs/features/types/number_handling.md b/docs/mkdocs/docs/features/types/number_handling.md index 13b6dfd43..d11ad2747 100644 --- a/docs/mkdocs/docs/features/types/number_handling.md +++ b/docs/mkdocs/docs/features/types/number_handling.md @@ -82,12 +82,13 @@ flowchart TD - Numbers with a decimal digit or scientific notation are always stored as `#!c double`. - The number types can be changed, see [Template number types](#template-number-types). - - Integers are converted by the library's own digit parser. Floating-point numbers are converted with - [`std::from_chars`](https://en.cppreference.com/w/cpp/utility/from_chars) if the library is compiled with C++17 - and the standard library supports it, then with an exact fast path for `#!c double` values with few significant - digits, and otherwise with the locale-aware - [`std::strtod`](https://en.cppreference.com/w/cpp/string/byte/strtof) (`std::strtof`/`std::strtold` for the - other floating-point types). Before version 3.13.0, the conversion was realized by + - The library converts integers and floating-point numbers itself, independent of the locale. Floating-point + numbers are correctly rounded (to nearest, ties to even). Only a `#!c long double` that is not IEEE 754 binary64 + (e.g., the 80-bit x87 format) is converted with `#!cpp std::from_chars` where available, or else with + [`std::strtold`](https://en.cppreference.com/w/cpp/string/byte/strtof). For that call, the library temporarily + replaces the `.` with the decimal point of the current locale (which may be longer than one byte, e.g., in + `fa_IR.UTF-8`), so the result does not depend on the locale either. Changing the locale in another thread during + parsing is undefined behavior of the C library, though. Before version 3.13.0, the conversion was realized by [`std::strtoull`](https://en.cppreference.com/w/cpp/string/byte/strtoul), [`std::strtoll`](https://en.cppreference.com/w/cpp/string/byte/strtol), and `std::strtod`, respectively. @@ -100,10 +101,10 @@ flowchart TD ### Number limits - Any 64-bit signed or unsigned integer can be stored without loss of precision. -- Numbers exceeding the limits of `#!c double` (i.e., numbers that after conversion via -[`std::strtod`](https://en.cppreference.com/w/cpp/string/byte/strtof) are not satisfying +- Numbers exceeding the limits of `#!c double` (i.e., numbers whose rounded value is not satisfying [`std::isfinite`](https://en.cppreference.com/w/cpp/numeric/math/isfinite) such as `#!c 1E400`) will throw exception -[`json.exception.out_of_range.406`](../../home/exceptions.md#jsonexceptionout_of_range406) during parsing. +[`json.exception.out_of_range.406`](../../home/exceptions.md#jsonexceptionout_of_range406) during parsing. Numbers too +small for `#!c double` (such as `#!c 1E-400`) become zero, with the sign of the number. - Floating-point numbers are rounded to the next number representable as `double`. For instance `#!c 3.141592653589793238462643383279` is stored as [`0x400921fb54442d18`](https://float.exposed/0x400921fb54442d18). This is the same behavior as the code `#!c double x = 3.141592653589793238462643383279;`. @@ -133,9 +134,10 @@ That is, `-0` is stored as a signed integer, but the serialization does not repr ### Number serialization - Integer numbers are serialized as is; that is, no scientific notation is used. -- Floating-point numbers are serialized as specified by the `#!c %g` printf modifier with - [`std::numeric_limits::max_digits10`](https://en.cppreference.com/w/cpp/types/numeric_limits/max_digits10) - significant digits. The rationale is to use the shortest representation while still allowing round-tripping. +- Floating-point numbers are serialized with the fewest digits that read back as the same value (the closest such + digits if there are several), in the layout of the `#!c %g` printf modifier: `#!c 1.5`, `#!c 100.0`, `#!c 1e+100`. + Doubles are converted with the algorithm of [Żmij](https://github.com/vitaut/zmij), floats with Grisu2, which + can write more digits than necessary. !!! hint "Notes regarding precision of floating-point numbers" @@ -343,8 +345,11 @@ The number types can be changed with template parameters. in case of overflow. - The type for unsigned integers must be convertible from `#!c unsigned long long`. The type for floating-point numbers is used in case of overflow. - - The types for signed and unsigned integers must be distinct, see - [#2573](https://github.com/nlohmann/json/issues/2573). + - The type for unsigned integers can also be the same signed type as the type for signed integers (e.g., + `#!c std::int64_t` for both). Then every integer is stored as a signed integer, and + [`is_number_unsigned()`](../../api/basic_json/is_number_unsigned.md) is always `#!c false`, see + [Template Parameter Requirements](template_parameters.md#same-type-for-both). + - The type for floating-point numbers must be distinct from the integer types. - Only `#!c double`, `#!c float`, and `#!c long double` are supported for floating-point numbers. !!! example diff --git a/docs/mkdocs/docs/features/types/template_parameters.md b/docs/mkdocs/docs/features/types/template_parameters.md index f20102878..888590234 100644 --- a/docs/mkdocs/docs/features/types/template_parameters.md +++ b/docs/mkdocs/docs/features/types/template_parameters.md @@ -30,9 +30,9 @@ Requirements are split into two groups: diagnosed with dedicated error messages, and violating most of them results in a compiler error somewhere inside the library. Four violations are not caught at compile time at all: - - A [`StringType`](#stringtype) whose `data()` is not null-terminated compiles and can silently misparse - floating-point numbers, because the lexer may hand the buffer to `#!cpp std::strtod`, which reads up to the - terminating null character. + - A [`StringType`](#stringtype) whose `data()` is not null-terminated compiles and silently misparses numbers + stored as a `#!cpp long double` that is not IEEE 754 binary64 (e.g., the 80-bit x87 format), because the lexer + hands the buffer to `#!cpp std::strtold`. - A stateful [`AllocatorType`](#allocatortype) compiles and silently ignores its state: allocation, deallocation, and [`get_allocator()`](../../api/basic_json/get_allocator.md) each use a different default-constructed instance. - The two [cross-specialization conversions](#cross-specialization-conversions) below. These abort on an assertion @@ -40,19 +40,19 @@ Requirements are split into two groups: ## Overview -| Template parameter | Default | Notable substitutes | -|-------------------------------------------------------------------|-----------------------------------|-----------------------------------------------------------------------| -| [`ObjectType`](#objecttype) | `std::map` | [`nlohmann::ordered_map`](../../api/ordered_map.md), Abseil hash maps | -| [`ArrayType`](#arraytype) | `std::vector` | `#!cpp std::deque` | -| [`StringType`](#stringtype) | `std::string` | `std::string`-like types over `char` | -| [`BooleanType`](#booleantype) | `bool` | none worth using | -| [`NumberIntegerType`](#numberintegertype-and-numberunsignedtype) | `std::int64_t` | any signed integer type | -| [`NumberUnsignedType`](#numberintegertype-and-numberunsignedtype) | `std::uint64_t` | any unsigned integer type at least as wide as `NumberIntegerType` | -| [`NumberFloatType`](#numberfloattype) | `double` | `float` (`long double`: no binary formats) | -| [`AllocatorType`](#allocatortype) | `std::allocator` | stateless allocators | -| [`JSONSerializer`](#jsonserializer) | `adl_serializer` | serializers with the same interface | -| [`BinaryType`](#binarytype) | `#!cpp std::vector` | `#!cpp std::vector` | -| [`CustomBaseClass`](#custombaseclass) | `void` | any default-constructible class | +| Template parameter | Default | Notable substitutes | +|-------------------------------------------------------------------|-----------------------------------|--------------------------------------------------------------------------------------------------| +| [`ObjectType`](#objecttype) | `std::map` | [`nlohmann::ordered_map`](../../api/ordered_map.md), Abseil hash maps | +| [`ArrayType`](#arraytype) | `std::vector` | `#!cpp std::deque` | +| [`StringType`](#stringtype) | `std::string` | `std::string`-like types over `char` | +| [`BooleanType`](#booleantype) | `bool` | none worth using | +| [`NumberIntegerType`](#numberintegertype-and-numberunsignedtype) | `std::int64_t` | any signed integer type | +| [`NumberUnsignedType`](#numberintegertype-and-numberunsignedtype) | `std::uint64_t` | any unsigned integer type at least as wide as `NumberIntegerType`, or `NumberIntegerType` itself | +| [`NumberFloatType`](#numberfloattype) | `double` | `float` (`long double`: no binary formats) | +| [`AllocatorType`](#allocatortype) | `std::allocator` | stateless allocators | +| [`JSONSerializer`](#jsonserializer) | `adl_serializer` | serializers with the same interface | +| [`BinaryType`](#binarytype) | `#!cpp std::vector` | `#!cpp std::vector` | +| [`CustomBaseClass`](#custombaseclass) | `void` | any default-constructible class | !!! warning "Third-party containers and incomplete types" @@ -353,16 +353,21 @@ using array_t = ArrayType>; ### Always required - A member type `value_type` that is one byte wide and `char`-compatible. The library stores and processes UTF-8 - encoded `char` data and passes `data()` to functions that take a `#!cpp const char*`, such as `#!cpp std::strtod`. + encoded `char` data and passes `data()` to functions that take a `#!cpp const char*`, such as `#!cpp std::strtold` + (only used to parse a `#!cpp long double` that is not IEEE 754 binary64, see + [`NumberFloatType`](#numberfloattype)). `#!cpp std::wstring`, `#!cpp std::u16string`, and `#!cpp std::u32string` are **not** valid choices; see the FAQ on [wide string handling](../../home/faq.md#wide-string-handling). - Constructors: default, copy, move, from `#!cpp const char*` (which must not be `#!cpp explicit`), from `#!cpp (const char*, size_type)`, and from `#!cpp (size_type, char)`; and copy or move assignment. - Member functions `size()`, `clear()`, `resize(n, c)`, `data()`, `push_back(char)`, and `operator[]` (const and non-const, returning references). `c_str()` and `back()` are **not** required. -- `data()` must return a pointer to a contiguous, **null-terminated** buffer -- the parser may hand it to - `#!cpp std::strtod`, which reads up to the null character. A type whose `data()` is not null-terminated does not - fail to compile; it can silently misparse floating-point numbers. +- `data()` must return a pointer to a contiguous, **null-terminated** buffer. `#!cpp float`, `#!cpp double`, and a + `#!cpp long double` that is IEEE 754 binary64 are converted by the library itself and do not depend on this. For any + other `NumberFloatType` (a `#!cpp long double` of another format), the parser falls back to `#!cpp std::strtold` when + `#!cpp std::from_chars` is not available or declines the token, and `std::strtold` reads up to the null character. A type whose `data()` + is not null-terminated does not fail to compile; with such a `NumberFloatType` it can silently misparse + floating-point numbers. - `append(const char*, size_type)`, used by [`dump`](../../api/basic_json/dump.md), and `append(const StringType&)`, used by the CBOR reader for indefinite-length strings. The library's internal string concatenation additionally has to append a `#!cpp char` and a `#!cpp const char*`; for each it selects between `append(arg)`, `#!cpp operator+=`, @@ -499,13 +504,14 @@ Both types are stored **directly** inside `basic_json`'s union. ### Always required - `#!cpp std::is_integral` must be satisfied: `NumberIntegerType` must be a **signed** integer type, - `NumberUnsignedType` an **unsigned** integer type. Class types are not supported -- among others, the constructors - taking integer values are constrained on `#!cpp std::is_integral`. + `NumberUnsignedType` an **unsigned** integer type or the same type as `NumberIntegerType` (see + [below](#same-type-for-both)). Class types are not supported -- among others, the constructors taking integer values + are constrained on `#!cpp std::is_integral`. - Trivially default-constructible, trivially copyable, and trivially destructible (union member). - `#!cpp std::numeric_limits` must be specialized for both types. -- `NumberUnsignedType` must be able to represent the absolute value of every `NumberIntegerType` value; serialization - of negative numbers converts the value to `NumberUnsignedType`. A `#!cpp static_assert` requires it to be at least as - wide as `NumberIntegerType`, which is what that amounts to for the standard integer types. +- An unsigned `NumberUnsignedType` must be able to represent the absolute value of every `NumberIntegerType` value; + serialization of negative numbers converts the value to `NumberUnsignedType`. A `#!cpp static_assert` requires it to + be at least as wide as `NumberIntegerType`, which is what that amounts to for the standard integer types. - Both types must fit into the internal 64-character number buffer used by [`dump`](../../api/basic_json/dump.md), which is the case for all standard integer types. - [`std::hash`](../../api/basic_json/std_hash.md) additionally requires `#!cpp std::hash` specializations. @@ -517,6 +523,23 @@ is stored as [`number_float_t`](../../api/basic_json/number_float_t.md) instead. therefore silently changes parse results rather than raising an error. See [Number Handling](number_handling.md) for details. +#### Same type for both + +`NumberUnsignedType` may be the same type as `NumberIntegerType`, e.g. +`#!cpp nlohmann::json::with_integers_t` (see +[`with_integers_t`](../../api/basic_json/with_t.md)), for code that only wants to handle one integer type. Such a `basic_json` stores every integer as +[`number_integer_t`](../../api/basic_json/number_integer_t.md): + +- [`is_number_unsigned()`](../../api/basic_json/is_number_unsigned.md) is always `#!cpp false`, and + [`type()`](../../api/basic_json/type.md) never returns `value_t::number_unsigned`. Constructing a value from + `value_t::number_unsigned` gives a `value_t::number_integer` value. +- Values of unsigned C++ types (e.g., `#!cpp 5u`) are converted to `NumberIntegerType`, so values above its maximum + wrap around, as values outside the range of a narrower `NumberIntegerType` already do. +- The parsers store a non-negative integer above the maximum of `NumberIntegerType` (e.g., `18446744073709551615` for + `#!cpp std::int64_t`) as [`number_float_t`](../../api/basic_json/number_float_t.md), like any other integer that does + not fit. +- Converting from a `basic_json` with distinct types turns its unsigned values into `number_integer_t` values. + ### Compatible types | Type pair | Support | @@ -524,8 +547,9 @@ therefore silently changes parse results rather than raising an error. See | `#!cpp std::int64_t` / `#!cpp std::uint64_t` (default) | full | | `#!cpp std::int32_t` / `#!cpp std::uint32_t`, `#!cpp long long` / `#!cpp unsigned long long` | full; narrower types change which literals the parser can represent | | any other pair of standard signed/unsigned integer types | full | +| the same signed integer type for both, e.g. `#!cpp std::int64_t` / `#!cpp std::int64_t` | full; every integer is stored as `number_integer_t`, see [above](#same-type-for-both) | | class types, enumerations | not usable; `#!cpp std::is_integral` must hold | -| `#!cpp bool`, or a type already used for another member of the union | not usable; `#!cpp std::is_integral` is in fact `#!cpp true`, but the `get_impl_ptr` overloads for `boolean_t`, `number_integer_t`, `number_unsigned_t` and `number_float_t` would collide | +| `#!cpp bool`, `number_float_t`, or the type of another member of the union | not usable; `#!cpp std::is_integral` is in fact `#!cpp true`, but the `get_impl_ptr` overloads for `boolean_t`, `number_integer_t`, `number_unsigned_t` and `number_float_t` would collide | ## `NumberFloatType` @@ -541,16 +565,18 @@ therefore silently changes parse results rather than raising an error. See `NumberFloatType` must be one of `#!cpp float`, `#!cpp double`, or `#!cpp long double`: -- The [parser](../parsing/index.md) converts number literals with `#!cpp std::from_chars` or, as a fallback, with - `#!cpp std::strtof`, `#!cpp std::strtod`, or `#!cpp std::strtold`; the library provides overloads for exactly these - three types. +- The [parser](../parsing/index.md) converts number literals to `#!cpp float`, `#!cpp double`, and a + `#!cpp long double` that is IEEE 754 binary64 itself; other `#!cpp long double` formats are converted with + `#!cpp std::from_chars` where available, or with `#!cpp std::strtold`. The library provides overloads for exactly + these three types. - [`dump`](../../api/basic_json/dump.md) falls back to `#!cpp std::snprintf` with the `%g` and `%Lg` conversion specifiers, for which the library likewise provides only `#!cpp double` and `#!cpp long double` overloads (`#!cpp float` is promoted to `#!cpp double`). -If `#!cpp std::numeric_limits` describes an IEEE 754 binary32 or binary64 number, `dump` uses the -Grisu2 algorithm, which produces the shortest representation that round-trips. Otherwise the `snprintf` fallback with -`max_digits10` digits is used. +If `#!cpp std::numeric_limits` describes an IEEE 754 binary64 number, `dump` uses the algorithm of +Żmij, which produces the shortest representation that round-trips. For IEEE 754 binary32 numbers, it uses Grisu2, +which produces a short representation that round-trips. Otherwise the `snprintf` fallback with `max_digits10` digits is +used. ### Required for the binary formats @@ -562,7 +588,7 @@ binary32 or binary64 field and have no encoding for `#!cpp long double`. | Type | Support | |--------------------------|-----------------------------------------------------------------------------------------------------------------------| -| `#!cpp double` (default) | full; short round-trip output through Grisu2 | +| `#!cpp double` (default) | full; shortest round-trip output through Żmij | | `#!cpp float` | full; short round-trip output through Grisu2 | | `#!cpp long double` | `dump` and `parse` only; the binary format writers do not compile, as they only handle IEEE 754 binary32 and binary64 | | any other type | not usable | diff --git a/docs/mkdocs/docs/home/architecture.md b/docs/mkdocs/docs/home/architecture.md index 6c0892f11..295ea09ba 100644 --- a/docs/mkdocs/docs/home/architecture.md +++ b/docs/mkdocs/docs/home/architecture.md @@ -34,7 +34,7 @@ flowchart LR - **JSON text** is read by an [input adapter](#input-adapters), tokenized by the lexer, and turned into SAX events by the parser. -- **Binary formats** (BJData, BSON, CBOR, MessagePack, UBJSON) are read by an input adapter and turned into the same SAX +- **Binary formats** (BJData, BON8, BSON, CBOR, MessagePack, UBJSON) are read by an input adapter and turned into the same SAX events by the `binary_reader`. - A [SAX consumer](#sax-interface) receives the events. The one used by [`parse`](../api/basic_json/parse.md) builds a `basic_json` value tree. @@ -51,6 +51,11 @@ The public headers are in [`include/nlohmann`](https://github.com/nlohmann/json/ [`adl_serializer`](../api/adl_serializer/index.md), [`byte_container_with_subtype`](../api/byte_container_with_subtype/index.md), and [`ordered_map`](../api/ordered_map.md). +- [`json_view.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/json_view.hpp) is a separate, + optional header that defines [`basic_json_document`](../api/basic_json_document/index.md) and + [`basic_json_view`](../api/basic_json_view/index.md), a flat-index, read-only, non-owning way to look at a parsed + JSON text; see [Zero-copy JSON views](../features/json_view.md). It builds on `json.hpp` internals (it requires the + same library version) and has its own `detail/view/` subdirectory. Everything else lives in [`detail/`](https://github.com/nlohmann/json/tree/develop/include/nlohmann/detail) and namespace `nlohmann::detail`, which is not part of the public API. Paths below are relative to `include/nlohmann`. @@ -59,7 +64,7 @@ below are relative to `include/nlohmann`. |-----------|----------| | Value type enumeration | [`detail/value_t.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/value_t.hpp) | | Input adapters | [`detail/input/input_adapters.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/input_adapters.hpp) | -| Lexer | [`detail/input/lexer.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/lexer.hpp), [`detail/input/number_parse.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/number_parse.hpp), [`detail/input/string_scan.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/string_scan.hpp) | +| Lexer | [`detail/input/lexer.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/lexer.hpp), [`detail/input/number_parse.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/number_parse.hpp), [`detail/input/string_scan.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/string_scan.hpp), [`detail/input/simd_scan.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/simd_scan.hpp) | | Parser | [`detail/input/parser.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/parser.hpp) | | SAX interface and DOM builders | [`detail/input/json_sax.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/json_sax.hpp) | | Binary format readers | [`detail/input/binary_reader.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/binary_reader.hpp) | @@ -74,7 +79,9 @@ below are relative to `include/nlohmann`. | Macros | [`detail/macro_scope.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/macro_scope.hpp), [`detail/macro_unscope.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/macro_unscope.hpp), [`detail/abi_macros.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/abi_macros.hpp) | The single-header version [`single_include/nlohmann/json.hpp`](https://github.com/nlohmann/json/blob/develop/single_include/nlohmann/json.hpp) -is generated from these files with `make amalgamate` and must not be edited by hand. +is generated from these files with `make amalgamate` and must not be edited by hand. The same command also generates +[`single_include/nlohmann/json_view.hpp`](https://github.com/nlohmann/json/blob/develop/single_include/nlohmann/json_view.hpp) +from `json_view.hpp` and `detail/view/`. ## Template parameters @@ -164,6 +171,105 @@ pointer to them. This keeps a `basic_json` value small: one pointer-sized union maintains the invariant that the pointer matching `m_type` is never null; `assert_invariant()` checks it with [runtime assertions](../features/assertions.md). +## Node index of JSON views + +A [`basic_json_document`](../api/basic_json_document/index.md) (see [Zero-copy JSON views](../features/json_view.md)) +does not build a tree of values. Its parser +([`detail/view/builder.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/view/builder.hpp)) +writes a flat array of 16-byte nodes, one per value and one per object key, in document order. A +[`basic_json_view`](../api/basic_json_view/index.md) is a pointer to the document and a pointer to one node. The layout +is `struct node` in +[`detail/view/node.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/view/node.hpp) +(the numbers are bit offsets, 32 bits per row): + +```mermaid +packet-beta + 0-7: "kind" + 8-15: "flags" + 16-31: "extra" + 32-63: "off" + 64-95: "len" + 96-127: "next" +``` + +| Bytes | Field | Type | Contents | +|-------|---------|------------|-------------------------------------------------------------------------------------------------------------------------------| +| 0 | `kind` | `uint8_t` | the type, numbered as [`value_t`](../api/basic_json/value_t.md): 0 null, 1 object, 2 array, 3 string, 4 boolean, 5 signed integer, 6 unsigned integer, 7 float; 10 for a link (see below). There is no node for a discarded value: a [discarded](../api/basic_json_view/is_discarded.md) view has none | +| 1 | `flags` | `uint8_t` | bits 0-1: where a string's bytes (or a number's token) are (0: the source text, 1: the buffer of decoded strings, for strings with escapes, 2: the edit buffer); bit 2: the value of a boolean; bit 3: moved, bit 4: new, bit 5: linked (see below) | +| 2-3 | `extra` | `uint16_t` | numbers: the number of integer digits (low byte) and fraction digits (high byte), 255 for more; objects: the number of their key hash index (1-based), or 0; otherwise 0 | +| 4-7 | `off` | `uint32_t` | where the value starts: the first byte after a string's opening quote (or its position in the buffer of decoded strings), the first byte of a number or literal, the bracket of an array or object | +| 8-11 | `len` | `uint32_t` | strings: the length after decoding; floats and literals: the length of the token; arrays and objects: the number of elements | +| 12-15 | `next` | `uint32_t` | arrays and objects: the number of nodes of the subtree, including the node itself | + +- **Integers** keep their converted value in bytes 8-15 instead of `len` and `next`; the length of their token follows + from the number of digits in `extra` (and the sign). Non-negative integers are unsigned integers, as with + [`parse`](../api/basic_json/parse.md). +- **Floats** keep only their token. The digit layout in `extra` lets the conversion read the digits without scanning the + token again, and only when the value is read. +- **Object members** are the node of the key (a string) followed by the nodes of the value. +- **Navigation** needs no pointers: the elements of an array or object follow its node, and the node after a value's + subtree is `next` nodes further for an array or object, and the next node otherwise (`document_data::after`). Views + step from element to element this way and skip whole subtrees in constant time. +- **Offsets** are 32 bits wide, so a document is limited to 4294967279 bytes, 4 GiB minus 16 bytes (a margin below + 2^32 for positions one scanner step past the end of the text; `out_of_range.416`). +- **Large objects** (128 members or more) get a hash index for their keys after parsing + ([`detail/view/object_index.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/view/object_index.hpp)): + an open-addressing table whose slots hold the distance from the object's node to a key's node, so that a lookup does + not compare every key. Of duplicate keys the table leads to the first, as a linear search does. The object's `extra` + holds the number of its table. Only 65,535 tables fit into `extra`; objects beyond them are searched linearly. + +For example, `#!json {"a": [1, 2.5]}` becomes five nodes. Each node's elements follow it, and `next` leads from an +array or object past its subtree: + +```mermaid +flowchart LR + n0["0: object
len 1, next 5"] + n1["1: key a"] + n2["2: array
len 2, next 3"] + n3["3: unsigned integer 1"] + n4["4: float 2.5"] + e(["end"]) + n0 --> n1 --> n2 --> n3 --> n4 --> e + n0 -. next .-> e + n2 -. next .-> e +``` + +| Node | `kind` | `extra` | `off` | `len` | `next` | +|------|----------------------|---------|-------|-------|--------| +| 0 | 1 (object) | 0 | 0 | 1 | 5 | +| 1 | 3 (string) | 0 | 2 | 1 | 0 | +| 2 | 2 (array) | 0 | 6 | 2 | 3 | +| 3 | 6 (unsigned integer) | 0x0001 | 7 | - | - | +| 4 | 7 (float) | 0x0101 | 10 | 3 | 0 | + +All `flags` are 0. The integer's bytes 8-15 hold its value, 1; its `extra` says it has one digit. The float's `extra` +says it has one integer and one fraction digit, and its `len` is that of the token `2.5`. + +Editable documents ([`json_editable_document`](../api/json_editable_document.md), +[`detail/view/edit.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/view/edit.hpp) and +[`detail/view/edit_storage.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/view/edit_storage.hpp)) +never write the source text and never move or resize the parsed index, so views stay valid while the document is +edited: + +- A new scalar is written over its node. Its text (a string, or the token of a number as `dump()` writes it) goes to + the edit buffer, which `flags` bits 0-1 then name. +- An array or object whose elements change gets the flag *moved* (bit 3): its elements then live in a separate + sequence (a header node, then the entries), whose number is in `off`. The entries are links (`kind` 10), whose bytes + 8-15 hold the address of the value's node, so values never move. +- A node written by an edit gets the flag *new* (bit 4): it has no position in the source text. +- A value that an entry of such a sequence links to gets the flag *linked* (bit 5): its extent in the parsed layout + (`next`) no longer matters, so a scalar can replace an array or object there in place. +- Views of read-only documents compile without any of this: how views walk the index is a template parameter + (`navigation`). + +Images ([`save`](../api/basic_json_document/save.md) and [`load`](../api/basic_json_document/load.md), +[`detail/view/image.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/view/image.hpp)) store +the nodes as they are: a 64-byte header (the magic bytes `NJVI`, a format version, the sizes, and reserved bytes that +must be zero), the nodes, the text, and the decoded strings. An edited document is first written in document order, as +the parser would have written it (without links), and the numbers of the hash indexes are cleared, since `load` +rebuilds the indexes. So a change of the node layout is a change of the image format: it must raise `image_version`, +and `load` then rejects images of other versions (`parse_error.116`) instead of misreading them. + ## Input adapters Input is read via **input adapters** that abstract a source. Every input adapter provides this interface: @@ -185,9 +291,11 @@ The lexer detects two optional extensions at compile time. Only `iterator_input_ random-access input of single-byte characters: - `supports_seek`, `get_consumed_count()`, and `copy_consumed_range()` let the lexer reconstruct already consumed input - for error messages instead of copying every character it reads. + for error messages instead of copying every character it reads. With `get_read_count()` and + `count_consumed_lines()`, the lexer also derives the read position from the adapter and counts lines only when an + error needs them, instead of counting characters and lines on every read. - `supports_bulk_scan`, `bulk_data()`, `bulk_remaining()`, and `bulk_skip()` let the lexer scan strings directly in - contiguous memory, several bytes at a time. + contiguous memory, several bytes at a time, and skip the spaces that indent a line in 8-byte steps. The function `input_adapter` picks the right adapter for the argument passed to `parse`, `accept`, `sax_parse`, or the `from_*` functions: @@ -231,6 +339,11 @@ The `serializer` (used by [`dump`](../api/basic_json/dump.md) and [`operator<<`] - `output_stream_adapter` writes to a `std::ostream`. - `output_string_adapter` appends to a string. +The `binary_writer` does not call an adapter for every byte. The `to_*` functions that return a `std::vector` write +into the vector through a sink (`output_vector_sink`) that is not virtual. The functions that take an output adapter +write through a sink (`output_adapter_sink`) that collects the output in a buffer of one kibibyte and passes it to the +adapter in blocks, so that the adapter is called (and a stream locked) once per block instead of once per value. + ## Value conversion Values are converted from and to other types with the `JSONSerializer` template parameter. The default, diff --git a/docs/mkdocs/docs/home/debugging.md b/docs/mkdocs/docs/home/debugging.md index 803322150..362afaa6b 100644 --- a/docs/mkdocs/docs/home/debugging.md +++ b/docs/mkdocs/docs/home/debugging.md @@ -1,7 +1,7 @@ # Debugging -This page collects the library's built-in debugger integrations and other debugging-related features. They are -not linked from a single place elsewhere in the docs, so are collected here. +This page collects the library's built-in debugger integrations and other debugging-related features. They are not +covered by any single page elsewhere in the docs, so they are collected here. ## Visual Studio (natvis) @@ -10,6 +10,29 @@ 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 fields, when debugging with the MSVC debug engine (`cppvsdbg`) in Visual Studio or VS Code. +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: + +1. Check out the release tag of the library version you use. +2. List the ABI tags and the macros that enable them: + + ```shell + make natvis NATVIS_ARGS="--list-tags" + ``` + +3. Generate a Natvis file with the tags of your configuration, for example for `JSON_DIAGNOSTICS`, + `JSON_DIAGNOSTIC_POSITIONS`, and `JSON_VIEW_NO_SIMD`: + + ```shell + make natvis NATVIS_ARGS="--abi-tags _diag,_dp,_vns --output my_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, doubling with each further tag). See +[`tools/generate_natvis`](https://github.com/nlohmann/json/tree/develop/tools/generate_natvis) for all options. + Debug engines that wrap LLDB instead of the MSVC debug engine (for example, `codelldb` in VS Code) only have partial/experimental Natvis support, and commonly fall back to showing raw internal fields even with the `.natvis` file present. Switching to `cppvsdbg` where available, or checking your debug extension's own Natvis diff --git a/docs/mkdocs/docs/home/design_goals.md b/docs/mkdocs/docs/home/design_goals.md index 549ba9a1a..987124378 100644 --- a/docs/mkdocs/docs/home/design_goals.md +++ b/docs/mkdocs/docs/home/design_goals.md @@ -6,7 +6,7 @@ There are myriads of [JSON](https://json.org) libraries out there, and each may - **Trivial integration**. Our whole code consists of a single header file [`json.hpp`](https://github.com/nlohmann/json/blob/develop/single_include/nlohmann/json.hpp). That's it. No library, no subproject, no dependencies, no complex build system. The class is written in vanilla C++11. All in all, everything should require no adjustment of your compiler flags or project settings. -- **Serious testing**. Our class is heavily [unit-tested](https://github.com/nlohmann/json/tree/develop/tests/src) and covers [100%](https://coveralls.io/r/nlohmann/json) of the code, including all exceptional behavior. Furthermore, we checked with [Valgrind](http://valgrind.org) and the [Clang Sanitizers](https://clang.llvm.org/docs/index.html) that there are no memory leaks. [Google OSS-Fuzz](https://github.com/google/oss-fuzz/tree/master/projects/json) additionally runs fuzz tests against all parsers 24/7, effectively executing billions of tests so far. To maintain high quality, the project is following the [OpenSSF Best Practices](https://www.bestpractices.dev/projects/289). +- **Serious testing**. Our class is heavily [unit-tested](https://github.com/nlohmann/json/tree/develop/tests/src) and covers [100%](https://coveralls.io/r/nlohmann/json) of the code, including all exceptional behavior. Furthermore, we checked with [Valgrind](https://valgrind.org) and the [Clang Sanitizers](https://clang.llvm.org/docs/index.html) that there are no memory leaks. [Google OSS-Fuzz](https://github.com/google/oss-fuzz/tree/master/projects/json) additionally runs fuzz tests against all parsers 24/7, effectively executing billions of tests so far. To maintain high quality, the project is following the [OpenSSF Best Practices](https://www.bestpractices.dev/projects/289). Other aspects were not so important to us: @@ -14,4 +14,4 @@ Other aspects were not so important to us: - **Speed**. There are certainly [faster JSON libraries](https://github.com/miloyip/nativejson-benchmark#parsing-time) out there. However, if your goal is to speed up your development by adding JSON support with a single header, then this library is the way to go. If you know how to use a `std::vector` or `std::map`, you are already set. -See the [contribution guidelines](https://github.com/nlohmann/json/blob/master/.github/CONTRIBUTING.md#please-dont) for more information. +See the [contribution guidelines](https://github.com/nlohmann/json/blob/develop/.github/CONTRIBUTING.md#please-dont) for more information. diff --git a/docs/mkdocs/docs/home/exceptions.md b/docs/mkdocs/docs/home/exceptions.md index d6fdce37a..4b8f6edd9 100644 --- a/docs/mkdocs/docs/home/exceptions.md +++ b/docs/mkdocs/docs/home/exceptions.md @@ -39,6 +39,8 @@ classDiagram Exceptions are used widely within the library. They can, however, be switched off with either using the compiler flag `-fno-exceptions` or by defining the symbol [`JSON_NOEXCEPTION`](../api/macros/json_noexception.md). In this case, exceptions are replaced by `abort()` calls. You can further control this behavior by defining `JSON_THROW_USER` (overriding `#!cpp throw`), `JSON_TRY_USER` (overriding `#!cpp try`), and `JSON_CATCH_USER` (overriding `#!cpp catch`). +See [Working without exceptions](../features/no_exceptions.md) for the functions that report errors without throwing. + Note that [`JSON_THROW_USER`](../api/macros/json_throw_user.md) should leave the current scope (e.g., by throwing or aborting), as continuing after it may yield undefined behavior. ??? example "Example: switch off exceptions and log errors before aborting" @@ -99,12 +101,13 @@ As this global context comes at the price of storing one additional pointer per Now the exception message contains a JSON Pointer `/address/housenumber` that indicates which value has the wrong type. -See [documentation of `JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md) for more information. +See [documentation of `JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md) for more information, and +[Debugging](debugging.md) for the debugger integrations of the library. ## Parse errors The library throws this exception when a parse error occurs. Parse errors -can occur during the deserialization of JSON text, CBOR, MessagePack, as well +can occur during the deserialization of JSON text or of one of the binary formats (BJData, BON8, BSON, CBOR, MessagePack, UBJSON), as well as when using JSON Patch. Exceptions have ids 1xx. @@ -190,7 +193,35 @@ This error indicates a syntax error while deserializing a JSON text. The error m !!! tip - Make sure the input is correctly read. Try to write the input to standard output to check if, for instance, the input file was successfully opened. - - Paste the input to a JSON validator like or a tool like [jq](https://stedolan.github.io/jq/). + - Paste the input to a JSON validator like or a tool like [jq](https://jqlang.github.io/jq/). + +??? info "Parsing a file fails at line 1, column 1 (unexpected end of input)" + + The messages `attempting to parse an empty input` (since version 3.11.3) and `unexpected end of input` (also the message of + earlier versions for an empty input) at the very start of the input mean + that the parser did not see a single character. The library only reads from the stream it is given: it does not + open files and cannot tell a missing file from an empty one. The usual causes are: + + - **The file was not opened.** A relative path is resolved against the *working directory of the running program*, + not against the source file or the project. IDEs and build tools often start the program in a build directory + (for example `build/` or `cmake-build-debug/`), so `"example.json"` may need to be `"../example.json"`. Use an + absolute path to rule this out. + - **The file is empty**, or a previous run truncated it (for example when it was opened for writing). + - **The stream was already read.** A stream whose content has been consumed (by printing it with + `std::cout << file.rdbuf()`, or by an earlier `accept`/`parse`/`>>`) is at its end. Parse it once, or rewind it + with `seekg(0)` after clearing the state with `clear()`. + + Check the stream before parsing it: + + ```cpp + std::ifstream f("example.json"); + if (!f.is_open()) + { + std::cerr << "cannot open example.json\n"; + return 1; + } + json data = json::parse(f); + ``` ### json.exception.parse_error.102 @@ -278,7 +309,9 @@ In a JSON Pointer, only `~0` and `~1` are valid escape sequences. ### json.exception.parse_error.109 -A JSON Pointer array index must be a number. +A JSON Pointer array index must be a number. This exception is thrown for an array index that does not begin with a +digit, except for `-` and the empty reference token, which throw [`out_of_range.404`](#jsonexceptionout_of_range404) +where they cannot be resolved. !!! failure "Example messages" @@ -289,6 +322,11 @@ A JSON Pointer array index must be a number. [json.exception.parse_error.109] parse error: array index '+1' is not a number ``` +!!! note + + Before version 3.13.0, a one-character array index that is not a digit (e.g., `x`) threw + [`out_of_range.404`](#jsonexceptionout_of_range404) instead. + ### json.exception.parse_error.110 When parsing a [binary format](../features/binary_formats/index.md), the byte vector ends before the complete value has @@ -388,6 +426,23 @@ A UBJSON high-precision number could not be parsed. [json.exception.parse_error.115] parse error at byte 5: syntax error while parsing UBJSON high-precision number: invalid number text: 1A ``` +### json.exception.parse_error.116 + +[`basic_json_document::load()`](../api/basic_json_document/load.md) rejected an +[image](../features/json_view.md#images): either the bytes are not one [`save()`](../api/basic_json_document/save.md) +could have written (too short, an unknown magic number or format version, or sizes that do not fit the buffer), or +they are, but fail the requested [`image_check`](../api/basic_json_document/load.md#image_check). + +!!! failure "Example message" + + ``` + [json.exception.parse_error.116] parse error: invalid json_document image: the check failed + ``` + +!!! note + + This exception was added in version 3.13.0, together with [images](../features/json_view.md#images). + ## Iterator errors This exception is thrown if iterators passed to a library function do not match @@ -805,12 +860,51 @@ does not list an enumerator and it is therefore converted like the first listed [json.exception.type_error.318] duplicate object key 'red' ``` +### json.exception.type_error.319 + +[`basic_json_document::set`](../api/basic_json_document/set.md), +[`basic_json_document::push_back`](../api/basic_json_document/push_back.md), and +[`basic_json_document::insert`](../api/basic_json_document/insert.md) can store any `basic_json` value except +a binary one: a `json_document` has no representation for [binary values](../features/binary_values.md), which only +ever arise from parsing a binary format or from an explicit [`json::binary`](../api/basic_json/binary.md) value, not +from JSON text. + +!!! failure "Example message" + + ``` + [json.exception.type_error.319] cannot store a binary value in a json_document + ``` + +!!! note + + This exception was added in version 3.13.0, together with editable [`json_document`s](../features/json_view.md). + +### json.exception.type_error.320 + +[`basic_json_document::save()`](../api/basic_json_document/save.md) cannot write an +[image](../features/json_view.md#images) of a [discarded](../api/basic_json_document/is_discarded.md) document. +[`save()`](../api/basic_json_document/save.md) and [`load()`](../api/basic_json_document/load.md) also throw this +exception on a big-endian target, since the image format is little-endian only. + +!!! failure "Example messages" + + ``` + [json.exception.type_error.320] cannot save a discarded json_document + ``` + ``` + [json.exception.type_error.320] json_document images need a little-endian target + ``` + +!!! note + + This exception was added in version 3.13.0, together with [images](../features/json_view.md#images). + ### json.exception.type_error.321 A discarded value (one created by [`parse()`](../api/basic_json/parse.md) with a callback that returns `false` for the value, or by default-constructing a [`basic_json`](../api/basic_json/index.md) with [`value_t::discarded`](../api/basic_json/value_t.md)) was passed to a binary serialization function, either directly or -nested in an array or object. There is no way to represent a discarded value in CBOR, MessagePack, UBJSON, BJData, or BSON. +nested in an array or object. There is no way to represent a discarded value in CBOR, MessagePack, UBJSON, BJData, BSON, or BON8. !!! failure "Example message" @@ -871,7 +965,8 @@ The provided key was not found in the JSON object. ### json.exception.out_of_range.404 -A reference token in a JSON Pointer could not be resolved. +A reference token in a JSON Pointer could not be resolved, for instance an array index that begins with a digit but +contains other characters (e.g., `1a`), or `-` where it cannot be used. !!! failure "Example message" @@ -881,7 +976,9 @@ A reference token in a JSON Pointer could not be resolved. ### json.exception.out_of_range.405 -The JSON Patch operations 'remove' and 'add' cannot be applied to the root element of the JSON value. +The JSON Patch operations 'remove' and 'move' cannot be applied to the root element of the JSON value. The exception is +also thrown when the parent or a reference token of an empty JSON Pointer is requested, for instance by +[`back`](../api/json_pointer/back.md) or [`pop_back`](../api/json_pointer/pop_back.md). !!! failure "Example message" @@ -906,15 +1003,39 @@ double-precision number when `number_float_t` is `#!cpp float`. ### json.exception.out_of_range.407 -This exception previously indicated that the UBJSON and BSON binary formats did not support integer numbers greater than -9223372036854775807 due to limitations in the implemented mapping. However, these limitations have since been resolved, -and this exception no longer occurs. +An integer number cannot be represented by the binary format it is serialized to: -!!! success "Exception cannot occur any more" +- [BON8](../features/binary_formats/bon8.md) only stores integers that fit into int64, so + [`to_bon8`](../api/basic_json/to_bon8.md) throws for unsigned integers greater than 9223372036854775807. +- [CBOR](../features/binary_formats/cbor.md), [MessagePack](../features/binary_formats/messagepack.md), and + [BSON](../features/binary_formats/bson.md) store integers in at most 64 bits. With the default number types, every + integer fits, but a [`number_integer_t`](../api/basic_json/number_integer_t.md) or + [`number_unsigned_t`](../api/basic_json/number_unsigned_t.md) wider than 64 bits (e.g., `__int128`) can hold values + outside the range of the format: [-2^64, 2^64-1] for CBOR, [-2^63, 2^64-1] for MessagePack, and the range of int64 + (signed integers) or uint64 (unsigned integers) for BSON. + +[UBJSON](../features/binary_formats/ubjson.md) and [BJData](../features/binary_formats/bjdata.md) never throw this +exception, because they serialize integers beyond 64 bits as high-precision numbers. + +!!! note "Former occurrences" + + The UBJSON and BSON writers used to throw this exception as well: - Since version 3.9.0, integer numbers beyond int64 are serialized as high-precision UBJSON numbers. - Since version 3.12.0, integer numbers beyond int64 are serialized as uint64 BSON numbers. + Before version 3.13.0, CBOR, MessagePack, and BSON silently truncated integers wider than 64 bits, and BJData + truncated unsigned integers wider than 64 bits. + +!!! failure "Example messages" + + ``` + integer number 18446744073709551615 cannot be represented by BON8 as it does not fit int64 + ``` + ``` + integer number 1267650600228229401496703205376 cannot be represented by CBOR as it does not fit [-2^64, 2^64-1] + ``` + ### json.exception.out_of_range.408 The size of an array or object in a [binary format](../features/binary_formats/index.md) exceeds the maximal capacity: @@ -1044,6 +1165,34 @@ MessagePack's ext type and BSON's binary subtype are each stored in a single byt This exception was added in version 3.13.0. Before that, subtypes above 255 were silently truncated modulo 256 instead of raising an error. +### json.exception.out_of_range.416 + +[`basic_json_document::parse()`](../api/basic_json_document/parse.md) and the other parsing functions of +[`basic_json_document`](../api/basic_json_document/index.md) index a value's position in the source text in 32 bits, +so they do not support an input of 4294967280 bytes (4 GiB minus 16 bytes) or more. The same 32-bit limit applies to an **editable** document's own +storage: [`set`](../api/basic_json_document/set.md) and [`push_back`](../api/basic_json_document/push_back.md) throw +this exception once the strings and number tokens written by edits reach 4 GiB in total, or once more than +4294967295 arrays/objects have had an element set or appended to them. The same limit applies to an +[image](../features/json_view.md#images): [`save()`](../api/basic_json_document/save.md) throws it if the node +count, the text, or the decoded strings it would write would individually reach 4 GiB. + +!!! failure "Example messages" + + ``` + [json.exception.out_of_range.416] input of 4294967280 bytes or more is not supported by json_document + ``` + ``` + [json.exception.out_of_range.416] edits of 4 GiB or more are not supported by json_document + ``` + ``` + [json.exception.out_of_range.416] images of 4 GiB or more are not supported by json_document + ``` + +!!! note + + This exception was added in version 3.13.0, together with [``](../features/json_view.md). + [`basic_json::parse()`](../api/basic_json/parse.md) has no such limit. + ## Further exceptions This exception is thrown in case of errors that cannot be classified with the diff --git a/docs/mkdocs/docs/home/faq.md b/docs/mkdocs/docs/home/faq.md index eec13a481..d52bdc006 100644 --- a/docs/mkdocs/docs/home/faq.md +++ b/docs/mkdocs/docs/home/faq.md @@ -58,6 +58,208 @@ json j{obj}; // -> {"key":"value"} (copy, not array) Without the macro (default behavior), `json j{obj}` creates `[{"key":"value"}]`. This opt-in macro fixes issue #5074 while preserving backwards compatibility for existing code. +## Getting started + +### Which file do I need? + +!!! question + + Do I have to clone the repository or build the library? + +No. The library is header-only: the single file [`json.hpp`](https://github.com/nlohmann/json/releases) from the +releases page is all you need, including the `_json` literals. Put it in a directory `nlohmann` on your include path. +Companion headers (`json_fwd.hpp`, `json_view.hpp`, `json_literals.hpp`) are only needed if you use them. See +[Integration](../integration/index.md#header-only). + +### Using CMake or a package manager + +!!! question + + How do I add the library with CMake, Conan, vcpkg, Homebrew, or another tool? + +See [CMake](../integration/cmake.md) for `find_package`, `add_subdirectory`, and `FetchContent`, and +[Package managers](../integration/package_managers.md) for the list of supported package managers. + +### The IDE cannot open the header + +!!! question + + The compiler finds `nlohmann/json.hpp`, but my IDE reports "cannot open source file". Why? + +The compiler's include path is set correctly, but the IDE's is not. Add the same directory to the include paths of the +IDE (or of its language server); the build itself is not affected. + +### Compile times + +!!! question + + Including the library slows down my build. What can I do? + +Include [`json_fwd.hpp`](../integration/compile_times.md#include-json_fwdhpp-in-headers) in headers and `json.hpp` only +in the source files that need it, instantiate `basic_json` once, and consider precompiled headers or +[C++20 modules](../features/modules.md). See [Compile times](../integration/compile_times.md). + +## Parsing + +### Parsing a file fails with "unexpected end of input" + +!!! question + + `json::parse(file)` throws `parse_error.101` at line 1, column 1 for a file that is valid JSON. Why? + +The parser did not see any character: the file was most likely not opened. A relative path is resolved against the +working directory of the running program, which is often the build directory of your IDE, not the source directory. +Check `is_open()` and try an absolute path. Other causes are an empty file and a stream that was already read. See +[`parse_error.101`](exceptions.md#jsonexceptionparse_error101). + +### Input that arrives in chunks + +!!! question + + My data comes from a socket in pieces. How do I parse it? + +The parser cannot be resumed. Frame the messages (one value per line, a length prefix) and parse each complete frame, or +buffer the chunks until the value is complete; a `parse_error` at the end of the buffer means the input ended +prematurely. See [Large, streamed, and partial input](../features/parsing/large_and_partial_input.md#input-arriving-in-chunks). + +### Several values in one stream + +!!! question + + How do I read concatenated JSON values or one value per line? + +Read newline-delimited input line by line ([JSON Lines](../features/parsing/json_lines.md)). For concatenated values, use +`operator>>` and check for the end of the stream before each read, as shown in +[Several values in one stream](../features/parsing/large_and_partial_input.md#several-values-in-one-stream). + +### Files that are too large for memory + +!!! question + + How do I process a huge JSON file? + +Handle and discard the records of a large array with a [parser callback](../features/parsing/parser_callbacks.md), stop +early with the [SAX interface](../features/parsing/sax_interface.md), or read parts of the text through a +[`json_document`](../features/json_view.md) (added in version 3.13.0). See +[Input too large for memory](../features/parsing/large_and_partial_input.md#input-too-large-for-memory). + +### Position of a parse error + +!!! question + + How do I get the line and column of a parse error? + +The message contains them. The exception stores the byte index in `byte`, from which you can compute both; see +[Where did the error occur?](../features/parsing/parse_exceptions.md#where-did-the-error-occur). To locate a value that +causes a *later* error, such as a `type_error`, use +[`JSON_DIAGNOSTIC_POSITIONS`](../features/parsing/parse_exceptions.md#where-is-the-value-that-caused-a-later-error). + +### Comments and trailing commas + +!!! question + + Can the parser read JSON with comments or trailing commas? + +Yes, when asked to: see [Comments](../features/comments.md) and [Trailing commas](../features/trailing_commas.md). Both +are rejected by default. + +## Accessing and creating values + +### Reading a value into a variable + +!!! question + + How do I get a C++ value out of a `json` value? + +Use [`get()`](../api/basic_json/get.md), for example `#!cpp int age = j["age"].get();`, or an implicit conversion. +`#!cpp auto x = j["age"];` is **not** a conversion: it copies the `json` value. See +[Converting values](../features/conversions.md). + +### Strings are printed with quotes + +!!! question + + `std::cout << j["name"]` prints `"Mary"` with quotes. How do I get the string? + +The value is a `json`, and printing it produces JSON text. Convert it first: `#!cpp j["name"].get()`. See +[Getting a string without quotes](../features/conversions.md#getting-values-out). + +### Testing whether a key exists + +!!! question + + How do I check if a key exists, without changing the object? + +Use `contains` or `find`. Do not test with the non-const `operator[]`: like `std::map::operator[]`, it inserts a `null` +value for a missing key. See [Does this key exist?](../features/element_access/index.md#does-this-key-exist). + +### Reading a value with a fallback + +!!! question + + How do I read a value that may be missing? + +Use [`value`](../features/element_access/default_value.md), which returns a default for a missing key. It still throws if +the key exists with a different type; see [Working without exceptions](../features/no_exceptions.md). + +### Iterating and nested values + +!!! question + + How do I loop over an array or object, or reach a nested value? + +See [Iterators](../features/iterators.md) and [`items()`](../api/basic_json/items.md) for loops, and +[JSON Pointer](../features/json_pointer.md) to address or test a nested value by path. + +### Unexpected arrays when creating values + +!!! question + + Why does `json j{x}` or `{{"a", 1}, {"b"}}` produce an array instead of what I expected? + +Braces create arrays and objects based on their content, and single-element braces create a one-element array. See +[Brace initialization yields arrays](#brace-initialization-yields-arrays) and the table in +[Creating values](../features/creating_values.md#what-does-this-produce). + +### Keeping the order of object keys + +!!! question + + The keys of my object come out sorted. How do I keep the insertion order? + +Use `ordered_json`, and call `ordered_json::parse` when reading. See [Object order](../features/object_order.md). + +## Converting your own types + +### Structs, enums, and optional fields + +!!! question + + How do I convert my own types to and from JSON? + +Write `to_json`/`from_json` functions or use the `NLOHMANN_DEFINE_TYPE_*` macros; see +[Arbitrary type conversions](../features/arbitrary_types.md). Enums can be mapped to strings +([Enum conversion](../features/enum_conversion.md)), and an empty `std::optional` can be omitted +([Omitting a field](../features/conversions.md#omitting-a-field-when-serializing-stdoptional)). + +### Smart pointers, variants, chrono, and class hierarchies + +!!! question + + Why can't I convert `std::shared_ptr`, `std::variant`, `std::chrono` types, or a pointer to a base class? + +There is more than one sensible JSON representation for them, so the library leaves the choice to you. +[Recipes for common types](../features/type_recipes.md) shows one for each. + +### My `to_json` is not found + +!!! question + + I wrote `to_json`/`from_json`, but the compiler does not find them. Why? + +Typical causes are a type alias of a standard type, a function in the wrong namespace, or a declaration that comes too +late. See [Why isn't my `to_json` found?](../features/arbitrary_types.md#why-isnt-my-to_json-or-from_json-found). + ## Limitations ### Relaxed parsing @@ -66,7 +268,9 @@ Without the macro (default behavior), `json j{obj}` creates `[{"key":"value"}]`. Can you add an option to ignore trailing commas? -This library does not support any feature that would jeopardize interoperability. +Yes, trailing commas in arrays and objects can be allowed by setting parameter `ignore_trailing_commas` of +[`parse`](../api/basic_json/parse.md) to `#!cpp true`; see [trailing commas](../features/trailing_commas.md). They are +rejected by default, and the library never adds them when serializing. ### Parse errors reading non-ASCII characters @@ -79,8 +283,9 @@ This library does not support any feature that would jeopardize interoperability The library supports **Unicode input** as follows: -- Only **UTF-8** encoded input is supported, which is the default encoding for JSON, according to [RFC 8259](https://tools.ietf.org/html/rfc8259.html#section-8.1). -- `std::u16string` and `std::u32string` can be parsed, assuming UTF-16 and UTF-32 encoding, respectively. These encodings are not supported when reading from files or other input containers. +- **UTF-8** encoded input is supported, which is the default encoding for JSON, according to [RFC 8259](https://tools.ietf.org/html/rfc8259.html#section-8.1). +- `std::u16string` and `std::u32string` can be parsed, assuming UTF-16 and UTF-32 encoding, respectively; `std::wstring` works as well and is interpreted as UTF-16 or UTF-32, depending on the width of the platform's `wchar_t`. These encodings cannot be read from a `FILE*` or a `std::istream`, which carry bytes only. +- Ill-formed UTF-16 or UTF-32 input is rejected: a lone surrogate, a high surrogate that is not followed by its low surrogate, and a code point above `U+10FFFF` all yield a parse error. The same holds for iterators over 2- or 4-byte characters. - Other encodings such as Latin-1 or ISO 8859-1 are **not** supported and will yield parse or serialization errors. - The library will not replace [Unicode noncharacters](http://www.unicode.org/faq/private_use.html#nonchar1). - Invalid surrogates (e.g., incomplete pairs such as `\uDEAD`) will yield parse errors. @@ -144,7 +349,7 @@ json::parse(std::string("\"") + '\0' + "\""); // throws parse_error.101: control Why are wide strings (e.g., `std::wstring`) dumped as arrays of numbers? -As described [above](#parse-errors-reading-non-ascii-characters), the library assumes UTF-8 as encoding. To store a wide string, you need to change the encoding. +As described [above](#parse-errors-reading-non-ascii-characters), the library stores its strings as UTF-8. To store a wide string as a string, convert it to a UTF-8 encoded `std::string` first, as in the example below. !!! example @@ -200,6 +405,36 @@ Not directly, but the companion project [json-schema-validator](https://github.c builds JSON Schema (draft 7; draft 4 in its older, now-superseded 1.x releases) validation on top of this library and is a common recommendation for this use case. +### Memory use + +!!! question + + Does the library leak memory? My process stays large after I free a `json` value. + +Destroying a value frees everything it owns. Freed memory is returned to the allocator, which does not necessarily give +it back to the operating system, so a high resident size alone is not a leak. Copies are deep, and `clear()` keeps the +capacity of an array. See [Memory](../features/performance.md#memory), and use Valgrind or the sanitizers to tell a leak +from retained memory. + +### Reproducible builds + +!!! question + + The header path ends up in my binary. How do I get path-independent builds? + +The runtime assertions embed `__FILE__`. Define `NDEBUG` (the default for release builds) or map the path prefix; see +[Notes for builds and distribution](../integration/index.md#notes-for-builds-and-distribution). + +### Slow debug builds + +!!! question + + Parsing a large file is very slow in my debug build. Can I compile the library with optimizations? + +The library is header-only, so there is no separate source file. Keep the code that uses `json` in its own source file and +compile that file with optimizations; see +[Notes for builds and distribution](../integration/index.md#notes-for-builds-and-distribution). + ## Exceptions ### Parsing without exceptions @@ -208,7 +443,8 @@ and is a common recommendation for this use case. Is it possible to indicate a parse error without throwing an exception? -Yes, see [Parsing and exceptions](../features/parsing/parse_exceptions.md). +Yes, see [Parsing and exceptions](../features/parsing/parse_exceptions.md) and +[Working without exceptions](../features/no_exceptions.md). ### Key name in exceptions @@ -230,11 +466,11 @@ Yes, you can. Please define the symbol [`JSON_DIAGNOSTICS`](../api/macros/json_d - It seems that precision is lost when serializing a double. - Can I change the precision for floating-point serialization? -The library uses `std::numeric_limits::digits10` (15 for IEEE `double`s) digits for serialization. This value is sufficient to guarantee roundtripping. If one uses more than this number of digits of precision, then string -> value -> string is not guaranteed to round-trip. +The library serializes floating-point numbers with the shortest representation that parses back to the same value, using at most `std::numeric_limits::max_digits10` (17 for IEEE `double`s) significant digits. This guarantees that value -> string -> value round-trips. The reverse direction, string -> value -> string, is only guaranteed for numbers with at most `std::numeric_limits::digits10` (15 for IEEE `double`s) significant digits; a number with more digits may not be representable exactly and is then serialized as the closest `double`. -!!! quote "[cppreference.com](https://en.cppreference.com/w/cpp/types/numeric_limits/digits10)" +!!! quote "[cppreference.com](https://en.cppreference.com/w/cpp/types/numeric_limits/max_digits10)" - The value of `std::numeric_limits::digits10` is the number of base-10 digits that can be represented by the type T without change, that is, any number with this many significant decimal digits can be converted to a value of type T and back to decimal form, without change due to rounding or overflow. + The value of `std::numeric_limits::max_digits10` is the number of base-10 digits that are necessary to uniquely represent all distinct values of the type T, such as necessary for serialization/deserialization to text. !!! tip diff --git a/docs/mkdocs/docs/home/license.md b/docs/mkdocs/docs/home/license.md index 11d0ec529..714e971ed 100644 --- a/docs/mkdocs/docs/home/license.md +++ b/docs/mkdocs/docs/home/license.md @@ -18,6 +18,12 @@ The class contains the UTF-8 Decoder from Bjoern Hoehrmann which is licensed und The class contains a slightly modified version of the Grisu2 algorithm from Florian Loitsch which is licensed under the [MIT License](https://opensource.org/licenses/MIT) (see above). Copyright © 2009 [Florian Loitsch](https://florian.loitsch.com/) +The class contains a port of the shortest double-to-decimal conversion of [Żmij](https://github.com/vitaut/zmij) by Victor Zverovich, including the conversion of the digits to text by Xiang JunBo and the SIMD instruction sequence of Dougall Johnson, which is licensed under the [MIT License](https://opensource.org/licenses/MIT) (see above). Copyright © 2025 [Victor Zverovich](https://github.com/vitaut) + The class contains a copy of [Hedley](https://nemequ.github.io/hedley/) from Evan Nemerson which is licensed as [CC0-1.0](https://creativecommons.org/publicdomain/zero/1.0/). -The class contains an adapted version of the Eisel-Lemire algorithm and its table of powers of five from [fast_float](https://github.com/fastfloat/fast_float) by Daniel Lemire and contributors, which is available under the [MIT License](https://opensource.org/licenses/MIT) (used here), the Apache 2.0 License, and the Boost Software License. Copyright © 2021 The fast_float authors +The class contains an adapted version of the Eisel-Lemire algorithm, its table of powers of five, and its digit comparison for long numbers from [fast_float](https://github.com/fastfloat/fast_float) by Daniel Lemire and contributors, which is available under the [MIT License](https://opensource.org/licenses/MIT) (used here), the Apache 2.0 License, and the Boost Software License. Copyright © 2021 The fast_float authors + +The view's parser (``) contains techniques and code adapted from [yyjson](https://github.com/ibireme/yyjson) by YaoYuan, which is licensed under the [MIT License](https://opensource.org/licenses/MIT) (see above): table-driven decoding of `\u` escapes and fixed-offset unrolled checks. + +The view's parser (``) validates non-ASCII strings with the vector UTF-8 check of [simdjson](https://github.com/simdjson/simdjson) by Daniel Lemire, Geoff Langdale, John Keiser, and contributors (its "lookup4" algorithm and tables, after J. Keiser and D. Lemire, "Validating UTF-8 In Less Than One Instruction Per Byte", 2021), which is available under the [MIT License](https://opensource.org/licenses/MIT) (used here) and the Apache 2.0 License. Copyright © 2018-2025 The simdjson authors diff --git a/docs/mkdocs/docs/home/releases.md b/docs/mkdocs/docs/home/releases.md deleted file mode 100644 index f91f82dda..000000000 --- a/docs/mkdocs/docs/home/releases.md +++ /dev/null @@ -1,444 +0,0 @@ -# Releases - -This page summarizes the notable changes of every release and links to the relevant documentation. -The **complete release notes** — including all changes, the download files, and their checksums — are -published on the [GitHub releases page](https://github.com/nlohmann/json/releases). - -!!! info "Unreleased changes" - - This documentation is built from the `develop` branch and may describe changes that are not part of a release - yet. Their version numbers are followed by an unreleased badge. - -## v3.12.0 (2025-04-11) - -Fixes bugs found in 3.11.3 and adds several features. All changes are backward-compatible. - -- Adds diagnostic byte positions via [`JSON_DIAGNOSTIC_POSITIONS`](../api/macros/json_diagnostic_positions.md), - exposed through the new [`start_pos`](../api/basic_json/start_pos.md) and - [`end_pos`](../api/basic_json/end_pos.md) member functions. -- Makes the [conversion macros](../features/arbitrary_types.md#simplify-your-life-with-macros) - templated (so they also work with [`ordered_json`](../api/ordered_json.md)) and adds - [`NLOHMANN_DEFINE_DERIVED_TYPE`](../api/macros/nlohmann_define_derived_type.md) for derived classes. -- Adds `std::optional` support (C++17) and lets [`patch`](../api/basic_json/patch.md), - [`diff`](../api/basic_json/diff.md), and [`flatten`](../api/basic_json/flatten.md) work with - arbitrary string types. -- Extends the [binary formats](../features/binary_formats/index.md): - [BJData](../features/binary_formats/bjdata.md) draft 3 and unsigned 64-bit integers for - [BSON](../features/binary_formats/bson.md). -- Adds multidimensional C-array conversion and UTF-8 encoded `std::filesystem::path` conversions, and - lowers the minimum [CMake](../integration/cmake.md) version to allow CMake 4.0. - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.12.0). - -## v3.11.3 (2023-11-28) - -Adds features and fixes bugs found in 3.11.2. All changes are backward-compatible. - -- Adds a [custom base class](../api/basic_json/json_base_class_t.md) as a node customization point. -- Adds serialization-only [conversion macros](../features/macros.md) - (`NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE` and `NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE`) - and a clearer parse error for empty input. -- Adds [Bazel](../integration/package_managers.md#bazel) and - [Swift Package Manager](../integration/package_managers.md#swift-package-manager) build support. -- Fixes custom allocators, a memory leak in [`adl_serializer`](../api/adl_serializer/to_json.md)'s - `to_json`, initializer-list construction when `size_type` is not `int`, and many compiler warnings. - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.11.3). - -## v3.11.2 (2022-08-12) - -Fixes bugs found in 3.11.1 and restructures the namespace. All changes are backward-compatible. - -- Fixes the [`value`](../api/basic_json/value.md) function (broken for strings, size types, and - `nullptr` in 3.11.0) and makes `json_fwd.hpp` self-contained. -- Restores using [`json_pointer`](../api/json_pointer/index.md) as a key in associative containers and - comparing it with strings. -- Restructures the inline [namespace](../features/namespace.md) and allows disabling the version - component, and avoids heap allocations in the [BJData](../features/binary_formats/bjdata.md) parser. - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.11.2). - -## v3.11.1 (2022-08-01) - -Fixes a regression from 3.11.0. All changes are backward-compatible. - -- Restores the global [user-defined string literals](../api/macros/json_use_global_udls.md) - [`operator""_json`](../api/operator_literal_json.md) and - [`operator""_json_pointer`](../api/operator_literal_json_pointer.md), which 3.11.0 had moved into a - namespace by default. - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.11.1). - -## v3.11.0 (2022-08-01) - -One of the largest releases ever. All changes are backward-compatible. - -- Allows `std::string_view` as object keys in [`at`](../api/basic_json/at.md), - [`operator[]`](../api/basic_json/operator%5B%5D.md), [`value`](../api/basic_json/value.md), - [`erase`](../api/basic_json/erase.md), [`find`](../api/basic_json/find.md), - [`contains`](../api/basic_json/contains.md), and [`count`](../api/basic_json/count.md). -- Adds the [BJData](../features/binary_formats/bjdata.md) binary format (the fifth supported format). -- Improves C++20 support, including [`operator<=>`](../api/basic_json/operator_spaceship.md) and - ``-compatible iterators. -- Adds a versioned, ABI-tagged inline [namespace](../features/namespace.md) - ([`NLOHMANN_JSON_NAMESPACE`](../api/macros/nlohmann_json_namespace.md)) and the option to move the - UDLs out of the global namespace ([`JSON_USE_GLOBAL_UDLS`](../api/macros/json_use_global_udls.md)). -- Adds [`patch_inplace`](../api/basic_json/patch_inplace.md), default values for the - [conversion macros](../features/arbitrary_types.md#simplify-your-life-with-macros), and an option to - disable enum serialization ([`JSON_DISABLE_ENUM_SERIALIZATION`](../api/macros/json_disable_enum_serialization.md)). - -This release introduced a UDL regression that was fixed in -[3.11.1](https://github.com/nlohmann/json/releases/tag/v3.11.1). -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.11.0). - -## v3.10.5 (2022-01-03) - -Bug-fix release. All changes are backward-compatible. - -- Guards the `std::filesystem` conversions behind compiler-support checks - ([`JSON_HAS_FILESYSTEM`](../api/macros/json_has_filesystem.md)), which can be set to `0` to disable - them altogether. - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.10.5). - -## v3.10.4 (2021-10-16) - -Fixes regressions introduced in 3.10.0. All changes are backward-compatible. - -- Fixes the `std::filesystem::path` conversion (which could trigger a stack overflow and broke - compilation on Windows). -- Fixes compilation for types with an explicit defaulted constructor and for code relying on the - return values of `std::find` and `std::remove`. - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.10.4). - -## v3.10.3 (2021-10-08) - -Fixes more regressions from 3.10.0. All changes are backward-compatible. - -- Fixes [extended-diagnostics](../api/macros/json_diagnostics.md) assertions triggered by - [`update`](../api/basic_json/update.md) and by inserting into arrays. -- Supports custom allocators when writing binary formats into a `std::vector`, and allows conversion - from types that only provide `begin()`/`end()`. - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.10.3). - -## v3.10.2 (2021-08-26) - -Re-release of 3.10.1, whose Git tag pointed at the wrong commit due to a bug in the release script. -All changes are backward-compatible. -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.10.2). - -## v3.10.1 (2021-08-24) - -Fixes a regression from 3.10.0. All changes are backward-compatible. - -- Fixes an [extended-diagnostics](../api/macros/json_diagnostics.md) assertion triggered when used - with [`ordered_json`](../api/ordered_json.md), and hardens the GDB pretty-printer. - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.10.1). - -## v3.10.0 (2021-08-17) - -Feature release. All changes are backward-compatible. - -- Adds [extended diagnostic messages](../api/macros/json_diagnostics.md) - ([`JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md)) that prepend a JSON pointer to exception - messages to pinpoint the offending value. -- Adds a GDB pretty-printer and a [`cbor_tag_handler_t`](../api/basic_json/cbor_tag_handler_t.md) - `store` option to keep CBOR tags as binary subtypes. -- Supports containers with non-default-constructible types and parsing from `std::byte`. -- Adds [`JSON_NO_IO`](../api/macros/json_no_io.md) to exclude the I/O headers and the - [`JSON_HAS_CPP_*`](../api/macros/json_has_cpp_11.md) macros to override the detected C++ standard. - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.10.0). - -## v3.9.1 (2020-08-06) - -Fixes two regressions from 3.9.0. All changes are backward-compatible. - -- Accepts consecutive [comments](../features/comments.md) and completes the - [`ordered_json`](../api/ordered_json.md) interface (e.g. `ordered_json::parse`). - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.9.1). - -## v3.9.0 (2020-07-27) - -Feature release adding four long-requested features. All changes are backward-compatible. - -- Optional [comment](../features/comments.md) parsing in [`parse`](../api/basic_json/parse.md) via the - `ignore_comments` parameter. -- [`ordered_json`](../api/ordered_json.md) to preserve the [insertion order](../features/object_order.md) - of object keys. -- An option to switch off [implicit conversions](../api/macros/json_use_implicit_conversions.md). -- The [`NLOHMANN_DEFINE_TYPE_*`](../features/arbitrary_types.md#simplify-your-life-with-macros) - convenience macros, plus high-precision-number support for - [UBJSON](../features/binary_formats/ubjson.md) and CBOR tag handling. - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.9.0). - -## v3.8.0 (2020-06-14) - -Feature release. All changes are backward-compatible. - -- Introduces a [binary value](../features/binary_values.md) type that is read from and written to - [CBOR](../features/binary_formats/cbor.md), [BSON](../features/binary_formats/bson.md), and - [MessagePack](../features/binary_formats/messagepack.md), and can be shared between formats. -- Generalizes the input adapters to read from any `LegacyInputIterator` container (3–10 % faster - parsing). -- Fixes [`contains`](../api/basic_json/contains.md) for JSON pointers and makes the binary - [`from_cbor`](../api/basic_json/from_cbor.md)/[`from_msgpack`](../api/basic_json/from_msgpack.md)/etc. - functions respect `allow_exceptions`. - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.8.0). - -## v3.7.3 (2019-11-17) - -Fixes a regression from 3.7.2 that could yield quadratic complexity in destructor calls. All changes -are backward-compatible. [Full release notes](https://github.com/nlohmann/json/releases/tag/v3.7.3). - -## v3.7.2 (2019-11-10) - -Fixes a stack overflow for deeply nested input by making the destructor iterative; parsing is now -bounded only by available memory. All changes are backward-compatible. -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.7.2). - -## v3.7.1 (2019-11-06) - -Bug-fix release. All changes are backward-compatible. - -- Fixes a segmentation fault when serializing the `std::int64_t` minimum value and fixes - [`contains`](../api/basic_json/contains.md) for JSON pointers. -- Allows [`items`](../api/basic_json/items.md) with a custom string type and makes `json_pointer::back` - `const`. - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.7.1). - -## v3.7.0 (2019-07-28) - -Convenience features and house-keeping. All changes are backward-compatible. - -- Adds a [`contains`](../api/basic_json/contains.md) overload that checks a JSON pointer without - throwing, a generic `to_string`, and a return value for - [`emplace_back`](../api/basic_json/emplace_back.md). - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.7.0). - -## v3.6.1 (2019-03-20) - -Fixes a regression (GCC 7/8 compilation) and a `` build error introduced in 3.6.0. All -changes are backward-compatible. [Full release notes](https://github.com/nlohmann/json/releases/tag/v3.6.1). - -## v3.6.0 (2019-03-20) - -Feature release. All changes are backward-compatible. - -- Reworks the [JSON pointer](../features/json_pointer.md) interface (`operator/`, `push_back`, - `parent_pointer`, …). -- Adds a [`contains`](../api/basic_json/contains.md) function to test for an object key and greatly - improves the performance of integer serialization. - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.6.0). - -## v3.5.0 (2018-12-22) - -Feature release. All changes are backward-compatible. - -- Adds structured-binding support via the [`items`](../api/basic_json/items.md) function and reading - from `FILE*` in the [`parse`](../api/basic_json/parse.md) function. -- Fixes the `eofbit` handling on input streams and a bug in the BSON SAX parser. - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.5.0). - -## v3.4.0 (2018-10-30) - -Feature release. All changes are backward-compatible. - -- Adds [BSON](../features/binary_formats/bson.md) read/write support. -- Adds configurable Unicode error handlers to [`dump`](../api/basic_json/dump.md) (throw, replace with - U+FFFD, or ignore) and the - [`NLOHMANN_JSON_SERIALIZE_ENUM`](../api/macros/nlohmann_json_serialize_enum.md) macro for - [enum conversion](../features/enum_conversion.md). -- Improves parse-error messages with line/column positions and context. - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.4.0). - -## v3.3.0 (2018-10-05) - -Feature release. All changes are backward-compatible. - -- Adds GCC 4.8 support, the [`get_to`](../api/basic_json/get_to.md) function, and an overhauled and - documented [CMake](../integration/cmake.md) integration. - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.3.0). - -## v3.2.0 (2018-08-20) - -Feature release. All changes are backward-compatible. - -- Adds a [SAX interface](../features/parsing/sax_interface.md) and a non-recursive parser. -- Adds parsing from wide-string types (`std::wstring`, `std::u16string`, `std::u32string`) and - `std::string_view` (C++17), and round-tripping of `std::map`/`std::unordered_map` with non-string - keys. - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.2.0). - -## v3.1.2 (2018-03-14) - -Bug-fix release. All changes are backward-compatible. - -- Fixes a memory leak in the parser callback and adds user-defined string-type support to the parser - and serializer. - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.1.2). - -## v3.1.1 (2018-02-13) - -Bug-fix release. All changes are backward-compatible. - -- Fixes parsing of indefinite-length CBOR strings, a user-defined conversion to vector types, and - overflow detection for UBJSON containers. - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.1.1). - -## v3.1.0 (2018-02-01) - -Feature release. All changes are backward-compatible. - -- Adds [UBJSON](../features/binary_formats/ubjson.md) read/write support and - [JSON Merge Patch](../features/merge_patch.md) via [`merge_patch`](../api/basic_json/merge_patch.md). -- Switches to the Grisu2 algorithm for short, round-trippable floating-point output, and splits the - header into [multiple files](../integration/index.md) with a forward-declaration header. - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.1.0). - -## v3.0.1 (2017-12-29) - -Fixes small issues in the [JSON Pointer](../features/json_pointer.md) and -[JSON Patch](../features/json_patch.md) implementations (invalid "copy" targets and non-integer array -indices). All changes are backward-compatible. -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.0.1). - -## v3.0.0 (2017-12-17) - -First 3.x release — a major release with breaking changes (see the -[migration guide](../integration/migration_guide.md)). - -- Introduces user-defined [exceptions](exceptions.md) (`json::exception` and subtypes, each with an - identifier). -- Adds a non-throwing [`accept`](../api/basic_json/accept.md) function and an `allow_exceptions` flag - for [`parse`](../api/basic_json/parse.md), and an [`update`](../api/basic_json/update.md) function to - merge objects. -- Adds streaming for CBOR and MessagePack and allows storing NaN/infinity. -- Non-UTF-8 strings now throw on serialization, and the iterator category changed to bidirectional. - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.0.0). - -## v2.1.1 (2017-02-25) - -Bug-fix release. All changes are backward-compatible. - -- Makes number parsing and serialization locale-independent with correct floating-point - round-tripping; released files are now GPG-signed. - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v2.1.1). - -## v2.1.0 (2017-01-28) - -Feature release. All changes are backward-compatible. - -- Adds conversions from and to [arbitrary user-defined types](../features/arbitrary_types.md) via - `to_json`/`from_json`, the [`meta`](../api/basic_json/meta.md) function, and the option to switch off - exceptions ([`JSON_NOEXCEPTION`](../api/macros/json_noexception.md)). - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v2.1.0). - -## v2.0.10 (2017-01-02) - -Fixes several security-relevant bugs in the CBOR and MessagePack parsers found by OSS-Fuzz. All -changes are backward-compatible. [Full release notes](https://github.com/nlohmann/json/releases/tag/v2.0.10). - -## v2.0.9 (2016-12-16) - -Adds the [CBOR](../features/binary_formats/cbor.md) and -[MessagePack](../features/binary_formats/messagepack.md) binary formats. All changes are -backward-compatible. [Full release notes](https://github.com/nlohmann/json/releases/tag/v2.0.9). - -## v2.0.8 (2016-12-02) - -Adds the [`emplace`](../api/basic_json/emplace.md) and -[`emplace_back`](../api/basic_json/emplace_back.md) functions and improves parsing and serialization -performance. All changes are backward-compatible. -[Full release notes](https://github.com/nlohmann/json/releases/tag/v2.0.8). - -## v2.0.7 (2016-11-02) - -Fixes several parser bugs found through the "Parsing JSON is a Minefield" study (short files, encoding -detection, surrogate pairs). All changes are backward-compatible. -[Full release notes](https://github.com/nlohmann/json/releases/tag/v2.0.7). - -## v2.0.6 (2016-10-15) - -Fixes [`operator[]`](../api/basic_json/operator%5B%5D.md) for [JSON pointers](../features/json_pointer.md) -so that it creates missing values like the other overloads. All changes are backward-compatible. -[Full release notes](https://github.com/nlohmann/json/releases/tag/v2.0.6). - -## v2.0.5 (2016-09-14) - -Fixes a remaining stream end-of-file detection bug in the parser. All changes are backward-compatible. -[Full release notes](https://github.com/nlohmann/json/releases/tag/v2.0.5). - -## v2.0.4 (2016-09-11) - -Fixes stream end-of-file detection in the parser. All changes are backward-compatible. -[Full release notes](https://github.com/nlohmann/json/releases/tag/v2.0.4). - -## v2.0.3 (2016-08-31) - -Generalizes the parser to accept any contiguous sequence of one-byte elements and deprecates the -input-stream constructor in favor of the [`parse`](../api/basic_json/parse.md) function. All changes -are backward-compatible. [Full release notes](https://github.com/nlohmann/json/releases/tag/v2.0.3). - -## v2.0.2 (2016-07-31) - -Overhauls the parser (now rejecting unescaped control characters), tightens the class invariants, and -cleans up the code. All changes are backward-compatible. -[Full release notes](https://github.com/nlohmann/json/releases/tag/v2.0.2). - -## v2.0.1 (2016-06-28) - -Fixes a performance regression in the [`dump`](../api/basic_json/dump.md) function by adjusting the -stream locale once per serialization. All changes are backward-compatible. -[Full release notes](https://github.com/nlohmann/json/releases/tag/v2.0.1). - -## v2.0.0 (2016-06-24) - -Feature release with a minor (potentially non-backward-compatible) API change from added `noexcept` -and `constexpr` specifiers. - -- Adds [JSON Pointer](../features/json_pointer.md) support in [`at`](../api/basic_json/at.md) and - [`operator[]`](../api/basic_json/operator%5B%5D.md), plus [`flatten`](../api/basic_json/flatten.md) - and [`unflatten`](../api/basic_json/unflatten.md). -- Adds [JSON Patch](../features/json_patch.md) via [`diff`](../api/basic_json/diff.md) and - [`patch`](../api/basic_json/patch.md), unsigned 64-bit integer support, and locale-independent - serialization. - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v2.0.0). - -## v1.1.0 (2016-01-24) - -Bug-fix and feature release. All changes are backward-compatible. - -- Improves floating-point round-tripping, adds a `get_ref` accessor for stored values, and introduces - runtime [assertions](../features/assertions.md). - -[Full release notes](https://github.com/nlohmann/json/releases/tag/v1.1.0). - -## v1.0.0 (2015-12-28) - -First official release. [Full release notes](https://github.com/nlohmann/json/releases/tag/v1.0.0). - -## See also - -- [Migration Guide](../integration/migration_guide.md) — how to future-proof your code for the next - major version and replace deprecated functions. diff --git a/docs/mkdocs/docs/home/releases/index.md b/docs/mkdocs/docs/home/releases/index.md new file mode 100644 index 000000000..e0dbca2d5 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/index.md @@ -0,0 +1,155 @@ +# Releases + +This section summarizes the notable changes of every release and links to the relevant documentation. +The **complete release notes** — including all changes, the download files, and their checksums — are +published on the [GitHub releases page](https://github.com/nlohmann/json/releases). + +!!! info "Unreleased changes" + + This documentation is built from the `develop` branch and may describe changes that are not part of a release + yet. Their version numbers are followed by an unreleased badge. + +Every release has its own page with a summary of its notable changes and an overview of the API that was added or +changed in it. The API overview is generated from the "Version history" section of the +[API pages](../../api/basic_json/index.md); it is not a change log, but helps to see whether a feature is available in +the version you use. + +## Unreleased + +| Release | Date | Summary | +|---------|------|---------| +| [Unreleased](unreleased.md) | | Changes on the `develop` branch that are not part of a release yet. | + +## Version 3.12.x + +| Release | Date | Summary | +|---------|------|---------| +| [v3.12.0](v3.12.0.md) | 2025-04-11 | Fixes bugs found in 3.11.3 and adds several features. No public API was removed or renamed. | + +## Version 3.11.x + +| Release | Date | Summary | +|---------|------|---------| +| [v3.11.3](v3.11.3.md) | 2023-11-28 | Adds features and fixes bugs found in 3.11.2. All changes are backward-compatible. | +| [v3.11.2](v3.11.2.md) | 2022-08-12 | Fixes bugs found in 3.11.1 and restructures the namespace. All changes are backward-compatible. | +| [v3.11.1](v3.11.1.md) | 2022-08-01 | Fixes a regression from 3.11.0. All changes are backward-compatible. | +| [v3.11.0](v3.11.0.md) | 2022-08-01 | One of the largest releases ever. All changes are backward-compatible. | + +## Version 3.10.x + +| Release | Date | Summary | +|---------|------|---------| +| [v3.10.5](v3.10.5.md) | 2022-01-03 | Bug-fix release. All changes are backward-compatible. | +| [v3.10.4](v3.10.4.md) | 2021-10-16 | Fixes regressions introduced in 3.10.0. All changes are backward-compatible. | +| [v3.10.3](v3.10.3.md) | 2021-10-08 | Fixes more regressions from 3.10.0. All changes are backward-compatible. | +| [v3.10.2](v3.10.2.md) | 2021-08-26 | Re-release of 3.10.1, whose Git tag pointed at the wrong commit due to a bug in the release script. All changes are backward-compatible. | +| [v3.10.1](v3.10.1.md) | 2021-08-24 | Fixes a regression from 3.10.0. All changes are backward-compatible. | +| [v3.10.0](v3.10.0.md) | 2021-08-17 | Feature release. All changes are backward-compatible. | + +## Version 3.9.x + +| Release | Date | Summary | +|---------|------|---------| +| [v3.9.1](v3.9.1.md) | 2020-08-06 | Fixes two regressions from 3.9.0. All changes are backward-compatible. | +| [v3.9.0](v3.9.0.md) | 2020-07-27 | Feature release adding four long-requested features. All changes are backward-compatible. | + +## Version 3.8.x + +| Release | Date | Summary | +|---------|------|---------| +| [v3.8.0](v3.8.0.md) | 2020-06-14 | Feature release. All changes are backward-compatible. | + +## Version 3.7.x + +| Release | Date | Summary | +|---------|------|---------| +| [v3.7.3](v3.7.3.md) | 2019-11-17 | Fixes a regression from 3.7.2 that could yield quadratic complexity in destructor calls. All changes are backward-compatible. | +| [v3.7.2](v3.7.2.md) | 2019-11-10 | Fixes a stack overflow for deeply nested input by making the destructor iterative; parsing of JSON text is now bounded only by available memory (the binary-format parsers still recurse). All changes are backward-compatible. | +| [v3.7.1](v3.7.1.md) | 2019-11-06 | Bug-fix release. All changes are backward-compatible. | +| [v3.7.0](v3.7.0.md) | 2019-07-28 | Convenience features and house-keeping. All changes are backward-compatible. | + +## Version 3.6.x + +| Release | Date | Summary | +|---------|------|---------| +| [v3.6.1](v3.6.1.md) | 2019-03-20 | Fixes a regression (GCC 7/8 compilation) and a `` build error introduced in 3.6.0. All changes are backward-compatible. | +| [v3.6.0](v3.6.0.md) | 2019-03-20 | Feature release. All changes are backward-compatible. | + +## Version 3.5.x + +| Release | Date | Summary | +|---------|------|---------| +| [v3.5.0](v3.5.0.md) | 2018-12-22 | Feature release. All changes are backward-compatible. | + +## Version 3.4.x + +| Release | Date | Summary | +|---------|------|---------| +| [v3.4.0](v3.4.0.md) | 2018-10-30 | Feature release. All changes are backward-compatible. | + +## Version 3.3.x + +| Release | Date | Summary | +|---------|------|---------| +| [v3.3.0](v3.3.0.md) | 2018-10-05 | Feature release. All changes are backward-compatible. | + +## Version 3.2.x + +| Release | Date | Summary | +|---------|------|---------| +| [v3.2.0](v3.2.0.md) | 2018-08-20 | Feature release. All changes are backward-compatible. | + +## Version 3.1.x + +| Release | Date | Summary | +|---------|------|---------| +| [v3.1.2](v3.1.2.md) | 2018-03-14 | Bug-fix release. All changes are backward-compatible. | +| [v3.1.1](v3.1.1.md) | 2018-02-13 | Bug-fix release. All changes are backward-compatible. | +| [v3.1.0](v3.1.0.md) | 2018-02-01 | Feature release. All changes are backward-compatible. | + +## Version 3.0.x + +| Release | Date | Summary | +|---------|------|---------| +| [v3.0.1](v3.0.1.md) | 2017-12-29 | Fixes small issues in the [JSON Pointer](../../features/json_pointer.md) and [JSON Patch](../../features/json_patch.md) implementations (invalid "copy" targets and non-integer array indices). All changes are backward-compatible. | +| [v3.0.0](v3.0.0.md) | 2017-12-17 | First 3.x release — a major release with breaking changes (see the [migration guide](../../integration/migration_guide.md)). | + +## Version 2.1.x + +| Release | Date | Summary | +|---------|------|---------| +| [v2.1.1](v2.1.1.md) | 2017-02-25 | Bug-fix release. All changes are backward-compatible. | +| [v2.1.0](v2.1.0.md) | 2017-01-28 | Feature release. All changes are backward-compatible. | + +## Version 2.0.x + +| Release | Date | Summary | +|---------|------|---------| +| [v2.0.10](v2.0.10.md) | 2017-01-02 | Fixes several security-relevant bugs in the CBOR and MessagePack parsers found by OSS-Fuzz. All changes are backward-compatible. | +| [v2.0.9](v2.0.9.md) | 2016-12-16 | Adds the [CBOR](../../features/binary_formats/cbor.md) and [MessagePack](../../features/binary_formats/messagepack.md) binary formats. All changes are backward-compatible. | +| [v2.0.8](v2.0.8.md) | 2016-12-02 | Adds the [`emplace`](../../api/basic_json/emplace.md) and [`emplace_back`](../../api/basic_json/emplace_back.md) functions and improves parsing and serialization performance. All changes are backward-compatible. | +| [v2.0.7](v2.0.7.md) | 2016-11-02 | Fixes several parser bugs found through the "Parsing JSON is a Minefield" study (short files, encoding detection, surrogate pairs). All changes are backward-compatible. | +| [v2.0.6](v2.0.6.md) | 2016-10-15 | Fixes [`operator[]`](../../api/basic_json/operator%5B%5D.md) for [JSON pointers](../../features/json_pointer.md) so that it creates missing values like the other overloads. All changes are backward-compatible. | +| [v2.0.5](v2.0.5.md) | 2016-09-14 | Fixes a remaining stream end-of-file detection bug in the parser. All changes are backward-compatible. | +| [v2.0.4](v2.0.4.md) | 2016-09-11 | Fixes stream end-of-file detection in the parser. All changes are backward-compatible. | +| [v2.0.3](v2.0.3.md) | 2016-08-31 | Generalizes the parser to accept any contiguous sequence of one-byte elements and deprecates the input-stream constructor in favor of the [`parse`](../../api/basic_json/parse.md) function. All changes are backward-compatible. | +| [v2.0.2](v2.0.2.md) | 2016-07-31 | Overhauls the parser (now rejecting unescaped control characters), tightens the class invariants, and cleans up the code. All changes are backward-compatible. | +| [v2.0.1](v2.0.1.md) | 2016-06-28 | Fixes a performance regression in the [`dump`](../../api/basic_json/dump.md) function by adjusting the stream locale once per serialization. All changes are backward-compatible. | +| [v2.0.0](v2.0.0.md) | 2016-06-24 | Feature release with a minor (potentially non-backward-compatible) API change from added `noexcept` and `constexpr` specifiers. | + +## Version 1.1.x + +| Release | Date | Summary | +|---------|------|---------| +| [v1.1.0](v1.1.0.md) | 2016-01-24 | Bug-fix and feature release. All changes are backward-compatible. | + +## Version 1.0.x + +| Release | Date | Summary | +|---------|------|---------| +| [v1.0.0](v1.0.0.md) | 2015-12-28 | First official release. | + +## See also + +- [Migration Guide](../../integration/migration_guide.md) — how to future-proof your code for the next + major version and replace deprecated functions. diff --git a/docs/mkdocs/docs/home/releases/unreleased.md b/docs/mkdocs/docs/home/releases/unreleased.md new file mode 100644 index 000000000..b27da9748 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/unreleased.md @@ -0,0 +1,14 @@ +# Unreleased + +These changes are part of the `develop` branch, but not of a release yet. + +- Converting an rvalue container to JSON (`json j = std::move(vec);`, `json j = std::map{...};`) + now **moves** the elements instead of copying them (e.g., `std::vector` with 100,000 strings: 4.7 ms to + 2.3 ms). Two consequences: the source container is left in a valid but unspecified (moved-from) state, and if an + exception is thrown during the conversion, the source may be partially moved-from, so the strong exception guarantee + no longer holds for the source of an rvalue conversion (the JSON value itself is unaffected). Lvalues and `const` + objects are still copied. See [`basic_json`](../../api/basic_json/basic_json.md). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v1.0.0.md b/docs/mkdocs/docs/home/releases/v1.0.0.md new file mode 100644 index 000000000..dde38ca57 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v1.0.0.md @@ -0,0 +1,9 @@ +# v1.0.0 + +Released on 2015-12-28. + +First official release. [Full release notes](https://github.com/nlohmann/json/releases/tag/v1.0.0). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v1.1.0.md b/docs/mkdocs/docs/home/releases/v1.1.0.md new file mode 100644 index 000000000..1bdff4b59 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v1.1.0.md @@ -0,0 +1,14 @@ +# v1.1.0 + +Released on 2016-01-24. + +Bug-fix and feature release. All changes are backward-compatible. + +- Improves floating-point round-tripping, adds a `get_ref` accessor for stored values, and introduces + runtime [assertions](../../features/assertions.md). + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v1.1.0). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v2.0.0.md b/docs/mkdocs/docs/home/releases/v2.0.0.md new file mode 100644 index 000000000..540593592 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v2.0.0.md @@ -0,0 +1,19 @@ +# v2.0.0 + +Released on 2016-06-24. + +Feature release with a minor (potentially non-backward-compatible) API change from added `noexcept` +and `constexpr` specifiers. + +- Adds [JSON Pointer](../../features/json_pointer.md) support in [`at`](../../api/basic_json/at.md) and + [`operator[]`](../../api/basic_json/operator%5B%5D.md), plus [`flatten`](../../api/basic_json/flatten.md) + and [`unflatten`](../../api/basic_json/unflatten.md). +- Adds [JSON Patch](../../features/json_patch.md) via [`diff`](../../api/basic_json/diff.md) and + [`patch`](../../api/basic_json/patch.md), unsigned 64-bit integer support, and locale-independent + serialization. + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v2.0.0). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v2.0.1.md b/docs/mkdocs/docs/home/releases/v2.0.1.md new file mode 100644 index 000000000..8d2820f14 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v2.0.1.md @@ -0,0 +1,11 @@ +# v2.0.1 + +Released on 2016-06-28. + +Fixes a performance regression in the [`dump`](../../api/basic_json/dump.md) function by adjusting the +stream locale once per serialization. All changes are backward-compatible. +[Full release notes](https://github.com/nlohmann/json/releases/tag/v2.0.1). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v2.0.10.md b/docs/mkdocs/docs/home/releases/v2.0.10.md new file mode 100644 index 000000000..56e390f44 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v2.0.10.md @@ -0,0 +1,10 @@ +# v2.0.10 + +Released on 2017-01-02. + +Fixes several security-relevant bugs in the CBOR and MessagePack parsers found by OSS-Fuzz. All +changes are backward-compatible. [Full release notes](https://github.com/nlohmann/json/releases/tag/v2.0.10). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v2.0.2.md b/docs/mkdocs/docs/home/releases/v2.0.2.md new file mode 100644 index 000000000..4824db4a9 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v2.0.2.md @@ -0,0 +1,11 @@ +# v2.0.2 + +Released on 2016-07-31. + +Overhauls the parser (now rejecting unescaped control characters), tightens the class invariants, and +cleans up the code. All changes are backward-compatible. +[Full release notes](https://github.com/nlohmann/json/releases/tag/v2.0.2). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v2.0.3.md b/docs/mkdocs/docs/home/releases/v2.0.3.md new file mode 100644 index 000000000..66182cb1d --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v2.0.3.md @@ -0,0 +1,11 @@ +# v2.0.3 + +Released on 2016-08-31. + +Generalizes the parser to accept any contiguous sequence of one-byte elements and deprecates the +input-stream constructor in favor of the [`parse`](../../api/basic_json/parse.md) function. All changes +are backward-compatible. [Full release notes](https://github.com/nlohmann/json/releases/tag/v2.0.3). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v2.0.4.md b/docs/mkdocs/docs/home/releases/v2.0.4.md new file mode 100644 index 000000000..b7cebb942 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v2.0.4.md @@ -0,0 +1,10 @@ +# v2.0.4 + +Released on 2016-09-11. + +Fixes stream end-of-file detection in the parser. All changes are backward-compatible. +[Full release notes](https://github.com/nlohmann/json/releases/tag/v2.0.4). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v2.0.5.md b/docs/mkdocs/docs/home/releases/v2.0.5.md new file mode 100644 index 000000000..2f383efc9 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v2.0.5.md @@ -0,0 +1,10 @@ +# v2.0.5 + +Released on 2016-09-14. + +Fixes a remaining stream end-of-file detection bug in the parser. All changes are backward-compatible. +[Full release notes](https://github.com/nlohmann/json/releases/tag/v2.0.5). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v2.0.6.md b/docs/mkdocs/docs/home/releases/v2.0.6.md new file mode 100644 index 000000000..cfee027fb --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v2.0.6.md @@ -0,0 +1,11 @@ +# v2.0.6 + +Released on 2016-10-15. + +Fixes [`operator[]`](../../api/basic_json/operator%5B%5D.md) for [JSON pointers](../../features/json_pointer.md) +so that it creates missing values like the other overloads. All changes are backward-compatible. +[Full release notes](https://github.com/nlohmann/json/releases/tag/v2.0.6). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v2.0.7.md b/docs/mkdocs/docs/home/releases/v2.0.7.md new file mode 100644 index 000000000..3d8069ae6 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v2.0.7.md @@ -0,0 +1,11 @@ +# v2.0.7 + +Released on 2016-11-02. + +Fixes several parser bugs found through the "Parsing JSON is a Minefield" study (short files, encoding +detection, surrogate pairs). All changes are backward-compatible. +[Full release notes](https://github.com/nlohmann/json/releases/tag/v2.0.7). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v2.0.8.md b/docs/mkdocs/docs/home/releases/v2.0.8.md new file mode 100644 index 000000000..177e335a5 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v2.0.8.md @@ -0,0 +1,12 @@ +# v2.0.8 + +Released on 2016-12-02. + +Adds the [`emplace`](../../api/basic_json/emplace.md) and +[`emplace_back`](../../api/basic_json/emplace_back.md) functions and improves parsing and serialization +performance. All changes are backward-compatible. +[Full release notes](https://github.com/nlohmann/json/releases/tag/v2.0.8). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v2.0.9.md b/docs/mkdocs/docs/home/releases/v2.0.9.md new file mode 100644 index 000000000..2587c790e --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v2.0.9.md @@ -0,0 +1,11 @@ +# v2.0.9 + +Released on 2016-12-16. + +Adds the [CBOR](../../features/binary_formats/cbor.md) and +[MessagePack](../../features/binary_formats/messagepack.md) binary formats. All changes are +backward-compatible. [Full release notes](https://github.com/nlohmann/json/releases/tag/v2.0.9). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v2.1.0.md b/docs/mkdocs/docs/home/releases/v2.1.0.md new file mode 100644 index 000000000..3e42cbd32 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v2.1.0.md @@ -0,0 +1,15 @@ +# v2.1.0 + +Released on 2017-01-28. + +Feature release. All changes are backward-compatible. + +- Adds conversions from and to [arbitrary user-defined types](../../features/arbitrary_types.md) via + `to_json`/`from_json`, the [`meta`](../../api/basic_json/meta.md) function, and the option to switch off + exceptions ([`JSON_NOEXCEPTION`](../../api/macros/json_noexception.md)). + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v2.1.0). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v2.1.1.md b/docs/mkdocs/docs/home/releases/v2.1.1.md new file mode 100644 index 000000000..7167dcd16 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v2.1.1.md @@ -0,0 +1,14 @@ +# v2.1.1 + +Released on 2017-02-25. + +Bug-fix release. All changes are backward-compatible. + +- Makes number parsing and serialization locale-independent with correct floating-point + round-tripping; commits and released files are GPG-signed. + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v2.1.1). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.0.0.md b/docs/mkdocs/docs/home/releases/v3.0.0.md new file mode 100644 index 000000000..cea928ee1 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.0.0.md @@ -0,0 +1,20 @@ +# v3.0.0 + +Released on 2017-12-17. + +First 3.x release — a major release with breaking changes (see the +[migration guide](../../integration/migration_guide.md)). + +- Introduces user-defined [exceptions](../exceptions.md) (`json::exception` and subtypes, each with an + identifier). +- Adds a non-throwing [`accept`](../../api/basic_json/accept.md) function and an `allow_exceptions` flag + for [`parse`](../../api/basic_json/parse.md), and an [`update`](../../api/basic_json/update.md) function to + merge objects. +- Adds streaming for CBOR and MessagePack and allows storing NaN/infinity. +- Non-UTF-8 strings now throw on serialization, and the iterator category changed to bidirectional. + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.0.0). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.0.1.md b/docs/mkdocs/docs/home/releases/v3.0.1.md new file mode 100644 index 000000000..95c6141cf --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.0.1.md @@ -0,0 +1,12 @@ +# v3.0.1 + +Released on 2017-12-29. + +Fixes small issues in the [JSON Pointer](../../features/json_pointer.md) and +[JSON Patch](../../features/json_patch.md) implementations (invalid "copy" targets and non-integer array +indices). All changes are backward-compatible. +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.0.1). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.1.0.md b/docs/mkdocs/docs/home/releases/v3.1.0.md new file mode 100644 index 000000000..31428a66f --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.1.0.md @@ -0,0 +1,16 @@ +# v3.1.0 + +Released on 2018-02-01. + +Feature release. All changes are backward-compatible. + +- Adds [UBJSON](../../features/binary_formats/ubjson.md) read/write support and + [JSON Merge Patch](../../features/merge_patch.md) via [`merge_patch`](../../api/basic_json/merge_patch.md). +- Switches to the Grisu2 algorithm for short, round-trippable floating-point output, and splits the + header into [multiple files](../../integration/index.md) with a forward-declaration header. + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.1.0). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.1.1.md b/docs/mkdocs/docs/home/releases/v3.1.1.md new file mode 100644 index 000000000..b7c6aca1f --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.1.1.md @@ -0,0 +1,14 @@ +# v3.1.1 + +Released on 2018-02-13. + +Bug-fix release. All changes are backward-compatible. + +- Fixes parsing of indefinite-length CBOR strings, a user-defined conversion to vector types, and + overflow detection for UBJSON containers. + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.1.1). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.1.2.md b/docs/mkdocs/docs/home/releases/v3.1.2.md new file mode 100644 index 000000000..30917e031 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.1.2.md @@ -0,0 +1,14 @@ +# v3.1.2 + +Released on 2018-03-14. + +Bug-fix release. All changes are backward-compatible. + +- Fixes a memory leak in the parser callback and adds user-defined string-type support to the parser + and serializer. + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.1.2). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.10.0.md b/docs/mkdocs/docs/home/releases/v3.10.0.md new file mode 100644 index 000000000..520e8ab39 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.10.0.md @@ -0,0 +1,20 @@ +# v3.10.0 + +Released on 2021-08-17. + +Feature release. All changes are backward-compatible. + +- Adds [extended diagnostic messages](../../api/macros/json_diagnostics.md) + ([`JSON_DIAGNOSTICS`](../../api/macros/json_diagnostics.md)) that prepend a JSON pointer to exception + messages to pinpoint the offending value. +- Adds a GDB pretty-printer and a [`cbor_tag_handler_t`](../../api/basic_json/cbor_tag_handler_t.md) + `store` option to keep CBOR tags as binary subtypes. +- Supports containers with non-default-constructible types and parsing from `std::byte`. +- Adds [`JSON_NO_IO`](../../api/macros/json_no_io.md) to exclude the I/O headers and the + [`JSON_HAS_CPP_*`](../../api/macros/json_has_cpp_11.md) macros to override the detected C++ standard. + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.10.0). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.10.1.md b/docs/mkdocs/docs/home/releases/v3.10.1.md new file mode 100644 index 000000000..e6881beee --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.10.1.md @@ -0,0 +1,14 @@ +# v3.10.1 + +Released on 2021-08-24. + +Fixes a regression from 3.10.0. All changes are backward-compatible. + +- Fixes an [extended-diagnostics](../../api/macros/json_diagnostics.md) assertion triggered when used + with [`ordered_json`](../../api/ordered_json.md), and hardens the GDB pretty-printer. + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.10.1). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.10.2.md b/docs/mkdocs/docs/home/releases/v3.10.2.md new file mode 100644 index 000000000..c442ac668 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.10.2.md @@ -0,0 +1,11 @@ +# v3.10.2 + +Released on 2021-08-26. + +Re-release of 3.10.1, whose Git tag pointed at the wrong commit due to a bug in the release script. +All changes are backward-compatible. +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.10.2). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.10.3.md b/docs/mkdocs/docs/home/releases/v3.10.3.md new file mode 100644 index 000000000..0c84ccfee --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.10.3.md @@ -0,0 +1,16 @@ +# v3.10.3 + +Released on 2021-10-08. + +Fixes more regressions from 3.10.0. All changes are backward-compatible. + +- Fixes [extended-diagnostics](../../api/macros/json_diagnostics.md) assertions triggered by + [`update`](../../api/basic_json/update.md) and by inserting into arrays. +- Supports custom allocators when writing binary formats into a `std::vector`, and allows conversion + from types that only provide `begin()`/`end()`. + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.10.3). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.10.4.md b/docs/mkdocs/docs/home/releases/v3.10.4.md new file mode 100644 index 000000000..dcb81e38a --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.10.4.md @@ -0,0 +1,16 @@ +# v3.10.4 + +Released on 2021-10-16. + +Fixes regressions introduced in 3.10.0. All changes are backward-compatible. + +- Fixes the `std::filesystem::path` conversion (which could trigger a stack overflow and broke + compilation on Windows). +- Fixes compilation for types with an explicit defaulted constructor and for code relying on the + return values of `std::find` and `std::remove`. + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.10.4). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.10.5.md b/docs/mkdocs/docs/home/releases/v3.10.5.md new file mode 100644 index 000000000..e1c82ba41 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.10.5.md @@ -0,0 +1,15 @@ +# v3.10.5 + +Released on 2022-01-03. + +Bug-fix release. All changes are backward-compatible. + +- Guards the `std::filesystem` conversions behind compiler-support checks + ([`JSON_HAS_FILESYSTEM`](../../api/macros/json_has_filesystem.md)), which can be set to `0` to disable + them altogether. + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.10.5). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.11.0.md b/docs/mkdocs/docs/home/releases/v3.11.0.md new file mode 100644 index 000000000..c25a069a2 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.11.0.md @@ -0,0 +1,27 @@ +# v3.11.0 + +Released on 2022-08-01. + +One of the largest releases ever. All changes are backward-compatible. + +- Allows `std::string_view` as object keys in [`at`](../../api/basic_json/at.md), + [`operator[]`](../../api/basic_json/operator%5B%5D.md), [`value`](../../api/basic_json/value.md), + [`erase`](../../api/basic_json/erase.md), [`find`](../../api/basic_json/find.md), + [`contains`](../../api/basic_json/contains.md), and [`count`](../../api/basic_json/count.md). +- Adds the [BJData](../../features/binary_formats/bjdata.md) binary format (the fifth supported format). +- Improves C++20 support, including [`operator<=>`](../../api/basic_json/operator_spaceship.md) and + ``-compatible iterators. +- Adds a versioned, ABI-tagged inline [namespace](../../features/namespace.md) + ([`NLOHMANN_JSON_NAMESPACE`](../../api/macros/nlohmann_json_namespace.md)) and the option to move the + UDLs out of the global namespace ([`JSON_USE_GLOBAL_UDLS`](../../api/macros/json_use_global_udls.md)). +- Adds [`patch_inplace`](../../api/basic_json/patch_inplace.md), default values for the + [conversion macros](../../features/arbitrary_types.md#simplify-your-life-with-macros), and an option to + disable enum serialization ([`JSON_DISABLE_ENUM_SERIALIZATION`](../../api/macros/json_disable_enum_serialization.md)). + +This release introduced a UDL regression that was fixed in +[3.11.1](https://github.com/nlohmann/json/releases/tag/v3.11.1). +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.11.0). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.11.1.md b/docs/mkdocs/docs/home/releases/v3.11.1.md new file mode 100644 index 000000000..666a8951b --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.11.1.md @@ -0,0 +1,16 @@ +# v3.11.1 + +Released on 2022-08-01. + +Fixes a regression from 3.11.0. All changes are backward-compatible. + +- Restores the global [user-defined string literals](../../api/macros/json_use_global_udls.md) + [`operator""_json`](../../api/operator_literal_json.md) and + [`operator""_json_pointer`](../../api/operator_literal_json_pointer.md), which 3.11.0 had moved into a + namespace by default. + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.11.1). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.11.2.md b/docs/mkdocs/docs/home/releases/v3.11.2.md new file mode 100644 index 000000000..a5f9e7ce3 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.11.2.md @@ -0,0 +1,18 @@ +# v3.11.2 + +Released on 2022-08-12. + +Fixes bugs found in 3.11.1 and restructures the namespace. All changes are backward-compatible. + +- Fixes the [`value`](../../api/basic_json/value.md) function (broken for strings, size types, and + `nullptr` in 3.11.0) and makes `json_fwd.hpp` self-contained. +- Restores using [`json_pointer`](../../api/json_pointer/index.md) as a key in associative containers and + comparing it with strings. +- Restructures the inline [namespace](../../features/namespace.md) and allows disabling the version + component, and avoids heap allocations in the [BJData](../../features/binary_formats/bjdata.md) parser. + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.11.2). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.11.3.md b/docs/mkdocs/docs/home/releases/v3.11.3.md new file mode 100644 index 000000000..238a774c7 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.11.3.md @@ -0,0 +1,20 @@ +# v3.11.3 + +Released on 2023-11-28. + +Adds features and fixes bugs found in 3.11.2. All changes are backward-compatible. + +- Adds a [custom base class](../../api/basic_json/json_base_class_t.md) as a node customization point. +- Adds serialization-only [conversion macros](../../features/macros.md) + (`NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE` and `NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE`) + and a clearer parse error for empty input. +- Adds [Bazel](../../integration/package_managers.md#bazel) and + [Swift Package Manager](../../integration/package_managers.md#swift-package-manager) build support. +- Fixes custom allocators, a memory leak in [`adl_serializer`](../../api/adl_serializer/to_json.md)'s + `to_json`, initializer-list construction when `size_type` is not `int`, and many compiler warnings. + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.11.3). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.12.0.md b/docs/mkdocs/docs/home/releases/v3.12.0.md new file mode 100644 index 000000000..07be55494 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.12.0.md @@ -0,0 +1,26 @@ +# v3.12.0 + +Released on 2025-04-11. + +Fixes bugs found in 3.11.3 and adds several features. No public API was removed or renamed. + +- Adds diagnostic byte positions via [`JSON_DIAGNOSTIC_POSITIONS`](../../api/macros/json_diagnostic_positions.md), + exposed through the new [`start_pos`](../../api/basic_json/start_pos.md) and + [`end_pos`](../../api/basic_json/end_pos.md) member functions. +- Makes the [conversion macros](../../features/arbitrary_types.md#simplify-your-life-with-macros) + templated (so they also work with [`ordered_json`](../../api/ordered_json.md)) and adds + [`NLOHMANN_DEFINE_DERIVED_TYPE`](../../api/macros/nlohmann_define_derived_type.md) for derived classes. +- Adds `std::optional` support (C++17) and lets [`patch`](../../api/basic_json/patch.md), + [`diff`](../../api/basic_json/diff.md), and [`flatten`](../../api/basic_json/flatten.md) work with + arbitrary string types. +- Extends the [binary formats](../../features/binary_formats/index.md): + [BJData](../../features/binary_formats/bjdata.md) draft 3 and unsigned 64-bit integers for + [BSON](../../features/binary_formats/bson.md). +- Adds multidimensional C-array conversion and UTF-8 encoded `std::filesystem::path` conversions, and + raises the minimum [CMake](../../integration/cmake.md) version to 3.5 and supports CMake 4.0. + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.12.0). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.2.0.md b/docs/mkdocs/docs/home/releases/v3.2.0.md new file mode 100644 index 000000000..6e6690254 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.2.0.md @@ -0,0 +1,16 @@ +# v3.2.0 + +Released on 2018-08-20. + +Feature release. All changes are backward-compatible. + +- Adds a [SAX interface](../../features/parsing/sax_interface.md) and a non-recursive parser. +- Adds parsing from wide-string types (`std::wstring`, `std::u16string`, `std::u32string`), conversion to + `std::string_view` (C++17), and round-tripping of `std::map`/`std::unordered_map` with non-string + keys. + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.2.0). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.3.0.md b/docs/mkdocs/docs/home/releases/v3.3.0.md new file mode 100644 index 000000000..e4ee07c84 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.3.0.md @@ -0,0 +1,14 @@ +# v3.3.0 + +Released on 2018-10-05. + +Feature release. All changes are backward-compatible. + +- Adds GCC 4.8 support, the [`get_to`](../../api/basic_json/get_to.md) function, and an overhauled and + documented [CMake](../../integration/cmake.md) integration. + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.3.0). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.4.0.md b/docs/mkdocs/docs/home/releases/v3.4.0.md new file mode 100644 index 000000000..49a4cf595 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.4.0.md @@ -0,0 +1,18 @@ +# v3.4.0 + +Released on 2018-10-30. + +Feature release. All changes are backward-compatible. + +- Adds [BSON](../../features/binary_formats/bson.md) read/write support. +- Adds configurable Unicode error handlers to [`dump`](../../api/basic_json/dump.md) (throw, replace with + U+FFFD, or ignore) and the + [`NLOHMANN_JSON_SERIALIZE_ENUM`](../../api/macros/nlohmann_json_serialize_enum.md) macro for + [enum conversion](../../features/enum_conversion.md). +- Improves parse-error messages with line/column positions and context. + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.4.0). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.5.0.md b/docs/mkdocs/docs/home/releases/v3.5.0.md new file mode 100644 index 000000000..40a14e159 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.5.0.md @@ -0,0 +1,15 @@ +# v3.5.0 + +Released on 2018-12-22. + +Feature release. All changes are backward-compatible. + +- Adds structured-binding support via the [`items`](../../api/basic_json/items.md) function and reading + from `FILE*` in the [`parse`](../../api/basic_json/parse.md) function. +- Fixes the `eofbit` handling on input streams and a bug in the BSON SAX parser. + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.5.0). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.6.0.md b/docs/mkdocs/docs/home/releases/v3.6.0.md new file mode 100644 index 000000000..48aa6e9b9 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.6.0.md @@ -0,0 +1,16 @@ +# v3.6.0 + +Released on 2019-03-20. + +Feature release. All changes are backward-compatible. + +- Reworks the [JSON pointer](../../features/json_pointer.md) interface (`operator/`, `push_back`, + `parent_pointer`, …). +- Adds a [`contains`](../../api/basic_json/contains.md) function to test for an object key and greatly + improves the performance of integer serialization. + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.6.0). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.6.1.md b/docs/mkdocs/docs/home/releases/v3.6.1.md new file mode 100644 index 000000000..c1e8daf47 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.6.1.md @@ -0,0 +1,10 @@ +# v3.6.1 + +Released on 2019-03-20. + +Fixes a regression (GCC 7/8 compilation) and a `` build error introduced in 3.6.0. All +changes are backward-compatible. [Full release notes](https://github.com/nlohmann/json/releases/tag/v3.6.1). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.7.0.md b/docs/mkdocs/docs/home/releases/v3.7.0.md new file mode 100644 index 000000000..c9e7b15b6 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.7.0.md @@ -0,0 +1,15 @@ +# v3.7.0 + +Released on 2019-07-28. + +Convenience features and house-keeping. All changes are backward-compatible. + +- Adds a [`contains`](../../api/basic_json/contains.md) overload that checks a JSON pointer without + throwing, a generic `to_string`, and a return value for + [`emplace_back`](../../api/basic_json/emplace_back.md). + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.7.0). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.7.1.md b/docs/mkdocs/docs/home/releases/v3.7.1.md new file mode 100644 index 000000000..98a85fb68 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.7.1.md @@ -0,0 +1,16 @@ +# v3.7.1 + +Released on 2019-11-06. + +Bug-fix release. All changes are backward-compatible. + +- Fixes a segmentation fault when serializing the `std::int64_t` minimum value and fixes + [`contains`](../../api/basic_json/contains.md) for JSON pointers. +- Allows [`items`](../../api/basic_json/items.md) with a custom string type and makes `json_pointer::back` + `const`. + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.7.1). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.7.2.md b/docs/mkdocs/docs/home/releases/v3.7.2.md new file mode 100644 index 000000000..aea237be8 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.7.2.md @@ -0,0 +1,11 @@ +# v3.7.2 + +Released on 2019-11-10. + +Fixes a stack overflow for deeply nested input by making the destructor iterative; parsing of JSON +text is now bounded only by available memory (the binary-format parsers still recurse). All changes are backward-compatible. +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.7.2). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.7.3.md b/docs/mkdocs/docs/home/releases/v3.7.3.md new file mode 100644 index 000000000..f0a106f39 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.7.3.md @@ -0,0 +1,10 @@ +# v3.7.3 + +Released on 2019-11-17. + +Fixes a regression from 3.7.2 that could yield quadratic complexity in destructor calls. All changes +are backward-compatible. [Full release notes](https://github.com/nlohmann/json/releases/tag/v3.7.3). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.8.0.md b/docs/mkdocs/docs/home/releases/v3.8.0.md new file mode 100644 index 000000000..babd55fac --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.8.0.md @@ -0,0 +1,20 @@ +# v3.8.0 + +Released on 2020-06-14. + +Feature release. All changes are backward-compatible. + +- Introduces a [binary value](../../features/binary_values.md) type that is read from and written to + [CBOR](../../features/binary_formats/cbor.md), [BSON](../../features/binary_formats/bson.md), and + [MessagePack](../../features/binary_formats/messagepack.md), and can be shared between formats. +- Generalizes the input adapters to read from any `LegacyInputIterator` container (3–10 % faster + parsing). +- Fixes [`contains`](../../api/basic_json/contains.md) for JSON pointers and makes the binary + [`from_cbor`](../../api/basic_json/from_cbor.md)/[`from_msgpack`](../../api/basic_json/from_msgpack.md)/etc. + functions respect `allow_exceptions`. + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.8.0). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.9.0.md b/docs/mkdocs/docs/home/releases/v3.9.0.md new file mode 100644 index 000000000..c04668c62 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.9.0.md @@ -0,0 +1,20 @@ +# v3.9.0 + +Released on 2020-07-27. + +Feature release adding four long-requested features. All changes are backward-compatible. + +- Optional [comment](../../features/comments.md) parsing in [`parse`](../../api/basic_json/parse.md) via the + `ignore_comments` parameter. +- [`ordered_json`](../../api/ordered_json.md) to preserve the [insertion order](../../features/object_order.md) + of object keys. +- An option to switch off [implicit conversions](../../api/macros/json_use_implicit_conversions.md). +- The [`NLOHMANN_DEFINE_TYPE_*`](../../features/arbitrary_types.md#simplify-your-life-with-macros) + convenience macros, plus high-precision-number support for + [UBJSON](../../features/binary_formats/ubjson.md) and CBOR tag handling. + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.9.0). + +## API changes + + diff --git a/docs/mkdocs/docs/home/releases/v3.9.1.md b/docs/mkdocs/docs/home/releases/v3.9.1.md new file mode 100644 index 000000000..a06f819d7 --- /dev/null +++ b/docs/mkdocs/docs/home/releases/v3.9.1.md @@ -0,0 +1,14 @@ +# v3.9.1 + +Released on 2020-08-06. + +Fixes two regressions from 3.9.0. All changes are backward-compatible. + +- Accepts consecutive [comments](../../features/comments.md) and completes the + [`ordered_json`](../../api/ordered_json.md) interface (e.g. `ordered_json::parse`). + +[Full release notes](https://github.com/nlohmann/json/releases/tag/v3.9.1). + +## API changes + + diff --git a/docs/mkdocs/docs/images/range-begin-end.svg b/docs/mkdocs/docs/images/range-begin-end.svg index 8e2b2fb6c..99c9f8662 100644 --- a/docs/mkdocs/docs/images/range-begin-end.svg +++ b/docs/mkdocs/docs/images/range-begin-end.svg @@ -16,6 +16,18 @@ version="1.1" inkscape:version="0.48.4 r9939" sodipodi:docname="range-begin-end.svg"> + + ` holds the user-defined string literals on their own, and +`` adds zero-copy views and editable documents. See +[header-only integration](integration/index.md) for details. It is heavily unit-tested with 100% code coverage, checked with Valgrind and the Clang Sanitizers for memory leaks, and continuously fuzz-tested by Google OSS-Fuzz. @@ -18,7 +20,7 @@ Add the single header to your project and use the library like this: #include #include -using json = nlohmann::json; +using nlohmann::json; int main() { @@ -29,6 +31,10 @@ int main() j["name"] = "Niels"; j["list"] = {1, 0, 2}; + // read a value into a C++ variable and use it + double pi = j["pi"].get(); + std::cout << "2 * pi = " << 2 * pi << '\n'; + // serialize with an indent of 4 spaces std::cout << j.dump(4) << '\n'; } @@ -58,7 +64,8 @@ Conda, and more), and pkg-config. --- - Creating, parsing, accessing, and serializing JSON values, JSON Pointer/Patch, binary formats, and more. + Creating, parsing, accessing, and serializing JSON values, JSON Pointer/Patch, binary formats, zero-copy views, + and more. [:octicons-arrow-right-24: Features](features/index.md) @@ -74,7 +81,8 @@ Conda, and more), and pkg-config. --- - The complete reference for `basic_json` and its member functions, types, and related classes. + The complete reference for `basic_json` and its member functions, types, and related classes, as well as for + `json_document` and `json_view`. [:octicons-arrow-right-24: API documentation](api/basic_json/index.md) @@ -92,7 +100,7 @@ Conda, and more), and pkg-config. What changed in each release, with links to the relevant documentation. - [:octicons-arrow-right-24: Releases](home/releases.md) + [:octicons-arrow-right-24: Releases](home/releases/index.md) - :octicons-people-24:{ .lg .middle } __Community__ @@ -108,7 +116,7 @@ Conda, and more), and pkg-config. This documentation is built from the `develop` branch and may describe changes that are not part of a release yet. Their version numbers are followed by an unreleased badge; see - [Releases](home/releases.md) for what shipped in each version. + [Releases](home/releases/index.md) for what shipped in each version. The library is licensed under the [MIT License](home/license.md). The source code, issue tracker, and discussions are on [GitHub](https://github.com/nlohmann/json). diff --git a/docs/mkdocs/docs/integration/bazel/example.cpp b/docs/mkdocs/docs/integration/bazel/example.cpp index 1a7ac4de2..c51a502aa 100644 --- a/docs/mkdocs/docs/integration/bazel/example.cpp +++ b/docs/mkdocs/docs/integration/bazel/example.cpp @@ -2,7 +2,7 @@ #include #include -using json = nlohmann::json; +using nlohmann::json; int main() { diff --git a/docs/mkdocs/docs/integration/build2/example.cpp b/docs/mkdocs/docs/integration/build2/example.cpp index 1a7ac4de2..c51a502aa 100644 --- a/docs/mkdocs/docs/integration/build2/example.cpp +++ b/docs/mkdocs/docs/integration/build2/example.cpp @@ -2,7 +2,7 @@ #include #include -using json = nlohmann::json; +using nlohmann::json; int main() { diff --git a/docs/mkdocs/docs/integration/cget/example.cpp b/docs/mkdocs/docs/integration/cget/example.cpp index 1a7ac4de2..c51a502aa 100644 --- a/docs/mkdocs/docs/integration/cget/example.cpp +++ b/docs/mkdocs/docs/integration/cget/example.cpp @@ -2,7 +2,7 @@ #include #include -using json = nlohmann::json; +using nlohmann::json; int main() { diff --git a/docs/mkdocs/docs/integration/cmake.md b/docs/mkdocs/docs/integration/cmake.md index 9492efe9f..65eed72a8 100644 --- a/docs/mkdocs/docs/integration/cmake.md +++ b/docs/mkdocs/docs/integration/cmake.md @@ -90,13 +90,13 @@ to the following. ### FetchContent -Since CMake v3.11, [FetchContent](https://cmake.org/cmake/help/v3.11/module/FetchContent.html) can be used to +Since CMake v3.14, [FetchContent](https://cmake.org/cmake/help/v3.14/module/FetchContent.html) can be used to automatically download a release as a dependency at configure time. !!! example ```cmake title="CMakeLists.txt" - cmake_minimum_required(VERSION 3.11) + cmake_minimum_required(VERSION 3.14) project(ExampleProject LANGUAGES CXX) include(FetchContent) diff --git a/docs/mkdocs/docs/integration/compile_times.md b/docs/mkdocs/docs/integration/compile_times.md index 0e3be638c..279a00ec7 100644 --- a/docs/mkdocs/docs/integration/compile_times.md +++ b/docs/mkdocs/docs/integration/compile_times.md @@ -14,7 +14,8 @@ that cost, ordered by how much they typically save. Header files that only need to *name* the `json` type — for function declarations, members held by pointer or reference, or friend declarations — can include `` instead of ``. It only -forward-declares `basic_json`, `json`, `ordered_json`, `json_pointer`, and `adl_serializer`. The translation units that +forward-declares `basic_json`, `json_pointer`, `adl_serializer`, and `ordered_map`, and defines the aliases `json` and +`ordered_json`. The translation units that actually use the values then include ``. ```cpp title="person.hpp" diff --git a/docs/mkdocs/docs/integration/conan/example.cpp b/docs/mkdocs/docs/integration/conan/example.cpp index 1a7ac4de2..c51a502aa 100644 --- a/docs/mkdocs/docs/integration/conan/example.cpp +++ b/docs/mkdocs/docs/integration/conan/example.cpp @@ -2,7 +2,7 @@ #include #include -using json = nlohmann::json; +using nlohmann::json; int main() { diff --git a/docs/mkdocs/docs/integration/conda/example.cpp b/docs/mkdocs/docs/integration/conda/example.cpp index 1a7ac4de2..c51a502aa 100644 --- a/docs/mkdocs/docs/integration/conda/example.cpp +++ b/docs/mkdocs/docs/integration/conda/example.cpp @@ -2,7 +2,7 @@ #include #include -using json = nlohmann::json; +using nlohmann::json; int main() { diff --git a/docs/mkdocs/docs/integration/cpm/example.cpp b/docs/mkdocs/docs/integration/cpm/example.cpp index 1a7ac4de2..c51a502aa 100644 --- a/docs/mkdocs/docs/integration/cpm/example.cpp +++ b/docs/mkdocs/docs/integration/cpm/example.cpp @@ -2,7 +2,7 @@ #include #include -using json = nlohmann::json; +using nlohmann::json; int main() { diff --git a/docs/mkdocs/docs/integration/example.cpp b/docs/mkdocs/docs/integration/example.cpp index 1a7ac4de2..c51a502aa 100644 --- a/docs/mkdocs/docs/integration/example.cpp +++ b/docs/mkdocs/docs/integration/example.cpp @@ -2,7 +2,7 @@ #include #include -using json = nlohmann::json; +using nlohmann::json; int main() { diff --git a/docs/mkdocs/docs/integration/homebrew/example.cpp b/docs/mkdocs/docs/integration/homebrew/example.cpp index 1a7ac4de2..c51a502aa 100644 --- a/docs/mkdocs/docs/integration/homebrew/example.cpp +++ b/docs/mkdocs/docs/integration/homebrew/example.cpp @@ -2,7 +2,7 @@ #include #include -using json = nlohmann::json; +using nlohmann::json; int main() { diff --git a/docs/mkdocs/docs/integration/hunter/CMakeLists.txt b/docs/mkdocs/docs/integration/hunter/CMakeLists.txt index 4acc32586..46f249f30 100644 --- a/docs/mkdocs/docs/integration/hunter/CMakeLists.txt +++ b/docs/mkdocs/docs/integration/hunter/CMakeLists.txt @@ -2,8 +2,8 @@ cmake_minimum_required(VERSION 3.15) include("cmake/HunterGate.cmake") HunterGate( - URL "https://github.com/cpp-pm/hunter/archive/v0.23.297.tar.gz" - SHA1 "3319fe6a3b08090df7df98dee75134d68e2ef5a3" + URL "https://github.com/cpp-pm/hunter/archive/v0.26.12.tar.gz" + SHA1 "6498c5d0dec25d7fffb2b0574ebaf265179b894d" ) project(json_example) diff --git a/docs/mkdocs/docs/integration/hunter/example.cpp b/docs/mkdocs/docs/integration/hunter/example.cpp index 1a7ac4de2..c51a502aa 100644 --- a/docs/mkdocs/docs/integration/hunter/example.cpp +++ b/docs/mkdocs/docs/integration/hunter/example.cpp @@ -2,7 +2,7 @@ #include #include -using json = nlohmann::json; +using nlohmann::json; int main() { diff --git a/docs/mkdocs/docs/integration/index.md b/docs/mkdocs/docs/integration/index.md index 846c15fdc..6b461ff99 100644 --- a/docs/mkdocs/docs/integration/index.md +++ b/docs/mkdocs/docs/integration/index.md @@ -37,15 +37,62 @@ file in `single_include/nlohmann` or [released here](https://github.com/nlohmann #include // for convenience -using json = nlohmann::json; +using nlohmann::json; ``` to the files you want to process JSON and set the necessary switches to enable C++11 (e.g., `-std=c++11` for GCC and Clang). +!!! tip "Which file do I download?" + + For a project without a package manager, the library itself, including the `_json` literals, is **one file**: + [`json.hpp`](https://github.com/nlohmann/json/releases/latest/download/json.hpp) from the + [releases page](https://github.com/nlohmann/json/releases). You do not need to clone the repository, build + anything, or link a library. Put the file in a directory named `nlohmann` that is on your include path (for + example `third_party/nlohmann/json.hpp` with `-I third_party`), so that `#include ` works. + Download the companion headers listed below only if you use them: `json_view.hpp` for the + [zero-copy views](../features/json_view.md), `json_fwd.hpp` for forward declarations, and `json_literals.hpp` if + you define `JSON_NO_AUTOMATIC_UDLS`. Keep them in the same `nlohmann` directory. + + Do not copy only parts of the repository's `include/` directory: the headers in it include each other and must be + kept together. If your IDE reports "cannot open source file" while the compiler succeeds, the compiler's include + path is set correctly but the IDE's is not; add the same directory to the IDE's include paths. + You can further use file [`single_include/nlohmann/json_fwd.hpp`](https://github.com/nlohmann/json/blob/develop/single_include/nlohmann/json_fwd.hpp) for forward declarations (see [Compile times](compile_times.md)), and file [`single_include/nlohmann/json_literals.hpp`](https://github.com/nlohmann/json/blob/develop/single_include/nlohmann/json_literals.hpp) for the user-defined string literals if you define [`JSON_NO_AUTOMATIC_UDLS`](../api/macros/json_no_automatic_udls.md). + +For the read-only, non-owning [`basic_json_document`](../api/basic_json_document/index.md)/[`basic_json_view`](../api/basic_json_view/index.md) +types, additionally include +[`single_include/nlohmann/json_view.hpp`](https://github.com/nlohmann/json/blob/develop/single_include/nlohmann/json_view.hpp); +see [Zero-copy JSON views](../features/json_view.md). + +## Notes for builds and distribution + +**Reproducible and path-independent builds.** The runtime assertions use `assert`, which embeds the name of the source +file (`__FILE__`) in the binary. Release builds do not contain them, because they define `NDEBUG`. If you compile with +assertions enabled and need binaries that do not depend on the build directory, either define `NDEBUG` or map the +prefix of the path with the compiler option `-ffile-prefix-map==` (GCC 8 and later, Clang 10 and later). See +[Runtime assertions](../features/assertions.md). + +**There is no separate source file to compile.** The library is header-only, so there is no `.cpp` that could be +built with different options than the code that includes it. It is also not available as a precompiled library. To +compile the library with optimizations in an otherwise unoptimized build (for example to parse large files in a debug +build, which is slow because of the unoptimized code and the checks of the debug standard library), keep the code that +uses `json` in a separate source file and set the options for that file only. [Compile times](compile_times.md) +shows how to [instantiate `basic_json` once](compile_times.md#instantiate-basic_json-once). + +**Debugger visualization.** The Natvis file works with the Visual Studio debugger, including the `cppvsdbg` engine in +Visual Studio Code, but not with debuggers that wrap LLDB, such as `codelldb`; see [Debugging](../home/debugging.md). + +## See also + +- [CMake](cmake.md) - integrate the library into a CMake project +- [Package managers](package_managers.md) +- [pkg-config](pkg-config.md) +- [Compile times](compile_times.md) - reduce the compile time overhead +- [Migration guide](migration_guide.md) - upgrade from an earlier version +- [Releases](../home/releases/index.md) diff --git a/docs/mkdocs/docs/integration/macports/example.cpp b/docs/mkdocs/docs/integration/macports/example.cpp index 1a7ac4de2..c51a502aa 100644 --- a/docs/mkdocs/docs/integration/macports/example.cpp +++ b/docs/mkdocs/docs/integration/macports/example.cpp @@ -2,7 +2,7 @@ #include #include -using json = nlohmann::json; +using nlohmann::json; int main() { diff --git a/docs/mkdocs/docs/integration/meson/example.cpp b/docs/mkdocs/docs/integration/meson/example.cpp index 1a7ac4de2..c51a502aa 100644 --- a/docs/mkdocs/docs/integration/meson/example.cpp +++ b/docs/mkdocs/docs/integration/meson/example.cpp @@ -2,7 +2,7 @@ #include #include -using json = nlohmann::json; +using nlohmann::json; int main() { diff --git a/docs/mkdocs/docs/integration/msys2/example.cpp b/docs/mkdocs/docs/integration/msys2/example.cpp index 1a7ac4de2..c51a502aa 100644 --- a/docs/mkdocs/docs/integration/msys2/example.cpp +++ b/docs/mkdocs/docs/integration/msys2/example.cpp @@ -2,7 +2,7 @@ #include #include -using json = nlohmann::json; +using nlohmann::json; int main() { diff --git a/docs/mkdocs/docs/integration/spack/example.cpp b/docs/mkdocs/docs/integration/spack/example.cpp index 1a7ac4de2..c51a502aa 100644 --- a/docs/mkdocs/docs/integration/spack/example.cpp +++ b/docs/mkdocs/docs/integration/spack/example.cpp @@ -2,7 +2,7 @@ #include #include -using json = nlohmann::json; +using nlohmann::json; int main() { diff --git a/docs/mkdocs/docs/integration/swift/example.cpp b/docs/mkdocs/docs/integration/swift/example.cpp index ad0a06827..33ccbfef0 100644 --- a/docs/mkdocs/docs/integration/swift/example.cpp +++ b/docs/mkdocs/docs/integration/swift/example.cpp @@ -2,7 +2,7 @@ #include #include -using json = nlohmann::json; +using nlohmann::json; int main() { diff --git a/docs/mkdocs/docs/integration/vcpkg/example.cpp b/docs/mkdocs/docs/integration/vcpkg/example.cpp index 1a7ac4de2..c51a502aa 100644 --- a/docs/mkdocs/docs/integration/vcpkg/example.cpp +++ b/docs/mkdocs/docs/integration/vcpkg/example.cpp @@ -2,7 +2,7 @@ #include #include -using json = nlohmann::json; +using nlohmann::json; int main() { diff --git a/docs/mkdocs/docs/integration/xmake/example.cpp b/docs/mkdocs/docs/integration/xmake/example.cpp index 1a7ac4de2..c51a502aa 100644 --- a/docs/mkdocs/docs/integration/xmake/example.cpp +++ b/docs/mkdocs/docs/integration/xmake/example.cpp @@ -2,7 +2,7 @@ #include #include -using json = nlohmann::json; +using nlohmann::json; int main() { diff --git a/docs/mkdocs/hooks/api_changes.py b/docs/mkdocs/hooks/api_changes.py new file mode 100644 index 000000000..94073f534 --- /dev/null +++ b/docs/mkdocs/hooks/api_changes.py @@ -0,0 +1,163 @@ +"""Generate the "API changes" section of the release pages from the version histories of the API pages.""" + +# Every page below docs/api ends in a "Version history" section ("Added in version 3.8.0.", "... since version 3.13.0."). +# This hook collects these entries and replaces the `` marker of each page in home/releases with +# the pages that were added in that release and the changes made to existing pages. The page of a release is named +# after its version (v3.12.0.md); unreleased.md collects all versions newer than the latest release. The result is an +# overview of what changed in the API, not a change log. + +import logging +import os +import posixpath +import re + +log = logging.getLogger("mkdocs.hooks.api_changes") + +_DIR = "home/releases" +_PAGE_VERSION = re.compile(r"v(\d+)\.(\d+)\.(\d+)\.md") +_MARKER = "" +_HISTORY = re.compile(r"^## Version history\n(.*?)(?=^## |\Z)", re.DOTALL | re.MULTILINE) +_TITLE = re.compile(r"^# (.+)$", re.MULTILINE) +_ITEM = re.compile(r"^(?:[-*]|\d+\.)\s+") +_SENTENCE = re.compile(r"(?<=\.)\s+(?=[A-Z`\[])") +_VERSION = re.compile(r"\b(?:version|since|in)\s+(\d+)\.(\d+)\.(\d+)\b", re.IGNORECASE) +_FUTURE = re.compile(r"\bwill\b|\bplanned\b", re.IGNORECASE) # statements about versions that do not exist yet +_NEXT_MAJOR = (4, 0, 0) # versions from here on are announcements, not changes +_ADDED = re.compile(r"^(?:Added|Introduced)\b.*\bversion \d|^Since version \d+\.\d+\.\d+\.?$", re.IGNORECASE) +_LINK = re.compile(r"(\]\()([^)\s]+)(\))") + + +def on_page_markdown(markdown, *, page, config, files): + uri = page.file.src_uri + if posixpath.dirname(uri) != _DIR or _MARKER not in markdown: + return markdown + docs_dir = config["docs_dir"] + entries = _collect(docs_dir) + released = sorted(tuple(map(int, m.groups())) for m in + (_PAGE_VERSION.fullmatch(name) for name in os.listdir(os.path.join(docs_dir, _DIR))) if m) + match = _PAGE_VERSION.fullmatch(posixpath.basename(uri)) + if match: + versions = [tuple(map(int, match.groups()))] + else: # unreleased.md + versions = sorted((v for v in entries if not released or v > released[-1]), reverse=True) + return markdown.replace(_MARKER, _render(uri, [(v, entries[v]) for v in versions if v in entries])) + + +def _title(text, path): + match = _TITLE.search(text) + title = match.group(1) if match else os.path.splitext(os.path.basename(path))[0] + title = re.sub(r"", "", title) + title = title.replace("`", "").replace("\\", "").strip() + return title[len("nlohmann::"):] if title.startswith("nlohmann::") else title + + +def _collect(docs_dir): + """Return {version: {"added": [(title, uri)], "changed": [(title, uri, text)]}}.""" + result = {} + api = os.path.join(docs_dir, "api") + for root, _, names in sorted(os.walk(api)): + for name in sorted(names): + if not name.endswith(".md"): + continue + path = os.path.join(root, name) + uri = posixpath.relpath(path.replace(os.sep, "/"), docs_dir.replace(os.sep, "/")) + with open(path, encoding="utf-8") as source: + text = source.read() + history = _HISTORY.search(text) + if not history: + continue + title = _title(text, path) + found = _sentences(history.group(1)) + # a page is added in the earliest version named by an "Added in" sentence that opens an entry (an entry of + # a numbered list describes one overload; later unnumbered entries extend an existing page) + starts = [v for i, n, first, v, sentence in found if first and _ADDED.match(sentence) and (i == 0 or n)] + added = min(starts) if starts else None + page_added = False + for index, number, first, version, sentence in found: + if version >= _NEXT_MAJOR: + continue + bucket = result.setdefault(version, {"added": [], "changed": []}) + opens = first and _ADDED.match(sentence) and (index == 0 or number) + if opens and version == added: + if not page_added: + bucket["added"].append((title, uri)) + page_added = True + continue + if number and re.fullmatch(r"Added in version [\d.]+\.?", sentence): + sentence = f"Overload {number} added." + elif number: + sentence = f"Overload {number}: {sentence}" + if (title, uri, sentence) not in bucket["changed"]: + bucket["changed"].append((title, uri, sentence)) + return result + + +def _sentences(section): + """Yield (entry index, overload number, opens entry, version, sentence) for every sentence naming a version.""" + found = [] + for index, (number, entry) in enumerate(_entries(section)): + for position, sentence in enumerate(_SENTENCE.split(entry)): + version = _VERSION.search(sentence) + if not version or _FUTURE.search(sentence): + continue + found.append((index, number, position == 0, tuple(int(part) for part in version.groups()), sentence.strip())) + return found + + +def _entries(section): + entries = [] + for line in section.split("\n"): + item = _ITEM.match(line) + if item: + number = re.match(r"\d+", line) + entries.append([number.group(0) if number else None, _ITEM.sub("", line).strip()]) + elif line.strip() and entries and line[0].isspace(): + entries[-1][1] += " " + line.strip() # wrapped list item + elif line.strip(): + break # text after the list, such as an admonition + return [tuple(entry) for entry in entries] + + +def _relink(text, uri, page): + """Rewrite links relative to the API page `uri` so they work from the generated page.""" + def fix(match): + target = match.group(2) + if re.match(r"[a-z]+:|#|/", target): + return match.group(0) + resolved = posixpath.normpath(posixpath.join(posixpath.dirname(uri), target)) + return match.group(1) + posixpath.relpath(resolved, posixpath.dirname(page)) + match.group(3) + return _LINK.sub(fix, text) + + +def _link(title, uri, page): + return f"[`{title}`]({posixpath.relpath(uri, posixpath.dirname(page))})" + + +def _added(pages, page): + """Group the added pages by class: `basic_json`: [`get`](..), ...; pages without a class come first.""" + groups, single = {}, [] + for title, uri in pages: + directory = posixpath.dirname(uri) + if directory == "api": + single.append(_link(title, uri, page)) + elif posixpath.basename(uri) == "index.md": + groups.setdefault(directory, []).insert(0, f"[(overview)]({posixpath.relpath(uri, posixpath.dirname(page))})") + else: + groups.setdefault(directory, []).append(_link(title.split("::")[-1], uri, page)) + lines = [f"- {item}" for item in single] + for directory, items in groups.items(): + lines.append(f"- `{posixpath.basename(directory)}`: " + ", ".join(items)) + return lines + + +def _render(page, entries): + lines = [] + for version, bucket in entries: + if len(entries) > 1: + lines += [f"### Version {'.'.join(map(str, version))}", ""] + if bucket["added"]: + lines += [f"**Added ({len(bucket['added'])} pages):**", ""] + _added(bucket["added"], page) + [""] + if bucket["changed"]: + lines += ["**Changed:**", ""] + lines += [f"- {_link(t, u, page)}: {_relink(x, u, page)}" for t, u, x in bucket["changed"]] + [""] + return "\n".join(lines) if lines else "No documented API changes." diff --git a/docs/mkdocs/hooks/copy_markdown_source.py b/docs/mkdocs/hooks/copy_markdown_source.py index dfdd9cc7b..967796c55 100644 --- a/docs/mkdocs/hooks/copy_markdown_source.py +++ b/docs/mkdocs/hooks/copy_markdown_source.py @@ -2,12 +2,30 @@ # Creates a `.md` sibling of each HTML output (for example, # `features/comments/` becomes `features/comments.md`) so agents and tools can -# fetch the raw Markdown directly instead of parsing rendered HTML. +# fetch the raw Markdown directly instead of parsing rendered HTML. The +# `--8<-- "path"` lines of pymdownx.snippets are expanded with the extension's +# own preprocessor and the settings from mkdocs.yml, so the copies contain the +# included example code and files instead of the include directives. +import io import os -import shutil + +import markdown +from pymdownx.snippets import SnippetMissingError _pages = [] +_snippets = None + + +def on_config(config): + global _snippets + md = markdown.Markdown( + extensions=["pymdownx.snippets"], + extension_configs={"pymdownx.snippets": config["mdx_configs"].get("pymdownx.snippets", {})}, + ) + _snippets = md.preprocessors["snippet"] + _snippets.auto_append = [] # the glossary is only needed to render abbreviations + return config def on_files(files, config): @@ -22,4 +40,11 @@ def on_post_build(config): url = file.url.rstrip("/") target = os.path.join(site_dir, (url or "index") + ".md") os.makedirs(os.path.dirname(target), exist_ok=True) - shutil.copyfile(file.abs_src_path, target) + with io.open(file.abs_src_path, encoding="utf-8", newline="") as source: + text = source.read() + try: + text = "\n".join(_snippets.run(text.split("\n"))) + except (SnippetMissingError, OSError): + pass # keep the include directives; the regular build reports the missing file + with io.open(target, "w", encoding="utf-8", newline="") as copy: + copy.write(text) diff --git a/docs/mkdocs/includes/glossary.md b/docs/mkdocs/includes/glossary.md index 9f2c6329d..b84567b14 100644 --- a/docs/mkdocs/includes/glossary.md +++ b/docs/mkdocs/includes/glossary.md @@ -1,7 +1,9 @@ +*[AArch64]: 64-bit execution state of the Arm architecture +*[ABI]: Application Binary Interface *[ADL]: Argument-dependent lookup -*[API]: Application Programming Interfaces +*[API]: Application Programming Interface *[ASCII]: American Standard Code for Information Interchange *[BDFL]: Benevolent Dictator for Life *[BJData]: Binary JData @@ -10,28 +12,51 @@ *[CBOR]: Concise Binary Object Representation *[CC0]: Creative Commons Zero *[CI]: Continuous Integration +*[CPU]: Central Processing Unit +*[CUDA]: Compute Unified Device Architecture +*[CVE]: Common Vulnerabilities and Exposures *[DOM]: Document Object Model *[EOF]: End of File *[FAQ]: Frequently Asked Questions *[GCC]: GNU Compiler Collection +*[GDB]: GNU Debugger +*[GiB]: Gibibyte (2^30 bytes) *[HTTP]: Hypertext Transfer Protocol *[ICC]: Intel C++ Compiler +*[IDE]: Integrated Development Environment *[IEEE]: Institute of Electrical and Electronics Engineers *[ISO]: International Organization for Standardization +*[IWYU]: Include What You Use *[JSON]: JavaScript Object Notation +*[JSONC]: JSON with Comments +*[JWCC]: JSON With Commas and Comments +*[LLDB]: LLVM Debugger +*[LTS]: Long-Term Support +*[MinGW]: Minimalist GNU for Windows *[MIT]: Massachusetts Institute of Technology -*[MSVC]: Microsoft Visual C++ *[MsgPack]: MessagePack +*[MSVC]: Microsoft Visual C++ +*[NaN]: Not a Number *[NASA]: National Aeronautics and Space Administration *[NDK]: Native Development Kit -*[NaN]: Not a Number +*[NEON]: Arm SIMD instruction set extension +*[NUL]: Null character (U+0000, byte value 0) +*[ODR]: One Definition Rule +*[REUSE]: Software licensing specification by the Free Software Foundation Europe *[RFC]: Request for Comments *[RTTI]: Runtime Type Information *[SAX]: Simple API for XML *[SDK]: Software Development Kit *[SFINAE]: Substitution failure is not an error +*[SGX]: Software Guard Extensions *[SHA]: Secure Hash Algorithm +*[SIMD]: Single Instruction, Multiple Data *[SPDX]: Software Package Data Exchange +*[SSE2]: Streaming SIMD Extensions 2 +*[SSSE3]: Supplemental Streaming SIMD Extensions 3 *[STL]: Standard Template Library *[UBJSON]: Universal Binary JSON +*[UDL]: User-defined literal +*[UDLs]: User-defined literals +*[URL]: Uniform Resource Locator *[UTF]: Unicode Transformation Format diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml index f38d70038..7e1c23aa8 100644 --- a/docs/mkdocs/mkdocs.yml +++ b/docs/mkdocs/mkdocs.yml @@ -51,7 +51,53 @@ nav: - home/license.md - "FAQ": home/faq.md - home/exceptions.md - - home/releases.md + - Releases: + - 'Overview': home/releases/index.md + - 'Unreleased': home/releases/unreleased.md + - 'v3.12.0': home/releases/v3.12.0.md + - 'v3.11.3': home/releases/v3.11.3.md + - 'v3.11.2': home/releases/v3.11.2.md + - 'v3.11.1': home/releases/v3.11.1.md + - 'v3.11.0': home/releases/v3.11.0.md + - 'v3.10.5': home/releases/v3.10.5.md + - 'v3.10.4': home/releases/v3.10.4.md + - 'v3.10.3': home/releases/v3.10.3.md + - 'v3.10.2': home/releases/v3.10.2.md + - 'v3.10.1': home/releases/v3.10.1.md + - 'v3.10.0': home/releases/v3.10.0.md + - 'v3.9.1': home/releases/v3.9.1.md + - 'v3.9.0': home/releases/v3.9.0.md + - 'v3.8.0': home/releases/v3.8.0.md + - 'v3.7.3': home/releases/v3.7.3.md + - 'v3.7.2': home/releases/v3.7.2.md + - 'v3.7.1': home/releases/v3.7.1.md + - 'v3.7.0': home/releases/v3.7.0.md + - 'v3.6.1': home/releases/v3.6.1.md + - 'v3.6.0': home/releases/v3.6.0.md + - 'v3.5.0': home/releases/v3.5.0.md + - 'v3.4.0': home/releases/v3.4.0.md + - 'v3.3.0': home/releases/v3.3.0.md + - 'v3.2.0': home/releases/v3.2.0.md + - 'v3.1.2': home/releases/v3.1.2.md + - 'v3.1.1': home/releases/v3.1.1.md + - 'v3.1.0': home/releases/v3.1.0.md + - 'v3.0.1': home/releases/v3.0.1.md + - 'v3.0.0': home/releases/v3.0.0.md + - 'v2.1.1': home/releases/v2.1.1.md + - 'v2.1.0': home/releases/v2.1.0.md + - 'v2.0.10': home/releases/v2.0.10.md + - 'v2.0.9': home/releases/v2.0.9.md + - 'v2.0.8': home/releases/v2.0.8.md + - 'v2.0.7': home/releases/v2.0.7.md + - 'v2.0.6': home/releases/v2.0.6.md + - 'v2.0.5': home/releases/v2.0.5.md + - 'v2.0.4': home/releases/v2.0.4.md + - 'v2.0.3': home/releases/v2.0.3.md + - 'v2.0.2': home/releases/v2.0.2.md + - 'v2.0.1': home/releases/v2.0.1.md + - 'v2.0.0': home/releases/v2.0.0.md + - 'v1.1.0': home/releases/v1.1.0.md + - 'v1.0.0': home/releases/v1.0.0.md - home/design_goals.md - home/architecture.md - home/customers.md @@ -60,6 +106,7 @@ nav: - Features: - features/index.md - features/arbitrary_types.md + - features/type_recipes.md - Binary Formats: - features/binary_formats/index.md - features/binary_formats/bjdata.md @@ -84,11 +131,13 @@ nav: - features/modifying_values.md - features/modules.md - 'nlohmann Namespace': features/namespace.md + - features/no_exceptions.md - features/object_order.md - features/performance.md - Parsing: - features/parsing/index.md - features/parsing/json_lines.md + - features/parsing/large_and_partial_input.md - features/parsing/parse_exceptions.md - features/parsing/parser_callbacks.md - features/parsing/sax_interface.md @@ -102,6 +151,7 @@ nav: - features/types/index.md - features/types/number_handling.md - features/types/template_parameters.md + - features/json_view.md - Integration: - integration/index.md - integration/migration_guide.md @@ -182,6 +232,7 @@ nav: - 'json_serializer': api/basic_json/json_serializer.md - 'max_size': api/basic_json/max_size.md - 'meta': api/basic_json/meta.md + - 'merge_diff': api/basic_json/merge_diff.md - 'merge_patch': api/basic_json/merge_patch.md - 'number_float_t': api/basic_json/number_float_t.md - 'number_integer_t': api/basic_json/number_integer_t.md @@ -233,6 +284,70 @@ nav: - 'value': api/basic_json/value.md - 'value_t': api/basic_json/value_t.md - 'with_t': api/basic_json/with_t.md + - basic_json_document: + - 'Overview': api/basic_json_document/index.md + - '(Constructor)': api/basic_json_document/basic_json_document.md + - 'accept': api/basic_json_document/accept.md + - 'erase': api/basic_json_document/erase.md + - 'insert': api/basic_json_document/insert.md + - 'is_discarded': api/basic_json_document/is_discarded.md + - 'load': api/basic_json_document/load.md + - 'memory_usage': api/basic_json_document/memory_usage.md + - 'node_count': api/basic_json_document/node_count.md + - 'owns_source': api/basic_json_document/owns_source.md + - 'parse': api/basic_json_document/parse.md + - 'parse_copy': api/basic_json_document/parse_copy.md + - 'push_back': api/basic_json_document/push_back.md + - 'read': api/basic_json_document/read.md + - 'root': api/basic_json_document/root.md + - 'save': api/basic_json_document/save.md + - 'set': api/basic_json_document/set.md + - 'shrink_to_fit': api/basic_json_document/shrink_to_fit.md + - 'source': api/basic_json_document/source.md + - basic_json_view: + - 'Overview': api/basic_json_view/index.md + - '(Constructor)': api/basic_json_view/basic_json_view.md + - 'at': api/basic_json_view/at.md + - 'back': api/basic_json_view/back.md + - 'begin': api/basic_json_view/begin.md + - 'cbegin': api/basic_json_view/cbegin.md + - 'cend': api/basic_json_view/cend.md + - 'contains': api/basic_json_view/contains.md + - 'count': api/basic_json_view/count.md + - 'dump': api/basic_json_view/dump.md + - 'empty': api/basic_json_view/empty.md + - 'end': api/basic_json_view/end.md + - 'find': api/basic_json_view/find.md + - 'front': api/basic_json_view/front.md + - 'get': api/basic_json_view/get.md + - 'get_string': api/basic_json_view/get_string.md + - 'get_to': api/basic_json_view/get_to.md + - 'is_array': api/basic_json_view/is_array.md + - 'is_binary': api/basic_json_view/is_binary.md + - 'is_boolean': api/basic_json_view/is_boolean.md + - 'is_discarded': api/basic_json_view/is_discarded.md + - 'is_null': api/basic_json_view/is_null.md + - 'is_number': api/basic_json_view/is_number.md + - 'is_number_float': api/basic_json_view/is_number_float.md + - 'is_number_integer': api/basic_json_view/is_number_integer.md + - 'is_number_unsigned': api/basic_json_view/is_number_unsigned.md + - 'is_object': api/basic_json_view/is_object.md + - 'is_primitive': api/basic_json_view/is_primitive.md + - 'is_string': api/basic_json_view/is_string.md + - 'is_structured': api/basic_json_view/is_structured.md + - 'items': api/basic_json_view/items.md + - 'materialize': api/basic_json_view/materialize.md + - 'number_format': api/basic_json_view/number_format.md + - 'number_token': api/basic_json_view/number_token.md + - 'operator<<': api/basic_json_view/operator_ltlt.md + - 'operator[]': api/basic_json_view/operator[].md + - 'operator==': api/basic_json_view/operator_eq.md + - 'operator!=': api/basic_json_view/operator_ne.md + - 'size': api/basic_json_view/size.md + - 'source_offset': api/basic_json_view/source_offset.md + - 'type': api/basic_json_view/type.md + - 'type_name': api/basic_json_view/type_name.md + - 'value': api/basic_json_view/value.md - byte_container_with_subtype: - 'Overview': api/byte_container_with_subtype/index.md - '(constructor)': api/byte_container_with_subtype/byte_container_with_subtype.md @@ -247,6 +362,9 @@ nav: - 'from_json': api/adl_serializer/from_json.md - 'to_json': api/adl_serializer/to_json.md - 'json': api/json.md + - 'json_document': api/json_document.md + - 'json_editable_document': api/json_editable_document.md + - 'json_editable_view': api/json_editable_view.md - json_pointer: - 'Overview': api/json_pointer/index.md - '(Constructor)': api/json_pointer/json_pointer.md @@ -281,11 +399,16 @@ nav: - 'start_array': api/json_sax/start_array.md - 'start_object': api/json_sax/start_object.md - 'string': api/json_sax/string.md + - 'json_view': api/json_view.md - 'operator<<(basic_json), operator<<(json_pointer)': api/operator_ltlt.md - 'operator>>(basic_json)': api/operator_gtgt.md - 'operator""_json': api/operator_literal_json.md - 'operator""_json_pointer': api/operator_literal_json_pointer.md - 'ordered_json': api/ordered_json.md + - 'ordered_json_document': api/ordered_json_document.md + - 'ordered_json_editable_document': api/ordered_json_editable_document.md + - 'ordered_json_editable_view': api/ordered_json_editable_view.md + - 'ordered_json_view': api/ordered_json_view.md - 'ordered_map': api/ordered_map.md - macros: - 'Overview': api/macros/index.md @@ -296,9 +419,12 @@ nav: - 'JSON_DELETE_DEPRECATED_FUNCTIONS': api/macros/json_delete_deprecated_functions.md - 'JSON_DIAGNOSTIC_POSITIONS': api/macros/json_diagnostic_positions.md - 'JSON_DISABLE_ENUM_SERIALIZATION': api/macros/json_disable_enum_serialization.md + - 'JSON_DISABLE_STREAM_BULK_SCAN': api/macros/json_disable_stream_bulk_scan.md - 'JSON_DISABLE_TUPLE_REFERENCE_CONVERSION': api/macros/json_disable_tuple_reference_conversion.md + - 'JSON_DISABLE_UNLOCKED_FILE_IO': api/macros/json_disable_unlocked_file_io.md - 'JSON_HAS_CPP_11, JSON_HAS_CPP_14, JSON_HAS_CPP_17, JSON_HAS_CPP_20, JSON_HAS_CPP_23, JSON_HAS_CPP_26': api/macros/json_has_cpp_11.md - 'JSON_HAS_EXPERIMENTAL_FILESYSTEM, JSON_HAS_FILESYSTEM': api/macros/json_has_filesystem.md + - 'JSON_HAS_RANGE_VIEW_CONVERSION': api/macros/json_has_range_view_conversion.md - 'JSON_HAS_RANGES': api/macros/json_has_ranges.md - 'JSON_HAS_STATIC_RTTI': api/macros/json_has_static_rtti.md - 'JSON_HAS_STD_FORMAT': api/macros/json_has_std_format.md @@ -306,6 +432,7 @@ nav: - 'JSON_NO_AUTOMATIC_UDLS': api/macros/json_no_automatic_udls.md - 'JSON_NOEXCEPTION': api/macros/json_noexception.md - 'JSON_NO_IO': api/macros/json_no_io.md + - 'JSON_NO_SIMD': api/macros/json_no_simd.md - 'JSON_NO_THREAD_LOCAL': api/macros/json_no_thread_local.md - 'JSON_PRECISE_STREAM_POSITION': api/macros/json_precise_stream_position.md - 'JSON_SKIP_LIBRARY_VERSION_CHECK': api/macros/json_skip_library_version_check.md @@ -317,6 +444,8 @@ nav: - 'JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON': api/macros/json_use_legacy_discarded_value_comparison.md - 'JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS': api/macros/json_use_objects_for_enum_keyed_maps.md - 'JSON_USE_SIMDUTF': api/macros/json_use_simdutf.md + - 'JSON_VIEW_NO_SIMD': api/macros/json_view_no_simd.md + - 'JSON_VIEW_USE_SSSE3': api/macros/json_view_use_ssse3.md - 'NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE, NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_WITH_DEFAULT, NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_ONLY_SERIALIZE, NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE, NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_WITH_DEFAULT, NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE': api/macros/nlohmann_define_derived_type.md - 'NLOHMANN_DEFINE_TYPE_INTRUSIVE, NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT, NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE': api/macros/nlohmann_define_type_intrusive.md - 'NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE, NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT, NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE': api/macros/nlohmann_define_type_non_intrusive.md @@ -404,6 +533,7 @@ validation: unrecognized_links: warn hooks: + - hooks/api_changes.py - hooks/copy_markdown_source.py - hooks/unreleased_versions.py @@ -451,8 +581,12 @@ plugins: # package badges (the fetch fails, then reading the missing cache entry # raises FileNotFoundError). Readers' browsers are served normally, so # leave these badges as external references instead of self-hosting them. + # The same goes for img.shields.io, whose version badges time out from + # time to time (strict mode turns the failed download into an error) and + # would go stale between builds if they were self-hosted anyway. assets_exclude: - repology.org/* + - img.shields.io/* - llmstxt: markdown_description: > JSON for Modern C++ is a C++11 header-only library implementing a JSON @@ -473,6 +607,8 @@ plugins: API Documentation: - api/*.md - api/basic_json/*.md + - api/basic_json_document/*.md + - api/basic_json_view/*.md - api/adl_serializer/*.md - api/byte_container_with_subtype/*.md - api/json_pointer/*.md diff --git a/docs/mkdocs/requirements.txt b/docs/mkdocs/requirements.txt index d9e7098ed..245ebc387 100644 --- a/docs/mkdocs/requirements.txt +++ b/docs/mkdocs/requirements.txt @@ -7,6 +7,6 @@ mkdocs-material-extensions==1.3.1 # extensions mkdocs-minify-plugin==0.8.0 # plugin "minify" mkdocs-redirects==1.2.3 # plugin "redirects" mkdocs-htmlproofer-plugin==1.6.0 # plugin "htmlproofer" -mkdocs-llmstxt==0.5.0 # plugin "llmstxt" +mkdocs-llmstxt==0.5.1 # plugin "llmstxt" PyYAML==6.0.3 # linter diff --git a/docs/mkdocs/scripts/check_structure.py b/docs/mkdocs/scripts/check_structure.py index dd76dbab0..c4ecddba3 100755 --- a/docs/mkdocs/scripts/check_structure.py +++ b/docs/mkdocs/scripts/check_structure.py @@ -288,36 +288,6 @@ def check_header_links() -> None: f'link to "{match.group(0)}" does not point to a documentation page') -def check_docset() -> None: - """Every API page and every macro has an entry in the docset index; no entry points to a missing page.""" - entry_re = re.compile(r"VALUES \('((?:[^']|'')*)', '(\w+)', '([^']*)'\);") - names_by_path = {} - with open("../../docset/docSet.sql", encoding="utf-8") as sql: - for name, _, path in entry_re.findall(sql.read()): - names_by_path.setdefault(path, set()).add(name.replace("''", "'")) - - def to_path(page): - if os.path.basename(page) == "index.md": - return page[:-len("index.md")] + "index.html" - return page[:-len(".md")] + "/index.html" - - pages = sorted(glob.glob("**/*.md", recursive=True)) - for path in sorted(set(names_by_path) - {to_path(p) for p in pages}): - report("docset/stale_entry", "../../docset/docSet.sql", f'entry "{path}" has no documentation page') - for page in (p for p in pages if p.startswith("api/")): - names = names_by_path.get(to_path(page)) - if not names: - report("docset/missing_entry", page, "page has no entry in docs/docset/docSet.sql") - elif page.startswith("api/macros/") and os.path.basename(page) != "index.md": - with open(page, encoding="utf-8") as content: - text = content.read() - match = re.search(r"^# (.+)$", text, re.MULTILINE) or re.search(r"

(.*?)

", text, re.DOTALL) - title = re.sub(r"<[^>]+>|\s+", " ", match.group(1)) - for macro in filter(None, (x.strip() for x in re.split(r"[,/]", title))): - if macro not in names: - report("docset/missing_macro", page, f'macro "{macro}" has no entry in docs/docset/docSet.sql') - - if __name__ == "__main__": print(120 * "-") check_structure() @@ -327,7 +297,6 @@ if __name__ == "__main__": check_heading_levels() check_image_alt_text() check_header_links() - check_docset() print(120 * "-") if warnings > 0: diff --git a/docs/mkdocs/scripts/mermaid/package-lock.json b/docs/mkdocs/scripts/mermaid/package-lock.json index 4f9dc0395..14bbc249e 100644 --- a/docs/mkdocs/scripts/mermaid/package-lock.json +++ b/docs/mkdocs/scripts/mermaid/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "check-mermaid", "dependencies": { - "jsdom": "30.1.1", + "jsdom": "30.1.3", "mermaid": "11.17.2" } }, @@ -24,14 +24,14 @@ } }, "node_modules/@asamuzakjp/css-color": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-7.1.2.tgz", - "integrity": "sha512-99DHAnXDB5z6EEK+9GMpVI7Mw4oxj97dY5bpOzMnjADQWxI8rN6TvTduuFLUhUMlS7/CfVZ06tcZsus6cltnNw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-7.1.3.tgz", + "integrity": "sha512-1t1U8Cm3RBl9vh3RAZhjkPaduzLSvQJRToRrxpGR/eDRW99mOREleuu+mWfiFa795bUzjwht7NgphCKTs2ianQ==", "license": "MIT", "dependencies": { - "@csstools/css-calc": "^3.4.1", - "@csstools/css-color-parser": "^4.2.4", - "@csstools/css-parser-algorithms": "^4.0.1", + "@csstools/css-calc": "^3.4.2", + "@csstools/css-color-parser": "^4.2.5", + "@csstools/css-parser-algorithms": "^4.0.2", "@csstools/css-tokenizer": "^4.0.2", "lru-cache": "^11.5.3" }, @@ -40,9 +40,9 @@ } }, "node_modules/@asamuzakjp/dom-selector": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-9.2.2.tgz", - "integrity": "sha512-lSWTBMjAcmu2xn5yEDU7jh6QDV+C8GEKtdJ4pIQhXh26RkKQ7S3FuQlt+zZkUbTdJ4d3XyV1kPI/G0zWXxqaqw==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-10.0.3.tgz", + "integrity": "sha512-3+777rEPsOOjoy5/sRmIzHnoRDaLpm3XWY2dmWn/N7f/XwDG0bcpnFaKL3X9FYJUbNwLmpcJC1bUYexJRxsFlw==", "license": "MIT", "dependencies": { "bidi-js": "^1.1.0", @@ -98,9 +98,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.1.tgz", - "integrity": "sha512-EtC7SoN1j6J4E4DCwg5QgbO5TGxgxIA1RXqe+W+qUM+BUcezx9wT+/tiQ/WO2yCX4i5X+Cuf9ciJ22aP4UEwWw==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.3.tgz", + "integrity": "sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA==", "funding": [ { "type": "github", @@ -116,14 +116,14 @@ "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.2", "@csstools/css-tokenizer": "^4.0.2" } }, "node_modules/@csstools/css-color-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.4.tgz", - "integrity": "sha512-DyefytAZ735mX4Dq/WcDAXFtXhaEFvme0ZS9tVEBAc2whxUthXr0R0L2rmEPm59SrMBkGrFzQviBXMs/UtnABQ==", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.6.tgz", + "integrity": "sha512-iiPQ3iRWwnJkeEn6RIu6SJPr7hYrLz6XZ9s/QZl+2/LI5KQVjpl2fdmDSZKuD4xP6GMmMPgHFFXg6k1Wkz0Trg==", "funding": [ { "type": "github", @@ -137,20 +137,20 @@ "license": "MIT", "dependencies": { "@csstools/color-helpers": "^6.1.2", - "@csstools/css-calc": "^3.4.1" + "@csstools/css-calc": "^3.4.3" }, "engines": { "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.2", "@csstools/css-tokenizer": "^4.0.2" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.1.tgz", - "integrity": "sha512-ShL8BqPfbKJrJiKFH0xBbN0i7Nrh9HXYRuF+pzyj93R/BL2YAsUxJeqANErzqM+0JGl7vjHp+3OIgd/DL69YIA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.2.tgz", + "integrity": "sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==", "funding": [ { "type": "github", @@ -170,9 +170,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.14.tgz", - "integrity": "sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.15.tgz", + "integrity": "sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA==", "funding": [ { "type": "github", @@ -1071,30 +1071,16 @@ } }, "node_modules/data-urls": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", - "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-8.0.0.tgz", + "integrity": "sha512-JQUCCK+/CAGbGGx618PlFZMXH81Jtj7t73CHv9dd/zVWvxgGT2zLFRjnKb+Ng46GIwcy+f7f88qd4duqllK+WQ==", "license": "MIT", "dependencies": { "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0" + "whatwg-url": "^17.1.2" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/data-urls/node_modules/whatwg-url": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", - "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.11.0", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.14.0 || >=24.0.0" } }, "node_modules/dayjs": { @@ -1119,9 +1105,9 @@ } }, "node_modules/dompurify": { - "version": "3.4.16", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz", - "integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==", + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz", + "integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -1216,30 +1202,30 @@ "license": "MIT" }, "node_modules/jsdom": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.1.1.tgz", - "integrity": "sha512-FahmoPK5vbPc+jxV1iErMHmAZypCZ942NHF4+qqaWAuvaKKTBZxawnmAtrbGWLU7MtlxfqIP0qw6aSI+aWGtLg==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.1.3.tgz", + "integrity": "sha512-hQMUPvRgN7KxsfIytDQji7bimuJeRQa6Qo5+iuiZQk5CafeRxLIA10/cG5YCBFNpD9RO3oqIiN4CO1Y44kxqXg==", "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^7.0.0", - "@asamuzakjp/dom-selector": "^9.2.1", + "@asamuzakjp/css-color": "^7.1.3", + "@asamuzakjp/dom-selector": "^10.0.3", "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.13", - "@exodus/bytes": "^1.15.1", + "@csstools/css-syntax-patches-for-csstree": "^1.1.15", + "@exodus/bytes": "^1.16.0", "css-tree": "^3.2.1", - "data-urls": "^7.0.0", + "data-urls": "^8.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^7.0.0", "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.5.2", + "lru-cache": "^11.5.3", "parse5": "^8.0.1", "saxes": "^6.0.0", "tough-cookie": "^6.0.2", - "undici": "^8.10.2", + "undici": "^8.12.1", "w3c-xmlserializer": "^6.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^17.1.1", + "whatwg-url": "^17.2.0", "xml-name-validator": "^5.0.0" }, "engines": { @@ -1514,15 +1500,15 @@ } }, "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-7.0.0.tgz", + "integrity": "sha512-BcDSDW+SIGWVOns1Zm6Yg91q9AEj7eaID0QDY9oIqJD1A8dscNwMcl9Sms2DeuWCNov1X0/oXanHGYQBSLwZsg==", "license": "MIT", "dependencies": { "punycode": "^2.3.1" }, "engines": { - "node": ">=20" + "node": "^22.14.0 || >=24.0.0" } }, "node_modules/ts-dedent": { @@ -1535,9 +1521,9 @@ } }, "node_modules/undici": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.11.2.tgz", - "integrity": "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.2.tgz", + "integrity": "sha512-XtuDWVdqTLr3NAxm2ly9/hU6kUsJOgGh1RASzMBbsw6aWCOaQzHdW+QQDfdERjfJ2V5vL5pRPa4RetO9UfivDg==", "license": "MIT", "engines": { "node": ">=22.19.0" @@ -1587,13 +1573,13 @@ } }, "node_modules/whatwg-url": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.2.tgz", - "integrity": "sha512-TEZA+Zqxin7Jjsm2cjRohCmen5awh+hT6Zi3VZdqZlNRk7zvOI/9WpBFg/DWlA56bWnzwm6DuB8NS0EsxQH9uQ==", + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.2.0.tgz", + "integrity": "sha512-IhLIHNcBrzIixC5XXsbr4DxdjajyA5vQ5RwGZMtrUp+GbtE5Nrsge9QKau7jXrC0aTXLYGV0xiW7Tb4YS2aomw==", "license": "MIT", "dependencies": { - "@exodus/bytes": "^1.15.1", - "tr46": "^6.0.0", + "@exodus/bytes": "^1.16.0", + "tr46": "^7.0.0", "webidl-conversions": "^8.0.1" }, "engines": { diff --git a/docs/mkdocs/scripts/mermaid/package.json b/docs/mkdocs/scripts/mermaid/package.json index 6cf40b9f6..d2c033d3d 100644 --- a/docs/mkdocs/scripts/mermaid/package.json +++ b/docs/mkdocs/scripts/mermaid/package.json @@ -4,7 +4,7 @@ "description": "Validate the Mermaid diagrams of the documentation (see check_mermaid.mjs)", "type": "module", "dependencies": { - "jsdom": "30.1.1", + "jsdom": "30.1.3", "mermaid": "11.17.2" } } diff --git a/include/nlohmann/detail/abi_config.hpp b/include/nlohmann/detail/abi_config.hpp new file mode 100644 index 000000000..510a67385 --- /dev/null +++ b/include/nlohmann/detail/abi_config.hpp @@ -0,0 +1,33 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +/*! +@brief the configuration macros that json_view.hpp reads + +json.hpp undefines these macros at its end (see macro_unscope.hpp), so code +that builds on the library after it (json_view.hpp) reads them here. A macro +is added when the view starts to depend on it. Like the +macros, they are part of the ABI namespace, so they always match the +basic_json they are used with. +*/ +struct abi_config +{ + /// JSON_STRICT_NUL_HANDLING: a null byte is an error, not the end of input + static constexpr bool strict_nul_handling = JSON_STRICT_NUL_HANDLING != 0; +}; + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/abi_macros.hpp b/include/nlohmann/detail/abi_macros.hpp index 063b9a0d9..827a9eef8 100644 --- a/include/nlohmann/detail/abi_macros.hpp +++ b/include/nlohmann/detail/abi_macros.hpp @@ -54,6 +54,14 @@ #define JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS 0 #endif +#ifndef JSON_DISABLE_STREAM_BULK_SCAN + #define JSON_DISABLE_STREAM_BULK_SCAN 0 +#endif + +#ifndef JSON_DISABLE_UNLOCKED_FILE_IO + #define JSON_DISABLE_UNLOCKED_FILE_IO 0 +#endif + #if JSON_DIAGNOSTICS #define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS _diag #else @@ -102,14 +110,44 @@ #define NLOHMANN_JSON_ABI_TAG_OBJECTS_FOR_ENUM_KEYED_MAPS #endif +#if JSON_DISABLE_STREAM_BULK_SCAN + #define NLOHMANN_JSON_ABI_TAG_DISABLE_STREAM_BULK_SCAN _dsbs +#else + #define NLOHMANN_JSON_ABI_TAG_DISABLE_STREAM_BULK_SCAN +#endif + +#if JSON_DISABLE_UNLOCKED_FILE_IO + #define NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO _dufio +#else + #define NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO +#endif + +#ifdef JSON_NO_SIMD + #define NLOHMANN_JSON_ABI_TAG_NO_SIMD _ns +#else + #define NLOHMANN_JSON_ABI_TAG_NO_SIMD +#endif + +#ifdef JSON_VIEW_NO_SIMD + #define NLOHMANN_JSON_ABI_TAG_VIEW_NO_SIMD _vns +#else + #define NLOHMANN_JSON_ABI_TAG_VIEW_NO_SIMD +#endif + +#ifdef JSON_VIEW_USE_SSSE3 + #define NLOHMANN_JSON_ABI_TAG_VIEW_USE_SSSE3 _vssse3 +#else + #define NLOHMANN_JSON_ABI_TAG_VIEW_USE_SSSE3 +#endif + #ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION #define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0 #endif // Construct the namespace ABI tags component -#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h) json_abi ## a ## b ## c ## d ## e ## f ## g ## h -#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c, d, e, f, g, h) \ - NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h) +#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l, m) json_abi ## a ## b ## c ## d ## e ## f ## g ## h ## i ## j ## k ## l ## m +#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c, d, e, f, g, h, i, j, k, l, m) \ + NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l, m) #define NLOHMANN_JSON_ABI_TAGS \ NLOHMANN_JSON_ABI_TAGS_CONCAT( \ @@ -120,7 +158,12 @@ NLOHMANN_JSON_ABI_TAG_PRECISE_STREAM_POSITION, \ NLOHMANN_JSON_ABI_TAG_STRICT_NUL_HANDLING, \ NLOHMANN_JSON_ABI_TAG_STRICT_BINARY_UTF8, \ - NLOHMANN_JSON_ABI_TAG_OBJECTS_FOR_ENUM_KEYED_MAPS) + NLOHMANN_JSON_ABI_TAG_OBJECTS_FOR_ENUM_KEYED_MAPS, \ + NLOHMANN_JSON_ABI_TAG_DISABLE_STREAM_BULK_SCAN, \ + NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO, \ + NLOHMANN_JSON_ABI_TAG_NO_SIMD, \ + NLOHMANN_JSON_ABI_TAG_VIEW_NO_SIMD, \ + NLOHMANN_JSON_ABI_TAG_VIEW_USE_SSSE3) // Construct the namespace version component #define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \ diff --git a/include/nlohmann/detail/bit_ops.hpp b/include/nlohmann/detail/bit_ops.hpp index 9ccb82c66..7202e7b4f 100644 --- a/include/nlohmann/detail/bit_ops.hpp +++ b/include/nlohmann/detail/bit_ops.hpp @@ -9,15 +9,29 @@ #pragma once #include // uint64_t -#if !defined(__SIZEOF_INT128__) && defined(_MSC_VER) && (defined(_M_X64) || defined(_M_ARM64)) - #include // __umulh, _umul128 +#include // memcpy +#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_ARM64)) && (!defined(__SIZEOF_INT128__) || (!defined(__GNUC__) && !defined(__clang__))) + #include // __umulh, _umul128, _BitScanForward64, _BitScanReverse64 #endif -#include +#ifdef _MSC_VER + #include // _byteswap_ushort, _byteswap_ulong, _byteswap_uint64 +#endif +#if defined(_MSC_VER) && !defined(__clang__) && (defined(_M_IX86) || defined(_M_ARM)) + #include // _BitScanForward, _BitScanReverse +#endif + +#include // JSON_HEDLEY_ALWAYS_INLINE, NLOHMANN_JSON_NAMESPACE_BEGIN + +#if defined(__cpp_lib_bit_cast) || defined(__cpp_lib_bitops) || defined(__cpp_lib_byteswap) + #include // bit_cast, byteswap, countl_zero, countr_zero +#endif // Portable bit-level helpers for the number and string scanners. They use -// compiler builtins or platform-specific intrinsics where available and plain -// C++ otherwise, so they work regardless of byte order. +// compiler builtins or platform-specific intrinsics where available, the +// C++20 functions next, and plain C++ otherwise, so they work regardless +// of byte order. (The builtins and intrinsics come first: MSVC's +// std::countl_zero checks for LZCNT at run time unless AVX2 is enabled.) NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail @@ -28,6 +42,20 @@ inline int count_leading_zeros(std::uint64_t x) noexcept { #if defined(__GNUC__) || defined(__clang__) return __builtin_clzll(x); +#elif defined(_MSC_VER) && (defined(_M_X64) || defined(_M_ARM64)) + unsigned long index = 0; // NOLINT(runtime/int): the type _BitScan*64 takes + _BitScanReverse64(&index, x); + return 63 - static_cast(index); +#elif defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_ARM)) + unsigned long index = 0; // NOLINT(runtime/int): the type _BitScan* takes + if (_BitScanReverse(&index, static_cast(x >> 32u))) // NOLINT(runtime/int) + { + return 31 - static_cast(index); + } + _BitScanReverse(&index, static_cast(x)); // NOLINT(runtime/int) + return 63 - static_cast(index); +#elif defined(__cpp_lib_bitops) + return std::countl_zero(x); #else int n = 0; for (int shift = 32; shift != 0; shift >>= 1) @@ -42,6 +70,100 @@ inline int count_leading_zeros(std::uint64_t x) noexcept #endif } +/// number of trailing zero bits of x (x != 0) +inline int count_trailing_zeros(std::uint64_t x) noexcept +{ +#if defined(__GNUC__) || defined(__clang__) + return __builtin_ctzll(x); +#elif defined(_MSC_VER) && (defined(_M_X64) || defined(_M_ARM64)) + unsigned long index = 0; // NOLINT(runtime/int): the type _BitScan*64 takes + _BitScanForward64(&index, x); + return static_cast(index); +#elif defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_ARM)) + unsigned long index = 0; // NOLINT(runtime/int): the type _BitScan* takes + if (_BitScanForward(&index, static_cast(x))) // NOLINT(runtime/int) + { + return static_cast(index); + } + _BitScanForward(&index, static_cast(x >> 32u)); // NOLINT(runtime/int) + return 32 + static_cast(index); +#elif defined(__cpp_lib_bitops) + return std::countr_zero(x); +#else + int n = 0; + for (int shift = 32; shift != 0; shift >>= 1) + { + if ((x << (64 - shift)) == 0) + { + n += shift; + x >>= shift; + } + } + return n; +#endif +} + +/// the object representation of @a from as a @a To (std::bit_cast in C++20) +template +JSON_HEDLEY_ALWAYS_INLINE To bit_cast(const From& from) noexcept +{ + static_assert(sizeof(To) == sizeof(From), "bit_cast needs types of the same size"); +#if defined(__cpp_lib_bit_cast) + return std::bit_cast(from); +#else + To to {}; + std::memcpy(&to, &from, sizeof(To)); + return to; +#endif +} + +/// x with its bytes in reverse order (std::byteswap in C++23); compilers lower +/// it to a single bswap/rev/movbe +inline std::uint16_t byteswap(std::uint16_t x) noexcept +{ +#if defined(__GNUC__) || defined(__clang__) + return __builtin_bswap16(x); +#elif defined(_MSC_VER) + return _byteswap_ushort(x); +#elif defined(__cpp_lib_byteswap) + return std::byteswap(x); +#else + return static_cast((x >> 8u) | (x << 8u)); +#endif +} + +/// x with its bytes in reverse order (std::byteswap in C++23) +inline std::uint32_t byteswap(std::uint32_t x) noexcept +{ +#if defined(__GNUC__) || defined(__clang__) + return __builtin_bswap32(x); +#elif defined(_MSC_VER) + return _byteswap_ulong(x); +#elif defined(__cpp_lib_byteswap) + return std::byteswap(x); +#else + return ((x & 0x000000FFu) << 24u) | ((x & 0x0000FF00u) << 8u) + | ((x & 0x00FF0000u) >> 8u) | ((x & 0xFF000000u) >> 24u); +#endif +} + +/// x with its bytes in reverse order (std::byteswap in C++23) +inline std::uint64_t byteswap(std::uint64_t x) noexcept +{ +#if defined(__GNUC__) || defined(__clang__) + return __builtin_bswap64(x); +#elif defined(_MSC_VER) + return _byteswap_uint64(x); +#elif defined(__cpp_lib_byteswap) + return std::byteswap(x); +#else + x = ((x & 0x00000000FFFFFFFFull) << 32u) | ((x & 0xFFFFFFFF00000000ull) >> 32u); + x = ((x & 0x0000FFFF0000FFFFull) << 16u) | ((x & 0xFFFF0000FFFF0000ull) >> 16u); + x = ((x & 0x00FF00FF00FF00FFull) << 8u) | ((x & 0xFF00FF00FF00FF00ull) >> 8u); + return x; +#endif +} + /// the 128-bit product of two 64-bit numbers struct uint128_parts { @@ -75,15 +197,27 @@ inline uint128_parts full_multiplication(std::uint64_t a, std::uint64_t b) noexc #endif } -/// eight bytes as a little-endian word (compilers fold this into one load on -/// little-endian targets) -inline std::uint64_t read_eight_bytes(const char* p) noexcept +/// eight bytes as a little-endian word (a single load on little-endian +/// targets; always inlined, as GCC otherwise calls it in the number loops) +JSON_HEDLEY_ALWAYS_INLINE std::uint64_t read_eight_bytes(const unsigned char* b) noexcept { - const auto* b = reinterpret_cast(p); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) +#if JSON_LITTLE_ENDIAN_TARGET + // the byte order already matches + std::uint64_t result = 0; + std::memcpy(&result, b, sizeof(result)); + return result; +#else return static_cast(b[0]) | (static_cast(b[1]) << 8u) | (static_cast(b[2]) << 16u) | (static_cast(b[3]) << 24u) | (static_cast(b[4]) << 32u) | (static_cast(b[5]) << 40u) | (static_cast(b[6]) << 48u) | (static_cast(b[7]) << 56u); +#endif +} + +/// eight bytes as a little-endian word +JSON_HEDLEY_ALWAYS_INLINE std::uint64_t read_eight_bytes(const char* p) noexcept +{ + return read_eight_bytes(reinterpret_cast(p)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) } } // namespace detail diff --git a/include/nlohmann/detail/conversions/from_json.hpp b/include/nlohmann/detail/conversions/from_json.hpp index 884ff014a..51f26706c 100644 --- a/include/nlohmann/detail/conversions/from_json.hpp +++ b/include/nlohmann/detail/conversions/from_json.hpp @@ -8,7 +8,7 @@ #pragma once -#include // transform +#include // min, transform #include // array #include // forward_list #include // inserter, front_inserter, end @@ -48,7 +48,7 @@ inline void from_json(const BasicJsonType& j, typename std::nullptr_t& n) { if (JSON_HEDLEY_UNLIKELY(!j.is_null())) { - JSON_THROW(type_error::create(302, concat("type must be null, but is ", j.type_name()), &j)); + throw_type_must_be("null", j); } n = nullptr; } @@ -69,29 +69,31 @@ void from_json(const BasicJsonType& j, std::optional& opt) } #endif // JSON_HAS_CPP_17 -// overloads for basic_json template parameters -template < typename BasicJsonType, typename ArithmeticType, - enable_if_t < std::is_arithmetic::value&& - !std::is_same::value, - int > = 0 > -void get_arithmetic_value(const BasicJsonType& j, ArithmeticType& val) +// read a number value of any of the three number types into val; +// returns false (leaving val untouched) if j is not a number +template +bool get_number_value(const BasicJsonType& j, ArithmeticType& val) { switch (static_cast(j)) { case value_t::number_unsigned: { - val = static_cast(*j.template get_ptr()); - break; + // never null: a basic_json whose number_unsigned_t is number_integer_t + // stores no number_unsigned values (#2573) + const auto* ptr = j.template get_ptr(); + JSON_ASSERT(ptr != nullptr); + val = static_cast(*ptr); + return true; } case value_t::number_integer: { val = static_cast(*j.template get_ptr()); - break; + return true; } case value_t::number_float: { val = static_cast(*j.template get_ptr()); - break; + return true; } case value_t::null: @@ -102,7 +104,20 @@ void get_arithmetic_value(const BasicJsonType& j, ArithmeticType& val) case value_t::binary: case value_t::discarded: default: - JSON_THROW(type_error::create(302, concat("type must be number, but is ", j.type_name()), &j)); + return false; + } +} + +// overloads for basic_json template parameters +template < typename BasicJsonType, typename ArithmeticType, + enable_if_t < std::is_arithmetic::value&& + !std::is_same::value, + int > = 0 > +void get_arithmetic_value(const BasicJsonType& j, ArithmeticType& val) +{ + if (JSON_HEDLEY_UNLIKELY(!get_number_value(j, val))) + { + throw_type_must_be("number", j); } } @@ -111,7 +126,7 @@ inline void from_json(const BasicJsonType& j, typename BasicJsonType::boolean_t& { if (JSON_HEDLEY_UNLIKELY(!j.is_boolean())) { - JSON_THROW(type_error::create(302, concat("type must be boolean, but is ", j.type_name()), &j)); + throw_type_must_be("boolean", j); } b = *j.template get_ptr(); } @@ -121,7 +136,7 @@ inline void from_json(const BasicJsonType& j, typename BasicJsonType::string_t& { if (JSON_HEDLEY_UNLIKELY(!j.is_string())) { - JSON_THROW(type_error::create(302, concat("type must be string, but is ", j.type_name()), &j)); + throw_type_must_be("string", j); } s = *j.template get_ptr(); } @@ -137,7 +152,7 @@ inline void from_json(const BasicJsonType& j, StringType& s) { if (JSON_HEDLEY_UNLIKELY(!j.is_string())) { - JSON_THROW(type_error::create(302, concat("type must be string, but is ", j.type_name()), &j)); + throw_type_must_be("string", j); } s = *j.template get_ptr(); @@ -150,7 +165,7 @@ inline void from_json(const BasicJsonType& j, typename BasicJsonType::number_flo } template -inline void from_json(const BasicJsonType& j, typename BasicJsonType::number_unsigned_t& val) +inline void from_json(const BasicJsonType& j, number_unsigned_overload_t& val) { get_arithmetic_value(j, val); } @@ -183,7 +198,7 @@ inline void from_json(const BasicJsonType& j, std::forward_list& l { if (JSON_HEDLEY_UNLIKELY(!j.is_array())) { - JSON_THROW(type_error::create(302, concat("type must be array, but is ", j.type_name()), &j)); + throw_type_must_be("array", j); } l.clear(); std::transform(j.rbegin(), j.rend(), @@ -200,7 +215,7 @@ inline void from_json(const BasicJsonType& j, std::valarray& l) { if (JSON_HEDLEY_UNLIKELY(!j.is_array())) { - JSON_THROW(type_error::create(302, concat("type must be array, but is ", j.type_name()), &j)); + throw_type_must_be("array", j); } l.resize(j.size()); std::transform(j.begin(), j.end(), std::begin(l), @@ -241,31 +256,62 @@ inline void from_json_array_impl(const BasicJsonType& j, typename BasicJsonType: arr = *j.template get_ptr(); } +// fill a std::array from the JSON array j: the size is checked once and the +// elements are read in order, so a short array converts the elements it has and +// then throws the out_of_range error at() would throw for the first missing one +template +void from_json_std_array(const BasicJsonType& j, std::array& arr) +{ + const auto& inner_array = *j.template get_ptr(); + const std::size_t size = (std::min)(N, inner_array.size()); + for (std::size_t i = 0; i < size; ++i) + { + arr[i] = inner_array[i].template get(); + } + if (JSON_HEDLEY_UNLIKELY(size < N)) + { + static_cast(j.at(size)); + } +} + template auto from_json_array_impl(const BasicJsonType& j, std::array& arr, priority_tag<2> /*unused*/) -> decltype(j.template get(), void()) { - for (std::size_t i = 0; i < N; ++i) - { - arr[i] = j.at(i).template get(); - } + from_json_std_array(j, arr); } -// reserve() is called through this pair (modeled on from_json_object_reserve) -// so from_json_array_impl below has a single body for both ConstructibleArrayType -// that support reserve() and those that don't. -template -auto from_json_array_reserve(ConstructibleArrayType& arr, typename ConstructibleArrayType::size_type size, priority_tag<1> /*unused*/) --> decltype(arr.reserve(size), void()) +// reserve() is called through this pair (used for arrays and objects alike) +// so from_json_array_impl and the object from_json have a single body for both +// containers that support reserve() and those that don't. +template +auto reserve_if_possible(ConstructibleContainerType& c, typename ConstructibleContainerType::size_type size, priority_tag<1> /*unused*/) +-> decltype(c.reserve(size), void()) { - arr.reserve(size); + c.reserve(size); } -template -void from_json_array_reserve(ConstructibleArrayType& /*arr*/, std::size_t /*size*/, priority_tag<0> /*unused*/) +template +void reserve_if_possible(ConstructibleContainerType& /*c*/, std::size_t /*size*/, priority_tag<0> /*unused*/) {} +// append through push_back() where available; containers without it (such as +// std::set) fall back to insert(end(), ...), which is what std::inserter did +template +auto from_json_array_append(ConstructibleArrayType& arr, ValueType&& value, priority_tag<1> /*unused*/) +-> decltype(arr.push_back(std::forward(value)), void()) +{ + arr.push_back(std::forward(value)); +} + +template +void from_json_array_append(ConstructibleArrayType& arr, ValueType&& value, priority_tag<0> /*unused*/) +{ + using std::end; + arr.insert(end(arr), std::forward(value)); +} + template::value, @@ -275,17 +321,15 @@ auto from_json_array_impl(const BasicJsonType& j, ConstructibleArrayType& arr, p j.template get(), void()) { - using std::end; - ConstructibleArrayType ret; - from_json_array_reserve(ret, j.size(), priority_tag<1> {}); - std::transform(j.begin(), j.end(), - std::inserter(ret, end(ret)), [](const BasicJsonType & i) + const auto* inner_array = j.template get_ptr(); + reserve_if_possible(ret, inner_array->size(), priority_tag<1> {}); + for (const auto& elem : *inner_array) { // get() returns *this, this won't call a from_json // method when value_type is BasicJsonType - return i.template get(); - }); + from_json_array_append(ret, elem.template get(), priority_tag<1> {}); + } arr = std::move(ret); } @@ -305,7 +349,7 @@ void()) { if (JSON_HEDLEY_UNLIKELY(!j.is_array())) { - JSON_THROW(type_error::create(302, concat("type must be array, but is ", j.type_name()), &j)); + throw_type_must_be("array", j); } from_json_array_impl(j, arr, priority_tag<3> {}); @@ -318,16 +362,47 @@ std::array from_json_inplace_array_impl(const BasicJsonType& return { { j.at(Idx).template get()... } }; } +// std::is_trivial is deprecated in C++26 (see binary_writer::to_char_type); +// a struct rather than an alias, as astyle indents an #ifdef block of aliases +template +struct is_trivial_element +#ifdef JSON_HAS_CPP_26 + : conjunction, std::is_trivially_default_constructible> +#else + : std::is_trivial +#endif +{}; + +// the element type may not be default constructible: construct every element +// in place (a pack expansion that is unrolled N times) +template +std::array from_json_make_array(const BasicJsonType& j, identity_tag> tag, + std::false_type /*is_trivial_element*/) +{ + return from_json_inplace_array_impl(j, tag, make_index_sequence {}); +} + +// a trivial element type cannot tell default construction plus assignment from +// construction in place, so use a loop instead of unrolling N conversions +template +std::array from_json_make_array(const BasicJsonType& j, identity_tag> /*unused*/, + std::true_type /*is_trivial_element*/) +{ + std::array arr; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): every element is assigned or an exception is thrown + from_json_std_array(j, arr); + return arr; +} + template < typename BasicJsonType, typename T, std::size_t N > auto from_json(const BasicJsonType& j, identity_tag> tag) -> decltype(from_json_inplace_array_impl(j, tag, make_index_sequence {})) { if (JSON_HEDLEY_UNLIKELY(!j.is_array())) { - JSON_THROW(type_error::create(302, concat("type must be array, but is ", j.type_name()), &j)); + throw_type_must_be("array", j); } - return from_json_inplace_array_impl(j, tag, make_index_sequence {}); + return from_json_make_array(j, tag, is_trivial_element {}); } template @@ -335,7 +410,7 @@ inline void from_json(const BasicJsonType& j, typename BasicJsonType::binary_t& { if (JSON_HEDLEY_UNLIKELY(!j.is_binary())) { - JSON_THROW(type_error::create(302, concat("type must be binary, but is ", j.type_name()), &j)); + throw_type_must_be("binary", j); } bin = *j.template get_ptr(); @@ -356,20 +431,26 @@ inline void from_json(const BasicJsonType& j, CompatibleArrayType& bin) } else { - JSON_THROW(type_error::create(302, concat("type must be binary or array, but is ", j.type_name()), &j)); + throw_type_must_be("binary or array", j); } } -template -auto from_json_object_reserve(ConstructibleObjectType& obj, typename ConstructibleObjectType::size_type size, priority_tag<1> /*unused*/) --> decltype(obj.reserve(size), void()) +// the keys of the source are sorted for the usual object types, so hinting the +// end makes every insertion constant-time (for containers that order their keys +// the same way); containers without emplace_hint() (such as ordered_map) fall +// back to emplace() +template +auto from_json_object_emplace(ConstructibleObjectType& obj, Key&& key, Value&& value, priority_tag<1> /*unused*/) +-> decltype(obj.emplace_hint(obj.end(), std::forward(key), std::forward(value)), void()) { - obj.reserve(size); + obj.emplace_hint(obj.end(), std::forward(key), std::forward(value)); } -template -inline void from_json_object_reserve(ConstructibleObjectType& /*obj*/, std::size_t /*size*/, priority_tag<0> /*unused*/) -{} +template +void from_json_object_emplace(ConstructibleObjectType& obj, Key&& key, Value&& value, priority_tag<0> /*unused*/) +{ + obj.emplace(std::forward(key), std::forward(value)); +} template::value, int> = 0> @@ -377,15 +458,15 @@ inline void from_json(const BasicJsonType& j, ConstructibleObjectType& obj) { if (JSON_HEDLEY_UNLIKELY(!j.is_object())) { - JSON_THROW(type_error::create(302, concat("type must be object, but is ", j.type_name()), &j)); + throw_type_must_be("object", j); } ConstructibleObjectType ret; const auto* inner_object = j.template get_ptr(); - from_json_object_reserve(ret, inner_object->size(), priority_tag<1> {}); + reserve_if_possible(ret, inner_object->size(), priority_tag<1> {}); for (const auto& p : *inner_object) { - ret.emplace(p.first, p.second.template get()); + from_json_object_emplace(ret, p.first, p.second.template get(), priority_tag<1> {}); } obj = std::move(ret); } @@ -402,38 +483,17 @@ template < typename BasicJsonType, typename ArithmeticType, int > = 0 > inline void from_json(const BasicJsonType& j, ArithmeticType& val) { - switch (static_cast(j)) + if (get_number_value(j, val)) { - case value_t::number_unsigned: - { - val = static_cast(*j.template get_ptr()); - break; - } - case value_t::number_integer: - { - val = static_cast(*j.template get_ptr()); - break; - } - case value_t::number_float: - { - val = static_cast(*j.template get_ptr()); - break; - } - case value_t::boolean: - { - val = static_cast(*j.template get_ptr()); - break; - } - - 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: - JSON_THROW(type_error::create(302, concat("type must be number, but is ", j.type_name()), &j)); + return; } + + // unlike get_arithmetic_value(), a boolean is accepted here + if (JSON_HEDLEY_UNLIKELY(!j.is_boolean())) + { + throw_type_must_be("number", j); + } + val = static_cast(*j.template get_ptr()); } template @@ -504,7 +564,7 @@ auto from_json(const BasicJsonType& j, TupleRelated&& t) { if (JSON_HEDLEY_UNLIKELY(!j.is_array())) { - JSON_THROW(type_error::create(302, concat("type must be array, but is ", j.type_name()), &j)); + throw_type_must_be("array", j); } return from_json_tuple_impl(j, std::forward(t), priority_tag<3> {}); @@ -517,14 +577,14 @@ void from_json_pair_array_to_map(const BasicJsonType& j, MapType& m) { if (JSON_HEDLEY_UNLIKELY(!j.is_array())) { - JSON_THROW(type_error::create(302, concat("type must be array, but is ", j.type_name()), &j)); + throw_type_must_be("array", j); } m.clear(); for (const auto& p : j) { if (JSON_HEDLEY_UNLIKELY(!p.is_array())) { - JSON_THROW(type_error::create(302, concat("type must be array, but is ", p.type_name()), &p)); + throw_type_must_be("array", p); } m.emplace(p.at(0).template get(), p.at(1).template get()); } @@ -591,7 +651,7 @@ inline void from_json(const BasicJsonType& j, std_fs::path& p) { if (JSON_HEDLEY_UNLIKELY(!j.is_string())) { - JSON_THROW(type_error::create(302, concat("type must be string, but is ", j.type_name()), &j)); + throw_type_must_be("string", j); } const auto& s = *j.template get_ptr(); // Checking for C++20 standard or later can be insufficient in case the diff --git a/include/nlohmann/detail/conversions/to_chars.hpp b/include/nlohmann/detail/conversions/to_chars.hpp index 52342db40..a977e797b 100644 --- a/include/nlohmann/detail/conversions/to_chars.hpp +++ b/include/nlohmann/detail/conversions/to_chars.hpp @@ -4,6 +4,7 @@ // |_____|_____|_____|_|___| https://github.com/nlohmann/json // // SPDX-FileCopyrightText: 2009 Florian Loitsch +// SPDX-FileCopyrightText: 2025 Victor Zverovich // SPDX-FileCopyrightText: 2013-2026 Niels Lohmann // SPDX-License-Identifier: MIT @@ -11,11 +12,30 @@ #include // array #include // signbit, isfinite +#include // size_t #include // intN_t, uintN_t #include // memcpy, memmove #include // numeric_limits #include // conditional + +// SSE2 (every x86-64 CPU) and NEON (every 64-bit Arm CPU) convert the 16 +// digits of a double at once (unless JSON_NO_SIMD selects the portable code) +#if !defined(JSON_NO_SIMD) && (defined(__x86_64__) || (defined(_M_X64) && !defined(_M_ARM64EC))) + #include + #define JSON_DTOA_SSE2 1 + #define JSON_DTOA_NEON 0 +#elif !defined(JSON_NO_SIMD) && (defined(__aarch64__) || defined(_M_ARM64)) && !defined(_M_ARM64EC) && !defined(__ARM_BIG_ENDIAN) + #include + #define JSON_DTOA_SSE2 0 + #define JSON_DTOA_NEON 1 +#else + #define JSON_DTOA_SSE2 0 + #define JSON_DTOA_NEON 0 +#endif + +#include +#include #include NLOHMANN_JSON_NAMESPACE_BEGIN @@ -44,16 +64,6 @@ For a detailed description of the algorithm see: namespace dtoa_impl { -template -Target reinterpret_bits(const Source source) -{ - static_assert(sizeof(Target) == sizeof(Source), "size mismatch"); - - Target target; - std::memcpy(&target, &source, sizeof(Source)); - return target; -} - struct diyfp // f * 2^e { static constexpr int kPrecision = 64; // = q @@ -208,7 +218,7 @@ boundaries compute_boundaries(FloatType value) using bits_type = typename std::conditional::type; - const auto bits = static_cast(reinterpret_bits(value)); + const auto bits = static_cast(bit_cast(value)); const std::uint64_t E = bits >> (kPrecision - 1); const std::uint64_t F = bits & (kHiddenBit - 1); @@ -1047,6 +1057,437 @@ inline char* format_buffer(char* buf, int len, int decimal_exponent, return append_exponent(buf, n - 1); } +/// eight decimal digits (a value below 10^8) as bytes 0..9, the first digit +/// in the most significant byte: three steps that divide all lanes at once +/// by a multiplication (the conversion of Xiang JunBo, as in Zmij) +inline std::uint64_t eight_digit_bytes(std::uint64_t abcdefgh) noexcept +{ + const std::uint64_t abcd_efgh = abcdefgh + (((std::uint64_t{1} << 32u) - 10000u) * ((abcdefgh * (((std::uint64_t{1} << 40u) / 10000u) + 1u)) >> 40u)); + const std::uint64_t ab_cd_ef_gh = abcd_efgh + (((std::uint64_t{1} << 16u) - 100u) * (((abcd_efgh * (((std::uint64_t{1} << 19u) / 100u) + 1u)) >> 19u) & 0x7F0000007Fu)); + return ab_cd_ef_gh + (((std::uint64_t{1} << 8u) - 10u) * (((ab_cd_ef_gh * (((std::uint64_t{1} << 10u) / 10u) + 1u)) >> 10u) & 0x000F000F000F000Fu)); +} + +/// store the bytes of v, the most significant one first (one byte swap and +/// one store where the byte order is known: compilers do not reliably merge +/// the byte stores once this is inlined) +inline void store_msb_first(char* p, std::uint64_t v) noexcept +{ +#if JSON_LITTLE_ENDIAN_TARGET + v = byteswap(v); + std::memcpy(p, &v, sizeof(v)); +#elif JSON_BIG_ENDIAN_TARGET + std::memcpy(p, &v, sizeof(v)); +#else + for (unsigned i = 0; i < 8; ++i) + { + p[i] = static_cast(v >> (56u - (8u * i))); + } +#endif +} + +/*! +@brief digits * 10^exp for a double, in the layout of format_buffer() + +The layout is that of format_buffer() with min_exp -4 and max_exp 15 (the +digits10 of double). The digits are converted eight at a time and placed +with fixed-size moves instead of per-digit loops and moves of the buffer. + +@param[in] digits the digits (not 0, at most 17 digits; trailing zeros allowed) +@param[in] exp the decimal exponent of the last digit +@return a pointer past the text; up to 41 bytes at @a first are written + (some beyond the returned end) +*/ +JSON_HEDLEY_NON_NULL(1) +JSON_HEDLEY_RETURNS_NON_NULL +inline char* write_decimal(char* first, std::uint64_t digits, int exp) noexcept +{ + JSON_ASSERT(digits != 0 && digits < 100000000000000000u); + const std::uint64_t upper = digits / 100000000u; + const std::uint64_t b0 = upper / 100000000u; // (one digit: it is its own byte) + const std::uint64_t b1 = eight_digit_bytes(upper % 100000000u); + const std::uint64_t b2 = eight_digit_bytes(digits % 100000000u); + // leading and trailing zero digits: zero bytes, counted without division + int leading = 16; + int zeros = 16; + if (b0 != 0) + { + leading = count_leading_zeros(b0) / 8; + } + else if (b1 != 0) + { + leading = 8 + (count_leading_zeros(b1) / 8); + } + else + { + leading += count_leading_zeros(b2) / 8; + } + if (b2 != 0) + { + zeros = count_trailing_zeros(b2) / 8; + } + else if (b1 != 0) + { + zeros = 8 + (count_trailing_zeros(b1) / 8); + } + // (else: 16, b0 is the one digit that is not 0) + // the digits as text at text + leading, then '0's, so that fixed-size + // moves need not check how many digits there are + std::array text; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read + store_msb_first(text.data(), b0 + 0x3030303030303030u); + store_msb_first(text.data() + 8, b1 + 0x3030303030303030u); + store_msb_first(text.data() + 16, b2 + 0x3030303030303030u); + std::memset(text.data() + 24, '0', 40); + const int k = 24 - leading - zeros; // significant digits + const int n = k + exp + zeros; // position of the decimal point after the first digit + const char* const s0 = text.data() + leading; + + if (-4 < n && n <= 15) + { + // "0.[000]digits" (n <= 0) is the digits after 1 - n leading '0's + // with the point after the first; "digits[000].0" (n >= k) and + // "dig.its" put the point after n characters + const int pad = n <= 0 ? 1 - n : 0; + const char* const s = s0 - pad; + const int len = k + pad; + const int point = n + pad; + std::memcpy(first, s, 16); + std::memcpy(first + point + 1, s + point, 24); + first[point] = '.'; + return first + (point >= len ? point + 2 : len + 1); + } + + // d.igitse+XX, with at least two exponent digits (as append_exponent()) + std::memcpy(first, s0, 16); + std::memcpy(first + 2, s0 + 1, 16); + first[1] = '.'; + char* const end = first + (k == 1 ? 1 : k + 1); + const int e = n - 1; + const auto ea = e < 0 ? 0u - static_cast(e) : static_cast(e); // (unsigned: no signed overflow to assume) + const bool three = ea >= 100; + end[0] = 'e'; + end[1] = e < 0 ? '-' : '+'; + end[2] = static_cast('0' + (three ? ea / 100 : (ea / 10) % 10)); + end[3] = static_cast('0' + (three ? (ea / 10) % 10 : ea % 10)); + end[4] = static_cast('0' + (ea % 10)); + return end + (three ? 5 : 4); +} + +/*! +@brief the shortest decimal of a positive double (Zmij), as write_decimal() +writes it + +For a normal double, the shorter candidate has 15 or 16 digits: they are +converted at once (two halves of eight digits) and followed by the digit +after them, if there is one, without the multiplication and division by 10 +that counting the digits of one number would take. The fixed layouts move +the digits after the point by one byte. + +@return a pointer past the text; up to 41 bytes at @a first are written + (some beyond the returned end) +*/ +JSON_HEDLEY_NON_NULL(1) +JSON_HEDLEY_RETURNS_NON_NULL +inline char* write_shortest(char* first, const zmij::shortest_decimal d) noexcept +{ + const std::uint64_t sig = d.integral; + if (JSON_HEDLEY_UNLIKELY(sig < 100000000000000u || sig >= 10000000000000000u)) + { + // (subnormals) + return d.has_digit ? write_decimal(first, (sig * 10) + d.digit, d.exponent) : write_decimal(first, sig, d.exponent + 1); + } + const bool sixteen = sig >= 1000000000000000u; // (else 15 digits) + const int last = d.has_digit ? d.digit : 0; + const std::uint64_t upper = sig / 100000000u; +#if JSON_DTOA_SSE2 + // NOLINTBEGIN(portability-simd-intrinsics) + // the two halves in the 64-bit lanes, each as abcd * 2^32 + efgh, then as + // bytes (as eight_digit_bytes(), one lane each) + const __m128i x = _mm_set_epi64x(static_cast(sig - (upper * 100000000u)), static_cast(upper)); // NOLINT(runtime/int) + const __m128i abcd = _mm_srli_epi64(_mm_mul_epu32(x, _mm_set1_epi64x(109951163)), 40); // 2^40 / 10000 + 1 + const __m128i abcd_efgh = _mm_add_epi64(x, _mm_mul_epu32(abcd, _mm_set1_epi64x(4294957296))); // 2^32 - 10000 + // 32-bit lanes in the order of the text: abcd, efgh of both halves + const __m128i fours = _mm_shuffle_epi32(abcd_efgh, _MM_SHUFFLE(2, 3, 0, 1)); + const __m128i ab = _mm_srli_epi16(_mm_mulhi_epu16(fours, _mm_set1_epi32(5243)), 3); + const __m128i ab_cd = _mm_or_si128(_mm_slli_epi32(_mm_sub_epi16(fours, _mm_mullo_epi16(ab, _mm_set1_epi32(100))), 16), ab); + // 16-bit lanes ab (< 100) -> bytes a, b: 256 * ab - 2559 * (ab / 10) + const __m128i bytes = _mm_sub_epi16(_mm_slli_epi16(ab_cd, 8), _mm_mullo_epi16(_mm_set1_epi16(2559), _mm_mulhi_epu16(ab_cd, _mm_set1_epi16(6554)))); + // the last digit that is not 0 (sig is not 0) + const auto nonzero = static_cast(_mm_movemask_epi8(_mm_cmpgt_epi8(bytes, _mm_setzero_si128()))); + const int digits = 63 - count_leading_zeros(nonzero) + (sixteen ? 1 : 0); // without trailing zeros + const __m128i chars = _mm_add_epi8(bytes, _mm_set1_epi8('0')); + // the 16 characters from the first digit + const __m128i s = sixteen ? chars : _mm_or_si128(_mm_srli_si128(chars, 1), _mm_slli_si128(_mm_cvtsi32_si128('0' + last), 15)); + const char s16 = static_cast(sixteen ? '0' + last : '0'); // the 17th + const auto store_16 = [&s](char* p) noexcept + { + std::memcpy(p, &s, 16); + }; + const char first_digit = static_cast(_mm_cvtsi128_si32(s)); + // NOLINTEND(portability-simd-intrinsics) +#elif JSON_DTOA_NEON + // as with SSE2: the halves in 32-bit lanes, then abcd, efgh of both + const uint32x2_t halves = vcreate_u32(upper | ((sig - (upper * 100000000u)) << 32u)); + const uint32x2_t abcd = vmovn_u64(vshrq_n_u64(vmull_n_u32(halves, static_cast(((std::uint64_t{1} << 40u) / 10000u) + 1u)), 40)); + const uint32x2_t efgh = vmls_n_u32(halves, abcd, 10000u); + const uint32x4_t fours = vcombine_u32(vzip1_u32(abcd, efgh), vzip2_u32(abcd, efgh)); + const uint32x4_t ab = vshrq_n_u32(vmulq_n_u32(fours, 5243u), 19); + const uint16x8_t ab_cd = vreinterpretq_u16_u32(vorrq_u32(ab, vshlq_n_u32(vmlsq_n_u32(fours, ab, 100u), 16))); + const uint16x8_t tens = vshrq_n_u16(vmulq_n_u16(ab_cd, 103u), 10); + const uint8x16_t bytes = vreinterpretq_u8_u16(vorrq_u16(tens, vshlq_n_u16(vmlsq_n_u16(ab_cd, tens, 10u), 8))); + // the last digit that is not 0 (sig is not 0): a nibble per byte + const std::uint64_t nonzero = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(vtstq_u8(bytes, bytes)), 4)), 0); + const int digits = ((63 - count_leading_zeros(nonzero)) / 4) + (sixteen ? 1 : 0); // without trailing zeros + const uint8x16_t chars = vaddq_u8(bytes, vdupq_n_u8('0')); + // the 16 characters from the first digit + const uint8x16_t s = sixteen ? chars : vextq_u8(chars, vdupq_n_u8(static_cast('0' + last)), 1); + const char s16 = static_cast(sixteen ? '0' + last : '0'); // the 17th + const auto store_16 = [&s](char* p) noexcept + { + vst1q_u8(reinterpret_cast(p), s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + }; + const auto first_digit = static_cast(vgetq_lane_u8(s, 0)); +#else + const std::uint64_t hi = eight_digit_bytes(upper); + const std::uint64_t lo = eight_digit_bytes(sig - (upper * 100000000u)); + // trailing zero digits: zero bytes (sig is not 0) + const int zeros = lo != 0 ? count_trailing_zeros(lo) / 8 : 8 + (count_trailing_zeros(hi) / 8); + const int digits = 15 - zeros + (sixteen ? 1 : 0); // without trailing zeros + // the 16 characters from the first digit + const std::uint64_t s_hi = (sixteen ? hi : (hi << 8u) | (lo >> 56u)) + 0x3030303030303030u; + const std::uint64_t s_lo = (sixteen ? lo : (lo << 8u) | static_cast(last)) + 0x3030303030303030u; + const char s16 = static_cast(sixteen ? '0' + last : '0'); // the 17th + const auto store_16 = [s_hi, s_lo](char* p) noexcept + { + store_msb_first(p, s_hi); + store_msb_first(p + 8, s_lo); + }; + const auto first_digit = static_cast(s_hi >> 56u); +#endif + const int len = d.has_digit ? 16 + (sixteen ? 1 : 0) : digits; // significant digits + const int n = 16 + (sixteen ? 1 : 0) + d.exponent; // digits before the point + + if (JSON_HEDLEY_LIKELY(n >= 1 && n <= 15)) + { + // "dig.its" and "digits[000].0": the digits after the point move by + // one byte ('0's follow the digits) +#if JSON_DTOA_SSE2 + // NOLINTBEGIN(portability-simd-intrinsics) + // (in the register: reading the digits back from memory right after + // storing them waits until the stores are done) + const __m128i at = _mm_set1_epi8(static_cast(n)); + const __m128i index = _mm_setr_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15); + const __m128i before = _mm_cmpgt_epi8(at, index); + const __m128i after = _mm_cmpgt_epi8(index, at); + const __m128i text = _mm_or_si128(_mm_or_si128(_mm_and_si128(s, before), _mm_and_si128(_mm_slli_si128(s, 1), after)), + _mm_andnot_si128(_mm_or_si128(before, after), _mm_set1_epi8('.'))); + std::memcpy(first, &text, 16); + first[16] = static_cast(_mm_extract_epi16(s, 7) >> 8); + first[17] = s16; + // NOLINTEND(portability-simd-intrinsics) +#elif JSON_DTOA_NEON + const uint8x16_t index = vcombine_u8(vcreate_u8(0x0706050403020100u), vcreate_u8(0x0F0E0D0C0B0A0908u)); + const uint8x16_t at = vdupq_n_u8(static_cast(n)); + const uint8x16_t after_point = vbslq_u8(vcgtq_u8(index, at), vextq_u8(vdupq_n_u8(0), s, 15), vdupq_n_u8('.')); + vst1q_u8(reinterpret_cast(first), vbslq_u8(vcltq_u8(index, at), s, after_point)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + first[16] = static_cast(vgetq_lane_u8(s, 15)); + first[17] = s16; +#else + store_16(first); + first[16] = s16; + std::uint64_t after_point[2]; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays,cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read + std::memcpy(after_point, first + n, 16); + std::memcpy(first + n + 1, after_point, 16); + first[n] = '.'; +#endif + return first + (n >= len ? n + 2 : len + 1); + } + if (n <= 0 && n > -4) + { + // "0.[000]digits" + std::memset(first, '0', 8); + first[1] = '.'; + store_16(first + 2 - n); + first[18 - n] = s16; + return first + 2 - n + len; + } + // d.igitse+XX, with at least two exponent digits (as append_exponent()) + store_16(first + 1); + first[17] = s16; + first[0] = first_digit; + first[1] = '.'; + char* const end = first + (len == 1 ? 1 : len + 1); + const int e = n - 1; + const auto ea = e < 0 ? 0u - static_cast(e) : static_cast(e); // (unsigned: no signed overflow to assume) + const bool three = ea >= 100; + end[0] = 'e'; + end[1] = e < 0 ? '-' : '+'; + end[2] = static_cast('0' + (three ? ea / 100 : (ea / 10) % 10)); + end[3] = static_cast('0' + (three ? (ea / 10) % 10 : ea % 10)); + end[4] = static_cast('0' + (ea % 10)); + return end + (three ? 5 : 4); +} + +/// the powers of ten up to 10^19 (all that fit in std::uint64_t) +inline const std::array& powers_of_ten() noexcept +{ + static const std::array powers = + { + { + 1u, + 10u, + 100u, + 1000u, + 10000u, + 100000u, + 1000000u, + 10000000u, + 100000000u, + 1000000000u, + 10000000000u, + 100000000000u, + 1000000000000u, + 10000000000000u, + 100000000000000u, + 1000000000000000u, + 10000000000000000u, + 100000000000000000u, + 1000000000000000000u, + 10000000000000000000u + } + }; + return powers; +} + +/*! +@brief digits * 10^exp, as write_decimal() writes it, for the digits of a +double that need no conversion (count digits, at most 15, the first not 0; +trailing zeros allowed): extended to 16 digits and written by write_shortest() + +@return a pointer past the text; up to 41 bytes at @a first are written + (some beyond the returned end) +*/ +JSON_HEDLEY_NON_NULL(1) +JSON_HEDLEY_RETURNS_NON_NULL +inline char* write_short_decimal(char* first, std::uint64_t digits, int count, int exp) noexcept +{ + JSON_ASSERT(digits >= powers_of_ten()[static_cast(count - 1)] && count <= 15); + const int scale = 16 - count; + return write_shortest(first, zmij::shortest_decimal{digits * powers_of_ten()[static_cast(scale)], exp - scale - 1, 0, false}); +} + +/// as write_short_decimal(), counting the digits (not 0, less than 10^15) +JSON_HEDLEY_NON_NULL(1) +JSON_HEDLEY_RETURNS_NON_NULL +inline char* write_short_decimal(char* first, std::uint64_t digits, int exp) noexcept +{ + JSON_ASSERT(digits != 0 && digits < 1000000000000000u); + // floor(log10(2^bits)) + 1 digits, or one less + const int log2_bound = ((64 - count_leading_zeros(digits)) * 1233) >> 12; + const int count = log2_bound + (digits >= powers_of_ten()[static_cast(log2_bound)] ? 1 : 0); + return write_short_decimal(first, digits, count, exp); +} + +/*! +@brief whether FloatType is an IEEE 754 binary64 type (a double, or a long double +that has the same format, as with MSVC and on Apple's Arm CPUs) + +These are the types the conversion of Zmij (see zmij.hpp) is used for; all +others (binary32, or a format the library does not know) use Grisu2. +*/ +template +constexpr bool has_binary64_format() noexcept +{ + return std::numeric_limits::is_iec559 + && std::numeric_limits::digits == 53 + && std::numeric_limits::max_exponent == 1024 + && sizeof(FloatType) == sizeof(std::uint64_t); +} + +template +struct is_binary64 : std::integral_constant()> {}; + +/// a positive finite float (other than binary64): Grisu2 and format_buffer() +template +JSON_HEDLEY_NON_NULL(1, 2) +JSON_HEDLEY_RETURNS_NON_NULL +char* write_positive_grisu2(char* first, const char* last, FloatType value) +{ + JSON_ASSERT(last - first >= std::numeric_limits::max_digits10); + static_cast(last); // (only used in the assertion) + + // Compute v = buffer * 10^decimal_exponent. + // The decimal digits are stored in the buffer, which needs to be interpreted + // as an unsigned decimal integer. + // len is the length of the buffer, i.e., the number of decimal digits. + int len = 0; + int decimal_exponent = 0; + grisu2(first, len, decimal_exponent, value); + + JSON_ASSERT(len <= std::numeric_limits::max_digits10); + + // Format the buffer like printf("%.*g", prec, value) + constexpr int kMinExp = -4; + // Use digits10 here to increase compatibility with version 2. + constexpr int kMaxExp = std::numeric_limits::digits10; + + JSON_ASSERT(last - first >= kMaxExp + 2); + JSON_ASSERT(last - first >= 2 + (-kMinExp - 1) + std::numeric_limits::max_digits10); + JSON_ASSERT(last - first >= std::numeric_limits::max_digits10 + 6); + + return format_buffer(first, len, decimal_exponent, kMinExp, kMaxExp); +} + +/// a positive finite binary64 number: the shortest digits (Zmij), laid out by +/// write_shortest() (through a local buffer if [first, last) is shorter than +/// the 41 bytes it may write) +template +JSON_HEDLEY_NON_NULL(1, 2) +JSON_HEDLEY_RETURNS_NON_NULL +char* write_positive_zmij(char* first, const char* last, FloatType value) +{ + static_assert(is_binary64::value, + "internal error: the conversion of Zmij needs IEEE 754 binary64 numbers"); + const auto bits = bit_cast(value); + const zmij::shortest_decimal d = zmij::to_shortest(bits); + if (JSON_HEDLEY_LIKELY(last - first >= 41)) + { + return write_shortest(first, d); + } + std::array buf; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read + const auto len = static_cast(write_shortest(buf.data(), d) - buf.data()); + JSON_ASSERT(last - first >= static_cast(len)); + std::memcpy(first, buf.data(), len); + return first + len; +} + +/// a positive finite binary64 number: Zmij (as a long double has the format of +/// a double here, its bits are those of the double of the same value) +template +JSON_HEDLEY_NON_NULL(1, 2) +JSON_HEDLEY_RETURNS_NON_NULL +char* write_positive(char* first, const char* last, FloatType value, std::true_type /*is_binary64*/) +{ + return write_positive_zmij(first, last, value); +} + +/// a positive finite float of any other format: Grisu2 +template +JSON_HEDLEY_NON_NULL(1, 2) +JSON_HEDLEY_RETURNS_NON_NULL +char* write_positive(char* first, const char* last, FloatType value, std::false_type /*is_binary64*/) +{ + return write_positive_grisu2(first, last, value); +} + +/// a positive finite float: Zmij for binary64 numbers, Grisu2 otherwise +template +JSON_HEDLEY_NON_NULL(1, 2) +JSON_HEDLEY_RETURNS_NON_NULL +char* write_positive(char* first, const char* last, FloatType value) +{ + return write_positive(first, last, value, is_binary64 {}); +} + } // namespace dtoa_impl /*! @@ -1064,7 +1505,6 @@ JSON_HEDLEY_NON_NULL(1, 2) JSON_HEDLEY_RETURNS_NON_NULL char* to_chars(char* first, const char* last, FloatType value) { - static_cast(last); // maybe unused - fix warning JSON_ASSERT(std::isfinite(value)); // Use signbit(value) instead of (value < 0) since signbit works for -0. @@ -1090,28 +1530,7 @@ char* to_chars(char* first, const char* last, FloatType value) JSON_HEDLEY_DIAGNOSTIC_POP #endif - JSON_ASSERT(last - first >= std::numeric_limits::max_digits10); - - // Compute v = buffer * 10^decimal_exponent. - // The decimal digits are stored in the buffer, which needs to be interpreted - // as an unsigned decimal integer. - // len is the length of the buffer, i.e., the number of decimal digits. - int len = 0; - int decimal_exponent = 0; - dtoa_impl::grisu2(first, len, decimal_exponent, value); - - JSON_ASSERT(len <= std::numeric_limits::max_digits10); - - // Format the buffer like printf("%.*g", prec, value) - constexpr int kMinExp = -4; - // Use digits10 here to increase compatibility with version 2. - constexpr int kMaxExp = std::numeric_limits::digits10; - - JSON_ASSERT(last - first >= kMaxExp + 2); - JSON_ASSERT(last - first >= 2 + (-kMinExp - 1) + std::numeric_limits::max_digits10); - JSON_ASSERT(last - first >= std::numeric_limits::max_digits10 + 6); - - return dtoa_impl::format_buffer(first, len, decimal_exponent, kMinExp, kMaxExp); + return dtoa_impl::write_positive(first, last, value); } } // namespace detail diff --git a/include/nlohmann/detail/conversions/to_json.hpp b/include/nlohmann/detail/conversions/to_json.hpp index 8edaee2fe..05ae82370 100644 --- a/include/nlohmann/detail/conversions/to_json.hpp +++ b/include/nlohmann/detail/conversions/to_json.hpp @@ -13,7 +13,7 @@ #include // optional #endif -#include // begin, end +#include // begin, end, make_move_iterator, move_iterator #include // allocator_traits #include // basic_string, char_traits #include // tuple, get @@ -48,6 +48,32 @@ namespace detail * leave j with the type of the new value, but the pointer to the destroyed old one. */ +// a move iterator over the elements of an rvalue container, unless they are +// const (e.g., std::set): libstdc++ before GCC 5 defines the reference of a +// std::move_iterator as value_type&&, which cannot bind to a const element +template +std::move_iterator make_move_iterator_if_mutable_impl(const Iterator& it, std::true_type /*mutable*/) +{ + return std::make_move_iterator(it); +} + +template +Iterator make_move_iterator_if_mutable_impl(const Iterator& it, std::false_type /*mutable*/) +{ + return it; +} + +template +using is_mutable_iterator = std::integral_constant < bool, + !std::is_const())>::type>::value >; + +template +auto make_move_iterator_if_mutable(const Iterator& it) +-> decltype(make_move_iterator_if_mutable_impl(it, is_mutable_iterator {})) +{ + return make_move_iterator_if_mutable_impl(it, is_mutable_iterator {}); +} + template struct external_constructor; template<> @@ -167,6 +193,17 @@ struct external_constructor j.replace_value(value_t::array, value); } + /// an rvalue container: move its elements + template + static void construct_moving(BasicJsonType& j, CompatibleArrayType& arr) + { + using std::begin; + using std::end; + + const typename BasicJsonType::json_value value(j.template create(make_move_iterator_if_mutable(begin(arr)), make_move_iterator_if_mutable(end(arr)))); + j.replace_value(value_t::array, value); + } + template static void construct(BasicJsonType& j, const std::vector& arr) { @@ -217,6 +254,17 @@ struct external_constructor j.replace_value(value_t::object, value); } + /// an rvalue container: move its values + template + static void construct_moving(BasicJsonType& j, CompatibleObjectType& obj) + { + using std::begin; + using std::end; + + const typename BasicJsonType::json_value value(j.template create(make_move_iterator_if_mutable(begin(obj)), make_move_iterator_if_mutable(end(obj)))); + j.replace_value(value_t::object, value); + } + template < typename BasicJsonType, typename CompatibleObjectType, enable_if_t < !std::is_same::value, int > = 0 > static void construct(BasicJsonType& j, const CompatibleObjectType& obj) @@ -303,14 +351,14 @@ inline void to_json(BasicJsonType& j, FloatType val) noexcept } template::value, int> = 0> + enable_if_t, CompatibleNumberUnsignedType>::value, int> = 0> inline void to_json(BasicJsonType& j, CompatibleNumberUnsignedType val) noexcept { external_constructor::construct(j, static_cast(val)); } template::value, int> = 0> + enable_if_t::value, int> = 0> inline void to_json(BasicJsonType& j, CompatibleNumberIntegerType val) noexcept { external_constructor::construct(j, static_cast(val)); @@ -354,6 +402,68 @@ inline void to_json(BasicJsonType& j, const CompatibleArrayType& arr) external_constructor::construct(j, arr); } +// types that have a dedicated to_json overload and must not be treated as generic containers +template +struct has_dedicated_to_json : std::false_type {}; + +#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM +template<> +struct has_dedicated_to_json : std::true_type {}; +#endif + +#if defined(__cpp_lib_char8_t) +template +struct has_dedicated_to_json> : std::true_type {}; +#endif + +#if JSON_HAS_RANGE_VIEW_CONVERSION +template +struct is_range_view_type : is_compatible_range_view {}; +#else +template +struct is_range_view_type : std::false_type {}; +#endif + +#if JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS +template +struct is_enum_keyed_map_type : is_enum_keyed_map {}; +#else +template +struct is_enum_keyed_map_type : std::false_type {}; +#endif + +// rvalue (non-const) container: the generic to_json takes its elements by move +template +using is_movable_container_rvalue = negation, + std::is_const, + has_dedicated_to_json, + is_basic_json, + is_enum_keyed_map_type, + std::is_same>>>; + +// rvalue array-like containers; object types are ruled out before views are +// checked, as std::ranges::view for the object_t of an ordered_json would +// need that ordered_json to be complete (GCC 11, see #3312) +template +using is_rvalue_array_to_json = conjunction, + negation>, + negation>, + is_compatible_array_type, + negation, + std::is_same, + is_compatible_string_type, + is_compatible_binary_type>>>; + +// rvalue containers: their elements are moved instead of copied +template::value, int> = 0> +inline void to_json(BasicJsonType& j, CompatibleArrayType && arr) // NOLINT(cppcoreguidelines-missing-std-forward) +{ + external_constructor::construct_moving(j, arr); +} + #if JSON_HAS_RANGE_VIEW_CONVERSION template < typename BasicJsonType, typename T, enable_if_t < is_compatible_range_view>::value @@ -414,19 +524,33 @@ inline void to_json(BasicJsonType& j, const EnumKeyedMap& map) BasicJsonType key = p.first; if (JSON_HEDLEY_UNLIKELY(!key.is_string())) { - JSON_THROW(type_error::create(302, concat("type must be string, but is ", key.type_name()), &key)); + throw_type_must_be("string", key); } auto& key_string = *key.template get_ptr(); if (JSON_HEDLEY_UNLIKELY(!obj.emplace(key_string, BasicJsonType(p.second)).second)) { - JSON_THROW(type_error::create(318, concat("duplicate object key '", key_string, "'"), &key)); + JSON_THROW(type_error::create(exception_id::enum_key_duplicate, concat("duplicate object key '", key_string, "'"), &key)); } } external_constructor::construct(j, std::move(obj)); } #endif +// rvalue object-like containers (views have no key_type and mapped_type, so +// they are no compatible object type) +template +using is_rvalue_object_to_json = conjunction, + negation>, + is_compatible_object_type>; + +template::value, int> = 0> +inline void to_json(BasicJsonType& j, CompatibleObjectType && obj) // NOLINT(cppcoreguidelines-missing-std-forward) +{ + external_constructor::construct_moving(j, obj); +} + template inline void to_json(BasicJsonType& j, typename BasicJsonType::object_t&& obj) { diff --git a/include/nlohmann/detail/conversions/zmij.hpp b/include/nlohmann/detail/conversions/zmij.hpp new file mode 100644 index 000000000..3b2e8e7a3 --- /dev/null +++ b/include/nlohmann/detail/conversions/zmij.hpp @@ -0,0 +1,222 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2025 Victor Zverovich +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // array +#include // size_t +#include // uint32_t, uint64_t + +#include +#include +#include +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +/*! +@brief the shortest decimal representation of a double + +A C++11 port of the conversion of Zmij by Victor Zverovich +(https://github.com/vitaut/zmij, MIT license): the shortest decimal in the +rounding interval of a double, the closest one if there are several. Zmij +credits Xiang JunBo (producing the shorter candidate without a division) and +Dougall Johnson (the compressed powers of ten). The powers of ten are taken +from the table for number parsing (pow5_table.hpp) where it holds them, and +computed from the compressed tables of Zmij beyond it. +*/ +namespace zmij +{ + +/// the compressed powers of ten of Zmij +inline const std::array& pow10_minor() noexcept +{ + static const std::array table = + { + { + 0x8000000000000000u, 0xa000000000000000u, 0xc800000000000000u, 0xfa00000000000000u, 0x9c40000000000000u, + 0xc350000000000000u, 0xf424000000000000u, 0x9896800000000000u, 0xbebc200000000000u, 0xee6b280000000000u, + 0x9502f90000000000u, 0xba43b74000000000u, 0xe8d4a51000000000u, 0x9184e72a00000000u, 0xb5e620f480000000u, + 0xe35fa931a0000000u, 0x8e1bc9bf04000000u, 0xb1a2bc2ec5000000u, 0xde0b6b3a76400000u, 0x8ac7230489e80000u, + 0xad78ebc5ac620000u, 0xd8d726b7177a8000u, 0x878678326eac9000u, 0xa968163f0a57b400u, 0xd3c21bcecceda100u, + 0x84595161401484a0u, 0xa56fa5b99019a5c8u, 0xcecb8f27f4200f3au + } + }; + return table; +} + +/// (high, low) pairs +inline const std::array& pow10_major() noexcept +{ + static const std::array table = + { + { + 0xaddcb9e83c6b1793u, 0xdf4abe242a1bbf3eu, 0xaf8e5410288e1b6fu, 0x07ecf0ae5ee44ddau, 0xb1442798f49ffb4au, 0x99cd11cfdf41779du, + 0xb2fe3f0b8599ef07u, 0x861fa7e6dcb4aa15u, 0xb4bca50b065abe63u, 0x0fed077a756b53aau, 0xb67f6455292cbf08u, 0x1a3bc84c17b1d543u, + 0xb84687c269ef3bfbu, 0x3d5d514f40eea742u, 0xba121a4650e4ddebu, 0x92f34d62616ce413u, 0xbbe226efb628afeau, 0x890489f70a55368cu, + 0xbdb6b8e905cb600fu, 0x5400e987bbc1c921u, 0xbf8fdb78849a5f96u, 0xde98520472bdd034u, 0xc16d9a0095928a27u, 0x75b7053c0f178294u, + 0xc350000000000000u, 0x0000000000000000u, 0xc5371912364ce305u, 0x6c28000000000000u, 0xc722f0ef9d80aad6u, 0x424d3ad2b7b97ef6u, + 0xc913936dd571c84cu, 0x03bc3a19cd1e38eau, 0xcb090c8001ab551cu, 0x5cadf5bfd3072cc6u, 0xcd036837130890a1u, 0x36dba887c37a8c10u, + 0xcf02b2c21207ef2eu, 0x94f967e45e03f4bcu, 0xd106f86e69d785c7u, 0xe13336d701beba52u, 0xd31045a8341ca07cu, 0x1ede48111209a051u, + 0xd51ea6fa85785631u, 0x552a74227f3ea566u, 0xd732290fbacaf133u, 0xa97c177947ad4096u, 0xd94ad8b1c7380874u, 0x18375281ae7822bdu, + 0xdb68c2ca82ed2a05u, 0xa67398db9f6820e1u + } + }; + return table; +} + +/// one bit per power: whether the computed value is one unit too large +inline const std::array& pow10_fixups() noexcept +{ + static const std::array table = + { + { + 0x8d8fc810u, 0x06100293u, 0x19000000u, 0x00100000u, 0x00000908u, 0x00000000u, 0x04e00300u, 0x3807e0b2u, 0x3d83d793u, 0x0006f5ccu, + 0x00000000u, 0xffff0000u, 0x8076337du, 0x4ff45ba0u, 0x09405033u, 0x034376d9u, 0x09000000u, 0x4e100501u, 0x076d14dcu, 0xf964f45eu, + 0x0000003du + } + }; + return table; +} + +/// the 128-bit significand of 10^k, rounded down, for k in [-307, 341] +/// (compute_pow10 of Zmij) +inline uint128_parts compute_pow10(int k) noexcept +{ + const auto i = static_cast(k + 307); + const std::uint64_t m = pow10_minor()[(i + 24) % 28]; + const std::size_t j = 2 * static_cast((i + 24) / 28); + const std::uint64_t h_hi = pow10_major()[j]; + const std::uint64_t h_lo = pow10_major()[j + 1]; + const std::uint64_t h1 = full_multiplication(h_lo, m).high; + const std::uint64_t c0 = h_lo * m; + const std::uint64_t c1 = h1 + (h_hi * m); + const std::uint64_t c2 = (c1 < h1 ? 1u : 0u) + full_multiplication(h_hi, m).high; + uint128_parts r{}; + if ((c2 >> 63u) != 0) + { + r.high = c2; + r.low = c1; + } + else + { + r.high = (c2 << 1u) | (c1 >> 63u); + r.low = (c1 << 1u) | (c0 >> 63u); + } + r.low -= (pow10_fixups()[i >> 5u] >> (i & 31u)) & 1u; + return r; +} + +/// The 128-bit significand of 10^k, rounded down, for k in [-342, 341]. +/// Up to 10^308, the table for number parsing holds the same significands +/// (those of 5^k), except for k in [-27, -1], where it holds them one unit +/// larger (as the Eisel-Lemire algorithm needs them). +inline uint128_parts pow10(int k) noexcept +{ + if (k > pow5_128_largest_power) + { + return compute_pow10(k); // (only for the smallest doubles) + } + const auto i = 2 * static_cast(k - pow5_128_smallest_power); + uint128_parts r{pow5_128()[i + 1], pow5_128()[i]}; + const std::uint64_t adjust = static_cast(k + 27) < 27u ? 1u : 0u; + r.high -= r.low < adjust ? 1u : 0u; + r.low -= adjust; + return r; +} + +/// (x_hi * 2^64 + x_lo) * y >> 64, as 128 bits +inline uint128_parts umul192_hi128(std::uint64_t x_hi, std::uint64_t x_lo, std::uint64_t y) noexcept +{ + const uint128_parts p = full_multiplication(x_hi, y); + uint128_parts r{}; + r.low = p.low + full_multiplication(x_lo, y).high; + r.high = p.high + (r.low < p.low ? 1u : 0u); + return r; +} + +/// (x * y + c) >> 64 +inline std::uint64_t umul128_add_hi64(std::uint64_t x, std::uint64_t y, std::uint64_t c) noexcept +{ + const uint128_parts p = full_multiplication(x, y); + return p.high + (p.low + c < p.low ? 1u : 0u); +} + +/// the result of Zmij: the shorter candidate and, if that is outside the +/// rounding interval, the digit after it (16 bytes: returned in registers) +struct shortest_decimal +{ + std::uint64_t integral; ///< the shorter candidate (15 or 16 digits for normal doubles) + int exponent; ///< the decimal exponent of the digit after it + unsigned char digit; ///< the digit after it (if has_digit) + bool has_digit; ///< whether the shortest decimal is integral * 10 + digit +}; + +/// The shortest decimal in the rounding interval of a positive finite double +/// given by its bits, the closest one if there are several (to_decimal of +/// Zmij, which keeps the last digit apart: the 15 or 16 digits before it can be +/// converted without a multiplication by 10 first). Always inlined: GCC +/// otherwise calls it, and its result goes through memory. +JSON_HEDLEY_ALWAYS_INLINE shortest_decimal to_shortest(std::uint64_t bits) noexcept +{ + constexpr int extra_shift = 9; + const auto raw_exp = static_cast((bits >> 52u) & 0x7FFu); + std::uint64_t bin_sig = bits & ((std::uint64_t{1} << 52u) - 1); + // a power of two has a narrower interval below (except the smallest normal) + const bool regular = bin_sig != 0 || raw_exp <= 1; + const int bin_exp = (raw_exp == 0 ? 1 : raw_exp) - 1075; + if (raw_exp != 0) + { + bin_sig |= std::uint64_t{1} << 52u; + } + // floor(log10(2^bin_exp)), or floor(log10(3/4 * 2^bin_exp)) for the irregular case + const int dec_exp = ((bin_exp * 315653) - (regular ? 0 : 131072)) >> 20; + // scaled by 10^(-dec_exp - 1): the integral part is the shorter candidate + const int shift = bin_exp + ((-(dec_exp + 1) * 217707) >> 16) + 1 + extra_shift; + const uint128_parts p10 = pow10(-dec_exp - 1); + const uint128_parts p = umul192_hi128(p10.high, p10.low, bin_sig << static_cast(shift)); + std::uint64_t integral = p.high >> static_cast(extra_shift); + // (no casts in the shifts: MSVC 2015 warns C4554 on them despite the parentheses) + constexpr auto fractional_low_shift = static_cast(extra_shift); + constexpr unsigned fractional_high_shift = 64u - fractional_low_shift; + const std::uint64_t fractional = (p.high << fractional_high_shift) | (p.low >> fractional_low_shift); + std::uint64_t digit = 0; + bool round_up = false; + bool round_down = false; + if (JSON_HEDLEY_LIKELY(regular)) + { + const std::uint64_t half_ulp = (p10.high >> static_cast(extra_shift + 1 - shift)) + (1 - (bin_sig & 1u)); + round_up = fractional + half_ulp < fractional; + round_down = half_ulp > fractional; + // the last digit of the longer candidate, rounded to nearest + digit = umul128_add_hi64(fractional, 10, (std::uint64_t{1} << 63u) + 6); + if (fractional == (std::uint64_t{1} << 62u)) + { + digit = 2; // 2.5 rounds to 2 + } + } + else + { + const std::uint64_t half_ulp = p10.high >> static_cast(extra_shift + 1 - shift); + round_up = half_ulp > ~std::uint64_t{0} - fractional; + round_down = (half_ulp >> 1u) > fractional; + digit = umul128_add_hi64(fractional, 10, (std::uint64_t{1} << 63u) - 1); + const std::uint64_t lowest = umul128_add_hi64(fractional - (half_ulp >> 1u), 10, ~std::uint64_t{0}); + digit = digit < lowest ? lowest : digit; + } + integral += round_up ? 1u : 0u; + // if the shorter candidate is outside the rounding interval: one digit more + return shortest_decimal{integral, dec_exp, static_cast(digit), !round_up && !round_down}; +} + +} // namespace zmij +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/exceptions.hpp b/include/nlohmann/detail/exceptions.hpp index 155f4a4e5..cdef3019f 100644 --- a/include/nlohmann/detail/exceptions.hpp +++ b/include/nlohmann/detail/exceptions.hpp @@ -45,6 +45,88 @@ namespace detail // exceptions // //////////////// +/*! +@brief the ids of the exceptions thrown by the library +@note The values are part of the public API: they are the `id` member of the + exceptions and appear in their `what()` messages. +@sa https://json.nlohmann.me/home/exceptions/ +*/ +enum class exception_id : int +{ + // parse_error + syntax_error = 101, ///< unexpected token or invalid literal while parsing JSON + patch_not_an_array = 104, ///< a JSON Patch document is not an array of objects + patch_invalid_operation = 105, ///< a JSON Patch operation is malformed + pointer_index_leading_zero = 106, ///< a JSON Pointer array index has a leading zero + pointer_missing_slash = 107, ///< a JSON Pointer does not start with '/' + pointer_invalid_escape = 108, ///< a JSON Pointer contains an escape other than ~0 and ~1 + pointer_index_not_a_number = 109, ///< a JSON Pointer array index is not a number + unexpected_end_of_input = 110, ///< a binary input ends early, or has bytes left at its end + unexpected_byte = 112, ///< a binary input contains an unexpected byte or invalid length + invalid_string_or_size = 113, ///< a binary input contains an invalid string or size specification + bson_unsupported_type = 114, ///< a BSON record type is not supported + invalid_high_precision_number = 115, ///< a UBJSON/BJData high-precision number cannot be parsed + invalid_document_image = 116, ///< json_document::load() rejects an image + // invalid_iterator + iterators_incompatible = 201, ///< the iterators of a range belong to different values + iterator_from_other_value = 202, ///< an iterator does not belong to the value it is used with + iterator_range_from_other_value = 203, ///< an iterator range does not belong to the value it is used with + iterator_range_out_of_range = 204, ///< an iterator range of a primitive value is not [begin, end) + iterator_out_of_range = 205, ///< an iterator of a primitive value is not begin() + iterator_range_of_null = 206, ///< an iterator range belongs to a null value + iterator_key_not_object = 207, ///< key() is called on an iterator of a non-object + iterator_subscript_on_object = 208, ///< operator[] is called on an iterator of an object + iterator_arithmetic_on_object = 209, ///< an offset operator is used on an iterator of an object + insert_range_incompatible = 210, ///< the iterators of an inserted range belong to different values + insert_range_into_itself = 211, ///< an inserted range belongs to the value it is inserted into + iterators_compare_different_values = 212, ///< iterators of different values are compared + iterator_order_on_object = 213, ///< iterators of an object are compared by order + iterator_value_unavailable = 214, ///< an iterator does not refer to a value + // type_error + object_from_non_pairs = 301, ///< an object is created from an initializer list that is not a list of pairs + type_mismatch = 302, ///< a value has the wrong type for a conversion + incompatible_reference_type = 303, ///< get_ref() is called with a reference type that does not match the value + at_wrong_type = 304, ///< at() is called on a value of the wrong type + subscript_wrong_type = 305, ///< operator[] is called on a value of the wrong type + value_wrong_type = 306, ///< value() is called on a value of the wrong type + erase_wrong_type = 307, ///< erase() is called on a value of the wrong type + push_back_wrong_type = 308, ///< push_back() or operator+= is called on a value of the wrong type + insert_wrong_type = 309, ///< insert() is called on a value of the wrong type + swap_wrong_type = 310, ///< swap() is called on a value of the wrong type + emplace_wrong_type = 311, ///< emplace() or emplace_back() is called on a value of the wrong type + update_wrong_type = 312, ///< update() is called on a value of the wrong type + unflatten_invalid_value = 313, ///< unflatten() finds conflicting paths + unflatten_not_object = 314, ///< unflatten() is called on a non-object + unflatten_value_not_primitive = 315, ///< unflatten() is called on an object with non-primitive values + invalid_utf8 = 316, ///< dump() finds a string that is not valid UTF-8 + type_not_serializable = 317, ///< a value cannot be serialized to the requested binary format + enum_key_duplicate = 318, ///< an enum-keyed map has two keys that serialize to the same string + document_binary_value = 319, ///< a binary value is stored in a json_document + document_image_unsupported = 320, ///< a json_document image cannot be saved or loaded (discarded document, big-endian target) + discarded_value_used = 321, ///< a discarded value is used + // out_of_range + array_index_out_of_range = 401, ///< an array index is out of range + pointer_past_the_end_index = 402, ///< a JSON Pointer uses the array index '-' + key_not_found = 403, ///< an object key is not found + pointer_unresolved = 404, ///< a JSON Pointer reference token cannot be resolved + patch_on_root = 405, ///< the root of a value has no parent, e.g. for a JSON Patch 'remove' or 'add' at the root + number_overflow = 406, ///< a number cannot be stored without overflowing to NaN or INF + integer_too_large = 407, ///< an integer cannot be represented in the binary format + container_too_large = 408, ///< the size of a container in a binary input exceeds the maximal capacity + bson_key_with_null = 409, ///< a BSON key contains U+0000 + value_out_of_range = 410, ///< an enum value is undefined, or a JSON Pointer array index exceeds size_type + patch_add_parent_not_container = 411, ///< the parent of a JSON Patch 'add' target is not a container + length_too_large = 412, ///< a length does not fit into the length field of a binary format + patch_remove_parent_not_container = 413, ///< the parent of a JSON Patch 'remove' target is not a container + patch_move_into_child = 414, ///< a JSON Patch 'move' moves a value into one of its children + subtype_out_of_range = 415, ///< a binary subtype does not fit into one byte + document_too_large = 416, ///< a json_document input, its edit storage, or an image reaches the 32-bit (4 GiB) limit + // other_error + internal_error = 500, ///< unreachable code was reached + patch_test_failed = 501, ///< a JSON Patch 'test' operation failed + size_marker_required = 502 ///< UBJSON/BJData output with type markers requires size markers +}; + /// @brief general exception of the @ref basic_json class /// @sa https://json.nlohmann.me/api/basic_json/exception/ class exception : public std::exception @@ -68,6 +150,13 @@ class exception : public std::exception return concat("[json.exception.", ename, '.', std::to_string(id_), "] "); } + /// build the message "[json.exception..] " + template + static std::string what_impl(const std::string& ename, int id_, const std::string& what_arg, BasicJsonContext context) + { + return concat(name(ename, id_), diagnostics(context), what_arg); + } + static std::string diagnostics(std::nullptr_t /*leaf_element*/) { return ""; @@ -80,48 +169,33 @@ class exception : public std::exception std::vector tokens; for (const auto* current = leaf_element; current != nullptr && current->m_parent != nullptr; current = current->m_parent) { - switch (current->m_parent->type()) + const auto* parent = current->m_parent; + if (parent->is_array()) { - case value_t::array: + for (std::size_t i = 0; i < parent->m_data.m_value.array->size(); ++i) { - for (std::size_t i = 0; i < current->m_parent->m_data.m_value.array->size(); ++i) + if (&parent->m_data.m_value.array->operator[](i) == current) { - if (¤t->m_parent->m_data.m_value.array->operator[](i) == current) - { - tokens.emplace_back(std::to_string(i)); - break; - } + tokens.emplace_back(std::to_string(i)); + break; } - break; } - - case value_t::object: - { - for (const auto& element : *current->m_parent->m_data.m_value.object) - { - if (&element.second == current) - { - // data() is null-terminated, so a key containing - // a null byte is cut short here rather than - // truncating the whole message at what() - tokens.emplace_back(element.first.data()); - break; - } - } - break; - } - - case value_t::null: // LCOV_EXCL_LINE - case value_t::string: // LCOV_EXCL_LINE - case value_t::boolean: // LCOV_EXCL_LINE - case value_t::number_integer: // LCOV_EXCL_LINE - case value_t::number_unsigned: // LCOV_EXCL_LINE - case value_t::number_float: // LCOV_EXCL_LINE - case value_t::binary: // LCOV_EXCL_LINE - case value_t::discarded: // LCOV_EXCL_LINE - default: // LCOV_EXCL_LINE - break; // LCOV_EXCL_LINE } + else if (parent->is_object()) + { + for (const auto& element : *parent->m_data.m_value.object) + { + if (&element.second == current) + { + // data() is null-terminated, so a key containing + // a null byte is cut short here rather than + // truncating the whole message at what() + tokens.emplace_back(element.first.data()); + break; + } + } + } + // all other parent types (primitives) contribute no token } if (tokens.empty()) @@ -195,6 +269,18 @@ class parse_error : public exception return {id_, byte_, w.c_str()}; } + template::value, int> = 0> + static parse_error create(exception_id id_, const position_t& pos, const std::string& what_arg, BasicJsonContext context) + { + return create(static_cast(id_), pos, what_arg, context); + } + + template::value, int> = 0> + static parse_error create(exception_id id_, std::size_t byte_, const std::string& what_arg, BasicJsonContext context) + { + return create(static_cast(id_), byte_, what_arg, context); + } + /*! @brief byte index of the parse error @@ -225,10 +311,16 @@ class invalid_iterator : public exception template::value, int> = 0> static invalid_iterator create(int id_, const std::string& what_arg, BasicJsonContext context) { - const std::string w = concat(exception::name("invalid_iterator", id_), exception::diagnostics(context), what_arg); + const std::string w = exception::what_impl("invalid_iterator", id_, what_arg, context); return {id_, w.c_str()}; } + template::value, int> = 0> + static invalid_iterator create(exception_id id_, const std::string& what_arg, BasicJsonContext context) + { + return create(static_cast(id_), what_arg, context); + } + private: JSON_HEDLEY_NON_NULL(3) invalid_iterator(int id_, const char* what_arg) @@ -243,10 +335,16 @@ class type_error : public exception template::value, int> = 0> static type_error create(int id_, const std::string& what_arg, BasicJsonContext context) { - const std::string w = concat(exception::name("type_error", id_), exception::diagnostics(context), what_arg); + const std::string w = exception::what_impl("type_error", id_, what_arg, context); return {id_, w.c_str()}; } + template::value, int> = 0> + static type_error create(exception_id id_, const std::string& what_arg, BasicJsonContext context) + { + return create(static_cast(id_), what_arg, context); + } + private: JSON_HEDLEY_NON_NULL(3) type_error(int id_, const char* what_arg) : exception(id_, what_arg) {} @@ -260,10 +358,16 @@ class out_of_range : public exception template::value, int> = 0> static out_of_range create(int id_, const std::string& what_arg, BasicJsonContext context) { - const std::string w = concat(exception::name("out_of_range", id_), exception::diagnostics(context), what_arg); + const std::string w = exception::what_impl("out_of_range", id_, what_arg, context); return {id_, w.c_str()}; } + template::value, int> = 0> + static out_of_range create(exception_id id_, const std::string& what_arg, BasicJsonContext context) + { + return create(static_cast(id_), what_arg, context); + } + private: JSON_HEDLEY_NON_NULL(3) out_of_range(int id_, const char* what_arg) : exception(id_, what_arg) {} @@ -277,10 +381,16 @@ class other_error : public exception template::value, int> = 0> static other_error create(int id_, const std::string& what_arg, BasicJsonContext context) { - const std::string w = concat(exception::name("other_error", id_), exception::diagnostics(context), what_arg); + const std::string w = exception::what_impl("other_error", id_, what_arg, context); return {id_, w.c_str()}; } + template::value, int> = 0> + static other_error create(exception_id id_, const std::string& what_arg, BasicJsonContext context) + { + return create(static_cast(id_), what_arg, context); + } + private: JSON_HEDLEY_NON_NULL(3) other_error(int id_, const char* what_arg) : exception(id_, what_arg) {} @@ -307,6 +417,106 @@ void templated_json_throw(ExceptionType exception) (void)exception; } +/*! +@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] j the value with the wrong type +@throw type_error.302 always +*/ +template +JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_type_must_be(const char* expected, const BasicJsonType& j) +{ + static_cast(expected); // unused when JSON_NOEXCEPTION is defined + static_cast(j); + JSON_THROW(type_error::create(exception_id::type_mismatch, concat("type must be ", expected, ", but is ", j.type_name()), &j)); +} + +/*! +@brief throws because an operation is not supported for the type of @a j +@param[in] id_ the id of the type_error exception (at_wrong_type..update_wrong_type) +@param[in] operation the operation, e.g. "erase()" +@param[in] j the value the operation was called on +@throw type_error always +*/ +template +JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_cannot_use_with(const exception_id id_, const char* operation, const BasicJsonType& j) +{ + static_cast(id_); // unused when JSON_NOEXCEPTION is defined + static_cast(operation); + static_cast(j); + JSON_THROW(type_error::create(id_, concat("cannot use ", operation, " with ", j.type_name()), &j)); +} + +/*! +@brief throws ExceptionType with a fixed message; kept out of line so that callers do not carry the exception construction +*/ +template +JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_literal(const exception_id id_, const char* what_arg, BasicJsonContext context) +{ + static_cast(id_); // unused when JSON_NOEXCEPTION is defined + static_cast(what_arg); + static_cast(context); + JSON_THROW(ExceptionType::create(id_, what_arg, context)); +} + +// JSON Patch errors; constructed out of line so that patch_inplace() does not carry them + +template +JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_patch_add_parent(const BasicJsonType& parent) +{ + static_cast(parent); + JSON_THROW(out_of_range::create(exception_id::patch_add_parent_not_container, concat("cannot add value: the JSON Patch 'add' target's parent is of type ", parent.type_name(), ", but must be an object or array"), &parent)); +} + +template +JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_patch_remove_parent(const BasicJsonType& parent) +{ + static_cast(parent); + JSON_THROW(out_of_range::create(exception_id::patch_remove_parent_not_container, concat("cannot remove value: the JSON Patch 'remove' target's parent is of type ", parent.type_name(), ", but must be an object or array"), &parent)); +} + +template +JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_patch_not_an_array(const BasicJsonType& j) +{ + static_cast(j); + JSON_THROW(parse_error::create(exception_id::patch_not_an_array, 0, "JSON patch must be an array of objects", &j)); +} + +template +JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_patch_missing_member(const StringType& op, const StringType& member, const bool string_type, const BasicJsonType& val) +{ + static_cast(op); + static_cast(member); + static_cast(string_type); + static_cast(val); + // context-sensitive error message + JSON_THROW(parse_error::create(exception_id::patch_invalid_operation, 0, concat((op == "op") ? std::string("operation") : concat("operation '", op, '\''), string_type ? " must have string member '" : " must have member '", member, "'"), &val)); +} + +template +JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_patch_move_into_child(const StringType& from_path, const StringType& path, const BasicJsonType& result) +{ + static_cast(from_path); + static_cast(path); + static_cast(result); + JSON_THROW(out_of_range::create(exception_id::patch_move_into_child, concat("cannot move value: 'from' path '", from_path, "' is a proper prefix of 'path' '", path, "'"), &result)); +} + +template +JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_patch_test_failed(const BasicJsonType& val) +{ + static_cast(val); + JSON_THROW(other_error::create(exception_id::patch_test_failed, concat("unsuccessful: ", val.dump()), &val)); +} + +template +JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_patch_invalid_operation(const StringType& op, const BasicJsonType& val) +{ + static_cast(op); + static_cast(val); + JSON_THROW(parse_error::create(exception_id::patch_invalid_operation, 0, concat("operation value '", op, "' is invalid"), &val)); +} + } // namespace detail NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index 3dfff13b9..d3c3d5bdc 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -12,7 +12,6 @@ #include // ldexp #include // size_t #include // uint8_t, uint16_t, uint32_t, uint64_t, uintmax_t -#include // snprintf #include // memcpy #include // back_inserter #include // numeric_limits @@ -87,7 +86,7 @@ JSON_INLINE_VARIABLE constexpr std::size_t max_valueless_container_size = 1 << 2 /*! @brief deserialization of BJData, BON8, BSON, CBOR, MessagePack, and UBJSON values */ -template> +template> class binary_reader { using number_integer_t = typename BasicJsonType::number_integer_t; @@ -196,8 +195,7 @@ class binary_reader if (JSON_HEDLEY_UNLIKELY(current != char_traits::eof())) { - return sax->parse_error(chars_read, get_token_string(), parse_error::create(110, chars_read, - exception_message(concat("expected end of input; last byte: 0x", get_token_string()), "value"), nullptr)); + return last_byte_error(exception_id::unexpected_end_of_input, "expected end of input", "value", ""); } } @@ -295,6 +293,40 @@ class binary_reader return is_object ? sax->end_object() : sax->end_array(); } + /*! + @brief close the innermost open container and report whether that + completed the value being read + + The value begun by the driver is complete once its container is not + inside another one. + + @param[out] value_complete whether no container is left open + @return whether the SAX parser accepted the end event + */ + bool leave_container_and_check(bool& value_complete) + { + if (JSON_HEDLEY_UNLIKELY(!leave_container())) + { + return false; + } + value_complete = container_stack.empty(); + return true; + } + + /*! + @brief read an object key and pass it to the SAX parser + + @tparam GetKey member function that reads the key into a string + @param[in,out] key string the key is read into; cleared first + @return whether reading the key and the SAX event succeeded + */ + template + bool read_object_key(string_t& key) + { + key.clear(); + return (this->*GetKey)(key) && sax->key(key); + } + ////////// // BSON // ////////// @@ -319,8 +351,7 @@ class binary_reader { if (JSON_HEDLEY_UNLIKELY(document_size < 0 || static_cast(document_size) != chars_read - document_start)) { - return sax->parse_error(chars_read, get_token_string(), parse_error::create(112, chars_read, - exception_message(concat("document size ", std::to_string(document_size), " does not match the number of bytes read (", std::to_string(chars_read - document_start), ")"), "document"), nullptr)); + return bson_document_size_error(document_size, chars_read - document_start); } return true; } @@ -512,9 +543,7 @@ class binary_reader { if (JSON_HEDLEY_UNLIKELY(len < 1)) { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message(concat("string length must be at least 1, is ", std::to_string(len)), "string"), nullptr)); + return bson_length_error("string length must be at least 1, is ", len, "string"); } if (JSON_HEDLEY_UNLIKELY(!get_string(len - static_cast(1), result))) @@ -524,10 +553,7 @@ class binary_reader if (JSON_HEDLEY_UNLIKELY(get() != 0x00)) { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message("BSON string is not null-terminated", - "string"), nullptr)); + return last_byte_error(exception_id::unexpected_byte, "BSON string is not null-terminated", "string"); } return check_string_utf8(result, "string"); @@ -547,9 +573,7 @@ class binary_reader { if (JSON_HEDLEY_UNLIKELY(len < 0)) { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message(concat("byte array length cannot be negative, is ", std::to_string(len)), "binary"), nullptr)); + return bson_length_error("byte array length cannot be negative, is ", len, "binary"); } // All BSON binary values have a subtype @@ -639,11 +663,7 @@ class binary_reader default: // anything else is not supported (yet) { - std::array cr{{}}; - static_cast((std::snprintf)(cr.data(), cr.size(), "%.2hhX", static_cast(element_type))); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) - const std::string cr_str{cr.data()}; - return sax->parse_error(element_type_parse_position, cr_str, - parse_error::create(114, element_type_parse_position, concat("Unsupported BSON record type 0x", cr_str), nullptr)); + return bson_unsupported_type_error(element_type_parse_position, element_type); } } } @@ -967,9 +987,7 @@ class binary_reader { if (tag_handler == cbor_tag_handler_t::error) { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message(concat("invalid byte: 0x", last_token), "value"), nullptr)); + return unexpected_byte("invalid byte", "value"); } // ignore and store: the tag value is already in the head, so @@ -989,9 +1007,7 @@ class binary_reader { case cbor_tag_handler_t::error: { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message(concat("invalid byte: 0x", last_token), "value"), nullptr)); + return unexpected_byte("invalid byte", "value"); } case cbor_tag_handler_t::ignore: @@ -1065,9 +1081,7 @@ class binary_reader default: // anything else (0xFF is handled inside the other types) { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message(concat("invalid byte: 0x", last_token), "value"), nullptr)); + return unexpected_byte("invalid byte", "value"); } } } @@ -1078,12 +1092,38 @@ class binary_reader @param[in] context parsing context for the error message @return whether the SAX consumer accepts the parse error */ + JSON_HEDLEY_NEVER_INLINE bool cbor_indefinite_string_error(const char* type_name, const char* context) { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, - exception_message(concat("indefinite-length ", type_name, - " is not allowed inside indefinite-length ", type_name, "; last byte: 0x", last_token), context), nullptr)); + return last_byte_error(exception_id::invalid_string_or_size, concat("indefinite-length ", type_name, " is not allowed inside indefinite-length ", type_name, "; last byte: 0x", get_token_string()), context); + } + + /*! + @brief report an object key that is not a string + + @param[in] found description of what was found instead of a string key + @return false + */ + bool unsupported_key_type(const char* found) const + { + return last_byte_error(exception_id::invalid_string_or_size, "only string keys are supported, but found ", "object key", found); + } + + /*! + @brief read the length of a CBOR string or byte array and then its bytes + + @tparam NumberType the type of the length field + @tparam ContainerType the destination container (string_t or binary_t) + @param[in] context further context information (for diagnostics) + @param[out] result container the bytes are appended to + + @return whether reading completed + */ + template + bool get_cbor_sized_bytes(const char* context, ContainerType& result) + { + NumberType len{}; + return get_number(len) && get_bytes(len, context, result); } /*! @@ -1136,33 +1176,27 @@ class binary_reader case 0x78: // UTF-8 string (one-byte uint8_t for n follows) { - std::uint8_t len{}; - return get_number(len) && get_string(len, result); + return get_cbor_sized_bytes("string", result); } case 0x79: // UTF-8 string (two-byte uint16_t for n follow) { - std::uint16_t len{}; - return get_number(len) && get_string(len, result); + return get_cbor_sized_bytes("string", result); } case 0x7A: // UTF-8 string (four-byte uint32_t for n follow) { - std::uint32_t len{}; - return get_number(len) && get_string(len, result); + return get_cbor_sized_bytes("string", result); } case 0x7B: // UTF-8 string (eight-byte uint64_t for n follow) { - std::uint64_t len{}; - return get_number(len) && get_string(len, result); + return get_cbor_sized_bytes("string", result); } default: { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, - exception_message(concat("expected length specification (0x60-0x7B)", inside_indefinite ? "" : " or indefinite string type (0x7F)", "; last byte: 0x", last_token), "string"), nullptr)); + return last_byte_error(exception_id::invalid_string_or_size, "expected length specification (0x60-0x7B)", "string", inside_indefinite ? "" : " or indefinite string type (0x7F)"); } } } @@ -1292,9 +1326,7 @@ class binary_reader break; } - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, - exception_message(concat("only string keys are supported, but found ", found, "; last byte: 0x", last_token), "object key"), nullptr)); + return unsupported_key_type(found); } /*! @@ -1347,37 +1379,27 @@ class binary_reader case 0x58: // Binary data (one-byte uint8_t for n follows) { - std::uint8_t len{}; - return get_number(len) && - get_binary(len, result); + return get_cbor_sized_bytes("binary", result); } case 0x59: // Binary data (two-byte uint16_t for n follow) { - std::uint16_t len{}; - return get_number(len) && - get_binary(len, result); + return get_cbor_sized_bytes("binary", result); } case 0x5A: // Binary data (four-byte uint32_t for n follow) { - std::uint32_t len{}; - return get_number(len) && - get_binary(len, result); + return get_cbor_sized_bytes("binary", result); } case 0x5B: // Binary data (eight-byte uint64_t for n follow) { - std::uint64_t len{}; - return get_number(len) && - get_binary(len, result); + return get_cbor_sized_bytes("binary", result); } default: { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, - exception_message(concat("expected length specification (0x40-0x5B)", inside_indefinite ? "" : " or indefinite binary array type (0x5F)", "; last byte: 0x", last_token), "binary"), nullptr)); + return last_byte_error(exception_id::invalid_string_or_size, "expected length specification (0x40-0x5B)", "binary", inside_indefinite ? "" : " or indefinite binary array type (0x5F)"); } } } @@ -1523,8 +1545,7 @@ class binary_reader { if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(len) || len == detail::unknown_size())) { - return sax->parse_error(chars_read, get_token_string(), out_of_range::create(408, - exception_message(concat("excessive ", context, " size"), "size"), nullptr)); + return container_size_error(context); } result = conditional_static_cast(len); return true; @@ -1588,12 +1609,12 @@ class binary_reader if (at_end) { - if (JSON_HEDLEY_UNLIKELY(!leave_container())) + bool value_complete = false; + if (JSON_HEDLEY_UNLIKELY(!leave_container_and_check(value_complete))) { return false; } - // the value begun here is complete once its container is - if (container_stack.empty()) + if (value_complete) { return true; } @@ -1602,8 +1623,7 @@ class binary_reader if (top.is_object) { - key.clear(); - if (JSON_HEDLEY_UNLIKELY(!get_cbor_object_key(key) || !sax->key(key))) + if (JSON_HEDLEY_UNLIKELY(!read_object_key<&binary_reader::get_cbor_object_key>(key))) { return false; } @@ -2011,9 +2031,7 @@ class binary_reader default: // anything else { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message(concat("invalid byte: 0x", last_token), "value"), nullptr)); + return unexpected_byte("invalid byte", "value"); } } } @@ -2076,27 +2094,22 @@ class binary_reader case 0xD9: // str 8 { - std::uint8_t len{}; - return get_number(len) && get_string(len, result) && check_string_utf8(result, context); + return get_string_with_length(result, context); } case 0xDA: // str 16 { - std::uint16_t len{}; - return get_number(len) && get_string(len, result) && check_string_utf8(result, context); + return get_string_with_length(result, context); } case 0xDB: // str 32 { - std::uint32_t len{}; - return get_number(len) && get_string(len, result) && check_string_utf8(result, context); + return get_string_with_length(result, context); } default: { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, - exception_message(concat("expected length specification (0xA0-0xBF, 0xD9-0xDB); last byte: 0x", last_token), "string"), nullptr)); + return last_byte_error(exception_id::invalid_string_or_size, "expected length specification (0xA0-0xBF, 0xD9-0xDB)", "string", ""); } } } @@ -2188,9 +2201,7 @@ class binary_reader break; } - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, - exception_message(concat("only string keys are supported, but found ", found, "; last byte: 0x", last_token), "object key"), nullptr)); + return unsupported_key_type(found); } /*! @@ -2335,12 +2346,12 @@ class binary_reader if (container_stack.back().remaining == 0) { - if (JSON_HEDLEY_UNLIKELY(!leave_container())) + bool value_complete = false; + if (JSON_HEDLEY_UNLIKELY(!leave_container_and_check(value_complete))) { return false; } - // the value begun here is complete once its container is - if (container_stack.empty()) + if (value_complete) { return true; } @@ -2353,8 +2364,7 @@ class binary_reader if (is_object) { get(); - key.clear(); - if (JSON_HEDLEY_UNLIKELY(!get_msgpack_object_key(key) || !sax->key(key))) + if (JSON_HEDLEY_UNLIKELY(!read_object_key<&binary_reader::get_msgpack_object_key>(key))) { return false; } @@ -2499,8 +2509,7 @@ class binary_reader { if (JSON_HEDLEY_UNLIKELY(len < 0)) { - return sax->parse_error(chars_read, get_token_string(), parse_error::create(113, chars_read, - exception_message("string length must not be negative", "string"), nullptr)); + return last_byte_error(exception_id::invalid_string_or_size, "string length must not be negative", "string"); } return true; } @@ -2539,8 +2548,7 @@ class binary_reader { case 'U': { - std::uint8_t len{}; - return get_number(len) && get_string(len, result) && check_string_utf8(result, context); + return get_string_with_length(result, context); } case 'i': @@ -2573,8 +2581,7 @@ class binary_reader { break; } - std::uint16_t len{}; - return get_number(len) && get_string(len, result) && check_string_utf8(result, context); + return get_string_with_length(result, context); } case 'm': @@ -2583,8 +2590,7 @@ class binary_reader { break; } - std::uint32_t len{}; - return get_number(len) && get_string(len, result) && check_string_utf8(result, context); + return get_string_with_length(result, context); } case 'M': @@ -2593,25 +2599,13 @@ class binary_reader { break; } - std::uint64_t len{}; - return get_number(len) && get_string(len, result) && check_string_utf8(result, context); + return get_string_with_length(result, context); } default: break; } - auto last_token = get_token_string(); - std::string message; - - if (input_format != input_format_t::bjdata) - { - message = "expected length type specification (U, i, I, l, L); last byte: 0x" + last_token; - } - else - { - message = "expected length type specification (U, i, u, I, m, l, M, L); last byte: 0x" + last_token; - } - return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, exception_message(message, "string"), nullptr)); + return length_type_error("", "string"); } /*! @@ -2696,13 +2690,11 @@ class binary_reader } if (JSON_HEDLEY_UNLIKELY(number < 0)) { - return sax->parse_error(chars_read, get_token_string(), parse_error::create(113, chars_read, - exception_message("count in an optimized container must be positive", "size"), nullptr)); + return last_byte_error(exception_id::invalid_string_or_size, "count in an optimized container must be positive", "size"); } if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(number))) { - return sax->parse_error(chars_read, get_token_string(), out_of_range::create(408, - exception_message("integer value overflow", "size"), nullptr)); + return out_of_range_error(exception_id::container_too_large, "integer value overflow", "size"); } result = static_cast(number); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): number is not a char return true; @@ -2799,8 +2791,7 @@ class binary_reader } if (!value_in_range_of(number)) { - return sax->parse_error(chars_read, get_token_string(), out_of_range::create(408, - exception_message("integer value overflow", "size"), nullptr)); + return out_of_range_error(exception_id::container_too_large, "integer value overflow", "size"); } result = detail::conditional_static_cast(number); return true; @@ -2814,7 +2805,7 @@ class binary_reader } if (is_ndarray) // ndarray dimensional vector can only contain integers and cannot embed another array { - return sax->parse_error(chars_read, get_token_string(), parse_error::create(113, chars_read, exception_message("ndarray dimensional vector is not allowed", "size"), nullptr)); + return last_byte_error(exception_id::invalid_string_or_size, "ndarray dimensional vector is not allowed", "size"); } std::vector dim; if (JSON_HEDLEY_UNLIKELY(!get_ubjson_ndarray_size(dim))) @@ -2850,9 +2841,7 @@ class binary_reader const char* type_name = bjd_type_name(ndarray_dtype); if (JSON_HEDLEY_UNLIKELY(type_name == nullptr)) { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message("invalid byte: 0x" + last_token, "type"), nullptr)); + return unexpected_byte("invalid byte", "type"); } string_t type_key = "_ArrayType_"; @@ -2880,7 +2869,7 @@ class binary_reader // or SIZE_MAX. if (JSON_HEDLEY_UNLIKELY(result > (std::numeric_limits::max)() / i)) { - return sax->parse_error(chars_read, get_token_string(), out_of_range::create(408, exception_message("excessive ndarray size caused overflow", "size"), nullptr)); + return out_of_range_error(exception_id::container_too_large, "excessive ndarray size caused overflow", "size"); } result *= i; // the pre-check above already rules out result becoming 0 @@ -2889,7 +2878,7 @@ class binary_reader // unknown-size container (see get_ubjson_size_type()) if (result == npos) { - return sax->parse_error(chars_read, get_token_string(), out_of_range::create(408, exception_message("excessive ndarray size caused overflow", "size"), nullptr)); + return out_of_range_error(exception_id::container_too_large, "excessive ndarray size caused overflow", "size"); } if (JSON_HEDLEY_UNLIKELY(!emit_unsigned(i))) { @@ -2906,18 +2895,7 @@ class binary_reader default: break; } - auto last_token = get_token_string(); - std::string message; - - if (input_format != input_format_t::bjdata) - { - message = "expected length type specification (U, i, I, l, L) after '#'; last byte: 0x" + last_token; - } - else - { - message = "expected length type specification (U, i, u, I, m, l, M, L) after '#'; last byte: 0x" + last_token; - } - return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, exception_message(message, "size"), nullptr)); + return length_type_error(" after '#'", "size"); } /*! @@ -2950,9 +2928,7 @@ class binary_reader if (input_format == input_format_t::bjdata && JSON_HEDLEY_UNLIKELY(is_bjd_excluded_optimized_type(result.second))) { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message(concat("marker 0x", last_token, " is not a permitted optimized array type"), "type"), nullptr)); + return optimized_type_error(); } if (JSON_HEDLEY_UNLIKELY(!unexpect_eof("type"))) @@ -2967,9 +2943,7 @@ class binary_reader { return false; } - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message(concat("expected '#' after type information; last byte: 0x", last_token), "size"), nullptr)); + return last_byte_error(exception_id::unexpected_byte, "expected '#' after type information", "size", ""); } const bool is_error = get_ubjson_size_value(result.first, is_ndarray, 0, result.second); @@ -2988,8 +2962,7 @@ class binary_reader const bool is_error = get_ubjson_size_value(result.first, is_ndarray); if (input_format == input_format_t::bjdata && is_ndarray && !inside_ndarray) { - return sax->parse_error(chars_read, get_token_string(), parse_error::create(112, chars_read, - exception_message("ndarray requires both type and size", "size"), nullptr)); + return last_byte_error(exception_id::unexpected_byte, "ndarray requires both type and size", "size"); } return is_error; } @@ -3121,9 +3094,7 @@ class binary_reader } if (JSON_HEDLEY_UNLIKELY(current > 127)) { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, - exception_message(concat("byte after 'C' must be in range 0x00..0x7F; last byte: 0x", last_token), "char"), nullptr)); + return last_byte_error(exception_id::invalid_string_or_size, "byte after 'C' must be in range 0x00..0x7F", "char", ""); } string_t s(1, static_cast(current)); return sax->string(s); @@ -3144,8 +3115,7 @@ class binary_reader default: // anything else break; } - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, exception_message("invalid byte: 0x" + last_token, "value"), nullptr)); + return unexpected_byte("invalid byte", "value"); } /*! @@ -3208,8 +3178,7 @@ class binary_reader if (JSON_HEDLEY_UNLIKELY((size_and_type.second == 'Z' || size_and_type.second == 'T' || size_and_type.second == 'F') && size_and_type.first > max_valueless_container_size)) { - return sax->parse_error(chars_read, get_token_string(), out_of_range::create(408, - exception_message("excessive array size", "size"), nullptr)); + return out_of_range_error(exception_id::container_too_large, "excessive array size", "size"); } if (JSON_HEDLEY_UNLIKELY(!enter_array(size_and_type.first, size_and_type.second))) @@ -3244,9 +3213,7 @@ class binary_reader // do not accept ND-array size in objects in BJData if (input_format == input_format_t::bjdata && size_and_type.first != npos && (size_and_type.second & (1 << 8)) != 0) { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message("BJData object does not support ND-array size in optimized format", "object"), nullptr)); + return last_byte_error(exception_id::unexpected_byte, "BJData object does not support ND-array size in optimized format", "object"); } if (size_and_type.first != npos) @@ -3283,7 +3250,7 @@ class binary_reader // the lexer would stop at a NUL and accept the digits before it if (JSON_HEDLEY_UNLIKELY(current == '\0')) { - return sax->parse_error(chars_read, "00", parse_error::create(115, chars_read, + return sax->parse_error(chars_read, "00", parse_error::create(exception_id::invalid_high_precision_number, chars_read, exception_message("invalid number text; last byte: 0x00", "high-precision number"), nullptr)); } number_vector.push_back(static_cast(current)); @@ -3300,7 +3267,7 @@ class binary_reader if (JSON_HEDLEY_UNLIKELY(result_remainder != token_type::end_of_input)) { - return sax->parse_error(chars_read, number_string, parse_error::create(115, chars_read, + return sax->parse_error(chars_read, number_string, parse_error::create(exception_id::invalid_high_precision_number, chars_read, exception_message(concat("invalid number text: ", number_lexer.get_token_string()), "high-precision number"), nullptr)); } @@ -3318,7 +3285,7 @@ class binary_reader return sax->parse_error( chars_read, number_string, - out_of_range::create(406, concat("number overflow parsing '", number_string, '\''), nullptr)); + out_of_range::create(exception_id::number_overflow, concat("number overflow parsing '", number_string, '\''), nullptr)); } // number_string is a std::string, while the SAX interface takes a // string_t; convert explicitly, as the two are only implicitly @@ -3340,7 +3307,7 @@ class binary_reader case token_type::end_of_input: case token_type::literal_or_value: default: - return sax->parse_error(chars_read, number_string, parse_error::create(115, chars_read, + return sax->parse_error(chars_read, number_string, parse_error::create(exception_id::invalid_high_precision_number, chars_read, exception_message(concat("invalid number text: ", number_lexer.get_token_string()), "high-precision number"), nullptr)); } } @@ -3399,20 +3366,6 @@ class binary_reader return 0x80 <= c && c <= 0xBF; } - /*! - @brief report a parse error at the last read byte - - @param[in] detail a detailed error message - @param[in] context further context information - @return false - */ - bool bon8_error(const std::string& detail, const char* context) - { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message(concat(detail, ": 0x", last_token), context), nullptr)); - } - /*! @brief read a BON8 value and everything nested inside it @@ -3462,12 +3415,12 @@ class binary_reader if (at_end) { - if (JSON_HEDLEY_UNLIKELY(!leave_container())) + bool value_complete = false; + if (JSON_HEDLEY_UNLIKELY(!leave_container_and_check(value_complete))) { return false; } - // the value begun here is complete once its container is - if (container_stack.empty()) + if (value_complete) { return true; } @@ -3476,8 +3429,7 @@ class binary_reader if (top.is_object) { - key.clear(); - if (JSON_HEDLEY_UNLIKELY(!get_bon8_key(key) || !sax->key(key))) + if (JSON_HEDLEY_UNLIKELY(!read_object_key<&binary_reader::get_bon8_key>(key))) { return false; } @@ -3628,7 +3580,7 @@ class binary_reader } // 0xFE: end of container where a value is expected - return bon8_error("invalid byte", "value"); + return unexpected_byte("invalid byte", "value"); } /*! @@ -3755,7 +3707,7 @@ class binary_reader current = byte; } - return bon8_error("expected a string; last byte", "key"); + return unexpected_byte("expected a string; last byte", "key"); } /*! @@ -3878,7 +3830,7 @@ class binary_reader if (JSON_HEDLEY_UNLIKELY(!valid_second)) { - return bon8_error("invalid UTF-8 byte", "string"); + return unexpected_byte("invalid UTF-8 byte", "string"); } result.push_back(static_cast(byte)); @@ -3892,7 +3844,7 @@ class binary_reader } if (JSON_HEDLEY_UNLIKELY(!is_bon8_continuation(current))) { - return bon8_error("invalid UTF-8 byte", "string"); + return unexpected_byte("invalid UTF-8 byte", "string"); } result.push_back(static_cast(current)); } @@ -3937,7 +3889,7 @@ class binary_reader { // in case of failure, advance position by 1 to report the failing location ++chars_read; - sax->parse_error(chars_read, "", parse_error::create(110, chars_read, exception_message("unexpected end of input", context), nullptr)); + static_cast(report_eof(context)); return false; } return true; @@ -4090,8 +4042,7 @@ class binary_reader const auto result = static_cast(number); if (JSON_HEDLEY_UNLIKELY(std::isfinite(number) && !std::isfinite(result))) { - return sax->parse_error(chars_read, get_token_string(), - out_of_range::create(406, exception_message("number overflow", "value"), nullptr)); + return out_of_range_error(exception_id::number_overflow, "number overflow", "value"); } return sax->number_float(result, ""); } @@ -4211,15 +4162,29 @@ class binary_reader if (error_handler == error_handler_t::strict) { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, - exception_message("invalid string: ill-formed UTF-8 byte", context), nullptr)); + return last_byte_error(exception_id::invalid_string_or_size, "invalid string: ill-formed UTF-8 byte", context); } result = sanitize_utf8(result, error_handler); return true; } + /*! + @brief read a length of type @a NumberType, then a string of that many bytes + + @tparam NumberType the type of the length field + @param[out] result string the bytes are appended to + @param[in] context further context information (for diagnostics) + + @return whether reading and validating the string succeeded + */ + template + bool get_string_with_length(string_t& result, const char* context) + { + NumberType len{}; + return get_number(len) && get_string(len, result) && check_string_utf8(result, context); + } + /*! @brief create a byte array by reading bytes from the input @@ -4308,20 +4273,116 @@ class binary_reader { if (JSON_HEDLEY_UNLIKELY(current == char_traits::eof())) { - return sax->parse_error(chars_read, "", - parse_error::create(110, chars_read, exception_message("unexpected end of input", context), nullptr)); + return report_eof(context); } return true; } + /*! + @brief reports an unexpected end of input (out of line to keep the exception + construction out of the hot reading functions) + @param[in] context further context information (for diagnostics) + @return the result of the SAX parser's parse_error() + */ + JSON_HEDLEY_NON_NULL(2) + JSON_HEDLEY_NEVER_INLINE + bool report_eof(const char* context) const + { + return sax->parse_error(chars_read, "", + parse_error::create(exception_id::unexpected_end_of_input, chars_read, exception_message("unexpected end of input", context), nullptr)); + } + + /*! + @brief reports an out_of_range error at the last read byte (out of line) + */ + JSON_HEDLEY_NEVER_INLINE + bool out_of_range_error(const exception_id id_, const char* detail, const char* context) const + { + return sax->parse_error(chars_read, get_token_string(), out_of_range::create(id_, exception_message(detail, context), nullptr)); + } + + JSON_HEDLEY_NEVER_INLINE + bool container_size_error(const char* what) const + { + return sax->parse_error(chars_read, get_token_string(), out_of_range::create(exception_id::container_too_large, + exception_message(concat("excessive ", what, " size"), "size"), nullptr)); + } + + JSON_HEDLEY_NEVER_INLINE + bool optimized_type_error() const + { + return last_byte_error(exception_id::unexpected_byte, concat("marker 0x", get_token_string(), " is not a permitted optimized array type"), "type"); + } + + template + JSON_HEDLEY_NEVER_INLINE + bool bson_length_error(const char* prefix, const NumberType len, const char* context) const + { + return last_byte_error(exception_id::unexpected_byte, concat(prefix, std::to_string(len)), context); + } + + JSON_HEDLEY_NEVER_INLINE + bool bson_document_size_error(const std::int32_t document_size, const std::size_t bytes_read) const + { + return last_byte_error(exception_id::unexpected_byte, concat("document size ", std::to_string(document_size), " does not match the number of bytes read (", std::to_string(bytes_read), ")"), "document"); + } + + JSON_HEDLEY_NEVER_INLINE + bool bson_unsupported_type_error(const std::size_t position, const char_int_type element_type) const + { + const std::string cr_str = hex_byte(static_cast(element_type)); + return sax->parse_error(position, cr_str, + parse_error::create(exception_id::bson_unsupported_type, position, concat("Unsupported BSON record type 0x", cr_str), nullptr)); + } + + /*! + @brief reports a parse error at the last read byte + @param[in] id_ the id of the parse_error exception + @param[in] detail a detailed error message + @param[in] context further context information + @param[in] tail optional text appended to the detail, followed by "; last byte: 0x" + @return the result of the SAX parser's parse_error() + */ + JSON_HEDLEY_NEVER_INLINE + bool last_byte_error(const exception_id id_, const std::string& detail, const char* context, const char* tail = nullptr) const + { + // with a tail, the message reads "; last byte: 0x" + return sax->parse_error(chars_read, get_token_string(), parse_error::create(id_, chars_read, + exception_message(tail == nullptr ? detail : concat(detail, tail, "; last byte: 0x", get_token_string()), context), nullptr)); + } + + /*! + @brief reports the last read byte as unexpected (parse_error.112) + @param[in] detail what is wrong with the byte, e.g. "invalid byte" + @param[in] context further context information + @return the result of the SAX parser's parse_error() + */ + JSON_HEDLEY_NEVER_INLINE + bool unexpected_byte(const char* detail, const char* context) const + { + return last_byte_error(exception_id::unexpected_byte, concat(detail, ": 0x", get_token_string()), context); + } + + /*! + @brief reports that the last read byte is not a UBJSON/BJData length type (parse_error.113) + @param[in] position where the length type was expected, e.g. " after '#'" + @param[in] context further context information + @return the result of the SAX parser's parse_error() + */ + JSON_HEDLEY_NEVER_INLINE + bool length_type_error(const char* position, const char* context) const + { + const char* types = input_format == input_format_t::bjdata ? "U, i, u, I, m, l, M, L" : "U, i, I, l, L"; + return last_byte_error(exception_id::invalid_string_or_size, + concat("expected length type specification (", types, ")", position, "; last byte: 0x", get_token_string()), context); + } + /*! @return a string representation of the last read byte */ std::string get_token_string() const { - std::array cr{{}}; - static_cast((std::snprintf)(cr.data(), cr.size(), "%.2hhX", static_cast(current))); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) - return std::string{cr.data()}; + return hex_byte(static_cast(current)); } /*! @@ -4333,37 +4394,7 @@ class binary_reader const std::string& context) const { std::string error_msg = "syntax error while parsing "; - - switch (input_format) - { - case input_format_t::cbor: - error_msg += "CBOR"; - break; - - case input_format_t::msgpack: - error_msg += "MessagePack"; - break; - - case input_format_t::ubjson: - error_msg += "UBJSON"; - break; - - case input_format_t::bson: - error_msg += "BSON"; - break; - - case input_format_t::bjdata: - error_msg += "BJData"; - break; - - case input_format_t::bon8: - error_msg += "BON8"; - break; - - case input_format_t::json: // LCOV_EXCL_LINE - default: // LCOV_EXCL_LINE - JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE - } + error_msg += input_format_name(input_format); return concat(error_msg, ' ', context, ": ", detail); } diff --git a/include/nlohmann/detail/input/input_adapters.hpp b/include/nlohmann/detail/input/input_adapters.hpp index 9578e275a..af1a6d537 100644 --- a/include/nlohmann/detail/input/input_adapters.hpp +++ b/include/nlohmann/detail/input/input_adapters.hpp @@ -8,16 +8,19 @@ #pragma once -#include // min +#include // count, find, max, min #include // array #include // size_t #include // uint32_t #include // strlen -#include // begin, end, iterator_traits, random_access_iterator_tag, distance, next +#include // begin, end, iterator_traits, random_access_iterator_tag, distance, next, reverse_iterator +#include // numeric_limits +#include // unique_ptr #include // streambuf #include // string, char_traits #include // enable_if, is_base_of, is_pointer, is_integral, remove_pointer -#include // pair, declval +#include // declval, move, pair +#include // vector #ifndef JSON_NO_IO #include // FILE * @@ -30,6 +33,33 @@ #include #include +#ifndef JSON_NO_IO + // Which unlocked stdio functions file_input_adapter uses (see + // JSON_DISABLE_UNLOCKED_FILE_IO): 1 = POSIX flockfile/getc_unlocked, + // 2 = Microsoft CRT _lock_file/_fgetc_nolock, 0 = none (std::fgetc, which + // locks the file for every character). Only C libraries known to declare + // the functions are listed; the macros are tested after , which + // defines __GLIBC__. MemorySanitizer builds always use std::fgetc: glibc's + // inline getc_unlocked reads the stdio buffer directly, which glibc (not + // instrumented) filled, so MemorySanitizer would report a false positive. + #if defined(__has_feature) + #if __has_feature(memory_sanitizer) + #define JSON_UNLOCKED_FILE_IO_MSAN 1 + #endif + #endif + + #if JSON_DISABLE_UNLOCKED_FILE_IO || defined(JSON_UNLOCKED_FILE_IO_MSAN) + #define JSON_UNLOCKED_FILE_IO 0 + #elif defined(_MSC_VER) + #define JSON_UNLOCKED_FILE_IO 2 + #elif defined(__GLIBC__) || defined(__APPLE__) || defined(__ANDROID__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__DragonFly__) + #define JSON_UNLOCKED_FILE_IO 1 + #else + #define JSON_UNLOCKED_FILE_IO 0 + #endif + #undef JSON_UNLOCKED_FILE_IO_MSAN +#endif // JSON_NO_IO + NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail { @@ -37,12 +67,118 @@ namespace detail /// the supported input formats enum class input_format_t { json, cbor, msgpack, ubjson, bson, bjdata, bon8 }; +/// @return the name of a binary input format, as used in exception messages +inline const char* input_format_name(const input_format_t format) noexcept +{ + switch (format) + { + case input_format_t::cbor: + return "CBOR"; + case input_format_t::msgpack: + return "MessagePack"; + case input_format_t::ubjson: + return "UBJSON"; + case input_format_t::bson: + return "BSON"; + case input_format_t::bjdata: + return "BJData"; + case input_format_t::bon8: + return "BON8"; + case input_format_t::json: // LCOV_EXCL_LINE + default: // LCOV_EXCL_LINE + JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE + return ""; // LCOV_EXCL_LINE + } +} + +/// @return the message of the parse_error thrown or reported for an empty input +inline const char* empty_input_message() noexcept +{ + return "attempting to parse an empty input; check that your input string or stream contains the expected JSON"; +} + //////////////////// // input adapters // //////////////////// #ifndef JSON_NO_IO /*! +A growable array of characters for the part of a token that an input adapter +copied out of its window: unlike std::vector::push_back(), appending a +character takes no reference to a local, and is a few instructions when there +is room. +*/ +class token_carry_buffer +{ + public: + token_carry_buffer() = default; + token_carry_buffer(const token_carry_buffer&) = delete; + token_carry_buffer& operator=(const token_carry_buffer&) = delete; + token_carry_buffer(token_carry_buffer&& rhs) noexcept + : m_data(std::move(rhs.m_data)), m_end(rhs.m_end), m_capacity_end(rhs.m_capacity_end) + { + rhs.m_end = rhs.m_capacity_end = nullptr; + } + token_carry_buffer& operator=(token_carry_buffer&&) = delete; + ~token_carry_buffer() = default; + + void clear() noexcept + { + m_end = m_data.get(); + } + + void push_back(char c) + { + if (JSON_HEDLEY_UNLIKELY(m_end == m_capacity_end)) + { + grow(1); + } + *m_end++ = c; + } + + void append(const char* first, const char* last) + { + const auto n = static_cast(last - first); + const auto room = static_cast(m_capacity_end - m_end); + if (n > room) + { + grow(n); + } + if (n != 0) + { + std::memcpy(m_end, first, n); + } + m_end += n; + } + + /// replace the contents of @a out + void copy_to(std::vector& out) const + { + out.assign(m_data.get(), m_end); + } + + private: + /// make room for @a extra more characters + JSON_HEDLEY_NEVER_INLINE void grow(std::size_t extra) + { + const auto size = static_cast(m_end - m_data.get()); + const auto capacity = static_cast(m_capacity_end - m_data.get()); + const std::size_t new_capacity = (std::max)(capacity < 32 ? static_cast(32) : 2 * capacity, size + extra); + std::unique_ptr data(new char[new_capacity]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + if (size != 0) + { + std::memcpy(data.get(), m_data.get(), size); + } + m_data = std::move(data); + m_end = m_data.get() + size; + m_capacity_end = m_data.get() + new_capacity; + } + + std::unique_ptr m_data{}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays,readability-redundant-member-init) + char* m_end = nullptr; + char* m_capacity_end = nullptr; +}; +/*! Input adapter for stdio file access. This adapter read only 1 byte and do not use any buffer. This adapter is a very low level adapter. */ @@ -56,18 +192,35 @@ class file_input_adapter : m_file(f) { JSON_ASSERT(m_file != nullptr); + lock(); } // make class move-only file_input_adapter(const file_input_adapter&) = delete; - file_input_adapter(file_input_adapter&&) noexcept = default; + file_input_adapter(file_input_adapter&& rhs) noexcept + : m_file(rhs.m_file) + { + // the lock is owned by the thread, not the adapter: hand it over + rhs.m_file = nullptr; + } file_input_adapter& operator=(const file_input_adapter&) = delete; file_input_adapter& operator=(file_input_adapter&&) = delete; - ~file_input_adapter() = default; + ~file_input_adapter() + { + unlock(); + } std::char_traits::int_type get_character() noexcept { + // the file is locked for the lifetime of the adapter, so the + // per-character lock of std::fgetc can be skipped +#if JSON_UNLOCKED_FILE_IO == 1 + return getc_unlocked(m_file); +#elif JSON_UNLOCKED_FILE_IO == 2 + return _fgetc_nolock(m_file); +#else return std::fgetc(m_file); +#endif } // returns the number of characters successfully read @@ -78,6 +231,32 @@ class file_input_adapter } private: + // Lock the file once for the whole parse instead of once per character. + // The lock is recursive, so fread() and the user's own stdio calls on the + // same thread keep working. + void lock() noexcept + { +#if JSON_UNLOCKED_FILE_IO == 1 + flockfile(m_file); +#elif JSON_UNLOCKED_FILE_IO == 2 + _lock_file(m_file); +#endif + } + + void unlock() noexcept + { + // a moved-from adapter has no file and holds no lock + if (m_file == nullptr) + { + return; + } +#if JSON_UNLOCKED_FILE_IO == 1 + funlockfile(m_file); +#elif JSON_UNLOCKED_FILE_IO == 2 + _unlock_file(m_file); +#endif + } + /// the file pointer to read from std::FILE* m_file; }; @@ -127,15 +306,27 @@ class input_stream_adapter input_stream_adapter& operator=(input_stream_adapter&) = delete; input_stream_adapter& operator=(input_stream_adapter&&) = delete; -#if JSON_PRECISE_STREAM_POSITION input_stream_adapter(input_stream_adapter&& rhs) noexcept - : is(rhs.is), sb(rhs.sb), lookahead(rhs.lookahead) + : is(rhs.is), sb(rhs.sb) +#if JSON_PRECISE_STREAM_POSITION + , lookahead(rhs.lookahead) +#endif +#if !JSON_DISABLE_STREAM_BULK_SCAN + , token_carry(std::move(rhs.token_carry)) + , token_segment(rhs.token_segment) + , current_in_get_area(rhs.current_in_get_area) + , tracking_token(rhs.tracking_token) + , no_get_area(rhs.no_get_area) +#endif { rhs.is = nullptr; rhs.sb = nullptr; +#if JSON_PRECISE_STREAM_POSITION rhs.lookahead = false; +#endif } +#if JSON_PRECISE_STREAM_POSITION // Whether the character last returned by get_character() can be given back // to the input with release_lookahead(). static constexpr bool supports_lookahead = true; @@ -155,17 +346,41 @@ class input_stream_adapter sb->sbumpc(); } +#if !JSON_DISABLE_STREAM_BULK_SCAN + // sgetc() refills the get area if it is exhausted + const bool refill = streambuf_access::get_ptr(sb) == streambuf_access::get_end(sb); + if (JSON_HEDLEY_UNLIKELY(refill)) + { + save_token_segment(); + } +#endif + auto res = sb->sgetc(); // set eof manually, as we don't use the istream interface. if (JSON_HEDLEY_UNLIKELY(res == std::char_traits::eof())) { // there is nothing to step over next time lookahead = false; +#if !JSON_DISABLE_STREAM_BULK_SCAN + current_in_get_area = false; +#endif is->clear(is->rdstate() | std::ios::eofbit); } else { lookahead = true; +#if !JSON_DISABLE_STREAM_BULK_SCAN + if (JSON_HEDLEY_UNLIKELY(refill)) + { + // the peeked character is at gptr(), unless the streambuf has + // no get area + after_refill(res, streambuf_access::get_ptr(sb) != streambuf_access::get_end(sb)); + } + else + { + current_in_get_area = true; + } +#endif } return res; } @@ -179,13 +394,6 @@ class input_stream_adapter lookahead = false; } #else - input_stream_adapter(input_stream_adapter&& rhs) noexcept - : is(rhs.is), sb(rhs.sb) - { - rhs.is = nullptr; - rhs.sb = nullptr; - } - // std::istream/std::streambuf use std::char_traits::to_int_type, to // ensure that std::char_traits::eof() and the character 0xFF do not // end up as the same value, e.g., 0xFFFFFFFF. @@ -194,13 +402,21 @@ class input_stream_adapter // stays consumed after parsing; see JSON_PRECISE_STREAM_POSITION. std::char_traits::int_type get_character() { - auto res = sb->sbumpc(); - // set eof manually, as we don't use the istream interface. - if (JSON_HEDLEY_UNLIKELY(res == std::char_traits::eof())) +#if !JSON_DISABLE_STREAM_BULK_SCAN + // the character is in the get area: step over it, as sbumpc() does + const char* const next = streambuf_access::get_ptr(sb); + if (JSON_HEDLEY_LIKELY(next != streambuf_access::get_end(sb))) { - is->clear(is->rdstate() | std::ios::eofbit); + current_in_get_area = true; + streambuf_access::bump(sb, 1); + return std::char_traits::to_int_type(*next); } - return res; + if (no_get_area) + { + return get_character_no_get_area(); + } +#endif + return get_character_slow(); } #endif @@ -209,6 +425,18 @@ class input_stream_adapter { #if JSON_PRECISE_STREAM_POSITION commit_lookahead(); +#endif +#if !JSON_DISABLE_STREAM_BULK_SCAN + // the elements are in the get area: copy them, as sgetn() does, but + // without the virtual call + const char* const first = streambuf_access::get_ptr(sb); + const auto available = static_cast(streambuf_access::get_end(sb) - first); + if (JSON_HEDLEY_LIKELY(count * sizeof(T) <= available && count * sizeof(T) <= static_cast((std::numeric_limits::max)()))) + { + std::memcpy(dest, first, count * sizeof(T)); + streambuf_access::bump(sb, static_cast(count * sizeof(T))); + return count * sizeof(T); + } #endif auto res = static_cast(sb->sgetn(reinterpret_cast(dest), static_cast(count * sizeof(T)))); if (JSON_HEDLEY_UNLIKELY(res < count * sizeof(T))) @@ -218,7 +446,248 @@ class input_stream_adapter return res; } +#if !JSON_DISABLE_STREAM_BULK_SCAN + // The lexer may scan the characters in the get area of the streambuf + // directly (SWAR fast paths). The get area is only a window into the + // input: it ends wherever the streambuf's buffer ends, and is refilled by + // the next get_character(). The lexer therefore keeps counting the + // position itself and treats the end of the window as "unknown", not as + // the end of the input. + static constexpr bool supports_bulk_scan = true; + static constexpr bool bulk_window = true; + + // Pointer to the next unread character; only valid when + // bulk_remaining() > 0. The character before it is the one last returned + // by get_character(). + const char* bulk_data() const + { + return current_end(); + } + + // Number of unread characters in the get area. + std::size_t bulk_remaining() const + { + if (!current_in_get_area) + { + return 0; + } + return static_cast(streambuf_access::get_end(sb) - current_end()); + } + + // Consume @a n characters previously inspected via bulk_data(); the last + // of them becomes the character last returned by get_character(). + void bulk_skip(std::size_t n) + { + JSON_ASSERT(n <= bulk_remaining()); + // gbump() takes an int, but a get area (e.g., of a std::stringbuf) can + // be larger + constexpr auto max_bump = static_cast((std::numeric_limits::max)()); + for (; n > max_bump; n -= max_bump) + { + streambuf_access::bump(sb, static_cast(max_bump)); + } + streambuf_access::bump(sb, static_cast(n)); + } + + // The adapter keeps the current token for the lexer's error messages: the + // part in the get area is only remembered as a pointer, and copied out + // when the get area is about to be refilled. + static constexpr bool supports_token_tracking = true; + + // Start a token with the characters read from now on. Until this is + // called, nothing is tracked (the binary readers never call it). + void begin_token() noexcept + { + tracking_token = true; + token_carry.clear(); + token_segment = current_end(); + } + + // Start a new token with the character @a c last returned by + // get_character(). + void mark_token_start(std::char_traits::int_type c) + { + tracking_token = true; + token_carry.clear(); + if (current_in_get_area) + { + token_segment = current_end() - 1; + } + else + { + token_segment = nullptr; + token_carry.push_back(std::char_traits::to_char_type(c)); + } + } + + // Copy the characters read since mark_token_start() to @a out, without the + // last one if @a drop_last. + void copy_token(std::vector& out, bool drop_last) const + { + token_carry.copy_to(out); + if (token_segment != nullptr) + { + out.insert(out.end(), token_segment, current_end()); + } + if (drop_last && !out.empty()) + { + out.pop_back(); + } + } +#endif + private: +#if !JSON_PRECISE_STREAM_POSITION + // the get area is exhausted, or there is none +#if !JSON_DISABLE_STREAM_BULK_SCAN + JSON_HEDLEY_NEVER_INLINE +#endif + std::char_traits::int_type get_character_slow() + { +#if !JSON_DISABLE_STREAM_BULK_SCAN + // sbumpc() refills the get area if it is exhausted + const bool refill = streambuf_access::get_ptr(sb) == streambuf_access::get_end(sb); + if (JSON_HEDLEY_UNLIKELY(refill)) + { + save_token_segment(); + } +#endif + + auto res = sb->sbumpc(); + // set eof manually, as we don't use the istream interface. + if (JSON_HEDLEY_UNLIKELY(res == std::char_traits::eof())) + { +#if !JSON_DISABLE_STREAM_BULK_SCAN + current_in_get_area = false; +#endif + is->clear(is->rdstate() | std::ios::eofbit); + } +#if !JSON_DISABLE_STREAM_BULK_SCAN + else if (JSON_HEDLEY_UNLIKELY(refill)) + { + // the consumed character is at gptr() - 1, unless the streambuf + // has no get area and returned it from uflow() + const char* const next = streambuf_access::get_ptr(sb); + after_refill(res, next != nullptr && next != streambuf_access::get_begin(sb)); + } + else + { + current_in_get_area = true; + } +#endif + return res; + } +#endif +#if !JSON_DISABLE_STREAM_BULK_SCAN + // Reads the protected get-area pointers of any std::streambuf. Naming the + // members through a derived class makes forming the member pointers + // accessible; calling them on another streambuf object is then ordinary + // member access through a pointer to member. + struct streambuf_access : std::streambuf + { + static const char* get_ptr(std::streambuf* s) + { + return (s->*(&streambuf_access::gptr))(); + } + static const char* get_end(std::streambuf* s) + { + return (s->*(&streambuf_access::egptr))(); + } + static const char* get_begin(std::streambuf* s) + { + return (s->*(&streambuf_access::eback))(); + } + static void bump(std::streambuf* s, int n) + { + (s->*(&streambuf_access::gbump))(n); + } + // sbumpc() for an exhausted get area + static std::streambuf::int_type next_character(std::streambuf* s) + { + return (s->*(&streambuf_access::uflow))(); + } + }; + + // one past the character last returned by get_character(), if that is in + // the get area + const char* current_end() const + { +#if JSON_PRECISE_STREAM_POSITION + // the character is peeked at gptr() and not stepped over yet, unless + // it was given back with release_lookahead() + return streambuf_access::get_ptr(sb) + (lookahead ? 1 : 0); +#else + return streambuf_access::get_ptr(sb); +#endif + } + + // the get area is about to be replaced: copy the token's part in it + void save_token_segment() + { + if (token_segment != nullptr) + { + token_carry.append(token_segment, streambuf_access::get_end(sb)); + token_segment = nullptr; + } + } + +#if !JSON_PRECISE_STREAM_POSITION + // The streambuf has no get area (e.g., std::cin synchronized with stdio): + // every character comes from uflow(), and the token is kept by value. + JSON_HEDLEY_NEVER_INLINE std::char_traits::int_type get_character_no_get_area() + { + // the get area is exhausted, so this is what sbumpc() does + const auto res = streambuf_access::next_character(sb); + if (JSON_HEDLEY_UNLIKELY(res == std::char_traits::eof() || streambuf_access::get_end(sb) != nullptr)) + { + return get_character_no_get_area_other(res); + } + // current_in_get_area is false: only the first character of a get + // area, or the end of the input, is read here + if (tracking_token) + { + token_carry.push_back(std::char_traits::to_char_type(res)); + } + return res; + } + + // @a res came from uflow() of a streambuf without a get area: it is the + // end of the input, or the streambuf has a get area now + JSON_HEDLEY_NEVER_INLINE std::char_traits::int_type get_character_no_get_area_other(std::char_traits::int_type res) + { + if (res == std::char_traits::eof()) + { + current_in_get_area = false; + is->clear(is->rdstate() | std::ios::eofbit); + return res; + } + // the consumed character is at gptr() - 1 + after_refill(res, streambuf_access::get_ptr(sb) != streambuf_access::get_begin(sb)); + return res; + } +#endif + + // character @a c was just read into a new get area (@a in_get_area) or + // returned without one + void after_refill(std::char_traits::int_type c, bool in_get_area) + { + current_in_get_area = in_get_area; + no_get_area = !in_get_area; + if (!tracking_token) + { + return; + } + if (in_get_area) + { + token_segment = current_end() - 1; + } + else + { + token_carry.push_back(std::char_traits::to_char_type(c)); + } + } +#endif + #if JSON_PRECISE_STREAM_POSITION // Step over the character last returned by get_character(). The character // has already been peeked successfully, so for every streambuf with a get @@ -240,6 +709,19 @@ class input_stream_adapter /// whether get_character() peeked a character that is not consumed yet bool lookahead = false; #endif +#if !JSON_DISABLE_STREAM_BULK_SCAN + /// the part of the current token that was in earlier get areas + token_carry_buffer token_carry{}; // NOLINT(readability-redundant-member-init) + /// start of the current token's part in the get area, or nullptr + const char* token_segment = nullptr; + /// whether the character last returned by get_character() is in the get area + bool current_in_get_area = false; + /// whether a token is tracked at all; the binary readers never start one + bool tracking_token = false; + /// whether the streambuf had no get area when it was last asked for a + /// character + bool no_get_area = false; +#endif }; #endif // JSON_NO_IO @@ -297,6 +779,7 @@ class iterator_input_adapter return result; } + ++eof_reads; return char_traits::eof(); } @@ -306,6 +789,26 @@ class iterator_input_adapter return static_cast(std::distance(begin, current)); } + // number of get_character() calls so far, including those that returned + // EOF; the lexer derives its position from it (see lexer::lazy_position) + std::size_t get_read_count() const + { + return get_consumed_count() + eof_reads; + } + + // number of newlines in the consumed range [0, last_index); @a line_start + // is set to the index after the last of them (0 if there is none); only + // valid when supports_seek + std::size_t count_consumed_lines(std::size_t last_index, std::size_t& line_start) const + { + const auto to = std::next(begin, static_cast::difference_type>(last_index)); + const auto newline = char_traits::to_char_type('\n'); + using reverse_it = std::reverse_iterator; + const auto last_newline = std::find(reverse_it(to), reverse_it(begin), newline); + line_start = static_cast(std::distance(begin, last_newline.base())); + return static_cast(std::count(begin, last_newline.base(), newline)); + } + // append the already-consumed characters in the half-open range // [first_index, last_index) to @a out; only valid when supports_seek template @@ -422,6 +925,8 @@ class iterator_input_adapter IteratorType begin; IteratorType current; SentinelType end; + /// number of get_character() calls that returned EOF + std::size_t eof_reads = 0; template friend struct wide_string_input_helper; @@ -581,7 +1086,7 @@ class wide_string_input_adapter template JSON_HEDLEY_NO_RETURN std::size_t get_elements(T* /*dest*/, std::size_t /*count*/ = 1) { - JSON_THROW(parse_error::create(112, 1, "wide string type cannot be interpreted as binary data", nullptr)); + JSON_THROW(parse_error::create(exception_id::unexpected_byte, 1, "wide string type cannot be interpreted as binary data", nullptr)); } private: @@ -793,7 +1298,7 @@ inline file_input_adapter input_adapter(std::FILE* file) { if (file == nullptr) { - JSON_THROW(parse_error::create(101, 0, "attempting to parse an empty input; check that your input string or stream contains the expected JSON", nullptr)); + JSON_THROW(parse_error::create(exception_id::syntax_error, 0, empty_input_message(), nullptr)); } return file_input_adapter(file); } @@ -802,7 +1307,7 @@ inline input_stream_adapter input_adapter(std::istream& stream) { if (stream.rdbuf() == nullptr) { - JSON_THROW(parse_error::create(101, 0, "attempting to parse an empty input; check that your input string or stream contains the expected JSON", nullptr)); + JSON_THROW(parse_error::create(exception_id::syntax_error, 0, empty_input_message(), nullptr)); } return input_stream_adapter(stream); } @@ -827,7 +1332,7 @@ contiguous_bytes_input_adapter input_adapter(CharT b) { if (b == nullptr) { - JSON_THROW(parse_error::create(101, 0, "attempting to parse an empty input; check that your input string or stream contains the expected JSON", nullptr)); + JSON_THROW(parse_error::create(exception_id::syntax_error, 0, empty_input_message(), nullptr)); } auto length = std::strlen(reinterpret_cast(b)); const auto* ptr = reinterpret_cast(b); diff --git a/include/nlohmann/detail/input/json_sax.hpp b/include/nlohmann/detail/input/json_sax.hpp index 146944261..04201efbe 100644 --- a/include/nlohmann/detail/input/json_sax.hpp +++ b/include/nlohmann/detail/input/json_sax.hpp @@ -8,9 +8,13 @@ #pragma once -#include // find_if, min +#include // copy, find_if, min +#include // array #include +#include // hash #include // numeric_limits +#include // unique_ptr +#include // operator new #include // string #include // enable_if_t #include // move, pair @@ -18,8 +22,10 @@ #include #include +#include #include #include +#include #include NLOHMANN_JSON_NAMESPACE_BEGIN @@ -176,6 +182,34 @@ template inline void reserve_array(ArrayType& /*arr*/, std::size_t /*len*/, priority_tag<0> /*unused*/) {} +/*! +@brief reports an object or array whose announced size exceeds max_size() + +Shared by json_sax_dom_parser and json_sax_dom_callback_parser. + +@param[in] sax the SAX parser to report the error to +@param[in] len the number of elements announced by the input, or unknown_size() +@param[in] kind "object" or "array" +@param[in] ref the object or array that was just created +@return whether parsing should continue (false after reporting out_of_range.408) +*/ +template +JSON_HEDLEY_NEVER_INLINE +bool container_too_large_error(SAX& sax, std::size_t len, const char* kind, BasicJsonType* ref) +{ + return sax.parse_error(0, "", out_of_range::create(exception_id::container_too_large, concat("excessive ", kind, " size: ", std::to_string(len)), ref)); +} + +template +bool check_container_size(SAX& sax, std::size_t len, const char* kind, BasicJsonType* ref) +{ + if (JSON_HEDLEY_UNLIKELY(len != detail::unknown_size() && len > ref->max_size())) + { + return container_too_large_error(sax, len, kind, ref); + } + return true; +} + #if JSON_DIAGNOSTIC_POSITIONS /*! @brief set the diagnostic positions of a value the DOM SAX parsers just stored @@ -185,6 +219,35 @@ befriends this struct, as the position members are private. */ struct diagnostic_positions { + /*! + @param[in,out] v the object or array whose opening brace or bracket was just read + @param[in] lexer the lexer that read it, or nullptr to leave @a v alone + */ + template + static void set_container_start(BasicJsonType& v, LexerType* lexer) + { + if (lexer) + { + // Lexer has read the first character of the container, so + // subtract 1 from the position to get the correct start position. + v.start_position = lexer->get_chars_read_total() - 1; + } + } + + /*! + @param[in,out] v the object or array whose closing brace or bracket was just read + @param[in] lexer the lexer that read it, or nullptr to leave @a v alone + */ + template + static void set_container_end(BasicJsonType& v, LexerType* lexer) + { + if (lexer) + { + // Lexer's position is past the closing brace or bracket, so set that as the end position. + v.end_position = lexer->get_chars_read_total(); + } + } + /*! @param[in,out] v the value that was just parsed @param[in] lexer the lexer that read it, or nullptr to leave @a v alone @@ -197,7 +260,7 @@ struct diagnostic_positions // Lexer has read past the current field value, so set the end position to the current position. // The start position will be set below based on the length of the string representation // of the value. - v.end_position = lexer->get_position(); + v.end_position = lexer->get_chars_read_total(); switch (v.type()) { @@ -258,21 +321,161 @@ struct diagnostic_positions }; #endif +/*! +@brief error state shared by the DOM-building SAX parsers + +Holds whether a parse error occurred and whether errors are thrown, and +implements the parse_error() and is_errored() members of the SAX interface for +json_sax_dom_parser and json_sax_dom_callback_parser. +*/ +class json_sax_dom_error_state +{ + public: + explicit json_sax_dom_error_state(const bool allow_exceptions_) noexcept + : allow_exceptions(allow_exceptions_) + {} + + // make class move-only + json_sax_dom_error_state(const json_sax_dom_error_state&) = delete; + json_sax_dom_error_state(json_sax_dom_error_state&&) = default; // NOLINT(hicpp-noexcept-move,performance-noexcept-move-constructor) + json_sax_dom_error_state& operator=(const json_sax_dom_error_state&) = delete; + json_sax_dom_error_state& operator=(json_sax_dom_error_state&&) = delete; // the const member prevents assignment + ~json_sax_dom_error_state() = default; + + template + bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, + const Exception& ex) + { + errored = true; + static_cast(ex); + if (allow_exceptions) + { + JSON_THROW(ex); + } + return false; + } + + bool is_errored() const + { + return errored; + } + + private: + /// whether a syntax error occurred + bool errored = false; + /// whether to throw exceptions in case of errors + const bool allow_exceptions = true; +}; + +/*! +@brief a hash table that finds the position of a key in an object whose + elements are kept in a vector (ordered_map) + +The elements stay where they are: the table only holds their hashes and +positions, and the caller compares the keys. +*/ +class object_key_index +{ + public: + static constexpr std::size_t npos = static_cast(-1); + + /// @param[in] element_hashes the hashes of the keys of the elements that are there already + explicit object_key_index(std::vector&& element_hashes) + : hashes(std::move(element_hashes)) + { + std::size_t size = 256; + while (size < 4 * hashes.size()) + { + size *= 2; + } + rebuild(size); + } + + /// @return the position of the element with @a hash for which @a matches(position) holds, or npos + template + std::size_t find(const std::size_t hash, Matches matches) const + { + for (std::size_t slot = hash & mask;; slot = (slot + 1) & mask) + { + const std::size_t entry = table[slot]; + if (entry == 0) + { + return npos; + } + if (hashes[entry - 1] == hash && matches(entry - 1)) + { + return entry - 1; + } + } + } + + /// add the next element, whose position is the number of elements so far + void add(const std::size_t hash) + { + hashes.push_back(hash); + if (hashes.size() * 2 > table.size()) + { + rebuild(table.size() * 2); + } + else + { + place(hash, hashes.size()); + } + } + + private: + void rebuild(const std::size_t size) + { + table.assign(size, 0); + mask = size - 1; + for (std::size_t i = 0; i < hashes.size(); ++i) + { + place(hashes[i], i + 1); + } + } + + void place(const std::size_t hash, const std::size_t entry) + { + std::size_t slot = hash & mask; + while (table[slot] != 0) + { + slot = (slot + 1) & mask; + } + table[slot] = entry; + } + + /// the hash of each element, by position + std::vector hashes; + /// open addressing, a power of two in size: position + 1 of an element, or 0 for a free slot + std::vector table {}; // NOLINT(readability-redundant-member-init) + std::size_t mask = 0; +}; + /*! @brief SAX implementation to create a JSON value from SAX events This class implements the @ref json_sax interface and processes the SAX events to create a JSON value which makes it basically a DOM parser. The structure or -hierarchy of the JSON value is managed by the stack `ref_stack` which contains -a pointer to the respective array or object for each recursion depth. +hierarchy of the JSON value is managed by the stack `frames` which contains +an entry for each open array or object. The parser of the text formats, for +which the size of a container is not known up front, collects the elements of +an array and the members of an `ordered_json` object on a stack and moves them +into a container of the exact size (with @a Collect, as basic_json's own parse +functions use it); the binary readers use the plain +@ref json_sax_dom_binary_parser instead. After successful parsing, the value that is passed by reference to the -constructor contains the parsed value. +constructor contains the parsed value. Without @a Collect, every value is +stored in place as it arrives, so after a parse error the value holds what was +read up to it. @tparam BasicJsonType the JSON type +@tparam Collect whether to collect the values of containers of unknown size on + a stack (only for a caller that does not use the value after a + parse error) */ -template -class json_sax_dom_parser +template +class json_sax_dom_parser : public json_sax_dom_error_state { public: using number_integer_t = typename BasicJsonType::number_integer_t; @@ -288,7 +491,7 @@ class json_sax_dom_parser @param[in] allow_exceptions_ whether parse errors yield exceptions */ explicit json_sax_dom_parser(BasicJsonType& r, const bool allow_exceptions_ = true, lexer_t* lexer_ = nullptr) - : root(r), allow_exceptions(allow_exceptions_), m_lexer_ref(lexer_) + : json_sax_dom_error_state(allow_exceptions_), root(r), m_lexer_ref(lexer_) {} // make class move-only @@ -344,27 +547,934 @@ class json_sax_dom_parser return true; } + bool start_object(std::size_t len) + { +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + // the values need their final place from the start (their parent + // pointers and positions refer to it), so no buffering + return start_stored_object(len); +#else + return start_object(len, std::integral_constant {}); +#endif + } + + bool key(string_t& val) + { + JSON_ASSERT(!frames.empty()); + return key(val, std::integral_constant {}); + } + + bool end_object() + { + JSON_ASSERT(!frames.empty()); + return end_object(std::integral_constant {}); + } + + bool start_array(std::size_t len) + { + if (JSON_HEDLEY_UNLIKELY(len != detail::unknown_size() && len > array_t().max_size())) + { + // the array does not exist yet: report with an empty one + BasicJsonType too_large(BasicJsonType::value_t::array); + return container_too_large_error(*this, len, "array", &too_large); + } + +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + // the values need their final place from the start (their parent + // pointers and positions refer to it), so no buffering + start_stored_array(len); +#else + if (!collects(len)) + { + // the number of elements is known (or the elements must be in + // place as they arrive): store them directly + start_stored_array(len); + } + else + { + // the number of elements is not known, so growing the array + // would reallocate it (and, being a vector, move its elements) + // again and again. Collect the elements on a stack shared by all + // arrays instead, and copy them to an array of the exact size + // when the closing bracket arrives. + BasicJsonType* slot = nullptr; + std::size_t dest_member = 0; + container_destination(slot, dest_member); + reserve_elements(); + frames.push_back(frame{nullptr, nullptr, slot, elements.size(), ordered_state(dest_member, false)}); + in_object = false; + stored_array = nullptr; + } +#endif + return true; + } + + bool end_array() + { + JSON_ASSERT(!frames.empty()); + JSON_ASSERT(frames.back().object == nullptr); + const frame f = frames.back(); + frames.pop_back(); + reenter_enclosing_container(); + + if (f.array == nullptr) + { + // the elements are the top of the shared elements stack, from + // f.start on (the elements below belong to enclosing arrays that + // are still open): move them into the array and pop them + const auto first = elements.begin() + static_cast(f.start); + auto* const arr = BasicJsonType::template create(std::make_move_iterator(first), std::make_move_iterator(elements.end())); + BasicJsonType value; + value.m_data.m_value.array = arr; + value.m_data.m_type = BasicJsonType::value_t::array; + elements.erase(first, elements.end()); + + place_container(f, frames.empty() ? nullptr : &frames.back(), std::move(value), true); + return true; + } + +#if JSON_DIAGNOSTIC_POSITIONS + diagnostic_positions::set_container_end(*f.slot, m_lexer_ref); +#endif +#if JSON_DIAGNOSTICS + f.slot->set_parents(); +#endif + return true; + } + + private: + using object_t = typename BasicJsonType::object_t; + using array_t = typename BasicJsonType::array_t; + + /// whether objects keep their elements in a vector and a key can be hashed, + /// so that a big object can find the keys it has in a hash table instead of + /// searching all of them + template + using detect_emplace_new_key = decltype(std::declval().emplace_new_key(std::declval(), std::declval())); + template + using detect_hash = decltype(std::hash()(std::declval())); + // std::hash is only named once the object can hold the keys in a vector: + // VS 2015 static-asserts on std::hash of a type without one instead of + // reporting a substitution failure + template::value> + struct detect_indexable_keys : std::false_type {}; + template + struct detect_indexable_keys : is_detected {}; + static constexpr bool indexed_keys = detect_indexable_keys::value; + + /// what a frame holds for objects that are kept in a vector: the key index + /// of an object with many keys, and for an object that is collected on the + /// stack the member of the enclosing object it will become + struct ordered_frame + { + /// the entry of key_indexes of an object that has one, else nullptr + object_key_index* index; + /// object or array without a slot in an object collected on the stack: + /// the index of the member in `members` it goes to + std::size_t dest_member; + /// whether this is an object collected on the stack + bool buffered; + }; + struct no_ordered_frame {}; + using ordered_frame_state = typename std::conditional::type; + + static ordered_frame make_ordered_state(const std::size_t dest_member, const bool buffered, std::true_type /*indexed*/) noexcept + { + ordered_frame state = {nullptr, dest_member, buffered}; + return state; + } + + static no_ordered_frame make_ordered_state(const std::size_t /*dest_member*/, const bool /*buffered*/, std::false_type /*indexed*/) noexcept + { + return no_ordered_frame(); + } + + static ordered_frame_state ordered_state(const std::size_t dest_member, const bool buffered) noexcept + { + return make_ordered_state(dest_member, buffered, std::integral_constant {}); + } + + /// an array or object that is being read + struct frame + { + /// the object, or nullptr for an array + object_t* object; + /// the array if its elements are stored in it as they arrive, or + /// nullptr for an array and an object + array_t* array; + /// array without `array`: where the finished array goes (nullptr: it + /// is an element of the enclosing array); otherwise: the value that + /// holds the object or array + BasicJsonType* slot; + /// array without `array`: the index of its first element in `elements` + std::size_t start; + /// what objects in a vector add (see ordered_frame) + ordered_frame_state ordered; + }; + + /// the open arrays and objects; the first few live in the object itself, so + /// that the common documents do not allocate for them + class frame_stack + { + public: + frame_stack() noexcept + : local() // NOLINT(readability-redundant-member-init) + {} + frame_stack(const frame_stack&) = delete; + frame_stack& operator=(const frame_stack&) = delete; + + frame_stack(frame_stack&& other) noexcept + : local() // NOLINT(readability-redundant-member-init) + { + take(other); + } + + frame_stack& operator=(frame_stack&& other) noexcept + { + if (this != &other) + { + release(); + take(other); + } + return *this; + } + + ~frame_stack() = default; + + bool empty() const noexcept + { + return count == 0; + } + + std::size_t size() const noexcept + { + return count; + } + + frame& back() noexcept + { + return data[count - 1]; + } + + frame& operator[](const std::size_t i) noexcept + { + return data[i]; + } + + void push_back(const frame& f) + { + if (JSON_HEDLEY_UNLIKELY(count == capacity)) + { + grow(); + } + data[count++] = f; + } + + void pop_back() noexcept + { + JSON_ASSERT(count > 0); + --count; + } + + private: + JSON_HEDLEY_NEVER_INLINE + void grow() + { + const std::size_t new_capacity = capacity * 2; + std::unique_ptr new_heap(new frame[new_capacity]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + for (std::size_t i = 0; i < count; ++i) + { + new_heap[i] = data[i]; + } + heap = std::move(new_heap); + data = heap.get(); + capacity = new_capacity; + } + + void release() noexcept + { + heap.reset(); + data = local.data(); + capacity = local_capacity; + } + + /// make the contents of @a other our own + void take(frame_stack& other) noexcept + { + if (other.heap == nullptr) + { + for (std::size_t i = 0; i < other.count; ++i) + { + local[i] = other.local[i]; + } + data = local.data(); + capacity = local_capacity; + } + else + { + heap = std::move(other.heap); + data = heap.get(); + capacity = other.capacity; + other.data = other.local.data(); + other.capacity = local_capacity; + } + count = other.count; + other.count = 0; + } + + static constexpr std::size_t local_capacity = 8; + + /// the first frames, left uninitialized: a frame is written before it is + /// read (the user-provided constructor keeps the member initialization, + /// which -Weffc++ asks for, from zeroing them) + struct local_frames : std::array + { + local_frames() noexcept {} // NOLINT(modernize-use-equals-default,hicpp-use-equals-default,cppcoreguidelines-pro-type-member-init,hicpp-member-init): see above + }; + + local_frames local; + /// the frames once there are more than local_capacity of them + std::unique_ptr heap {}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays,readability-redundant-member-init) + frame* data = local.data(); + std::size_t count = 0; + std::size_t capacity = local_capacity; + }; + + void release_key_index(const frame& f, std::true_type /*indexed*/) + { + if (JSON_HEDLEY_UNLIKELY(f.ordered.index != nullptr)) + { + ordered_data.key_indexes.pop_back(); + } + } + + static void release_key_index(const frame& /*f*/, std::false_type /*indexed*/) noexcept {} + + /// the number of keys from which an object looks them up in a hash table + static constexpr std::size_t key_index_threshold = 64; + + /// add @a key to the object of @a f (or find it, if it is there), searching it in std::map + static BasicJsonType* insert_key(frame& f, string_t&& key, std::false_type /*indexed*/) + { + return &f.object->operator[](std::move(key)); + } + + /// add @a key to the object of @a f (or find it, if it is there), searching it in a vector + BasicJsonType* insert_key(frame& f, string_t&& key, std::true_type /*indexed*/) + { + object_t& obj = *f.object; + if (JSON_HEDLEY_LIKELY(obj.size() < key_index_threshold)) + { + return &obj[std::move(key)]; + } + return insert_key_with_index(f, std::move(key)); + } + + /// add @a key to the big object of @a f (or find it, if it is there), using its key index + JSON_HEDLEY_NEVER_INLINE + BasicJsonType* insert_key_with_index(frame& f, string_t&& key) + { + object_t& obj = *f.object; + const std::hash hasher {}; + const std::size_t hash = hasher(key); + if (JSON_HEDLEY_UNLIKELY(f.ordered.index == nullptr)) + { + std::vector hashes; + hashes.reserve(obj.size()); + for (const auto& element : obj) + { + hashes.push_back(hasher(element.first)); + } + ordered_data.key_indexes.emplace_back(new object_key_index(std::move(hashes))); + f.ordered.index = ordered_data.key_indexes.back().get(); + } + + const std::size_t position = f.ordered.index->find(hash, [&obj, &key](std::size_t i) + { + return (obj.begin() + static_cast(i))->first == key; + }); + if (position != object_key_index::npos) + { + return &(obj.begin() + static_cast(position))->second; + } + + obj.emplace_new_key(key, BasicJsonType()); + f.ordered.index->add(hash); + return &obj.back().second; + } + + /// where an array or object that starts now will go when it is finished: + /// @a slot is the value to assign it to (the root or a member of an object + /// that is stored directly), or nullptr if it is an element of the + /// enclosing array or a member of the enclosing object that is collected on + /// the stack, in which case @a dest_member is the member + void container_destination(BasicJsonType*& slot, std::size_t& dest_member) + { + if (frames.empty()) + { + slot = &root; + } + else if (frames.back().object != nullptr) + { + slot = object_element; + } + else + { + collected_object_member(frames.back(), dest_member, std::integral_constant {}); + } + } + + void collected_object_member(const frame& enclosing, std::size_t& dest_member, std::true_type /*indexed*/) const noexcept + { + if (enclosing.ordered.buffered) + { + dest_member = ordered_data.member_index; + } + } + + static void collected_object_member(const frame& /*enclosing*/, std::size_t& /*dest_member*/, std::false_type /*indexed*/) noexcept {} + + static bool is_collected_object(const frame& f, std::true_type /*indexed*/) noexcept + { + return f.ordered.buffered; + } + + static bool is_collected_object(const frame& /*f*/, std::false_type /*indexed*/) noexcept + { + return false; + } + + /// put a finished array or object that was collected on the stack, or + /// that outgrew it, where it belongs; @a enclosing is the container it is in. + /// @a enclosing_is_innermost tells whether that is the container that gets + /// the next value, so that it can be switched to storing its elements + /// directly if the value was the one too many. + void place_container(const frame& f, frame* enclosing, BasicJsonType&& value, const bool enclosing_is_innermost) + { + // only a container with a slot may have no enclosing container + JSON_ASSERT(f.slot != nullptr || enclosing != nullptr); + if (f.slot != nullptr) + { + *f.slot = std::move(value); + } + else if (is_collected_object(*enclosing, std::integral_constant {})) + { + place_member(f, std::move(value), std::integral_constant {}); + } + else if (enclosing->array != nullptr) + { + enclosing->array->emplace_back(std::move(value)); + } + else + { + elements.emplace_back(std::move(value)); + if (JSON_HEDLEY_UNLIKELY(enclosing_is_innermost && elements.size() - enclosing->start > buffer_limit)) + { + spill(*enclosing); + } + } + } + + void place_member(const frame& f, BasicJsonType&& value, std::true_type /*indexed*/) + { + 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 {} + + /// store the members of a new object directly in it + bool start_stored_object(std::size_t len) + { + BasicJsonType* const ref = store(BasicJsonType::value_t::object); + frames.push_back(frame{ref->m_data.m_value.object, nullptr, ref, 0, ordered_state(0, false)}); + in_object = true; + stored_array = nullptr; + +#if JSON_DIAGNOSTIC_POSITIONS + // Ensure this is after the call to store to ensure correct start position. + diagnostic_positions::set_container_start(*ref, m_lexer_ref); +#endif + + return check_container_size(*this, len, "object", ref); + } + + /// objects in std::map and the like have no use for collecting their members + bool start_object(std::size_t len, std::false_type /*indexed*/) + { + return start_stored_object(len); + } + + /// An object in a vector of members adds a member by appending it to the + /// vector, which grows it again and again (and copies the keys each time). + /// Collect the members of an object of unknown size on a stack shared by + /// all open objects instead, and move them into a vector of the exact size + /// when the closing brace arrives. + bool start_object(std::size_t len, std::true_type /*indexed*/) + { + if (!collects(len)) + { + return start_stored_object(len); + } + + BasicJsonType* slot = nullptr; + std::size_t dest_member = 0; + container_destination(slot, dest_member); + frames.push_back(frame{nullptr, nullptr, slot, ordered_data.members.size(), ordered_state(dest_member, true)}); + in_object = false; + stored_array = nullptr; + return true; + } + + bool key(string_t& val, std::false_type /*indexed*/) + { + JSON_ASSERT(frames.back().object != nullptr); + + // add null at the given key and store the reference for later; the + // key is moved, as the lexer starts the next token with reset() + object_element = insert_key(frames.back(), std::move(val), std::false_type {}); + return true; + } + + bool key(string_t& val, std::true_type /*indexed*/) + { + frame& f = frames.back(); + if (JSON_HEDLEY_UNLIKELY(f.object == nullptr)) + { + JSON_ASSERT(f.ordered.buffered); + if (collect_member(f, val)) + { + return true; + } + // the object has grown big: it is stored directly from here on + } + object_element = insert_key(f, std::move(val), std::true_type {}); + return true; + } + + /*! + @brief add a member to the object that is collected on the stack, or find the one with the key + @return false if the object has collected enough members to be stored + directly, which is done and which leaves the key to be inserted + */ + bool collect_member(frame& f, string_t& key) + { + for (std::size_t i = f.start; i < ordered_data.members.size(); ++i) + { + if (ordered_data.members[i].first == key) + { + ordered_data.member_index = i; + return true; + } + } + + if (JSON_HEDLEY_UNLIKELY(ordered_data.members.size() - f.start >= object_buffer_limit)) + { + spill_object(f); + return false; + } + + reserve_members(); + ordered_data.members.emplace_back(std::move(key), BasicJsonType()); + ordered_data.member_index = ordered_data.members.size() - 1; + return true; + } + + /// the members of an object that is collected on the stack as an object; + /// they are the top of the shared member stack, from f.start on (the + /// members below belong to enclosing objects that are still open) + object_t* members_to_object(const frame& f, BasicJsonType& value) + { + const auto first = ordered_data.members.begin() + static_cast(f.start); + auto* const obj = BasicJsonType::template create(std::make_move_iterator(first), std::make_move_iterator(ordered_data.members.end())); + value.m_data.m_value.object = obj; + value.m_data.m_type = BasicJsonType::value_t::object; + ordered_data.members.erase(first, ordered_data.members.end()); + return obj; + } + + /// switch an object that collects its members on the stack to storing them directly + JSON_HEDLEY_NEVER_INLINE + void spill_object(frame& f) + { + BasicJsonType value; + f.object = members_to_object(f, value); + f.ordered.buffered = false; + in_object = true; + // false positive: an object without an enclosing container is the root, + // which has a slot, so place_container() does not use the nullptr + // @infer-ignore NULLPTR_DEREFERENCE + place_container(f, frames.size() >= 2 ? &frames[frames.size() - 2] : nullptr, std::move(value), false); + } + + bool end_object(std::false_type /*indexed*/) + { + return end_stored_object(); + } + + bool end_object(std::true_type /*indexed*/) + { + if (JSON_HEDLEY_LIKELY(frames.back().object != nullptr)) + { + return end_stored_object(); + } + + // the members are on the stack: make them the object + const frame f = frames.back(); + frames.pop_back(); + reenter_enclosing_container(); + + BasicJsonType value; + members_to_object(f, value); + place_container(f, frames.empty() ? nullptr : &frames.back(), std::move(value), true); + return true; + } + + bool end_stored_object() + { + JSON_ASSERT(frames.back().object != nullptr); + +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + // objects are never collected, so their value is where it was stored + BasicJsonType* const ref = frames.back().slot; +#endif +#if JSON_DIAGNOSTIC_POSITIONS + diagnostic_positions::set_container_end(*ref, m_lexer_ref); +#endif +#if JSON_DIAGNOSTICS + ref->set_parents(); +#endif + + release_key_index(frames.back(), std::integral_constant {}); + frames.pop_back(); + reenter_enclosing_container(); + return true; + } + + /// the number of members an object collects on the stack before it + /// switches to storing them directly + static constexpr std::size_t object_buffer_limit = 32; + + void reserve_members() + { + if (JSON_HEDLEY_UNLIKELY(ordered_data.members.capacity() == 0)) + { + ordered_data.members.reserve(16); + } + } + + /// the number of elements an array collects on the stack before it + /// switches to storing them directly, so that it does not hold up the + /// stack with its elements (and move all of them in the end) + static constexpr std::size_t buffer_limit = 16; + + void reserve_elements() + { + if (JSON_HEDLEY_UNLIKELY(elements.capacity() == 0)) + { + elements.reserve(32); + } + } + + /// store the elements of a new array directly in it + void start_stored_array(const std::size_t len) + { + BasicJsonType* const ref = store(BasicJsonType::value_t::array); + frames.push_back(frame{nullptr, ref->m_data.m_value.array, ref, 0, ordered_state(0, false)}); + in_object = false; + stored_array = ref->m_data.m_value.array; + +#if JSON_DIAGNOSTIC_POSITIONS + // Ensure this is after the call to store to ensure correct start position. + diagnostic_positions::set_container_start(*ref, m_lexer_ref); +#endif + + if (len != detail::unknown_size()) + { + reserve_array(*ref->m_data.m_value.array, len, priority_tag<1> {}); + } + } + + /*! + @brief store a value as the next element of the open array or object (or + as the root) + @return the stored value + */ + template + JSON_HEDLEY_RETURNS_NON_NULL + BasicJsonType* store(Value&& v) + { + if (stored_array != nullptr) + { + stored_array->emplace_back(std::forward(v)); + return &stored_array->back(); + } + + if (in_object) + { + JSON_ASSERT(object_element); + set_member(*object_element, std::forward(v), std::is_same {}); + return object_element; + } + + return store_collected(std::forward(v), std::integral_constant {}); + } + + /// store a value that is the root, an element of an array that is collected + /// on the stack, or (in a vector) a member of an object that is + template + BasicJsonType* store_collected(Value&& v, std::true_type /*indexed*/) + { + if (!frames.empty() && frames.back().ordered.buffered) + { + BasicJsonType& member = ordered_data.members[ordered_data.member_index].second; + set_member(member, std::forward(v), std::is_same {}); + return &member; + } + return store_collected(std::forward(v), std::false_type {}); + } + + template + BasicJsonType* store_collected(Value&& v, std::false_type /*indexed*/) + { + if (frames.empty()) + { + root = BasicJsonType(std::forward(v)); + return &root; + } + + frame& f = frames.back(); + elements.emplace_back(std::forward(v)); + if (JSON_HEDLEY_UNLIKELY(elements.size() - f.start > buffer_limit)) + { + return spill(f); + } + return &elements.back(); + } + + /*! + @brief switch an array that collects its elements on the stack to storing + them directly + @return the last element of the array + */ + JSON_HEDLEY_NEVER_INLINE + BasicJsonType* spill(frame& f) + { + JSON_ASSERT(f.array == nullptr); + const auto first = elements.begin() + static_cast(f.start); + BasicJsonType value(BasicJsonType::value_t::array); + array_t& arr = *value.m_data.m_value.array; + // the capacity a vector that grew by itself has with this many elements + reserve_array(arr, 2 * buffer_limit, priority_tag<1> {}); + arr.insert(arr.end(), std::make_move_iterator(first), std::make_move_iterator(elements.end())); + elements.erase(first, elements.end()); + f.array = &arr; + stored_array = &arr; + + // the array takes the place its first element had on the stack + place_container(f, frames.size() >= 2 ? &frames[frames.size() - 2] : nullptr, std::move(value), false); + return &arr.back(); + } + + /// set the value of the member key() just added + template + JSON_HEDLEY_ALWAYS_INLINE + static void set_member(BasicJsonType& member, Value&& v, std::true_type /*default base*/) + { + if (JSON_HEDLEY_LIKELY(member.is_null())) + { + // a new member is null and holds nothing: construct the value + // over it instead of destroying and swapping + ::new (static_cast(&member)) BasicJsonType(std::forward(v)); + } + else + { + // the key occurred before + member = BasicJsonType(std::forward(v)); + } + } + + /// a custom base class may care about the lifetime of the value + template + static void set_member(BasicJsonType& member, Value&& v, std::false_type /*custom base*/) + { + member = BasicJsonType(std::forward(v)); + } + + /// whether the values of a container of @a len elements are collected on the stack + static bool collects(const std::size_t len) noexcept + { + return collects(len, std::integral_constant {}); + } + + static bool collects(const std::size_t len, std::true_type /*collect*/) noexcept + { + return len == detail::unknown_size(); + } + + static bool collects(const std::size_t /*len*/, std::false_type /*collect*/) noexcept + { + return false; + } + + /// after a container was closed: the one it was in is the one that gets the next value + void reenter_enclosing_container() + { + if (frames.empty()) + { + in_object = false; + stored_array = nullptr; + } + else + { + const frame& f = frames.back(); + in_object = f.object != nullptr; + stored_array = f.array; + } + } + + template + void handle_value(Value&& v) + { +#if JSON_DIAGNOSTIC_POSITIONS + BasicJsonType* const stored = store(std::forward(v)); + diagnostic_positions::set_from_lexer(*stored, m_lexer_ref); +#else + store(std::forward(v)); +#endif + } + + /// the parsed JSON value + BasicJsonType& root; + /// the open arrays and objects + frame_stack frames {}; + /// the elements of the open arrays that are not stored in them yet + std::vector elements {}; + /// what objects in a vector need besides the frames (std::map based objects + /// need nothing, so they do not carry it) + struct ordered_data_t + { + /// the members of the open objects in a vector that are not stored in them yet + std::vector> members {}; + /// the member of the innermost such object that key() added or found last + std::size_t member_index = 0; + /// the key indexes of the open objects that have one, innermost last + std::vector> key_indexes {}; // NOLINT(readability-redundant-member-init) + }; + struct no_ordered_data_t {}; + typename std::conditional::type ordered_data {}; + /// helper to hold the reference for the next object element + BasicJsonType* object_element = nullptr; + /// whether the innermost container is an object (frames.back().object) + bool in_object = false; + /// the array that stores the elements of the innermost array as they + /// arrive (frames.back().array), or nullptr + array_t* stored_array = nullptr; + /// the lexer reference to obtain the current position + lexer_t* m_lexer_ref = nullptr; +}; + +/*! +@brief SAX implementation to create a JSON value from the events of a binary reader + +The DOM parser of the text formats (json_sax_dom_parser) collects the elements +of arrays and the members of objects on a stack and builds them in one +exact-size allocation, which pays off because a text parser does not know the +number of elements up front. Binary formats announce the size of their +containers (CBOR, MessagePack, BSON and BON8 do, and so do the optimized +containers of UBJSON and BJData), so they store each value directly in its +container, and the bookkeeping of the collecting parser would be pure +overhead for them. This is the plain parser: the structure or hierarchy of the +JSON value is managed by the stack `ref_stack` which contains a pointer to the +respective array or object for each recursion depth. + +After successful parsing, the value that is passed by reference to the +constructor contains the parsed value. + +@tparam BasicJsonType the JSON type +*/ +template +class json_sax_dom_binary_parser : public json_sax_dom_error_state +{ + public: + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + using binary_t = typename BasicJsonType::binary_t; + using lexer_t = lexer; + + /*! + @param[in,out] r reference to a JSON value that is manipulated while + parsing + @param[in] allow_exceptions_ whether parse errors yield exceptions + */ + explicit json_sax_dom_binary_parser(BasicJsonType& r, const bool allow_exceptions_ = true, lexer_t* lexer_ = nullptr) + : json_sax_dom_error_state(allow_exceptions_), root(r), m_lexer_ref(lexer_) + {} + + // make class move-only + json_sax_dom_binary_parser(const json_sax_dom_binary_parser&) = delete; + json_sax_dom_binary_parser(json_sax_dom_binary_parser&&) = default; // NOLINT(hicpp-noexcept-move,performance-noexcept-move-constructor) + json_sax_dom_binary_parser& operator=(const json_sax_dom_binary_parser&) = delete; + json_sax_dom_binary_parser& operator=(json_sax_dom_binary_parser&&) = default; // NOLINT(hicpp-noexcept-move,performance-noexcept-move-constructor) + ~json_sax_dom_binary_parser() = default; + + bool null() + { + handle_value(nullptr); + return true; + } + + bool boolean(bool val) + { + handle_value(val); + return true; + } + + bool number_integer(number_integer_t val) + { + handle_value(val); + return true; + } + + bool number_unsigned(number_unsigned_t val) + { + handle_value(val); + return true; + } + + bool number_float(number_float_t val, const string_t& /*unused*/) + { + handle_value(val); + return true; + } + + bool string(string_t& val) + { + // json_sax documents that the passed value may be moved from, + // so hand the buffer over instead of copying it + handle_value(std::move(val)); + return true; + } + + bool binary(binary_t& val) + { + // json_sax documents that the passed value may be moved from, + // so hand the buffer over instead of copying it + handle_value(std::move(val)); + return true; + } + bool start_object(std::size_t len) { ref_stack.push_back(handle_value(BasicJsonType::value_t::object)); #if JSON_DIAGNOSTIC_POSITIONS - // Manually set the start position of the object here. // Ensure this is after the call to handle_value to ensure correct start position. - if (m_lexer_ref) - { - // Lexer has read the first character of the object, so - // subtract 1 from the position to get the correct start position. - ref_stack.back()->start_position = m_lexer_ref->get_position() - 1; - } + diagnostic_positions::set_container_start(*ref_stack.back(), m_lexer_ref); #endif - if (JSON_HEDLEY_UNLIKELY(len != detail::unknown_size() && len > ref_stack.back()->max_size())) - { - return parse_error(0, "", out_of_range::create(408, concat("excessive object size: ", std::to_string(len)), ref_stack.back())); - } - - return true; + return check_container_size(*this, len, "object", ref_stack.back()); } bool key(string_t& val) @@ -372,8 +1482,9 @@ class json_sax_dom_parser JSON_ASSERT(!ref_stack.empty()); JSON_ASSERT(ref_stack.back()->is_object()); - // add null at the given key and store the reference for later - object_element = &(ref_stack.back()->m_data.m_value.object->operator[](val)); + // add null at the given key and store the reference for later; the + // key is moved, as the lexer starts the next token with reset() + object_element = &(ref_stack.back()->m_data.m_value.object->operator[](std::move(val))); return true; } @@ -383,11 +1494,7 @@ class json_sax_dom_parser JSON_ASSERT(ref_stack.back()->is_object()); #if JSON_DIAGNOSTIC_POSITIONS - if (m_lexer_ref) - { - // Lexer's position is past the closing brace, so set that as the end position. - ref_stack.back()->end_position = m_lexer_ref->get_position(); - } + diagnostic_positions::set_container_end(*ref_stack.back(), m_lexer_ref); #endif ref_stack.back()->set_parents(); @@ -400,17 +1507,13 @@ class json_sax_dom_parser ref_stack.push_back(handle_value(BasicJsonType::value_t::array)); #if JSON_DIAGNOSTIC_POSITIONS - // Manually set the start position of the array here. // Ensure this is after the call to handle_value to ensure correct start position. - if (m_lexer_ref) - { - ref_stack.back()->start_position = m_lexer_ref->get_position() - 1; - } + diagnostic_positions::set_container_start(*ref_stack.back(), m_lexer_ref); #endif - if (JSON_HEDLEY_UNLIKELY(len != detail::unknown_size() && len > ref_stack.back()->max_size())) + if (JSON_HEDLEY_UNLIKELY(!check_container_size(*this, len, "array", ref_stack.back()))) { - return parse_error(0, "", out_of_range::create(408, concat("excessive array size: ", std::to_string(len)), ref_stack.back())); + return false; } if (len != detail::unknown_size()) @@ -427,11 +1530,7 @@ class json_sax_dom_parser JSON_ASSERT(ref_stack.back()->is_array()); #if JSON_DIAGNOSTIC_POSITIONS - if (m_lexer_ref) - { - // Lexer's position is past the closing bracket, so set that as the end position. - ref_stack.back()->end_position = m_lexer_ref->get_position(); - } + diagnostic_positions::set_container_end(*ref_stack.back(), m_lexer_ref); #endif ref_stack.back()->set_parents(); @@ -439,24 +1538,6 @@ class json_sax_dom_parser return true; } - template - bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, - const Exception& ex) - { - errored = true; - static_cast(ex); - if (allow_exceptions) - { - JSON_THROW(ex); - } - return false; - } - - constexpr bool is_errored() const - { - return errored; - } - private: /*! @@ -510,16 +1591,12 @@ class json_sax_dom_parser std::vector ref_stack {}; /// helper to hold the reference for the next object element BasicJsonType* object_element = nullptr; - /// whether a syntax error occurred - bool errored = false; - /// whether to throw exceptions in case of errors - const bool allow_exceptions = true; /// the lexer reference to obtain the current position lexer_t* m_lexer_ref = nullptr; }; template -class json_sax_dom_callback_parser +class json_sax_dom_callback_parser : public json_sax_dom_error_state { public: using number_integer_t = typename BasicJsonType::number_integer_t; @@ -535,7 +1612,7 @@ class json_sax_dom_callback_parser parser_callback_t cb, const bool allow_exceptions_ = true, lexer_t* lexer_ = nullptr) - : root(r), callback(std::move(cb)), allow_exceptions(allow_exceptions_), m_lexer_ref(lexer_) + : json_sax_dom_error_state(allow_exceptions_), root(r), callback(std::move(cb)), m_lexer_ref(lexer_) { keep_stack.push_back(true); } @@ -611,21 +1688,11 @@ class json_sax_dom_callback_parser { #if JSON_DIAGNOSTIC_POSITIONS - // Manually set the start position of the object here. // Ensure this is after the call to handle_value to ensure correct start position. - if (m_lexer_ref) - { - // Lexer has read the first character of the object, so - // subtract 1 from the position to get the correct start position. - ref_stack.back()->start_position = m_lexer_ref->get_position() - 1; - } + diagnostic_positions::set_container_start(*ref_stack.back(), m_lexer_ref); #endif - // check object limit - if (JSON_HEDLEY_UNLIKELY(len != detail::unknown_size() && len > ref_stack.back()->max_size())) - { - return parse_error(0, "", out_of_range::create(408, concat("excessive object size: ", std::to_string(len)), ref_stack.back())); - } + return check_container_size(*this, len, "object", ref_stack.back()); } return true; } @@ -638,16 +1705,13 @@ class json_sax_dom_callback_parser // handle_value() without touching the key stacks if (keep_stack.back()) { - BasicJsonType k = BasicJsonType(val); - static_cast(callback(static_cast(ref_stack.size()), parse_event_t::key, k)); + static_cast(callback(static_cast(ref_stack.size()), parse_event_t::key, key_value(val))); } return true; } - BasicJsonType k = BasicJsonType(val); - // check callback for the key - const bool keep = callback(static_cast(ref_stack.size()), parse_event_t::key, k); + const bool keep = callback(static_cast(ref_stack.size()), parse_event_t::key, key_value(val)); key_keep_stack.push_back(keep); // remember the key so a rejected value can be erased without searching // the object for it (kept in lockstep with key_keep_stack) @@ -657,16 +1721,19 @@ class json_sax_dom_callback_parser if (keep && ref_stack.back()) { auto& obj = *ref_stack.back()->m_data.m_value.object; - const auto it = obj.find(val); - if (it != obj.end()) + // a single lookup: insert the placeholder, or find the existing member + const auto inserted = obj.emplace(std::move(val), discarded); + const auto it = inserted.first; + if (!inserted.second) { // this is a duplicate key (legal in JSON); remember its // current value so it can be restored later if the new // value is rejected by the callback, instead of being // erased together with the discarded placeholder duplicate_key_stash.emplace_back(&(it->second), it->second); + it->second = discarded; } - object_element = &(obj[val] = discarded); + object_element = &(it->second); } return true; @@ -695,11 +1762,7 @@ class json_sax_dom_callback_parser { #if JSON_DIAGNOSTIC_POSITIONS - if (m_lexer_ref) - { - // Lexer's position is past the closing brace, so set that as the end position. - ref_stack.back()->end_position = m_lexer_ref->get_position(); - } + diagnostic_positions::set_container_end(*ref_stack.back(), m_lexer_ref); #endif ref_stack.back()->set_parents(); @@ -710,13 +1773,7 @@ class json_sax_dom_callback_parser } } - JSON_ASSERT(!ref_stack.empty()); - JSON_ASSERT(!keep_stack.empty()); - JSON_ASSERT(!container_key_stack.empty()); - ref_stack.pop_back(); - keep_stack.pop_back(); - const string_t object_key = std::move(container_key_stack.back()); - container_key_stack.pop_back(); + const string_t object_key = pop_container(); if (!ref_stack.empty() && ref_stack.back() && ref_stack.back()->is_structured()) { @@ -742,20 +1799,13 @@ class json_sax_dom_callback_parser { #if JSON_DIAGNOSTIC_POSITIONS - // Manually set the start position of the array here. // Ensure this is after the call to handle_value to ensure correct start position. - if (m_lexer_ref) - { - // Lexer has read the first character of the array, so - // subtract 1 from the position to get the correct start position. - ref_stack.back()->start_position = m_lexer_ref->get_position() - 1; - } + diagnostic_positions::set_container_start(*ref_stack.back(), m_lexer_ref); #endif - // check array limit - if (JSON_HEDLEY_UNLIKELY(len != detail::unknown_size() && len > ref_stack.back()->max_size())) + if (JSON_HEDLEY_UNLIKELY(!check_container_size(*this, len, "array", ref_stack.back()))) { - return parse_error(0, "", out_of_range::create(408, concat("excessive array size: ", std::to_string(len)), ref_stack.back())); + return false; } if (len != detail::unknown_size()) @@ -779,11 +1829,7 @@ class json_sax_dom_callback_parser { #if JSON_DIAGNOSTIC_POSITIONS - if (m_lexer_ref) - { - // Lexer's position is past the closing bracket, so set that as the end position. - ref_stack.back()->end_position = m_lexer_ref->get_position(); - } + diagnostic_positions::set_container_end(*ref_stack.back(), m_lexer_ref); #endif ref_stack.back()->set_parents(); @@ -809,13 +1855,7 @@ class json_sax_dom_callback_parser } } - JSON_ASSERT(!ref_stack.empty()); - JSON_ASSERT(!keep_stack.empty()); - JSON_ASSERT(!container_key_stack.empty()); - ref_stack.pop_back(); - keep_stack.pop_back(); - const string_t object_key = std::move(container_key_stack.back()); - container_key_stack.pop_back(); + const string_t object_key = pop_container(); // remove discarded value if (!ref_stack.empty() && ref_stack.back()) @@ -836,26 +1876,33 @@ class json_sax_dom_callback_parser return true; } - template - bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, - const Exception& ex) - { - errored = true; - static_cast(ex); - if (allow_exceptions) - { - JSON_THROW(ex); - } - return false; - } - - constexpr bool is_errored() const - { - return errored; - } - private: + /*! + @brief the value of a key event for the callback + + A string value of its own for every key would allocate it every time, so + one is kept and set to the key. A callback that changed or moved it gets a + new one, as it would have got a new value for every key anyway. + */ + BasicJsonType& key_value(const string_t& key) + { +#if JSON_DIAGNOSTIC_POSITIONS + // a callback may have left positions on the value + key_scratch = BasicJsonType(key); +#else + if (JSON_HEDLEY_LIKELY(key_scratch.is_string())) + { + *key_scratch.m_data.m_value.string = key; + } + else + { + key_scratch = BasicJsonType(key); + } +#endif + return key_scratch; + } + /// if there is a pending duplicate-key stash entry for this exact slot, /// remove it from the stash; if restore_value is true, the stashed /// previous value is moved back into the slot first (use this when the @@ -902,6 +1949,23 @@ class json_sax_dom_callback_parser return string_t{}; } + /*! + @brief leave the object or array that end_object()/end_array() just closed + @return the key the container is stored under in its parent (see + container_key_stack) + */ + string_t pop_container() + { + JSON_ASSERT(!ref_stack.empty()); + JSON_ASSERT(!keep_stack.empty()); + JSON_ASSERT(!container_key_stack.empty()); + ref_stack.pop_back(); + keep_stack.pop_back(); + string_t object_key = std::move(container_key_stack.back()); + container_key_stack.pop_back(); + return object_key; + } + /*! @brief remove the discarded value the callback rejected from its parent, unless it is a duplicate key's slot with a stashed previous value, in @@ -1078,14 +2142,12 @@ class json_sax_dom_callback_parser /// value is later rejected by the callback, instead of erasing the /// member entirely std::vector> duplicate_key_stash {}; - /// whether a syntax error occurred - bool errored = false; /// callback function const parser_callback_t callback = nullptr; - /// whether to throw exceptions in case of errors - const bool allow_exceptions = true; /// a discarded value for the callback BasicJsonType discarded = BasicJsonType::value_t::discarded; + /// the value of the key event for the callback (see key_value()) + BasicJsonType key_scratch {}; /// the lexer reference to obtain the current position lexer_t* m_lexer_ref = nullptr; }; diff --git a/include/nlohmann/detail/input/lexer.hpp b/include/nlohmann/detail/input/lexer.hpp index 361baa642..14d14f42a 100644 --- a/include/nlohmann/detail/input/lexer.hpp +++ b/include/nlohmann/detail/input/lexer.hpp @@ -12,11 +12,14 @@ #include // size_t #include // uint32_t #include // snprintf +#include // memcmp #include // initializer_list +#include // numeric_limits #include // char_traits, string #include // move #include // vector +#include #include #include #include @@ -165,6 +168,43 @@ constexpr bool input_adapter_supports_bulk_scan(std::false_type /*detected*/) return false; } +// Detect whether an input adapter keeps the current token itself and can +// hand it out on demand (see input_stream_adapter::supports_token_tracking), +// so the lexer need not copy every scanned character into token_string. +template +using detect_supports_token_tracking = decltype(InputAdapterType::supports_token_tracking); + +template +constexpr bool input_adapter_supports_token_tracking(std::true_type /*detected*/) +{ + return InputAdapterType::supports_token_tracking; +} + +template +constexpr bool input_adapter_supports_token_tracking(std::false_type /*detected*/) +{ + return false; +} + +// Detect whether the contiguous block of a bulk-scan adapter is only a window +// into the input (see input_stream_adapter::bulk_window): its end is not the +// end of the input, so a token that touches it must be finished character by +// character. +template +using detect_bulk_window = decltype(InputAdapterType::bulk_window); + +template +constexpr bool input_adapter_bulk_window(std::true_type /*detected*/) +{ + return InputAdapterType::bulk_window; +} + +template +constexpr bool input_adapter_bulk_window(std::false_type /*detected*/) +{ + return false; +} + /*! @brief lexical analysis @@ -183,9 +223,29 @@ class lexer : public lexer_base /// whether the last read token can be reconstructed from the input adapter /// on demand (in error paths) instead of being copied on every scanned /// character; see input_adapter_supports_seek - static constexpr bool lazy_token_string = + static constexpr bool seekable = input_adapter_supports_seek(is_detected {}); + /// whether the adapter keeps the token itself (mark_token_start() / + /// copy_token()); see input_adapter_supports_token_tracking + static constexpr bool adapter_token = + !seekable && input_adapter_supports_token_tracking(is_detected {}); + + /// whether the last read token is obtained from the input adapter on + /// demand (in error paths) instead of being copied on every scanned + /// character: seekable adapters reconstruct it from the consumed input, + /// token-tracking adapters keep it themselves + static constexpr bool lazy_token_string = seekable || adapter_token; + + /// whether the read position is derived from the input adapter on demand + /// instead of being counted on every scanned character. Seekable adapters + /// know how much input they consumed and can revisit it, so + /// chars_read_total is the adapter's read count (minus a pending unget), + /// and line and column are recomputed from the consumed input only when + /// get_position() is called (errors). Counting per character kept two + /// increments and a newline check in the hot path of every get(). + static constexpr bool lazy_position = seekable; + /// whether a simulated unget can be passed on to the input adapter, which /// then leaves the character in the input; see /// input_adapter_supports_lookahead @@ -193,12 +253,17 @@ class lexer : public lexer_base input_adapter_supports_lookahead(is_detected {}); /// whether string scanning may bulk-consume runs of ordinary characters - /// directly from a contiguous input buffer (SWAR fast path). This requires - /// the token to be reconstructible lazily (lazy_token_string), so bypassing - /// the per-character capture in get() cannot lose error diagnostics. + /// directly from a contiguous input buffer (SWAR fast path). Without + /// lazy_token_string / lazy_position, the bulk paths copy the consumed + /// characters into token_string and advance the position counters + /// themselves (see bulk_consume). static constexpr bool bulk_scan = - lazy_token_string - && input_adapter_supports_bulk_scan(is_detected {}); + input_adapter_supports_bulk_scan(is_detected {}); + + /// whether the contiguous block is only a window into the input, so that + /// its end does not end a token; see input_adapter_bulk_window + static constexpr bool bulk_window = + input_adapter_bulk_window(is_detected {}); public: using token_type = typename lexer_base::token_type; @@ -207,7 +272,11 @@ class lexer : public lexer_base : ia(std::move(adapter)) , ignore_comments(ignore_comments_) , discard_number_values(discard_number_values_) - {} + { + // the token string of an error before the first token is everything + // read so far + begin_token(std::integral_constant {}); + } // deleted because of pointer members lexer(const lexer&) = delete; @@ -221,6 +290,41 @@ class lexer : public lexer_base // scan functions ///////////////////// + /// contiguous input: try to decode the 4 hex digits following `\\u` + /// directly from the input buffer via hex_codepoint(), instead of 4 calls + /// to get(). On success, advances the adapter exactly as those 4 get() + /// calls would (including the position and token bookkeeping of a + /// streaming adapter, see bulk_consume) and leaves @a current holding the last of + /// the 4 digits, just as the last such get() would; the codepoint is + /// written to @a out. Makes no state change and returns false for a + /// pending unget, fewer than 4 remaining bytes, or any of the 4 bytes not + /// being a hex digit, so the caller falls back unchanged to the + /// per-character loop, which then reports the same diagnostic (stopping + /// at the first invalid digit) as before this optimization. + bool get_codepoint_bulk(std::true_type /*bulk*/, int& out) + { + if (next_unget || ia.bulk_remaining() < 4) + { + return false; + } + const char_type* const raw = ia.bulk_data(); + const int codepoint = hex_codepoint(reinterpret_cast(raw)); + if (codepoint < 0) + { + return false; + } + bulk_consume(reinterpret_cast(raw), 4); + current = char_traits::to_int_type(raw[3]); + out = codepoint; + return true; + } + + /// streaming input: no bulk fast path + bool get_codepoint_bulk(std::false_type /*bulk*/, int& /*out*/) const noexcept + { + return false; + } + /*! @brief get codepoint from 4 hex characters following `\\u` @@ -240,6 +344,14 @@ class lexer : public lexer_base { // this function only makes sense after reading `\u` JSON_ASSERT(current == 'u'); + + // contiguous input: decode all 4 hex digits directly from the buffer + int fast_codepoint = 0; + if (get_codepoint_bulk(std::integral_constant {}, fast_codepoint)) + { + return fast_codepoint; + } + int codepoint = 0; const auto factors = { 12u, 8u, 4u, 0u }; @@ -307,38 +419,79 @@ class lexer : public lexer_base } /// contiguous input: bulk-append the run of ordinary characters and complete - /// well-formed UTF-8 sequences starting at the current read position, leaving - /// the first byte that needs individual handling (the closing quote, an - /// escape, a control character, or an ill-formed UTF-8 byte) for get() - void scan_string_bulk(std::true_type /*bulk*/) + /// well-formed UTF-8 sequences starting at the current read position, then + /// 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(). + /// 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 + /// function, which load the read position again right after it was stored + /// (3% of json::accept() on jeopardy.json on Apple M1). + char_int_type get_after_string_run(std::true_type /*bulk*/) { // a pending unget must be consumed through the normal path first if (next_unget) { - return; + return get(); } const std::size_t remaining = ia.bulk_remaining(); if (remaining == 0) { - return; + return get(); } - const auto* const data = reinterpret_cast(ia.bulk_data()); + const auto* const raw = ia.bulk_data(); + const auto* const data = reinterpret_cast(raw); - const std::size_t pos = string_bulk_run(data, remaining); - if (pos == 0) + const std::size_t pos = lexer_string_bulk_run(data, remaining); + if (pos != 0) { - return; + token_buffer.append(reinterpret_cast(data), pos); + bulk_consume(reinterpret_cast(data), pos); } - token_buffer.append(reinterpret_cast(data), pos); - ia.bulk_skip(pos); - // the run contains no newline (all bytes < 0x20 are treated as special), - // so only the flat character counters advance - position.chars_read_total += pos; - position.chars_read_current_line += pos; + if (pos == remaining) + { + return get(); // end of the contiguous input + } + // what get() does, with the byte at hand + advance_position(); + ia.bulk_skip(1); + current = char_traits::to_int_type(raw[pos]); + return track_after_read(); + } + + /// consume @a n characters at @a data (the adapter's bulk_data()) and do + /// the bookkeeping get() would have done for them; the characters are + /// never newlines + void bulk_consume(const char* data, std::size_t n) + { + bulk_track_position(n, std::integral_constant {}); + bulk_track_token(data, n, std::integral_constant {}); + ia.bulk_skip(n); + } + + /// seekable adapter: the position is derived from the adapter + void bulk_track_position(std::size_t /*n*/, std::true_type /*lazy*/) const noexcept {} + + /// streaming adapter: count the characters + void bulk_track_position(std::size_t n, std::false_type /*lazy*/) noexcept + { + position.chars_read_total += n; + position.chars_read_current_line += n; + } + + /// the token is obtained from the adapter + void bulk_track_token(const char* /*data*/, std::size_t /*n*/, std::true_type /*lazy*/) const noexcept {} + + /// streaming adapter: capture the characters + void bulk_track_token(const char* data, std::size_t n, std::false_type /*lazy*/) + { + token_string.insert(token_string.end(), data, data + n); } /// streaming input: no bulk fast path - void scan_string_bulk(std::false_type /*bulk*/) const noexcept {} + char_int_type get_after_string_run(std::false_type /*bulk*/) + { + return get(); + } /*! @brief scan a string literal @@ -366,11 +519,8 @@ class lexer : public lexer_base while (true) { // bulk-consume ordinary characters from contiguous input, then - // handle the next special byte through the switch below - scan_string_bulk(std::integral_constant {}); - - // get the next character - switch (get()) + // get the next character, the one that ends the run + switch (get_after_string_run(std::integral_constant())) { // end of file while parsing the string case char_traits::eof(): @@ -507,194 +657,74 @@ class lexer : public lexer_base // invalid control characters case 0x00: - { - error_message = "invalid string: control character U+0000 (NUL) must be escaped to \\u0000"; - return token_type::parse_error; - } - case 0x01: - { - error_message = "invalid string: control character U+0001 (SOH) must be escaped to \\u0001"; - return token_type::parse_error; - } - case 0x02: - { - error_message = "invalid string: control character U+0002 (STX) must be escaped to \\u0002"; - return token_type::parse_error; - } - case 0x03: - { - error_message = "invalid string: control character U+0003 (ETX) must be escaped to \\u0003"; - return token_type::parse_error; - } - case 0x04: - { - error_message = "invalid string: control character U+0004 (EOT) must be escaped to \\u0004"; - return token_type::parse_error; - } - case 0x05: - { - error_message = "invalid string: control character U+0005 (ENQ) must be escaped to \\u0005"; - return token_type::parse_error; - } - case 0x06: - { - error_message = "invalid string: control character U+0006 (ACK) must be escaped to \\u0006"; - return token_type::parse_error; - } - case 0x07: - { - error_message = "invalid string: control character U+0007 (BEL) must be escaped to \\u0007"; - return token_type::parse_error; - } - case 0x08: - { - error_message = "invalid string: control character U+0008 (BS) must be escaped to \\u0008 or \\b"; - return token_type::parse_error; - } - case 0x09: - { - error_message = "invalid string: control character U+0009 (HT) must be escaped to \\u0009 or \\t"; - return token_type::parse_error; - } - case 0x0A: - { - error_message = "invalid string: control character U+000A (LF) must be escaped to \\u000A or \\n"; - return token_type::parse_error; - } - case 0x0B: - { - error_message = "invalid string: control character U+000B (VT) must be escaped to \\u000B"; - return token_type::parse_error; - } - case 0x0C: - { - error_message = "invalid string: control character U+000C (FF) must be escaped to \\u000C or \\f"; - return token_type::parse_error; - } - case 0x0D: - { - error_message = "invalid string: control character U+000D (CR) must be escaped to \\u000D or \\r"; - return token_type::parse_error; - } - case 0x0E: - { - error_message = "invalid string: control character U+000E (SO) must be escaped to \\u000E"; - return token_type::parse_error; - } - case 0x0F: - { - error_message = "invalid string: control character U+000F (SI) must be escaped to \\u000F"; - return token_type::parse_error; - } - case 0x10: - { - error_message = "invalid string: control character U+0010 (DLE) must be escaped to \\u0010"; - return token_type::parse_error; - } - case 0x11: - { - error_message = "invalid string: control character U+0011 (DC1) must be escaped to \\u0011"; - return token_type::parse_error; - } - case 0x12: - { - error_message = "invalid string: control character U+0012 (DC2) must be escaped to \\u0012"; - return token_type::parse_error; - } - case 0x13: - { - error_message = "invalid string: control character U+0013 (DC3) must be escaped to \\u0013"; - return token_type::parse_error; - } - case 0x14: - { - error_message = "invalid string: control character U+0014 (DC4) must be escaped to \\u0014"; - return token_type::parse_error; - } - case 0x15: - { - error_message = "invalid string: control character U+0015 (NAK) must be escaped to \\u0015"; - return token_type::parse_error; - } - case 0x16: - { - error_message = "invalid string: control character U+0016 (SYN) must be escaped to \\u0016"; - return token_type::parse_error; - } - case 0x17: - { - error_message = "invalid string: control character U+0017 (ETB) must be escaped to \\u0017"; - return token_type::parse_error; - } - case 0x18: - { - error_message = "invalid string: control character U+0018 (CAN) must be escaped to \\u0018"; - return token_type::parse_error; - } - case 0x19: - { - error_message = "invalid string: control character U+0019 (EM) must be escaped to \\u0019"; - return token_type::parse_error; - } - case 0x1A: - { - error_message = "invalid string: control character U+001A (SUB) must be escaped to \\u001A"; - return token_type::parse_error; - } - case 0x1B: - { - error_message = "invalid string: control character U+001B (ESC) must be escaped to \\u001B"; - return token_type::parse_error; - } - case 0x1C: - { - error_message = "invalid string: control character U+001C (FS) must be escaped to \\u001C"; - return token_type::parse_error; - } - case 0x1D: - { - error_message = "invalid string: control character U+001D (GS) must be escaped to \\u001D"; - return token_type::parse_error; - } - case 0x1E: - { - error_message = "invalid string: control character U+001E (RS) must be escaped to \\u001E"; - return token_type::parse_error; - } - case 0x1F: { - error_message = "invalid string: control character U+001F (US) must be escaped to \\u001F"; + static const std::array control_character_messages = {{ + "invalid string: control character U+0000 (NUL) must be escaped to \\u0000", + "invalid string: control character U+0001 (SOH) must be escaped to \\u0001", + "invalid string: control character U+0002 (STX) must be escaped to \\u0002", + "invalid string: control character U+0003 (ETX) must be escaped to \\u0003", + "invalid string: control character U+0004 (EOT) must be escaped to \\u0004", + "invalid string: control character U+0005 (ENQ) must be escaped to \\u0005", + "invalid string: control character U+0006 (ACK) must be escaped to \\u0006", + "invalid string: control character U+0007 (BEL) must be escaped to \\u0007", + "invalid string: control character U+0008 (BS) must be escaped to \\u0008 or \\b", + "invalid string: control character U+0009 (HT) must be escaped to \\u0009 or \\t", + "invalid string: control character U+000A (LF) must be escaped to \\u000A or \\n", + "invalid string: control character U+000B (VT) must be escaped to \\u000B", + "invalid string: control character U+000C (FF) must be escaped to \\u000C or \\f", + "invalid string: control character U+000D (CR) must be escaped to \\u000D or \\r", + "invalid string: control character U+000E (SO) must be escaped to \\u000E", + "invalid string: control character U+000F (SI) must be escaped to \\u000F", + "invalid string: control character U+0010 (DLE) must be escaped to \\u0010", + "invalid string: control character U+0011 (DC1) must be escaped to \\u0011", + "invalid string: control character U+0012 (DC2) must be escaped to \\u0012", + "invalid string: control character U+0013 (DC3) must be escaped to \\u0013", + "invalid string: control character U+0014 (DC4) must be escaped to \\u0014", + "invalid string: control character U+0015 (NAK) must be escaped to \\u0015", + "invalid string: control character U+0016 (SYN) must be escaped to \\u0016", + "invalid string: control character U+0017 (ETB) must be escaped to \\u0017", + "invalid string: control character U+0018 (CAN) must be escaped to \\u0018", + "invalid string: control character U+0019 (EM) must be escaped to \\u0019", + "invalid string: control character U+001A (SUB) must be escaped to \\u001A", + "invalid string: control character U+001B (ESC) must be escaped to \\u001B", + "invalid string: control character U+001C (FS) must be escaped to \\u001C", + "invalid string: control character U+001D (GS) must be escaped to \\u001D", + "invalid string: control character U+001E (RS) must be escaped to \\u001E", + "invalid string: control character U+001F (US) must be escaped to \\u001F" + } + }; + error_message = control_character_messages[static_cast(current)]; return token_type::parse_error; } @@ -1044,9 +1074,11 @@ class lexer : public lexer_base token_type::parse_error otherwise @note The scanner is independent of the current locale: token_buffer - always holds `.`. Only the std::strtod fallback of convert_number() - depends on the locale, and it looks up the decimal point right - before converting (see detail::convert_float_locale_aware()). + always holds `.`. The conversion of float and double does not use + the locale either. Only the std::strtold fallback of + convert_number() for long double formats other than binary64 + depends on it, and it looks up the decimal point right before + converting (see detail::convert_float_locale_aware()). */ token_type scan_number() // lgtm [cpp/use-of-goto] `goto` is used in this function to implement the number-parsing state machine described above. By design, any finite input will eventually reach the "done" state or return token_type::parse_error. In each intermediate state, 1 byte of the input is appended to the token_buffer vector, and only the already initialized variables token_buffer, number_type, and error_message are manipulated. { @@ -1059,7 +1091,7 @@ class lexer : public lexer_base // offset just past the last mantissa byte in token_buffer (i.e. the // index of 'e'/'E', or the whole token when there is no exponent). - // convert_number() uses it to count significant digits; npos means + // convert_number() uses it to split the token; npos means // "not seen an exponent yet" and is resolved at scan_number_done std::size_t mantissa_end = std::string::npos; @@ -1389,8 +1421,8 @@ scan_number_done: @param[in] mantissa_end offset just past the last mantissa byte in token_buffer (the index of 'e'/'E', or token_buffer.size() when there is no exponent); - used to skip Clinger's fast path when it cannot - possibly succeed - see detail::mantissa_fits_clinger() + with decimal_point_position, it locates the parts + of a float token without scanning it again */ token_type convert_number(token_type number_type, std::size_t mantissa_end) { @@ -1444,10 +1476,11 @@ scan_number_done: } // this code is reached if we parse a floating-point number or if an - // integer conversion above overflowed. Prefer std::from_chars - // (Eisel-Lemire, locale-independent, correctly rounded) when available; - // otherwise the exact Clinger fast path (double only); otherwise the - // locale-aware strtof/strtod/strtold. + // integer conversion above overflowed. float and double (and long + // double where it is binary64) are converted by the library itself, + // correctly rounded and independent of the locale; other long double + // formats use std::from_chars when available, otherwise the + // locale-aware strtold. if (convert_float_fast(num_begin, num_end, decimal_point_position, mantissa_end, value_float)) { return token_type::value_float; @@ -1487,9 +1520,12 @@ scan_number_done: const std::size_t avail = rem + 1; // validate + classify the number extent (mirrors scan_number()'s grammar) + // and accumulate the mantissa digits on the way, so that tokens with up + // to 19 digits need no second pass for their value std::size_t i = 0; std::size_t dot_index = std::string::npos; token_type number_type = token_type::value_unsigned; + std::uint64_t w = 0; if (data[0] == '-') { number_type = token_type::value_integer; @@ -1499,22 +1535,21 @@ scan_number_done: return token_type::uninitialized; } } + const std::size_t integer_begin = i; if (data[i] == '0') { ++i; } else if (data[i] >= '1' && data[i] <= '9') { - ++i; - while (i < avail && data[i] >= '0' && data[i] <= '9') - { - ++i; - } + i = accumulate_digits(data, i, avail, w); } else { return token_type::uninitialized; } + std::size_t digit_count = i - integer_begin; + std::size_t fraction_digits = 0; if (i < avail && data[i] == '.') { number_type = token_type::value_float; @@ -1524,31 +1559,45 @@ scan_number_done: { return token_type::uninitialized; } - while (i < avail && data[i] >= '0' && data[i] <= '9') - { - ++i; - } + const std::size_t fraction_begin = i; + i = accumulate_digits(data, i, avail, w); + fraction_digits = i - fraction_begin; + digit_count += fraction_digits; } // the mantissa ends here, whether or not an exponent part follows const std::size_t mantissa_end = i; + std::int64_t exponent = 0; if (i < avail && (data[i] == 'e' || data[i] == 'E')) { number_type = token_type::value_float; ++i; + bool negative_exponent = false; if (i < avail && (data[i] == '+' || data[i] == '-')) { + negative_exponent = data[i] == '-'; ++i; } if (i >= avail || !(data[i] >= '0' && data[i] <= '9')) { return token_type::uninitialized; } + // saturated far beyond every range, see parse_float_exponent() + constexpr std::int64_t saturation = 100000000000000000; // 10^17 while (i < avail && data[i] >= '0' && data[i] <= '9') { + if (exponent < saturation) + { + exponent = (exponent * 10) + (data[i] - '0'); + } ++i; } + exponent = negative_exponent ? -exponent : exponent; } const std::size_t len = i; + if (number_may_continue(len == avail, std::integral_constant {})) + { + return token_type::uninitialized; + } // reset() records where this token starts (for diagnostics), so it has // to run before the input position advances below @@ -1563,12 +1612,14 @@ scan_number_done: #if !JSON_DIAGNOSTIC_POSITIONS if (number_type != token_type::value_float) { - const token_type integer_result = convert_integer(number_type, data, data + len); + // up to 18 digits always fit into std::uint64_t and std::int64_t + const token_type integer_result = (digit_count <= 18) + ? store_integer(number_type, w) + : convert_integer(number_type, data, data + len); if (JSON_HEDLEY_LIKELY(integer_result != token_type::uninitialized)) { - ia.bulk_skip(len - 1); - position.chars_read_total += (len - 1); - position.chars_read_current_line += (len - 1); + bulk_consume(data + 1, len - 1); + read_number_terminator(std::integral_constant {}); return integer_result; } // The value does not fit an integer, so this token converts as a @@ -1578,19 +1629,108 @@ scan_number_done: } #endif + decimal_point_position = dot_index; + + bulk_consume(data + 1, len - 1); + read_number_terminator(std::integral_constant {}); + + // a float whose digits all fit into w converts from the accumulated + // value; longer ones (and other float formats) take the general path + if (number_type == token_type::value_float && digit_count <= 19) + { + float_significand s; + s.w = w; + s.exponent = exponent - static_cast(fraction_digits); + s.negative = data[0] == '-'; + if (convert_accumulated_float(s, std::integral_constant::value> {})) + { + // the token text is only copied into token_buffer when + // get_string() asks for it; the library's own SAX handlers + // ignore it. A window into a stream may be refilled by the + // next read, so there the text is copied right away. + if (bulk_window) + { + token_buffer.append(reinterpret_cast(data), len); + } + else + { + pending_number_text = data; + pending_number_length = len; + } + return token_type::value_float; + } + } + // materialize the token exactly as scan_number() would. reset() already // cleared token_buffer, so append() fills it (assign() is avoided // because custom string_t types need not provide it) token_buffer.append(reinterpret_cast(data), len); - decimal_point_position = dot_index; - - ia.bulk_skip(len - 1); - position.chars_read_total += (len - 1); - position.chars_read_current_line += (len - 1); - return convert_number(number_type, mantissa_end); } + /// window adapter: a number that reaches the end of the window may continue + /// after it, so scan_number() has to read it character by character + static constexpr bool number_may_continue(bool at_end, std::true_type /*window*/) noexcept + { + return at_end; + } + + /// the end of the block is the end of the input, which ends the number + static constexpr bool number_may_continue(bool /*at_end*/, std::false_type /*window*/) noexcept + { + return false; + } + + /// window adapter: read the character after a bulk-scanned number and unget + /// it, as scan_number() does. The adapter is a stream that is handed back to + /// the user, who must find the same input consumed either way. + void read_number_terminator(std::true_type /*window*/) + { + get(); + unget(); + } + + /// the input is not handed back, so the terminator can stay unread + void read_number_terminator(std::false_type /*window*/) const noexcept {} + + /// binary32/binary64: the value of a float token from its accumulated + /// significand (see scan_number_bulk_contiguous()) + bool convert_accumulated_float(const float_significand& s, std::true_type /*native*/) noexcept + { + // accept() only needs to know that the value is finite (see the parser): + // 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 + // may overflow has to be converted to find out. + if (JSON_HEDLEY_UNLIKELY(discard_number_values && s.exponent + 19 <= std::numeric_limits::max_exponent10)) + { + value_float = 0; + return true; + } + value_float = decimal_to_float(s); + return true; + } + + /// other formats (e.g. x87 long double): not instantiated for them, as + /// decimal_to_float() only supports binary32/binary64 + static bool convert_accumulated_float(const float_significand& /*s*/, std::false_type /*native*/) noexcept + { + return false; + } + + /// the value of an integer token with at most 18 digits, accumulated in @a w + token_type store_integer(token_type number_type, std::uint64_t w) noexcept + { + if (number_type == token_type::value_unsigned) + { + value_unsigned = static_cast(w); + // reject values that do not round-trip into a narrower type + return (static_cast(value_unsigned) == w) ? token_type::value_unsigned : token_type::uninitialized; + } + const std::int64_t x = -static_cast(w); + value_integer = static_cast(x); + return (static_cast(value_integer) == x) ? token_type::value_integer : token_type::uninitialized; + } + /// contiguous input: try the number fast path, else the byte-path scanner token_type scan_number_dispatch(std::true_type /*bulk*/) { @@ -1605,18 +1745,23 @@ scan_number_done: } /*! - @param[in] literal_text the literal text to expect - @param[in] length the length of the passed literal text - @param[in] return_type the token type to return on success + @param[in] rest the literal text after its first character, which + is current (its length is a template parameter, so + that the comparison of the contiguous fast path is + a fixed-size load, not a call of memcmp) + @param[in] return_type the token type to return on success */ - JSON_HEDLEY_NON_NULL(2) - token_type scan_literal(const char_type* literal_text, const std::size_t length, - token_type return_type) + template + JSON_HEDLEY_NEVER_INLINE token_type scan_literal(const char (&rest)[N], token_type return_type) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) { - JSON_ASSERT(char_traits::to_char_type(current) == literal_text[0]); - for (std::size_t i = 1; i < length; ++i) + constexpr std::size_t length = N - 1; + if (scan_literal_bulk(rest, length, std::integral_constant {})) { - if (JSON_HEDLEY_UNLIKELY(char_traits::to_char_type(get()) != literal_text[i])) + return return_type; + } + for (std::size_t i = 0; i < length; ++i) + { + if (JSON_HEDLEY_UNLIKELY(char_traits::to_char_type(get()) != static_cast(rest[i]))) { error_message = "invalid literal"; return token_type::parse_error; @@ -1625,6 +1770,27 @@ scan_number_done: return return_type; } + /// contiguous input: compare the rest of a literal at once; false (and + /// nothing consumed) if it does not match, so that the byte path reports + /// 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*/) + { + if (next_unget || ia.bulk_remaining() < length + || std::memcmp(ia.bulk_data(), rest, length) != 0) + { + return false; + } + bulk_consume(reinterpret_cast(ia.bulk_data()), length); + current = static_cast(static_cast(rest[length - 1])); + return true; + } + + /// streaming input: no bulk fast path + bool scan_literal_bulk(const char* /*rest*/, const std::size_t /*length*/, std::false_type /*bulk*/) const noexcept + { + return false; + } + ///////////////////// // input management ///////////////////// @@ -1633,11 +1799,12 @@ scan_number_done: void reset() noexcept { token_buffer.clear(); + pending_number_text = nullptr; decimal_point_position = std::string::npos; #if JSON_DIAGNOSTIC_POSITIONS // the first character of the token has already been read, hence the -1 - token_start_position = position.chars_read_total - 1; + token_start_position = get_chars_read_total() - 1; #endif note_token_start(std::integral_constant {}); @@ -1646,11 +1813,30 @@ scan_number_done: /// seekable adapter: remember where the current token starts so it can be /// reconstructed from the input on error; current has already been /// consumed, hence the -1 - void note_token_start(std::true_type /*lazy*/) noexcept + void note_token_start(std::true_type /*lazy*/) + { + note_token_start_lazy(std::integral_constant {}); + } + + void note_token_start_lazy(std::false_type /*adapter_token*/) noexcept { token_string_start = ia.get_consumed_count() - 1; } + /// token-tracking adapter: start tracking the token at the first character + void begin_token(std::true_type /*adapter_token*/) noexcept + { + ia.begin_token(); + } + + void begin_token(std::false_type /*adapter_token*/) const noexcept {} + + /// token-tracking adapter: the token starts with the current character + void note_token_start_lazy(std::true_type /*adapter_token*/) + { + ia.mark_token_start(current); + } + /// streaming adapter: start copying the token eagerly, beginning with the /// already-read first character void note_token_start(std::false_type /*lazy*/) noexcept @@ -1690,6 +1876,15 @@ scan_number_done: /// per-character position counters (line-count-on-'\n' bookkeeping is /// handled afterwards, in track_after_read(), once `current` is known) void advance_position() noexcept + { + advance_position_impl(std::integral_constant {}); + } + + /// seekable adapter: the position is derived from the adapter on demand + void advance_position_impl(std::true_type /*lazy*/) const noexcept {} + + /// streaming adapter: count the character + void advance_position_impl(std::false_type /*lazy*/) noexcept { ++position.chars_read_total; ++position.chars_read_current_line; @@ -1703,7 +1898,16 @@ scan_number_done: // seekable adapters reconstruct the token lazily on error (see // get_token_string), so the eager per-character copy is skipped capture_char(std::integral_constant {}); + track_newline(std::integral_constant {}); + return current; + } + /// seekable adapter: lines are counted on demand in get_position() + void track_newline(std::true_type /*lazy*/) const noexcept {} + + /// streaming adapter: start a new line after reading a newline + void track_newline(std::false_type /*lazy*/) noexcept + { if (current == '\n') { ++position.lines_read; @@ -1712,8 +1916,6 @@ scan_number_done: chars_read_before_newline = position.chars_read_current_line; position.chars_read_current_line = 0; } - - return current; } /*! @@ -1757,12 +1959,22 @@ scan_number_done: We implement unget by setting variable next_unget to true. The input is not changed - we just simulate ungetting by modifying chars_read_total, chars_read_current_line, and token_string. The next call to get() will - behave as if the unget character is read again. + behave as if the unget character is read again. With lazy_position, the + pending next_unget is all get_chars_read_total() needs. */ void unget() { next_unget = true; + unget_position(std::integral_constant {}); + uncapture_char(std::integral_constant {}); + } + /// seekable adapter: get_chars_read_total() accounts for next_unget + void unget_position(std::true_type /*lazy*/) const noexcept {} + + /// streaming adapter: step the counters back over the ungotten character + void unget_position(std::false_type /*lazy*/) noexcept + { --position.chars_read_total; // in case we "unget" a newline, we have to also decrement the lines_read @@ -1785,8 +1997,6 @@ scan_number_done: { --position.chars_read_current_line; } - - uncapture_char(std::integral_constant {}); } /// adapter without lookahead: nothing to do (see release_lookahead) @@ -1849,18 +2059,49 @@ scan_number_done: /// return current string value string_t& get_string() { + if (pending_number_text != nullptr) + { + token_buffer.append(reinterpret_cast(pending_number_text), pending_number_length); + pending_number_text = nullptr; + } // a number token holds '.' regardless of the locale (#4084) return token_buffer; } + /// the value of a string token (key or string), which never has pending + /// number text, so get_string()'s check is not needed + string_t& get_string_token() noexcept + { + JSON_ASSERT(pending_number_text == nullptr); + return token_buffer; + } + + /// the text of a float token for a SAX handler that ignores it (the + /// library's own handlers): empty, without copying the token + const string_t& get_ignored_number_text() const noexcept + { + return token_buffer; + } + ///////////////////// // diagnostics ///////////////////// - /// return position of last read token - constexpr position_t get_position() const noexcept + /*! + @brief return the number of characters read so far + + Unlike get_position(), this is O(1) for all adapters, so it may be called + for every value (JSON_DIAGNOSTIC_POSITIONS). + */ + std::size_t get_chars_read_total() const noexcept { - return position; + return get_chars_read_total_impl(std::integral_constant {}); + } + + /// return position of last read token + position_t get_position() const + { + return get_position_impl(std::integral_constant {}); } /*! @@ -1897,8 +2138,59 @@ scan_number_done: } #endif + private: + /// seekable adapter: every get() either read from the adapter (including + /// reads at EOF, which get() counts as characters as well) or replayed an + /// ungotten character, so the count is the adapter's read count minus a + /// still pending unget + std::size_t get_chars_read_total_impl(std::true_type /*lazy*/) const noexcept + { + return ia.get_read_count() - (next_unget ? 1u : 0u); + } + + /// streaming adapter: the counter is maintained by get() and unget() + std::size_t get_chars_read_total_impl(std::false_type /*lazy*/) const noexcept + { + return position.chars_read_total; + } + + /// seekable adapter: count the lines in the consumed input; O(input read + /// so far), but only called on errors + position_t get_position_impl(std::true_type /*lazy*/) const + { + position_t result; + result.chars_read_total = get_chars_read_total(); + // reads at EOF count as characters but are not part of the input + const std::size_t consumed = (std::min)(result.chars_read_total, ia.get_consumed_count()); + std::size_t line_start = 0; + result.lines_read = ia.count_consumed_lines(consumed, line_start); + result.chars_read_current_line = result.chars_read_total - line_start; + return result; + } + + /// streaming adapter: the position is maintained by get() and unget() + position_t get_position_impl(std::false_type /*lazy*/) const noexcept + { + return position; + } + + public: /// seekable adapter: rebuild the last read token from the input on demand const std::vector& collect_token_chars(std::vector& out, std::true_type /*lazy*/) const + { + return collect_token_chars_lazy(out, std::integral_constant {}); + } + + /// token-tracking adapter: the adapter hands out everything read since + /// mark_token_start(), except a pending unget (see below) + const std::vector& collect_token_chars_lazy(std::vector& out, std::true_type /*adapter_token*/) const + { + const bool pending_real_unget = next_unget && current != char_traits::eof(); + ia.copy_token(out, pending_real_unget); + return out; + } + + const std::vector& collect_token_chars_lazy(std::vector& out, std::false_type /*adapter_token*/) const { // a pending unget of a real (non-EOF) character means that character // was consumed from the input but is not part of the token; EOF is @@ -2004,15 +2296,56 @@ scan_number_done: // into one that reloads the pointer from memory every character do { + if (current == '\n') + { + skip_indentation(std::integral_constant {}); + } get_ignoring_pending_unget(); } while (current_is_whitespace()); } + /*! + @brief contiguous input: skip the space indentation that follows a newline + 8 bytes at a time + + Pretty-printed JSON indents every line with a run of spaces, which the loop + in skip_whitespace() would otherwise read one character at a time. The + spaces are skipped 8 bytes at a time; the last 7 bytes of the input and any + other whitespace are left to that loop. Skipping bypasses get(); + bulk_consume() does its bookkeeping. + */ + void skip_indentation(std::true_type /*bulk*/) + { + JSON_ASSERT(!next_unget); + const std::size_t remaining = ia.bulk_remaining(); + if (remaining < 8) + { + return; + } + const char* const data = reinterpret_cast(ia.bulk_data()); + std::size_t pos = 0; + for (; pos + 8 <= remaining; pos += 8) + { + // little-endian, so the first byte is the lowest + const std::uint64_t non_space = read_eight_bytes(data + pos) ^ 0x2020202020202020ull; + if (non_space != 0) + { + // skip the spaces before the first other byte of this word + pos += static_cast(count_trailing_zeros(non_space)) / 8; + break; + } + } + bulk_consume(data, pos); + } + + /// streaming input: no bulk fast path + void skip_indentation(std::false_type /*bulk*/) const noexcept {} + token_type scan() { // initially, skip the BOM - if (position.chars_read_total == 0 && !skip_bom()) + if (get_chars_read_total() == 0 && !skip_bom()) { error_message = "invalid BOM; must be 0xEF 0xBB 0xBF if given"; return token_type::parse_error; @@ -2039,7 +2372,7 @@ scan_number_done: token_type scan_expecting(token_type expected_type) { JSON_ASSERT(expected_type == token_type::name_separator || expected_type == token_type::value_separator); - JSON_ASSERT(position.chars_read_total > 0); + JSON_ASSERT(get_chars_read_total() > 0); const char_int_type expected_char = static_cast((expected_type == token_type::name_separator) ? ':' : ','); skip_whitespace(); if (JSON_HEDLEY_LIKELY(current == expected_char)) @@ -2083,21 +2416,14 @@ scan_number_done: return token_type::value_separator; // literals + // (the text after the first character; no local arrays, which + // would give this function a stack canary on every token) case 't': - { - std::array true_literal = {{static_cast('t'), static_cast('r'), static_cast('u'), static_cast('e')}}; - return scan_literal(true_literal.data(), true_literal.size(), token_type::literal_true); - } + return scan_literal("rue", token_type::literal_true); case 'f': - { - std::array false_literal = {{static_cast('f'), static_cast('a'), static_cast('l'), static_cast('s'), static_cast('e')}}; - return scan_literal(false_literal.data(), false_literal.size(), token_type::literal_false); - } + return scan_literal("alse", token_type::literal_false); case 'n': - { - std::array null_literal = {{static_cast('n'), static_cast('u'), static_cast('l'), static_cast('l')}}; - return scan_literal(null_literal.data(), null_literal.size(), token_type::literal_null); - } + return scan_literal("ull", token_type::literal_null); // string case '\"': @@ -2145,6 +2471,11 @@ scan_number_done: /// whether the next get() call should just return current bool next_unget = false; + /// the text of the current float token in the input, not yet copied into + /// token_buffer (see get_string()), or nullptr + const char* pending_number_text = nullptr; + std::size_t pending_number_length = 0; + /// the start position of the current token position_t position {}; diff --git a/include/nlohmann/detail/input/number_parse.hpp b/include/nlohmann/detail/input/number_parse.hpp index 8971f7193..dce0105de 100644 --- a/include/nlohmann/detail/input/number_parse.hpp +++ b/include/nlohmann/detail/input/number_parse.hpp @@ -9,15 +9,17 @@ #pragma once +#include // any_of #include // array #include // FLT_EVAL_METHOD #include // localeconv #include // size_t #include // int64_t, uint64_t #include // strtof, strtod, strtold -#include // memcpy #include // numeric_limits #include // string +#include // conditional, integral_constant, is_signed, true_type, false_type +#include // move #include #include @@ -35,15 +37,32 @@ #endif // This file contains the value-conversion helpers used by the lexer to turn an -// already-validated number token into a value, without the locale/errno -// overhead of std::strtoull/std::strtod where possible. They are free functions -// so the lexer stays focused on scanning (see lexer::convert_number()) and so -// that other parsers of JSON text can convert tokens exactly like it does. +// already-validated number token into a value. Integers and binary32/binary64 +// floats (float, double, and long double where it is binary64) are converted +// by the library itself, without the locale/errno overhead of +// std::strtoull/std::strtod and correctly rounded; other long double formats +// use std::from_chars or std::strtold. They are free functions so the lexer +// stays focused on scanning (see lexer::convert_number()) and so that other +// parsers of JSON text can convert tokens exactly like it does. NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail { +// whether an integer of a signed type is negative; the overload for other types +// avoids comparing an unsigned value with zero +template +constexpr bool is_negative_integer(NumberType x, std::true_type /*is_signed*/) noexcept +{ + return x < 0; +} + +template +constexpr bool is_negative_integer(NumberType /*unused*/, std::false_type /*is_signed*/) noexcept +{ + return false; +} + /*! @brief fast integer parser for an already-validated unsigned integer @@ -77,8 +96,10 @@ bool parse_integer_unsigned(const char* first, const char* last, NumberUnsignedT x = (x * 10u) + digit; } value = static_cast(x); - // reject values that do not round-trip into a narrower NumberUnsignedType - return static_cast(value) == x; + // reject values that do not round-trip into a narrower NumberUnsignedType, + // and values that wrap around to negative numbers if it is a signed type + // (number_unsigned_t may be the same type as number_integer_t, #2573) + return static_cast(value) == x && !is_negative_integer(value, std::is_signed()); } /*! @@ -115,233 +136,337 @@ bool parse_integer_signed(const char* first, const char* last, NumberIntegerType } /*! -@brief exact fast path for parsing a `double` (Clinger's algorithm) - -For the common case - at most 19 significant digits, a decimal exponent in -[-22, 22], and a significand below 2^53 - the value equals significand * -10^exp computed in IEEE-754 double arithmetic, which is exact under -round-to-nearest because both operands are exactly representable. This is the -same fast path used by fast_float/simdjson; the general cases are left to -std::strtod. The parser only activates for number_float_t == double; float and -long double keep the std::strtof/std::strtold paths (see the templated overload -below). - -@param[in] first pointer to the first character of the number -@param[in] last pointer past the last character -@param[out] out the parsed value on success -@return true if the value was parsed exactly; false to fall back to strtod +@brief parameters of the IEEE-754 binary32 and binary64 formats for the float + conversion (after fast_float's binary_format) */ -inline bool parse_float_fast(const char* first, const char* last, double& out) noexcept +template +struct ieee_binary_format; + +template<> +struct ieee_binary_format<24> // binary32 { -#if defined(FLT_EVAL_METHOD) && FLT_EVAL_METHOD != 0 - // Clinger's fast path is only exact when double operations are evaluated in - // true double precision. On platforms that keep intermediates in extended - // precision (e.g. the x87 FPU on 32-bit x86, where FLT_EVAL_METHOD == 2) the - // single significand * 10^scale step is double-rounded and can be 1 ULP off, - // so decline and let the caller fall back to the correctly-rounded - // std::from_chars / std::strtod path. - static_cast(first); - static_cast(last); - static_cast(out); - return false; -#else - static const std::array powers_of_ten = + static constexpr int mantissa_bits() noexcept { - { - 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, - 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22 - } - }; + return 23; + } + static constexpr int sign_bit() noexcept + { + return 31; + } + static constexpr int minimum_exponent() noexcept + { + return -127; + } + static constexpr int infinite_power() noexcept + { + return 0xFF; + } + // w * 10^q with w < 2^64 is below half the smallest subnormal number for + // q < smallest_power_of_ten() and at least infinity for q > largest_power_of_ten() + static constexpr int smallest_power_of_ten() noexcept + { + return -64; + } + static constexpr int largest_power_of_ten() noexcept + { + return 38; + } + // w * 10^q can only be exactly between two numbers for q in this range + static constexpr int min_exponent_round_to_even() noexcept + { + return -17; + } + static constexpr int max_exponent_round_to_even() noexcept + { + return 10; + } + // Clinger's fast path: w and 10^|q| are exact + static constexpr int max_exponent_fast_path() noexcept + { + return 10; + } + static constexpr std::uint64_t max_mantissa_fast_path() noexcept + { + return std::uint64_t{2} << 23u; + } + // a midpoint between two numbers has at most this many significant digits + static constexpr std::int64_t max_digits() noexcept + { + return 114; + } +}; - const char* p = first; - bool negative = false; - if (p != last && (*p == '-' || *p == '+')) - { - negative = (*p == '-'); - ++p; - } - - std::uint64_t significand = 0; - int num_digits = 0; - int fractional_digits = 0; - bool seen_dot = false; - bool any_digit = false; - for (; p != last; ++p) - { - const char c = *p; - if (c >= '0' && c <= '9') - { - any_digit = true; - if (JSON_HEDLEY_UNLIKELY(num_digits >= 19)) - { - return false; // significand may not fit into uint64_t - } - significand = (significand * 10u) + static_cast(c - '0'); - ++num_digits; - fractional_digits += static_cast(seen_dot); - } - else if (c == '.') - { - if (JSON_HEDLEY_UNLIKELY(seen_dot)) - { - return false; - } - seen_dot = true; - } - else if (c == 'e' || c == 'E') - { - ++p; - break; - } - else - { - return false; - } - } - if (JSON_HEDLEY_UNLIKELY(!any_digit)) - { - return false; - } - - int exponent = 0; - if (p != last) // an exponent part remains - { - bool exp_negative = false; - if (p != last && (*p == '-' || *p == '+')) - { - exp_negative = (*p == '-'); - ++p; - } - bool any_exp_digit = false; - for (; p != last; ++p) - { - if (JSON_HEDLEY_UNLIKELY(*p < '0' || *p > '9')) - { - return false; - } - exponent = (exponent * 10) + (*p - '0'); - any_exp_digit = true; - if (JSON_HEDLEY_UNLIKELY(exponent > 9999)) - { - return false; - } - } - if (JSON_HEDLEY_UNLIKELY(!any_exp_digit)) - { - return false; - } - if (exp_negative) - { - exponent = -exponent; - } - } - - const int scale = exponent - fractional_digits; - if (JSON_HEDLEY_UNLIKELY(significand >= (static_cast(1) << 53))) - { - return false; // significand not exactly representable as double - } - - auto result = static_cast(significand); - if (scale >= 0) - { - if (JSON_HEDLEY_UNLIKELY(scale > 22)) - { - return false; - } - result *= powers_of_ten[static_cast(scale)]; - } - else - { - if (JSON_HEDLEY_UNLIKELY(-scale > 22)) - { - return false; - } - result /= powers_of_ten[static_cast(-scale)]; - } - out = negative ? -result : result; - return true; -#endif -} - -/// fast float path is only exact for `double`; decline for float/long double -template -bool parse_float_fast(const char* /*first*/, const char* /*last*/, FloatType& /*out*/) noexcept +template<> +struct ieee_binary_format<53> // binary64 { - return false; -} + static constexpr int mantissa_bits() noexcept + { + return 52; + } + static constexpr int sign_bit() noexcept + { + return 63; + } + static constexpr int minimum_exponent() noexcept + { + return -1023; + } + static constexpr int infinite_power() noexcept + { + return 0x7FF; + } + static constexpr int smallest_power_of_ten() noexcept + { + return -342; + } + static constexpr int largest_power_of_ten() noexcept + { + return 308; + } + static constexpr int min_exponent_round_to_even() noexcept + { + return -4; + } + static constexpr int max_exponent_round_to_even() noexcept + { + return 23; + } + static constexpr int max_exponent_fast_path() noexcept + { + return 22; + } + static constexpr std::uint64_t max_mantissa_fast_path() noexcept + { + return std::uint64_t{2} << 52u; + } + static constexpr std::int64_t max_digits() noexcept + { + return 769; + } +}; /*! -@brief parse a float with std::from_chars (Eisel-Lemire) when available +@brief whether @a FloatType is IEEE-754 binary32 or binary64 -std::from_chars is locale-independent, correctly rounded, and - via the -Eisel-Lemire algorithm in modern standard libraries - much faster than strtod -over the whole value range (not just the Clinger subset). It is used only when -__cpp_lib_to_chars indicates full floating-point support and only when it -consumes the entire token ([first, last)). An under-/overflow (result_out_of_range) also declines, so -the caller's strtod fallback supplies the well-defined ±inf/0 result the parser -expects (side-stepping the P4168 divergence between implementations). - -@return true if the value was parsed exactly and fully; false to fall back +These formats (float, double, and long double where it is binary64, e.g. +with MSVC or on Apple arm64) are converted by parse_float_native(). The +predicate is the one the serializer uses to choose Grisu2. */ template -bool parse_float_from_chars(const char* first, const char* last, FloatType& out) noexcept +struct has_native_float_format { - // JSON_HAS_CPP_17 must gate the use as well as the include above: - // some standard libraries (e.g. libstdc++ 15) define __cpp_lib_to_chars even - // in C++14 mode, where is not included. -#if defined(JSON_HAS_CPP_17) && defined(__cpp_lib_to_chars) - const auto result = std::from_chars(first, last, out); - return result.ec == std::errc() && result.ptr == last; -#else - static_cast(first); - static_cast(last); - static_cast(out); - return false; -#endif -} + static constexpr bool value = + (std::numeric_limits::is_iec559 && std::numeric_limits::digits == 24 && std::numeric_limits::max_exponent == 128) || + (std::numeric_limits::is_iec559 && std::numeric_limits::digits == 53 && std::numeric_limits::max_exponent == 1024); +}; -/// whether the eight bytes of @a v (see read_eight_bytes()) are ASCII digits -/// (after fast_float's is_made_of_eight_digits_fast) -inline bool is_eight_digits(std::uint64_t v) noexcept -{ - return ((v & 0xF0F0F0F0F0F0F0F0u) | (((v + 0x0606060606060606u) & 0xF0F0F0F0F0F0F0F0u) >> 4u)) == 0x3333333333333333u; -} +/// the C++ type (float or double) that holds a binary32 or binary64 @a FloatType +template +using native_float_t = typename std::conditional::digits == 24, float, double>::type; /// the value of the eight ASCII digits in @a v (see read_eight_bytes()), three -/// multiplications instead of eight (after simdjson and fast_float) -inline std::uint32_t parse_eight_digits(std::uint64_t v) noexcept +/// multiplications instead of eight (after simdjson and fast_float); always +/// inlined, as GCC otherwise calls it in the number loops +JSON_HEDLEY_ALWAYS_INLINE std::uint32_t parse_eight_digits(std::uint64_t v) noexcept { v = ((v & 0x0F0F0F0F0F0F0F0Fu) * 2561u) >> 8u; v = ((v & 0x00FF00FF00FF00FFu) * 6553601u) >> 16u; return static_cast(((v & 0x0000FFFF0000FFFFu) * 42949672960001u) >> 32u); } +/// whether the eight bytes in @a v (see read_eight_bytes()) are all ASCII +/// digits (after fast_float) +JSON_HEDLEY_ALWAYS_INLINE bool is_eight_digits(std::uint64_t v) noexcept +{ + return ((v & 0xF0F0F0F0F0F0F0F0u) | (((v + 0x0606060606060606u) & 0xF0F0F0F0F0F0F0F0u) >> 4u)) == 0x3333333333333333u; +} + /*! -@brief the double nearest to w * 10^q (Eisel-Lemire) +@brief skip the ASCII digits starting at data[i] and accumulate their value + +Eight digits are taken at a time while at least eight bytes remain. @a w wraps +around after 19 digits; the caller only uses it for shorter digit runs. + +@return the index of the first byte at or after @a i that is not a digit +*/ +JSON_HEDLEY_ALWAYS_INLINE std::size_t accumulate_digits(const char* data, std::size_t i, std::size_t avail, std::uint64_t& w) noexcept +{ + while (avail - i >= 8) + { + const std::uint64_t v = read_eight_bytes(data + i); + if (!is_eight_digits(v)) + { + break; + } + w = (w * 100000000u) + parse_eight_digits(v); + i += 8; + } + while (i < avail && data[i] >= '0' && data[i] <= '9') + { + w = (w * 10u) + static_cast(data[i] - '0'); + ++i; + } + return i; +} + +/// whether [first, last) contains a digit other than '0'; a plain loop, as +/// GCC calls std::any_of out of line even for the usual empty range +JSON_HEDLEY_ALWAYS_INLINE bool has_nonzero_digit(const char* first, const char* last) noexcept +{ + for (; first != last; ++first) + { + if (*first != '0') + { + return true; + } + } + return false; +} + +/*! +@brief accumulate the validated digits [p, last) into @a w, as long as + @a remaining digits still fit into it + +Eight digits are taken at a time while at least eight remain, and @a remaining +is decreased by the number of digits taken. + +@return a pointer past the last digit taken +*/ +JSON_HEDLEY_ALWAYS_INLINE const char* accumulate_significant_digits(const char* p, const char* last, int& remaining, std::uint64_t& w) noexcept +{ + while (remaining >= 8 && last - p >= 8) + { + w = (w * 100000000u) + parse_eight_digits(read_eight_bytes(p)); + p += 8; + remaining -= 8; + } + for (; remaining > 0 && p != last; ++p, --remaining) + { + w = (w * 10u) + static_cast(*p - '0'); + } + return p; +} + +/// the value of the validated exponent digits [+-]?[0-9]+ in [first, last), +/// saturated far beyond every range +inline std::int64_t parse_float_exponent(const char* first, const char* last) noexcept +{ + const bool negative = *first == '-'; + first += (*first == '-' || *first == '+') ? 1 : 0; + constexpr std::int64_t saturation = 100000000000000000; // 10^17 + std::int64_t value = 0; + for (; first != last; ++first) + { + if (value < saturation) + { + value = (value * 10) + (*first - '0'); + } + } + return negative ? -value : value; +} + +/// a float token as w * 10^exponent, see parse_float_significand() +struct float_significand +{ + std::uint64_t w = 0; ///< the first (at most 19) significant digits + std::int64_t exponent = 0; ///< the decimal exponent of the last digit in w + bool negative = false; ///< whether the token starts with '-' + bool truncated = false; ///< whether nonzero digits follow the ones in w +}; + +/*! +@brief split a validated number token into sign, significand, and exponent + +The lexer has validated the token against the JSON grammar and knows where its +parts are, so this needs no character classification: the integer part ends at +@a decimal_point_position (or @a mantissa_end), the fraction at @a mantissa_end, +and an exponent follows. At most 19 significant digits are kept; the value then +lies in [w, w + 1) * 10^exponent, and is exactly w * 10^exponent unless +truncated is set. + +@param[in] first pointer to the first character of the token +@param[in] last pointer past the last character +@param[in] decimal_point_position index of the '.' in the token, or + std::string::npos if there is none +@param[in] mantissa_end index of the 'e'/'E', or the token length +*/ +JSON_HEDLEY_ALWAYS_INLINE float_significand parse_float_significand(const char* first, const char* last, + std::size_t decimal_point_position, std::size_t mantissa_end) noexcept +{ + float_significand s; + const char* p = first; + s.negative = *p == '-'; + p += s.negative ? 1 : 0; + const bool has_dot = decimal_point_position != std::string::npos; + const char* const mantissa_last = first + mantissa_end; + const char* const integer_last = has_dot ? first + decimal_point_position : mantissa_last; + + std::uint64_t w = 0; + int remaining = 19; // digits that still fit into w + if (*p != '0') // the integer part is "0" or [1-9][0-9]* + { + p = accumulate_significant_digits(p, integer_last, remaining, w); + s.exponent = integer_last - p; + s.truncated = has_nonzero_digit(p, integer_last); + } + + if (has_dot) + { + p = integer_last + 1; + if (w == 0) + { + // zeros after the decimal point of "0." are not significant + const char* const zeros = p; + while (p != mantissa_last && *p == '0') + { + ++p; + } + s.exponent -= p - zeros; + } + const char* const digits = p; + p = accumulate_significant_digits(p, mantissa_last, remaining, w); + s.exponent -= p - digits; + s.truncated = s.truncated || has_nonzero_digit(p, mantissa_last); + } + + if (mantissa_last != last) + { + s.exponent += parse_float_exponent(mantissa_last + 1, last); + } + s.w = w; + return s; +} + +/*! +@brief the bits of the float nearest to w * 10^q (Eisel-Lemire) The algorithm of Daniel Lemire, "Number Parsing at a Gigabyte per Second" (Software: Practice and Experience, 2021), after fast_float's compute_float (used under the MIT license). With a 128-bit approximation of 5^q, the product -is always sufficient to round correctly for w with at most 19 digits (Noble -Mushtak and Daniel Lemire, "Fast number parsing without fallback", Software: -Practice and Experience, 2023). Only integer arithmetic is used, so the result -does not depend on the floating-point environment. +is always sufficient to round correctly for w < 2^64 (Noble Mushtak and Daniel +Lemire, "Fast number parsing without fallback", Software: Practice and +Experience, 2023). Only integer arithmetic is used, so the result does not +depend on the floating-point environment. +It is always inlined, like decimal_to_float(), so that hot loops of callers +keep the whole conversion inline. + +@tparam Format ieee_binary_format<24> (binary32) or ieee_binary_format<53> (binary64) @param[in] q decimal exponent -@param[in] w significand, w != 0 +@param[in] w significand @return the IEEE-754 bits of the positive result (0 for underflow, infinity for overflow) */ -inline std::uint64_t eisel_lemire(std::int64_t q, std::uint64_t w) noexcept +template +JSON_HEDLEY_ALWAYS_INLINE std::uint64_t eisel_lemire(std::int64_t q, std::uint64_t w) noexcept { - constexpr int mantissa_bits = 52; - constexpr std::uint64_t infinity = std::uint64_t{0x7FF} << mantissa_bits; - if (q < pow5_128_smallest_power) + constexpr int mantissa_bits = Format::mantissa_bits(); + constexpr std::uint64_t infinity = static_cast(Format::infinite_power()) << mantissa_bits; + if (w == 0 || q < Format::smallest_power_of_ten()) { return 0; } - if (q > pow5_128_largest_power) + if (q > Format::largest_power_of_ten()) { return infinity; } @@ -365,8 +490,8 @@ inline std::uint64_t eisel_lemire(std::int64_t q, std::uint64_t w) noexcept const auto upperbit = static_cast(product.high >> 63u); const int shift = upperbit + 64 - mantissa_bits - 3; std::uint64_t mantissa = product.high >> static_cast(shift); - // floor(log2(10^q)) + 63 + 1023, with log2(10) ~ 217706 / 2^16 - std::int64_t power2 = (((152170 + 65536) * q) >> 16) + 63 + upperbit - lz + 1023; + // floor(log2(10^q)) + 63 + bias, with log2(10) ~ 217706 / 2^16 + std::int64_t power2 = (((152170 + 65536) * q) >> 16) + 63 + upperbit - lz - Format::minimum_exponent(); if (power2 <= 0) // subnormal { @@ -375,17 +500,18 @@ inline std::uint64_t eisel_lemire(std::int64_t q, std::uint64_t w) noexcept return 0; } mantissa >>= static_cast(-power2 + 1); + // no tie is possible here: that needs a small |q| mantissa += (mantissa & 1u); mantissa >>= 1u; // rounding up may produce the smallest normal number power2 = (mantissa < (std::uint64_t{1} << mantissa_bits)) ? 0 : 1; - return mantissa | (static_cast(power2) << mantissa_bits); + return (mantissa & ((std::uint64_t{1} << mantissa_bits) - 1)) | (static_cast(power2) << mantissa_bits); } - // a value exactly between two doubles rounds to even; this can only + // a value exactly between two floats rounds to even; this can only // happen for small |q|, where 5^q is exact - if (product.low <= 1 && q >= -4 && q <= 23 && (mantissa & 3u) == 1 - && (mantissa << static_cast(shift)) == product.high) + if (product.low <= 1 && q >= Format::min_exponent_round_to_even() && q <= Format::max_exponent_round_to_even() + && (mantissa & 3u) == 1 && (mantissa << static_cast(shift)) == product.high) { mantissa &= ~std::uint64_t{1}; } @@ -397,198 +523,419 @@ inline std::uint64_t eisel_lemire(std::int64_t q, std::uint64_t w) noexcept ++power2; } mantissa &= ~(std::uint64_t{1} << mantissa_bits); - if (power2 >= 0x7FF) + if (power2 >= Format::infinite_power()) { return infinity; } return mantissa | (static_cast(power2) << mantissa_bits); } -/*! -@brief parse a validated float token with the Eisel-Lemire algorithm - -The significand is accumulated eight digits at a time where possible. A token -with more than 19 significant digits is truncated to w; the value then lies -in [w, w + 1) * 10^q, and it is only returned if both ends round to the same -double, which covers all but a few such tokens. - -@param[in] first pointer to the first character of the token -@param[in] last pointer past the last character -@param[out] out the correctly rounded value on success (±infinity if it - overflows, like strtod) -@return true on success; false if strtod must decide -*/ -inline bool parse_float_eisel_lemire(const char* first, const char* last, double& out) noexcept +/// an unsigned integer of up to 4096 bits for digit_comparison() (32-bit limbs, +/// so only 32x32->64-bit multiplications are needed) +class float_bigint { - const char* p = first; - const bool negative = (p != last && *p == '-'); - if (negative) + public: + explicit float_bigint(std::uint64_t value) noexcept { - ++p; + for (; value != 0; value >>= 32u) + { + limbs[count++] = static_cast(value); + } } - std::uint64_t w = 0; - unsigned int digits = 0; // significant digits in w - std::int64_t exponent = 0; - bool truncated = false; - bool in_fraction = false; - for (;;) + /// *this = *this * factor + summand + void multiply_add(std::uint32_t factor, std::uint32_t summand) noexcept { - // eight digits at a time, as long as they fit into w - while (w != 0 && digits <= 19u - 8u && last - p >= 8) + std::uint64_t carry = summand; + for (std::size_t i = 0; i < count; ++i) { - const std::uint64_t v = read_eight_bytes(p); - if (!is_eight_digits(v)) - { - break; - } - w = (w * 100000000u) + parse_eight_digits(v); - digits += 8u; - exponent -= in_fraction ? 8 : 0; - p += 8; + const std::uint64_t product = (static_cast(limbs[i]) * factor) + carry; + limbs[i] = static_cast(product); + carry = product >> 32u; } - if (p == last) + if (carry != 0) { - break; + JSON_ASSERT(count < limbs.size()); + limbs[count++] = static_cast(carry); } - const char c = *p; - if (c >= '0' && c <= '9') + } + + /// *this = *this * 5^n + void multiply_power_of_five(std::int64_t n) noexcept + { + static const std::array powers = { - if (w == 0 && c == '0') - { - // leading zeros are not significant, but scale a fraction - exponent -= in_fraction ? 1 : 0; - } - else if (digits < 19u) - { - w = (w * 10u) + static_cast(c - '0'); - ++digits; - exponent -= in_fraction ? 1 : 0; - } - else - { - // dropped: the value lies between w and w + 1 (in units of - // the last kept digit) unless all dropped digits are zero - truncated = truncated || c != '0'; - exponent += in_fraction ? 0 : 1; - } - ++p; + {1u, 5u, 25u, 125u, 625u, 3125u, 15625u, 78125u, 390625u, 1953125u, 9765625u, 48828125u, 244140625u, 1220703125u} + }; + for (; n >= 13; n -= 13) + { + multiply_add(powers[13], 0); } - else if (c == '.') + multiply_add(powers[static_cast(n)], 0); + } + + /// *this = *this * 2^n + void shift_left(std::int64_t n) noexcept + { + if (count == 0) { - in_fraction = true; - ++p; + return; + } + const auto limb_shift = static_cast(n / 32); + const auto bit_shift = static_cast(n % 32); + JSON_ASSERT(count + limb_shift + 1 <= limbs.size()); + if (bit_shift != 0) + { + std::uint32_t carry = 0; + for (std::size_t i = 0; i < count; ++i) + { + const std::uint32_t limb = limbs[i]; + limbs[i] = (limb << bit_shift) | carry; + carry = limb >> (32u - bit_shift); + } + if (carry != 0) + { + limbs[count++] = carry; + } + } + if (limb_shift != 0) + { + for (std::size_t i = count; i-- > 0;) + { + limbs[i + limb_shift] = limbs[i]; + } + for (std::size_t i = 0; i < limb_shift; ++i) + { + limbs[i] = 0; + } + count += limb_shift; + } + } + + /// -1, 0, or 1 if *this is less than, equal to, or greater than @a other + int compare(const float_bigint& other) const noexcept + { + if (count != other.count) + { + return count < other.count ? -1 : 1; + } + for (std::size_t i = count; i-- > 0;) + { + if (limbs[i] != other.limbs[i]) + { + return limbs[i] < other.limbs[i] ? -1 : 1; + } + } + return 0; + } + + private: + std::array limbs{{}}; + std::size_t count = 0; +}; + +/*! +@brief round a token exactly when eisel_lemire() cannot decide (slow path) + +The value v of the token lies strictly between two adjacent floats, whose +lower one has the bits @a lower, and the result depends on whether v is below, +at, or above the midpoint m between them. Both are compared exactly as big +integers: v = D * 10^s with the significant digits D (at most +Format::max_digits() of them, more than any midpoint has; further nonzero +digits only put v above m) and m = (2 * mantissa + 1) * 2^(e - 1). This is the +digit comparison of fast_float (Daniel Lemire and contributors, used under the +MIT license), simplified by starting from the two candidates. + +@param[in] first pointer to the first character of the token +@param[in] last pointer past the last character +@param[in] lower the bits of the float below v +@return the bits of the correctly rounded result +*/ +template +std::uint64_t digit_comparison(const char* first, const char* last, std::uint64_t lower) noexcept +{ + const char* p = first + ((*first == '-') ? 1 : 0); + + // D, in chunks of up to 9 digits, and s + float_bigint digits(0); + std::int64_t count = 0; + std::int64_t point = 0; // the value is 0.D... * 10^point + bool truncated = false; + std::uint32_t chunk = 0; + int chunk_digits = 0; + static const std::array powers_of_ten = {{1u, 10u, 100u, 1000u, 10000u, 100000u, 1000000u, 10000000u, 100000000u, 1000000000u}}; + const auto append = [&](char c) noexcept + { + if (count < Format::max_digits()) + { + chunk = (chunk * 10u) + static_cast(c - '0'); + ++count; + if (++chunk_digits == 9) + { + digits.multiply_add(powers_of_ten[9], chunk); + chunk = 0; + chunk_digits = 0; + } } else { - break; // 'e' or 'E' + truncated = truncated || c != '0'; + } + }; + bool significant = false; + for (; p != last && *p >= '0' && *p <= '9'; ++p) + { + significant = significant || *p != '0'; + if (significant) + { + append(*p); + ++point; } } - - if (p != last) + if (p != last && *p == '.') { - ++p; // 'e' or 'E' - bool exp_negative = false; - if (p != last && (*p == '-' || *p == '+')) + for (++p; p != last && *p >= '0' && *p <= '9'; ++p) { - exp_negative = (*p == '-'); - ++p; - } - std::int64_t exp_value = 0; - for (; p != last; ++p) - { - // saturate: any exponent beyond this under- or overflows anyway - if (exp_value < 100000) + significant = significant || *p != '0'; + if (significant) { - exp_value = (exp_value * 10) + (*p - '0'); + append(*p); + } + else + { + --point; } } - exponent += exp_negative ? -exp_value : exp_value; } - - std::uint64_t bits = 0; - if (w != 0) + if (chunk_digits != 0) { - bits = eisel_lemire(exponent, w); - if (truncated && (w + 1 == 0 || eisel_lemire(exponent, w + 1) != bits)) - { - return false; - } + digits.multiply_add(powers_of_ten[static_cast(chunk_digits)], chunk); } - bits |= negative ? (std::uint64_t{1} << 63u) : 0u; - static_assert(sizeof(double) == sizeof(std::uint64_t), "double must have 64 bits"); - std::memcpy(&out, &bits, sizeof(out)); - return true; + if (p != last) + { + point += parse_float_exponent(p + 1, last); + } + const std::int64_t s = point - count; // v = D * 10^s + + // the midpoint above the lower candidate + constexpr int mantissa_bits = Format::mantissa_bits(); + const std::uint64_t exponent_field = lower >> mantissa_bits; + std::uint64_t mantissa = lower & ((std::uint64_t{1} << mantissa_bits) - 1); + std::int64_t e = 1 + Format::minimum_exponent() - mantissa_bits; // of the smallest subnormal number + if (exponent_field != 0) + { + mantissa |= std::uint64_t{1} << mantissa_bits; + e += static_cast(exponent_field) - 1; + } + float_bigint midpoint((2 * mantissa) + 1); + const std::int64_t midpoint_exponent = e - 1; // m = midpoint * 2^midpoint_exponent + + // compare D * 5^s * 2^s with midpoint * 2^midpoint_exponent + if (s >= 0) + { + digits.multiply_power_of_five(s); + } + else + { + midpoint.multiply_power_of_five(-s); + } + const std::int64_t shift = s - midpoint_exponent; + if (shift >= 0) + { + digits.shift_left(shift); + } + else + { + midpoint.shift_left(-shift); + } + const int order = digits.compare(midpoint); + const bool round_up = order > 0 || (order == 0 && (truncated || (mantissa & 1u) != 0)); + return lower + (round_up ? 1u : 0u); } -/// Eisel-Lemire is only implemented for `double` -template -bool parse_float_eisel_lemire(const char* /*first*/, const char* /*last*/, FloatType& /*out*/) noexcept +/// the double with the IEEE-754 bits @a bits +inline void float_from_bits(std::uint64_t bits, double& value) noexcept { - return false; + static_assert(sizeof(double) == sizeof(std::uint64_t), "double must have 64 bits"); + value = bit_cast(bits); +} + +/// the float with the IEEE-754 bits @a bits (the lower 32) +inline void float_from_bits(std::uint64_t bits, float& value) noexcept +{ + static_assert(sizeof(float) == sizeof(std::uint32_t), "float must have 32 bits"); + value = bit_cast(static_cast(bits)); +} + +/// the powers of ten that are exact in binary64 (up to 10^22) +inline double exact_power_of_ten(std::int64_t n, double /*tag*/) noexcept +{ + static const std::array powers = + { + { + 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, + 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22 + } + }; + return powers[static_cast(n)]; +} + +/// the powers of ten that are exact in binary32 (up to 10^10) +inline float exact_power_of_ten(std::int64_t n, float /*tag*/) noexcept +{ + static const std::array powers = + { + {1e0f, 1e1f, 1e2f, 1e3f, 1e4f, 1e5f, 1e6f, 1e7f, 1e8f, 1e9f, 1e10f} + }; + return powers[static_cast(n)]; } /*! -@brief check whether Clinger's fast path can still succeed for a float token +@brief the binary32/binary64 value of a significand that was not truncated -parse_float_fast() needs a significand below 2^53. A mantissa with 17 or -more significant digits is at least 10^16 and therefore always exceeds it, -so calling the fast path would walk the token one extra time only to -decline before strtod has to run anyway. +The result is (-1)^negative * w * 10^exponent, correctly rounded (ties to +even): with Clinger's fast path where w and 10^|exponent| are exact, so that a +single floating-point operation rounds (only where intermediate results are not +kept in extended precision, see FLT_EVAL_METHOD), and with eisel_lemire() +otherwise. A value too large for the type becomes ±infinity, a value too small +±0. -Significant digits are the mantissa's digits from the first nonzero one on; -the sign, the decimal point, leading zeros, and the exponent do not count. -The answer is derived from indices - the digits are not scanned again - so -this stays off the hot path of the number scanners. +This is the core of the conversion that other parsers of JSON text share: they +can split a token themselves and still get the lexer's result. It is always +inlined, so that their hot loops keep the whole conversion inline. -@param[in] token the validated number token ('.' as decimal point) -@param[in] decimal_point_position index of the '.' in @a token, or - std::string::npos if there is none -@param[in] mantissa_end offset just past the last mantissa byte -@return false if parse_float_fast() is guaranteed to decline +@param[in] s the significand, with s.truncated == false */ -inline bool mantissa_fits_clinger(const char* token, std::size_t decimal_point_position, std::size_t mantissa_end) noexcept +template +JSON_HEDLEY_ALWAYS_INLINE FloatType decimal_to_float(const float_significand& s) noexcept { - // 10^16 already exceeds 2^53, so 17 digits can never fit - constexpr std::size_t limit = 17; + using result_type = native_float_t; + using format = ieee_binary_format::digits>; + JSON_ASSERT(!s.truncated); - const std::size_t neg = (token[0] == '-') ? 1u : 0u; - const std::size_t has_dot = (decimal_point_position != std::string::npos) ? 1u : 0u; - // the JSON grammar restricts the integer part to "0" or [1-9][0-9]*, so - // a leading zero can only be a lone "0", which is not significant - const std::size_t lead_zero = (token[neg] == '0') ? 1u : 0u; - JSON_ASSERT(mantissa_end >= neg + has_dot + lead_zero); - std::size_t digits = mantissa_end - neg - has_dot - lead_zero; - - if (JSON_HEDLEY_LIKELY(digits < limit)) +#if !defined(FLT_EVAL_METHOD) || FLT_EVAL_METHOD == 0 + if (s.exponent >= -format::max_exponent_fast_path() && s.exponent <= format::max_exponent_fast_path() + && s.w <= format::max_mantissa_fast_path()) { - return true; - } - - // Only a number below 1 can carry further insignificant zeros, and only - // while the count stays at the limit does removing them change the - // answer - so this loop is skipped for all but a few tokens. The - // fraction is located through decimal_point_position rather than by - // searching '.'. - if (lead_zero != 0) - { - JSON_ASSERT(has_dot != 0); // an integer "0" cannot reach the limit - for (std::size_t i = decimal_point_position + 1; - digits >= limit && i < mantissa_end && token[i] == '0'; ++i) + auto value = static_cast(s.w); + if (s.exponent < 0) { - --digits; + value /= exact_power_of_ten(-s.exponent, result_type{}); } + else + { + value *= exact_power_of_ten(s.exponent, result_type{}); + } + const FloatType result = s.negative ? -value : value; + return result; + } +#endif + + result_type value{}; + float_from_bits(eisel_lemire(s.exponent, s.w) | (s.negative ? (std::uint64_t{1} << format::sign_bit()) : 0u), value); + const FloatType result = value; + return result; +} + +/*! +@brief convert a validated number token to the nearest binary32/binary64 value + +The conversion is correctly rounded (ties to even) and independent of the +locale and of the C and C++ libraries: +1. parse_float_significand() splits the token into w * 10^q. +2. If no digits were dropped, decimal_to_float() rounds w * 10^q (Clinger's + fast path or Eisel-Lemire). +3. Otherwise, the value lies in [w, w + 1) * 10^q: if eisel_lemire() rounds + both ends to the same value, so does the token (in all but rare cases). +4. Otherwise, digit_comparison() compares the token exactly with the midpoint + between the two candidates. +A value too large for the type becomes ±infinity (the parser reports +out_of_range.406), a value too small ±0. + +@param[in] first pointer to the first character of the token +@param[in] last pointer past the last character +@param[in] decimal_point_position index of the '.' in the token, or + std::string::npos if there is none +@param[in] mantissa_end index of the 'e'/'E', or the token length +*/ +template +FloatType parse_float_native(const char* first, const char* last, + std::size_t decimal_point_position, std::size_t mantissa_end) noexcept +{ + using result_type = native_float_t; + using format = ieee_binary_format::digits>; + static_assert(std::numeric_limits::digits == std::numeric_limits::digits, "unexpected float format"); + + const float_significand s = parse_float_significand(first, last, decimal_point_position, mantissa_end); + if (JSON_HEDLEY_LIKELY(!s.truncated)) + { + return decimal_to_float(s); } - return digits < limit; + std::uint64_t bits = eisel_lemire(s.exponent, s.w); + if (JSON_HEDLEY_UNLIKELY(bits != eisel_lemire(s.exponent, s.w + 1))) + { + bits = digit_comparison(first, last, bits); + } + result_type value{}; + float_from_bits(bits | (s.negative ? (std::uint64_t{1} << format::sign_bit()) : 0u), value); + const FloatType result = value; + return result; +} + +/*! +@brief parse a float with std::from_chars when available + +Only used for the formats parse_float_native() does not convert (long double +formats other than binary64). std::from_chars is locale-independent and +correctly rounded. It is used only when __cpp_lib_to_chars indicates full +floating-point support and only when it consumes the entire token ([first, +last)). An under-/overflow (result_out_of_range) also declines, so the +caller's strtold fallback supplies the well-defined ±inf/0 result the parser +expects (side-stepping the P4168 divergence between implementations). + +@return true if the value was parsed exactly and fully; false to fall back +*/ +template +bool parse_float_from_chars(const char* first, const char* last, FloatType& out) noexcept +{ + // JSON_HAS_CPP_17 must gate the use as well as the include above: + // some standard libraries (e.g. libstdc++ 15) define __cpp_lib_to_chars even + // in C++14 mode, where is not included. +#if defined(JSON_HAS_CPP_17) && defined(__cpp_lib_to_chars) + const auto result = std::from_chars(first, last, out); + return result.ec == std::errc() && result.ptr == last; +#else + static_cast(first); + static_cast(last); + static_cast(out); + return false; +#endif +} + +/// binary32 and binary64: the library's own conversion, which always succeeds +template +bool convert_float_fast(const char* first, const char* last, std::size_t decimal_point_position, + std::size_t mantissa_end, FloatType& value, std::true_type /*native*/) noexcept +{ + value = parse_float_native(first, last, decimal_point_position, mantissa_end); + return true; +} + +/// other formats (long double on x87, binary128, double-double): std::from_chars, if available +template +bool convert_float_fast(const char* first, const char* last, std::size_t /*decimal_point_position*/, + std::size_t /*mantissa_end*/, FloatType& value, std::false_type /*native*/) noexcept +{ + return parse_float_from_chars(first, last, value); } /*! @brief convert a validated float token without the C library, if possible -Tries std::from_chars (when available), Clinger's exact fast path (double -only, skipped when it cannot succeed), and the Eisel-Lemire algorithm (double -only). +float, double, and long double where it is binary64 are always converted, by +parse_float_native(). Other long double formats are converted with +std::from_chars where the standard library supports it. @param[in] first pointer to the first character of the token @param[in] last pointer past the last character @@ -604,19 +951,8 @@ template bool convert_float_fast(const char* first, const char* last, std::size_t decimal_point_position, std::size_t mantissa_end, FloatType& value) noexcept { - if (parse_float_from_chars(first, last, value)) - { - return true; - } - // Skipping a fast path that cannot succeed is lossless and saves a full - // extra pass over the token's bytes, which otherwise shows up on - // high-precision inputs such as canada.json - if (mantissa_fits_clinger(first, decimal_point_position, mantissa_end) - && parse_float_fast(first, last, value)) - { - return true; - } - return parse_float_eisel_lemire(first, last, value); + return convert_float_fast(first, last, decimal_point_position, mantissa_end, value, + std::integral_constant::value> {}); } /// std::strtof, std::strtod, or std::strtold, chosen by the type of @a f @@ -640,36 +976,45 @@ inline void strtof_by_type(long double& f, const char* str, char** endptr) noexc f = std::strtold(str, endptr); } -/// return the decimal point of the current locale -inline char get_decimal_point() noexcept +/// return the decimal point of the current locale (it may be longer than one byte) +inline std::string get_decimal_point() { const auto* loc = localeconv(); JSON_ASSERT(loc != nullptr); - return (loc->decimal_point == nullptr) ? '.' : *(loc->decimal_point); + return (loc->decimal_point == nullptr || *loc->decimal_point == '\0') ? "." : loc->decimal_point; } /*! @brief convert a validated float token with strtof/strtod/strtold +Only used for what convert_float_fast() does not convert: long double formats +other than binary64 where std::from_chars is unavailable or reports an under- +or overflow, and floating-point types that are not IEEE-754 (see +has_native_float_format). + These functions expect the decimal point of the *current* locale, so it is looked up right before the conversion instead of once when the lexer is constructed: a locale change in between (by a parser callback, a SAX -handler, or another thread) must not truncate the value (#5198). The -token has been validated before, so if the conversion stops early and the -decimal point changed in the meantime, the locale changed between the +handler, or another thread) must not truncate the value (#5198). A +single-byte decimal point is substituted in place and restored afterwards, +because the token is also handed to the SAX interface. A longer one (e.g., +the two-byte U+066B of ar_EG.UTF-8 or fa_IR.UTF-8) is put into a copy of the +token instead (#5660). + +The token has been validated before, so if the conversion stops early and +the decimal point changed in the meantime, the locale changed between the lookup and the call, and the conversion is repeated with the new decimal -point. If the decimal point did not change, a retry cannot succeed: the -locale's decimal point is not a single character (e.g., the two-byte -U+066B of ar_EG.UTF-8 or fa_IR.UTF-8) and cannot be substituted in place. -The value strtod parsed up to that point is kept, as before this change. +point. If it did not change, the value strtod parsed up to that point is +kept. Note that changing the locale in another thread *while* strtod runs is undefined behavior of the C library, which this function cannot prevent. -@param[in,out] token the token with '.' as decimal point; its - decimal point is replaced during the - conversion and restored afterwards - (data() must be NUL-terminated) +@param[in,out] token the token with '.' as decimal point; a + single-byte decimal point is put in + place during the conversion and + restored afterwards (data() must be + NUL-terminated) @param[in] decimal_point_position index of the '.' in @a token, or std::string::npos if there is none @param[out] value the converted value @@ -678,38 +1023,76 @@ template void convert_float_locale_aware(StringType& token, std::size_t decimal_point_position, FloatType& value) { const bool has_dot = decimal_point_position != std::string::npos; - char decimal_point = get_decimal_point(); + std::string decimal_point = get_decimal_point(); for (;;) { - const bool substitute = has_dot && decimal_point != '.'; - if (substitute) - { - token[decimal_point_position] = static_cast(decimal_point); - } - char* endptr = nullptr; // NOLINT(misc-const-correctness,cppcoreguidelines-pro-type-vararg,hicpp-vararg) - strtof_by_type(value, token.data(), &endptr); - - if (substitute) + bool complete = false; + if (!has_dot || decimal_point.size() == 1) { - // the caller hands the token on (e.g. to the SAX interface) with '.' - token[decimal_point_position] = '.'; + const bool substitute = has_dot && decimal_point[0] != '.'; + if (substitute) + { + token[decimal_point_position] = static_cast(decimal_point[0]); + } + strtof_by_type(value, token.data(), &endptr); + if (substitute) + { + // the caller hands the token on (e.g. to the SAX interface) with '.' + token[decimal_point_position] = '.'; + } + complete = endptr == token.data() + token.size(); + } + else + { + std::string buffer(token.data(), token.size()); + buffer.replace(decimal_point_position, 1, decimal_point); + strtof_by_type(value, buffer.data(), &endptr); + complete = endptr == buffer.data() + buffer.size(); } - if (JSON_HEDLEY_LIKELY(endptr == token.data() + token.size())) + if (JSON_HEDLEY_LIKELY(complete)) { return; } // retry only if the locale changed; otherwise, this would loop forever - const char current_decimal_point = get_decimal_point(); + std::string current_decimal_point = get_decimal_point(); if (current_decimal_point == decimal_point) { return; } - decimal_point = current_decimal_point; + decimal_point = std::move(current_decimal_point); } } +/*! +@brief convert a validated float token like the lexer does + +For parsers of JSON text other than the lexer, which converts its own token +buffer in place. float, double, and long double where it is binary64 are +converted without allocation and independent of the locale; only other long +double formats that std::from_chars does not support need a copy of the token +for convert_float_locale_aware(). + +@param[in] first pointer to the first character of the token +@param[in] last pointer past the last character +@param[in] decimal_point_position index of the '.' in the token, or + std::string::npos if there is none +@param[in] mantissa_end index of the 'e'/'E', or the token length +@return the value, ±infinity if it overflows +*/ +template +FloatType convert_float(const char* first, const char* last, std::size_t decimal_point_position, std::size_t mantissa_end) +{ + FloatType value{}; + if (!convert_float_fast(first, last, decimal_point_position, mantissa_end, value)) + { + std::string token(first, last); + convert_float_locale_aware(token, decimal_point_position, value); + } + return value; +} + } // namespace detail NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/input/parser.hpp b/include/nlohmann/detail/input/parser.hpp index aa2b30fd7..665052d73 100644 --- a/include/nlohmann/detail/input/parser.hpp +++ b/include/nlohmann/detail/input/parser.hpp @@ -51,6 +51,20 @@ template using parser_callback_t = std::function; +/// whether a SAX handler ignores the text of float tokens (the library's own +/// handlers do), so that the parser need not materialize it +template +struct sax_ignores_number_text : std::false_type {}; + +template +struct sax_ignores_number_text> : std::true_type {}; + +template +struct sax_ignores_number_text> : std::true_type {}; + +template +struct sax_ignores_number_text> : std::true_type {}; + /*! @brief syntax analysis @@ -117,7 +131,7 @@ class parser } else { - json_sax_dom_parser sdp(result, allow_exceptions, &m_lexer); + json_sax_dom_parser sdp(result, allow_exceptions, &m_lexer); // in case of an error, return a discarded value if (!parse_dom(sdp, strict)) @@ -151,28 +165,63 @@ class parser if (result) { - if (strict) - { - // strict mode: next byte must be EOF - if (get_token() != token_type::end_of_input) - { - return sax->parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), exception_message(token_type::end_of_input, "value"), nullptr)); - } - } - else - { - // the caller keeps using the input: position it right after - // the value by leaving the character that terminated it - m_lexer.release_lookahead(); - } + return finish_input(*sax, strict); } - return result; + return false; } private: + /// the text of the current float token for @a sax (see sax_ignores_number_text) + template + const string_t& number_float_text(SAX* /*sax*/) + { + return number_float_text(std::integral_constant::value> {}); + } + + const string_t& number_float_text(std::true_type /*ignored*/) const noexcept + { + return m_lexer.get_ignored_number_text(); + } + + string_t& number_float_text(std::false_type /*ignored*/) + { + return m_lexer.get_string(); + } + + /*! + @brief apply the end-of-input handling after a value was parsed + + In strict mode, the next token must be the end of the input; otherwise + parse_error.101 is reported through @a sax. In non-strict mode, the + lookahead is released so the caller can keep reading the input right after + the parsed value. + + @param[in,out] sax the SAX parser to report an error to + @param[in] strict whether to expect the last token to be EOF + @return true, or what @a sax returned for the error + */ + template + bool finish_input(SAX& sax, const bool strict) + { + if (strict) + { + // strict mode: next byte must be EOF + if (get_token() != token_type::end_of_input) + { + return syntax_error(sax, exception_message(token_type::end_of_input, "value")); + } + } + else + { + // the caller keeps using the input: position it right after + // the value by leaving the character that terminated it + m_lexer.release_lookahead(); + } + + return true; + } + /*! @brief run a DOM SAX parser to completion and position the lexer @@ -192,23 +241,7 @@ class parser { sax_parse_internal(&sdp); - if (strict) - { - // in strict mode, input must be completely read - if (get_token() != token_type::end_of_input) - { - sdp.parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), - exception_message(token_type::end_of_input, "value"), nullptr)); - } - } - else - { - // the caller keeps using the input: position it right after - // the value by leaving the character that terminated it - m_lexer.release_lookahead(); - } + static_cast(finish_input(sdp, strict)); return !sdp.is_errored(); } @@ -217,12 +250,20 @@ class parser JSON_HEDLEY_NON_NULL(2) bool sax_parse_internal(SAX* sax) { - // stack to remember the hierarchy of structured values we are parsing - // true = array; false = object - std::vector states; // value to avoid a goto (see comment where set to true) bool skip_to_state_evaluation = false; + // stack to remember the hierarchy of structured values we are parsing + // true = array; false = object (bytes, not std::vector, whose + // push_back() is a call with GCC) + std::vector states; + // a document that starts with a container pushes onto the stack at + // once: allocate it in one step instead of growing it from 1 byte + if (last_token == token_type::begin_object || last_token == token_type::begin_array) + { + states.reserve(32); + } + while (true) { if (!skip_to_state_evaluation) @@ -250,11 +291,9 @@ class parser // parse key if (JSON_HEDLEY_UNLIKELY(last_token != token_type::value_string)) { - return sax->parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), exception_message(token_type::value_string, "object key"), nullptr)); + return syntax_error(*sax, exception_message(token_type::value_string, "object key")); } - if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string()))) + if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string_token()))) { return false; } @@ -262,13 +301,11 @@ class parser // parse separator (:) if (JSON_HEDLEY_UNLIKELY(!get_token_expecting(token_type::name_separator))) { - return sax->parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), exception_message(token_type::name_separator, "object separator"), nullptr)); + return syntax_error(*sax, exception_message(token_type::name_separator, "object separator")); } // remember we are now inside an object - states.push_back(false); + states.push_back(0); // parse values get_token(); @@ -293,7 +330,7 @@ class parser } // remember we are now inside an array - states.push_back(true); + states.push_back(1); // parse values (no need to call get_token) continue; @@ -307,10 +344,10 @@ class parser { return sax->parse_error(m_lexer.get_position(), m_lexer.get_token_string(), - out_of_range::create(406, concat("number overflow parsing '", m_lexer.get_token_string(), '\''), nullptr)); + out_of_range::create(exception_id::number_overflow, concat("number overflow parsing '", m_lexer.get_token_string(), '\''), nullptr)); } - if (JSON_HEDLEY_UNLIKELY(!sax->number_float(res, m_lexer.get_string()))) + if (JSON_HEDLEY_UNLIKELY(!sax->number_float(res, number_float_text(sax)))) { return false; } @@ -356,7 +393,7 @@ class parser case token_type::value_string: { - if (JSON_HEDLEY_UNLIKELY(!sax->string(m_lexer.get_string()))) + if (JSON_HEDLEY_UNLIKELY(!sax->string(m_lexer.get_string_token()))) { return false; } @@ -375,23 +412,16 @@ class parser case token_type::parse_error: { // using "uninitialized" to avoid an "expected" message - return sax->parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), exception_message(token_type::uninitialized, "value"), nullptr)); + return syntax_error(*sax, exception_message(token_type::uninitialized, "value")); } case token_type::end_of_input: { - if (JSON_HEDLEY_UNLIKELY(m_lexer.get_position().chars_read_total == 1)) + if (JSON_HEDLEY_UNLIKELY(m_lexer.get_chars_read_total() == 1)) { - return sax->parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), - "attempting to parse an empty input; check that your input string or stream contains the expected JSON", nullptr)); + return syntax_error(*sax, empty_input_message()); } - return sax->parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), exception_message(token_type::literal_or_value, "value"), nullptr)); + return syntax_error(*sax, exception_message(token_type::literal_or_value, "value")); } case token_type::uninitialized: case token_type::end_array: @@ -401,9 +431,7 @@ class parser case token_type::literal_or_value: default: // the last token was unexpected { - return sax->parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), exception_message(token_type::literal_or_value, "value"), nullptr)); + return syntax_error(*sax, exception_message(token_type::literal_or_value, "value")); } } } @@ -419,7 +447,7 @@ class parser return true; } - if (states.back()) // array + if (states.back() != 0) // array { // comma -> next value // or end of array (ignore_trailing_commas = true) @@ -454,9 +482,7 @@ class parser continue; } - return sax->parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), exception_message(token_type::end_array, "array"), nullptr)); + return syntax_error(*sax, exception_message(token_type::end_array, "array")); } // states.back() is false -> object @@ -473,12 +499,10 @@ class parser // parse key if (JSON_HEDLEY_UNLIKELY(last_token != token_type::value_string)) { - return sax->parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), exception_message(token_type::value_string, "object key"), nullptr)); + return syntax_error(*sax, exception_message(token_type::value_string, "object key")); } - if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string()))) + if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string_token()))) { return false; } @@ -486,9 +510,7 @@ class parser // parse separator (:) if (JSON_HEDLEY_UNLIKELY(!get_token_expecting(token_type::name_separator))) { - return sax->parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), exception_message(token_type::name_separator, "object separator"), nullptr)); + return syntax_error(*sax, exception_message(token_type::name_separator, "object separator")); } // parse values @@ -516,9 +538,7 @@ class parser continue; } - return sax->parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), exception_message(token_type::end_object, "object"), nullptr)); + return syntax_error(*sax, exception_message(token_type::end_object, "object")); } } @@ -535,6 +555,23 @@ class parser return (last_token = m_lexer.scan_expecting(expected_type)) == expected_type; } + /*! + @brief reports a syntax error at the current token (parse_error.101) + @param[in] sax the SAX parser to report the error to + @param[in] message the error message + @return the result of the SAX parser's parse_error() + */ + template + bool syntax_error(SAX& sax, const std::string& message) + { + // MSVC 2015 reports C4100 (unreferenced parameter) when SAX::parse_error is static + static_cast(sax); + // computing the position is O(input read so far) for seekable input + const position_t position = m_lexer.get_position(); + return sax.parse_error(position, m_lexer.get_token_string(), + parse_error::create(exception_id::syntax_error, position, message, nullptr)); + } + std::string exception_message(const token_type expected, const std::string& context) { std::string error_msg = "syntax error "; diff --git a/include/nlohmann/detail/input/pow5_table.hpp b/include/nlohmann/detail/input/pow5_table.hpp index 496fdb2f7..c7170e903 100644 --- a/include/nlohmann/detail/input/pow5_table.hpp +++ b/include/nlohmann/detail/input/pow5_table.hpp @@ -22,6 +22,10 @@ namespace detail constexpr std::int64_t pow5_128_smallest_power = -342; constexpr std::int64_t pow5_128_largest_power = 308; +// every entry of pow5_128() holds two 64-bit halves of 5^q, one per covered power of 5 +static_assert((pow5_128_largest_power - pow5_128_smallest_power + 1) * 2 == 1302, + "pow5_128_smallest_power/pow5_128_largest_power must match the size of the pow5_128() table"); + /*! @brief 128-bit approximations of 5^q for q in [-342, 308] diff --git a/include/nlohmann/detail/input/simd_scan.hpp b/include/nlohmann/detail/input/simd_scan.hpp new file mode 100644 index 000000000..8caed1814 --- /dev/null +++ b/include/nlohmann/detail/input/simd_scan.hpp @@ -0,0 +1,321 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-FileCopyrightText: 2018-2025 The simdjson authors +// SPDX-License-Identifier: MIT + +#pragma once + +#include // array +#include // atomic +#include // size_t +#include // uint8_t, uint64_t + +#include +#include + +// Vector code for runs of string bytes, used by the lexer for contiguous input +// (see string_bulk_run()). NEON (AArch64) and SSE2 (x86-64) belong to the +// baseline instruction sets and are used by default. The vector UTF-8 check +// needs NEON or SSSE3. SSSE3 is not part of x86-64, and the code must not +// depend on the flags of a translation unit (two translation units with +// different flags would have different definitions of the same inline +// functions): the check is compiled for SSSE3 with a function attribute and +// used where the CPU has SSSE3 (all x86-64 CPUs since about 2011), else the +// portable check. Code compiled for SSSE3 anyway (-mssse3 or more) skips the +// CPU check. JSON_NO_SIMD selects the portable code everywhere. +// NLOHMANN_JSON_NO_TARGET_ATTRIBUTE (for compilers that ignore the target +// attribute and then reject the SSSE3 intrinsics) keeps the check portable +// without disabling the SSE2 code. +#if !defined(JSON_NO_SIMD) && defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) && defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + #include + #define JSON_SCAN_NEON 1 +#else + #define JSON_SCAN_NEON 0 +#endif +// (x86 only: other targets can define __SSE2__ as well, e.g., WebAssembly with -msse2, but have no ) +#if !defined(JSON_NO_SIMD) && !JSON_SCAN_NEON && (defined(__x86_64__) || defined(__i386__) || defined(_M_X64) || defined(_M_IX86)) && !defined(_M_ARM64EC) && (defined(__SSE2__) || defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2)) + #include + #define JSON_SCAN_SSE2 1 +#else + #define JSON_SCAN_SSE2 0 +#endif +#if JSON_SCAN_SSE2 && defined(__SSSE3__) + #include + #define JSON_SCAN_SSSE3 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) +#else + #define JSON_SCAN_SSSE3 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) +#endif +#if JSON_SCAN_SSE2 && !JSON_SCAN_SSSE3 && !defined(NLOHMANN_JSON_NO_TARGET_ATTRIBUTE) && ((defined(__clang__) && __clang_major__ >= 4) || (defined(__GNUC__) && !defined(__clang__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 9)) && !defined(__cpp_modules))) + // (GCC before 4.9 has no SSSE3 intrinsics without -mssse3; GCC with C++20 + // modules drops the target attribute from functions it reads from a module + // interface and then rejects the intrinsics: "needs isa option -mssse3") + // of GCC before 11 has no include guard, and json.hpp and + // json_view.hpp both include it + #ifndef NLOHMANN_JSON_CPUID_INCLUDED + #define NLOHMANN_JSON_CPUID_INCLUDED + #include + #endif + #include + #define JSON_SCAN_SSSE3_DISPATCH 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) + #define JSON_SCAN_SSSE3_TARGET __attribute__((target("ssse3"))) +#elif JSON_SCAN_SSE2 && !JSON_SCAN_SSSE3 && defined(_MSC_VER) + // (MSVC compiles intrinsics of any instruction set) + #include + #include + #define JSON_SCAN_SSSE3_DISPATCH 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) + #define JSON_SCAN_SSSE3_TARGET +#else + #define JSON_SCAN_SSSE3_DISPATCH 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) + #define JSON_SCAN_SSSE3_TARGET +#endif +#define JSON_SCAN_VECTOR (JSON_SCAN_NEON || JSON_SCAN_SSE2) +#define JSON_SCAN_VECTOR_UTF8 (JSON_SCAN_NEON || JSON_SCAN_SSSE3 || JSON_SCAN_SSSE3_DISPATCH) + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +#if JSON_SCAN_VECTOR +/*! +@brief the first byte of a string run that is a quote, a backslash, a control +character, or not ASCII, 16 bytes per step + +Stops at such a byte, or where fewer than 16 bytes are left (the caller tells +the two apart). A signed compare with 0x20 finds control characters and +non-ASCII bytes at once. +*/ +JSON_HEDLEY_ALWAYS_INLINE const unsigned char* vector_plain_run(const unsigned char* p, const unsigned char* e) noexcept +{ + while (e - p >= 16) + { +#if JSON_SCAN_NEON + const uint8x16_t in = vld1q_u8(p); + const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))), + vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20))); + // one nibble per byte (the usual NEON replacement of x86's movemask, see + // D. Kutenin, "Porting x86 vector bitmask optimizations to Arm NEON", 2022) + const std::uint64_t bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0); + if (bits != 0) + { + return p + (count_trailing_zeros(bits) >> 2u); + } +#else + const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_cmplt_epi8(in, _mm_set1_epi8(0x20))); + const auto bits = static_cast(static_cast(_mm_movemask_epi8(special))); + if (bits != 0) + { + return p + count_trailing_zeros(bits); + } +#endif + p += 16; + } + return p; +} +#endif + +#if JSON_SCAN_VECTOR +/// vector_plain_run() for runs that are already longer than a few words: +/// inline with SSE2, out of line with NEON, where a larger string loop slows +/// down the rest of the lexer +#if JSON_SCAN_NEON + JSON_HEDLEY_NEVER_INLINE inline +#else + JSON_HEDLEY_ALWAYS_INLINE +#endif +const unsigned char* vector_plain_run_long(const unsigned char* p, const unsigned char* e) noexcept +{ + return vector_plain_run(p, e); +} +#endif + +#if JSON_SCAN_SSSE3_DISPATCH +/// whether the CPU has SSSE3 (CPUID leaf 1, ECX bit 9) +inline bool cpu_ssse3() noexcept +{ +#if defined(_MSC_VER) && !defined(__clang__) + std::array regs {{}}; + __cpuid(regs.data(), 1); + return (static_cast(regs[2]) & (1u << 9u)) != 0; +#else + unsigned eax = 0; + unsigned ebx = 0; + unsigned ecx = 0; + unsigned edx = 0; + return __get_cpuid(1, &eax, &ebx, &ecx, &edx) != 0 && (ecx & (1u << 9u)) != 0; +#endif +} + +/// whether the CPU has SSSE3, asked once: the answer is kept in an atomic +/// that is initialized at compile time, so that neither a guard of a local +/// static nor a global constructor is needed (threads that ask at the same +/// time all store the same answer) +JSON_HEDLEY_ALWAYS_INLINE bool cpu_has_ssse3() noexcept +{ + static std::atomic known{0}; // 0: not asked yet, 1: no, 2: yes + int state = known.load(std::memory_order_relaxed); + if (JSON_HEDLEY_UNLIKELY(state == 0)) + { + state = cpu_ssse3() ? 2 : 1; + known.store(state, std::memory_order_relaxed); + } + return state == 2; +} +#endif + +#if JSON_SCAN_VECTOR_UTF8 +/// Tables of the UTF-8 check of J. Keiser and D. Lemire, "Validating UTF-8 In +/// Less Than One Instruction Per Byte" (2021), as in simdjson ("lookup4"): each +/// maps a nibble (high and low nibble of the previous byte, high nibble of the +/// current byte) to the errors it allows; a byte pair is ill-formed if all +/// three have an error bit in common. +template +struct utf8_lookup4 +{ + static constexpr std::uint8_t too_short = 1u << 0u, too_long = 1u << 1u, overlong_3 = 1u << 2u, too_large = 1u << 3u; + static constexpr std::uint8_t surrogate = 1u << 4u, overlong_2 = 1u << 5u, too_large_1000 = 1u << 6u, overlong_4 = 1u << 6u; + static constexpr std::uint8_t two_conts = 1u << 7u, carry = too_short | too_long | two_conts; + static const std::array byte_1_high; + static const std::array byte_1_low; + static const std::array byte_2_high; +}; + +template +const std::array utf8_lookup4::byte_1_high = +{ + { + too_long, too_long, too_long, too_long, too_long, too_long, too_long, too_long, + two_conts, two_conts, two_conts, two_conts, + too_short | overlong_2, too_short, too_short | overlong_3 | surrogate, too_short | too_large | too_large_1000 | overlong_4 + } +}; + +template +const std::array utf8_lookup4::byte_1_low = +{ + { + carry | overlong_3 | overlong_2 | overlong_4, carry | overlong_2, carry, carry, + carry | too_large, carry | too_large | too_large_1000, carry | too_large | too_large_1000, carry | too_large | too_large_1000, + carry | too_large | too_large_1000, carry | too_large | too_large_1000, carry | too_large | too_large_1000, carry | too_large | too_large_1000, + carry | too_large | too_large_1000, carry | too_large | too_large_1000 | surrogate, carry | too_large | too_large_1000, carry | too_large | too_large_1000 + } +}; + +template +const std::array utf8_lookup4::byte_2_high = +{ + { + too_short, too_short, too_short, too_short, too_short, too_short, too_short, too_short, + static_cast(too_long | overlong_2 | two_conts | overlong_3 | too_large_1000 | overlong_4), + static_cast(too_long | overlong_2 | two_conts | overlong_3 | too_large), + static_cast(too_long | overlong_2 | two_conts | surrogate | too_large), + static_cast(too_long | overlong_2 | two_conts | surrogate | too_large), + too_short, too_short, too_short, too_short + } +}; + +/*! +@brief the run of plain string bytes and well-formed UTF-8 at p (a character + boundary), 16 bytes per step + +The first quote, backslash, or control character is found with vector +compares, and the UTF-8 check of J. Keiser and D. Lemire covers the bytes up +to it. Returns true with @a block at that byte; returns false with @a block at +the start of the 16 bytes where the check found ill-formed UTF-8 or where +fewer than 16 bytes are left, for the caller to finish one sequence at a time. +Out of line, so that no constants of the check occupy registers in the +caller's loop. On x86-64, it is compiled for SSSE3 (see cpu_has_ssse3()). +*/ +JSON_SCAN_SSSE3_TARGET JSON_HEDLEY_NEVER_INLINE inline bool vector_utf8_run(const unsigned char* p, const unsigned char* e, const unsigned char*& block_out) noexcept +{ + using lookup = utf8_lookup4<>; + const unsigned char* block = p; +#if JSON_SCAN_NEON + const uint8x16_t t1h = vld1q_u8(lookup::byte_1_high.data()); + const uint8x16_t t1l = vld1q_u8(lookup::byte_1_low.data()); + const uint8x16_t t2h = vld1q_u8(lookup::byte_2_high.data()); + uint8x16_t prev = vdupq_n_u8(0); + while (e - block >= 16) + { + const uint8x16_t in = vld1q_u8(block); + const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))), vcltq_u8(in, vdupq_n_u8(0x20))); + const uint8x16_t prev1 = vextq_u8(prev, in, 15); + const uint8x16_t sc = vandq_u8(vandq_u8(vqtbl1q_u8(t1h, vshrq_n_u8(prev1, 4)), vqtbl1q_u8(t1l, vandq_u8(prev1, vdupq_n_u8(0x0F)))), vqtbl1q_u8(t2h, vshrq_n_u8(in, 4))); + const uint8x16_t must23 = vorrq_u8(vqsubq_u8(vextq_u8(prev, in, 14), vdupq_n_u8(0xE0 - 0x80)), vqsubq_u8(vextq_u8(prev, in, 13), vdupq_n_u8(0xF0 - 0x80))); + const uint8x16_t err = veorq_u8(vandq_u8(must23, vdupq_n_u8(0x80)), sc); + const std::uint64_t special_bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0); + const std::uint64_t err_bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(vtstq_u8(err, err)), 4)), 0); + if (special_bits != 0) + { + // errors up to the special byte count (an incomplete sequence + // before a quote shows at the quote); the bytes after it do not + const unsigned k = static_cast(count_trailing_zeros(special_bits)) >> 2u; + const std::uint64_t upto = k == 15 ? ~std::uint64_t{0} : + (std::uint64_t{1} << (4u * (k + 1u))) - 1u; + if ((err_bits & upto) == 0) + { + block_out = block + k; + return true; + } + break; + } + if (err_bits != 0) + { + break; + } + prev = in; + block += 16; + } +#else + // the same with SSSE3 (pshufb for the table lookups; nibbles from 16-bit + // shifts, as there are no byte shifts) + const __m128i t1h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_high.data()))); // NOLINT(bugprone-casting-through-void) + const __m128i t1l = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_low.data()))); // NOLINT(bugprone-casting-through-void) + const __m128i t2h = _mm_loadu_si128(static_cast(static_cast(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(static_cast(block))); // NOLINT(bugprone-casting-through-void) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_cmpeq_epi8(_mm_subs_epu8(in, _mm_set1_epi8(0x1F)), zero)); // in < 0x20 + const __m128i prev1 = _mm_alignr_epi8(in, prev, 15); + const __m128i sc = _mm_and_si128(_mm_and_si128(_mm_shuffle_epi8(t1h, _mm_and_si128(_mm_srli_epi16(prev1, 4), nibble)), + _mm_shuffle_epi8(t1l, _mm_and_si128(prev1, nibble))), + _mm_shuffle_epi8(t2h, _mm_and_si128(_mm_srli_epi16(in, 4), nibble))); + const __m128i must23 = _mm_or_si128(_mm_subs_epu8(_mm_alignr_epi8(in, prev, 14), _mm_set1_epi8(0xE0 - 0x80)), + _mm_subs_epu8(_mm_alignr_epi8(in, prev, 13), _mm_set1_epi8(0xF0 - 0x80))); + const __m128i err = _mm_xor_si128(_mm_and_si128(must23, _mm_set1_epi8(static_cast(-128))), sc); + const auto special_bits = static_cast(_mm_movemask_epi8(special)); + const auto err_bits = ~static_cast(_mm_movemask_epi8(_mm_cmpeq_epi8(err, zero))) & 0xFFFFu; + if (special_bits != 0) + { + const auto k = static_cast(count_trailing_zeros(static_cast(special_bits))); + if ((err_bits & ((2u << k) - 1u)) == 0) + { + block_out = block + k; + return true; + } + break; + } + if (err_bits != 0) + { + break; + } + prev = in; + block += 16; + } +#endif + block_out = block; + return false; +} +#endif + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/input/string_scan.hpp b/include/nlohmann/detail/input/string_scan.hpp index 8a6f03122..d01a2f1a4 100644 --- a/include/nlohmann/detail/input/string_scan.hpp +++ b/include/nlohmann/detail/input/string_scan.hpp @@ -8,10 +8,13 @@ #pragma once +#include // array #include // size_t -#include // uint64_t +#include // uint64_t, uint8_t #include // memcpy +#include +#include #include // Optional SIMD backend for bulk UTF-8 validation. This is an opt-in external @@ -32,7 +35,7 @@ // This file contains the byte-level string-scanning helpers used by the lexer's // contiguous fast path. They operate purely on raw bytes (no dependency on the // lexer's template parameters) so they are free functions, keeping the lexer -// itself focused on the state machine; see lexer::scan_string_bulk(). +// itself focused on the state machine; see lexer::get_after_string_run(). NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail @@ -64,23 +67,17 @@ inline std::uint64_t swar_string_special(std::uint64_t v) noexcept // return the index of the first is_string_special() byte in [data, data+n), or // n if every byte is ordinary; scans 8 bytes at a time -inline std::size_t find_string_special(const unsigned char* data, std::size_t n) noexcept +JSON_HEDLEY_ALWAYS_INLINE std::size_t find_string_special(const unsigned char* data, std::size_t n) noexcept { std::size_t i = 0; for (; i + 8 <= n; i += 8) { - std::uint64_t word = 0; - std::memcpy(&word, data + i, sizeof(word)); - if (swar_string_special(word) != 0) + const std::uint64_t special = swar_string_special(read_eight_bytes(data + i)); + if (special != 0) { - // a special byte is in this word; locate it (endian-agnostic) - for (std::size_t j = 0; j < 8; ++j) - { - if (is_string_special(data[i + j])) - { - return i + j; - } - } + // the lowest flagged byte is the first special one: the borrows of + // the subtractions can only flag bytes above a true hit + return i + (static_cast(count_trailing_zeros(special)) / 8); } } for (; i < n; ++i) @@ -99,7 +96,7 @@ inline std::size_t find_string_special(const unsigned char* data, std::size_t n) // printable ASCII except '"' and '\\' - is emitted unchanged. Note this differs // from is_string_special() only in that 0x7F is also a stop (it is escaped as // \u007f under ensure_ascii). -inline bool is_ascii_copyable(unsigned char c) noexcept +constexpr bool is_ascii_copyable(unsigned char c) noexcept { return c >= 0x20u && c < 0x7Fu && c != '"' && c != '\\'; } @@ -114,8 +111,7 @@ inline std::size_t find_ascii_copyable_run(const unsigned char* data, std::size_ std::size_t i = 0; for (; i + 8 <= n; i += 8) { - std::uint64_t v = 0; - std::memcpy(&v, data + i, sizeof(v)); + const std::uint64_t v = read_eight_bytes(data + i); const std::uint64_t q = v ^ 0x2222222222222222ull; // '"' (0x22) const std::uint64_t b = v ^ 0x5C5C5C5C5C5C5C5Cull; // '\\' (0x5C) const std::uint64_t d = v ^ 0x7F7F7F7F7F7F7F7Full; // DEL (0x7F) @@ -126,7 +122,9 @@ inline std::size_t find_ascii_copyable_run(const unsigned char* data, std::size_ | (v & high); // >= 0x80 if (stop != 0) { - break; + // the lowest flagged byte is the first one to stop at (see + // find_string_special()) + return i + (static_cast(count_trailing_zeros(stop)) / 8); } } for (; i < n; ++i) @@ -238,29 +236,143 @@ inline std::size_t valid_utf8_prefix(const unsigned char* data, std::size_t n) n return pos; } -// Scalar (C++11) computation of the bulk run length: the number of leading -// bytes in [data, data+n) that are ordinary ASCII or complete well-formed UTF-8 +#if JSON_SCAN_VECTOR_UTF8 +/// the end of scan_string_vector from block, where the vector loop stopped +/// (ill-formed UTF-8, or fewer than 16 bytes left): one byte or sequence at a +/// time, from the start of a sequence that crosses into the block +inline const unsigned char* scan_string_finish(const unsigned char* p, const unsigned char* block, const unsigned char* e) noexcept +{ + for (int i = 1; i <= 3 && block - i >= p; ++i) + { + const unsigned char c = block[-i]; + if (c < 0x80) + { + break; + } + if (c >= 0xC0) + { + const int len = 2 + static_cast(c >= 0xE0) + static_cast(c >= 0xF0); + if (len > i) + { + block -= i; + } + break; + } + } + for (p = block; p != e;) + { + if (*p < 0x80) + { + if (is_string_special(*p)) + { + return p; + } + ++p; + continue; + } + const std::size_t n = validate_one_utf8(p, static_cast(e - p)); + if (n == 0) + { + return p; + } + p += n; + } + return p; +} + +/// the rest of a string from p (a character boundary), with the vector UTF-8 +/// check and the scalar check for the end of the block where it stopped +inline const unsigned char* scan_string_vector(const unsigned char* p, const unsigned char* e) noexcept +{ + const unsigned char* block = p; + if (vector_utf8_run(p, e, block)) + { + return block; + } + return scan_string_finish(p, block, e); +} +#endif + +#if JSON_SCAN_VECTOR +/// the end of the run of plain string characters at @a p: the first quote, +/// backslash, control character, or non-ASCII byte, or @a e. The first words +/// are checked with SWAR (one; four with NEON, where the vector mask costs +/// more), as most keys and short values end there; longer runs continue 16 +/// bytes at a time. +JSON_HEDLEY_ALWAYS_INLINE const unsigned char* plain_run_end(const unsigned char* p, const unsigned char* e) noexcept +{ + constexpr int prefix_words = JSON_SCAN_NEON ? 4 : 1; + for (int words = prefix_words; words > 0 && e - p >= 8; --words) + { + const std::uint64_t special = swar_string_special(read_eight_bytes(p)); + if (special != 0) + { + return p + (static_cast(count_trailing_zeros(special)) / 8); + } + p += 8; + } + // the vector loop only for 16 bytes or more: the serializer passes a + // string without its closing quote, so most of its runs end at e, and a + // call of the out-of-line NEON loop for a shorter rest made dump() up to + // 10% slower + if (e - p >= 16) + { + p = vector_plain_run_long(p, e); + if (e - p >= 16) + { + return p; // the vector loop stopped at a special byte + } + } + return p + find_string_special(p, static_cast(e - p)); +} +#endif + +// Computation of the bulk run length: the number of leading bytes in +// [data, data+n) that are ordinary ASCII or complete well-formed UTF-8 // sequences, stopping before the first byte that needs individual handling (the // closing quote, an escape, a control character, or an ill-formed/truncated -// sequence). ASCII is skipped 8 bytes at a time. -inline std::size_t scalar_string_bulk_run(const unsigned char* data, std::size_t n) noexcept +// sequence). ASCII is skipped 16 bytes at a time with SSE2 and 8 bytes at a +// time otherwise; runs of non-ASCII text use the vector UTF-8 check where +// available. +JSON_HEDLEY_ALWAYS_INLINE std::size_t scalar_string_bulk_run(const unsigned char* data, std::size_t n) noexcept { - std::size_t pos = 0; - while (pos < n) + const unsigned char* const e = data + n; + const unsigned char* p = data; + while (p != e) { - pos += find_string_special(data + pos, n - pos); - if (pos >= n || data[pos] < 0x80u) +#if JSON_SCAN_VECTOR + p = plain_run_end(p, e); +#else + p += find_string_special(p, static_cast(e - p)); +#endif + if (p == e || *p < 0x80u) { break; // end of buffer, or a quote/escape/control byte } - const std::size_t seq = validate_one_utf8(data + pos, n - pos); - if (seq == 0) +#if JSON_SCAN_VECTOR_UTF8 +#if JSON_SCAN_SSSE3_DISPATCH + if (JSON_HEDLEY_LIKELY(cpu_has_ssse3())) +#endif { - break; // ill-formed or truncated: let the byte path diagnose it + return static_cast(scan_string_vector(p, e) - data); } - pos += seq; +#endif +#if !JSON_SCAN_VECTOR_UTF8 || JSON_SCAN_SSSE3_DISPATCH + // a run of multi-byte sequences (e.g. CJK text) is validated sequence + // by sequence without searching for the next special byte in between + do + { + const std::size_t seq = validate_one_utf8(p, static_cast(e - p)); + if (seq == 0) + { + return static_cast(p - data); // ill-formed or truncated: let the byte path diagnose it + } + p += seq; + } + while (p != e && *p >= 0x80u); +#endif } - return pos; + return static_cast(p - data); } #if defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17) @@ -273,8 +385,7 @@ inline std::size_t find_string_delimiter(const unsigned char* data, std::size_t std::size_t i = 0; for (; i + 8 <= n; i += 8) { - std::uint64_t v = 0; - std::memcpy(&v, data + i, sizeof(v)); + const std::uint64_t v = read_eight_bytes(data + i); const std::uint64_t q = v ^ 0x2222222222222222ull; const std::uint64_t b = v ^ 0x5C5C5C5C5C5C5C5Cull; const std::uint64_t hit = ((q - ones) & ~q & high) @@ -282,14 +393,8 @@ inline std::size_t find_string_delimiter(const unsigned char* data, std::size_t | ((v - 0x2020202020202020ull) & ~v & high); if (hit != 0) { - for (std::size_t j = 0; j < 8; ++j) - { - const unsigned char c = data[i + j]; - if (c == '\"' || c == '\\' || c < 0x20u) - { - return i + j; - } - } + // the lowest flagged byte is the first delimiter (see find_string_special()) + return i + (static_cast(count_trailing_zeros(hit)) / 8); } } for (; i < n; ++i) @@ -308,7 +413,7 @@ inline std::size_t find_string_delimiter(const unsigned char* data, std::size_t // next delimiter is validated in one shot by simdutf; on the rare failure the // scalar helper recomputes the exact valid prefix so the byte path still // produces the precise diagnostic. Without it, the pure scalar path is used. -inline std::size_t string_bulk_run(const unsigned char* data, std::size_t n) noexcept +JSON_HEDLEY_ALWAYS_INLINE std::size_t string_bulk_run(const unsigned char* data, std::size_t n) noexcept { #if defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17) const std::size_t run = find_string_delimiter(data, n); @@ -320,5 +425,86 @@ inline std::size_t string_bulk_run(const unsigned char* data, std::size_t n) noe return scalar_string_bulk_run(data, n); } +#if JSON_SCAN_NEON && !(defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17)) +/// scalar_string_bulk_run() from @a start on, out of line (see +/// lexer_string_bulk_run()) +JSON_HEDLEY_NEVER_INLINE inline std::size_t string_bulk_run_from(const unsigned char* data, std::size_t start, std::size_t n) noexcept +{ + return start + scalar_string_bulk_run(data + start, n - start); +} +#endif + +// string_bulk_run() for the lexer. With NEON, only the first four words are +// checked inline (most keys and short values end there), and longer runs and +// non-ASCII text continue out of line. The lexer's scan function runs once per +// token, and the vector code inlined into it needs more registers, which every +// token pays for: 5% of json::accept() on jeopardy.json with Apple Clang on +// Apple M1, even though no string of that file reaches the vector code. The +// serializer keeps string_bulk_run() inline, which is faster there. +JSON_HEDLEY_ALWAYS_INLINE std::size_t lexer_string_bulk_run(const unsigned char* data, std::size_t n) noexcept +{ +#if JSON_SCAN_NEON && !(defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17)) + std::size_t i = 0; + for (int words = 4; words > 0 && n - i >= 8; --words) + { + const std::uint64_t special = swar_string_special(read_eight_bytes(data + i)); + if (special != 0) + { + const std::size_t pos = i + (static_cast(count_trailing_zeros(special)) / 8); + return data[pos] < 0x80u ? pos : string_bulk_run_from(data, pos, n); + } + i += 8; + } + return string_bulk_run_from(data, i, n); +#else + return string_bulk_run(data, n); +#endif +} + +// Decode the 4 hex digits at [data, data+4) - the digits following a `\u` +// escape - into a codepoint 0x0000..0xFFFF via one table lookup per byte +// (after yyjson's read_hex_u16), or return -1 if any of the 4 bytes is not a +// hex digit ('0'..'9', 'A'..'F', 'a'..'f'). The caller must already have +// checked that 4 bytes are available; used by lexer::get_codepoint()'s +// contiguous fast path. On -1 it falls back to the byte-at-a-time loop, which +// stops at the first invalid digit, so the reported error and position are +// unaffected by this fast path. +inline int hex_codepoint(const unsigned char* data) noexcept +{ + static const std::array hex_digit_table = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + { + { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 00..0F + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 10..1F + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 20..2F + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 30..3F ('0'..'9') + 0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 40..4F ('A'..'F') + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 50..5F + 0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 60..6F ('a'..'f') + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 70..7F + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 80..8F + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 90..9F + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // A0..AF + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // B0..BF + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // C0..CF + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // D0..DF + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // E0..EF + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // F0..FF + } + }; + + const std::uint8_t d0 = hex_digit_table[data[0]]; + const std::uint8_t d1 = hex_digit_table[data[1]]; + const std::uint8_t d2 = hex_digit_table[data[2]]; + const std::uint8_t d3 = hex_digit_table[data[3]]; + // every valid digit is <= 0xF; the combined OR only exceeds it if at + // least one of the four bytes was not a hex digit (looked up as 0xFF) + if ((d0 | d1 | d2 | d3) > 0x0F) + { + return -1; + } + return (d0 << 12) | (d1 << 8) | (d2 << 4) | d3; +} + } // namespace detail NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/iterators/iter_impl.hpp b/include/nlohmann/detail/iterators/iter_impl.hpp index 84fdd07f8..84a850356 100644 --- a/include/nlohmann/detail/iterators/iter_impl.hpp +++ b/include/nlohmann/detail/iterators/iter_impl.hpp @@ -108,33 +108,18 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_data.m_type) + if (m_object->is_object()) { - case value_t::object: - { - m_it.object_iterator = typename object_t::iterator(); - break; - } - - case value_t::array: - { - m_it.array_iterator = typename array_t::iterator(); - break; - } - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - { - m_it.primitive_iterator = primitive_iterator_t(); - break; - } + m_it.object_iterator = typename object_t::iterator(); + } + else if (m_object->is_array()) + { + m_it.array_iterator = typename array_t::iterator(); + } + else + { + // primitive (is_primitive(), including discarded) + m_it.primitive_iterator = primitive_iterator_t(); } } @@ -249,34 +234,35 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_data.m_type) + if (m_object->is_object()) { - case value_t::object: - { - m_it.object_iterator = m_object->m_data.m_value.object->end(); - break; - } - - case value_t::array: - { - m_it.array_iterator = m_object->m_data.m_value.array->end(); - break; - } - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - { - m_it.primitive_iterator.set_end(); - break; - } + m_it.object_iterator = m_object->m_data.m_value.object->end(); } + else if (m_object->is_array()) + { + m_it.array_iterator = m_object->m_data.m_value.array->end(); + } + else + { + // primitive (is_primitive(), including discarded) + m_it.primitive_iterator.set_end(); + } + } + + /// @throw invalid_iterator.212 if @a other points to another container + template + JSON_HEDLEY_ALWAYS_INLINE void check_same_container(const IterImpl& other) const + { + if (JSON_HEDLEY_UNLIKELY(m_object != other.m_object)) + { + throw_literal(exception_id::iterators_compare_different_values, "cannot compare iterators of different containers", m_object); + } + } + + /// @throw invalid_iterator.214 always + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_cannot_get_value() const + { + JSON_THROW(invalid_iterator::create(exception_id::iterator_value_unavailable, "cannot get value", m_object)); } public: @@ -288,40 +274,25 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_data.m_type) + if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array)) { - case value_t::object: - { - JSON_ASSERT(m_it.object_iterator != m_object->m_data.m_value.object->end()); - return m_it.object_iterator->second; - } - - case value_t::array: - { - JSON_ASSERT(m_it.array_iterator != m_object->m_data.m_value.array->end()); - return *m_it.array_iterator; - } - - case value_t::null: - JSON_THROW(invalid_iterator::create(214, "cannot get value", m_object)); - - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - { - if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.is_begin())) - { - return *m_object; - } - - JSON_THROW(invalid_iterator::create(214, "cannot get value", m_object)); - } + JSON_ASSERT(m_it.array_iterator != m_object->m_data.m_value.array->end()); + return *m_it.array_iterator; } + + if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object)) + { + JSON_ASSERT(m_it.object_iterator != m_object->m_data.m_value.object->end()); + return m_it.object_iterator->second; + } + + // neither array nor object: a primitive value (null has no value to point to) + if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type != value_t::null && m_it.primitive_iterator.is_begin())) + { + return *m_object; + } + + throw_cannot_get_value(); } /*! @@ -332,38 +303,25 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_data.m_type) + if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array)) { - case value_t::object: - { - JSON_ASSERT(m_it.object_iterator != m_object->m_data.m_value.object->end()); - return &(m_it.object_iterator->second); - } - - case value_t::array: - { - JSON_ASSERT(m_it.array_iterator != m_object->m_data.m_value.array->end()); - return &*m_it.array_iterator; - } - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - { - if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.is_begin())) - { - return m_object; - } - - JSON_THROW(invalid_iterator::create(214, "cannot get value", m_object)); - } + JSON_ASSERT(m_it.array_iterator != m_object->m_data.m_value.array->end()); + return &*m_it.array_iterator; } + + if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object)) + { + JSON_ASSERT(m_it.object_iterator != m_object->m_data.m_value.object->end()); + return &(m_it.object_iterator->second); + } + + // neither array nor object: a primitive value + if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.is_begin())) + { + return m_object; + } + + throw_cannot_get_value(); } /*! @@ -385,35 +343,20 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_data.m_type) + if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array)) { - case value_t::object: - { - std::advance(m_it.object_iterator, 1); - break; - } - - case value_t::array: - { - std::advance(m_it.array_iterator, 1); - break; - } - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - { - ++m_it.primitive_iterator; - break; - } + ++m_it.array_iterator; + return *this; } + if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object)) + { + std::advance(m_it.object_iterator, 1); + return *this; + } + + // neither array nor object: a primitive value + ++m_it.primitive_iterator; return *this; } @@ -436,35 +379,20 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_data.m_type) + if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array)) { - case value_t::object: - { - std::advance(m_it.object_iterator, -1); - break; - } - - case value_t::array: - { - std::advance(m_it.array_iterator, -1); - break; - } - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - { - --m_it.primitive_iterator; - break; - } + --m_it.array_iterator; + return *this; } + if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object)) + { + std::advance(m_it.object_iterator, -1); + return *this; + } + + // neither array nor object: a primitive value + --m_it.primitive_iterator; return *this; } @@ -476,10 +404,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci bool operator==(const IterImpl& other) const { // if objects are not the same, the comparison is undefined - if (JSON_HEDLEY_UNLIKELY(m_object != other.m_object)) - { - JSON_THROW(invalid_iterator::create(212, "cannot compare iterators of different containers", m_object)); - } + check_same_container(other); // value-initialized forward iterators can be compared, and must compare equal to other value-initialized iterators of the same type #4493 if (m_object == nullptr) @@ -487,25 +412,18 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci return true; } - switch (m_object->m_data.m_type) + if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array)) { - case value_t::object: - return (m_it.object_iterator == other.m_it.object_iterator); - - case value_t::array: - return (m_it.array_iterator == other.m_it.array_iterator); - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - return (m_it.primitive_iterator == other.m_it.primitive_iterator); + return (m_it.array_iterator == other.m_it.array_iterator); } + + if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object)) + { + return (m_it.object_iterator == other.m_it.object_iterator); + } + + // neither array nor object: a primitive value + return (m_it.primitive_iterator == other.m_it.primitive_iterator); } /*! @@ -525,10 +443,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci bool operator<(const iter_impl& other) const { // if objects are not the same, the comparison is undefined - if (JSON_HEDLEY_UNLIKELY(m_object != other.m_object)) - { - JSON_THROW(invalid_iterator::create(212, "cannot compare iterators of different containers", m_object)); - } + check_same_container(other); // value-initialized forward iterators can be compared, and must compare equal to other value-initialized iterators of the same type #4493 if (m_object == nullptr) @@ -537,25 +452,18 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci return false; } - switch (m_object->m_data.m_type) + if (m_object->is_object()) { - case value_t::object: - JSON_THROW(invalid_iterator::create(213, "cannot compare order of object iterators", m_object)); - - case value_t::array: - return (m_it.array_iterator < other.m_it.array_iterator); - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - return (m_it.primitive_iterator < other.m_it.primitive_iterator); + throw_literal(exception_id::iterator_order_on_object, "cannot compare order of object iterators", m_object); } + + if (m_object->is_array()) + { + return (m_it.array_iterator < other.m_it.array_iterator); + } + + // primitive (is_primitive(), including discarded) + return (m_it.primitive_iterator < other.m_it.primitive_iterator); } /*! @@ -593,30 +501,19 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_data.m_type) + if (m_object->is_object()) { - case value_t::object: - JSON_THROW(invalid_iterator::create(209, "cannot use offsets with object iterators", m_object)); + throw_literal(exception_id::iterator_arithmetic_on_object, "cannot use offsets with object iterators", m_object); + } - case value_t::array: - { - std::advance(m_it.array_iterator, i); - break; - } - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - { - m_it.primitive_iterator += i; - break; - } + if (m_object->is_array()) + { + std::advance(m_it.array_iterator, i); + } + else + { + // primitive (is_primitive(), including discarded) + m_it.primitive_iterator += i; } return *this; @@ -672,25 +569,18 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_data.m_type) + if (m_object->is_object()) { - case value_t::object: - JSON_THROW(invalid_iterator::create(209, "cannot use offsets with object iterators", m_object)); - - case value_t::array: - return m_it.array_iterator - other.m_it.array_iterator; - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - return m_it.primitive_iterator - other.m_it.primitive_iterator; + throw_literal(exception_id::iterator_arithmetic_on_object, "cannot use offsets with object iterators", m_object); } + + if (m_object->is_array()) + { + return m_it.array_iterator - other.m_it.array_iterator; + } + + // primitive (is_primitive(), including discarded) + return m_it.primitive_iterator - other.m_it.primitive_iterator; } /*! @@ -704,13 +594,13 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci switch (m_object->m_data.m_type) { case value_t::object: - JSON_THROW(invalid_iterator::create(208, "cannot use operator[] for object iterators", m_object)); + throw_literal(exception_id::iterator_subscript_on_object, "cannot use operator[] for object iterators", m_object); case value_t::array: return *std::next(m_it.array_iterator, n); case value_t::null: - JSON_THROW(invalid_iterator::create(214, "cannot get value", m_object)); + throw_cannot_get_value(); case value_t::string: case value_t::boolean: @@ -726,7 +616,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci return *m_object; } - JSON_THROW(invalid_iterator::create(214, "cannot get value", m_object)); + throw_cannot_get_value(); } } } @@ -739,12 +629,12 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - if (JSON_HEDLEY_LIKELY(m_object->is_object())) + if (JSON_HEDLEY_UNLIKELY(!m_object->is_object())) { - return m_it.object_iterator->first; + throw_literal(exception_id::iterator_key_not_object, "cannot use key() for non-object iterators", m_object); } - JSON_THROW(invalid_iterator::create(207, "cannot use key() for non-object iterators", m_object)); + return m_it.object_iterator->first; } /*! diff --git a/include/nlohmann/detail/iterators/iteration_proxy.hpp b/include/nlohmann/detail/iterators/iteration_proxy.hpp index c66a5c0fd..23a9e3b0d 100644 --- a/include/nlohmann/detail/iterators/iteration_proxy.hpp +++ b/include/nlohmann/detail/iterators/iteration_proxy.hpp @@ -109,35 +109,25 @@ template class iteration_proxy_value { JSON_ASSERT(anchor.m_object != nullptr); - switch (anchor.m_object->type()) + if (anchor.m_object->is_array()) { // use integer array index as key - case value_t::array: + if (array_index != array_index_last) { - if (array_index != array_index_last) - { - int_to_string( array_index_str, array_index ); - array_index_last = array_index; - } - return array_index_str; + int_to_string( array_index_str, array_index ); + array_index_last = array_index; } - - // use key from the object - case value_t::object: - return anchor.key(); - - // use an empty key for all primitive types - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - return empty_str; + return array_index_str; } + + if (anchor.m_object->is_object()) + { + // use key from the object + return anchor.key(); + } + + // use an empty key for all primitive types + return empty_str; } /// return value of the iterator diff --git a/include/nlohmann/detail/json_pointer.hpp b/include/nlohmann/detail/json_pointer.hpp index c59687709..457985480 100644 --- a/include/nlohmann/detail/json_pointer.hpp +++ b/include/nlohmann/detail/json_pointer.hpp @@ -8,22 +8,26 @@ #pragma once -#include // all_of +#include // all_of, copy +#include // array #include // isdigit #include // errno, ERANGE +#include // size_t +#include // uint64_t #include // strtoull +#include // memchr, memcmp, memcpy #ifndef JSON_NO_IO #include // ostream #endif // JSON_NO_IO #include // max -#include // accumulate -#include // set #include // string #include // move #include // vector #include #include +#include +#include #include #include #include @@ -31,6 +35,107 @@ NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +struct json_pointer_access; + +/*! +@brief result of parse_array_index() + +json_pointer::array_index() maps each value to the corresponding +parse_error/out_of_range exception; json_pointer::contains() and +get_checked_or_null(), which must not throw for an out-of-range or +unrepresentable index, and the json_view pointer resolution switch on it +directly instead. +*/ +enum class array_index_status +{ + ok, ///< @a s is a valid, representable array index + leading_zero, ///< @a s begins with '0' but has more than one character + not_a_number, ///< @a s is neither empty nor "-" and does not begin with a digit + unresolved, ///< @a s could not be converted to an integer + exceeds_size_type ///< @a s converts to an integer that exceeds SizeType +}; + +/*! +@brief convert a JSON Pointer reference token into an array index (RFC 6901, Sect. 4) + +@param[in] s reference token to be converted into an array index; its data() + must be NUL-terminated +@param[out] idx the integer representation of @a s if array_index_status::ok + is returned; left unchanged otherwise + +@return whether @a s is a valid array index, and if not, why + +@note this function never throws; json_pointer::array_index(), the callers that + must not throw, and the json_view pointer resolution build on it instead + of each re-implementing the RFC 6901 digit rules and the range check +@note force-inlined: as a free function template it is no longer inlined into + the pointer resolution, which makes JsonPointerAt up to 17% slower (#5891) +*/ +template +JSON_HEDLEY_ALWAYS_INLINE array_index_status parse_array_index(const StringType& s, SizeType& idx) noexcept +{ + // error condition (cf. RFC 6901, Sect. 4) + if (JSON_HEDLEY_UNLIKELY(s.size() > 1 && s[0] == '0')) + { + return array_index_status::leading_zero; + } + + // error condition (cf. RFC 6901, Sect. 4); this also covers single- + // character tokens, so "/x" and "/xy" fail alike; "-" and the empty + // token are left to the conversion below and are reported as unresolved + if (JSON_HEDLEY_UNLIKELY(!s.empty() && !(s.size() == 1 && s[0] == '-') && !(s[0] >= '0' && s[0] <= '9'))) + { + return array_index_status::not_a_number; + } + + // fast path: up to 18 decimal digits cannot overflow unsigned long long or + // SizeType (9 digits for a 32-bit SizeType), so strtoull and errno are not needed + constexpr std::size_t max_fast_digits = sizeof(SizeType) >= 8 ? 18 : 9; + if (!s.empty() && s.size() <= max_fast_digits) + { + SizeType acc = 0; + for (std::size_t i = 0; i < s.size(); ++i) + { + const char c = s[i]; + if (JSON_HEDLEY_UNLIKELY(c < '0' || c > '9')) + { + break; // not a plain number: let strtoull decide + } + acc = static_cast((acc * 10) + static_cast(c - '0')); + if (JSON_HEDLEY_UNLIKELY(i == s.size() - 1)) + { + idx = acc; + return array_index_status::ok; + } + } + } + + const char* p = s.data(); + char* p_end = nullptr; // NOLINT(misc-const-correctness) + errno = 0; // strtoull doesn't reset errno + const unsigned long long res = std::strtoull(p, &p_end, 10); // NOLINT(runtime/int) + if (p == p_end // invalid input or empty string + || errno == ERANGE // out of range + || JSON_HEDLEY_UNLIKELY(static_cast(p_end - p) != s.size())) // incomplete read + { + return array_index_status::unresolved; + } + + // the index does not fit into SizeType; on 64-bit platforms this is + // only SIZE_MAX itself (see #2203 and #5395) + if (res >= static_cast((std::numeric_limits::max)())) // NOLINT(runtime/int) + { + return array_index_status::exceeds_size_type; + } + + idx = static_cast(res); + return array_index_status::ok; +} + +} // namespace detail + /// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document /// @sa https://json.nlohmann.me/api/json_pointer/ template @@ -43,6 +148,8 @@ class json_pointer template friend class json_pointer; + friend struct detail::json_pointer_access; + template struct string_t_helper { @@ -69,12 +176,45 @@ class json_pointer /// @sa https://json.nlohmann.me/api/json_pointer/to_string/ string_t to_string() const { - return std::accumulate(reference_tokens.begin(), reference_tokens.end(), - string_t{}, - [](const string_t& a, const string_t& b) + if (reference_tokens.empty()) { - return detail::concat(a, '/', detail::escape(b)); - }); + return string_t(); + } + + // one buffer: every token adds its escaped form (at least its length) and a slash + std::size_t length = 0; + bool plain = true; + for (const auto& reference_token : reference_tokens) + { + length += reference_token.size() + 1; + const char* const data = reference_token.data(); + for (std::size_t i = 0, size = reference_token.size(); plain && i < size; ++i) + { + plain = data[i] != '~' && data[i] != '/'; + } + } + + string_t result; + if (JSON_HEDLEY_LIKELY(plain)) + { + // nothing to escape: the tokens are copied into the string + result.resize(length); + char* out = &result[0]; + for (const auto& reference_token : reference_tokens) + { + *out++ = '/'; + out = std::copy(reference_token.data(), reference_token.data() + reference_token.size(), out); + } + return result; + } + + result.reserve(length); + for (const auto& reference_token : reference_tokens) + { + result.append("/", 1); + detail::escape_into(result, reference_token); + } + return result; } /// @brief return a string representation of the JSON pointer @@ -129,21 +269,29 @@ class json_pointer friend json_pointer operator/(const json_pointer& lhs, const json_pointer& rhs) { - return json_pointer(lhs) /= rhs; + json_pointer result; + result.reference_tokens.reserve(lhs.reference_tokens.size() + rhs.reference_tokens.size()); + result.reference_tokens.insert(result.reference_tokens.end(), lhs.reference_tokens.begin(), lhs.reference_tokens.end()); + result.reference_tokens.insert(result.reference_tokens.end(), rhs.reference_tokens.begin(), rhs.reference_tokens.end()); + return result; } /// @brief create a new JSON pointer by appending the unescaped token at the end of the JSON pointer /// @sa https://json.nlohmann.me/api/json_pointer/operator_slash/ friend json_pointer operator/(const json_pointer& lhs, string_t token) // NOLINT(performance-unnecessary-value-param) { - return json_pointer(lhs) /= std::move(token); + json_pointer result; + result.reference_tokens.reserve(lhs.reference_tokens.size() + 1); + result.reference_tokens.insert(result.reference_tokens.end(), lhs.reference_tokens.begin(), lhs.reference_tokens.end()); + result.reference_tokens.push_back(std::move(token)); + return result; } /// @brief create a new JSON pointer by appending the array-index-token at the end of the JSON pointer /// @sa https://json.nlohmann.me/api/json_pointer/operator_slash/ friend json_pointer operator/(const json_pointer& lhs, std::size_t array_idx) { - return json_pointer(lhs) /= array_idx; + return lhs / detail::to_string(array_idx); } /// @brief returns the parent of this JSON pointer @@ -155,8 +303,8 @@ class json_pointer return *this; } - json_pointer res = *this; - res.pop_back(); + json_pointer res; + res.reference_tokens = std::vector(reference_tokens.begin(), reference_tokens.end() - 1); return res; } @@ -166,7 +314,7 @@ class json_pointer { if (JSON_HEDLEY_UNLIKELY(empty())) { - JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent", nullptr)); + throw_no_parent(); } reference_tokens.erase(reference_tokens.begin()); @@ -178,7 +326,7 @@ class json_pointer { if (JSON_HEDLEY_UNLIKELY(empty())) { - JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent", nullptr)); + throw_no_parent(); } return reference_tokens.front(); @@ -204,7 +352,7 @@ class json_pointer { if (JSON_HEDLEY_UNLIKELY(empty())) { - JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent", nullptr)); + throw_no_parent(); } reference_tokens.pop_back(); @@ -216,7 +364,7 @@ class json_pointer { if (JSON_HEDLEY_UNLIKELY(empty())) { - JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent", nullptr)); + throw_no_parent(); } return reference_tokens.back(); @@ -244,70 +392,56 @@ class json_pointer } private: - /*! - @brief result of @ref parse_array_index - - @ref array_index maps each value to the corresponding parse_error/out_of_range - exception; @ref contains and @ref get_checked_or_null, which must not throw for - an out-of-range or unrepresentable index, switch on it directly instead. - */ - enum class array_index_status + /// @throw out_of_range.405 always + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE static void throw_no_parent() { - ok, ///< @a s is a valid, representable array index - leading_zero, ///< @a s begins with '0' but has more than one character - not_a_number, ///< @a s does not begin with a digit - unresolved, ///< @a s could not be converted to an integer - exceeds_size_type ///< @a s converts to an integer that exceeds size_type - }; + JSON_THROW(detail::out_of_range::create(detail::exception_id::patch_on_root, "JSON pointer has no parent", nullptr)); + } - /*! - @param[in] s reference token to be converted into an array index - @param[out] idx the integer representation of @a s if @ref array_index_status::ok - is returned; left unchanged otherwise + /// whether the '~' at @a pos of @a s is not followed by '0' or '1' + JSON_HEDLEY_ALWAYS_INLINE static bool is_invalid_escape(const string_t& s, std::size_t pos) + { + return pos == s.size() - 1 || (s[pos + 1] != '0' && s[pos + 1] != '1'); + } - @return whether @a s is a valid array index, and if not, why + /// @throw parse_error.107 always + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE static void throw_missing_slash(const string_t& pointer) + { + static_cast(pointer); // unused when JSON_NOEXCEPTION is defined + JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_missing_slash, 1, detail::concat("JSON pointer must be empty or begin with '/' - was: '", pointer, "'"), nullptr)); + } - @note this function never throws; @ref array_index and the callers that must not - throw (@ref contains, @ref get_checked_or_null) build on it instead of each - re-implementing the RFC 6901 digit rules and the @a size_type range check - */ + /// @throw parse_error.107 if a nonempty @a pointer does not begin with '/' + JSON_HEDLEY_ALWAYS_INLINE static void check_begins_with_slash(const string_t& pointer) + { + if (JSON_HEDLEY_UNLIKELY(pointer[0] != '/')) + { + throw_missing_slash(pointer); + } + } + + /// @throw out_of_range.404 always + template + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE static void throw_unresolved(const string_t& reference_token, BasicJsonContext context) + { + static_cast(reference_token); // unused when JSON_NOEXCEPTION is defined + static_cast(context); + JSON_THROW(detail::out_of_range::create(detail::exception_id::pointer_unresolved, detail::concat("unresolved reference token '", reference_token, "'"), context)); + } + + using array_index_status = detail::array_index_status; + + /// whether a reference token is "-" (a plain comparison with a string literal is not always inlined) + static bool is_dash(const string_t& s) noexcept + { + return s.size() == 1 && s[0] == '-'; + } + + /// detail::parse_array_index() for the size_type of @a BasicJsonType template static array_index_status parse_array_index(const string_t& s, typename BasicJsonType::size_type& idx) noexcept { - using size_type = typename BasicJsonType::size_type; - - // error condition (cf. RFC 6901, Sect. 4) - if (JSON_HEDLEY_UNLIKELY(s.size() > 1 && s[0] == '0')) - { - return array_index_status::leading_zero; - } - - // error condition (cf. RFC 6901, Sect. 4) - if (JSON_HEDLEY_UNLIKELY(s.size() > 1 && !(s[0] >= '1' && s[0] <= '9'))) - { - return array_index_status::not_a_number; - } - - const char* p = s.data(); - char* p_end = nullptr; // NOLINT(misc-const-correctness) - errno = 0; // strtoull doesn't reset errno - const unsigned long long res = std::strtoull(p, &p_end, 10); // NOLINT(runtime/int) - if (p == p_end // invalid input or empty string - || errno == ERANGE // out of range - || JSON_HEDLEY_UNLIKELY(static_cast(p_end - p) != s.size())) // incomplete read - { - return array_index_status::unresolved; - } - - // the index does not fit into size_type; on 64-bit platforms this is - // only SIZE_MAX itself (see #2203 and #5395) - if (res >= static_cast((std::numeric_limits::max)())) // NOLINT(runtime/int) - { - return array_index_status::exceeds_size_type; - } - - idx = static_cast(res); - return array_index_status::ok; + return detail::parse_array_index(s, idx); } /*! @@ -329,13 +463,13 @@ class json_pointer // the branches differ in their messages, not after JSON_THROW's expansion // NOLINTNEXTLINE(bugprone-branch-clone) case array_index_status::leading_zero: - JSON_THROW(detail::parse_error::create(106, 0, detail::concat("array index '", s, "' must not begin with '0'"), nullptr)); + JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_index_leading_zero, 0, detail::concat("array index '", s, "' must not begin with '0'"), nullptr)); case array_index_status::not_a_number: - JSON_THROW(detail::parse_error::create(109, 0, detail::concat("array index '", s, "' is not a number"), nullptr)); + JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_index_not_a_number, 0, detail::concat("array index '", s, "' is not a number"), nullptr)); case array_index_status::unresolved: - JSON_THROW(detail::out_of_range::create(404, detail::concat("unresolved reference token '", s, "'"), nullptr)); + throw_unresolved(s, nullptr); case array_index_status::exceeds_size_type: - JSON_THROW(detail::out_of_range::create(410, detail::concat("array index ", s, " exceeds size_type"), nullptr)); + JSON_THROW(detail::out_of_range::create(detail::exception_id::value_out_of_range, detail::concat("array index ", s, " exceeds size_type"), nullptr)); case array_index_status::ok: default: break; @@ -344,12 +478,123 @@ class json_pointer return idx; } + /*! + @brief compare with a JSON pointer string without splitting it into tokens + + @throw parse_error.107 if the string is not empty or begins with '/' + @throw parse_error.108 if character '~' is not followed by '0' or '1' + */ + bool equals_string(const string_t& reference_string) const + { + if (reference_string.empty()) + { + return reference_tokens.empty(); + } + + check_begins_with_slash(reference_string); + + // the whole string is validated, whether or not it matches + const std::size_t size = reference_string.size(); + for (std::size_t pos = reference_string.find_first_of('~'); + pos != string_t::npos; + pos = reference_string.find_first_of('~', pos + 1)) + { + if (JSON_HEDLEY_UNLIKELY(is_invalid_escape(reference_string, pos))) + { + JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_invalid_escape, 0, "escape character '~' must be followed with '0' or '1'", nullptr)); + } + } + + // compare token by token, unescaping the string on the fly + std::size_t pos = 0; // the slash in front of the next token + for (const auto& reference_token : reference_tokens) + { + if (pos >= size) + { + return false; + } + + std::size_t i = pos + 1; + std::size_t j = 0; + const std::size_t token_size = reference_token.size(); + while (i < size && reference_string[i] != '/') + { + char c = reference_string[i++]; + if (c == '~') + { + c = (reference_string[i++] == '0') ? '~' : '/'; + } + if (j == token_size || reference_token[j] != c) + { + return false; + } + ++j; + } + if (j != token_size) + { + return false; + } + pos = i; + } + return pos == size; + } + + /// reserve room for @a n elements of an array type that has reserve() + template + static auto reserve_elements(ArrayType& array, const std::size_t n, detail::priority_tag<1> /*unused*/) + -> decltype(array.reserve(n), void()) + { + array.reserve(n); + } + + template + static void reserve_elements(ArrayType& /*array*/, const std::size_t /*n*/, detail::priority_tag<0> /*unused*/) + {} + + /// @brief object member access with a key that is only copied if it is inserted + /// + /// Objects that can find the position of a key and insert there + /// (std::map) search once, others look the key up and add it if it is missing. + template + static auto object_member_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<1> /*unused*/) + -> decltype(object.m_data.m_value.object->emplace_hint(object.m_data.m_value.object->lower_bound(key), key, nullptr), std::declval()) + { + auto& members = *object.m_data.m_value.object; + auto it = members.lower_bound(key); + if (it != members.end() && !members.key_comp()(key, it->first)) + { + return it->second; + } + + const auto old_capacity = object.container_capacity(); + it = members.emplace_hint(it, key, nullptr); + return object.set_parent_after_insert(it->second, old_capacity); + } + + template + static BasicJsonType& object_member_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<0> /*unused*/) + { + auto& members = *object.m_data.m_value.object; + const auto it = members.find(key); + if (it != members.end()) + { + return it->second; + } + return object.operator[](key); + } + + template + static BasicJsonType& object_member(BasicJsonType& object, const string_t& key) + { + return object_member_impl(object, key, detail::priority_tag<1> {}); + } + JSON_PRIVATE_UNLESS_TESTED: json_pointer top() const { if (JSON_HEDLEY_UNLIKELY(empty())) { - JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent", nullptr)); + throw_no_parent(); } json_pointer result = *this; @@ -359,91 +604,357 @@ class json_pointer private: /*! - @brief the reference token sequences that denote arrays + @brief the pointer prefixes of a flattened object that denote arrays - @ref unflatten collects the pointer prefixes that have a reference token 0 - among their children; @ref get_and_create creates arrays exactly below - those prefixes and objects everywhere else. Deciding this up front keeps - the result independent of the order in which the flattened object is - iterated, which is unspecified for some object types. + @ref unflatten creates arrays exactly below the prefixes that have a + reference token 0 among their children, and objects everywhere else. + Deciding this up front keeps the result independent of the order in which + the flattened object is iterated, which is unspecified for some object + types. + + A prefix is identified by the text of the key it is the beginning of: + escaping is unambiguous, so equal prefixes of valid keys are equal texts. + The set refers to the keys, which must outlive it. */ - using array_parents_t = std::set>; - - /*! - @brief create and return a reference to the pointed to value - - Complexity: Linear in the number of reference tokens. - - @throw parse_error.106 if an array index begins with '0' - @throw parse_error.109 if array index is not a number - @throw type_error.313 if value cannot be unflattened - */ - template - BasicJsonType& get_and_create(BasicJsonType& j, const array_parents_t& array_parents) const + class array_prefix_set { - auto* result = &j; - - // the reference tokens that have been consumed so far; used to look up - // whether the value to be created below is an array or an object - std::vector prefix; - - // in case no reference tokens exist, return a reference to the JSON value - // j which will be overwritten by a primitive value - for (const auto& reference_token : reference_tokens) + public: + void insert(const char* data, std::size_t size) { - switch (result->type()) + // flattened objects are usually sorted, so most keys that have a + // 0 yield the prefix of the previous one + if (last_data != nullptr && last_size == size && std::memcmp(last_data, data, size) == 0) { - case detail::value_t::null: - { - if (array_parents.find(prefix) != array_parents.end()) - { - // some reference token below this position is 0, so the - // value is an array - result = &result->operator[](array_index(reference_token)); - } - else - { - // start a new object otherwise - result = &result->operator[](reference_token); - } - break; - } - - case detail::value_t::object: - { - // create an entry in the object - result = &result->operator[](reference_token); - break; - } - - case detail::value_t::array: - { - // create an entry in the array - result = &result->operator[](array_index(reference_token)); - break; - } - - /* - The following code is only reached if there exists a reference - token _and_ the current value is primitive. In this case, we have - an error situation, because primitive values may only occur as - a single value; that is, with an empty list of reference tokens. - */ - case detail::value_t::string: - case detail::value_t::boolean: - case detail::value_t::number_integer: - case detail::value_t::number_unsigned: - case detail::value_t::number_float: - case detail::value_t::binary: - case detail::value_t::discarded: - default: - JSON_THROW(detail::type_error::create(313, "invalid value to unflatten", &j)); + return; } + last_data = data; + last_size = size; - prefix.push_back(reference_token); + if (table.empty()) + { + table.resize(16); + } + const std::size_t hash = hash_of(data, size); + const std::size_t mask = table.size() - 1; + std::size_t i = hash & mask; + while (table[i].data != nullptr) + { + if (table[i].hash == hash && table[i].size == size && std::memcmp(table[i].data, data, size) == 0) + { + return; + } + i = (i + 1) & mask; + } + table[i] = slot(hash, data, size); + + // keep the load factor below 1/2 + if (++used * 2 > table.size()) + { + grow(); + } } - return *result; + bool empty() const noexcept + { + return used == 0; + } + + bool contains(const char* data, std::size_t size) const noexcept + { + if (used == 0) + { + return false; + } + const std::size_t hash = hash_of(data, size); + const std::size_t mask = table.size() - 1; + for (std::size_t i = hash & mask; table[i].data != nullptr; i = (i + 1) & mask) + { + if (table[i].hash == hash && table[i].size == size && std::memcmp(table[i].data, data, size) == 0) + { + return true; + } + } + return false; + } + + private: + struct slot + { + slot() noexcept = default; + + slot(std::size_t h, const char* d, std::size_t s) noexcept + : hash(h), data(d), size(s) + {} + + std::size_t hash = 0; + const char* data = nullptr; // nullptr: the slot is empty + std::size_t size = 0; + }; + + static std::size_t hash_of(const char* data, std::size_t size) noexcept + { + std::uint64_t h = 0x9E3779B97F4A7C15ULL ^ size; + std::size_t i = 0; + for (; i + 8 <= size; i += 8) + { + std::uint64_t w = 0; + std::memcpy(&w, data + i, 8); + h = (h ^ w) * 0x100000001B3ULL; + h ^= h >> 32u; + } + std::uint64_t w = 0; + if (i < size) + { + std::memcpy(&w, data + i, size - i); + } + h = (h ^ w) * 0x9E3779B97F4A7C15ULL; + return detail::conditional_static_cast(h ^ (h >> 29u)); + } + + void grow() + { + std::vector old(table.size() * 2); + old.swap(table); + const std::size_t mask = table.size() - 1; + for (const auto& s : old) + { + if (s.data != nullptr) + { + std::size_t i = s.hash & mask; + while (table[i].data != nullptr) + { + i = (i + 1) & mask; + } + table[i] = s; + } + } + } + + std::vector table{}; + std::size_t used = 0; + const char* last_data = nullptr; + std::size_t last_size = 0; + }; + + /// @brief the length of the longest common prefix of the first @a limit bytes of @a a and @a b + static std::size_t common_prefix_length(const char* a, const char* b, const std::size_t limit) noexcept + { + std::size_t i = 0; + for (; i + 8 <= limit; i += 8) + { + std::uint64_t x = 0; + std::uint64_t y = 0; + std::memcpy(&x, a + i, 8); + std::memcpy(&y, b + i, 8); + if (x != y) + { + break; + } + } + while (i < limit && a[i] == b[i]) + { + ++i; + } + return i; + } + + /*! + @brief check that @a key is a valid JSON pointer, and add the prefixes of + it that denote arrays to @a arrays + + @throw parse_error.107 if the key is not empty or begins with '/' + @throw parse_error.108 if character '~' is not followed by '0' or '1' + + The same checks as @ref split, which throws the same exceptions, but + without building the reference tokens. + */ + static void scan_key(const string_t& key, const string_t* previous, array_prefix_set& arrays) + { + const std::size_t size = key.size(); + if (size == 0) + { + return; + } + + const char* const data = key.data(); + check_begins_with_slash(key); + + // The beginning of the key that it has in common with the previous + // one, which was checked already, is skipped: the scan starts with + // the token after the last slash in it. + std::size_t begin = 1; + if (previous != nullptr) + { + std::size_t same = common_prefix_length(data, previous->data(), (std::min)(size, previous->size())); + while (same > 1 && data[same - 1] != '/') + { + --same; + } + if (same > 1) + { + begin = same; + } + } + + for (std::size_t i = begin; i <= size; ++i) + { + if (i == size || data[i] == '/') + { + // a token 0 makes the pointer before it an array + if (i - begin == 1 && data[begin] == '0') + { + arrays.insert(data, begin - 1); + } + begin = i + 1; + } + else if (data[i] == '~') + { + // ~ must be followed by 0 or 1 + if (JSON_HEDLEY_UNLIKELY(is_invalid_escape(key, i))) + { + JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_invalid_escape, 0, "escape character '~' must be followed with '0' or '1'", nullptr)); + } + } + } + } + + /*! + @brief the values along the pointer last resolved by @ref resolve_plain + + A patch from @ref basic_json::diff lists its operations in document order, + so consecutive pointers share most of their reference tokens; the values + along the previous pointer are kept to resume from the common prefix. + `steps[k]` is the value after the first `k` reference tokens together with + the length of the prefix of `*path` that holds them. A value stays valid as + long as no operation changes its container: callers keep only values up to + the parent of the pointer they modify, or call `reset()`. + */ + template + struct plain_steps + { + const string_t* path = nullptr; + std::vector> steps{}; + + void reset() noexcept + { + path = nullptr; + steps.clear(); + } + }; + + /*! + @brief resolve a plain JSON pointer without building its reference tokens + + @param[in] root the value the pointer starts at + @param[in] path a JSON pointer string + @param[in] length the number of leading bytes of @a path to resolve; 0 or + the position of a slash + @param[in,out] token a string to hold the reference tokens, to reuse its storage + @param[in,out] cache the values along the previously resolved pointer; + @a path must outlive the next call + + @return the pointed to value, or `nullptr` if the pointer does not denote + an existing value, or does so in a way that is not plain: an array + index that is malformed or out of range, or a "-", or a reference + token with an escape. No exception is + thrown for these; callers use @ref json_pointer instead, which + handles them. + */ + template + static BasicJsonType* resolve_plain(BasicJsonType* root, const string_t& path, const std::size_t length, string_t& token, plain_steps& cache) + { + auto& steps = cache.steps; + + // resume from the deepest step that is also a prefix of this path + std::size_t depth = 1; + if (cache.path != nullptr && !steps.empty() && steps[0].second == root) + { + const string_t& previous = *cache.path; + for (depth = steps.size(); depth > 1; --depth) + { + // a step ends at a token boundary of the previous path + const std::size_t prefix = steps[depth - 1].first; + if (prefix <= length && (prefix == length || path[prefix] == '/') && std::memcmp(path.data(), previous.data(), prefix) == 0) + { + break; + } + } + steps.resize(depth); + } + else + { + steps.assign(1, std::make_pair(std::size_t{0}, root)); + } + cache.path = &path; + + BasicJsonType* ptr = steps[depth - 1].second; + for (std::size_t pos = steps[depth - 1].first; pos < length;) + { + if (JSON_HEDLEY_UNLIKELY(path[pos] != '/')) + { + return nullptr; + } + + // the reference token between the slash at pos and the next one, + // which must not have an escape + const char* const begin = path.data() + pos + 1; + const char* const limit = path.data() + length; + const char* end = begin; + while (end < limit && *end != '/') + { + if (JSON_HEDLEY_UNLIKELY(*end == '~')) + { + return nullptr; + } + ++end; + } + + if (ptr->is_object()) + { + token.clear(); + token.append(begin, static_cast(end - begin)); + const auto it = ptr->m_data.m_value.object->find(token); + if (JSON_HEDLEY_UNLIKELY(it == ptr->m_data.m_value.object->end())) + { + return nullptr; + } + ptr = &it->second; + } + else if (ptr->is_array()) + { + // an index of up to 18 digits without a leading zero fits into size_type + // (9 digits for a 32-bit size_type); everything else is left to json_pointer + constexpr std::size_t max_digits = sizeof(typename BasicJsonType::size_type) >= 8 ? 18 : 9; + const auto digits = static_cast(end - begin); + if (JSON_HEDLEY_UNLIKELY(digits == 0 || digits > max_digits || (digits > 1 && *begin == '0'))) + { + return nullptr; + } + typename BasicJsonType::size_type idx = 0; + for (const char* c = begin; c != end; ++c) + { + if (JSON_HEDLEY_UNLIKELY(*c < '0' || *c > '9')) + { + return nullptr; + } + idx = static_cast((idx * 10) + static_cast(*c - '0')); + } + if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size())) + { + return nullptr; + } + ptr = &(*ptr->m_data.m_value.array)[idx]; + } + else + { + // primitive value with a reference token left + return nullptr; + } + pos = static_cast(end - path.data()); + steps.emplace_back(pos, ptr); + } + + return ptr; } /*! @@ -491,40 +1002,28 @@ class json_pointer : detail::value_t::object; } - switch (ptr->type()) + if (ptr->is_object()) { - case detail::value_t::object: + // use unchecked object access + ptr = &object_member(*ptr, reference_token); + } + else if (ptr->is_array()) + { + if (reference_token == "-") { - // use unchecked object access - ptr = &ptr->operator[](reference_token); - break; + // explicitly treat "-" as index beyond the end + ptr = &ptr->operator[](ptr->m_data.m_value.array->size()); } - - case detail::value_t::array: + else { - if (reference_token == "-") - { - // explicitly treat "-" as index beyond the end - ptr = &ptr->operator[](ptr->m_data.m_value.array->size()); - } - else - { - // convert array index to number; unchecked access - ptr = &ptr->operator[](array_index(reference_token)); - } - break; + // convert array index to number; unchecked access + ptr = &ptr->operator[](array_index(reference_token)); } - - case detail::value_t::null: - case detail::value_t::string: - case detail::value_t::boolean: - case detail::value_t::number_integer: - case detail::value_t::number_unsigned: - case detail::value_t::number_float: - case detail::value_t::binary: - case detail::value_t::discarded: - default: - JSON_THROW(detail::out_of_range::create(404, detail::concat("unresolved reference token '", reference_token, "'"), ptr)); + } + else + { + // primitive value with a reference token left + throw_unresolved(reference_token, ptr); } } @@ -542,46 +1041,39 @@ class json_pointer { for (const auto& reference_token : reference_tokens) { - switch (ptr->type()) + if (ptr->is_object()) { - case detail::value_t::object: + const auto it = ptr->m_data.m_value.object->find(reference_token); + if (JSON_HEDLEY_UNLIKELY(it == ptr->m_data.m_value.object->end())) { - // note: at performs range check - ptr = &ptr->at(reference_token); - break; + // at() throws out_of_range.403 with the usual message + ptr->at(reference_token); + } + ptr = &it->second; + } + else if (ptr->is_array()) + { + if (JSON_HEDLEY_UNLIKELY(is_dash(reference_token))) + { + // "-" always fails the range check + JSON_THROW(detail::out_of_range::create(detail::exception_id::pointer_past_the_end_index, detail::concat( + "array index '-' (", std::to_string(ptr->m_data.m_value.array->size()), + ") is out of range"), ptr)); } - case detail::value_t::array: + const auto idx = array_index(reference_token); + // Bounds check before access to avoid exception with JSON_NOEXCEPTION + if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size())) { - if (JSON_HEDLEY_UNLIKELY(reference_token == "-")) - { - // "-" always fails the range check - JSON_THROW(detail::out_of_range::create(402, detail::concat( - "array index '-' (", std::to_string(ptr->m_data.m_value.array->size()), - ") is out of range"), ptr)); - } - - const auto idx = array_index(reference_token); - // Bounds check before access to avoid exception with JSON_NOEXCEPTION - if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size())) - { - JSON_THROW(detail::out_of_range::create(401, detail::concat( - "array index ", std::to_string(idx), " is out of range"), ptr)); - } - ptr = &ptr->operator[](idx); - break; + JSON_THROW(detail::out_of_range::create(detail::exception_id::array_index_out_of_range, detail::concat( + "array index ", std::to_string(idx), " is out of range"), ptr)); } - - case detail::value_t::null: - case detail::value_t::string: - case detail::value_t::boolean: - case detail::value_t::number_integer: - case detail::value_t::number_unsigned: - case detail::value_t::number_float: - case detail::value_t::binary: - case detail::value_t::discarded: - default: - JSON_THROW(detail::out_of_range::create(404, detail::concat("unresolved reference token '", reference_token, "'"), ptr)); + ptr = &(*ptr->m_data.m_value.array)[idx]; + } + else + { + // primitive value with a reference token left + throw_unresolved(reference_token, ptr); } } @@ -610,40 +1102,32 @@ class json_pointer { for (const auto& reference_token : reference_tokens) { - switch (ptr->type()) + if (ptr->is_object()) { - case detail::value_t::object: + // use unchecked object access; the const operator[] + // asserts that the key exists + const auto it = ptr->m_data.m_value.object->find(reference_token); + JSON_ASSERT(it != ptr->m_data.m_value.object->end()); + ptr = &it->second; + } + else if (ptr->is_array()) + { + if (JSON_HEDLEY_UNLIKELY(is_dash(reference_token))) { - // use unchecked object access; the const operator[] - // asserts that the key exists - ptr = &ptr->operator[](reference_token); - break; + // "-" cannot be used for const access + JSON_THROW(detail::out_of_range::create(detail::exception_id::pointer_past_the_end_index, detail::concat("array index '-' (", std::to_string(ptr->m_data.m_value.array->size()), ") is out of range"), ptr)); } - case detail::value_t::array: - { - if (JSON_HEDLEY_UNLIKELY(reference_token == "-")) - { - // "-" cannot be used for const access - JSON_THROW(detail::out_of_range::create(402, detail::concat("array index '-' (", std::to_string(ptr->m_data.m_value.array->size()), ") is out of range"), ptr)); - } - - // use unchecked array access; the const operator[] - // asserts that the index exists - ptr = &ptr->operator[](array_index(reference_token)); - break; - } - - case detail::value_t::null: - case detail::value_t::string: - case detail::value_t::boolean: - case detail::value_t::number_integer: - case detail::value_t::number_unsigned: - case detail::value_t::number_float: - case detail::value_t::binary: - case detail::value_t::discarded: - default: - JSON_THROW(detail::out_of_range::create(404, detail::concat("unresolved reference token '", reference_token, "'"), ptr)); + // use unchecked array access; the const operator[] + // asserts that the index exists + const auto idx = array_index(reference_token); + JSON_ASSERT(idx < ptr->m_data.m_value.array->size()); + ptr = &(*ptr->m_data.m_value.array)[idx]; + } + else + { + // primitive value with a reference token left + throw_unresolved(reference_token, ptr); } } @@ -667,66 +1151,54 @@ class json_pointer { for (const auto& reference_token : reference_tokens) { - switch (ptr->type()) + if (ptr->is_object()) { - case detail::value_t::object: + const auto it = ptr->m_data.m_value.object->find(reference_token); + if (JSON_HEDLEY_UNLIKELY(it == ptr->m_data.m_value.object->end())) { - const auto it = ptr->find(reference_token); - if (JSON_HEDLEY_UNLIKELY(it == ptr->end())) - { - return nullptr; - } - ptr = &*it; - break; - } - - case detail::value_t::array: - { - if (JSON_HEDLEY_UNLIKELY(reference_token == "-")) - { - // "-" always fails the range check - return nullptr; - } - - // a malformed index throws parse_error.106/109; an - // index that is syntactically valid but cannot be - // represented (out_of_range.404/410) is treated like an - // out-of-range index below - typename BasicJsonType::size_type idx{}; - switch (parse_array_index(reference_token, idx)) - { - // the branches differ in their messages, not after JSON_THROW's expansion - // NOLINTNEXTLINE(bugprone-branch-clone) - case array_index_status::leading_zero: - JSON_THROW(detail::parse_error::create(106, 0, detail::concat("array index '", reference_token, "' must not begin with '0'"), nullptr)); - case array_index_status::not_a_number: - JSON_THROW(detail::parse_error::create(109, 0, detail::concat("array index '", reference_token, "' is not a number"), nullptr)); - case array_index_status::unresolved: - case array_index_status::exceeds_size_type: - return nullptr; - case array_index_status::ok: - default: - break; - } - - if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size())) - { - return nullptr; - } - ptr = &ptr->operator[](idx); - break; - } - - case detail::value_t::null: - case detail::value_t::string: - case detail::value_t::boolean: - case detail::value_t::number_integer: - case detail::value_t::number_unsigned: - case detail::value_t::number_float: - case detail::value_t::binary: - case detail::value_t::discarded: - default: return nullptr; + } + ptr = &it->second; + } + else if (ptr->is_array()) + { + if (JSON_HEDLEY_UNLIKELY(is_dash(reference_token))) + { + // "-" always fails the range check + return nullptr; + } + + // a malformed index throws parse_error.106/109; an + // index that is syntactically valid but cannot be + // represented (out_of_range.404/410) is treated like an + // out-of-range index below + typename BasicJsonType::size_type idx{}; + switch (parse_array_index(reference_token, idx)) + { + // the branches differ in their messages, not after JSON_THROW's expansion + // NOLINTNEXTLINE(bugprone-branch-clone) + case array_index_status::leading_zero: + JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_index_leading_zero, 0, detail::concat("array index '", reference_token, "' must not begin with '0'"), nullptr)); + case array_index_status::not_a_number: + JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_index_not_a_number, 0, detail::concat("array index '", reference_token, "' is not a number"), nullptr)); + case array_index_status::unresolved: + case array_index_status::exceeds_size_type: + return nullptr; + case array_index_status::ok: + default: + break; + } + + if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size())) + { + return nullptr; + } + ptr = &(*ptr->m_data.m_value.array)[idx]; + } + else + { + // primitive value with a reference token left + return nullptr; } } @@ -742,62 +1214,48 @@ class json_pointer { for (const auto& reference_token : reference_tokens) { - switch (ptr->type()) + if (ptr->is_object()) { - case detail::value_t::object: + const auto it = ptr->m_data.m_value.object->find(reference_token); + if (it == ptr->m_data.m_value.object->end()) { - if (!ptr->contains(reference_token)) - { - // we did not find the key in the object - return false; - } - - ptr = &ptr->operator[](reference_token); - break; - } - - case detail::value_t::array: - { - if (JSON_HEDLEY_UNLIKELY(reference_token == "-")) - { - // "-" always fails the range check - return false; - } - - // any parse failure (malformed index, or one that is syntactically - // valid but not representable as size_type) means the reference - // token cannot denote an existing array element -- contains() must - // not throw (see #5395), so it is treated as "not found" - typename BasicJsonType::size_type idx{}; - if (JSON_HEDLEY_UNLIKELY(parse_array_index(reference_token, idx) != array_index_status::ok)) - { - return false; - } - - if (idx >= ptr->size()) - { - // index out of range - return false; - } - - ptr = &ptr->operator[](idx); - break; - } - - case detail::value_t::null: - case detail::value_t::string: - case detail::value_t::boolean: - case detail::value_t::number_integer: - case detail::value_t::number_unsigned: - case detail::value_t::number_float: - case detail::value_t::binary: - case detail::value_t::discarded: - default: - { - // we do not expect primitive values if there is still a - // reference token to process + // we did not find the key in the object return false; } + + ptr = &it->second; + } + else if (ptr->is_array()) + { + if (JSON_HEDLEY_UNLIKELY(is_dash(reference_token))) + { + // "-" always fails the range check + return false; + } + + // any parse failure (malformed index, or one that is syntactically + // valid but not representable as size_type) means the reference + // token cannot denote an existing array element -- contains() must + // not throw (see #5395), so it is treated as "not found" + typename BasicJsonType::size_type idx{}; + if (JSON_HEDLEY_UNLIKELY(parse_array_index(reference_token, idx) != array_index_status::ok)) + { + return false; + } + + if (idx >= ptr->m_data.m_value.array->size()) + { + // index out of range + return false; + } + + ptr = &(*ptr->m_data.m_value.array)[idx]; + } + else + { + // we do not expect primitive values if there is still a + // reference token to process + return false; } } @@ -825,123 +1283,296 @@ class json_pointer } // check if a nonempty reference string begins with slash - if (JSON_HEDLEY_UNLIKELY(reference_string[0] != '/')) + check_begins_with_slash(reference_string); + + // every slash starts a reference token + const std::size_t size = reference_string.size(); + std::size_t token_count = 0; + for (std::size_t i = 0; i < size; ++i) { - JSON_THROW(detail::parse_error::create(107, 1, detail::concat("JSON pointer must be empty or begin with '/' - was: '", reference_string, "'"), nullptr)); + token_count += static_cast(reference_string[i] == '/'); } + result.reserve(token_count); - // extract the reference tokens: - // - slash: position of the last read slash (or end of string) - // - start: position after the previous slash - for ( - // search for the first slash after the first character - std::size_t slash = reference_string.find_first_of('/', 1), - // set the beginning of the first reference token - start = 1; - // we can stop if start == 0 (if slash == string_t::npos) - start != 0; - // set the beginning of the next reference token - // (will eventually be 0 if slash == string_t::npos) - start = (slash == string_t::npos) ? 0 : slash + 1, - // find next slash - slash = reference_string.find_first_of('/', start)) + // extract the reference tokens: every one runs from after a slash to + // the next slash or the end of the string; a token without a '~' (the + // common case) is stored as it is, others are checked and unescaped + const char* const data = reference_string.data(); + const char* const end = data + size; + for (const char* start = data + 1;; ++start) { - // use the text between the beginning of the reference token - // (start) and the last slash (slash). - const auto count = (slash == string_t::npos ? reference_string.size() : slash) - start; - auto reference_token = string_t(reference_string.data() + start, count); - - // check reference tokens are properly escaped - for (std::size_t pos = reference_token.find_first_of('~'); - pos != string_t::npos; - pos = reference_token.find_first_of('~', pos + 1)) + const char* slash = start; + bool escaped = false; + while (slash != end && *slash != '/') { - JSON_ASSERT(reference_token[pos] == '~'); - - // ~ must be followed by 0 or 1 - if (JSON_HEDLEY_UNLIKELY(pos == reference_token.size() - 1 || - (reference_token[pos + 1] != '0' && - reference_token[pos + 1] != '1'))) - { - JSON_THROW(detail::parse_error::create(108, 0, "escape character '~' must be followed with '0' or '1'", nullptr)); - } + escaped = escaped || *slash == '~'; + ++slash; } - // finally, store the reference token - detail::unescape(reference_token); - result.push_back(reference_token); + const auto count = static_cast(slash - start); + if (JSON_HEDLEY_LIKELY(!escaped)) + { + result.emplace_back(start, count); + } + else + { + auto reference_token = string_t(start, count); + + // check reference tokens are properly escaped + for (std::size_t pos = reference_token.find_first_of('~'); pos != string_t::npos; pos = reference_token.find_first_of('~', pos + 1)) + { + JSON_ASSERT(reference_token[pos] == '~'); + + // ~ must be followed by 0 or 1 + if (JSON_HEDLEY_UNLIKELY(is_invalid_escape(reference_token, pos))) + { + JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_invalid_escape, 0, "escape character '~' must be followed with '0' or '1'", nullptr)); + } + } + + detail::unescape(reference_token); + result.push_back(std::move(reference_token)); + } + + if (slash == end) + { + break; + } + start = slash; } return result; } private: + // Add the entry (key, v) to the object `obj`. Flattened paths are pairwise + // distinct, so objects that can append a key known to be absent + // (ordered_map) skip the lookup, which is linear and would make flatten + // quadratic. Objects with a positional insert (std::map) insert next to + // the entry added last: paths come out in nearly sorted order, so the + // position is usually right and no search from the root is needed. + // All other object types use operator[]. + template + static auto add_flattened(ObjectType& obj, typename ObjectType::iterator& /*hint*/, const string_t& key, ValueType&& v, detail::priority_tag<2> /*unused*/) + -> decltype(obj.emplace_new_key(key, std::forward(v)), void()) + { + obj.emplace_new_key(key, std::forward(v)); + } + + template + static auto add_flattened(ObjectType& obj, typename ObjectType::iterator& hint, const string_t& key, ValueType&& v, detail::priority_tag<1> /*unused*/) + -> decltype(hint = obj.emplace_hint(hint, key, std::forward(v)), void()) + { + hint = obj.emplace_hint(hint, key, std::forward(v)); + ++hint; + } + + template + static void add_flattened(ObjectType& obj, typename ObjectType::iterator& /*hint*/, const string_t& key, ValueType&& v, detail::priority_tag<0> /*unused*/) + { + obj[key] = std::forward(v); + } + + /// the number of entries that flattening @a value adds: its primitive + /// values and empty containers + template + static std::size_t count_flattened(const BasicJsonType& value) + { + std::size_t count = 0; + std::vector pending(1, &value); + while (!pending.empty()) + { + const BasicJsonType* const v = pending.back(); + pending.pop_back(); + if (v->is_array() && !v->m_data.m_value.array->empty()) + { + for (const auto& element : *v->m_data.m_value.array) + { + pending.push_back(&element); + } + } + else if (v->is_object() && !v->m_data.m_value.object->empty()) + { + for (const auto& member : *v->m_data.m_value.object) + { + pending.push_back(&member.second); + } + } + else + { + ++count; + } + } + return count; + } + + // Reserve the storage for @a value of objects that have a reserve(), so + // that it does not have to be moved while it grows + template + static auto reserve_flattened(ObjectType& obj, const BasicJsonType& value, detail::priority_tag<1> /*unused*/) + -> decltype(obj.reserve(std::size_t {}), void()) + { + obj.reserve(count_flattened(value)); + } + + template + static void reserve_flattened(ObjectType& /*obj*/, const BasicJsonType& /*value*/, detail::priority_tag<0> /*unused*/) + {} + + /// append the decimal representation of @a i to @a out + static void append_index(string_t& out, std::size_t i) + { + std::array buf{}; + std::size_t pos = buf.size(); + do + { + buf[--pos] = static_cast('0' + (i % 10)); + i /= 10; + } + while (i != 0); + out.append(buf.data() + pos, buf.size() - pos); + } + /*! @param[in] reference_string the reference string to the current value @param[in] value the value to consider @param[in,out] result the result object to insert values to @note Empty objects or arrays are flattened to `null`. + + The value is walked with an explicit stack rather than the call stack, so + arbitrarily deeply nested values can be flattened. + + @sa https://github.com/nlohmann/json/issues/5393 */ template static void flatten(const string_t& reference_string, const BasicJsonType& value, BasicJsonType& result) { - switch (value.type()) + using object_const_iterator = typename BasicJsonType::object_t::const_iterator; + + // an array or object being walked: the container, the array index or + // object iterator of the next child, and the length of the path of the + // container itself + struct frame { - case detail::value_t::array: + frame(const BasicJsonType* container_, object_const_iterator member_, const std::size_t path_length_) noexcept + : container(container_), member(std::move(member_)), path_length(path_length_) + {} + + const BasicJsonType* container; + std::size_t index = 0; + object_const_iterator member; + std::size_t path_length; + }; + + // The containers being flattened are kept on an explicit stack, and + // every child is flattened completely before the next one, so the + // entries come out in the same order as with a recursive walk. The + // path of the value being flattened is kept in one buffer that grows + // and shrinks with the stack, rather than in a new string per level. + std::vector stack; + string_t path = reference_string; + + // flatten `v`, whose path is `path`: primitives and empty containers + // are added to the result right away; other containers get a frame + auto& object = *result.m_data.m_value.object; + auto hint = object.end(); + const auto enter = [&stack, &path, &object, &hint](const BasicJsonType & v) + { + if (v.is_array()) { - if (value.m_data.m_value.array->empty()) + if (v.m_data.m_value.array->empty()) { // flatten empty array as null - result[reference_string] = nullptr; + add_flattened(object, hint, path, nullptr, detail::priority_tag<2> {}); } else { - // iterate array and use index as a reference string - for (std::size_t i = 0; i < value.m_data.m_value.array->size(); ++i) - { - flatten(detail::concat(reference_string, '/', std::to_string(i)), - value.m_data.m_value.array->operator[](i), result); - } + stack.emplace_back(&v, object_const_iterator(), path.size()); } - break; } - - case detail::value_t::object: + else if (v.is_object()) { - if (value.m_data.m_value.object->empty()) + if (v.m_data.m_value.object->empty()) { // flatten empty object as null - result[reference_string] = nullptr; + add_flattened(object, hint, path, nullptr, detail::priority_tag<2> {}); } else { - // iterate object and use keys as reference string - for (const auto& element : *value.m_data.m_value.object) - { - flatten(detail::concat(reference_string, '/', detail::escape(element.first)), element.second, result); - } + stack.emplace_back(&v, v.m_data.m_value.object->begin(), path.size()); } - break; } - - case detail::value_t::null: - case detail::value_t::string: - case detail::value_t::boolean: - case detail::value_t::number_integer: - case detail::value_t::number_unsigned: - case detail::value_t::number_float: - case detail::value_t::binary: - case detail::value_t::discarded: - default: + else { // add a primitive value with its reference string - result[reference_string] = value; - break; + add_flattened(object, hint, path, v, detail::priority_tag<2> {}); + } + }; + + reserve_flattened(object, value, detail::priority_tag<1> {}); + enter(value); + while (!stack.empty()) + { + // the frame is changed through stack.back(): enter() may push a + // frame, which would invalidate a reference to it + const BasicJsonType* const container = stack.back().container; + + // the path of the previous child is replaced by the path of the + // next one; for an array element in one resize + const std::size_t path_length = stack.back().path_length; + + if (container->is_array()) + { + const auto& array = *container->m_data.m_value.array; + const std::size_t i = stack.back().index; + if (i == array.size()) + { + stack.pop_back(); + continue; + } + + // iterate array and use index as a reference string + ++stack.back().index; + std::array digits{}; + std::size_t pos = digits.size(); + std::size_t rest = i; + do + { + digits[--pos] = static_cast('0' + (rest % 10)); + rest /= 10; + } + while (rest != 0); + const std::size_t count = digits.size() - pos; + path.resize(path_length + 1 + count); + char* const out = &path[path_length]; + out[0] = '/'; + std::memcpy(out + 1, digits.data() + pos, count); + enter(array[i]); + } + else + { + const object_const_iterator it = stack.back().member; + if (it == container->m_data.m_value.object->end()) + { + stack.pop_back(); + continue; + } + + // iterate object and use keys as reference string + ++stack.back().member; + path.resize(path_length); + path.push_back('/'); + detail::escape_into(path, it->first); + enter(it->second); } } + + // the entries were added without going through basic_json, so the + // parent pointers (JSON_DIAGNOSTICS) have to be set now + result.set_parents(); } /*! @@ -960,42 +1591,159 @@ class json_pointer { if (JSON_HEDLEY_UNLIKELY(!value.is_object())) { - JSON_THROW(detail::type_error::create(314, "only objects can be unflattened", &value)); + JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_not_object, "only objects can be unflattened", &value)); } BasicJsonType result; - // collect the pointer prefixes that have a reference token 0 among - // their children; the values below them are arrays, all others are - // objects (see array_parents_t) - array_parents_t array_parents; + // check every key, and collect the prefixes that have a reference + // token 0 among their children; the values below them are arrays, + // all others are objects (see array_prefix_set) + array_prefix_set arrays; + const string_t* previous_key = nullptr; for (const auto& element : *value.m_data.m_value.object) { - json_pointer ptr(element.first); - std::vector prefix; - for (auto& reference_token : ptr.reference_tokens) - { - if (reference_token == "0") - { - array_parents.insert(prefix); - } - prefix.push_back(std::move(reference_token)); - } + scan_key(element.first, previous_key, arrays); + previous_key = &element.first; } + // values[i] is the value after the first i reference tokens of the + // previous key, ends[i] the length of that prefix of the key. Keys are + // often sorted, so most of the path to the value of a key is the + // same as for the previous key and need not be looked up again. Only + // the values below the common prefix are dropped, and only they can + // move when a value is added: the ones above are not inside the + // container that grows. + std::vector values(1, &result); + std::vector ends(1, 0); + const string_t* previous = nullptr; + string_t token; + // iterate the JSON object values for (const auto& element : *value.m_data.m_value.object) { if (JSON_HEDLEY_UNLIKELY(!element.second.is_primitive())) { - JSON_THROW(detail::type_error::create(315, "values in object must be primitive", &element.second)); + JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_value_not_primitive, "values in object must be primitive", &element.second)); } + const string_t& key = element.first; + const char* const data = key.data(); + + // the number of reference tokens the key has in common with the + // previous one + std::size_t depth = 0; + if (previous != nullptr) + { + const std::size_t same = common_prefix_length(data, previous->data(), (std::min)(key.size(), previous->size())); + depth = ends.size() - 1; + while (depth > 0 && (ends[depth] > same || (ends[depth] == same && same < key.size() && data[same] != '/'))) + { + --depth; + } + } + values.resize(depth + 1); + ends.resize(depth + 1); + previous = &key; + // Assign the value to the reference pointed to by JSON pointer. Note // that if the JSON pointer is "" (i.e., points to the whole value), - // function get_and_create returns a reference to the result itself. - // An assignment will then create a primitive value. - json_pointer(element.first).get_and_create(result, array_parents) = element.second; + // the value is the result itself. An assignment will then create a + // primitive value. + BasicJsonType* current = values.back(); + for (std::size_t pos = ends.back(); pos < key.size();) + { + // the reference token between the slash at pos and the next + std::size_t end = key.find_first_of('/', pos + 1); + if (end == string_t::npos) + { + end = key.size(); + } + const std::size_t token_size = end - pos - 1; + + // a token that is a plain index of up to 9 digits, which is + // the case for almost all array elements, is not copied + std::size_t index = 0; + bool plain_index = current->type() == detail::value_t::array && token_size != 0 && token_size <= 9 && (token_size == 1 || data[pos + 1] != '0'); + for (std::size_t k = pos + 1; plain_index && k < end; ++k) + { + plain_index = data[k] >= '0' && data[k] <= '9'; + index = (index * 10) + static_cast(data[k] - '0'); + } + if (plain_index) + { + current = ¤t->operator[](static_cast(index)); + values.push_back(current); + ends.push_back(end); + pos = end; + continue; + } + + token.clear(); + token.append(data + pos + 1, token_size); + if (token.find_first_of('~') != string_t::npos) + { + detail::unescape(token); + } + + switch (current->type()) + { + case detail::value_t::null: + { + // a first child 0 makes the value an array without a look-up + if ((token_size == 1 && data[pos + 1] == '0') || arrays.contains(data, pos)) + { + // some reference token below this position is 0, so the + // value is an array + *current = BasicJsonType(detail::value_t::array); + reserve_elements(*current->m_data.m_value.array, 2, detail::priority_tag<1> {}); + current = ¤t->operator[](array_index(token)); + } + else + { + // start a new object otherwise + current = ¤t->operator[](token); + } + break; + } + + case detail::value_t::object: + { + // create an entry in the object + current = &object_member(*current, token); + break; + } + + case detail::value_t::array: + { + // create an entry in the array + current = ¤t->operator[](array_index(token)); + break; + } + + /* + The following code is only reached if there exists a reference + token _and_ the current value is primitive. In this case, we have + an error situation, because primitive values may only occur as + a single value; that is, with an empty list of reference tokens. + */ + case detail::value_t::string: + case detail::value_t::boolean: + case detail::value_t::number_integer: + case detail::value_t::number_unsigned: + case detail::value_t::number_float: + case detail::value_t::binary: + case detail::value_t::discarded: + default: + JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_invalid_value, "invalid value to unflatten", &result)); + } + + values.push_back(current); + ends.push_back(end); + pos = end; + } + + *current = element.second; } return result; @@ -1034,7 +1782,7 @@ class json_pointer = delete; #else { - return *this == json_pointer(rhs); + return equals_string(rhs); } #endif @@ -1118,7 +1866,7 @@ inline bool operator==(const json_pointer& lhs, = delete; #else { - return lhs == json_pointer(rhs); + return lhs.equals_string(rhs); } #endif @@ -1131,7 +1879,7 @@ inline bool operator==(const StringType& lhs, = delete; #else { - return json_pointer(lhs) == rhs; + return rhs.equals_string(lhs); } #endif @@ -1176,4 +1924,18 @@ inline bool operator<(const json_pointer& lhs, } #endif +namespace detail +{ +/// the reference tokens of a json_pointer, for code that resolves pointers +/// without a basic_json value (such as the zero-copy view) +struct json_pointer_access +{ + template + static const std::vector::string_t>& reference_tokens(const json_pointer& ptr) noexcept + { + return ptr.reference_tokens; + } +}; +} // namespace detail + NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/macro_scope.hpp b/include/nlohmann/detail/macro_scope.hpp index 1a847e1a9..061c48853 100644 --- a/include/nlohmann/detail/macro_scope.hpp +++ b/include/nlohmann/detail/macro_scope.hpp @@ -187,6 +187,22 @@ #endif #endif +// the byte order of the target where the compiler tells it (std::endian is +// C++20 and cannot select code in #if): JSON_LITTLE_ENDIAN_TARGET or +// JSON_BIG_ENDIAN_TARGET is 1, or both are 0 where it is unknown (code then +// handles the bytes one at a time); MSVC and x86 targets are little-endian +#if defined(_MSC_VER) || defined(__x86_64__) || defined(__i386__) \ + || (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) + #define JSON_LITTLE_ENDIAN_TARGET 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) + #define JSON_BIG_ENDIAN_TARGET 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) +#elif defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + #define JSON_LITTLE_ENDIAN_TARGET 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) + #define JSON_BIG_ENDIAN_TARGET 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) +#else + #define JSON_LITTLE_ENDIAN_TARGET 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) + #define JSON_BIG_ENDIAN_TARGET 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) +#endif + #ifdef JSON_HAS_CPP_17 #define JSON_INLINE_VARIABLE inline #else @@ -312,7 +328,7 @@ return ej_pair.first == e; \ }); \ if (it != std::end(m)) j = it->second; \ - else ::nlohmann::detail::templated_json_throw(nlohmann::detail::out_of_range::create(410,"enum value out of range for " #ENUM_TYPE, nullptr)); \ + else ::nlohmann::detail::templated_json_throw(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range,"enum value out of range for " #ENUM_TYPE, nullptr)); \ } \ template \ inline void from_json(const BasicJsonType& j, ENUM_TYPE& e) \ @@ -327,7 +343,7 @@ return ej_pair.second == j; \ }); \ if (it != std::end(m)) e = it->first; \ - else ::nlohmann::detail::templated_json_throw(nlohmann::detail::out_of_range::create(410, nlohmann::detail::concat("enum value out of range for " #ENUM_TYPE ": ", j.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace)), &j)); \ + else ::nlohmann::detail::templated_json_throw(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 ": ", j.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace)), &j)); \ } // Ugly macros to avoid uglier copy-paste when specializing basic_json. They diff --git a/include/nlohmann/detail/macro_unscope.hpp b/include/nlohmann/detail/macro_unscope.hpp index 494cba0d8..d3cdb0d3f 100644 --- a/include/nlohmann/detail/macro_unscope.hpp +++ b/include/nlohmann/detail/macro_unscope.hpp @@ -21,6 +21,18 @@ #undef JSON_NO_UNIQUE_ADDRESS #undef JSON_DISABLE_ENUM_SERIALIZATION #undef JSON_DISABLE_TUPLE_REFERENCE_CONVERSION +#undef JSON_UNLOCKED_FILE_IO +#undef JSON_DTOA_SSE2 +#undef JSON_DTOA_NEON +#undef JSON_LITTLE_ENDIAN_TARGET +#undef JSON_BIG_ENDIAN_TARGET +#undef JSON_SCAN_NEON +#undef JSON_SCAN_SSE2 +#undef JSON_SCAN_SSSE3 +#undef JSON_SCAN_SSSE3_DISPATCH +#undef JSON_SCAN_SSSE3_TARGET +#undef JSON_SCAN_VECTOR +#undef JSON_SCAN_VECTOR_UTF8 #ifndef JSON_TEST_KEEP_MACROS #undef JSON_CATCH @@ -45,6 +57,8 @@ #undef JSON_STRICT_BINARY_UTF8 #undef JSON_DELETE_DEPRECATED_FUNCTIONS #undef JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS + #undef JSON_DISABLE_STREAM_BULK_SCAN + #undef JSON_DISABLE_UNLOCKED_FILE_IO #endif #include diff --git a/include/nlohmann/detail/meta/cpp_future.hpp b/include/nlohmann/detail/meta/cpp_future.hpp index 3e012b341..6faa79c24 100644 --- a/include/nlohmann/detail/meta/cpp_future.hpp +++ b/include/nlohmann/detail/meta/cpp_future.hpp @@ -10,17 +10,26 @@ #pragma once #include // size_t -#include // conditional, enable_if, false_type, integral_constant, is_constructible, is_integral, is_same, remove_cv, remove_reference, true_type +#include // conditional, enable_if, false_type, integral_constant, is_arithmetic, is_constructible, is_integral, is_same, remove_cv, remove_reference, true_type #include // index_sequence, make_index_sequence, index_sequence_for #include +#ifdef JSON_HAS_CPP_17 + #include // clamp +#endif + NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail { -template -using uncvref_t = typename std::remove_cv::type>::type; +#ifdef __cpp_lib_remove_cvref + template + using uncvref_t = std::remove_cvref_t; +#else + template + using uncvref_t = typename std::remove_cv::type>::type; +#endif #ifdef JSON_HAS_CPP_14 @@ -160,5 +169,19 @@ struct static_const constexpr T static_const::value; #endif +/// @a v bounded to [@a lo, @a hi] (requires !(hi < lo)); like std::clamp, +/// but by value, so that static constexpr members can be passed without an +/// out-of-line definition before C++17 +template::value, int> = 0> +constexpr T clamp(T v, T lo, T hi) noexcept +{ +#ifdef __cpp_lib_clamp + return std::clamp(v, lo, hi); +#else + // NOLINTNEXTLINE(readability-avoid-nested-conditional-operator): a constant expression of C++11 + return v < lo ? lo : (hi < v ? hi : v); +#endif +} + } // namespace detail NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/meta/type_traits.hpp b/include/nlohmann/detail/meta/type_traits.hpp index 4c90c68a3..387691b6b 100644 --- a/include/nlohmann/detail/meta/type_traits.hpp +++ b/include/nlohmann/detail/meta/type_traits.hpp @@ -9,9 +9,11 @@ #pragma once #include // numeric_limits +#include // map, multimap #include // char_traits #include // tuple #include // false_type, is_constructible, is_integral, is_same, true_type +#include // unordered_map, unordered_multimap #include // declval #include // vector #if defined(__cpp_lib_byte) && __cpp_lib_byte >= 201603L @@ -172,6 +174,10 @@ struct has_to_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> template using detect_key_compare = typename T::key_compare; +// detects whether two values of type T can be compared with operator== +template +using detect_equal_comparable = decltype(static_cast(std::declval() == std::declval())); + // obtains the actual object key comparator: object_t::key_compare if the // object type defines it, and default_object_comparator_t otherwise // @@ -307,6 +313,14 @@ struct char_traits : std::char_traits // is_ functions // /////////////////// +#ifdef __cpp_lib_logical_traits + +using std::conjunction; +using std::disjunction; +using std::negation; + +#else + // https://en.cppreference.com/w/cpp/types/conjunction template struct conjunction : std::true_type { }; template struct conjunction : B { }; @@ -324,6 +338,8 @@ struct disjunction // https://en.cppreference.com/w/cpp/types/negation template struct negation : std::integral_constant < bool, !B::value > { }; +#endif + // Reimplementation of is_constructible and is_default_constructible, due to them being broken for // std::pair and std::tuple until LWG 2367 fix (see https://cplusplus.github.io/LWG/lwg-defects.html#2367). // This causes compile errors in e.g., Clang 3.5 or GCC 4.9. @@ -679,6 +695,22 @@ struct is_compatible_integer_type : is_compatible_integer_type_impl {}; +// whether to_json() stores an integer of type CompatibleNumberIntegerType as +// number_integer +template +struct is_compatible_number_integer_type + : is_compatible_integer_type {}; + +// if number_unsigned_t is the same type as number_integer_t, unsigned integers +// are stored as number_integer as well (#2573) +template +struct is_compatible_number_integer_type, + std::is_integral, + std::is_unsigned, + negation>>::value>> + : std::is_constructible {}; + template struct is_compatible_type_impl: std::false_type {}; @@ -893,6 +925,31 @@ struct has_capacity : std::integral_constant struct is_ordered_map : has_capacity {}; +// whether T is a specialization of nlohmann::ordered_map, which only ever +// appends members, so they can only move when its capacity changes +template +struct is_nlohmann_ordered_map : std::false_type {}; + +template +struct is_nlohmann_ordered_map> : std::true_type {}; + +// whether adding or erasing elements never moves the other elements of the +// container T; only node-based standard containers are known to guarantee this +template +struct has_stable_elements : std::false_type {}; + +template +struct has_stable_elements> : std::true_type {}; + +template +struct has_stable_elements> : std::true_type {}; + +template +struct has_stable_elements> : std::true_type {}; + +template +struct has_stable_elements> : std::true_type {}; + // to avoid useless casts (see https://github.com/nlohmann/json/issues/2893#issuecomment-889152324) template < typename T, typename U, enable_if_t < !std::is_same::value, int > = 0 > T conditional_static_cast(U value) @@ -1013,8 +1070,31 @@ constexpr bool value_in_range_of(T val) return value_in_range_of_impl1::test(val); } -template -using bool_constant = std::integral_constant; +#ifdef __cpp_lib_bool_constant + using std::bool_constant; +#else + template + using bool_constant = std::integral_constant; +#endif + +// Stands in for number_unsigned_t in overload sets if it is the same type as +// number_integer_t, so that the unsigned overloads do not collide with the +// integer ones. Never constructed. Such a basic_json stores every integer as +// number_integer (#2573). +struct same_as_number_integer_type {}; + +// NumberUnsignedType, or same_as_number_integer_type if it is the same type as +// NumberIntegerType; basic_json uses this one, as it cannot name its own member +// types through an incomplete basic_json (MSVC reports a recursive alias, and +// old GCC versions crash) +template +using number_unsigned_overload_type = typename std::conditional::value, + same_as_number_integer_type, NumberUnsignedType>::type; + +// number_unsigned_t of BasicJsonType, or same_as_number_integer_type if it is +// the same type as number_integer_t +template +using number_unsigned_overload_t = number_unsigned_overload_type; /////////////////////////////////////////////////////////////////////////////// // is_c_string diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index 1cb64f08d..7ff828074 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -8,7 +8,7 @@ #pragma once -#include // reverse +#include // max, min, reverse #include // array #include // isnan, isinf #include // uint8_t, uint16_t, uint32_t, uint64_t @@ -19,11 +19,9 @@ #include // move #include // vector -#ifdef _MSC_VER - #include // _byteswap_ushort, _byteswap_ulong, _byteswap_uint64 -#endif - +#include #include +#include #include #include #include @@ -78,6 +76,21 @@ std::size_t binary_reserve_hint(const BasicJsonType& j) return 0; } +/*! +@brief the capacity to reserve for the result of a binary serialization into a + std::vector + +The hint of @ref binary_reserve_hint, but not less than the 64 characters that +output_vector_sink grows an empty vector to on its first write anyway. A small +document then costs one allocation, instead of an allocation of a few bytes that +the first write replaces with one of 64. +*/ +template +std::size_t binary_initial_capacity(const BasicJsonType& j) +{ + return (std::max)(binary_reserve_hint(j), min_vector_sink_capacity); +} + /*! @brief serialization to BJData, BON8, BSON, CBOR, MessagePack, and UBJSON values */ @@ -85,6 +98,12 @@ template::value, + const string_t&, string_t >::type; using binary_t = typename BasicJsonType::binary_t; using number_float_t = typename BasicJsonType::number_float_t; @@ -120,9 +139,22 @@ class binary_writer template < typename SinkType = OutputSinkType, typename std::enable_if < std::is_constructible>::value, int >::type = 0 > explicit binary_writer(output_adapter_t adapter, const error_handler_t error_handler_ = binary_writer_default_error_handler()) - : oa(SinkType(std::move(adapter))), error_handler(error_handler_) + : oa(std::move(adapter)), error_handler(error_handler_) {} + /*! + @brief pass on everything written so far to the output adapter + + A writer over an output_adapter_sink buffers what it writes; this has to + be called after the last write, or the buffered end is only passed on, and + an error of the adapter lost, when the writer is destroyed. A writer over + an output_vector_sink writes straight into the vector. + */ + void flush() + { + oa.flush(); + } + /*! @param[in] j JSON value to serialize @throw type_error.316 if a string value or an object key is not valid @@ -151,7 +183,7 @@ class binary_writer case value_t::discarded: default: { - JSON_THROW(type_error::create(317, concat("to serialize to BSON, top-level type must be object, but is ", j.type_name()), &j)); + JSON_THROW(type_error::create(exception_id::type_not_serializable, concat("to serialize to BSON, top-level type must be object, but is ", j.type_name()), &j)); } } } @@ -198,24 +230,12 @@ class binary_writer } case value_t::number_integer: - { - if (j.m_data.m_value.number_integer >= 0) - { - // CBOR does not differentiate between positive signed - // integers and unsigned integers - write_cbor_head(0x00, static_cast(j.m_data.m_value.number_integer)); - } - else - { - // a negative integer n is encoded as -1 - n - write_cbor_head(0x20, static_cast(-1 - j.m_data.m_value.number_integer)); - } - break; - } - case value_t::number_unsigned: { - write_cbor_head(0x00, j.m_data.m_value.number_unsigned); + // one case for both, so that an array mixing negative + // (number_integer) and non-negative (number_unsigned) values + // does not mispredict the switch + write_cbor_integer(j, std::integral_constant()); break; } @@ -244,28 +264,13 @@ class binary_writer case value_t::string: { - string_t storage; - const string_t& value = sanitize_utf8_for_write(*j.m_data.m_value.string, j, storage); - - // step 1: write control byte and the string length - write_cbor_head(0x60, value.size()); - - // step 2: write the string - oa.write_characters( - reinterpret_cast(value.data()), - value.size()); + write_cbor_string(*j.m_data.m_value.string, j); break; } case value_t::array: { - // step 1: write control byte and the array size - write_cbor_head(0x80, j.m_data.m_value.array->size()); - - for (const auto& el : *j.m_data.m_value.array) - { - write_cbor(el, depth + 1); - } + write_cbor_array(j, depth); break; } @@ -307,40 +312,20 @@ class binary_writer write_cbor_head(0x40, N); // step 2: write each element - oa.write_characters( - reinterpret_cast(j.m_data.m_value.binary->data()), - N); + write_bytes(j.m_data.m_value.binary->data(), N); break; } case value_t::object: { - // step 1: write control byte and the object size - write_cbor_head(0xA0, j.m_data.m_value.object->size()); - - for (const auto& el : *j.m_data.m_value.object) - { - // el.first is checked here, against the object as - // diagnostics context, because write_cbor(el.first) - // converts it to a temporary basic_json that would be - // used as the context instead; for error_handler_t::keep - // and ::replace/::ignore the recursive write_cbor(el.first) - // call below handles the key like any other string, so no - // separate check is needed here for those - if (error_handler == error_handler_t::strict) - { - check_utf8(el.first, j); - } - write_cbor(el.first); - write_cbor(el.second, depth + 1); - } + write_cbor_object(j, depth); break; } case value_t::discarded: default: - throw_on_discarded(j, "CBOR"); + throw_on_discarded(j, input_format_t::cbor); } } @@ -354,7 +339,7 @@ class binary_writer { if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(length))) { - JSON_THROW(out_of_range::create(412, concat("MessagePack length ", std::to_string(length), " exceeds maximum of ", std::to_string((std::numeric_limits::max)())), &j)); + throw_msgpack_length_too_large(length, j); } static_cast(j); @@ -432,110 +417,30 @@ class binary_writer } case value_t::number_integer: - { - if (j.m_data.m_value.number_integer >= 0) - { - // MessagePack does not differentiate between positive - // signed integers and unsigned integers. - write_msgpack_unsigned(static_cast(j.m_data.m_value.number_integer)); - } - else - { - if (j.m_data.m_value.number_integer >= -32) - { - // negative fixnum - write_number(static_cast(j.m_data.m_value.number_integer)); - } - else if (j.m_data.m_value.number_integer >= (std::numeric_limits::min)() && - j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) - { - // int 8 - oa.write_character(to_char_type(0xD0)); - write_number(static_cast(j.m_data.m_value.number_integer)); - } - else if (j.m_data.m_value.number_integer >= (std::numeric_limits::min)() && - j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) - { - // int 16 - oa.write_character(to_char_type(0xD1)); - write_number(static_cast(j.m_data.m_value.number_integer)); - } - else if (j.m_data.m_value.number_integer >= (std::numeric_limits::min)() && - j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) - { - // int 32 - oa.write_character(to_char_type(0xD2)); - write_number(static_cast(j.m_data.m_value.number_integer)); - } - else - { - // int 64 - oa.write_character(to_char_type(0xD3)); - write_number(static_cast(j.m_data.m_value.number_integer)); - } - } - break; - } - case value_t::number_unsigned: { - write_msgpack_unsigned(static_cast(j.m_data.m_value.number_unsigned)); + // one case for both, so that an array mixing negative + // (number_integer) and non-negative (number_unsigned) values + // does not mispredict the switch + write_msgpack_integer(j, std::integral_constant()); break; } case value_t::number_float: { - write_compact_float(j.m_data.m_value.number_float, to_char_type(0xCA), to_char_type(0xCB)); + write_msgpack_float(j.m_data.m_value.number_float); break; } case value_t::string: { - string_t storage; - const string_t& value = sanitize_utf8_for_write(*j.m_data.m_value.string, j, storage); - - // step 1: write control byte and the string length - const auto N = to_msgpack_length(value.size(), j); - if (N <= 31) - { - // fixstr - write_number(static_cast(0xA0 | N)); - } - else if (N <= (std::numeric_limits::max)()) - { - // str 8 - oa.write_character(to_char_type(0xD9)); - write_number(static_cast(N)); - } - else if (N <= (std::numeric_limits::max)()) - { - // str 16 - oa.write_character(to_char_type(0xDA)); - write_number(static_cast(N)); - } - else - { - // str 32 - oa.write_character(to_char_type(0xDB)); - write_number(static_cast(N)); - } - - // step 2: write the string - oa.write_characters( - reinterpret_cast(value.data()), - value.size()); + write_msgpack_string(*j.m_data.m_value.string, j); break; } case value_t::array: { - // step 1: write control byte and the array size - write_msgpack_array_prefix(j.m_data.m_value.array->size(), j); - - for (const auto& el : *j.m_data.m_value.array) - { - write_msgpack(el, depth + 1); - } + write_msgpack_array(j, depth); break; } @@ -613,16 +518,14 @@ class binary_writer { if (JSON_HEDLEY_UNLIKELY(j.m_data.m_value.binary->subtype() > (std::numeric_limits::max)())) { - JSON_THROW(out_of_range::create(415, concat("subtype ", std::to_string(j.m_data.m_value.binary->subtype()), " is too large for the MessagePack ext type (max 255)"), &j)); + throw_subtype_out_of_range(j.m_data.m_value.binary->subtype(), "the MessagePack ext type", j); } write_number(static_cast(j.m_data.m_value.binary->subtype())); } // step 2: write the byte string - oa.write_characters( - reinterpret_cast(j.m_data.m_value.binary->data()), - N); + write_bytes(j.m_data.m_value.binary->data(), N); break; } @@ -630,26 +533,13 @@ class binary_writer case value_t::object: { // step 1: write control byte and the object size - write_msgpack_object_prefix(j.m_data.m_value.object->size(), j); - - for (const auto& el : *j.m_data.m_value.object) - { - // as in write_cbor, el.first is checked here against the - // object as diagnostics context; the recursive call below - // handles keep/replace/ignore like any other string - if (error_handler == error_handler_t::strict) - { - check_utf8(el.first, j); - } - write_msgpack(el.first); - write_msgpack(el.second, depth + 1); - } + write_msgpack_object(j, depth); break; } case value_t::discarded: default: - throw_on_discarded(j, "MessagePack"); + throw_on_discarded(j, input_format_t::msgpack); } } @@ -705,14 +595,16 @@ class binary_writer } case value_t::number_integer: - { - write_number_with_ubjson_prefix(j.m_data.m_value.number_integer, add_prefix, use_bjdata); - break; - } - case value_t::number_unsigned: { - write_number_with_ubjson_prefix(j.m_data.m_value.number_unsigned, add_prefix, use_bjdata); + if (j.m_data.m_type == value_t::number_integer) + { + write_number_with_ubjson_prefix(j.m_data.m_value.number_integer, add_prefix, use_bjdata); + } + else + { + write_number_with_ubjson_prefix(j.m_data.m_value.number_unsigned, add_prefix, use_bjdata); + } break; } @@ -724,28 +616,28 @@ class binary_writer case value_t::string: { - string_t storage; - const string_t& value = sanitize_utf8_for_write(*j.m_data.m_value.string, j, storage); - - if (add_prefix) - { - oa.write_character(to_char_type('S')); - } - write_number_with_ubjson_prefix(value.size(), true, use_bjdata); - oa.write_characters( - reinterpret_cast(value.data()), - value.size()); + write_ubjson_text(*j.m_data.m_value.string, j, add_prefix, use_bjdata); break; } case value_t::array: { bool prefix_required = true; - const bool write_closer = write_ubjson_start_array(j, use_count, use_type, add_prefix, use_bjdata, prefix_required); + const bool write_closer = write_ubjson_start_container(j, use_count, use_type, add_prefix, use_bjdata, prefix_required); - for (const auto& el : *j.m_data.m_value.array) { - write_ubjson(el, use_count, use_type, prefix_required, use_bjdata, bjdata_version, depth + 1); + const auto& array = *j.m_data.m_value.array; + if (starts_with_ubjson_number(array, use_bjdata, std::integral_constant())) + { + write_ubjson_array_elements(array, use_count, use_type, prefix_required, use_bjdata, bjdata_version, depth); + } + else + { + for (const auto& el : array) + { + write_ubjson(el, use_count, use_type, prefix_required, use_bjdata, bjdata_version, depth + 1); + } + } } if (write_closer) @@ -767,7 +659,7 @@ class binary_writer { if (!use_count) { - JSON_THROW(other_error::create(502, "use_type requires use_size = true", &j)); + throw_size_marker_required(j); } oa.write_character(to_char_type('$')); oa.write_character(bjdata_draft3 ? 'B' : 'U'); @@ -781,19 +673,11 @@ class binary_writer if (use_type) { - oa.write_characters( - reinterpret_cast(j.m_data.m_value.binary->data()), - j.m_data.m_value.binary->size()); + write_bytes(j.m_data.m_value.binary->data(), j.m_data.m_value.binary->size()); } else { - for (size_t i = 0; i < j.m_data.m_value.binary->size(); ++i) - { - oa.write_character(to_char_type(bjdata_draft3 ? 'B' : 'U')); - // the cast is needed for binary types whose value type - // is not an integer (e.g., std::byte) - oa.write_character(to_char_type(static_cast(j.m_data.m_value.binary->data()[i]))); - } + write_ubjson_binary_bytes(*j.m_data.m_value.binary, bjdata_draft3); } if (!use_count) @@ -815,18 +699,9 @@ class binary_writer } bool prefix_required = true; - const bool write_closer = write_ubjson_start_object(j, use_count, use_type, add_prefix, use_bjdata, prefix_required); + const bool write_closer = write_ubjson_start_container(j, use_count, use_type, add_prefix, use_bjdata, prefix_required); - for (const auto& el : *j.m_data.m_value.object) - { - string_t storage; - const string_t& key = sanitize_utf8_for_write(el.first, j, storage); - write_number_with_ubjson_prefix(key.size(), true, use_bjdata); - oa.write_characters( - reinterpret_cast(key.data()), - key.size()); - write_ubjson(el.second, use_count, use_type, prefix_required, use_bjdata, bjdata_version, depth + 1); - } + write_ubjson_members(j, use_count, use_type, prefix_required, use_bjdata, bjdata_version, depth); if (write_closer) { @@ -838,12 +713,14 @@ class binary_writer case value_t::discarded: default: - throw_on_discarded(j, use_bjdata ? "BJData" : "UBJSON"); + throw_on_discarded(j, use_bjdata ? input_format_t::bjdata : input_format_t::ubjson); } } /*! @param[in] j JSON value to serialize + + @throw type_error.321 if @a j or a value nested in it is discarded */ void write_bon8(const BasicJsonType& j) { @@ -862,60 +739,240 @@ class binary_writer @brief throws because @a j is discarded and cannot be serialized @throw type_error.321 always */ - JSON_HEDLEY_NO_RETURN static void throw_on_discarded(const BasicJsonType& j, const char* format_name) + JSON_HEDLEY_NO_RETURN static void throw_on_discarded(const BasicJsonType& j, const input_format_t format) { static_cast(j); // unused when JSON_NOEXCEPTION is defined - static_cast(format_name); - JSON_THROW(type_error::create(321, concat("cannot serialize discarded value to ", format_name), &j)); + static_cast(format); + JSON_THROW(type_error::create(exception_id::discarded_value_used, concat("cannot serialize discarded value to ", input_format_name(format)), &j)); } + /// how the range of integer type @a NumberType relates to that of @a TargetType + template + struct integer_range + { + /// whether NumberType can hold values above the maximum of TargetType + static constexpr bool may_exceed_max = std::numeric_limits::digits > std::numeric_limits::digits; + /// whether NumberType can hold values below the minimum of TargetType + static constexpr bool may_fall_below_min = std::numeric_limits::is_signed + && (!std::numeric_limits::is_signed || may_exceed_max); + }; + + /*! + @brief whether integer @a n does not exceed the maximum of @a TargetType + + The binary formats store integers in at most 64 bits, but number_integer_t + and number_unsigned_t may be wider (e.g., __int128). The range of the + integer is taken from std::numeric_limits, so a number type whose range + fits into @a TargetType is never checked: the function is then constant + true, and the default int64_t/uint64_t types pay nothing. + */ + template + static constexpr bool integer_fits_max(const NumberType n) noexcept + { + return integer_fits_max(n, std::integral_constant::may_exceed_max>()); + } + + template + static constexpr bool integer_fits_max(const NumberType /*unused*/, std::false_type /*may_exceed*/) noexcept + { + return true; + } + + template + static constexpr bool integer_fits_max(const NumberType n, std::true_type /*may_exceed*/) noexcept + { + // NumberType has more digits than TargetType, so it can hold its maximum + return n <= static_cast((std::numeric_limits::max)()); + } + + /*! + @brief whether integer @a n is not below the minimum of @a TargetType; + constant true if NumberType cannot hold such a value + */ + template + static constexpr bool integer_fits_min(const NumberType n) noexcept + { + return integer_fits_min(n, std::integral_constant::may_fall_below_min>()); + } + + template + static constexpr bool integer_fits_min(const NumberType /*unused*/, std::false_type /*may_fall_below*/) noexcept + { + return true; + } + + template + static constexpr bool integer_fits_min(const NumberType n, std::true_type /*may_fall_below*/) noexcept + { + // NumberType is signed and either TargetType is unsigned (minimum 0) + // or NumberType has more digits, so it can hold the minimum + return n >= static_cast((std::numeric_limits::min)()); + } + + /*! + @brief whether integer @a n lies in the range of @a TargetType + */ + template + static constexpr bool integer_fits(const NumberType n) noexcept + { + return integer_fits_min(n) && integer_fits_max(n); + } + + /*! + @brief the decimal digits of the non-negative integer @a n + */ + template + static std::string integer_to_string(NumberType n, std::false_type /*is_signed*/) + { + std::string result; + do + { + result.push_back(static_cast('0' + (n % 10))); + n = static_cast(n / 10); + } + while (n != 0); + std::reverse(result.begin(), result.end()); + return result; + } + + /*! + @brief the decimal digits of the integer @a n, with a leading '-' if it + is negative + */ + template + static std::string integer_to_string(NumberType n, std::true_type /*is_signed*/) + { + // the remainder of a negative n is not positive; each digit is + // negated on its own, because negating the minimum of NumberType + // as a whole would overflow + const bool negative = n < 0; + std::string result; + do + { + const auto remainder = n % 10; + result.push_back(static_cast('0' + (remainder < 0 ? -remainder : remainder))); + n = static_cast(n / 10); + } + while (n != 0); + if (negative) + { + result.push_back('-'); + } + std::reverse(result.begin(), result.end()); + return result; + } + + /*! + @brief throws because the integer @a j is too large for @a format + @throw out_of_range.407 always + */ + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_integer_out_of_range(const BasicJsonType& j, const input_format_t format, const char* range) + { + // neither std::to_string(), which has no overload for integer types + // wider than 64 bits, nor dump(), which would also instantiate the + // serializer for objects and fail for key types it cannot write + using integer_limits = std::numeric_limits; + using unsigned_limits = std::numeric_limits; + const std::string number = j.is_number_unsigned() + ? integer_to_string(j.m_data.m_value.number_unsigned, std::integral_constant()) + : integer_to_string(j.m_data.m_value.number_integer, std::integral_constant()); + static_cast(number); // unused when JSON_NOEXCEPTION is defined + static_cast(format); + static_cast(range); + JSON_THROW(out_of_range::create(exception_id::integer_too_large, concat("integer number ", number, " cannot be represented by ", input_format_name(format), " as it does not fit ", range), &j)); + } + + // The exceptions below are thrown from functions of their own, out of line: + // the code that builds a message (concat, std::to_string) would otherwise + // make the frame of the function that checks, which most calls never throw. + + /// @throw out_of_range.412 always + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_msgpack_length_too_large(const std::size_t length, const BasicJsonType& j) + { + static_cast(length); // unused when JSON_NOEXCEPTION is defined + static_cast(j); + JSON_THROW(out_of_range::create(exception_id::length_too_large, concat("MessagePack length ", std::to_string(length), " exceeds maximum of ", std::to_string((std::numeric_limits::max)())), &j)); + } + + /// @throw out_of_range.412 always + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_bson_length_too_large(const std::size_t size) + { + static_cast(size); // unused when JSON_NOEXCEPTION is defined + JSON_THROW(out_of_range::create(exception_id::length_too_large, concat("BSON length ", std::to_string(size), " exceeds maximum of ", std::to_string((std::numeric_limits::max)())), nullptr)); + } + + /// @throw out_of_range.415 always + template + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_subtype_out_of_range(const SubtypeType subtype, const char* what, const BasicJsonType& j) + { + static_cast(subtype); // unused when JSON_NOEXCEPTION is defined + static_cast(what); + static_cast(j); + JSON_THROW(out_of_range::create(exception_id::subtype_out_of_range, concat("subtype ", std::to_string(subtype), " is too large for ", what, " (max 255)"), &j)); + } + + /// @throw other_error.407 always + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_size_marker_required(const BasicJsonType& j) + { + static_cast(j); // unused when JSON_NOEXCEPTION is defined + JSON_THROW(other_error::create(exception_id::size_marker_required, "use_type requires use_size = true", &j)); + } + + /// @brief write @a size bytes starting at @a data to the output adapter + JSON_HEDLEY_ALWAYS_INLINE + void write_bytes(const void* data, const std::size_t size) + { + oa.write_characters(static_cast(data), size); + } + + /// @brief write the MessagePack prefix of an array or object with @a N + /// elements: the fix marker @a fix_marker | n, or @a marker_16 or + /// @a marker_32 followed by the length + JSON_HEDLEY_ALWAYS_INLINE + void write_msgpack_container_prefix(const std::size_t N, const BasicJsonType& j, + const std::uint8_t fix_marker, const std::uint8_t marker_16, const std::uint8_t marker_32) + { + const auto n = to_msgpack_length(N, j); + if (n <= 15) + { + // fixarray, fixmap + oa.write_character(to_char_type(static_cast(fix_marker | n))); + } + else if (n <= (std::numeric_limits::max)()) + { + // array 16, map 16 + oa.write_character(to_char_type(marker_16)); + write_number(static_cast(n)); + } + else + { + // array 32, map 32 + oa.write_character(to_char_type(marker_32)); + write_number(static_cast(n)); + } + } + + JSON_HEDLEY_ALWAYS_INLINE void write_msgpack_array_prefix(const std::size_t N, const BasicJsonType& j) { - const auto n = to_msgpack_length(N, j); - if (n <= 15) - { - // fixarray - write_number(static_cast(0x90 | n)); - } - else if (n <= (std::numeric_limits::max)()) - { - // array 16 - oa.write_character(to_char_type(0xDC)); - write_number(static_cast(n)); - } - else - { - // array 32 - oa.write_character(to_char_type(0xDD)); - write_number(static_cast(n)); - } + write_msgpack_container_prefix(N, j, 0x90, 0xDC, 0xDD); } + JSON_HEDLEY_ALWAYS_INLINE void write_msgpack_object_prefix(const std::size_t N, const BasicJsonType& j) { - const auto n = to_msgpack_length(N, j); - if (n <= 15) - { - // fixmap - write_number(static_cast(0x80 | (n & 0xF))); - } - else if (n <= (std::numeric_limits::max)()) - { - // map 16 - oa.write_character(to_char_type(0xDE)); - write_number(static_cast(n)); - } - else - { - // map 32 - oa.write_character(to_char_type(0xDF)); - write_number(static_cast(n)); - } + write_msgpack_container_prefix(N, j, 0x80, 0xDE, 0xDF); } - /// @brief a CBOR or MessagePack array or object whose elements - /// @ref write_cbor_iterative or @ref write_msgpack_iterative is - /// still writing + /// @brief a CBOR, MessagePack, BON8, or (as the base of @ref ubjson_frame) + /// UBJSON array or object whose elements @ref write_container_iterative, + /// @ref write_bon8_iterative, or @ref write_ubjson_iterative is still + /// writing struct binary_container_frame { explicit binary_container_frame(const BasicJsonType* value_) noexcept @@ -951,19 +1008,82 @@ class binary_writer typename BasicJsonType::array_t::const_iterator array_it{}; }; + /// @brief the CBOR specifics of @ref write_container_iterative + struct cbor_walk + { + static void array_head(binary_writer& w, const BasicJsonType& j) + { + w.write_cbor_head(0x80, j.m_data.m_value.array->size()); + } + + static void object_head(binary_writer& w, const BasicJsonType& j) + { + w.write_cbor_head(0xA0, j.m_data.m_value.object->size()); + } + + static void scalar(binary_writer& w, const BasicJsonType& j) + { + w.write_cbor(j); + } + + static void string(binary_writer& w, const string_t& key, const BasicJsonType& context) + { + w.write_cbor_string(key, context); + } + + template + static void key(binary_writer& w, const KeyType& k, const BasicJsonType& object) + { + w.write_cbor_key(k, object); + } + }; + + /// @brief the MessagePack specifics of @ref write_container_iterative + struct msgpack_walk + { + static void array_head(binary_writer& w, const BasicJsonType& j) + { + w.write_msgpack_array_prefix(j.m_data.m_value.array->size(), j); + } + + static void object_head(binary_writer& w, const BasicJsonType& j) + { + w.write_msgpack_object_prefix(j.m_data.m_value.object->size(), j); + } + + static void scalar(binary_writer& w, const BasicJsonType& j) + { + w.write_msgpack(j); + } + + static void string(binary_writer& w, const string_t& key, const BasicJsonType& context) + { + w.write_msgpack_string(key, context); + } + + template + static void key(binary_writer& w, const KeyType& k, const BasicJsonType& object) + { + w.write_msgpack_key(k, object); + } + }; + /*! - @brief write @a j with @ref write_cbor, or write its header and push a - frame for @ref write_cbor_iterative to continue with its elements + @brief write @a j with @a Walk::scalar, or write its header and push a + frame for @ref write_container_iterative to continue with its + elements A scalar, and an empty array or object, are written out in full: there is - nothing below them for @ref write_cbor_iterative to come back to, so + nothing below them for @ref write_container_iterative to come back to, so nothing is pushed for them. */ - void write_cbor_value_or_push(const BasicJsonType& j, std::vector& stack) + template + JSON_HEDLEY_ALWAYS_INLINE + void write_container_value_or_push(const BasicJsonType& j, std::vector& stack) { if (j.is_array()) { - write_cbor_head(0x80, j.m_data.m_value.array->size()); + Walk::array_head(*this, j); if (!j.m_data.m_value.array->empty()) { stack.emplace_back(&j); @@ -973,7 +1093,7 @@ class binary_writer if (j.is_object()) { - write_cbor_head(0xA0, j.m_data.m_value.object->size()); + Walk::object_head(*this, j); if (!j.m_data.m_value.object->empty()) { stack.emplace_back(&j); @@ -981,22 +1101,26 @@ class binary_writer return; } - write_cbor(j); + Walk::scalar(*this, j); } /*! @brief write out @a root and everything below it without the call stack - Emits the same bytes as @ref write_cbor, keeping the containers it has + Emits the same bytes as the recursive writer, keeping the containers it has entered on an explicit stack instead of descending into them. Only reached for values nested deeper than @ref recursion_depth_limit, which is why it - is not written for speed. + is not written for speed. Shared by CBOR and MessagePack, which differ only + in the @a Walk policy (@ref cbor_walk, @ref msgpack_walk); BON8 has its own + walker, as it tracks an open string run and closes a container with a + marker. */ - void write_cbor_iterative(const BasicJsonType& root) + template + void write_container_iterative(const BasicJsonType& root) { - // only a container with elements is ever pushed; see write_cbor_value_or_push + // only a container with elements is ever pushed; see write_container_value_or_push std::vector stack; - write_cbor_value_or_push(root, stack); + write_container_value_or_push(root, stack); while (!stack.empty()) { @@ -1014,7 +1138,7 @@ class binary_writer // read the child before pushing: entering it can move every frame const BasicJsonType* child = &(*current.array_it); ++stack.back().array_it; - write_cbor_value_or_push(*child, stack); + write_container_value_or_push(*child, stack); } else { @@ -1025,117 +1149,54 @@ class binary_writer continue; } - // el.first is checked here, against the object as diagnostics - // context, like the matching check in write_cbor's object case - if (error_handler == error_handler_t::strict) - { - check_utf8(current.object_it->first, *current.value); - } - write_cbor(current.object_it->first); + // the key is written directly (not via a temporary basic_json), + // with the object as diagnostics context, as in the recursive writer + Walk::key(*this, current.object_it->first, *current.value); const BasicJsonType* child = &(current.object_it->second); ++stack.back().object_it; - write_cbor_value_or_push(*child, stack); + write_container_value_or_push(*child, stack); } } } - /*! - @brief write @a j with @ref write_msgpack, or write its header and push a - frame for @ref write_msgpack_iterative to continue with its elements - - @sa @ref write_cbor_value_or_push - */ - void write_msgpack_value_or_push(const BasicJsonType& j, std::vector& stack) + void write_cbor_iterative(const BasicJsonType& root) { - if (j.is_array()) - { - write_msgpack_array_prefix(j.m_data.m_value.array->size(), j); - if (!j.m_data.m_value.array->empty()) - { - stack.emplace_back(&j); - } - return; - } - - if (j.is_object()) - { - write_msgpack_object_prefix(j.m_data.m_value.object->size(), j); - if (!j.m_data.m_value.object->empty()) - { - stack.emplace_back(&j); - } - return; - } - - write_msgpack(j); + write_container_iterative(root); } - /*! - @brief write out @a root and everything below it without the call stack - - @sa @ref write_cbor_iterative - */ void write_msgpack_iterative(const BasicJsonType& root) { - std::vector stack; - write_msgpack_value_or_push(root, stack); - - while (!stack.empty()) - { - const binary_container_frame current = stack.back(); - - if (current.value->is_array()) - { - const auto& array = *current.value->m_data.m_value.array; - if (current.array_it == array.cend()) - { - stack.pop_back(); - continue; - } - - const BasicJsonType* child = &(*current.array_it); - ++stack.back().array_it; - write_msgpack_value_or_push(*child, stack); - } - else - { - const auto& object = *current.value->m_data.m_value.object; - if (current.object_it == object.cend()) - { - stack.pop_back(); - continue; - } - - if (error_handler == error_handler_t::strict) - { - check_utf8(current.object_it->first, *current.value); - } - write_msgpack(current.object_it->first); - const BasicJsonType* child = &(current.object_it->second); - ++stack.back().object_it; - write_msgpack_value_or_push(*child, stack); - } - } + write_container_iterative(root); } - /// @return true when a closing ']' still has to be written after the elements - bool write_ubjson_start_array(const BasicJsonType& j, const bool use_count, const bool use_type, - const bool add_prefix, const bool use_bjdata, bool& prefix_required) + /*! + @brief write the start of an array (@a IsArray) or object: its opening + marker, the optimized type, and the count + + @return true when a closing ']' or '}' still has to be written after the + elements + */ + template + bool write_ubjson_start_container(const BasicJsonType& j, const bool use_count, const bool use_type, + const bool add_prefix, const bool use_bjdata, bool& prefix_required) { prefix_required = true; if (add_prefix) { - oa.write_character(to_char_type('[')); + oa.write_character(to_char_type(IsArray ? '[' : '{')); } - if (use_type && !j.m_data.m_value.array->empty()) + const std::size_t size = IsArray ? j.m_data.m_value.array->size() : j.m_data.m_value.object->size(); + + if (use_type && size != 0) { if (!use_count) { - JSON_THROW(other_error::create(502, "use_type requires use_size = true", &j)); + throw_size_marker_required(j); } const CharType first_prefix = ubjson_prefix(j.front(), use_bjdata); - const bool same_prefix = std::all_of(j.begin() + 1, j.end(), + // the first element of an array has been compared with itself + const bool same_prefix = std::all_of(IsArray ? j.begin() + 1 : j.begin(), j.end(), [this, first_prefix, use_bjdata](const BasicJsonType & v) { return ubjson_prefix(v, use_bjdata) == first_prefix; @@ -1147,8 +1208,9 @@ class binary_writer // those, at one byte per element, so the result can be read back. // Objects are not affected: every element is preceded by its key. const bool valueless_type = (first_prefix == 'Z' || first_prefix == 'T' || first_prefix == 'F'); - const bool excessive_valueless = valueless_type - && j.m_data.m_value.array->size() > detail::max_valueless_container_size; + const bool excessive_valueless = IsArray + && valueless_type + && size > detail::max_valueless_container_size; if (same_prefix && !excessive_valueless && !(use_bjdata && is_bjdata_excluded_type_marker(first_prefix))) @@ -1161,48 +1223,7 @@ class binary_writer if (use_count) { - oa.write_character(to_char_type('#')); - write_number_with_ubjson_prefix(j.m_data.m_value.array->size(), true, use_bjdata); - } - - return !use_count; - } - - /// @return true when a closing '}' still has to be written after the elements - bool write_ubjson_start_object(const BasicJsonType& j, const bool use_count, const bool use_type, - const bool add_prefix, const bool use_bjdata, bool& prefix_required) - { - prefix_required = true; - if (add_prefix) - { - oa.write_character(to_char_type('{')); - } - - if (use_type && !j.m_data.m_value.object->empty()) - { - if (!use_count) - { - JSON_THROW(other_error::create(502, "use_type requires use_size = true", &j)); - } - const CharType first_prefix = ubjson_prefix(j.front(), use_bjdata); - const bool same_prefix = std::all_of(j.begin(), j.end(), - [this, first_prefix, use_bjdata](const BasicJsonType & v) - { - return ubjson_prefix(v, use_bjdata) == first_prefix; - }); - - if (same_prefix && !(use_bjdata && is_bjdata_excluded_type_marker(first_prefix))) - { - prefix_required = false; - oa.write_character(to_char_type('$')); - oa.write_character(first_prefix); - } - } - - if (use_count) - { - oa.write_character(to_char_type('#')); - write_number_with_ubjson_prefix(j.m_data.m_value.object->size(), true, use_bjdata); + write_ubjson_count(size, use_bjdata); } return !use_count; @@ -1230,21 +1251,12 @@ class binary_writer /// @brief an object or array @ref write_ubjson_iterative is still writing /// the elements of - struct ubjson_frame + struct ubjson_frame : binary_container_frame { ubjson_frame(const BasicJsonType* value_, const bool prefix_required_) noexcept - : value(value_) + : binary_container_frame(value_) , prefix_required(prefix_required_) - { - if (value->is_object()) - { - object_it = value->m_data.m_value.object->cbegin(); - } - else - { - array_it = value->m_data.m_value.array->cbegin(); - } - } + {} // declared for GCC's -Weffc++, which asks for them in a class with // pointer members and a non-trivial destructor; the exception @@ -1256,13 +1268,9 @@ class binary_writer ubjson_frame& operator=(ubjson_frame&&) = default; ~ubjson_frame() = default; - /// the array or object being written - const BasicJsonType* value; /// whether value's elements each carry their own type marker; an /// optimized ($type) container writes it once for all of them instead bool prefix_required; - typename BasicJsonType::object_t::const_iterator object_it{}; - typename BasicJsonType::array_t::const_iterator array_it{}; }; /*! @@ -1273,7 +1281,7 @@ class binary_writer elements of an optimized container, and everything below the top level, never repeat it) - @sa @ref write_cbor_value_or_push + @sa @ref write_container_value_or_push */ void write_ubjson_value_or_push(const BasicJsonType& j, const bool add_prefix, const bool use_count, const bool use_type, const bool use_bjdata, const bjdata_version_t bjdata_version, @@ -1296,11 +1304,11 @@ class binary_writer bool prefix_required = true; if (is_array) { - write_ubjson_start_array(j, use_count, use_type, add_prefix, use_bjdata, prefix_required); + write_ubjson_start_container(j, use_count, use_type, add_prefix, use_bjdata, prefix_required); } else { - write_ubjson_start_object(j, use_count, use_type, add_prefix, use_bjdata, prefix_required); + write_ubjson_start_container(j, use_count, use_type, add_prefix, use_bjdata, prefix_required); } const bool empty = is_array ? j.m_data.m_value.array->empty() : j.m_data.m_value.object->empty(); @@ -1310,7 +1318,7 @@ class binary_writer return; } - // write_ubjson_start_array/_object return !use_count, i.e. whether a + // write_ubjson_start_container returns !use_count, i.e. whether a // closer still has to be written; use_count is constant for the whole // document, so that is recomputed here instead of being carried along if (!use_count) @@ -1366,12 +1374,9 @@ class binary_writer continue; } - string_t storage; - const string_t& key = sanitize_utf8_for_write(current.object_it->first, j, storage); - write_number_with_ubjson_prefix(key.size(), true, use_bjdata); - oa.write_characters( - reinterpret_cast(key.data()), - key.size()); + // a converted key must outlive the reference returned by sanitize_utf8_for_write + const object_key_string_t key_string = current.object_it->first; + write_ubjson_text(key_string, j, false, use_bjdata); const BasicJsonType* child = &(current.object_it->second); const bool child_prefix = current.prefix_required; ++stack.back().object_it; @@ -1394,18 +1399,85 @@ class binary_writer */ std::size_t calc_bson_entry_header_size(const string_t& name, const BasicJsonType& j) { - const auto it = name.find(static_cast(0)); + const std::size_t it = find_nul(name); if (JSON_HEDLEY_UNLIKELY(it != BasicJsonType::string_t::npos)) { - JSON_THROW(out_of_range::create(409, concat("BSON key cannot contain code point U+0000 (at byte ", std::to_string(it), ")"), &j)); + throw_bson_key_with_null(it, j); } + if (JSON_HEDLEY_LIKELY(error_handler == error_handler_t::keep)) + { + return /*id*/ 1ul + name.size() + /*zero-terminator*/1u; + } + + return calc_bson_checked_entry_header_size(name, j); + } + + /// @brief throw out_of_range.409 for a name with U+0000 at byte @a index; + /// out of line, so that the message does not burden the check + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_bson_key_with_null(const std::size_t index, const BasicJsonType& j) + { + static_cast(index); // only used when exceptions are enabled + static_cast(j); + JSON_THROW(out_of_range::create(exception_id::bson_key_with_null, concat("BSON key cannot contain code point U+0000 (at byte ", std::to_string(index), ")"), &j)); + } + + /// @brief the size of a BSON document entry header for a name that the + /// error handler checks or sanitizes; out of line, as the string_t + /// for the sanitized copy would otherwise burden every call of + /// @ref calc_bson_entry_header_size + JSON_HEDLEY_NEVER_INLINE + std::size_t calc_bson_checked_entry_header_size(const string_t& name, const BasicJsonType& j) + { string_t storage; const string_t& sanitized = sanitize_utf8_for_write(name, j, storage); return /*id*/ 1ul + sanitized.size() + /*zero-terminator*/1u; } + /*! + @brief the position of the first U+0000 in @a name, or string_t::npos + + The same as `name.find(0)`, without the calls into the standard library for + names of a few characters: it looks at eight characters at a time. + */ + static std::size_t find_nul(const string_t& name) noexcept + { + return find_nul(name, std::integral_constant()); + } + + static std::size_t find_nul(const string_t& name, std::true_type /*is_byte*/) noexcept + { + constexpr std::uint64_t ones = 0x0101010101010101ull; + constexpr std::uint64_t high = 0x8080808080808080ull; + const std::size_t size = name.size(); + const char* const data = reinterpret_cast(name.data()); + std::size_t pos = 0; + for (; pos + 8 <= size; pos += 8) + { + std::uint64_t word = 0; + std::memcpy(&word, data + pos, sizeof(word)); + if (((word - ones) & ~word & high) != 0) + { + break; + } + } + for (; pos < size; ++pos) + { + if (data[pos] == '\0') + { + return pos; + } + } + return BasicJsonType::string_t::npos; + } + + static std::size_t find_nul(const string_t& name, std::false_type /*is_byte*/) noexcept + { + return name.find(static_cast(0)); + } + /*! @brief Checks that @a size fits into the 32-bit length field used by BSON @return The size as a signed 32-bit integer @@ -1415,7 +1487,7 @@ class binary_writer { if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(size))) { - JSON_THROW(out_of_range::create(412, concat("BSON length ", std::to_string(size), " exceeds maximum of ", std::to_string((std::numeric_limits::max)())), nullptr)); + throw_bson_length_too_large(size); } return static_cast(size); @@ -1435,14 +1507,15 @@ class binary_writer { oa.write_character(to_char_type(element_type)); - if (error_handler == error_handler_t::keep || error_handler == error_handler_t::strict || is_valid_utf8(name)) + if (JSON_HEDLEY_LIKELY(error_handler == error_handler_t::keep || error_handler == error_handler_t::strict)) { - oa.write_characters(reinterpret_cast(name.data()), name.size()); + write_bytes(name.data(), name.size()); } else { - const string_t sanitized = sanitize_utf8(name, error_handler); - oa.write_characters(reinterpret_cast(sanitized.data()), sanitized.size()); + string_t storage; + const string_t& written = sanitize_utf8_for_bson_write(name, storage); + write_bytes(written.data(), written.size()); } // the terminating null byte is written explicitly rather than taken @@ -1485,13 +1558,25 @@ class binary_writer { if (JSON_HEDLEY_LIKELY(value_in_range_of(value.size()))) { - string_t storage; - const string_t& sanitized = sanitize_utf8_for_write(value, j, storage); - return sizeof(std::int32_t) + sanitized.size() + 1ul; + if (JSON_HEDLEY_LIKELY(error_handler == error_handler_t::keep)) + { + return sizeof(std::int32_t) + value.size() + 1ul; + } + return calc_bson_checked_string_size(value, j); } return sizeof(std::int32_t) + value.size() + 1ul; } + /// @brief the size of a BSON string that the error handler checks or + /// sanitizes; out of line, like @ref calc_bson_checked_entry_header_size + JSON_HEDLEY_NEVER_INLINE + std::size_t calc_bson_checked_string_size(const string_t& value, const BasicJsonType& j) + { + string_t storage; + const string_t& sanitized = sanitize_utf8_for_write(value, j, storage); + return sizeof(std::int32_t) + sanitized.size() + 1ul; + } + /*! @brief Writes a BSON element with key @a name and string value @a value @@ -1506,16 +1591,11 @@ class binary_writer { write_bson_entry_header(name, 0x02); - const bool sanitize = error_handler != error_handler_t::keep - && error_handler != error_handler_t::strict - && !is_valid_utf8(value); - const string_t sanitized = sanitize ? sanitize_utf8(value, error_handler) : string_t{}; - const string_t& written = sanitize ? sanitized : value; + string_t storage; + const string_t& written = sanitize_utf8_for_bson_write(value, storage); write_number(to_bson_length(written.size() + 1ul), true); - oa.write_characters( - reinterpret_cast(written.data()), - written.size()); + write_bytes(written.data(), written.size()); // the terminating null byte is written explicitly rather than taken // from the buffer, so that string_t::data() need not be null-terminated oa.write_character(to_char_type(0x00)); @@ -1601,7 +1681,7 @@ class binary_writer if (value.has_subtype() && JSON_HEDLEY_UNLIKELY(value.subtype() > (std::numeric_limits::max)())) { - JSON_THROW(out_of_range::create(415, concat("subtype ", std::to_string(value.subtype()), " is too large for the BSON binary subtype (max 255)"), &j)); + throw_subtype_out_of_range(value.subtype(), "the BSON binary subtype", j); } return sizeof(std::int32_t) + value.size() + 1ul; @@ -1621,7 +1701,7 @@ class binary_writer write_number(value.has_subtype() ? static_cast(value.subtype()) : static_cast(0x00)); - oa.write_characters(reinterpret_cast(value.data()), value.size()); + write_bytes(value.data(), value.size()); } /*! @@ -1629,6 +1709,8 @@ class binary_writer is neither an object nor an array @throw out_of_range.415 if @a j is binary with a subtype that does not fit into a byte, before anything is written + @throw out_of_range.407 if @a j is an integer that does not fit the 64 bits + of a BSON integer, before anything is written @throw type_error.316 if @a j is a string that is not valid UTF-8, before anything is written @throw type_error.321 if @a j is discarded @@ -1647,10 +1729,18 @@ class binary_writer return 8ul; case value_t::number_integer: - return calc_bson_integer_size(j.m_data.m_value.number_integer); + if (JSON_HEDLEY_UNLIKELY(!integer_fits(j.m_data.m_value.number_integer))) + { + throw_integer_out_of_range(j, input_format_t::bson, "int64"); + } + return calc_bson_integer_size(static_cast(j.m_data.m_value.number_integer)); case value_t::number_unsigned: - return calc_bson_unsigned_size(j.m_data.m_value.number_unsigned); + if (JSON_HEDLEY_UNLIKELY(!integer_fits_max(j.m_data.m_value.number_unsigned))) + { + throw_integer_out_of_range(j, input_format_t::bson, "uint64"); + } + return calc_bson_unsigned_size(conditional_static_cast(j.m_data.m_value.number_unsigned)); case value_t::string: return calc_bson_string_size(*j.m_data.m_value.string, j); @@ -1659,7 +1749,7 @@ class binary_writer return 0ul; case value_t::discarded: - throw_on_discarded(j, "BSON"); + throw_on_discarded(j, input_format_t::bson); // LCOV_EXCL_START case value_t::object: @@ -1671,12 +1761,198 @@ class binary_writer } } + /// the longest name for which write_bson_scalar() writes an entry through a + /// single reservation: the type, name, terminating null byte, and the length + /// of a string or binary value (4 bytes) or the largest fixed-size value (8 + /// bytes, a double or a 64-bit integer) must fit max_reserve_size + static constexpr std::size_t bson_scalar_max_name_size = max_reserve_size - 2 - 8 - 1; + + /*! + @brief store @a n at @a out in little-endian byte order + */ + template + void store_little_endian(CharType* out, const NumberType n) const noexcept + { + std::memcpy(out, &n, sizeof(NumberType)); + if (!is_little_endian) + { + std::reverse(out, out + sizeof(NumberType)); + } + } + + /*! + @brief encode the type and the value of the BSON document entry for @a j + if it has a fixed size + @param[in] out where the type byte goes + @param[in] value_out where the value goes: the characters after the name + and its terminating null byte + @return the end of the entry, or nullptr (and nothing is written to + @a out) if @a j is not a boolean, number, or null; at most 9 + characters are written to @a out and @a value_out + + The entry is encoded as @ref write_bson_value does. + */ + CharType* encode_bson_fixed_entry(CharType* out, CharType* value_out, const BasicJsonType& j) const noexcept + { + switch (j.type()) + { + case value_t::boolean: + out[0] = to_char_type(0x08); + *value_out++ = j.m_data.m_value.boolean ? to_char_type(0x01) : to_char_type(0x00); + return value_out; + + case value_t::number_float: + out[0] = to_char_type(0x01); + store_little_endian(value_out, static_cast(j.m_data.m_value.number_float)); + return value_out + sizeof(double); + + case value_t::number_integer: + { + const auto value = static_cast(j.m_data.m_value.number_integer); + if ((std::numeric_limits::min)() <= value && value <= (std::numeric_limits::max)()) + { + out[0] = to_char_type(0x10); // int32 + store_little_endian(value_out, static_cast(value)); + return value_out + sizeof(std::int32_t); + } + out[0] = to_char_type(0x12); // int64 + store_little_endian(value_out, value); + return value_out + sizeof(std::int64_t); + } + + case value_t::number_unsigned: + { + const auto value = conditional_static_cast(j.m_data.m_value.number_unsigned); + if (value <= static_cast((std::numeric_limits::max)())) + { + out[0] = to_char_type(0x10); // int32 + store_little_endian(value_out, static_cast(value)); + return value_out + sizeof(std::int32_t); + } + if (value <= static_cast((std::numeric_limits::max)())) + { + out[0] = to_char_type(0x12); // int64 + store_little_endian(value_out, static_cast(value)); + return value_out + sizeof(std::int64_t); + } + out[0] = to_char_type(0x11); // uint64 + store_little_endian(value_out, value); + return value_out + sizeof(std::uint64_t); + } + + case value_t::null: + out[0] = to_char_type(0x0A); + return value_out; + + case value_t::string: + case value_t::binary: + case value_t::object: + case value_t::array: + case value_t::discarded: + default: + return nullptr; + } + } + + /*! + @brief Writes the BSON document entry with key @a name for @a j, which is + neither an object nor an array, with the key and a fixed-size value + (or the header of a string or binary value) in one reservation + @return whether the entry was written; false for a discarded value, a + string or binary value whose length does not fit the length + field, in which case nothing was written + @pre @a name is at most bson_scalar_max_name_size characters long and is + not changed by the error handler + */ + bool write_bson_scalar(const string_t& name, const BasicJsonType& j) + { + const std::size_t name_size = name.size(); + // a short string is written whole: its type, the name and its null + // byte, the length, the characters, and their null byte + const bool whole_string = j.is_string() && j.m_data.m_value.string->size() <= max_reserve_size - name_size - 7; + CharType* const out = oa.reserve(whole_string ? name_size + j.m_data.m_value.string->size() + 7 : name_size + 2 + 8); + // the name and its terminating null byte follow the type byte + CharType* p = out + 1; + copy_characters(p, reinterpret_cast(name.data()), name_size); + p += name_size; + *p++ = to_char_type(0x00); + + if (const CharType* const end = encode_bson_fixed_entry(out, p, j)) + { + oa.commit(static_cast(end - out)); + return true; + } + + if (whole_string) + { + const string_t& value = *j.m_data.m_value.string; + out[0] = to_char_type(0x02); + store_little_endian(p, static_cast(value.size() + 1ul)); + p += sizeof(std::int32_t); + copy_characters(p, reinterpret_cast(value.data()), value.size()); + p += value.size(); + // the terminating null byte is written explicitly rather than + // taken from the buffer, so that string_t::data() need not be + // null-terminated + *p++ = to_char_type(0x00); + oa.commit(static_cast(p - out)); + return true; + } + + if (j.is_string()) + { + const string_t& value = *j.m_data.m_value.string; + if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(value.size() + 1ul))) + { + return false; + } + out[0] = to_char_type(0x02); + store_little_endian(p, static_cast(value.size() + 1ul)); + p += sizeof(std::int32_t); + oa.commit(static_cast(p - out)); + // the terminating null byte is written explicitly rather than + // taken from the buffer, so that string_t::data() need not be + // null-terminated + write_bytes(value.data(), value.size()); + oa.write_character(to_char_type(0x00)); + return true; + } + + if (j.is_binary()) + { + const binary_t& value = *j.m_data.m_value.binary; + if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(value.size()))) + { + return false; + } + out[0] = to_char_type(0x05); + store_little_endian(p, static_cast(value.size())); + p += sizeof(std::int32_t); + *p++ = value.has_subtype() ? to_char_type(static_cast(value.subtype())) : to_char_type(0x00); + oa.commit(static_cast(p - out)); + write_bytes(value.data(), value.size()); + return true; + } + + return false; + } + /*! @brief Writes the BSON document entry with key @a name for @a j, which is neither an object nor an array */ void write_bson_value(const string_t& name, const BasicJsonType& j) { + // The strings are written as they are, as they are neither checked nor + // sanitized again here by these two error handlers. The name has to leave + // room for the larger payloads (see write_bson_scalar). + if (name.size() <= bson_scalar_max_name_size + && (error_handler == error_handler_t::keep || error_handler == error_handler_t::strict) + && write_bson_scalar(name, j)) + { + return; + } + switch (j.type()) { case value_t::binary: @@ -1688,11 +1964,12 @@ class binary_writer case value_t::number_float: return write_bson_double(name, j.m_data.m_value.number_float); + // calc_bson_value_size() checked that integers fit 64 bits case value_t::number_integer: - return write_bson_integer(name, j.m_data.m_value.number_integer); + return write_bson_integer(name, static_cast(j.m_data.m_value.number_integer)); case value_t::number_unsigned: - return write_bson_unsigned(name, j.m_data.m_value.number_unsigned); + return write_bson_unsigned(name, conditional_static_cast(j.m_data.m_value.number_unsigned)); case value_t::string: return write_bson_string(name, *j.m_data.m_value.string); @@ -1701,7 +1978,7 @@ class binary_writer return write_bson_null(name); case value_t::discarded: - throw_on_discarded(j, "BSON"); + throw_on_discarded(j, input_format_t::bson); // LCOV_EXCL_START case value_t::object: @@ -1713,6 +1990,24 @@ class binary_writer } } + /*! + @brief Writes the head of a BSON element with key @a name whose value is + the object or array @a j: its type, key, and @a size + + Called while the key is in scope: an object key is a key_type, which + may only convert to a temporary string_t, so it cannot be kept by + pointer until after the loop over the entries. + */ + void write_bson_nested_head(const string_t& name, const BasicJsonType& j, + const std::size_t size) + { + write_bson_entry_header(name, j.is_object() ? 0x03 : 0x04); + write_number(to_bson_length(size), true); + } + + /// space for the name of an array element, the decimal index, in the frame + static constexpr std::size_t bson_index_name_space = 24; + /// @brief an object or array of the BSON document being sized or written struct bson_frame { @@ -1726,28 +2021,196 @@ class binary_writer } } + /// @ref calc_bson_sizes only: make @ref index the next one and count the + /// digits of its name + void count_index() noexcept + { + if (++index == index_boundary) + { + ++index_name_size; + index_boundary *= 10; + } + } + + /// @ref write_bson_document only: make @ref index the next one and + /// update its decimal name + void next_index() noexcept + { + ++index; + std::size_t i = index_name_size; + while (i > 0 && index_name[i - 1] == '9') + { + index_name[--i] = '0'; + } + if (i > 0) + { + ++index_name[i - 1]; + } + else + { + // all digits were 9: 10, 100, ... + index_name[0] = '1'; + index_name[index_name_size++] = '0'; + } + } + /// the object or array const BasicJsonType* value; /// objects: the next member typename BasicJsonType::object_t::const_iterator member{}; /// arrays: the index of the next element std::size_t index = 0; + /// arrays: the name BSON gives that element, its decimal index (at most + /// 2^64 - 1, which has 20 digits), in space for copying a few characters + /// more; only maintained by @ref write_bson_document + std::array index_name{{'0'}}; + /// arrays: the number of digits of the index + std::size_t index_name_size = 1; + /// arrays: the first index with one more digit; only maintained by + /// @ref calc_bson_sizes + std::uint64_t index_boundary = 10; /// @ref calc_bson_sizes only: where its size goes in the table std::size_t size_slot; /// @ref calc_bson_sizes only: the size of its entries seen so far std::size_t entries_size = 0; }; - /*! - @brief creates the name BSON gives the array element with index @a index - @param[out] name receives the decimal index - */ - static void create_bson_index_name(const std::size_t index, string_t& name) + /// whether the BSON entry of @a j has a fixed size, which is the case for + /// a boolean, a number, and null + static bool is_bson_fixed_size(const BasicJsonType& j) noexcept { - // the index is built as a std::string; convert explicitly, as the - // two are only implicitly convertible for some string types - const auto key = std::to_string(index); - name = string_t(key.data(), key.size()); + return j.is_number() || j.is_boolean() || j.is_null(); + } + + /*! + @brief Writes the entries of the array in @a frame that have a fixed size + (booleans, numbers, and null), from its next element on, until it + reaches another element or the end of the array + + The index of the element and its name are advanced in @a frame. + + The entries are encoded straight into space reserved from the sink for as + many of them as fit, with the index, its digits, and the write pointer + kept in local variables: stores of characters may alias the frame, and + would force them to be loaded from it again for every element. + */ + void write_bson_array_fixed_entries(bson_frame& frame) + { + const auto& array = *frame.value->m_data.m_value.array; + const std::size_t count = array.size(); + std::size_t index = frame.index; + std::size_t name_size = frame.index_name_size; + std::array name = frame.index_name; + + // an entry is at most the type, 20 digits, the null byte, and 8 bytes + constexpr std::size_t max_entry = 1 + 20 + 1 + 8; + while (index < count) + { + CharType* const start = oa.reserve(max_reserve_size); + CharType* out = start; + const CharType* const limit = start + (max_reserve_size - max_entry); + while (out <= limit && index < count) + { + CharType* p = out + 1; + std::memcpy(p, name.data(), name.size()); + p += name_size; + *p++ = to_char_type(0x00); + CharType* const end = encode_bson_fixed_entry(out, p, array[index]); + if (end == nullptr) + { + break; + } + out = end; + + ++index; + std::size_t i = name_size; + while (i > 0 && name[i - 1] == '9') + { + name[--i] = '0'; + } + if (i > 0) + { + ++name[i - 1]; + } + else + { + // all digits were 9: 10, 100, ... + name[0] = '1'; + name[name_size++] = '0'; + } + } + oa.commit(static_cast(out - start)); + if (out <= limit && index < count) + { + // stopped at an element that has no fixed size + break; + } + } + + frame.index = index; + frame.index_name_size = name_size; + frame.index_name = name; + } + + /*! + @brief Calculates the size of the BSON array @a j if all its elements are + booleans, numbers, or null + @param[out] size the size of the array, including its own size field + @return false (and nothing is known about @a size) if an element is not, + or is an integer that does not fit 64 bits (which the generic + path rejects), which is not looked at further + */ + bool calc_bson_fixed_array_size(const BasicJsonType& j, std::size_t& size) const noexcept + { + std::size_t total = sizeof(std::int32_t) + 1ul; + std::size_t index_name_size = 1; + std::uint64_t index_boundary = 10; + std::uint64_t index = 0; + for (const auto& el : *j.m_data.m_value.array) + { + // the type, the decimal index, and the terminating null byte + total += 2ul + index_name_size; + if (++index == index_boundary) + { + ++index_name_size; + index_boundary *= 10; + } + switch (el.type()) + { + case value_t::boolean: + total += 1ul; + break; + case value_t::number_float: + total += 8ul; + break; + case value_t::number_integer: + // an integer beyond 64 bits is left to the generic path, which throws + if (JSON_HEDLEY_UNLIKELY(!integer_fits(el.m_data.m_value.number_integer))) + { + return false; + } + total += calc_bson_integer_size(static_cast(el.m_data.m_value.number_integer)); + break; + case value_t::number_unsigned: + if (JSON_HEDLEY_UNLIKELY(!integer_fits_max(el.m_data.m_value.number_unsigned))) + { + return false; + } + total += calc_bson_unsigned_size(conditional_static_cast(el.m_data.m_value.number_unsigned)); + break; + case value_t::null: + break; + case value_t::string: + case value_t::binary: + case value_t::object: + case value_t::array: + case value_t::discarded: + default: + return false; + } + } + size = total; + return true; } /*! @@ -1781,8 +2244,6 @@ class binary_writer // is in; nothing is allocated unless the document nests bson_frame current(&document); std::vector parents; - // string_t need not be default constructible - string_t index_name("", 0); while (true) { @@ -1813,9 +2274,10 @@ class binary_writer while (nested == nullptr && current.index < array.size()) { const BasicJsonType& el = array[current.index]; - create_bson_index_name(current.index, index_name); - current.entries_size += calc_bson_entry_header_size(index_name, el); - ++current.index; + // the type, the decimal index (which is valid UTF-8 and + // has no U+0000), and the terminating null byte + current.entries_size += 2ul + current.index_name_size; + current.count_index(); if (el.is_structured()) { nested = ⪙ @@ -1829,6 +2291,15 @@ class binary_writer if (nested != nullptr) { + // an array of booleans, numbers and null: sized without a frame + std::size_t fixed_size = 0; + if (nested->is_array() && calc_bson_fixed_array_size(*nested, fixed_size)) + { + nested_sizes.push_back(fixed_size); + current.entries_size += fixed_size; + continue; + } + // its size is added to the current one's once it is done nested_sizes.push_back(0); parents.push_back(std::move(current)); @@ -1870,14 +2341,11 @@ class binary_writer bson_frame current(&document); std::vector parents; std::size_t next_size = 0; - // string_t need not be default constructible - string_t index_name("", 0); while (true) { // write entries until the current object or array is done, or an // entry is an object or array itself - const string_t* nested_name = nullptr; const BasicJsonType* nested = nullptr; if (current.value->is_object()) { @@ -1888,7 +2356,7 @@ class binary_writer ++current.member; if (el.second.is_structured()) { - nested_name = &el.first; + write_bson_nested_head(el.first, el.second, nested_sizes[next_size++]); nested = &el.second; } else @@ -1902,12 +2370,22 @@ class binary_writer 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]; - create_bson_index_name(current.index, index_name); - ++current.index; + const string_t index_name(current.index_name.data(), current.index_name_size); + current.next_index(); if (el.is_structured()) { - nested_name = &index_name; + write_bson_nested_head(index_name, el, nested_sizes[next_size++]); nested = ⪙ } else @@ -1919,8 +2397,20 @@ class binary_writer if (nested != nullptr) { - write_bson_entry_header(*nested_name, nested->is_object() ? 0x03 : 0x04); - write_number(to_bson_length(nested_sizes[next_size++]), true); + if (nested->is_array()) + { + // an array of booleans, numbers and null is written without a frame on the stack + bson_frame leaf(nested); + write_bson_array_fixed_entries(leaf); + if (leaf.index == nested->m_data.m_value.array->size()) + { + oa.write_character(to_char_type(0x00)); + continue; + } + parents.push_back(std::move(current)); + current = std::move(leaf); + continue; + } parents.push_back(std::move(current)); current = bson_frame(nested); continue; @@ -1988,6 +2478,727 @@ class binary_writer } } + /*! + @brief size class of an unsigned integer from its bit length + + 1, 2, 3, or 4 if @a x fits 1, 2, 4, or 8 bytes. Looked up from the number + of leading zero bits instead of found by branches: the sizes of a stream + of numbers are hard to predict, and a mispredicted branch costs more than + writing the number. (A table, as comparisons are vectorized by clang into + slower code.) + */ + static unsigned integer_size_class(const std::uint64_t x) noexcept + { + static constexpr std::array size_class_by_leading_zeros = + { + { + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1 + } + }; + // x | 1 keeps count_leading_zeros() away from 0 + return size_class_by_leading_zeros[static_cast(count_leading_zeros(x | 1u))]; + } + + /*! + @brief encode @a first_byte and the low-order bytes of @a value that + @a size_class calls for (none, or 1, 2, 4, or 8) in network order + to @a out + @return the number of characters of the encoding; up to + @ref integer_encoding_space characters at @a out are overwritten + + The bytes are written as one byte and one 8-byte word whose bytes after + the value are scratch that the caller drops, so that writing them does not + depend on the size class. For class 0 the word is all scratch; it is + shifted by 60 bits rather than 64, which would be undefined. + */ + std::size_t encode_first_byte_and_tail(CharType* out, const std::uint8_t first_byte, const unsigned size_class, const std::uint64_t value) const noexcept + { + out[0] = to_char_type(first_byte); + const unsigned unused_bits = 64u - ((8u << size_class) >> 1u); + const std::uint64_t tail = is_little_endian ? byteswap(value) >> unused_bits : value << unused_bits; + std::memcpy(out + 1, &tail, sizeof(tail)); + return ((std::size_t{1} << size_class) >> 1u) + 1; + } + + /// characters encode_first_byte_and_tail() may overwrite: the first byte + /// and one 8-byte word + static constexpr std::size_t integer_encoding_space = 9; + + /*! + @brief the 64 bits of a number_integer or number_unsigned value, and a mask + that is all ones if it is negative, else zero + + Selected through masks rather than branches, as the type and the sign of + the values in an array of mixed signs alternate unpredictably (non-negative + integers are number_unsigned after parsing). Up to 64 bits, the sign is bit + 63 of the result (a narrower integer is sign-extended); computing it with + masks keeps GCC from branching on the type. A wider integer has to be + compared. + */ + static std::uint64_t integer_bits(const BasicJsonType& j, std::uint64_t& sign_mask) noexcept + { + const bool is_signed = j.m_data.m_type == value_t::number_integer; + const std::uint64_t raw = is_signed + ? static_cast(j.m_data.m_value.number_integer) + : static_cast(j.m_data.m_value.number_unsigned); + sign_mask = sizeof(typename BasicJsonType::number_integer_t) <= sizeof(std::uint64_t) + ? (std::uint64_t{0} - static_cast(is_signed)) & (std::uint64_t{0} - (raw >> 63u)) + : std::uint64_t{0} - static_cast(is_signed && j.m_data.m_value.number_integer < 0); + return raw; + } + + /*! + @brief encode the head of a CBOR integer to @a out without branching on its + size + @return the number of characters of the head + + Same output as @ref write_cbor_head. The lengths of strings and containers + are mostly predictable, so their heads keep the branches; the sizes of a + run of integers are not, so an integer's head is assembled without them. + */ + std::size_t encode_cbor_integer_head(CharType* out, const std::uint8_t major_type, const std::uint64_t argument) const noexcept + { + // an argument of at most 23 is in size class 1 (one byte) and is moved + // to class 0, the first byte, by subtracting 1 + const auto is_immediate = static_cast(argument <= 0x17u); + const unsigned size_class = integer_size_class(argument) - is_immediate; + + // the first byte: the argument itself if it is immediate, else + // 0x17 + size_class, selected through a mask + const std::uint64_t immediate_mask = std::uint64_t{0} - is_immediate; + const auto first_byte = static_cast(major_type + ((argument & immediate_mask) | ((0x17u + size_class) & ~immediate_mask))); + return encode_first_byte_and_tail(out, first_byte, size_class, argument); + } + + /// write the head of an array or object: one byte for a size of up to 23, else as + /// @ref write_cbor_head + void write_cbor_container_head(const std::uint8_t major_type, const std::uint64_t size) + { + if (JSON_HEDLEY_LIKELY(size <= 0x17)) + { + oa.write_character(to_char_type(static_cast(major_type + size))); + return; + } + write_cbor_head(major_type, size); + } + + /*! + @brief write an object value (head and members) as CBOR + + A member whose value is null, a boolean, an integer, or a string is written + without a call of @ref write_cbor. Kept out of line, like @ref write_cbor_array. + */ + JSON_HEDLEY_NEVER_INLINE + void write_cbor_object(const BasicJsonType& j, const std::size_t depth) + { + // step 1: write control byte and the object size + write_cbor_container_head(0xA0, j.m_data.m_value.object->size()); + + for (const auto& el : *j.m_data.m_value.object) + { + // el.first is written directly (not via a temporary + // basic_json), with the object as diagnostics context + write_cbor_key(el.first, j); + + const BasicJsonType& value = el.second; + switch (value.type()) + { + case value_t::null: + oa.write_character(to_char_type(0xF6)); + break; + + case value_t::boolean: + oa.write_character(value.m_data.m_value.boolean ? to_char_type(0xF5) : to_char_type(0xF4)); + break; + + case value_t::number_integer: + case value_t::number_unsigned: + write_cbor_integer(value, std::integral_constant()); + break; + + case value_t::string: + write_cbor_string(*value.m_data.m_value.string, value); + break; + + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::number_float: + case value_t::discarded: + default: + write_cbor(value, depth + 1); + break; + } + } + } + + /*! + @brief write an array value (head and elements) as CBOR + + Integer elements, the most common scalar, are written without a call to + @ref write_cbor. The loop is kept out of line: inlined into write_cbor, its + registers would be saved and restored on every call of write_cbor, which + is one call per value nested in an object. + */ + JSON_HEDLEY_NEVER_INLINE + void write_cbor_array(const BasicJsonType& j, const std::size_t depth) + { + write_cbor_container_head(0x80, j.m_data.m_value.array->size()); + if (!j.m_data.m_value.array->empty()) + { + write_cbor_array_elements(j, depth, std::integral_constant()); + } + } + + JSON_HEDLEY_NEVER_INLINE + void write_cbor_array_elements(const BasicJsonType& j, const std::size_t depth, std::true_type /*fits_64_bits*/) + { + write_array_elements(j, [this](CharType * out, const BasicJsonType & el) + { + return encode_cbor_integer(out, el); + }, [this](CharType * out, const number_float_t n) + { + return encode_cbor_float(out, n); + }, [this, depth](const BasicJsonType & el) + { + write_cbor(el, depth + 1); + }); + } + + void write_cbor_array_elements(const BasicJsonType& j, const std::size_t depth, std::false_type /*fits_64_bits*/) + { + for (const auto& el : *j.m_data.m_value.array) + { + write_cbor(el, depth + 1); + } + } + + /*! + @brief write the elements of the array @a j, runs of integers with + @a encode_integer, runs of floats with @a encode_float, and all + other values with @a write_value + + Runs of numbers are encoded straight into space reserved from the sink + for a whole run: the write pointer stays a local, where it would otherwise + be reloaded from the sink for every value, as a store of a character may + alias it. + */ + template + void write_array_elements(const BasicJsonType& j, EncodeInteger encode_integer, EncodeFloat encode_float, WriteValue write_value) + { + constexpr std::size_t max_run = max_reserve_size / integer_encoding_space; + auto it = j.m_data.m_value.array->begin(); + const auto end = j.m_data.m_value.array->end(); + while (it != end) + { + if (!it->is_number()) + { + write_value(*it); + ++it; + } + else if (!it->is_number_float()) + { + CharType* const start = oa.reserve(max_run * integer_encoding_space); + CharType* out = start; + for (std::size_t n = 0; n < max_run && it != end && it->is_number_integer(); ++n, ++it) + { + out += encode_integer(out, *it); + } + oa.commit(static_cast(out - start)); + } + else + { + it = write_float_elements(it, end, encode_float); + } + } + } + + /*! + @brief write the run of floats that starts at @a it with @a encode_float + @return the first element after the run + + Out of line, so that the loop of integers in @ref write_array_elements is + not changed by it. + */ + template + JSON_HEDLEY_NEVER_INLINE + Iterator write_float_elements(Iterator it, const Iterator end, EncodeFloat encode_float) // NOLINT(performance-unnecessary-value-param): iterators are passed by value + { + constexpr std::size_t max_run = max_reserve_size / integer_encoding_space; + CharType* const start = oa.reserve(max_run * integer_encoding_space); + CharType* out = start; + for (std::size_t n = 0; n < max_run && it != end && it->is_number_float(); ++n, ++it) + { + out += encode_float(out, it->m_data.m_value.number_float); + } + oa.commit(static_cast(out - start)); + return it; + } + + /*! + @brief write a number_integer or number_unsigned value as a CBOR integer + + A negative integer n has major type 1 and the argument -1 - n, which is ~n. + Kept out of line, like the other per-value paths, so that write_cbor stays + small (runs of integers in an array are encoded inline instead). + */ + JSON_HEDLEY_NEVER_INLINE + void write_cbor_integer(const BasicJsonType& j, std::true_type /*fits_64_bits*/) + { + oa.commit(encode_cbor_integer(oa.reserve(integer_encoding_space), j)); + } + + /// write a number_integer or number_unsigned value of a number type wider + /// than 64 bits as a CBOR integer, with range checks on the whole value + void write_cbor_integer(const BasicJsonType& j, std::false_type /*fits_64_bits*/) + { + if (j.m_data.m_type == value_t::number_unsigned) + { + if (JSON_HEDLEY_UNLIKELY(!integer_fits_max(j.m_data.m_value.number_unsigned))) + { + throw_integer_out_of_range(j, input_format_t::cbor, "uint64"); + } + write_cbor_head(0x00, conditional_static_cast(j.m_data.m_value.number_unsigned)); + return; + } + + if (j.m_data.m_value.number_integer >= 0) + { + if (JSON_HEDLEY_UNLIKELY(!integer_fits_max(j.m_data.m_value.number_integer))) + { + throw_integer_out_of_range(j, input_format_t::cbor, "[-2^64, 2^64-1]"); + } + // CBOR does not differentiate between positive signed + // integers and unsigned integers + write_cbor_head(0x00, static_cast(j.m_data.m_value.number_integer)); + } + else + { + // a negative integer n is encoded as -1 - n + if (JSON_HEDLEY_UNLIKELY(!integer_fits_max(-1 - j.m_data.m_value.number_integer))) + { + throw_integer_out_of_range(j, input_format_t::cbor, "[-2^64, 2^64-1]"); + } + write_cbor_head(0x20, static_cast(-1 - j.m_data.m_value.number_integer)); + } + } + + /// encode a number_integer or number_unsigned value to @a out (see + /// @ref write_cbor_integer) and return the number of characters + std::size_t encode_cbor_integer(CharType* out, const BasicJsonType& j) const noexcept + { + std::uint64_t sign_mask = 0; + const std::uint64_t raw = integer_bits(j, sign_mask); + // ~raw is -1 - n in the 64 bits that are written + return encode_cbor_integer_head(out, static_cast(sign_mask & 0x20u), raw ^ sign_mask); + } + + /*! + @brief whether the CBOR and MessagePack integers of BasicJsonType are + written by @ref encode_cbor_integer and @ref encode_msgpack_integer + + These pick the format from the low 64 bits; a wider number type is written + with range checks on the whole value instead. + */ + static constexpr bool integers_fit_64_bits() noexcept + { + return sizeof(typename BasicJsonType::number_integer_t) <= sizeof(std::uint64_t) + && sizeof(typename BasicJsonType::number_unsigned_t) <= sizeof(std::uint64_t); + } + + /*! + @brief write a number_integer or number_unsigned value as a MessagePack + integer without branching on its sign or size + + Same output as @ref write_msgpack: a non-negative integer as positive fixint + (< 128) or uint 8/16/32/64, a negative one as negative fixint (>= -32) or + int 8/16/32/64. Kept out of line (see @ref write_cbor_integer). + */ + JSON_HEDLEY_NEVER_INLINE + void write_msgpack_integer(const BasicJsonType& j, std::true_type /*fits_64_bits*/) + { + oa.commit(encode_msgpack_integer(oa.reserve(integer_encoding_space), j)); + } + + /// encode a number_integer or number_unsigned value to @a out (see + /// @ref write_msgpack_integer) and return the number of characters + std::size_t encode_msgpack_integer(CharType* out, const BasicJsonType& j) const noexcept + { + std::uint64_t sign_mask = 0; + const std::uint64_t raw = integer_bits(j, sign_mask); + + // A negative n fits int 8/16/32 exactly if 2 * (-1 - n) + 1 fits + // uint 8/16/32, so x gives the size class of either sign. + const std::uint64_t is_negative = sign_mask & 1u; + const std::uint64_t x = ((raw ^ sign_mask) << is_negative) | is_negative; + + // A fixint is at most 127, or at least -32 (x at most 63). It is in + // size class 1 and is moved to class 0, the first byte, by subtracting 1. + const auto is_fixint = static_cast(x <= (127u ^ (sign_mask & 64u))); + const unsigned size_class = integer_size_class(x) - is_fixint; + + // the first byte: the low byte of the integer for a fixint, else the + // marker 0xCC..0xCF (uint) or 0xD0..0xD3 (int), selected through a mask + const std::uint64_t fixint_mask = std::uint64_t{0} - is_fixint; + const std::uint64_t marker = 0xCBu + size_class + (sign_mask & 4u); + const auto first_byte = static_cast((raw & fixint_mask) | (marker & ~fixint_mask)); + return encode_first_byte_and_tail(out, first_byte, size_class, raw); + } + + /// write a number_integer or number_unsigned value of a number type wider + /// than 64 bits as a MessagePack integer, with range checks on the whole value + void write_msgpack_integer(const BasicJsonType& j, std::false_type /*fits_64_bits*/) + { + if (j.m_data.m_type == value_t::number_unsigned) + { + if (JSON_HEDLEY_UNLIKELY(!integer_fits_max(j.m_data.m_value.number_unsigned))) + { + throw_integer_out_of_range(j, input_format_t::msgpack, "uint64"); + } + write_msgpack_unsigned(static_cast(j.m_data.m_value.number_unsigned)); + return; + } + + if (j.m_data.m_value.number_integer >= 0) + { + if (JSON_HEDLEY_UNLIKELY(!integer_fits_max(j.m_data.m_value.number_integer))) + { + throw_integer_out_of_range(j, input_format_t::msgpack, "[-2^63, 2^64-1]"); + } + // MessagePack does not differentiate between positive + // signed integers and unsigned integers. + write_msgpack_unsigned(static_cast(j.m_data.m_value.number_integer)); + } + else + { + if (JSON_HEDLEY_UNLIKELY(!integer_fits_min(j.m_data.m_value.number_integer))) + { + throw_integer_out_of_range(j, input_format_t::msgpack, "[-2^63, 2^64-1]"); + } + if (j.m_data.m_value.number_integer >= -32) + { + // negative fixnum + write_number(static_cast(j.m_data.m_value.number_integer)); + } + else if (j.m_data.m_value.number_integer >= (std::numeric_limits::min)() && + j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) + { + // int 8 + oa.write_character(to_char_type(0xD0)); + write_number(static_cast(j.m_data.m_value.number_integer)); + } + else if (j.m_data.m_value.number_integer >= (std::numeric_limits::min)() && + j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) + { + // int 16 + oa.write_character(to_char_type(0xD1)); + write_number(static_cast(j.m_data.m_value.number_integer)); + } + else if (j.m_data.m_value.number_integer >= (std::numeric_limits::min)() && + j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) + { + // int 32 + oa.write_character(to_char_type(0xD2)); + write_number(static_cast(j.m_data.m_value.number_integer)); + } + else + { + // int 64 + oa.write_character(to_char_type(0xD3)); + write_number(static_cast(j.m_data.m_value.number_integer)); + } + } + } + + /// write a float as MessagePack, into space reserved from the sink + JSON_HEDLEY_NEVER_INLINE + void write_msgpack_float(const number_float_t n) + { + oa.commit(encode_compact_float(oa.reserve(integer_encoding_space), n, to_char_type(0xCA), to_char_type(0xCB))); + } + + /*! + @brief write an object value (head and members) as MessagePack + + See @ref write_cbor_object. + */ + JSON_HEDLEY_NEVER_INLINE + void write_msgpack_object(const BasicJsonType& j, const std::size_t depth) + { + write_msgpack_object_prefix(j.m_data.m_value.object->size(), j); + + for (const auto& el : *j.m_data.m_value.object) + { + // as in write_cbor, el.first is written directly with the + // object as diagnostics context + write_msgpack_key(el.first, j); + + const BasicJsonType& value = el.second; + switch (value.type()) + { + case value_t::null: + oa.write_character(to_char_type(0xC0)); + break; + + case value_t::boolean: + oa.write_character(value.m_data.m_value.boolean ? to_char_type(0xC3) : to_char_type(0xC2)); + break; + + case value_t::number_integer: + case value_t::number_unsigned: + write_msgpack_integer(value, std::integral_constant()); + break; + + case value_t::number_float: + write_msgpack_float(value.m_data.m_value.number_float); + break; + + case value_t::string: + write_msgpack_string(*value.m_data.m_value.string, value); + break; + + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::discarded: + default: + write_msgpack(value, depth + 1); + break; + } + } + } + + /*! + @brief write an array value (head and elements) as MessagePack + + See @ref write_cbor_array. + */ + JSON_HEDLEY_NEVER_INLINE + void write_msgpack_array(const BasicJsonType& j, const std::size_t depth) + { + write_msgpack_array_prefix(j.m_data.m_value.array->size(), j); + if (!j.m_data.m_value.array->empty()) + { + write_msgpack_array_elements(j, depth, std::integral_constant()); + } + } + + JSON_HEDLEY_NEVER_INLINE + void write_msgpack_array_elements(const BasicJsonType& j, const std::size_t depth, std::true_type /*fits_64_bits*/) + { + write_array_elements(j, [this](CharType * out, const BasicJsonType & el) + { + return encode_msgpack_integer(out, el); + }, [this](CharType * out, const number_float_t n) + { + return encode_compact_float(out, n, to_char_type(0xCA), to_char_type(0xCB)); + }, [this, depth](const BasicJsonType & el) + { + write_msgpack(el, depth + 1); + }); + } + + void write_msgpack_array_elements(const BasicJsonType& j, const std::size_t depth, std::false_type /*fits_64_bits*/) + { + for (const auto& el : *j.m_data.m_value.array) + { + write_msgpack(el, depth + 1); + } + } + + /*! + @brief write an object key as a CBOR or MessagePack string (@a Walk) + + A key convertible to string_t is written directly. Other key types (only + an explicit conversion, or only a to_json overload) go through a temporary + basic_json, as in version 3.12.0; the temporary is then the diagnostics + context for strict UTF-8 checks. + */ + template::value, int> = 0> + void write_object_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& context) + { + Walk::string(*this, key, context); + } + + template>::value, int> = 0> + void write_object_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& /*context*/) + { + Walk::scalar(*this, BasicJsonType(key)); + } + + /// @brief write an object key as a CBOR text string + void write_cbor_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& context) + { + write_object_key(key, context); + } + + /// @brief write an object key as a MessagePack str + void write_msgpack_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& context) + { + write_object_key(key, context); + } + + /// the longest string that is written, with its head of one or two + /// characters, in a single reservation from the sink + static constexpr std::size_t max_text_in_reservation = max_reserve_size - 2; + + /// copy the characters of a string to space in the sink; a short one + /// without a call of memcpy() + static void copy_characters(CharType* dst, const CharType* src, const std::size_t length) noexcept + { + if (length <= 32) + { + copy_short(dst, src, length, std::integral_constant()); + } + else + { + std::memcpy(dst, src, length * sizeof(CharType)); + } + } + + /*! + @brief write a CBOR text string + + @a value is checked or sanitized according to @ref error_handler, with + @a context (the string value itself, or the object a key belongs to) used + as diagnostics context; this avoids converting object keys to a temporary + basic_json just to write them + + @note When object_t::key_type is not string_t, @a value is a temporary + string_t converted from the key, which lives only until the end of + the caller's statement. The reference returned by + @ref sanitize_utf8_for_write may refer to it, so it must not escape + this function. + */ + void write_cbor_string(const string_t& value, const BasicJsonType& context) + { + if (JSON_HEDLEY_LIKELY(error_handler == error_handler_t::keep)) + { + // the string is written as it is: head and characters in one reservation + if (JSON_HEDLEY_LIKELY(value.size() <= max_text_in_reservation)) + { + CharType* const out = oa.reserve(value.size() + 2); + CharType* p = out; + if (value.size() <= 0x17) + { + *p++ = to_char_type(static_cast(0x60 + value.size())); + } + else + { + *p++ = to_char_type(0x78); + *p++ = to_char_type(static_cast(value.size())); + } + copy_characters(p, reinterpret_cast(value.data()), value.size()); + oa.commit(static_cast(p - out) + value.size()); + return; + } + } + + write_cbor_checked_string(value, context); + } + + /// @brief write a CBOR text string checked or sanitized by the error handler; + /// out of line, as the string_t for the sanitized copy would make + /// every call of @ref write_cbor_string set up a frame for it + JSON_HEDLEY_NEVER_INLINE + void write_cbor_checked_string(const string_t& value, const BasicJsonType& context) + { + string_t storage; + const string_t& sanitized = sanitize_utf8_for_write(value, context, storage); + + // step 1: write control byte and the string length + write_cbor_head(0x60, sanitized.size()); + + // step 2: write the string + write_bytes(sanitized.data(), sanitized.size()); + } + + ///////////// + // MsgPack // + ///////////// + + /*! + @brief write a MessagePack str + + @a value is checked or sanitized according to @ref error_handler, with + @a context used as diagnostics context, as in @ref write_cbor_string + + @note As in @ref write_cbor_string, @a value may be a temporary string_t + converted from a key, so the reference returned by + @ref sanitize_utf8_for_write must not escape this function. + */ + void write_msgpack_string(const string_t& value, const BasicJsonType& context) + { + if (JSON_HEDLEY_LIKELY(error_handler == error_handler_t::keep)) + { + // the string is written as it is: head and characters in one reservation + if (JSON_HEDLEY_LIKELY(value.size() <= max_text_in_reservation)) + { + CharType* const out = oa.reserve(value.size() + 2); + CharType* p = out; + if (value.size() <= 31) + { + // fixstr + *p++ = to_char_type(static_cast(0xA0 | value.size())); + } + else + { + // str 8 + *p++ = to_char_type(0xD9); + *p++ = to_char_type(static_cast(value.size())); + } + copy_characters(p, reinterpret_cast(value.data()), value.size()); + oa.commit(static_cast(p - out) + value.size()); + return; + } + } + + write_msgpack_checked_string(value, context); + } + + /// @brief write a MessagePack str checked or sanitized by the error handler; + /// out of line, like @ref write_cbor_checked_string + JSON_HEDLEY_NEVER_INLINE + void write_msgpack_checked_string(const string_t& value, const BasicJsonType& context) + { + string_t storage; + const string_t& sanitized = sanitize_utf8_for_write(value, context, storage); + + // step 1: write control byte and the string length + const auto N = to_msgpack_length(sanitized.size(), context); + if (N <= 31) + { + // fixstr + write_number(static_cast(0xA0 | N)); + } + else if (N <= (std::numeric_limits::max)()) + { + // str 8 + oa.write_character(to_char_type(0xD9)); + write_number(static_cast(N)); + } + else if (N <= (std::numeric_limits::max)()) + { + // str 16 + oa.write_character(to_char_type(0xDA)); + write_number(static_cast(N)); + } + else + { + // str 32 + oa.write_character(to_char_type(0xDB)); + write_number(static_cast(N)); + } + + // step 2: write the string + write_bytes(sanitized.data(), sanitized.size()); + } + //////////// // UBJSON // //////////// @@ -2021,6 +3232,373 @@ class binary_writer write_ubjson_integer_payload(prefix, n, use_bjdata); } + /*! + @brief markers and size classes of UBJSON/BJData integers, by the bit length + of the magnitude + + A non-negative integer has the magnitude n, a negative one ~n = -1 - n; the + bit length of the magnitude is 1 to 64. The first 130 characters of the + table are the markers: 65 for a non-negative integer, then 65 for a negative + one (index + 65). The marker is the first of `i`, `U`, `I`, `u` (BJData), + `l`, `m` (BJData), `L`, `M` (BJData) whose range contains the integer; `H` + (a high-precision number) is for a non-negative integer of 64 bits in + UBJSON. The following 130 characters are the size classes at index + 130: + the digit 0, 1, 2, or 3 for 1, 2, 4, or 8 bytes. Looked up, like + @ref integer_size_class, instead of found by branches or comparisons. + + @sa ubjson_integer_prefix for the integers of types of more than 64 bits + */ + static const char* ubjson_integer_table(const bool use_bjdata) noexcept + { + // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays): initialized from string literals + static constexpr char ubjson_table[] = + // the markers for n >= 0, then n < 0 + "i" "iiiiiii" "U" "IIIIIII" "llllllllllllllll" "LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL" "H" + "i" "iiiiiii" "IIIIIIII" "llllllllllllllll" "LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL" "L" + // the size classes + "000000000" "1111111" "2222222222222222" "333333333333333333333333333333333" + "00000000" "11111111" "2222222222222222" "333333333333333333333333333333333"; + // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays): initialized from string literals + static constexpr char bjdata_table[] = + "i" "iiiiiii" "U" "IIIIIII" "u" "lllllllllllllll" "m" "LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL" "M" + "i" "iiiiiii" "IIIIIIII" "llllllllllllllll" "LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL" "L" + "000000000" "11111111" "2222222222222222" "33333333333333333333333333333333" + "00000000" "11111111" "2222222222222222" "333333333333333333333333333333333"; + static_assert(sizeof(ubjson_table) == 261 && sizeof(bjdata_table) == 261, "two tables of 130 markers and 130 size classes"); + return use_bjdata ? bjdata_table : ubjson_table; + } + + /// the index in ubjson_integer_table() of the integer with the 64 bits + /// @a raw and the sign mask @a sign_mask (all ones if it is negative) + static std::size_t ubjson_integer_index(const std::uint64_t raw, const std::uint64_t sign_mask) noexcept + { + const std::size_t sign_offset = sign_mask != 0 ? std::size_t{65} : + std::size_t{0}; + return static_cast(64 - count_leading_zeros((raw ^ sign_mask) | 1u)) + sign_offset; + } + + /*! + @brief encode a UBJSON/BJData integer without branching on its size + @param[in] out where the encoding goes; at most 9 characters are written + @param[in] raw the 64 bits of the integer (two's complement) + @param[in] sign_mask all ones if the integer is negative, else zero + @param[in] table ubjson_integer_table(use_bjdata) + @return the end of the encoding, or nullptr if the integer is a high-precision + number (a non-negative one of 64 bits in UBJSON), which is not written + */ + CharType* encode_ubjson_integer(CharType* out, const std::uint64_t raw, const std::uint64_t sign_mask, + const bool add_prefix, const bool use_bjdata, const char* table) const noexcept + { + const std::size_t index = ubjson_integer_index(raw, sign_mask); + const char marker = table[index]; + if (JSON_HEDLEY_UNLIKELY(marker == 'H')) + { + return nullptr; + } + const unsigned size_class = static_cast(table[index + 130]) & 3u; + + // the marker (or its place, if not written) and one 8-byte word, whose + // bytes after the integer are scratch that the caller drops + out[0] = to_char_type(static_cast(marker)); + out += add_prefix ? 1 : 0; + const unsigned unused_bits = 64u - (8u << size_class); + // UBJSON in network order, BJData in little-endian + std::uint64_t tail = 0; + if (use_bjdata) + { + tail = is_little_endian ? raw : byteswap(raw); + } + else + { + tail = is_little_endian ? byteswap(raw) >> unused_bits : raw << unused_bits; + } + std::memcpy(out, &tail, sizeof(tail)); + return out + (std::size_t{1} << size_class); + } + + /// @brief whether the integers of BasicJsonType are written by + /// @ref encode_ubjson_integer + static constexpr bool ubjson_integers_fit_64_bits() noexcept + { + return integers_fit_64_bits(); + } + + /// space for the largest encoding of encode_ubjson_integer(): the marker and 8 bytes + static constexpr std::size_t ubjson_integer_encoding_space = 9; + + /*! + @brief write the bytes of a binary value as an array of UBJSON uint8 values + + Every byte is written with its marker, in blocks that fit the space reserved + from the sink. Kept out of line, so that write_ubjson does not carry the + registers of its loop. + */ + JSON_HEDLEY_NEVER_INLINE + void write_ubjson_binary_bytes(const binary_t& binary, const bool bjdata_draft3) + { + const CharType marker = to_char_type(bjdata_draft3 ? 'B' : 'U'); + const auto* const data = binary.data(); + const std::size_t size = binary.size(); + for (std::size_t i = 0; i < size;) + { + const std::size_t block = (std::min)(size - i, max_reserve_size / 2); + CharType* const out = oa.reserve(2 * block); + for (std::size_t k = 0; k < block; ++k) + { + out[2 * k] = marker; + // the cast is needed for binary types whose value type + // is not an integer (e.g., std::byte) + out[(2 * k) + 1] = to_char_type(static_cast(data[i + k])); + } + i += block; + oa.commit(2 * block); + } + } + + /*! + @brief write the members of the object @a j of a UBJSON/BJData container + + A member whose value is null, a boolean, an integer, a float, or a string is + written without a call of @ref write_ubjson. Kept out of line, like + @ref write_cbor_object. + */ + JSON_HEDLEY_NEVER_INLINE + void write_ubjson_members(const BasicJsonType& j, const bool use_count, const bool use_type, + const bool prefix_required, const bool use_bjdata, const bjdata_version_t bjdata_version, + const std::size_t depth) + { + for (const auto& el : *j.m_data.m_value.object) + { + // a converted key must outlive the reference returned by sanitize_utf8_for_write + const object_key_string_t key_string = el.first; + write_ubjson_text(key_string, j, false, use_bjdata); + + const BasicJsonType& value = el.second; + switch (value.type()) + { + case value_t::null: + if (prefix_required) + { + oa.write_character(to_char_type('Z')); + } + break; + + case value_t::boolean: + if (prefix_required) + { + oa.write_character(value.m_data.m_value.boolean ? to_char_type('T') : to_char_type('F')); + } + break; + + case value_t::number_integer: + write_number_with_ubjson_prefix(value.m_data.m_value.number_integer, prefix_required, use_bjdata); + break; + + case value_t::number_unsigned: + write_number_with_ubjson_prefix(value.m_data.m_value.number_unsigned, prefix_required, use_bjdata); + break; + + case value_t::number_float: + write_number_with_ubjson_prefix(value.m_data.m_value.number_float, prefix_required, use_bjdata); + break; + + case value_t::string: + write_ubjson_text(*value.m_data.m_value.string, value, prefix_required, use_bjdata); + break; + + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::discarded: + default: + write_ubjson(value, use_count, use_type, prefix_required, use_bjdata, bjdata_version, depth + 1); + break; + } + } + } + + /// write the count marker `#` and the number of elements of a container as + /// an integer; a count of up to 127, which is `i` and a byte, in one reservation + void write_ubjson_count(const std::size_t count, const bool use_bjdata) + { + if (JSON_HEDLEY_LIKELY(count <= 127)) + { + CharType* const out = oa.reserve(3); + out[0] = to_char_type('#'); + out[1] = to_char_type('i'); + out[2] = to_char_type(static_cast(count)); + oa.commit(3); + return; + } + oa.write_character(to_char_type('#')); + write_number_with_ubjson_prefix(count, true, use_bjdata); + } + + /*! + @brief write a UBJSON/BJData string or object key: the optional string + marker `S`, the length as an integer with its marker, and the characters + + With the default error handler (keep) and a string of up to 253 characters, + which has a length of the marker `i` or `U` and a single byte, everything is + written into space reserved from the sink in one go. + + @param[in] value the string; checked or sanitized according to + @ref error_handler, with @a context as diagnostics context + @param[in] add_marker whether `S` is written first (not for an object key) + */ + void write_ubjson_text(const string_t& value, const BasicJsonType& context, const bool add_marker, const bool use_bjdata) + { + const std::size_t size = value.size(); + if (JSON_HEDLEY_LIKELY(error_handler == error_handler_t::keep) && JSON_HEDLEY_LIKELY(size <= max_text_in_reservation - 1)) + { + CharType* const out = oa.reserve(size + 3); + CharType* p = out; + if (add_marker) + { + *p++ = to_char_type('S'); + } + *p++ = to_char_type(size <= 127 ? 'i' : 'U'); + *p++ = to_char_type(static_cast(size)); + copy_characters(p, reinterpret_cast(value.data()), size); + oa.commit(static_cast(p - out) + size); + return; + } + + write_ubjson_checked_text(value, context, add_marker, use_bjdata); + } + + /// @brief write a UBJSON/BJData string checked or sanitized by the error + /// handler; out of line, like @ref write_cbor_checked_string + JSON_HEDLEY_NEVER_INLINE + void write_ubjson_checked_text(const string_t& value, const BasicJsonType& context, const bool add_marker, const bool use_bjdata) + { + string_t storage; + const string_t& sanitized = sanitize_utf8_for_write(value, context, storage); + if (add_marker) + { + oa.write_character(to_char_type('S')); + } + write_number_with_ubjson_prefix(sanitized.size(), true, use_bjdata); + write_bytes(sanitized.data(), sanitized.size()); + } + + /*! + @brief encode a UBJSON/BJData float, with its marker if @a add_prefix + @param[in] out where the encoding goes; at most 9 characters are written + @return the end of the encoding + */ + CharType* encode_ubjson_float(CharType* out, const number_float_t n, const bool add_prefix, const bool use_bjdata) const noexcept + { + if (add_prefix) + { + *out++ = get_ubjson_float_prefix(); + } + return store_number(out, n, use_bjdata); + } + + /// whether @a array starts with a number that @ref write_ubjson_array_elements + /// can encode (all but an integer that UBJSON writes as a high-precision + /// number); an array that does not is written by write_ubjson one element at a time + template + static bool starts_with_ubjson_number(const ArrayType& array, const bool use_bjdata, std::true_type /*fits_64_bits*/) noexcept + { + if (array.empty() || !array.front().is_number()) + { + return false; + } + const BasicJsonType& first = array.front(); + return use_bjdata || first.m_data.m_type != value_t::number_unsigned + || (static_cast(first.m_data.m_value.number_unsigned) >> 63u) == 0; + } + + template + static bool starts_with_ubjson_number(const ArrayType& /*array*/, const bool /*use_bjdata*/, std::false_type /*fits_64_bits*/) noexcept + { + return false; + } + + /*! + @brief write the elements of an array of a UBJSON/BJData container that + starts with a number, with runs of numbers encoded without a call of + @ref write_ubjson for each + + Same output as calling write_ubjson for each element. The numbers of a + run are encoded straight into space reserved from the sink, as in + @ref write_array_elements; all other values, and an integer that UBJSON + writes as a high-precision number, are written with write_ubjson. Kept out + of line, like @ref write_cbor_array. + */ + template + JSON_HEDLEY_NEVER_INLINE + void write_ubjson_array_elements(const ArrayType& array, const bool use_count, const bool use_type, + const bool prefix_required, const bool use_bjdata, const bjdata_version_t bjdata_version, + const std::size_t depth) + { + constexpr std::size_t max_run = max_reserve_size / ubjson_integer_encoding_space; + const char* const table = ubjson_integer_table(use_bjdata); + auto it = array.begin(); + const auto end = array.end(); + while (it != end) + { + if (!it->is_number()) + { + write_ubjson(*it, use_count, use_type, prefix_required, use_bjdata, bjdata_version, depth + 1); + ++it; + continue; + } + + CharType* const start = oa.reserve(max_run * ubjson_integer_encoding_space); + CharType* out = start; + bool unencodable = false; + for (std::size_t n = 0; n < max_run && it != end && it->is_number(); ++n) + { + CharType* next = nullptr; + if (it->is_number_float()) + { + next = encode_ubjson_float(out, it->m_data.m_value.number_float, prefix_required, use_bjdata); + } + else + { + std::uint64_t sign_mask = 0; + const std::uint64_t raw = integer_bits(*it, sign_mask); + next = encode_ubjson_integer(out, raw, sign_mask, prefix_required, use_bjdata, table); + } + if (JSON_HEDLEY_UNLIKELY(next == nullptr)) + { + unencodable = true; + break; + } + out = next; + ++it; + } + oa.commit(static_cast(out - start)); + + if (unencodable) + { + write_ubjson(*it, use_count, use_type, prefix_required, use_bjdata, bjdata_version, depth + 1); + ++it; + } + } + } + + /*! + @brief store @a n at @a out in the byte order of the output + @param[in] output_little_endian whether the output is little-endian + @return the end of the number + */ + template + CharType* store_number(CharType* out, const NumberType n, const bool output_little_endian) const noexcept + { + std::array vec{}; + std::memcpy(vec.data(), &n, sizeof(NumberType)); + if (is_little_endian != output_little_endian) + { + reverse_bytes(vec); + } + std::memcpy(out, vec.data(), sizeof(NumberType)); + return out + sizeof(NumberType); + } + /*! @brief determine the UBJSON/BJData type marker of an integer @@ -2068,7 +3646,7 @@ class binary_writer { return 'L'; } - if (use_bjdata && std::is_unsigned::value) + if (use_bjdata && std::is_unsigned::value && integer_fits_max(n)) { return 'M'; } @@ -2111,19 +3689,58 @@ class binary_writer break; default: { - // high-precision number: the decimal digits as a string JSON_ASSERT(prefix == 'H'); - const auto number = BasicJsonType(n).dump(); - write_number_with_ubjson_prefix(number.size(), true, use_bjdata); - for (std::size_t i = 0; i < number.size(); ++i) - { - oa.write_character(to_char_type(static_cast(number[i]))); - } + write_ubjson_high_precision(n, use_bjdata); break; } } } + /// @brief write an integer as a high-precision number: its decimal digits as + /// a string; out of line, as the string would otherwise burden + /// @ref write_ubjson_integer_payload for every integer + template + JSON_HEDLEY_NEVER_INLINE + void write_ubjson_high_precision(const NumberType n, const bool use_bjdata) + { + write_ubjson_high_precision(n, use_bjdata, std::integral_constant < bool, std::is_unsigned::value && sizeof(NumberType) <= sizeof(std::uint64_t) > ()); + } + + /// an unsigned integer of at most 64 bits: its decimal digits are written directly + template + void write_ubjson_high_precision(const NumberType n, const bool use_bjdata, std::true_type /*is_unsigned_64_bits*/) + { + std::array digits; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read + std::size_t first = digits.size(); + auto value = static_cast(n); + do + { + digits[--first] = static_cast('0' + (value % 10)); + value /= 10; + } + while (value != 0); + const std::size_t length = digits.size() - first; + write_number_with_ubjson_prefix(length, true, use_bjdata); + CharType* const out = oa.reserve(digits.size()); + for (std::size_t i = 0; i < length; ++i) + { + out[i] = to_char_type(static_cast(digits[first + i])); + } + oa.commit(length); + } + + /// any other integer: its decimal digits from basic_json::dump() + template + void write_ubjson_high_precision(const NumberType n, const bool use_bjdata, std::false_type /*is_unsigned_64_bits*/) + { + const auto number = BasicJsonType(n).dump(); + write_number_with_ubjson_prefix(number.size(), true, use_bjdata); + for (std::size_t i = 0; i < number.size(); ++i) + { + oa.write_character(to_char_type(static_cast(number[i]))); + } + } + /*! @brief determine the type prefix of container values */ @@ -2193,14 +3810,16 @@ class binary_writer @brief checks whether a JSON number fits into @a TargetType @param[in] el a JSON number of either the signed or unsigned integer kind @return whether @a el's value can be represented by @a TargetType without - wrapping, regardless of which of the two kinds it is stored as + wrapping, regardless of which of the two kinds it is stored as; + false for a value that does not even fit the 64-bit type it is + read as (possible for number types wider than 64 bits) */ template static bool bjdata_ndarray_value_in_range(const BasicJsonType& el) { return el.is_number_unsigned() - ? value_in_range_of(el.template get()) - : value_in_range_of(el.template get()); + ? integer_fits_max(el.m_data.m_value.number_unsigned) && value_in_range_of(static_cast(el.m_data.m_value.number_unsigned)) + : integer_fits(el.m_data.m_value.number_integer) && value_in_range_of(static_cast(el.m_data.m_value.number_integer)); } /*! @@ -2289,7 +3908,7 @@ class binary_writer */ bool write_bjdata_ndarray_float_element(const BasicJsonType& el, const bool dry_run) { - const auto dval = el.template get(); + const auto dval = static_cast(el.m_data.m_value.number_float); if (dry_run) { #ifdef __GNUC__ @@ -2313,6 +3932,90 @@ class binary_writer return true; } + /*! + @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 + in range by a dry run of @ref write_bjdata_ndarray_elements + + The elements are encoded in blocks into space reserved from the sink. + */ + void write_bjdata_ndarray_values(const BasicJsonType& array_data, const CharType dtype) + { + switch (dtype) + { + case 'U': + case 'C': + case 'B': + write_bjdata_ndarray_block(array_data); + break; + case 'i': + write_bjdata_ndarray_block(array_data); + break; + case 'u': + write_bjdata_ndarray_block(array_data); + break; + case 'I': + write_bjdata_ndarray_block(array_data); + break; + case 'm': + write_bjdata_ndarray_block(array_data); + break; + case 'l': + write_bjdata_ndarray_block(array_data); + break; + case 'M': + write_bjdata_ndarray_block(array_data); + break; + case 'L': + write_bjdata_ndarray_block(array_data); + break; + case 'd': + write_bjdata_ndarray_block(array_data); + break; + case 'D': + default: + // 'D' (double) already spans the full range of number_float_t + write_bjdata_ndarray_block(array_data); + break; + } + } + + /// the value of an ND-array element that is an integer, as the integer type that @a T is converted from + template + static T bjdata_ndarray_integer(const BasicJsonType& el, std::true_type /*is_integral*/) noexcept + { + using storage_type = typename std::conditional::value, std::uint64_t, std::int64_t>::type; + return static_cast(el.is_number_unsigned() + ? static_cast(el.m_data.m_value.number_unsigned) + : static_cast(el.m_data.m_value.number_integer)); + } + + /// the value of an ND-array element that is a float + template + static T bjdata_ndarray_integer(const BasicJsonType& el, std::false_type /*is_integral*/) noexcept + { + return static_cast(static_cast(el.m_data.m_value.number_float)); + } + + /// write the elements of an ND-array as @a T, little-endian, in blocks + template + void write_bjdata_ndarray_block(const BasicJsonType& array_data) + { + constexpr std::size_t per_block = max_reserve_size / sizeof(T); + auto it = array_data.m_data.m_value.array->begin(); + const auto end = array_data.m_data.m_value.array->end(); + while (it != end) + { + CharType* const start = oa.reserve(per_block * sizeof(T)); + CharType* out = start; + for (std::size_t n = 0; n < per_block && it != end; ++n, ++it) + { + out = store_number(out, bjdata_ndarray_integer(*it, std::is_integral()), true); + } + oa.commit(static_cast(out - start)); + } + } + /*! @brief validate or write every element of a BJData ND-array's `_ArrayData_` @param[in] array_data the `_ArrayData_` array @@ -2322,6 +4025,12 @@ class binary_writer */ bool write_bjdata_ndarray_elements(const BasicJsonType& array_data, const CharType dtype, const bool dry_run) { + if (!dry_run) + { + write_bjdata_ndarray_values(array_data, dtype); + return true; + } + for (const auto& el : array_data) { bool ok = true; @@ -2432,10 +4141,11 @@ class binary_writer for (const auto& el : dims) { // a dimension is read as an unsigned value below, so anything that - // is not a non-negative integer is rejected: a non-integer entry - // would pun unrelated bytes as the dimension, and a negative one - // would wrap into a nonsensical length - if (!el.is_number_integer() || (!el.is_number_unsigned() && el.template get() < 0)) + // is not a non-negative integer in the range of std::uint64_t is + // rejected: a non-integer entry would pun unrelated bytes as the + // dimension, and a negative or wider one would wrap into a + // nonsensical length + if (!el.is_number_integer() || !bjdata_ndarray_value_in_range(el)) { return true; } @@ -2530,9 +4240,27 @@ class binary_writer @param[in] j JSON value to serialize @param[in,out] string_open whether the output ends with a non-empty string that has not been terminated with 0xFF + @param[in] depth nesting level of @a j, counted from the + top-level value passed to @ref basic_json::to_bon8 + @throw type_error.316 if a string value or an object key is not valid + UTF-8 + @throw out_of_range.407 if an unsigned integer does not fit int64 + @throw type_error.321 if @a j or a value nested in it is discarded + + Values nested deeper than @ref recursion_depth_limit are written by + @ref write_bon8_iterative without the call stack. + + @sa @ref write_cbor + @sa https://github.com/nlohmann/json/issues/5392 */ - void write_bon8_value(const BasicJsonType& j, bool& string_open) + void write_bon8_value(const BasicJsonType& j, bool& string_open, const std::size_t depth = 0) { + if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit()) && (j.is_array() || j.is_object())) + { + write_bon8_iterative(j, string_open); + return; + } + switch (j.type()) { case value_t::null: @@ -2549,9 +4277,9 @@ class binary_writer case value_t::number_unsigned: { - if (j.m_data.m_value.number_unsigned > static_cast((std::numeric_limits::max)())) + if (JSON_HEDLEY_UNLIKELY(!integer_fits_max(j.m_data.m_value.number_unsigned))) { - JSON_THROW(out_of_range::create(407, concat("integer number ", std::to_string(j.m_data.m_value.number_unsigned), " cannot be represented by BON8 as it does not fit int64"), &j)); + throw_integer_out_of_range(j, input_format_t::bon8, "int64"); } write_bon8_integer(static_cast(j.m_data.m_value.number_unsigned)); string_open = false; @@ -2560,6 +4288,10 @@ class binary_writer case value_t::number_integer: { + if (JSON_HEDLEY_UNLIKELY(!integer_fits(j.m_data.m_value.number_integer))) + { + throw_integer_out_of_range(j, input_format_t::bon8, "int64"); + } write_bon8_integer(static_cast(j.m_data.m_value.number_integer)); string_open = false; break; @@ -2584,9 +4316,16 @@ class binary_writer // 0x80..0x84: array with 0..4 elements; 0x85: array ended by 0xFE write_bon8_marker(static_cast(N <= 4 ? 0x80 + N : 0x85), string_open); - for (const auto& el : *j.m_data.m_value.array) + if (N != 0 && j.m_data.m_value.array->front().is_number()) { - write_bon8_value(el, string_open); + write_bon8_array_elements(*j.m_data.m_value.array, string_open, depth); + } + else + { + for (const auto& el : *j.m_data.m_value.array) + { + write_bon8_value(el, string_open, depth + 1); + } } if (N > 4) @@ -2602,11 +4341,7 @@ class binary_writer // 0x86..0x8A: object with 0..4 members; 0x8B: object ended by 0xFE write_bon8_marker(static_cast(N <= 4 ? 0x86 + N : 0x8B), string_open); - for (const auto& el : *j.m_data.m_value.object) - { - write_bon8_string(el.first, string_open, j); - write_bon8_value(el.second, string_open); - } + write_bon8_members(j, string_open, depth); if (N > 4) { @@ -2617,28 +4352,365 @@ class binary_writer case value_t::binary: { - // BON8 has no binary type: write the bytes as an array of - // integers, like UBJSON and BJData do - const auto N = j.m_data.m_value.binary->size(); - write_bon8_marker(static_cast(N <= 4 ? 0x80 + N : 0x85), string_open); - - for (std::size_t i = 0; i < N; ++i) - { - // the cast is needed for binary types whose value type - // is not an integer (e.g., std::byte) - write_bon8_integer(static_cast(j.m_data.m_value.binary->data()[i])); - } - - if (N > 4) - { - write_bon8_marker(0xFE, string_open); - } + write_bon8_binary(*j.m_data.m_value.binary, string_open); break; } case value_t::discarded: default: - break; + throw_on_discarded(j, input_format_t::bon8); + } + } + + /*! + @brief write @a j with @ref write_bon8_value, or write its marker and push + a frame for @ref write_bon8_iterative to continue with its elements + + A scalar, and an empty array or object, are written out in full and not + pushed. + + @sa @ref write_container_value_or_push + */ + void write_bon8_value_or_push(const BasicJsonType& j, bool& string_open, std::vector& stack) + { + if (j.is_array() || j.is_object()) + { + // arrays use the markers 0x80..0x85, objects 0x86..0x8B; the last + // one stands for more than four elements, closed later with 0xFE + const auto N = j.size(); + const std::size_t base = j.is_array() ? 0x80 : 0x86; + write_bon8_marker(static_cast(base + (N <= 4 ? N : 5)), string_open); + if (N != 0) + { + stack.emplace_back(&j); + } + return; + } + + write_bon8_value(j, string_open); + } + + /*! + @brief write out @a root and everything below it without the call stack + + A container with more than four elements is closed with 0xFE once its + last element is written, as in @ref write_bon8_value. + + @sa @ref write_cbor_iterative + */ + void write_bon8_iterative(const BasicJsonType& root, bool& string_open) + { + // only a container with elements is ever pushed; see write_bon8_value_or_push + std::vector stack; + write_bon8_value_or_push(root, string_open, stack); + + while (!stack.empty()) + { + const binary_container_frame current = stack.back(); + const bool is_array = current.value->is_array(); + const bool at_end = is_array + ? current.array_it == current.value->m_data.m_value.array->cend() + : current.object_it == current.value->m_data.m_value.object->cend(); + + if (at_end) + { + if (current.value->size() > 4) + { + write_bon8_marker(0xFE, string_open); + } + stack.pop_back(); + continue; + } + + // read the child before pushing: entering it can move every frame + const BasicJsonType* child = nullptr; + if (is_array) + { + child = &(*current.array_it); + ++stack.back().array_it; + } + else + { + write_bon8_string(current.object_it->first, string_open, *current.value); + child = &(current.object_it->second); + ++stack.back().object_it; + } + write_bon8_value_or_push(*child, string_open, stack); + } + } + + /*! + @brief encode an integer as @ref write_bon8_integer writes it + @param[in] out where the encoding goes; at most 9 characters are written + @return the end of the encoding + */ + CharType* encode_bon8_integer(CharType* out, std::int64_t value) const noexcept + { + if (value < (std::numeric_limits::min)() || value > (std::numeric_limits::max)()) + { + *out++ = to_char_type(0x8D); + return store_number(out, value, false); + } + if (value < -33818506 || value > 67637031) + { + *out++ = to_char_type(0x8C); + return store_number(out, static_cast(value), false); + } + if (value <= -264075) + { + value = -(value + 264075); + return put_bon8_bytes(out, 0xF0 + ((value >> 22) & 0x07), 0xC0 + ((value >> 16) & 0x3F), value >> 8, value); + } + if (value <= -1931) + { + value = -(value + 1931); + return put_bon8_bytes(out, 0xE0 + ((value >> 14) & 0x0F), 0xC0 + ((value >> 8) & 0x3F), value); + } + if (value <= -11) + { + value = -(value + 11); + return put_bon8_bytes(out, 0xC2 + ((value >> 6) & 0x1F), 0xC0 + (value & 0x3F)); + } + if (value <= -1) + { + return put_bon8_bytes(out, 0xB8 - (value + 1)); + } + if (value <= 39) + { + return put_bon8_bytes(out, 0x90 + value); + } + if (value <= 3879) + { + value -= 40; + return put_bon8_bytes(out, 0xC2 + ((value >> 7) & 0x1F), value & 0x7F); + } + if (value <= 528167) + { + value -= 3880; + return put_bon8_bytes(out, 0xE0 + ((value >> 15) & 0x0F), (value >> 8) & 0x7F, value); + } + value -= 528168; + return put_bon8_bytes(out, 0xF0 + ((value >> 23) & 0x07), (value >> 16) & 0x7F, value >> 8, value); + } + + /// store the low byte of each argument at @a out; return the end + template + CharType* put_bon8_bytes(CharType* out, const Bytes... bytes) const noexcept + { + const std::array buffer{{to_char_type(static_cast(bytes & 0xFF))...}}; + std::memcpy(out, buffer.data(), buffer.size()); + return out + buffer.size(); + } + + /*! + @brief encode a float as @ref write_bon8_float writes it + @param[in] out where the encoding goes; at most 9 characters are written + @return the end of the encoding + */ + CharType* encode_bon8_float(CharType* out, const number_float_t n) const noexcept + { +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_PUSH + JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") +#endif + if (n == static_cast(-1)) + { + *out++ = to_char_type(0xFB); + return out; + } + if (n == static_cast(0) && !std::signbit(n)) + { + *out++ = to_char_type(0xFC); + return out; + } + if (n == static_cast(1)) + { + *out++ = to_char_type(0xFD); + return out; + } + if (std::isnan(n)) + { + return put_bon8_bytes(out, 0x8E, 0x7F, 0x80, 0x00, 0x01); + } + return out + encode_compact_float(out, n, to_char_type(0x8E), to_char_type(0x8F)); +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_POP +#endif + } + + /*! + @brief write the elements of an array that starts with a number, with runs + of numbers encoded without a call of @ref write_bon8_value for each + + Same output as calling write_bon8_value for each element. The numbers of a + run are encoded into space reserved from the sink, as in + @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 + from write_bon8_value, like any other value that is not a number. Kept out + of line, like @ref write_cbor_array. + */ + template + JSON_HEDLEY_NEVER_INLINE + void write_bon8_array_elements(const ArrayType& array, bool& string_open, const std::size_t depth) + { + constexpr std::size_t max_run = max_reserve_size / 9; + auto it = array.begin(); + const auto end = array.end(); + while (it != end) + { + if (!it->is_number()) + { + write_bon8_value(*it, string_open, depth + 1); + ++it; + continue; + } + + CharType* const start = oa.reserve(max_run * 9); + CharType* out = start; + bool unencodable = false; + for (std::size_t n = 0; n < max_run && it != end && it->is_number(); ++n) + { + if (it->is_number_float()) + { + out = encode_bon8_float(out, it->m_data.m_value.number_float); + } + else if (JSON_HEDLEY_UNLIKELY((it->m_data.m_type == value_t::number_unsigned + && !integer_fits_max(it->m_data.m_value.number_unsigned)) + || (it->m_data.m_type == value_t::number_integer + && !integer_fits(it->m_data.m_value.number_integer)))) + { + unencodable = true; + break; + } + else + { + out = encode_bon8_integer(out, it->m_data.m_type == value_t::number_integer + ? static_cast(it->m_data.m_value.number_integer) + : static_cast(it->m_data.m_value.number_unsigned)); + } + ++it; + } + oa.commit(static_cast(out - start)); + if (out != start) + { + // a number follows a string without terminating it + string_open = false; + } + + if (unencodable) + { + write_bon8_value(*it, string_open, depth + 1); + ++it; + } + } + } + + /*! + @brief write the members of the object @a j as BON8 + + A member whose value is null, a boolean, a number, or a string is written + without a call of @ref write_bon8_value. Kept out of line, like + @ref write_cbor_object. + */ + JSON_HEDLEY_NEVER_INLINE + void write_bon8_members(const BasicJsonType& j, bool& string_open, const std::size_t depth) + { + for (const auto& el : *j.m_data.m_value.object) + { + write_bon8_string(el.first, string_open, j); + + const BasicJsonType& value = el.second; + switch (value.type()) + { + case value_t::null: + write_bon8_marker(0xFA, string_open); + break; + + case value_t::boolean: + write_bon8_marker(value.m_data.m_value.boolean ? 0xF9 : 0xF8, string_open); + break; + + case value_t::string: + write_bon8_string(*value.m_data.m_value.string, string_open, value); + break; + + case value_t::number_integer: + if (JSON_HEDLEY_LIKELY(integer_fits(value.m_data.m_value.number_integer))) + { + write_bon8_integer(static_cast(value.m_data.m_value.number_integer)); + string_open = false; + break; + } + // beyond 64 bits (number types wider than that): throws + write_bon8_value(value, string_open, depth + 1); + break; + + case value_t::number_float: + write_bon8_float(value.m_data.m_value.number_float); + string_open = false; + break; + + case value_t::number_unsigned: + if (JSON_HEDLEY_LIKELY(integer_fits_max(value.m_data.m_value.number_unsigned))) + { + write_bon8_integer(static_cast(value.m_data.m_value.number_unsigned)); + string_open = false; + break; + } + // too large for BON8: throws + write_bon8_value(value, string_open, depth + 1); + break; + + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::discarded: + default: + write_bon8_value(value, string_open, depth + 1); + break; + } + } + } + + /*! + @brief write a binary value as BON8 + + BON8 has no binary type: the bytes are written as an array of integers, + like UBJSON and BJData do. Kept out of line, so that write_bon8_value does + not carry the registers of its loop. + */ + JSON_HEDLEY_NEVER_INLINE + void write_bon8_binary(const binary_t& binary, bool& string_open) + { + const auto N = binary.size(); + write_bon8_marker(static_cast(N <= 4 ? 0x80 + N : 0x85), string_open); + + // a byte is an integer of 0 to 255: one byte up to 39, two bytes above + const auto* const data = binary.data(); + for (std::size_t i = 0; i < N;) + { + const std::size_t block = (std::min)(N - i, max_reserve_size / 2); + CharType* const start = oa.reserve(2 * block); + CharType* out = start; + for (std::size_t k = 0; k < block; ++k, ++i) + { + // the cast is needed for binary types whose value type + // is not an integer (e.g., std::byte) + const auto value = static_cast(static_cast(data[i])); + // both bytes are stored, the second only counts for a value above 39: + // 0xC2 + ((value - 40) >> 7), (value - 40) & 0x7F + const unsigned wide = value - 40u; + const bool is_wide = value > 39u; + out[0] = to_char_type(static_cast(is_wide ? 0xC2u + (wide >> 7u) : 0x90u + value)); + out[1] = to_char_type(static_cast(wide & 0x7Fu)); + out += is_wide ? 2 : 1; + } + oa.commit(static_cast(out - start)); + } + + if (N > 4) + { + write_bon8_marker(0xFE, string_open); } } @@ -2683,7 +4755,7 @@ class binary_writer } else { - oa.write_characters(reinterpret_cast(s.data()), s.size()); + write_bytes(s.data(), s.size()); string_open = true; } } @@ -2699,15 +4771,25 @@ class binary_writer */ static void check_utf8(const string_t& s, const BasicJsonType& context) { - static_cast(context); // only used when exceptions are enabled const auto* data = reinterpret_cast(s.data()); const std::size_t valid = valid_utf8_prefix(data, s.size()); if (JSON_HEDLEY_UNLIKELY(valid != s.size())) { - JSON_THROW(type_error::create(316, concat("invalid UTF-8 byte at index ", std::to_string(valid), ": 0x", detail::hex_byte(data[valid])), &context)); + throw_invalid_utf8(data, valid, context); } } + /// @brief throw type_error.316 for the invalid byte at @a index of @a data; + /// out of line, so that the message does not burden check_utf8() + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_invalid_utf8(const unsigned char* data, const std::size_t index, const BasicJsonType& context) + { + static_cast(data); // only used when exceptions are enabled + static_cast(index); + static_cast(context); + JSON_THROW(type_error::create(exception_id::invalid_utf8, concat("invalid UTF-8 byte at index ", std::to_string(index), ": 0x", detail::hex_byte(data[index])), &context)); + } + /*! @brief return @a s as it should be written, honoring @ref error_handler @@ -2730,6 +4812,11 @@ class binary_writer itself in every case but a sanitized `replace`/`ignore` one, so @a storage must outlive the returned reference only then. + @a s must be an lvalue that outlives the returned reference. An object key + whose `key_type` is not @ref string_t must therefore first be converted + into a named string_t (see @ref object_key_string_t); the deleted overload + below enforces this at compile time. + @param[in] s the string (value or object key) to write @param[in] context the value @a s belongs to (for diagnostics) @param[out] storage backing storage for a sanitized copy @@ -2759,6 +4846,35 @@ class binary_writer } } + /*! + @brief the write-pass counterpart of @ref sanitize_utf8_for_write for BSON + + The size pass (@ref calc_bson_entry_header_size, @ref + calc_bson_string_size) has already validated @a s, and thrown for @ref + error_handler_t::strict if it was ill-formed. Here, @ref + error_handler_t::strict therefore behaves like @ref error_handler_t::keep, + and @ref error_handler_t::replace / @ref error_handler_t::ignore produce + exactly the bytes the size pass measured. Hence no context is needed. + + @param[in] s the string (value or object key) to write + @param[out] storage backing storage for a sanitized copy + + @return a reference to @a s, or to @a storage once it holds a sanitized copy + */ + const string_t& sanitize_utf8_for_bson_write(const string_t& s, string_t& storage) const + { + if (error_handler == error_handler_t::keep || error_handler == error_handler_t::strict || is_valid_utf8(s)) + { + return s; // NOLINT(bugprone-return-const-ref-from-parameter): callers pass lvalues that outlive the call + } + storage = sanitize_utf8(s, error_handler); + return storage; + } + + /// deleted: anything but a string_t would bind a temporary that dies before the returned reference is used + template < typename T, enable_if_t < !std::is_same::value, int > = 0 > + const string_t& sanitize_utf8_for_write(const T& /*s*/, const BasicJsonType& /*context*/, string_t& /*storage*/) const = delete; // NOLINT(hicpp-use-equals-delete,modernize-use-equals-delete): a private helper's guard, not part of the interface + /*! @brief write an integer in the shortest encoding @@ -2875,53 +4991,14 @@ class binary_writer // Utility functions // /////////////////////// - // single-instruction byte swaps (compilers lower these to bswap/rev/movbe); - // used to emit big-endian numbers without a per-byte std::reverse loop - static std::uint16_t byte_swap(std::uint16_t x) noexcept - { -#if defined(__GNUC__) || defined(__clang__) - return __builtin_bswap16(x); -#elif defined(_MSC_VER) - return _byteswap_ushort(x); -#else - return static_cast((x >> 8) | (x << 8)); -#endif - } - - static std::uint32_t byte_swap(std::uint32_t x) noexcept - { -#if defined(__GNUC__) || defined(__clang__) - return __builtin_bswap32(x); -#elif defined(_MSC_VER) - return _byteswap_ulong(x); -#else - return ((x & 0x000000FFu) << 24) | ((x & 0x0000FF00u) << 8) - | ((x & 0x00FF0000u) >> 8) | ((x & 0xFF000000u) >> 24); -#endif - } - - static std::uint64_t byte_swap(std::uint64_t x) noexcept - { -#if defined(__GNUC__) || defined(__clang__) - return __builtin_bswap64(x); -#elif defined(_MSC_VER) - return _byteswap_uint64(x); -#else - x = ((x & 0x00000000FFFFFFFFull) << 32) | ((x & 0xFFFFFFFF00000000ull) >> 32); - x = ((x & 0x0000FFFF0000FFFFull) << 16) | ((x & 0xFFFF0000FFFF0000ull) >> 16); - x = ((x & 0x00FF00FF00FF00FFull) << 8) | ((x & 0xFF00FF00FF00FF00ull) >> 8); - return x; -#endif - } - /*! @brief reverse the bytes of a buffer by byte-swapping it as UIntType Loading the buffer into an unsigned integer of the same width and swapping that is what lets the compiler emit a single bswap/rev/movbe; reversing the buffer element by element does not reliably get there (clang keeps a scalar - shuffle). The two memcpy calls are the only portable way to reinterpret the - bytes and are folded away by every optimizer. + shuffle). (The two memcpy calls, rather than bit_cast, keep GCC from + inlining differently in C++20, which made writing slower.) */ template static void byte_swap_buffer(std::array& a) noexcept @@ -2929,11 +5006,11 @@ class binary_writer static_assert(sizeof(UIntType) == N, "swap width must match the buffer size"); UIntType v{}; std::memcpy(&v, a.data(), sizeof(v)); - v = byte_swap(v); + v = byteswap(v); std::memcpy(a.data(), &v, sizeof(v)); } - // reverse the bytes of a fixed-size buffer; a single byte_swap() for the + // reverse the bytes of a fixed-size buffer; a single byteswap() for the // common 2/4/8-byte number payloads, std::reverse for any other size static void reverse_bytes(std::array& a) noexcept { @@ -2986,6 +5063,67 @@ class binary_writer oa.write_characters(vec.data(), sizeof(NumberType)); } + /// whether @a n is written as a float32 by @ref write_compact_float + static bool fits_float32(const number_float_t n) noexcept + { + static_assert(std::is_same::value || std::is_same::value, + "number_float_t must be float or double for the CBOR/MessagePack/BON8 writer"); +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_PUSH + JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") +#endif +#if defined(__GNUC__) && !defined(__clang__) && (__GNUC__ >= 7) + JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wduplicated-branches") +#endif + return !std::isfinite(n) || ((static_cast(n) >= static_cast(std::numeric_limits::lowest()) && + static_cast(n) <= static_cast((std::numeric_limits::max)()) && + static_cast(static_cast(n)) == static_cast(n))); +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_POP +#endif + } + + /*! + @brief encode @a n to @a out as @ref write_compact_float writes it + @return the number of characters, 5 or 9 + */ + std::size_t encode_compact_float(CharType* out, const number_float_t n, const CharType float32_marker, const CharType float64_marker) const noexcept + { + if (fits_float32(n)) + { + *out = float32_marker; + store_number(out + 1, static_cast(n), false); + return 1 + sizeof(float); + } + *out = float64_marker; + store_number(out + 1, n, false); + return 1 + sizeof(number_float_t); + } + + /*! + @brief encode a float as @ref write_cbor writes it + @return the number of characters, 3, 5 or 9 + */ + std::size_t encode_cbor_float(CharType* out, const number_float_t n) const noexcept + { + if (JSON_HEDLEY_UNLIKELY(!std::isfinite(n))) + { + // NaN is 0xf97e00, Infinity is 0xf97c00, -Infinity is 0xf9fc00 + out[0] = to_char_type(0xF9); + if (std::isnan(n)) + { + out[1] = to_char_type(0x7E); + } + else + { + out[1] = n > 0 ? to_char_type(0x7C) : to_char_type(0xFC); + } + out[2] = to_char_type(0x00); + return 3; + } + return encode_compact_float(out, n, to_char_type(0xFA), to_char_type(0xFB)); + } + /// @brief write @a n using @a float32_marker if it round-trips through /// float, otherwise using @a float64_marker /// @@ -2995,24 +5133,7 @@ class binary_writer /// no longer needs to be passed in. void write_compact_float(const number_float_t n, const CharType float32_marker, const CharType float64_marker) { - static_assert(std::is_same::value || std::is_same::value, - "number_float_t must be float or double for the CBOR/MessagePack/BON8 writer"); -#ifdef __GNUC__ - JSON_HEDLEY_DIAGNOSTIC_PUSH - JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") -#endif - // When number_float_t is float, static_cast(n) is the identity and - // both branches below are intentionally identical (the "compact" float - // representation is the value itself). Only GCC diagnoses this, and only - // when the sink calls are inlined; clang has no such warning. - // (-Wduplicated-branches only exists from GCC 7 on; naming it on an older - // GCC would itself warn under -Wpragmas) -#if defined(__GNUC__) && !defined(__clang__) && (__GNUC__ >= 7) - JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wduplicated-branches") -#endif - if (!std::isfinite(n) || ((static_cast(n) >= static_cast(std::numeric_limits::lowest()) && - static_cast(n) <= static_cast((std::numeric_limits::max)()) && - static_cast(static_cast(n)) == static_cast(n)))) + if (fits_float32(n)) { oa.write_character(float32_marker); write_number(static_cast(n)); @@ -3022,9 +5143,6 @@ class binary_writer oa.write_character(float64_marker); write_number(n); } -#ifdef __GNUC__ - JSON_HEDLEY_DIAGNOSTIC_POP -#endif } public: @@ -3058,9 +5176,7 @@ class binary_writer #endif static_assert(sizeof(std::uint8_t) == sizeof(CharType), "size of CharType must be equal to std::uint8_t"); - CharType result; - std::memcpy(&result, &x, sizeof(x)); - return result; + return bit_cast(x); } template // max +#include // array #include // size_t +#include // memcpy #include // shared_ptr, make_shared #include // basic_string #include // conditional, integral_constant, is_same @@ -48,32 +51,258 @@ template struct output_adapter_protocol template using output_adapter_t = std::shared_ptr>; +/// the largest number of characters a writer may reserve() from a sink at once; +/// output_vector_sink also appends writes of at least this size with insert() +constexpr std::size_t max_reserve_size = 256; + +/// @brief copy @a length characters, at most 32, without a call of memcpy() +/// +/// Short strings are the typical payload of a binary value, and a call of +/// memcpy() costs more than copying them. A copy of 16 to 32 characters is two +/// possibly overlapping 16-byte copies, one of 8 to 15 two 8-byte copies, and +/// so on down to a single character; each is a fixed-size load and store. +/// Nothing outside of [src, src + length) is read or written. +template +inline void copy_short(CharType* dst, const CharType* src, const std::size_t length, std::true_type /*is_byte*/) noexcept +{ + JSON_ASSERT(length <= 32); + if (length >= 16) + { + std::array first; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read + std::array last; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read + std::memcpy(first.data(), src, 16); + std::memcpy(last.data(), src + (length - 16), 16); + std::memcpy(dst, first.data(), 16); + std::memcpy(dst + (length - 16), last.data(), 16); + } + else if (length >= 8) + { + std::array first; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read + std::array last; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read + std::memcpy(first.data(), src, 8); + std::memcpy(last.data(), src + (length - 8), 8); + std::memcpy(dst, first.data(), 8); + std::memcpy(dst + (length - 8), last.data(), 8); + } + else if (length >= 4) + { + std::array first; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read + std::array last; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read + std::memcpy(first.data(), src, 4); + std::memcpy(last.data(), src + (length - 4), 4); + std::memcpy(dst, first.data(), 4); + std::memcpy(dst + (length - 4), last.data(), 4); + } + else if (length != 0) + { + // 1 to 3: the first, the middle, and the last character + const CharType a = src[0]; + const CharType b = src[length >> 1]; + const CharType c = src[length - 1]; + dst[0] = a; + dst[length >> 1] = b; + dst[length - 1] = c; + } +} + +/// @brief copy @a length characters, of a character type wider than a byte +template +inline void copy_short(CharType* dst, const CharType* src, const std::size_t length, std::false_type /*is_byte*/) noexcept +{ + std::memcpy(dst, src, length * sizeof(CharType)); +} + +/// the number of characters an empty vector is grown to by output_vector_sink +constexpr std::size_t min_vector_sink_capacity = 64; + /// @brief non-virtual output sink writing into a std::vector /// /// This sink is not part of the virtual output_adapter_protocol hierarchy: it is /// passed to binary_writer by value as a template parameter, so /// write_character()/write_characters() are ordinary (inlinable) calls with no /// vtable lookup and no shared_ptr. It is used for the common -/// `to_cbor`/`to_msgpack`/... into a std::vector. output_vector_adapter below -/// wraps this same sink to provide the virtual interface. +/// `to_cbor`/`to_msgpack`/... into a std::vector. +/// +/// Most writes are a few bytes - a type marker, or a number of 1, 2, 4, or 8 +/// bytes - and appending those with push_back() or insert() cost more than +/// encoding them: insert() is an out-of-line call into a routine that can insert +/// anywhere, and resize() is one as well with libstdc++. The sink therefore keeps +/// its own write position: it grows the vector geometrically ahead of the +/// position and copies each write to it, so a write is a capacity check and a +/// short copy. The destructor shrinks the vector to the bytes actually written, +/// also when a write throws; shrinking a vector never allocates. The sink owns +/// that trim, so it can be moved but not copied, and the vector must not be used +/// while the sink is alive. template> class output_vector_sink { public: explicit output_vector_sink(std::vector& vec) noexcept + : v(&vec), cursor(vec.data() + vec.size()), limit(cursor) + {} + + output_vector_sink(const output_vector_sink&) = delete; + output_vector_sink& operator=(const output_vector_sink&) = delete; + output_vector_sink& operator=(output_vector_sink&&) = delete; + + output_vector_sink(output_vector_sink&& other) noexcept + : v(other.v), cursor(other.cursor), limit(other.limit) + { + other.v = nullptr; + } + + ~output_vector_sink() + { + if (v != nullptr) + { + v->resize(position()); + } + } + + // always inlined: GCC otherwise keeps these out of line in a large + // translation unit, and a call per byte or number costs more than the write + JSON_HEDLEY_ALWAYS_INLINE + void write_character(CharType c) + { + if (JSON_HEDLEY_UNLIKELY(cursor == limit)) + { + grow(1); + } + *cursor++ = c; + } + + // no JSON_HEDLEY_NON_NULL here: binary_writer legitimately passes a null + // pointer with length 0 for empty strings/binary values; memcpy must not + // see it. + JSON_HEDLEY_ALWAYS_INLINE + void write_characters(const CharType* s, std::size_t length) + { + if (JSON_HEDLEY_UNLIKELY(space() < length)) + { + if (length >= max_reserve_size) + { + append(s, length); + return; + } + grow(length); + } + if (length <= 32) + { + copy_short(cursor, s, length, std::integral_constant()); + cursor += length; + } + else + { + std::memcpy(cursor, s, length * sizeof(CharType)); + cursor += length; + } + } + + /// @brief space for writing up to @a length characters directly + /// + /// Returns a pointer to at least @a length (at most @ref max_reserve_size) + /// writable characters at the write position; @ref commit then keeps the + /// first ones of them. A writer can store whole words there and keep only + /// the bytes it needs, and keep its write pointer in a register across many + /// values instead of updating the sink after each. + JSON_HEDLEY_ALWAYS_INLINE + CharType* reserve(const std::size_t length) + { + JSON_ASSERT(length <= max_reserve_size); + if (JSON_HEDLEY_UNLIKELY(space() < length)) + { + grow(length); + } + return cursor; + } + + /// keep the first @a length characters written to the space from reserve() + JSON_HEDLEY_ALWAYS_INLINE + void commit(const std::size_t length) noexcept + { + cursor += length; + } + + /// nothing to pass on: the characters are in the vector (see output_adapter_sink) + void flush() noexcept + {} + + private: + /// number of characters of *v written so far + std::size_t position() const noexcept + { + return static_cast(cursor - v->data()); + } + + /// number of characters that fit the vector behind the write position + std::size_t space() const noexcept + { + return static_cast(limit - cursor); + } + + /// make room for at least @a length more characters after the position; + /// kept out of line so that the write functions stay small enough for + /// GCC to inline them into the writer + JSON_HEDLEY_NEVER_INLINE + void grow(const std::size_t length) + { + // use the capacity the vector already has, else at least double it; + // never less than min_vector_sink_capacity, so that a small value does not + // reallocate for every few bytes + const std::size_t written = position(); + const std::size_t needed = written + length; + v->resize((std::max)(min_vector_sink_capacity, needed <= v->capacity() + ? v->capacity() + : (std::max)(needed, v->capacity() * 2))); + cursor = v->data() + written; + limit = v->data() + v->size(); + } + + /// append @a length (at least max_reserve_size) characters that do not fit + /// the space ahead of the position: insert() copies them once into the + /// grown vector, where grow() would first zero the new space and then copy + /// over it - noticeable for a large string or binary value + JSON_HEDLEY_NEVER_INLINE + void append(const CharType* s, const std::size_t length) + { + const std::size_t written = position(); + v->resize(written); + if (v->capacity() - written < length) + { + // grow geometrically, with room for the writes that follow, so + // that the next grow() need not reallocate right away + v->reserve((std::max)(v->capacity() * 2, written + length + max_reserve_size)); + } + v->insert(v->end(), s, s + length); + cursor = v->data() + v->size(); + limit = cursor; + } + + std::vector* v; + /// the write position: in *v, at the number of characters written so far + CharType* cursor; + /// the end of *v + CharType* limit; +}; + +/// output adapter for byte vectors +template> +class output_vector_adapter : public output_adapter_protocol +{ + public: + explicit output_vector_adapter(std::vector& vec) noexcept : v(vec) {} - void write_character(CharType c) + void write_character(CharType c) override { v.push_back(c); } // no JSON_HEDLEY_NON_NULL here: binary_writer legitimately passes a null - // pointer with length 0 for empty strings/binary values. Appending an empty - // range is a no-op; the type-erased path tolerates this via the (unattributed) - // virtual base, and the concrete sink must do the same. - void write_characters(const CharType* s, std::size_t length) + // pointer with length 0 for empty strings/binary values; appending an empty + // range is a no-op + void write_characters(const CharType* s, std::size_t length) override { v.insert(v.end(), s, s + length); } @@ -82,33 +311,6 @@ class output_vector_sink std::vector& v; }; -/// output adapter for byte vectors -/// -/// The appending itself lives in output_vector_sink; this class only adds the -/// virtual output_adapter_protocol interface on top of it, so both the -/// type-erased and the templated path share one implementation. -template> -class output_vector_adapter : public output_adapter_protocol -{ - public: - explicit output_vector_adapter(std::vector& vec) noexcept - : sink(vec) - {} - - void write_character(CharType c) override - { - sink.write_character(c); - } - - void write_characters(const CharType* s, std::size_t length) override - { - sink.write_characters(s, length); - } - - private: - output_vector_sink sink; -}; - #ifndef JSON_NO_IO /// output adapter for output streams template @@ -163,9 +365,17 @@ class output_string_adapter : public output_adapter_protocol /// /// Wraps the polymorphic output_adapter_t so the same binary_writer template can /// also target arbitrary adapters (output streams, strings, user-provided -/// adapters) via the `output_adapter`-based overloads. Each write still goes -/// through one virtual call, exactly as before; only the concrete sinks above -/// avoid it. +/// adapters) via the `output_adapter`-based overloads. +/// +/// A write through the adapter is a virtual call, and for a stream a call with +/// a sentry on top, which costs more than encoding the few bytes of most values. +/// The sink therefore collects what is written in a buffer and passes it on in +/// one piece when it is full, a write is too large for it, or @ref flush is +/// called. Everything written reaches the adapter in the same order and with +/// the same bytes; the destructor passes on what is left, so that a write that +/// throws leaves the adapter with exactly the bytes written before it. Only +/// @ref flush can report an error of the adapter (the destructor must not +/// throw), so it has to be called once the value is written. template class output_adapter_sink { @@ -176,20 +386,121 @@ class output_adapter_sink JSON_ASSERT(oa); } + output_adapter_sink(const output_adapter_sink&) = delete; + output_adapter_sink& operator=(const output_adapter_sink&) = delete; + output_adapter_sink& operator=(output_adapter_sink&&) = delete; + + output_adapter_sink(output_adapter_sink&& other) noexcept + : oa(std::move(other.oa)), used(other.used) + { + std::memcpy(buffer.data(), other.buffer.data(), other.used * sizeof(CharType)); + other.used = 0; + } + + ~output_adapter_sink() + { + // a normal return has flushed already, so this is for a write that threw + if (used != 0) + { + JSON_TRY + { + flush(); + } + JSON_CATCH(...) + { + // nothing can be done about an adapter that throws here + } + } + } + + JSON_HEDLEY_ALWAYS_INLINE void write_character(CharType c) { - oa->write_character(c); + if (JSON_HEDLEY_UNLIKELY(used == buffer_size)) + { + flush(); + } + buffer[used++] = c; } // no JSON_HEDLEY_NON_NULL: forwards (null, 0) for empty payloads, exactly as // the type-erased path already did before this sink existed + JSON_HEDLEY_ALWAYS_INLINE void write_characters(const CharType* s, std::size_t length) { - oa->write_characters(s, length); + if (JSON_HEDLEY_UNLIKELY(buffer_size - used < length)) + { + write_large(s, length); + return; + } + if (length <= 32) + { + copy_short(buffer.data() + used, s, length, std::integral_constant()); + used += length; + } + else + { + std::memcpy(buffer.data() + used, s, length * sizeof(CharType)); + used += length; + } + } + + /// space for writing up to @a length characters (see + /// output_vector_sink::reserve) + JSON_HEDLEY_ALWAYS_INLINE + CharType* reserve(const std::size_t length) + { + JSON_ASSERT(length <= max_reserve_size); + if (JSON_HEDLEY_UNLIKELY(buffer_size - used < length)) + { + flush(); + } + return buffer.data() + used; + } + + /// keep the first @a length characters written to the space from reserve() + JSON_HEDLEY_ALWAYS_INLINE + void commit(const std::size_t length) noexcept + { + used += length; + } + + /// pass everything written so far on to the adapter + void flush() + { + if (used != 0) + { + // reset first: the characters count as written even if the adapter throws + const std::size_t length = used; + used = 0; + oa->write_characters(buffer.data(), length); + } } private: + /// a write that does not fit the space left in the buffer + JSON_HEDLEY_NEVER_INLINE + void write_large(const CharType* s, const std::size_t length) + { + flush(); + if (length >= buffer_size) + { + oa->write_characters(s, length); + } + else + { + std::memcpy(buffer.data(), s, length * sizeof(CharType)); + used = length; + } + } + + /// large enough for reserve() at any time + static constexpr std::size_t buffer_size = 4 * max_reserve_size; + output_adapter_t oa; + /// number of characters of @ref buffer not yet passed on + std::size_t used = 0; + std::array buffer{}; }; /// @brief whether std::basic_string has a non-deprecated std::char_traits diff --git a/include/nlohmann/detail/output/serializer.hpp b/include/nlohmann/detail/output/serializer.hpp index 8306440ef..598fda98a 100644 --- a/include/nlohmann/detail/output/serializer.hpp +++ b/include/nlohmann/detail/output/serializer.hpp @@ -49,6 +49,12 @@ class serializer using number_float_t = typename BasicJsonType::number_float_t; using number_integer_t = typename BasicJsonType::number_integer_t; using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + /// type of the absolute value of an integer: number_unsigned_t, or its + /// unsigned counterpart if it is signed (it may be number_integer_t, #2573) + using number_magnitude_t = typename std::conditional < + std::is_integral::value && std::is_signed::value, + std::make_unsigned, + std::enable_if>::type::type; using binary_char_t = typename BasicJsonType::binary_t::value_type; public: @@ -65,7 +71,7 @@ class serializer None of @a pretty_print_, @a ensure_ascii_ and @a indent_step_ change over the life of the serializer, so they are captured once here instead of - being threaded through every call to @ref dump, @ref dump_internal and + being threaded through every call to @ref dump, @ref dump_container and @ref dump_iteratively. */ serializer(output_adapter_protocol& s, const char ichar, @@ -74,11 +80,14 @@ class serializer const std::size_t indent_step_ = 0, error_handler_t error_handler_ = error_handler_t::strict) : o(&s) + , number_buffer() // NOLINT(readability-redundant-member-init) + , string_buffer() // NOLINT(readability-redundant-member-init) , indent_char(ichar) , pretty_print(pretty_print_) , ensure_ascii(ensure_ascii_) , indent_step(indent_step_) , error_handler(error_handler_) + , write_buffer() // NOLINT(readability-redundant-member-init) {} // deleted because of pointer members @@ -109,18 +118,18 @@ class serializer void dump(const BasicJsonType& val, const std::size_t current_indent = 0) { - dump_internal(val, current_indent); + dump_element(val, current_indent, 0); flush(); } JSON_PRIVATE_UNLESS_TESTED: /*! - @brief worker for @ref dump + @brief serialize the object or array @a val - Identical in behavior to the historical @ref dump, but writes into the - serializer's internal @ref write_buffer instead of issuing a virtual call - per token. The public @ref dump wraps this and flushes the buffer once the - top-level value has been serialized. + Writes into the serializer's internal @ref write_buffer instead of issuing + a virtual call per token; the public @ref dump flushes the buffer once the + top-level value has been serialized. Scalars are written by @ref dump_scalar, + which @ref dump_element calls without entering this function. Serializing a container descends into its elements, so a value nested deeply enough used to exhaust the call stack and terminate the process with no @@ -131,167 +140,149 @@ class serializer @sa https://github.com/nlohmann/json/issues/5387 */ - void dump_internal(const BasicJsonType& val, - const std::size_t current_indent = 0, - const std::size_t depth = 0) + void dump_container(const BasicJsonType& val, + const std::size_t current_indent, + const std::size_t depth) { - switch (val.m_data.m_type) + JSON_ASSERT(val.m_data.m_type == value_t::object || val.m_data.m_type == value_t::array); + + if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit())) { - case value_t::object: + dump_iteratively(val, current_indent); + return; + } + + if (val.m_data.m_type == value_t::object) + { + if (val.m_data.m_value.object->empty()) { - if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit())) - { - dump_iteratively(val, current_indent); - return; - } - - if (val.m_data.m_value.object->empty()) - { - put_literal("{}"); - return; - } - - if (pretty_print) - { - put_literal("{\n"); - - // variable to hold indentation for recursive calls - const auto new_indent = next_indent(current_indent, indent_step); - - // first n-1 elements - auto i = val.m_data.m_value.object->cbegin(); - for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i) - { - put_indent(new_indent); - put_char('"'); - dump_escaped(i->first); - put_literal("\": "); - dump_internal(i->second, new_indent, depth + 1); - put_literal(",\n"); - } - - // last element - JSON_ASSERT(i != val.m_data.m_value.object->cend()); - JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend()); - put_indent(new_indent); - put_char('"'); - dump_escaped(i->first); - put_literal("\": "); - dump_internal(i->second, new_indent, depth + 1); - - put_char('\n'); - put_indent(current_indent); - put_char('}'); - } - else - { - put_char('{'); - - // first n-1 elements - auto i = val.m_data.m_value.object->cbegin(); - for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i) - { - put_char('"'); - dump_escaped(i->first); - put_literal("\":"); - dump_internal(i->second, current_indent, depth + 1); - put_char(','); - } - - // last element - JSON_ASSERT(i != val.m_data.m_value.object->cend()); - JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend()); - put_char('"'); - dump_escaped(i->first); - put_literal("\":"); - dump_internal(i->second, current_indent, depth + 1); - - put_char('}'); - } - + put_literal("{}"); return; } - case value_t::array: + if (pretty_print) { - if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit())) + // variable to hold indentation for recursive calls + const auto new_indent = next_indent(current_indent, indent_step); + + put_literal_indent("{\n", new_indent); + + // first n-1 elements + auto i = val.m_data.m_value.object->cbegin(); + for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i) { - dump_iteratively(val, current_indent); - return; + put_object_key(i->first, "\": "); + dump_element(i->second, new_indent, depth + 1); + put_literal_indent(",\n", new_indent); } - if (val.m_data.m_value.array->empty()) + // last element + JSON_ASSERT(i != val.m_data.m_value.object->cend()); + JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend()); + put_object_key(i->first, "\": "); + dump_element(i->second, new_indent, depth + 1); + + put_literal_indent("\n", current_indent); + put_char('}'); + } + else + { + put_char('{'); + + // first n-1 elements + auto i = val.m_data.m_value.object->cbegin(); + for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i) { - put_literal("[]"); - return; + put_object_key(i->first, "\":"); + dump_element(i->second, 0, depth + 1); + put_char(','); } - if (pretty_print) - { - put_literal("[\n"); + // last element + JSON_ASSERT(i != val.m_data.m_value.object->cend()); + JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend()); + put_object_key(i->first, "\":"); + dump_element(i->second, 0, depth + 1); - // variable to hold indentation for recursive calls - const auto new_indent = next_indent(current_indent, indent_step); + put_char('}'); + } + return; + } - // first n-1 elements - for (auto i = val.m_data.m_value.array->cbegin(); - i != val.m_data.m_value.array->cend() - 1; ++i) - { - put_indent(new_indent); - dump_internal(*i, new_indent, depth + 1); - put_literal(",\n"); - } + if (val.m_data.m_value.array->empty()) + { + put_literal("[]"); + return; + } - // last element - JSON_ASSERT(!val.m_data.m_value.array->empty()); - put_indent(new_indent); - dump_internal(val.m_data.m_value.array->back(), new_indent, depth + 1); + if (pretty_print) + { + // variable to hold indentation for recursive calls + const auto new_indent = next_indent(current_indent, indent_step); - put_char('\n'); - put_indent(current_indent); - put_char(']'); - } - else - { - put_char('['); + put_literal_indent("[\n", new_indent); - // first n-1 elements - for (auto i = val.m_data.m_value.array->cbegin(); - i != val.m_data.m_value.array->cend() - 1; ++i) - { - dump_internal(*i, current_indent, depth + 1); - put_char(','); - } - - // last element - JSON_ASSERT(!val.m_data.m_value.array->empty()); - dump_internal(val.m_data.m_value.array->back(), current_indent, depth + 1); - - put_char(']'); - } - - return; + // first n-1 elements + for (auto i = val.m_data.m_value.array->cbegin(); + i != val.m_data.m_value.array->cend() - 1; ++i) + { + dump_element(*i, new_indent, depth + 1); + put_literal_indent(",\n", new_indent); } - case value_t::string: - case value_t::binary: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::discarded: - case value_t::null: - default: - dump_scalar(val, current_indent); - return; + // last element + JSON_ASSERT(!val.m_data.m_value.array->empty()); + dump_element(val.m_data.m_value.array->back(), new_indent, depth + 1); + + put_literal_indent("\n", current_indent); + put_char(']'); + } + else + { + put_char('['); + + // first n-1 elements + for (auto i = val.m_data.m_value.array->cbegin(); + i != val.m_data.m_value.array->cend() - 1; ++i) + { + dump_element(*i, 0, depth + 1); + put_char(','); + } + + // last element + JSON_ASSERT(!val.m_data.m_value.array->empty()); + dump_element(val.m_data.m_value.array->back(), 0, depth + 1); + + put_char(']'); } } private: + /*! + @brief serialize an element of a container + + Most elements are scalars, which need none of the state dump_container() keeps + for a container; writing them from here spares each the entry and exit of + that function. + */ + JSON_HEDLEY_ALWAYS_INLINE void dump_element(const BasicJsonType& val, + const std::size_t current_indent, + const std::size_t depth) + { + if (val.m_data.m_type == value_t::object || val.m_data.m_type == value_t::array) + { + dump_container(val, current_indent, depth); + } + else + { + dump_scalar(val, current_indent); + } + } + /*! @brief write out @a val and everything below it without the call stack - Emits the same bytes as @ref dump_internal, keeping the containers it has + Emits the same bytes as @ref dump_container, keeping the containers it has entered on an explicit stack instead of descending into them. Only reached for values nested deeper than @ref recursion_depth_limit, which is why it is not written for speed: walking every value this way measured up to 20% slower on @@ -319,8 +310,7 @@ class serializer { if (pretty_print) { - put_char('\n'); - put_indent(frame.current_indent); + put_literal_indent("\n", frame.current_indent); } put_char('}'); @@ -347,16 +337,13 @@ class serializer put_indent(frame.child_indent); } - put_char('"'); - dump_escaped(frame.object_it->first); - if (pretty_print) { - put_literal("\": "); + put_object_key(frame.object_it->first, "\": "); } else { - put_literal("\":"); + put_object_key(frame.object_it->first, "\":"); } const BasicJsonType& element = frame.object_it->second; @@ -375,8 +362,7 @@ class serializer { if (pretty_print) { - put_char('\n'); - put_indent(frame.current_indent); + put_literal_indent("\n", frame.current_indent); } put_char(']'); @@ -517,7 +503,7 @@ class serializer /*! @brief serialize the value @a val, which is neither an object nor an array - Shared by @ref dump_internal and @ref dump_value, so that a value is written + Shared by @ref dump_element and @ref dump_value, so that a value is written the same way however deeply it is nested. A binary value is written out here in full: it looks like an object, but has no elements to descend into. @@ -531,78 +517,13 @@ class serializer { case value_t::string: { - put_char('"'); - dump_escaped(*val.m_data.m_value.string); - put_char('"'); + dump_string_value(*val.m_data.m_value.string); return; } case value_t::binary: { - if (pretty_print) - { - put_literal("{\n"); - - // variable to hold indentation for the bytes - const auto new_indent = next_indent(current_indent, indent_step); - - put_indent(new_indent); - - put_literal("\"bytes\": ["); - - if (!val.m_data.m_value.binary->empty()) - { - for (auto i = val.m_data.m_value.binary->cbegin(); - i != val.m_data.m_value.binary->cend() - 1; ++i) - { - dump_byte(*i); - put_literal(", "); - } - dump_byte(val.m_data.m_value.binary->back()); - } - - put_literal("],\n"); - put_indent(new_indent); - - put_literal("\"subtype\": "); - if (val.m_data.m_value.binary->has_subtype()) - { - dump_integer(val.m_data.m_value.binary->subtype()); - } - else - { - put_literal("null"); - } - put_char('\n'); - put_indent(current_indent); - put_char('}'); - } - else - { - put_literal("{\"bytes\":["); - - if (!val.m_data.m_value.binary->empty()) - { - for (auto i = val.m_data.m_value.binary->cbegin(); - i != val.m_data.m_value.binary->cend() - 1; ++i) - { - dump_byte(*i); - put_char(','); - } - dump_byte(val.m_data.m_value.binary->back()); - } - - put_literal("],\"subtype\":"); - if (val.m_data.m_value.binary->has_subtype()) - { - dump_integer(val.m_data.m_value.binary->subtype()); - put_char('}'); - } - else - { - put_literal("null}"); - } - } + dump_binary_value(current_indent, *val.m_data.m_value.binary); return; } @@ -621,19 +542,19 @@ class serializer case value_t::number_integer: { - dump_integer(val.m_data.m_value.number_integer); + dump_signed_value(val.m_data.m_value.number_integer); return; } case value_t::number_unsigned: { - dump_integer(val.m_data.m_value.number_unsigned); + dump_unsigned_value(val.m_data.m_value.number_unsigned); return; } case value_t::number_float: { - dump_float(val.m_data.m_value.number_float); + dump_float_value(val.m_data.m_value.number_float); return; } @@ -656,6 +577,100 @@ class serializer } } + /// @brief write the string @a s in quotes; kept out of line, so that the + /// other scalars do not pay for its register needs (and the other way round) + JSON_HEDLEY_NEVER_INLINE void dump_string_value(const string_t& s) + { +#if JSON_LITTLE_ENDIAN_TARGET + if (s.size() <= short_string_max && put_short_quoted(s, "\"")) + { + return; + } +#endif + put_char('"'); + dump_escaped(s); + put_char('"'); + } + + // The numbers, and the rest of what dump_scalar() writes, are left to + // functions of their own as well: a single function holding all of them + // saves and restores every register any of them needs, on every call. Now + // dump_scalar() only dispatches, and each writer pays for what it uses. + JSON_HEDLEY_NEVER_INLINE void dump_signed_value(const number_integer_t x) + { + dump_integer(x); + } + + JSON_HEDLEY_NEVER_INLINE void dump_unsigned_value(const number_unsigned_t x) + { + dump_integer(x); + } + + JSON_HEDLEY_NEVER_INLINE void dump_float_value(const number_float_t x) + { + dump_float(x); + } + + JSON_HEDLEY_NEVER_INLINE void dump_binary_value(const std::size_t current_indent, const typename BasicJsonType::binary_t& binary) + { + if (pretty_print) + { + // variable to hold indentation for the bytes + const auto new_indent = next_indent(current_indent, indent_step); + + put_literal_indent("{\n", new_indent); + put_literal("\"bytes\": ["); + + if (!binary.empty()) + { + for (auto i = binary.cbegin(); i != binary.cend() - 1; ++i) + { + dump_byte(*i); + put_literal(", "); + } + dump_byte(binary.back()); + } + + put_literal_indent("],\n", new_indent); + + put_literal("\"subtype\": "); + if (binary.has_subtype()) + { + dump_integer(binary.subtype()); + } + else + { + put_literal("null"); + } + put_literal_indent("\n", current_indent); + put_char('}'); + } + else + { + put_literal("{\"bytes\":["); + + if (!binary.empty()) + { + for (auto i = binary.cbegin(); i != binary.cend() - 1; ++i) + { + dump_byte(*i); + put_char(','); + } + dump_byte(binary.back()); + } + + put_literal("],\"subtype\":"); + if (binary.has_subtype()) + { + dump_integer(binary.subtype()); + put_char('}'); + } + else + { + put_literal("null}"); + } + } + } /*! @brief the indentation level to use for the children of the current value @@ -672,6 +687,220 @@ class serializer return new_indent; } + private: + /*! + @brief append the object key @a s in quotes, followed by @a tail + + @a tail is the closing quote and the colon, followed by a space when + pretty-printing. The callers pass it because each of them already sits on + the pretty-printing or the compact path; choosing it here would test + @ref pretty_print once more for every key. Most keys are short and plain + and are written in one go by put_short_quoted(); everything else takes the + general path. + */ + template + void put_object_key(const string_t& s, const char (&tail)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + { +#if JSON_LITTLE_ENDIAN_TARGET + if (put_short_quoted(s, tail)) + { + return; + } +#endif + put_char('"'); + dump_escaped(s); + put_literal(tail); + } + + // put_short_quoted() assembles words from little-endian loads +#if JSON_LITTLE_ENDIAN_TARGET + /// the longest string put_short_quoted() takes + static constexpr std::size_t short_string_max = JSON_SCAN_VECTOR ? 32 : 16; + + /// the high bit of every byte of @a v that is not printable ASCII or is a + /// quote, a backslash, or DEL (zero if the bytes can be copied as they are) + static std::uint64_t swar_not_plain(const std::uint64_t v) noexcept + { + constexpr std::uint64_t ones = 0x0101010101010101ull; + const std::uint64_t q = v ^ 0x2222222222222222ull; // '"' + const std::uint64_t b = v ^ 0x5C5C5C5C5C5C5C5Cull; // '\\' + return (((q - ones) & ~q) | ((b - ones) & ~b) // == '"', == '\\' + | ((v - 0x2020202020202020ull) & ~v) // < 0x20 + | v | (v + ones)) // >= 0x7F + & 0x8080808080808080ull; + } + + template + static WordType load_word(const char* p) noexcept + { + WordType w; + std::memcpy(&w, p, sizeof(w)); + return w; + } + + template + static void store_word(char* p, const WordType w) noexcept + { + std::memcpy(p, &w, sizeof(w)); + } + + /*! + @brief write `"`, @a s and @a tail if @a s is short and needs no escaping + + A string of up to 16 (32 with vector instructions) printable ASCII + characters (no quote, backslash, or DEL) is read with two overlapping loads + and written with two overlapping stores, instead of scanning it and calling + memcpy with a length that is only known at run time. Returns false, having + changed nothing, if @a s does not qualify or the write buffer lacks the + room. + */ + template + bool put_short_quoted(const string_t& s, const char (&tail)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + { + const std::size_t n = s.size(); + if (n > short_string_max || write_buffer_pos + n + N > write_buffer.size()) + { + return false; + } + + char* d = write_buffer.data() + write_buffer_pos + 1; + const char* const p = s.data(); + + if (n >= 8) + { +#if JSON_SCAN_VECTOR + if (!(n >= 16 ? copy_plain_vectors(d, p, n) : copy_plain_halves(d, p, n))) + { + return false; + } +#else + const auto a = load_word(p); + const auto b = load_word(p + n - 8); + if ((swar_not_plain(a) | swar_not_plain(b)) != 0) + { + return false; + } + store_word(d, a); + store_word(d + n - 8, b); +#endif + } + else if (n >= 4) + { + const auto a = load_word(p); + const auto b = load_word(p + n - 4); + // the bytes above the string are filled with a plain character + const std::uint64_t w = a | (static_cast(b) << (8 * (n - 4))) + | (0x4141414141414141ull << (8 * n)); + if (swar_not_plain(w) != 0) + { + return false; + } + store_word(d, a); + store_word(d + n - 4, b); + } + else if (n >= 2) + { + const auto a = load_word(p); + const auto b = load_word(p + n - 2); + const std::uint64_t w = a | (static_cast(b) << (8 * (n - 2))) + | (0x4141414141414141ull << (8 * n)); + if (swar_not_plain(w) != 0) + { + return false; + } + store_word(d, a); + store_word(d + n - 2, b); + } + else if (n == 1) + { + const std::uint64_t w = static_cast(static_cast(p[0])) | (0x4141414141414141ull << 8u); + if (swar_not_plain(w) != 0) + { + return false; + } + *d = p[0]; + } + + d[-1] = '"'; + std::memcpy(d + n, tail, N - 1); + write_buffer_pos += n + N; + return true; + } + +#if JSON_SCAN_VECTOR + /// copy the 8 to 15 bytes [@a p, @a p + @a n) to @a d with two overlapping + /// words, tested together as one vector, see copy_plain_vectors() + static bool copy_plain_halves(char* d, const char* p, const std::size_t n) noexcept + { +#if JSON_SCAN_NEON + const uint8x8_t lo = vld1_u8(reinterpret_cast(p)); + const uint8x8_t hi = vld1_u8(reinterpret_cast(p + n - 8)); + const uint8x16_t in = vcombine_u8(lo, hi); + const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))), + vorrq_u8(vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20)), vceqq_u8(in, vdupq_n_u8(0x7F)))); + if (vmaxvq_u8(special) != 0) + { + return false; + } + vst1_u8(reinterpret_cast(d), lo); + vst1_u8(reinterpret_cast(d + n - 8), hi); +#else + const __m128i lo = _mm_loadl_epi64(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) + const __m128i hi = _mm_loadl_epi64(static_cast(static_cast(p + n - 8))); // NOLINT(bugprone-casting-through-void) + const __m128i in = _mm_unpacklo_epi64(lo, hi); + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_or_si128(_mm_cmplt_epi8(in, _mm_set1_epi8(0x20)), _mm_cmpeq_epi8(in, _mm_set1_epi8(0x7F)))); + if (_mm_movemask_epi8(special) != 0) + { + return false; + } + _mm_storel_epi64(static_cast<__m128i*>(static_cast(d)), lo); // NOLINT(bugprone-casting-through-void) + _mm_storel_epi64(static_cast<__m128i*>(static_cast(d + n - 8)), hi); // NOLINT(bugprone-casting-through-void) +#endif + return true; + } + + /// copy the 16 to 32 bytes [@a p, @a p + @a n) to @a d with two overlapping + /// vectors, unless one of them is a quote, a backslash, a control + /// character, DEL or not ASCII + static bool copy_plain_vectors(char* d, const char* p, const std::size_t n) noexcept + { +#if JSON_SCAN_NEON + const uint8x16_t a = vld1q_u8(reinterpret_cast(p)); + const uint8x16_t b = vld1q_u8(reinterpret_cast(p + n - 16)); + const uint8x16_t special = vorrq_u8(vorrq_u8(vorrq_u8(vceqq_u8(a, vdupq_n_u8('"')), vceqq_u8(a, vdupq_n_u8('\\'))), + vorrq_u8(vceqq_u8(b, vdupq_n_u8('"')), vceqq_u8(b, vdupq_n_u8('\\')))), + vorrq_u8(vorrq_u8(vcltq_s8(vreinterpretq_s8_u8(a), vdupq_n_s8(0x20)), vcltq_s8(vreinterpretq_s8_u8(b), vdupq_n_s8(0x20))), + vorrq_u8(vceqq_u8(a, vdupq_n_u8(0x7F)), vceqq_u8(b, vdupq_n_u8(0x7F))))); + if (vmaxvq_u8(special) != 0) + { + return false; + } + vst1q_u8(reinterpret_cast(d), a); + vst1q_u8(reinterpret_cast(d + n - 16), b); +#else + const __m128i a = _mm_loadu_si128(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) + const __m128i b = _mm_loadu_si128(static_cast(static_cast(p + n - 16))); // NOLINT(bugprone-casting-through-void) + const __m128i quote = _mm_set1_epi8('"'); + const __m128i backslash = _mm_set1_epi8('\\'); + const __m128i space = _mm_set1_epi8(0x20); + const __m128i del = _mm_set1_epi8(0x7F); + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(a, quote), _mm_cmpeq_epi8(a, backslash)), + _mm_or_si128(_mm_cmpeq_epi8(b, quote), _mm_cmpeq_epi8(b, backslash))), + _mm_or_si128(_mm_or_si128(_mm_cmplt_epi8(a, space), _mm_cmplt_epi8(b, space)), + _mm_or_si128(_mm_cmpeq_epi8(a, del), _mm_cmpeq_epi8(b, del)))); + if (_mm_movemask_epi8(special) != 0) + { + return false; + } + _mm_storeu_si128(static_cast<__m128i*>(static_cast(d)), a); // NOLINT(bugprone-casting-through-void) + _mm_storeu_si128(static_cast<__m128i*>(static_cast(d + n - 16)), b); // NOLINT(bugprone-casting-through-void) +#endif + return true; + } +#endif +#endif + JSON_PRIVATE_UNLESS_TESTED: /*! @brief dump escaped string @@ -684,9 +913,46 @@ class serializer @param[in] s the string to escape Complexity: Linear in the length of string @a s. + + @note kept out of line: with the short-string attempts in front of it gone + from here, compilers would otherwise inline it into @ref put_object_key + and then no longer into the loops that call that */ - void dump_escaped(const string_t& s) + JSON_HEDLEY_NEVER_INLINE void dump_escaped(const string_t& s) { + // Short strings of printable ASCII - most object keys and many values - + // are checked byte by byte against a table and then copied in one go. + // For them, this is cheaper than starting the vector scanner below, + // whose setup costs more than such a string takes to scan. The same + // check serves both values of ensure_ascii. + // + // Where put_short_quoted() exists and takes strings of up to 32 bytes, + // the callers have tried it already, and repeating the attempt only + // costs time for strings that need escaping or are not ASCII. +#if !JSON_LITTLE_ENDIAN_TARGET || !JSON_SCAN_VECTOR + if (s.size() <= 32) + { + const auto& copyable = ascii_copyable(); + const auto* const data = reinterpret_cast(s.data()); + const std::size_t n = s.size(); + std::size_t k = 0; + while (k + 4 <= n && copyable[data[k]] != 0 && copyable[data[k + 1]] != 0 + && copyable[data[k + 2]] != 0 && copyable[data[k + 3]] != 0) + { + k += 4; + } + while (k < n && copyable[data[k]] != 0) + { + ++k; + } + if (k == n) + { + put_string(s, 0, n); + return; + } + } +#endif + // dispatch once here rather than test the flag inside the loop: it does // not change while a string is written, and folding it lets each of the // two scanners be inlined into a loop of its own @@ -700,6 +966,28 @@ class serializer } } + /// write out the bytes pending in @ref string_buffer and reset @a bytes + JSON_HEDLEY_ALWAYS_INLINE void emit_pending(std::size_t& bytes) + { + if (bytes != 0) + { + put_buffer(string_buffer, bytes); + bytes = 0; + } + } + + /// write buffer and reset index if fewer than 13 bytes are left, as this + /// is the maximal number of bytes to be written ("\uxxxx\uxxxx\0") for one + /// code point + JSON_HEDLEY_ALWAYS_INLINE void reserve_string_buffer(std::size_t& bytes) + { + if (string_buffer.size() - bytes < 13) + { + put_buffer(string_buffer, bytes); + bytes = 0; + } + } + /*! @brief worker for @ref dump_escaped @@ -758,12 +1046,8 @@ class serializer { // emit any bytes still pending in string_buffer first to // preserve output order, then write the run directly - if (bytes != 0) - { - put_buffer(string_buffer, bytes); - bytes = 0; - } - put_string(s, i, i + run); + emit_pending(bytes); + put_run(s, i, run); bytes_after_last_accept = 0; undumped_chars = 0; i += run; @@ -772,110 +1056,97 @@ class serializer break; } } + + // A byte below 0x60 is a complete code point and needs no + // UTF-8 decoding. The scanner stops only at a control + // character, a quote, or a backslash in that range, so look up + // its escape sequence and write all 8 bytes of the table entry + // straight into the write buffer; only the length stored in the + // last byte of the entry is kept. + if (data[i] < 0x60) + { + // emit any bytes still pending in string_buffer first to + // preserve output order + emit_pending(bytes); + // deliberately not put_buffer(): a fixed 8-byte memcpy + // has no asserts and no write-through branch, which + // matters on the hot path + const auto& escape = ascii_escapes()[data[i]]; + if (JSON_HEDLEY_UNLIKELY(write_buffer.size() - write_buffer_pos < escape.size())) + { + flush(); + } + std::memcpy(write_buffer.data() + write_buffer_pos, escape.data(), escape.size()); + write_buffer_pos += static_cast(escape[7]); + bytes_after_last_accept = 0; + undumped_chars = 0; + continue; + } } const auto byte = static_cast(s[i]); + if (EnsureAscii && state == UTF8_ACCEPT && byte >= 0x80) + { + // a run of well-formed multi-byte characters is escaped + // straight into the write buffer; an ill-formed sequence + // ends the run and is left to the byte-at-a-time path + if (bytes != 0) + { + put_buffer(string_buffer, bytes); + bytes = 0; + bytes_after_last_accept = 0; + } + const std::size_t escaped = write_u_escapes(reinterpret_cast(s.data()) + i, s.size() - i); + if (escaped != 0) + { + bytes_after_last_accept = 0; + undumped_chars = 0; + i += escaped - 1; + continue; + } + } + switch (decode(state, codepoint, byte)) { case UTF8_ACCEPT: // decode found a new code point { - switch (codepoint) + // the code point is at least 0x60 (smaller ones are handled + // above) and is not printable ASCII (which the scanner + // copies): it is DEL (0x7F) under EnsureAscii or a non-ASCII + // character + if (EnsureAscii) { - case 0x08: // backspace + JSON_ASSERT(codepoint >= 0x7F); + if (error_handler == error_handler_t::keep) { - string_buffer[bytes++] = '\\'; - string_buffer[bytes++] = 'b'; - break; + // this character was buffered as raw bytes below + // in case it turned out to be part of an + // ill-formed sequence (which is kept as is); now + // that it decoded to a well-formed code point, + // undo that and \u-escape it like any other + // character instead + bytes = bytes_after_last_accept; } - - case 0x09: // horizontal tab + if (codepoint <= 0xFFFF) { - string_buffer[bytes++] = '\\'; - string_buffer[bytes++] = 't'; - break; + write_u_escape(bytes, static_cast(codepoint)); } - - case 0x0A: // newline + else { - string_buffer[bytes++] = '\\'; - string_buffer[bytes++] = 'n'; - break; - } - - case 0x0C: // formfeed - { - string_buffer[bytes++] = '\\'; - string_buffer[bytes++] = 'f'; - break; - } - - case 0x0D: // carriage return - { - string_buffer[bytes++] = '\\'; - string_buffer[bytes++] = 'r'; - break; - } - - case 0x22: // quotation mark - { - string_buffer[bytes++] = '\\'; - string_buffer[bytes++] = '"'; - break; - } - - case 0x5C: // reverse solidus - { - string_buffer[bytes++] = '\\'; - string_buffer[bytes++] = '\\'; - break; - } - - default: - { - // escape control characters (0x00..0x1F) or, if - // EnsureAscii parameter is used, non-ASCII characters - if ((codepoint <= 0x1F) || (EnsureAscii && (codepoint >= 0x7F))) - { - if (EnsureAscii && error_handler == error_handler_t::keep) - { - // this character was buffered as raw bytes - // below in case it turned out to be part of - // an ill-formed sequence (which is kept as - // is); now that it decoded to a well-formed - // code point, undo that and \u-escape it - // like any other character instead - bytes = bytes_after_last_accept; - } - if (codepoint <= 0xFFFF) - { - write_u_escape(bytes, static_cast(codepoint)); - } - else - { - write_u_escape(bytes, static_cast(0xD7C0u + (codepoint >> 10u))); - write_u_escape(bytes, static_cast(0xDC00u + (codepoint & 0x3FFu))); - } - } - else - { - // copy byte to buffer (all previous bytes - // been copied have in default case above) - string_buffer[bytes++] = s[i]; - } - break; + write_u_escape(bytes, static_cast(0xD7C0u + (codepoint >> 10u))); + write_u_escape(bytes, static_cast(0xDC00u + (codepoint & 0x3FFu))); } } - - // write buffer and reset index; there must be 13 bytes - // left, as this is the maximal number of bytes to be - // written ("\uxxxx\uxxxx\0") for one code point - if (string_buffer.size() - bytes < 13) + else { - put_buffer(string_buffer, bytes); - bytes = 0; + // copy byte to buffer (all previous bytes of the + // sequence have been copied in the default case below) + string_buffer[bytes++] = s[i]; } + reserve_string_buffer(bytes); + // remember the byte position of this accept bytes_after_last_accept = bytes; undumped_chars = 0; @@ -888,7 +1159,7 @@ class serializer { case error_handler_t::strict: { - JSON_THROW(type_error::create(316, concat("invalid UTF-8 byte at index ", std::to_string(i), ": 0x", detail::hex_byte(byte)), nullptr)); + JSON_THROW(type_error::create(exception_id::invalid_utf8, concat("invalid UTF-8 byte at index ", std::to_string(i), ": 0x", detail::hex_byte(byte)), nullptr)); } case error_handler_t::ignore: @@ -926,14 +1197,7 @@ class serializer string_buffer[bytes++] = '\xBD'; } - // write buffer and reset index; there must be 13 bytes - // left, as this is the maximal number of bytes to be - // written ("\uxxxx\uxxxx\0") for one code point - if (string_buffer.size() - bytes < 13) - { - put_buffer(string_buffer, bytes); - bytes = 0; - } + reserve_string_buffer(bytes); bytes_after_last_accept = bytes; } @@ -966,14 +1230,7 @@ class serializer string_buffer[bytes++] = s[i]; } - // write buffer and reset index; there must be 13 bytes - // left, as this is the maximal number of bytes to be - // written ("\uxxxx\uxxxx\0") for one code point - if (string_buffer.size() - bytes < 13) - { - put_buffer(string_buffer, bytes); - bytes = 0; - } + reserve_string_buffer(bytes); bytes_after_last_accept = bytes; undumped_chars = 0; @@ -1021,7 +1278,7 @@ class serializer { case error_handler_t::strict: { - JSON_THROW(type_error::create(316, concat("incomplete UTF-8 string; last byte: 0x", detail::hex_byte(static_cast(s[s.size() - 1]))), nullptr)); + JSON_THROW(type_error::create(exception_id::invalid_utf8, concat("incomplete UTF-8 string; last byte: 0x", detail::hex_byte(static_cast(s[s.size() - 1]))), nullptr)); } case error_handler_t::ignore: @@ -1093,8 +1350,12 @@ class serializer the indentation character once and flushing that same content repeatedly: flushing does not disturb what the buffer holds, so re-filling it between flushes would be redundant work. + + @note inlined on purpose, as is @ref put_literal: GCC otherwise stops + inlining both into the pretty-printing loops once @ref dump_escaped + has grown, which made indented output up to 13% slower */ - void put_indent(std::size_t indent) + JSON_HEDLEY_ALWAYS_INLINE void put_indent(std::size_t indent) { // closing braces at the outermost level ask for no indentation at all if (indent == 0) @@ -1102,6 +1363,12 @@ class serializer return; } + // the usual indentation, without a call of memset + if (JSON_HEDLEY_LIKELY(put_short_literal_indent("", indent))) + { + return; + } + const std::size_t capacity = write_buffer.size(); // fill whatever room is left in the buffer; this is the whole job @@ -1135,6 +1402,51 @@ class serializer write_buffer_pos = indent; } + /*! + @brief append a string literal and then @a indent indentation characters + + The usual line break and indentation go into the buffer with one check of + its room and four stores of the indentation, see @ref put_short_literal_indent. + */ + template + JSON_HEDLEY_ALWAYS_INLINE void put_literal_indent(const char (&s)[N], const std::size_t indent) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + { + if (JSON_HEDLEY_LIKELY(put_short_literal_indent(s, indent))) + { + return; + } + put_literal(s); + put_indent(indent); + } + + /*! + @brief append a string literal and up to 32 indentation characters in one go + + Four words of the indentation character are stored unconditionally and only + the part wanted is claimed, which spares the call of memset. Shared by + @ref put_indent (with an empty literal, whose copy compiles to nothing) and + @ref put_literal_indent. Returns false, having written nothing, if + @a indent is wider or the write buffer lacks the room. + */ + template + JSON_HEDLEY_ALWAYS_INLINE bool put_short_literal_indent(const char (&s)[N], const std::size_t indent) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + { + constexpr std::size_t length = N - 1; + if (indent > 32 || write_buffer_pos + length + 32 > write_buffer.size()) + { + return false; + } + const std::uint64_t word = 0x0101010101010101ull * static_cast(indent_char); + char* const d = write_buffer.data() + write_buffer_pos; + std::memcpy(d, s, length); + std::memcpy(d + length, &word, sizeof(word)); + std::memcpy(d + length + 8, &word, sizeof(word)); + std::memcpy(d + length + 16, &word, sizeof(word)); + std::memcpy(d + length + 24, &word, sizeof(word)); + write_buffer_pos += length + indent; + return true; + } + /*! @brief append a string literal to the write buffer @@ -1144,7 +1456,7 @@ class serializer write-through path for oversized runs. */ template - void put_literal(const char (&s)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + JSON_HEDLEY_ALWAYS_INLINE void put_literal(const char (&s)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) { static_assert(N >= 2, "put_literal expects a non-empty string literal"); // the array bound counts the terminating NUL, which is not written @@ -1159,6 +1471,28 @@ class serializer write_buffer_pos += length; } + /*! + @brief append the characters of @a str in [@a start, @a start + @a length) + + A run of up to 16 characters with 16 or more in the string from @a start on + and in the write buffer from its end on is copied as a whole block of 16, + which spares the call of memcpy with a length known only at run time; the + surplus characters in the buffer are overwritten by what follows. + */ + template + void put_run(const StringType& str, const std::size_t start, const std::size_t length) + { + // a short run is the exception in text without escapes (CJK, accented + // Latin), for which this keeps the long path the fast one + if (JSON_HEDLEY_UNLIKELY(length <= 16 && start + 16 <= str.size() && write_buffer_pos + 16 <= write_buffer.size())) + { + std::memcpy(write_buffer.data() + write_buffer_pos, str.data() + start, 16); + write_buffer_pos += length; + return; + } + put_string(str, start, start + length); + } + /*! @brief append the characters of @a str in [@a start, @a end) @@ -1227,7 +1561,7 @@ class serializer @param[in] x unsigned integer number to count its digits @return number of decimal digits */ - unsigned int count_digits(number_unsigned_t x) noexcept + unsigned int count_digits(number_magnitude_t x) noexcept { unsigned int n_digits = 1; for (;;) @@ -1253,13 +1587,214 @@ class serializer } } + /*! + @brief whether each byte can be written verbatim under either value of ensure_ascii + + Entry c is 1 for printable ASCII other than '"' and '\\' (see + is_ascii_copyable()) and 0 otherwise. A table lookup per byte is the + cheapest test for the short strings handled first in dump_escaped(). + */ + static const std::array& ascii_copyable() noexcept + { + // constant-initialized from the predicate: no runtime loop, no guard + static constexpr std::array copyable = make_ascii_copyable(make_index_sequence<256> {}); + return copyable; + } + + template + static constexpr std::array make_ascii_copyable(index_sequence /*unused*/) noexcept + { + return {{ static_cast(is_ascii_copyable(static_cast(Is)) ? 1 : 0)... }}; + } + + /*! + @brief the JSON form of each character below 0x60 + + Entry c holds the escape sequence of character c - the short form for + \\b, \\t, \\n, \\f, \\r, \\" and \\\\, \\u00xx for the other control characters - + or c itself if it needs no escaping, padded with zeros to 7 bytes. The last + byte is the length of the sequence. The 8 bytes of an entry are copied at + once, so writing an escape needs neither a branch nor a byte-wise loop. + */ + static const std::array, 0x60>& ascii_escapes() noexcept + { + static const std::array, 0x60> escapes = + { + { + {{'\\', 'u', '0', '0', '0', '0', 0, 6}}, // 0x00 + {{'\\', 'u', '0', '0', '0', '1', 0, 6}}, // 0x01 + {{'\\', 'u', '0', '0', '0', '2', 0, 6}}, // 0x02 + {{'\\', 'u', '0', '0', '0', '3', 0, 6}}, // 0x03 + {{'\\', 'u', '0', '0', '0', '4', 0, 6}}, // 0x04 + {{'\\', 'u', '0', '0', '0', '5', 0, 6}}, // 0x05 + {{'\\', 'u', '0', '0', '0', '6', 0, 6}}, // 0x06 + {{'\\', 'u', '0', '0', '0', '7', 0, 6}}, // 0x07 + {{'\\', 'b', 0, 0, 0, 0, 0, 2}}, // 0x08 + {{'\\', 't', 0, 0, 0, 0, 0, 2}}, // 0x09 + {{'\\', 'n', 0, 0, 0, 0, 0, 2}}, // 0x0A + {{'\\', 'u', '0', '0', '0', 'b', 0, 6}}, // 0x0B + {{'\\', 'f', 0, 0, 0, 0, 0, 2}}, // 0x0C + {{'\\', 'r', 0, 0, 0, 0, 0, 2}}, // 0x0D + {{'\\', 'u', '0', '0', '0', 'e', 0, 6}}, // 0x0E + {{'\\', 'u', '0', '0', '0', 'f', 0, 6}}, // 0x0F + {{'\\', 'u', '0', '0', '1', '0', 0, 6}}, // 0x10 + {{'\\', 'u', '0', '0', '1', '1', 0, 6}}, // 0x11 + {{'\\', 'u', '0', '0', '1', '2', 0, 6}}, // 0x12 + {{'\\', 'u', '0', '0', '1', '3', 0, 6}}, // 0x13 + {{'\\', 'u', '0', '0', '1', '4', 0, 6}}, // 0x14 + {{'\\', 'u', '0', '0', '1', '5', 0, 6}}, // 0x15 + {{'\\', 'u', '0', '0', '1', '6', 0, 6}}, // 0x16 + {{'\\', 'u', '0', '0', '1', '7', 0, 6}}, // 0x17 + {{'\\', 'u', '0', '0', '1', '8', 0, 6}}, // 0x18 + {{'\\', 'u', '0', '0', '1', '9', 0, 6}}, // 0x19 + {{'\\', 'u', '0', '0', '1', 'a', 0, 6}}, // 0x1A + {{'\\', 'u', '0', '0', '1', 'b', 0, 6}}, // 0x1B + {{'\\', 'u', '0', '0', '1', 'c', 0, 6}}, // 0x1C + {{'\\', 'u', '0', '0', '1', 'd', 0, 6}}, // 0x1D + {{'\\', 'u', '0', '0', '1', 'e', 0, 6}}, // 0x1E + {{'\\', 'u', '0', '0', '1', 'f', 0, 6}}, // 0x1F + {{' ', 0, 0, 0, 0, 0, 0, 1}}, // 0x20 + {{'!', 0, 0, 0, 0, 0, 0, 1}}, // 0x21 + {{'\\', '"', 0, 0, 0, 0, 0, 2}}, // 0x22 + {{'#', 0, 0, 0, 0, 0, 0, 1}}, // 0x23 + {{'$', 0, 0, 0, 0, 0, 0, 1}}, // 0x24 + {{'%', 0, 0, 0, 0, 0, 0, 1}}, // 0x25 + {{'&', 0, 0, 0, 0, 0, 0, 1}}, // 0x26 + {{'\'', 0, 0, 0, 0, 0, 0, 1}}, // 0x27 + {{'(', 0, 0, 0, 0, 0, 0, 1}}, // 0x28 + {{')', 0, 0, 0, 0, 0, 0, 1}}, // 0x29 + {{'*', 0, 0, 0, 0, 0, 0, 1}}, // 0x2A + {{'+', 0, 0, 0, 0, 0, 0, 1}}, // 0x2B + {{',', 0, 0, 0, 0, 0, 0, 1}}, // 0x2C + {{'-', 0, 0, 0, 0, 0, 0, 1}}, // 0x2D + {{'.', 0, 0, 0, 0, 0, 0, 1}}, // 0x2E + {{'/', 0, 0, 0, 0, 0, 0, 1}}, // 0x2F + {{'0', 0, 0, 0, 0, 0, 0, 1}}, // 0x30 + {{'1', 0, 0, 0, 0, 0, 0, 1}}, // 0x31 + {{'2', 0, 0, 0, 0, 0, 0, 1}}, // 0x32 + {{'3', 0, 0, 0, 0, 0, 0, 1}}, // 0x33 + {{'4', 0, 0, 0, 0, 0, 0, 1}}, // 0x34 + {{'5', 0, 0, 0, 0, 0, 0, 1}}, // 0x35 + {{'6', 0, 0, 0, 0, 0, 0, 1}}, // 0x36 + {{'7', 0, 0, 0, 0, 0, 0, 1}}, // 0x37 + {{'8', 0, 0, 0, 0, 0, 0, 1}}, // 0x38 + {{'9', 0, 0, 0, 0, 0, 0, 1}}, // 0x39 + {{':', 0, 0, 0, 0, 0, 0, 1}}, // 0x3A + {{';', 0, 0, 0, 0, 0, 0, 1}}, // 0x3B + {{'<', 0, 0, 0, 0, 0, 0, 1}}, // 0x3C + {{'=', 0, 0, 0, 0, 0, 0, 1}}, // 0x3D + {{'>', 0, 0, 0, 0, 0, 0, 1}}, // 0x3E + {{'?', 0, 0, 0, 0, 0, 0, 1}}, // 0x3F + {{'@', 0, 0, 0, 0, 0, 0, 1}}, // 0x40 + {{'A', 0, 0, 0, 0, 0, 0, 1}}, // 0x41 + {{'B', 0, 0, 0, 0, 0, 0, 1}}, // 0x42 + {{'C', 0, 0, 0, 0, 0, 0, 1}}, // 0x43 + {{'D', 0, 0, 0, 0, 0, 0, 1}}, // 0x44 + {{'E', 0, 0, 0, 0, 0, 0, 1}}, // 0x45 + {{'F', 0, 0, 0, 0, 0, 0, 1}}, // 0x46 + {{'G', 0, 0, 0, 0, 0, 0, 1}}, // 0x47 + {{'H', 0, 0, 0, 0, 0, 0, 1}}, // 0x48 + {{'I', 0, 0, 0, 0, 0, 0, 1}}, // 0x49 + {{'J', 0, 0, 0, 0, 0, 0, 1}}, // 0x4A + {{'K', 0, 0, 0, 0, 0, 0, 1}}, // 0x4B + {{'L', 0, 0, 0, 0, 0, 0, 1}}, // 0x4C + {{'M', 0, 0, 0, 0, 0, 0, 1}}, // 0x4D + {{'N', 0, 0, 0, 0, 0, 0, 1}}, // 0x4E + {{'O', 0, 0, 0, 0, 0, 0, 1}}, // 0x4F + {{'P', 0, 0, 0, 0, 0, 0, 1}}, // 0x50 + {{'Q', 0, 0, 0, 0, 0, 0, 1}}, // 0x51 + {{'R', 0, 0, 0, 0, 0, 0, 1}}, // 0x52 + {{'S', 0, 0, 0, 0, 0, 0, 1}}, // 0x53 + {{'T', 0, 0, 0, 0, 0, 0, 1}}, // 0x54 + {{'U', 0, 0, 0, 0, 0, 0, 1}}, // 0x55 + {{'V', 0, 0, 0, 0, 0, 0, 1}}, // 0x56 + {{'W', 0, 0, 0, 0, 0, 0, 1}}, // 0x57 + {{'X', 0, 0, 0, 0, 0, 0, 1}}, // 0x58 + {{'Y', 0, 0, 0, 0, 0, 0, 1}}, // 0x59 + {{'Z', 0, 0, 0, 0, 0, 0, 1}}, // 0x5A + {{'[', 0, 0, 0, 0, 0, 0, 1}}, // 0x5B + {{'\\', '\\', 0, 0, 0, 0, 0, 2}}, // 0x5C + {{']', 0, 0, 0, 0, 0, 0, 1}}, // 0x5D + {{'^', 0, 0, 0, 0, 0, 0, 1}}, // 0x5E + {{'_', 0, 0, 0, 0, 0, 0, 1}}, // 0x5F + } + }; + return escapes; + } + + /*! + @brief escape the well-formed multi-byte characters at the start of @a data + + Writes the "\\uXXXX" escape of each character (two of them for a character + above U+FFFF) straight into the write buffer, which is why the caller must + have written out what is pending in @ref string_buffer. Stops at the first + byte that is below 0x80 or starts an ill-formed or truncated sequence, which + the caller handles. + + @return the number of bytes of @a data that have been escaped + */ + JSON_HEDLEY_NEVER_INLINE std::size_t write_u_escapes(const unsigned char* data, std::size_t n) + { + std::size_t i = 0; + while (i < n && data[i] >= 0x80) + { + const std::size_t length = validate_one_utf8(data + i, n - i); + if (length == 0) + { + break; + } + + // a lead byte of 0x80 or above starts a sequence of 2 to 4 bytes + JSON_ASSERT(length >= 2 && length <= 4); + + const auto b0 = static_cast(data[i]); + const auto b1 = static_cast(data[i + 1]); + std::uint32_t codepoint = 0; + switch (length) + { + case 2: + codepoint = ((b0 & 0x1Fu) << 6u) | (b1 & 0x3Fu); + break; + case 3: + codepoint = ((b0 & 0x0Fu) << 12u) | ((b1 & 0x3Fu) << 6u) | (static_cast(data[i + 2]) & 0x3Fu); + break; + default: // 4 + codepoint = ((b0 & 0x07u) << 18u) | ((b1 & 0x3Fu) << 12u) | ((static_cast(data[i + 2]) & 0x3Fu) << 6u) | (static_cast(data[i + 3]) & 0x3Fu); + break; + } + + // at most two escapes of six bytes each + if (JSON_HEDLEY_UNLIKELY(write_buffer.size() - write_buffer_pos < 12)) + { + flush(); + } + char* d = write_buffer.data() + write_buffer_pos; + if (codepoint > 0xFFFFu) + { + d = put_u_escape(d, static_cast(0xD7C0u + (codepoint >> 10u))); + codepoint = 0xDC00u + (codepoint & 0x3FFu); + } + d = put_u_escape(d, static_cast(codepoint)); + write_buffer_pos = static_cast(d - write_buffer.data()); + i += length; + } + return i; + } + + static char* put_u_escape(char* d, const std::uint16_t codeunit) noexcept + { + d[0] = '\\'; + d[1] = 'u'; + return hex4_lower(codeunit, d + 2); + } + /*! * @brief write a lowercase "\\uXXXX" escape sequence into @a string_buffer * * Branch-free replacement for `snprintf(buf, 7, "\\u%04x", codeunit)` in the * string escaping hot path. It writes exactly six characters ('\\', 'u' and * four hex digits) at position @a pos of @a string_buffer via a nibble - * lookup table, avoiding the format-string parsing and locale machinery of + * lookup table (@ref hex4_lower), avoiding the format-string parsing and locale machinery of * `snprintf`. Advances @a pos by the number of bytes written (6). * * @param[in] pos position in @a string_buffer to write at; there must @@ -1269,13 +1804,9 @@ class serializer void write_u_escape(std::size_t& pos, std::uint16_t codeunit) noexcept { JSON_ASSERT(string_buffer.size() - pos >= 6); - constexpr const char* nibble_to_hex = "0123456789abcdef"; string_buffer[pos + 0] = '\\'; string_buffer[pos + 1] = 'u'; - string_buffer[pos + 2] = nibble_to_hex[(codeunit >> 12u) & 0x0Fu]; - string_buffer[pos + 3] = nibble_to_hex[(codeunit >> 8u) & 0x0Fu]; - string_buffer[pos + 4] = nibble_to_hex[(codeunit >> 4u) & 0x0Fu]; - string_buffer[pos + 5] = nibble_to_hex[codeunit & 0x0Fu]; + hex4_lower(codeunit, &string_buffer[pos + 2]); pos += 6; } @@ -1366,8 +1897,9 @@ class serializer /*! @brief dump an integer - Dump a given integer, appending it to @ref write_buffer. Works internally with - @a number_buffer. + Dump a given integer, appending it to @ref write_buffer (directly: copying + the digits from another buffer right after writing them waits until the + stores are done). @param[in] x integer number (signed or unsigned) to dump @tparam NumberType either @a number_integer_t or @a number_unsigned_t @@ -1402,33 +1934,57 @@ class serializer return; } - // use a pointer to fill the buffer - auto buffer_ptr = number_buffer.begin(); // NOLINT(llvm-qualified-auto,readability-qualified-auto) + // use a pointer to fill the buffer (room for as much as number_buffer holds) + if (JSON_HEDLEY_UNLIKELY(write_buffer_pos + number_buffer.size() > write_buffer.size())) + { + flush(); + } + auto* buffer_ptr = write_buffer.data() + write_buffer_pos; - number_unsigned_t abs_value; + number_magnitude_t abs_value; - unsigned int n_chars{}; + // one byte for the minus sign + unsigned int n_chars = 0; if (is_negative_number(x)) { *buffer_ptr = '-'; abs_value = remove_sign(static_cast(x)); - - // account one more byte for the minus sign - n_chars = 1 + count_digits(abs_value); + n_chars = 1; } else { - abs_value = static_cast(x); - n_chars = count_digits(abs_value); + abs_value = static_cast(x); } + // up to 16 digits: eight at a time (as the digits of floats), written + // without leading zeros + if (abs_value < 10000000000000000u) + { + const auto value = static_cast(abs_value); // fits: abs_value < 10^16 + const std::uint64_t upper = value / 100000000u; + const std::uint64_t first = dtoa_impl::eight_digit_bytes(upper != 0 ? upper : value); + const auto leading = static_cast(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); + n_chars += 8 - leading; + if (upper != 0) + { + dtoa_impl::store_msb_first(p + 8 - leading, dtoa_impl::eight_digit_bytes(value - (upper * 100000000u)) + 0x3030303030303030u); + n_chars += 8; + } + write_buffer_pos += n_chars; + return; + } + + n_chars += count_digits(abs_value); + // spare 1 byte for '\0' JSON_ASSERT(n_chars < number_buffer.size() - 1); // jump to the end to generate the string from backward, // so we later avoid reversing the result - buffer_ptr += static_cast(n_chars); + buffer_ptr += n_chars; // Fast int2ascii implementation inspired by "Fastware" talk by Andrei Alexandrescu // See: https://www.youtube.com/watch?v=o4-CwDo2zpg @@ -1451,14 +2007,13 @@ class serializer *(--buffer_ptr) = static_cast('0' + abs_value); } - put_buffer(number_buffer, n_chars); + write_buffer_pos += n_chars; } /*! @brief dump a floating-point number - Dump a given floating-point number, appending it to @ref write_buffer. Works internally - with @a number_buffer. + Dump a given floating-point number, appending it to @ref write_buffer. @param[in] x floating-point number to dump */ @@ -1485,10 +2040,15 @@ class serializer void dump_float(number_float_t x, std::true_type /*is_ieee_single_or_double*/) { - auto* begin = number_buffer.data(); + // directly into the write buffer: copying the text from number_buffer + // right after to_chars() wrote it waits until its stores are done + if (JSON_HEDLEY_UNLIKELY(write_buffer_pos + number_buffer.size() > write_buffer.size())) + { + flush(); + } + auto* begin = write_buffer.data() + write_buffer_pos; auto* end = ::nlohmann::detail::to_chars(begin, begin + number_buffer.size(), x); - - put_buffer(number_buffer, static_cast(end - begin)); + write_buffer_pos += static_cast(end - begin); } JSON_HEDLEY_NON_NULL(1) @@ -1562,17 +2122,6 @@ class serializer } } - /* - * Overload to make the compiler happy while it is instantiating - * dump_integer for number_unsigned_t. - * Must never be called. - */ - number_unsigned_t remove_sign(number_unsigned_t x) - { - JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE - return x; // LCOV_EXCL_LINE - } - /* * Helper function for dump_integer * @@ -1582,21 +2131,32 @@ class serializer * absolute values of INT_MIN and INT_MAX are usually not the same. See * #1708 for details. */ - number_unsigned_t remove_sign(number_integer_t x) noexcept + number_magnitude_t remove_sign(number_integer_t x) noexcept { JSON_ASSERT(x < 0); - return static_cast(-(x + 1)) + 1; + return static_cast(-(x + 1)) + 1; } private: /// the output of the serializer (non-owning; the adapter lives at the call site) output_adapter_protocol* o = nullptr; + /// a character buffer left uninitialized: the buffers below are written + /// before they are read (only the part written is ever read), and zeroing + /// 1.6 KiB took more than half of what dumping a small value costs (the + /// user-provided constructor keeps the member initialization, which + /// -Weffc++ asks for, from zeroing them) + template + struct uninitialized_buffer : std::array + { + uninitialized_buffer() noexcept {} // NOLINT(modernize-use-equals-default,hicpp-use-equals-default,cppcoreguidelines-pro-type-member-init,hicpp-member-init): see above + }; + /// a (hopefully) large enough character buffer - std::array number_buffer{{}}; + uninitialized_buffer<64> number_buffer; /// string buffer - std::array string_buffer{{}}; + uninitialized_buffer<512> string_buffer; /// the indentation character const char indent_char; @@ -1616,7 +2176,7 @@ class serializer /// buffer collecting output before it is flushed to the output adapter, so /// that the many small structural writes become few bulk writes static constexpr std::size_t write_buffer_size = 1024; - std::array write_buffer{{}}; + uninitialized_buffer write_buffer; /// number of valid bytes currently held in @ref write_buffer std::size_t write_buffer_pos = 0; }; diff --git a/include/nlohmann/detail/string_escape.hpp b/include/nlohmann/detail/string_escape.hpp index a6489e054..1a4b6c501 100644 --- a/include/nlohmann/detail/string_escape.hpp +++ b/include/nlohmann/detail/string_escape.hpp @@ -9,6 +9,7 @@ #pragma once #include // size_t +#include // move #include @@ -16,6 +17,25 @@ NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail { +/// append @a s to @a out, escaped like @ref escape does +template +inline void escape_into(StringType& out, const StringType& s) +{ + const std::size_t size = s.size(); + std::size_t run = 0; + for (std::size_t i = 0; i < size; ++i) + { + const char c = s[i]; + if (c == '~' || c == '/') + { + out.append(s.data() + run, i - run); + out.append(c == '~' ? "~0" : "~1", 2); + run = i + 1; + } + } + out.append(s.data() + run, size - run); +} + /*! * @brief string escaping as described in RFC 6901 (Sect. 4) * @param[in] s string to escape @@ -24,39 +44,28 @@ namespace detail * Note the order of escaping "~" to "~0" and "/" to "~1" is important. * * The string is rebuilt in a single pass, appending whole runs between the - * characters that need escaping. Scanning with find_first_of() keeps the - * common case -- nothing to escape -- as fast as a single search, while - * repeated replace() calls would move the tail of the string once per - * escaped character. + * characters that need escaping, while repeated replace() calls would move + * the tail of the string once per escaped character. @ref escape_into appends + * the escaped string to an existing one. */ template inline StringType escape(const StringType& s) { - auto next_special = [&s](std::size_t from) + // a single scan for both special characters; nothing to escape is the common case + const std::size_t size = s.size(); + std::size_t i = 0; + while (i < size && s[i] != '~' && s[i] != '/') { - const auto tilde = s.find_first_of('~', from); - const auto slash = s.find_first_of('/', from); - return tilde < slash ? tilde : slash; // npos is the largest value - }; - - auto pos = next_special(0); - if (pos == StringType::npos) + ++i; + } + if (i == size) { return s; } StringType result; - result.reserve(s.size() + 2); - - std::size_t run = 0; - while (pos != StringType::npos) - { - result.append(s.data() + run, pos - run); - result.append(s[pos] == '~' ? "~0" : "~1", 2); - run = pos + 1; - pos = next_special(run); - } - result.append(s.data() + run, s.size() - run); + result.reserve(size + 2); + escape_into(result, s); return result; } @@ -101,7 +110,7 @@ inline void unescape(StringType& s) pos = s.find_first_of('~', run); } result.append(s.data() + run, s.size() - run); - s = result; + s = std::move(result); } } // namespace detail diff --git a/include/nlohmann/detail/string_utils.hpp b/include/nlohmann/detail/string_utils.hpp index 4ef748f13..9b44623a5 100644 --- a/include/nlohmann/detail/string_utils.hpp +++ b/include/nlohmann/detail/string_utils.hpp @@ -48,6 +48,24 @@ inline std::string hex_byte(const std::uint8_t byte) return result; } +/*! +@brief write a UTF-16 code unit as four lowercase hexadecimal digits + +The lowercase counterpart of hex_byte(), used for the `\\uXXXX` escapes of the +serializers, whose output is lowercase. Writes exactly four characters to +@a out and returns the pointer past them. +*/ +JSON_HEDLEY_ALWAYS_INLINE char* hex4_lower(const std::uint16_t codeunit, char* out) noexcept +{ + constexpr const char* nibble_to_hex = "0123456789abcdef"; + const unsigned int unit = codeunit; + out[0] = nibble_to_hex[(unit >> 12u) & 0x0Fu]; + out[1] = nibble_to_hex[(unit >> 8u) & 0x0Fu]; + out[2] = nibble_to_hex[(unit >> 4u) & 0x0Fu]; + out[3] = nibble_to_hex[unit & 0x0Fu]; + return out + 4; +} + /////////////////// // UTF-8 encoding // /////////////////// @@ -103,13 +121,73 @@ void encode_utf8(std::uint32_t cp, const Out& out) } } +/*! +@brief the shape of a well-formed UTF-8 sequence, by its lead byte + +@a length is 1 for ASCII, 2 to 4 for a valid lead byte, and 0 for a byte that +cannot start a well-formed sequence (a continuation byte, 0xC0, 0xC1, or 0xF5 +to 0xFF). For a multibyte sequence, the second byte must lie in +[@a second_lo, @a second_hi], and every later byte in [0x80, 0xBF] +(RFC 3629, section 4; Unicode Table 3-7). The narrower second-byte ranges +exclude overlong forms (0xE0, 0xF0), surrogates (0xED), and code points +beyond U+10FFFF (0xF4). +*/ +struct utf8_sequence_shape +{ + std::uint8_t length; + std::uint8_t second_lo; + std::uint8_t second_hi; +}; + +/// @return the shape of a UTF-8 sequence starting with @a lead, see utf8_sequence_shape +inline utf8_sequence_shape utf8_shape(const std::uint8_t lead) noexcept +{ + if (lead < 0x80u) + { + return {1, 0, 0}; + } + if (lead < 0xC2u) + { + return {0, 0, 0}; + } + if (lead <= 0xDFu) + { + return {2, 0x80u, 0xBFu}; + } + if (lead == 0xE0u) + { + return {3, 0xA0u, 0xBFu}; + } + if (lead == 0xEDu) + { + return {3, 0x80u, 0x9Fu}; + } + if (lead <= 0xEFu) + { + return {3, 0x80u, 0xBFu}; + } + if (lead == 0xF0u) + { + return {4, 0x90u, 0xBFu}; + } + if (lead <= 0xF3u) + { + return {4, 0x80u, 0xBFu}; + } + if (lead == 0xF4u) + { + return {4, 0x80u, 0x8Fu}; + } + return {0, 0, 0}; +} + /////////////////// // UTF-8 decoding // /////////////////// // UTF-8 decoder states used by decode() below -static constexpr std::uint8_t UTF8_ACCEPT = 0; -static constexpr std::uint8_t UTF8_REJECT = 1; +JSON_INLINE_VARIABLE constexpr std::uint8_t UTF8_ACCEPT = 0; +JSON_INLINE_VARIABLE constexpr std::uint8_t UTF8_REJECT = 1; /*! @brief process a byte of a UTF-8 sequence @@ -118,7 +196,7 @@ This is a single-byte step of a "shift-based" UTF-8 decoder originally written by Björn Hoehrmann. See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for details. -The library checks UTF-8 well-formedness (RFC 3629, section 4) in three +The library checks UTF-8 well-formedness (RFC 3629, section 4) in five places, which differ in speed, diagnostics, and how they read the input: - decode() below: the serializer, to escape and, in strict mode, reject @@ -132,10 +210,15 @@ places, which differ in speed, diagnostics, and how they read the input: bulk string scan, the bulk path of the BON8 reader, and the BON8 writer. They must accept exactly what the lexer's switch accepts. - the byte path of binary_reader::get_bon8_string(): BON8 input without bulk - access, and the bytes the bulk path leaves to it. + access, and the bytes the bulk path leaves to it. It keeps its own ranges: + taking them from utf8_shape() made BON8 decoding 2-3% slower (#5891). +- check_utf8() in detail/view/edit.hpp: strings stored in an editable + json_document, with the index of the offending byte. It also uses + utf8_shape(). -All four must accept the same set of sequences, so a change to one needs a -matching change to the others. +All five must accept the same set of sequences, which the test "UTF-8 +validators accept the same sequences" in unit-class_lexer.cpp checks +exhaustively against decode() and utf8_shape(). @param[in,out] state the current decoder state @param[in,out] codep codepoint (valid only if resulting state is UTF8_ACCEPT) diff --git a/include/nlohmann/detail/value_t.hpp b/include/nlohmann/detail/value_t.hpp index 9fc217bcc..6cdff395b 100644 --- a/include/nlohmann/detail/value_t.hpp +++ b/include/nlohmann/detail/value_t.hpp @@ -14,7 +14,6 @@ #include // uint8_t #include // numeric_limits #include // string -#include // is_signed #include #if JSON_HAS_THREE_WAY_COMPARISON @@ -67,6 +66,39 @@ enum class value_t : std::uint8_t discarded ///< discarded by the parser callback function }; +/*! +@brief the name of a JSON type, as returned by basic_json::type_name() + +Used in exception messages; also by code that reports types without a +basic_json value at hand (such as the zero-copy view). +*/ +inline const char* value_type_name(const value_t t) noexcept +{ + switch (t) + { + case value_t::null: + return "null"; + case value_t::object: + return "object"; + case value_t::array: + return "array"; + case value_t::string: + return "string"; + case value_t::boolean: + return "boolean"; + case value_t::binary: + return "binary"; + case value_t::discarded: + return "discarded"; + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + return "number"; + default: + return "invalid"; + } +} + /*! @brief comparison operator for JSON types @@ -117,6 +149,64 @@ inline bool operator<(const value_t lhs, const value_t rhs) noexcept } #endif +/*! +@brief whether a value of the given type owns heap memory + +Strings, binaries, objects, and arrays are stored as pointers to allocated +values; all other types are stored directly in the union. Every enumerator is +listed so that -Wswitch-enum flags this function when a new type is added. +*/ +inline bool owns_memory(const value_t t) noexcept +{ + switch (t) + { + case value_t::object: + case value_t::array: + case value_t::string: + case value_t::binary: + return true; + + case value_t::null: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::discarded: + default: + return false; + } +} + +/*! +@brief whether a value of the given type is neither an object nor an array + +Unlike basic_json::is_primitive(), this includes value_t::discarded: it names +the types that iterators walk with a primitive_iterator_t (a single value, or no +value for null and discarded) and that JSON Pointer resolution and the +serializer treat as leaves. Every enumerator is listed so that -Wswitch-enum +flags this function when a new type is added. +*/ +inline bool is_primitive(const value_t t) noexcept +{ + switch (t) + { + case value_t::object: + case value_t::array: + return false; + + case value_t::null: + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + return true; + } +} + /*! @brief compare an integer with a floating point number without precision loss @@ -147,13 +237,16 @@ FloatType compare_integer_with_float(const IntegerType i, const FloatType f) noe // values of IntegerType lie in [-bound, bound) when signed and in // [0, bound) when unsigned; digits excludes the sign bit, so bound is a - // power of two that the float represents exactly - const FloatType bound = std::ldexp(static_cast(1), std::numeric_limits::digits); + // power of two that the float represents exactly; the signedness comes + // from numeric_limits as well, because std::is_signed is false for class + // types such as 128-bit or multiprecision integers + using limits = std::numeric_limits; + const FloatType bound = std::ldexp(static_cast(1), limits::digits); if (f >= bound) { return ordered(-1); } - if (std::is_signed::value ? (f < -bound) : (f < static_cast(0))) + if (limits::is_signed ? (f < -bound) : (f < static_cast(0))) { return ordered(1); } diff --git a/include/nlohmann/detail/view/builder.hpp b/include/nlohmann/detail/view/builder.hpp new file mode 100644 index 000000000..3e4ea268a --- /dev/null +++ b/include/nlohmann/detail/view/builder.hpp @@ -0,0 +1,1227 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-FileCopyrightText: 2020 YaoYuan +// SPDX-License-Identifier: MIT + +#pragma once + +#include // find, find_if, max, min +#include // array +#include // size_t, ptrdiff_t +#include // int64_t, uint8_t, uint16_t, uint32_t, uint64_t +#include // memcmp, memcpy +#include // numeric_limits +#include // string +#include // vector + +#include +#include +#include +#include +#include + +// The view's parser: one pass over the input that emits the node index (see +// node.hpp). The table-driven decoding of \u escapes and the fast paths for +// ": " and indentation follow yyjson (https://github.com/ibireme/yyjson, MIT +// license). + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +enum class error_code : std::uint8_t +{ + none, + empty_input, + unexpected_value, + invalid_literal, + expected_key, + expected_colon, + expected_array_end, + expected_object_end, + trailing_characters, + number_after_minus, + number_after_dot, + number_after_exponent, + number_overflow, + string_missing_quote, + string_control_character, + string_utf8, + string_escape, + string_unicode_hex, + string_surrogate_high, + string_surrogate_low, + comment_start, + comment_unterminated, + input_too_large, +}; + +struct parse_failure +{ + error_code code = error_code::none; + std::size_t offset = 0; ///< byte offset of the offending character +}; + +/// FloatType: the number_float_t of the document, whose overflow parse() rejects +template +class builder +{ + public: + builder(document_data& d, const char* src, std::size_t size) noexcept + : m_doc(d) + , m_begin(reinterpret_cast(src)) + , m_end(m_begin + size) + , m_shallow() + {} + + /// returns false and fills `failure` on error + bool run() + { + cursor c(*this); + return c.run(); + } + + builder(const builder&) = delete; + builder& operator=(const builder&) = delete; + builder(builder&&) = delete; + builder& operator=(builder&&) = delete; + ~builder() = default; + + /// where and why the parse failed (after run() returned false) + const parse_failure& failure() const noexcept + { + return m_failure; + } + + private: + struct frame + { + std::uint32_t idx; + std::uint32_t count; + bool is_object; + }; + + /// 64 frames, left uninitialized: they are filled as containers open (the + /// user-provided constructor keeps the member initialization, which + /// -Weffc++ asks for, from zeroing them; that costs up to 30 % on small + /// documents) + struct frame_stack : std::array + { + frame_stack() noexcept {} // NOLINT(modernize-use-equals-default,hicpp-use-equals-default,cppcoreguidelines-pro-type-member-init,hicpp-member-init): see above + }; + + document_data& m_doc; + const unsigned char* const m_begin; + const unsigned char* const m_end; + parse_failure m_failure{}; + + // the open array/object is in the cursor; enclosing ones on a stack that is + // inline for the first 64 levels + frame_stack m_shallow; + std::vector m_deep{}; + + /// remember an object to index after parsing (out of line, so that the + /// parse loop only has a call for it) + NLOHMANN_VIEW_NOINLINE void note_large_object(std::uint32_t idx) + { + m_doc.large_objects.push_back(idx); + } + + NLOHMANN_VIEW_NOINLINE bool fail(error_code c, const unsigned char* at) noexcept + { + m_failure.code = c; + m_failure.offset = static_cast(at - m_begin); + m_doc.node_count = 0; + return false; + } + + /// a failure (recorded by fail) as a comment() result + const unsigned char* fail_at(error_code c, const unsigned char* at) noexcept + { + fail(c, at); + return nullptr; + } + + /// a decoded string: p after its closing quote (nullptr: an error), and + /// its bytes in the arena + struct decoded + { + const unsigned char* p; + std::size_t start; + std::size_t len; + }; + + decoded failed(error_code c, const unsigned char* at) noexcept + { + fail(c, at); + return decoded{nullptr, 0, 0}; + } + + /// the comment at p (*p == '/'): the position after it, or nullptr on error + NLOHMANN_VIEW_NOINLINE const unsigned char* comment(const unsigned char* p) + { + if (m_end - p < 2) + { + ++p; + return fail_at(error_code::comment_start, p); + } + if (p[1] == '/') + { + p += 2; + // (as in parse(), a null byte is the end of the input, so it is + // left for the caller to see) + while (p != m_end && *p != '\n' && *p != '\r' && !(NulIsEnd && *p == 0)) + { + ++p; + } + return p; + } + if (p[1] == '*') + { + p += 2; + for (;;) + { + if (p == m_end || (NulIsEnd && *p == 0)) + { + return fail_at(error_code::comment_unterminated, p); + } + if (*p == '*' && p + 1 != m_end && p[1] == '/') + { + p += 2; + return p; + } + ++p; + } + } + ++p; + return fail_at(error_code::comment_start, p); + } + + /// the index is full (n nodes, parsed up to at): extrapolate the node + /// count from the nodes per input byte so far (with headroom, and at least + /// 1.5 times as many), so that dense inputs regrow once instead of + /// doubling repeatedly; returns the new node array + NLOHMANN_VIEW_NOINLINE node* grow(std::size_t n, const unsigned char* at) + { + // (n nodes are written; at the limit there is no room for another one) + const std::uint64_t limit = document_data::max_nodes(); + // LCOV_EXCL_START (a node array that fills the address space) + if (NLOHMANN_VIEW_UNLIKELY(n >= limit)) + { + document_data::throw_bad_alloc(); + } + // LCOV_EXCL_STOP + const std::uint64_t done = static_cast(at - m_begin) + 1; + const std::uint64_t guess = static_cast(n) * static_cast(m_end - m_begin + 1) / done; + const std::uint64_t grown = guess + (guess / 4) + 64; // a variable: GCC calls a cast of the sum useless where std::uint64_t is std::size_t + // (n is below 2^32: the input is smaller than 4 GiB; the sum cannot wrap) + const std::uint64_t wanted = (std::max)(grown, static_cast(n) + (n / 2) + 64); + m_doc.node_count = n; // the number of nodes reserve() copies into the new array + // (a count beyond the limit is cut: the index does not grow beyond what can be addressed) + m_doc.reserve(static_cast((std::min)(wanted, limit))); + return m_doc.nodes; + } + + /// the four hex digits of a unicode escape at p as a code unit, and p + /// moved past them; or -1, and p at the first bad digit (one table lookup + /// per digit and a single check: the library's table, after yyjson's + /// read_hex_u16) + NLOHMANN_VIEW_ALWAYS_INLINE int hex4(const unsigned char*& p) noexcept + { + if (NLOHMANN_VIEW_LIKELY(m_end - p >= 4)) + { + const int cp = hex_codepoint(p); + if (NLOHMANN_VIEW_LIKELY(cp >= 0)) + { + p += 4; + return cp; + } + } + p = hex4_error(p); + return -1; + } + + /// hex4() failed: the first bad digit (none: p) + NLOHMANN_VIEW_NOINLINE const unsigned char* hex4_error(const unsigned char* p) noexcept + { + if (m_end - p >= 4) + { + while (is_hex(*p)) + { + ++p; + } + } + return p; + } + + /// 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 + /// the arena + NLOHMANN_VIEW_NOINLINE decoded slow_string(const unsigned char* s, const unsigned char* p) + { + // single-character escapes; 0: invalid (and 'u', handled separately) + static const std::array simple_escape = + { + { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, '"', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '/', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '\\', 0, 0, 0, + 0, 0, '\b', 0, 0, 0, '\f', 0, 0, 0, 0, 0, 0, 0, '\n', 0, 0, 0, '\r', 0, '\t', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + } + }; + // The write position is kept in locals: the stores of characters into + // the arena may alias the members of the builder, which would then be + // reloaded after every one. It is stored back where the arena grows, + // and by `write_back` on every return. + const unsigned char* const end = m_end; + const std::size_t start = arena_used(); + char* w = m_arena_write; + const char* w_end = m_arena_end; + const arena_write_back write_back(*this, w); + { + const auto n = static_cast(p - s); + if (n <= 16 && end - s >= 16) + { + arena_ensure(w, w_end, 16); + std::memcpy(w, s, 16); + } + else if (n != 0) + { + // (an empty run leaves an empty arena unallocated, and memcpy() + // must not be passed its null pointer even for zero bytes) + arena_ensure(w, w_end, n); + std::memcpy(w, s, n); + } + w += n; + } + for (;;) + { + if (p == end) + { + return failed(error_code::string_missing_quote, p); + } + const unsigned char c = *p; + if (c == '"') + { + ++p; + // (from w: `write_back` stores it only after the return value + // is made; the arena is allocated, since an escape was written) + return decoded{p, start, static_cast(w - m_doc.arena.data()) - start}; + } + if (c != '\\') + { + // (without JSON_STRICT_NUL_HANDLING, a NUL between values ends + // the input, as in json::parse; inside a string, a NUL is still + // a control character and an error) + 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') + { + 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); + } + cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); + } + arena_ensure(w, w_end, 4); + w = put_utf8(w, cp); + } + else if (NLOHMANN_VIEW_LIKELY(d < 128 && simple_escape[d] != 0)) + { + arena_ensure(w, w_end, 1); + *w++ = simple_escape[d]; + } + else + { + --p; + return failed(error_code::string_escape, p); + } +#if NLOHMANN_VIEW_NEON + if (p != end && *p == '\\') + { + continue; // consecutive escapes ("\u00e4\u00f6"): no run in between + } + // The first 16 bytes of the run are checked one by one, in predicted branches, and copied + // as one block: where the runs between escapes are short, the vector step below would put + // its longer dependency chain (load, compare, mask, count, add) on p for each of them, + // and be slower than these steps (the scanner of the keys works the same way) + { + if (end - p >= 16) + { + const std::uint8_t* const plain = string_plain(); + std::size_t steps = 16; +#define NLOHMANN_VIEW_STEP(i) if (NLOHMANN_VIEW_LIKELY(plain[p[i]] != 0)) {} else { steps = (i); goto run_found; } + NLOHMANN_VIEW_REPEAT16(NLOHMANN_VIEW_STEP) +#undef NLOHMANN_VIEW_STEP +run_found: + arena_ensure(w, w_end, 16); + std::memcpy(w, p, 16); // (only the first `steps` bytes are kept) + w += steps; + p += steps; + if (steps < 16) + { + if (*p >= 0x80) + { + // non-ASCII: the UTF-8 check, and the rest of the run + const unsigned char* const r = p; + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; + } + continue; + } + } + } +#else + if (p != end && (*p == '\\' || *p == '"')) + { + continue; // consecutive escapes ("\u00e4\u00f6"), or the end of the string: no run in between + } +#endif +#if NLOHMANN_VIEW_VECTOR + // the plain bytes up to the next quote, backslash, or control + // character are copied as they are scanned, 16 bytes at a time + for (;;) + { + if (NLOHMANN_VIEW_UNLIKELY(end - p < 16)) + { + const unsigned char* const r = p; + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; + break; + } + arena_ensure(w, w_end, 16); + const unsigned k = copy_plain_run16(p, w); + w += k; + p += k; + if (k == 16) + { + continue; + } + if (NLOHMANN_VIEW_UNLIKELY(*p >= 0x80)) + { + // non-ASCII: the UTF-8 check, and the rest of the run + const unsigned char* const r = p; + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; + } + break; + } +#else + const unsigned char* const r = p; + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; +#endif + } + } + + /// does the float token [s, p) overflow FloatType? (parse() rejects it) + NLOHMANN_VIEW_NOINLINE static bool float_overflows(const unsigned char* s, const unsigned char* p) + { + const auto* const first = reinterpret_cast(s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + const auto* const last = reinterpret_cast(p); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + const char* const dot = std::find(first, last, '.'); + const char* const exponent = std::find_if(first, last, [](char c) + { + return c == 'e' || c == 'E'; + }); + const auto v = convert_float(first, last, dot == last ? std::string::npos : static_cast(dot - first), + static_cast(exponent - first)); + return v > (std::numeric_limits::max)() || v < -(std::numeric_limits::max)(); + } + + /// do the `digits` digits of a magnitude at d exceed `limit`, the largest + /// magnitude that fits (a string literal: N counts its terminating NUL, so + /// it has N - 1 digits)? + template + static NLOHMANN_VIEW_ALWAYS_INLINE bool digits_exceed(const unsigned char* d, std::size_t digits, const char (&limit)[N]) noexcept // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays): a string literal + { + if (NLOHMANN_VIEW_LIKELY(digits < N - 1)) + { + return false; + } + return digits > N - 1 || std::memcmp(d, limit, N - 1) > 0; + } + + static bool is_hex(unsigned char c) noexcept + { + return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); + } + + // decode arena: the vector in the document, written through a raw + // pointer (resized ahead in large steps; trimmed when parsing succeeds) + char* m_arena_write = nullptr; + char* m_arena_end = nullptr; + + /// room for n more bytes at w, the write position kept in a local, with + /// w_end the end of the arena. (A lambda instead is not always inlined, + /// and then w would be kept in memory, as m_arena_write is.) + NLOHMANN_VIEW_ALWAYS_INLINE void arena_ensure(char*& w, const char*& w_end, std::size_t n) + { + if (NLOHMANN_VIEW_UNLIKELY(static_cast(w_end - w) < n)) + { + m_arena_write = w; + arena_grow(n); + w = m_arena_write; + w_end = m_arena_end; + } + } + + /// stores a write position kept in a local back into m_arena_write when + /// the function that keeps it returns, on every path + class arena_write_back + { + public: + arena_write_back(builder& b, char*& w) noexcept + : m_builder(b) + , m_w(w) + {} + ~arena_write_back() + { + m_builder.m_arena_write = m_w; + } + + arena_write_back(const arena_write_back&) = delete; + arena_write_back& operator=(const arena_write_back&) = delete; + arena_write_back(arena_write_back&&) = delete; + arena_write_back& operator=(arena_write_back&&) = delete; + + private: + builder& m_builder; + char*& m_w; + }; + + NLOHMANN_VIEW_ALWAYS_INLINE std::size_t arena_used() const noexcept + { + return m_arena_write != nullptr ? static_cast(m_arena_write - m_doc.arena.data()) : 0; + } + + NLOHMANN_VIEW_NOINLINE void arena_grow(std::size_t n) + { + const std::size_t used = arena_used(); + m_doc.arena.resize((std::max)(m_doc.arena.size() * 2, used + n + 256)); + m_arena_write = m_doc.arena.data() + used; + m_arena_end = m_doc.arena.data() + m_doc.arena.size(); + } + + /// UTF-8 encoding of cp at w (room for 4 bytes); returns the position after + /// it. (A returned pointer stays in a register; writing through a + /// reference to m_arena_write would store and reload it around every byte.) + static char* put_utf8(char* w, int cp) noexcept + { + if (cp < 0x80) + { + *w++ = static_cast(cp); + } + else if (cp < 0x800) + { + *w++ = static_cast(0xC0 | (cp >> 6)); + *w++ = static_cast(0x80 | (cp & 0x3F)); + } + else if (cp < 0x10000) + { + *w++ = static_cast(0xE0 | (cp >> 12)); + *w++ = static_cast(0x80 | ((cp >> 6) & 0x3F)); + *w++ = static_cast(0x80 | (cp & 0x3F)); + } + else + { + *w++ = static_cast(0xF0 | (cp >> 18)); + *w++ = static_cast(0x80 | ((cp >> 12) & 0x3F)); + *w++ = static_cast(0x80 | ((cp >> 6) & 0x3F)); + *w++ = static_cast(0x80 | (cp & 0x3F)); + } + return w; + } + + /// a compile-time option as a runtime condition: testing the template + /// argument directly makes a condition like `TrailingCommas && c == ']'` + /// constant when the option is off, which MSVC reports as C4127 + static NLOHMANN_VIEW_ALWAYS_INLINE bool enabled(bool option) noexcept + { + return option; + } + + /// The parse state and the parser proper. The cursor is a local object of + /// run() whose address never escapes (everything it calls out of line is a + /// member of the builder and gets the positions it needs), so that the + /// compiler keeps the state in registers instead of reloading it from + /// memory after every node store and call. + struct cursor + { + explicit cursor(builder& owner) noexcept + : cold(owner) + , b(owner.m_begin) + , p(owner.m_begin) + , e(owner.m_end) + {} + + builder& cold; ///< out-of-line helpers and state that needs no registers + const unsigned char* const b; + const unsigned char* p; + const unsigned char* const e; + node* base = nullptr; + node* out = nullptr; + node* cap = nullptr; + + // the open array/object + std::uint32_t cur_idx = 0; + std::uint32_t cur_count = 0; + bool cur_is_object = false; + std::size_t depth = 0; + std::size_t max_depth = 0; + + NLOHMANN_VIEW_ALWAYS_INLINE bool run() + { + cold.m_doc.reserve(estimate_nodes(reinterpret_cast(b), static_cast(e - b))); + 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) + { + p += 3; // byte order mark + } + if (!ws()) + { + return false; + } + if (p == e || (NulIsEnd && *p == 0)) + { + return fail(error_code::empty_input); + } + + // root value + switch (cur()) + { + case '{': + open(value_t::object); + ++p; + goto obj_first; + case '[': + open(value_t::array); + ++p; + goto arr_first; + default: + if (!scalar()) + { + return false; + } + goto root_done; + } + + // value dispatch, expanded once for array elements and once for member + // values: each jump has its own history (arrays tend to hold one kind of + // value), and the continuation needs no branch on the container kind; + // after a scalar, the switch is left to the label that follows it +#define NLOHMANN_VIEW_VALUE \ + switch (cur()) \ + { \ + case '"': \ + if (NLOHMANN_VIEW_UNLIKELY(!string())) { return false; } \ + break; \ + case '{': \ + open(value_t::object); \ + ++p; \ + goto obj_first; \ + case '[': \ + open(value_t::array); \ + ++p; \ + goto arr_first; \ + case '-': \ + if (NLOHMANN_VIEW_UNLIKELY(!number())) { return false; } \ + break; \ + case '0': case '1': case '2': case '3': case '4': \ + case '5': case '6': case '7': case '8': case '9': \ + if (NLOHMANN_VIEW_UNLIKELY(!number())) { return false; } \ + break; \ + case 't': \ + if (NLOHMANN_VIEW_UNLIKELY(!literal_true())) { return false; } \ + break; \ + case 'f': \ + if (NLOHMANN_VIEW_UNLIKELY(!literal_false())) { return false; } \ + break; \ + case 'n': \ + if (NLOHMANN_VIEW_UNLIKELY(!literal_null())) { return false; } \ + break; \ + default: \ + return fail(error_code::unexpected_value); \ + } + +arr_first: + if (!ws()) + { + return false; + } + if (cur() == ']') + { + ++p; + goto close_container; + } +value: + NLOHMANN_VIEW_VALUE +arr_next: + ++cur_count; + if (!ws()) + { + return false; + } + if (NLOHMANN_VIEW_LIKELY(cur() == ',')) + { + ++p; + if (!ws()) + { + return false; + } + if (enabled(TrailingCommas) && cur() == ']') + { + ++p; + goto close_container; + } + goto value; + } + if (cur() == ']') + { + ++p; + goto close_container; + } + return fail(error_code::expected_array_end); + +obj_first: + if (!ws()) + { + return false; + } + if (cur() == '}') + { + ++p; + goto close_container; + } +obj_key: + if (NLOHMANN_VIEW_UNLIKELY(cur() != '"')) + { + return fail(error_code::expected_key); + } + if (NLOHMANN_VIEW_UNLIKELY(!string())) + { + return false; + } + if (NLOHMANN_VIEW_LIKELY(cur() == ':' && (Sentinel || e - p >= 2) && p[1] == ' ')) + { + p += 2; // ": " (pretty-printed input; a fast path of yyjson) + } + else + { + if (!ws()) + { + return false; + } + if (NLOHMANN_VIEW_UNLIKELY(cur() != ':')) + { + return fail(error_code::expected_colon); + } + ++p; + } + if (!ws()) + { + return false; + } + NLOHMANN_VIEW_VALUE +#undef NLOHMANN_VIEW_VALUE +obj_next: + ++cur_count; + if (!ws()) + { + return false; + } + if (NLOHMANN_VIEW_LIKELY(cur() == ',')) + { + ++p; + if (!ws()) + { + return false; + } + if (enabled(TrailingCommas) && cur() == '}') + { + ++p; + goto close_object; + } + goto obj_key; + } + if (cur() == '}') + { + ++p; + goto close_object; + } + return fail(error_code::expected_object_end); + +close_object: + // a large object gets a hash index for its keys (objects only, so that closing + // an array pays nothing for this) + if (NLOHMANN_VIEW_UNLIKELY(cur_count >= document_data::index_min_members)) + { + cold.note_large_object(cur_idx); + } + +close_container: + close(); + if (NLOHMANN_VIEW_UNLIKELY(depth == 0)) + { + goto root_done; + } + if (cur_is_object) + { + goto obj_next; + } + goto arr_next; + +root_done: + if (!ws()) + { + return false; + } + if (p != e && !(NulIsEnd && *p == 0)) + { + return fail(error_code::trailing_characters); + } + cold.m_doc.node_count = static_cast(out - base); + cold.m_doc.max_depth = max_depth; + // (so that size() is the number of bytes used: the arena grows ahead + // in large steps, and shrink_to_fit() keeps size() bytes) + cold.m_doc.arena.resize(cold.arena_used()); + return true; + } + + NLOHMANN_VIEW_ALWAYS_INLINE bool fail(error_code c) noexcept + { + return cold.fail(c, p); + } + + /// the current byte, or 0 at the end. With a NUL-terminated input + /// (Sentinel) the terminator is read instead of checking the bounds; a 0 + /// never matches a JSON token, so the error paths tell the end apart. + NLOHMANN_VIEW_ALWAYS_INLINE unsigned char cur() const noexcept + { + if (Sentinel) + { + return *p; + } + return p != e ? *p : 0; + } + + /// root scalar + NLOHMANN_VIEW_ALWAYS_INLINE bool scalar() + { + switch (cur()) + { + case '"': + return string(); + case 't': + return literal_true(); + case 'f': + return literal_false(); + case 'n': + return literal_null(); + case '-': + return number(); + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + return number(); + default: + return fail(error_code::unexpected_value); + } + } + + NLOHMANN_VIEW_ALWAYS_INLINE bool literal_true() + { + return literal("true", value_t::boolean, node_flags::is_true); + } + + NLOHMANN_VIEW_ALWAYS_INLINE bool literal_false() + { + return literal("false", value_t::boolean, 0); + } + + NLOHMANN_VIEW_ALWAYS_INLINE bool literal_null() + { + return literal("null", value_t::null, 0); + } + + /// skip whitespace (and comments); false on a malformed comment + NLOHMANN_VIEW_ALWAYS_INLINE bool ws() + { + const unsigned char c = cur(); + if (NLOHMANN_VIEW_LIKELY(c > ' ' && (!Comments || c != '/'))) + { + return true; // no whitespace: the common case in minified input + } + return ws_slow(); + } + + NLOHMANN_VIEW_ALWAYS_INLINE bool ws_slow() + { + for (;;) + { + if (cur() == ' ' && (Sentinel || e - p >= 2) && p[1] > ' ' && (!Comments || p[1] != '/')) + { + ++p; // single space, e.g. after ':' or ',' + return true; + } + if (cur() == '\n' || cur() == '\r') + { + // (a branch, not an add of the comparison: p must not + // wait for the byte after the line break) + if (NLOHMANN_VIEW_UNLIKELY(cur() == '\r') && (Sentinel || e - p >= 2) && p[1] == '\n') + { + p += 2; + } + else + { + ++p; + } + // indentation: two spaces per step, fixed offsets (after yyjson) + while (e - p >= 32) + { +#define NLOHMANN_VIEW_STEP(i) if (NLOHMANN_VIEW_LIKELY(load16(p + (std::ptrdiff_t{2} * (i))) == 0x2020)) {} else { p += std::ptrdiff_t{2} * (i); goto indent_done; } + NLOHMANN_VIEW_REPEAT16(NLOHMANN_VIEW_STEP) +#undef NLOHMANN_VIEW_STEP + p += 32; + } +indent_done: + ; + } + for (unsigned char c = cur(); c == ' ' || c == '\n' || c == '\r' || c == '\t'; c = cur()) + { + ++p; + } + if (enabled(Comments) && cur() == '/') + { + const unsigned char* const q = cold.comment(p); + if (q == nullptr) + { + return false; + } + p = q; + continue; + } + return true; + } + } + + /// append a node: (kind, flags, extra, off) and the second word (len, or + /// an integer's value); two stores on little-endian targets + NLOHMANN_VIEW_ALWAYS_INLINE node* push_node(value_t k, std::uint8_t flags, std::uint16_t extra, std::size_t off, std::uint64_t second) + { + if (NLOHMANN_VIEW_UNLIKELY(out == cap)) + { + const auto n = static_cast(out - base); + base = cold.grow(n, p); + out = base + n; + cap = base + cold.m_doc.node_capacity; + } + node* n = out++; +#if NLOHMANN_VIEW_LITTLE_ENDIAN + const std::uint64_t first = static_cast(k) | (static_cast(flags) << 8) + | (static_cast(extra) << 16) | (static_cast(off) << 32); + std::memcpy(reinterpret_cast(n), &first, 8); + std::memcpy(reinterpret_cast(n) + 8, &second, 8); +#else + n->kind = static_cast(k); + n->flags = flags; + n->extra = extra; + n->off = static_cast(off); + // len is the low half of the second word, next the high half + // (not a native word over both, which swaps them on big-endian) + n->len = static_cast(second); + n->next = static_cast(second >> 32); +#endif + return n; + } + + NLOHMANN_VIEW_ALWAYS_INLINE void open(value_t k) + { + // (base is read after push_node(), which moves the node array when it + // grows; in one expression the order of the two is unspecified) + const node* const n = push_node(k, 0, 0, static_cast(p - b), 0); + const auto idx = static_cast(n - base); + if (depth != 0) + { + if (NLOHMANN_VIEW_LIKELY(depth <= 64)) + { + // field by field: a frame put together on the stack and + // copied would be read back wider than it was written, + // and that load waits until the stores are done + frame& f = cold.m_shallow[depth - 1]; + f.idx = cur_idx; + f.count = cur_count; + f.is_object = cur_is_object; + } + else + { + cold.m_deep.push_back(frame{cur_idx, cur_count, cur_is_object}); + } + } + ++depth; + max_depth = (std::max)(max_depth, depth); + cur_idx = idx; + cur_count = 0; + cur_is_object = k == value_t::object; + } + + NLOHMANN_VIEW_ALWAYS_INLINE void close() + { + node& n = base[cur_idx]; + n.len = cur_count; + n.next = static_cast(out - base) - cur_idx; + if (--depth != 0) + { + if (NLOHMANN_VIEW_LIKELY(depth <= 64)) + { + const frame& f = cold.m_shallow[depth - 1]; + cur_idx = f.idx; + cur_count = f.count; + cur_is_object = f.is_object; + } + else + { + const frame f = cold.m_deep.back(); + cold.m_deep.pop_back(); + cur_idx = f.idx; + cur_count = f.count; + cur_is_object = f.is_object; + } + } + } + + /// the literal `text` at p (N counts the terminating NUL) + template + NLOHMANN_VIEW_ALWAYS_INLINE bool literal(const char (&text)[N], value_t k, std::uint8_t flags) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays): a string literal + { + const std::size_t n = N - 1; + if (NLOHMANN_VIEW_UNLIKELY(e - p < static_cast(n) || std::memcmp(p, text, n) != 0)) + { + return fail(error_code::invalid_literal); + } + push_node(k, flags, 0, static_cast(p - b), n); + p += n; + return true; + } + + /// a number at p; the sign is known from the dispatch (so that p does + /// not have to wait for the first byte) + template + NLOHMANN_VIEW_ALWAYS_INLINE bool number() + { + const unsigned char* const s = p; + if (negative) + { + ++p; + } + const unsigned char* const int_start = p; + if (cur() == '0') + { + ++p; + } + else if (NLOHMANN_VIEW_LIKELY(cur() >= '1' && cur() <= '9')) + { + p = skip_digits(p + 1, e); + } + else + { + return fail(error_code::number_after_minus); + } + const auto int_digits = static_cast(p - int_start); + std::size_t frac_digits = 0; + bool is_float = false; + if (cur() == '.') + { + ++p; + const unsigned char* const f0 = p; + p = skip_digits(p, e); + if (NLOHMANN_VIEW_UNLIKELY(p == f0)) + { + return fail(error_code::number_after_dot); + } + frac_digits = static_cast(p - f0); + is_float = true; + } + std::int64_t exponent = 0; + if ((cur() | 0x20) == 'e') + { + ++p; + bool exp_negative = false; + if (cur() == '+' || cur() == '-') + { + exp_negative = *p == '-'; + ++p; + } + if (NLOHMANN_VIEW_UNLIKELY(!is_digit(cur()))) + { + return fail(error_code::number_after_exponent); + } + while (is_digit(cur())) + { + if (exponent < 100000) + { + exponent = (exponent * 10) + (*p - '0'); + } + ++p; + } + if (exp_negative) + { + exponent = -exponent; + } + is_float = true; + } + + value_t kind = value_t::number_float; + if (!is_float) + { + // integers that do not fit become floats, as in parse() + if (negative) + { + kind = digits_exceed(int_start, int_digits, "9223372036854775808") ? value_t::number_float : value_t::number_integer; + } + else + { + kind = digits_exceed(int_start, int_digits, "18446744073709551615") ? value_t::number_float : value_t::number_unsigned; + } + } + // parse() rejects floats that overflow; only numbers whose magnitude + // could reach the largest FloatType (1e308 for double, 1e38 for + // float) need the conversion + if (NLOHMANN_VIEW_UNLIKELY(static_cast(int_digits) + exponent > std::numeric_limits::max_exponent10 - 8 && kind == value_t::number_float)) + { + if (builder::float_overflows(s, p)) + { + p = s; + return fail(error_code::number_overflow); + } + } + const auto layout = static_cast((int_digits < 255 ? int_digits : 255) | ((frac_digits < 255 ? frac_digits : 255) << 8)); + auto second = static_cast(p - s); + if (kind != value_t::number_float) + { + // integers are converted now, while their digits are in cache + const std::uint64_t m = int_digits <= 19 ? parse_upto19(int_start, static_cast(int_digits), e) + : (parse_upto19(int_start, 19, e) * 10) + static_cast(int_start[19] - '0'); + // (0 - m rather than -m: unary minus on an unsigned type is + // MSVC's C4146, an error under /sdl; both give the bit pattern + // of the negative std::int64_t) + second = negative ? 0 - m : m; + } + push_node(kind, 0, layout, static_cast(s - b), second); + return true; + } + + /// a string at p: a value (Value) or a key + template + NLOHMANN_VIEW_ALWAYS_INLINE bool string() + { + ++p; // opening quote + const unsigned char* const s = p; + p = scan_string_run(p, e); + if (NLOHMANN_VIEW_LIKELY(cur() == '"')) + { + push_node(value_t::string, 0, 0, static_cast(s - b), static_cast(p - s)); + ++p; + return true; + } + const decoded r = cold.slow_string(s, p); + if (r.p == nullptr) + { + return false; + } + p = r.p; + push_node(value_t::string, node_flags::escaped, 0, r.start, r.len); + return true; + } + }; +}; + +/// run the builder with compile-time options +template +inline bool build_with(document_data& d, const char* src, std::size_t size, bool sentinel, parse_failure& failure) +{ + if (sentinel) + { + builder bld(d, src, size); + const bool ok = bld.run(); + failure = bld.failure(); + return ok; + } + builder bld(d, src, size); + const bool ok = bld.run(); + failure = bld.failure(); + return ok; +} + +/// sentinel: src[size] is readable and 0 (e.g. std::string); FloatType: the +/// number_float_t of the document +template +inline bool build(document_data& d, const char* src, std::size_t size, bool comments, bool trailing_commas, bool sentinel, parse_failure& failure) +{ + if (comments) + { + return trailing_commas ? build_with(d, src, size, sentinel, failure) + : build_with(d, src, size, sentinel, failure); + } + return trailing_commas ? build_with(d, src, size, sentinel, failure) + : build_with(d, src, size, sentinel, failure); +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/compare.hpp b/include/nlohmann/detail/view/compare.hpp new file mode 100644 index 000000000..8ca6058f0 --- /dev/null +++ b/include/nlohmann/detail/view/compare.hpp @@ -0,0 +1,320 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // sort, stable_sort +#include // size_t +#include // string +#include // move, pair +#include // vector + +#include +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +// Equality of views, and of views with basic_json values, with the semantics +// of basic_json's operator== applied to the values parse() would produce: +// numbers compare by value across their types, an object is compared by its +// members with duplicate keys resolved as parse() resolves them (the last +// value, at the position of the first occurrence), and in document order if +// the object type keeps an order (ordered_json), by key otherwise. + +/// one side of a comparison: a view +template +class view_side +{ + public: + using string_view_t = typename View::string_view_t; + + explicit view_side(const View& v) noexcept + : m_view(v) + {} + + value_t type() const noexcept + { + return m_view.type(); + } + + std::size_t size() const noexcept + { + return m_view.size(); + } + + string_view_t string() const + { + return m_view.get_string(); + } + + /// a number, boolean, or null as a basic_json value (no allocation) + BasicJsonType scalar() const + { + switch (m_view.type()) + { + case value_t::number_integer: + return BasicJsonType(m_view.template get()); + case value_t::number_unsigned: + return BasicJsonType(m_view.template get()); + case value_t::number_float: + return BasicJsonType(m_view.template get()); + case value_t::boolean: + return BasicJsonType(m_view.template get()); + 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); + } + } + + void elements(std::vector& out) const + { + out.reserve(m_view.size()); + for (const View e : m_view) + { + out.emplace_back(e); + } + } + + /// the members as parse() keeps them: one per key, the last value at the + /// position of the first occurrence; in that order, or sorted by key + void members(std::vector>& out, bool ordered) const + { + struct member + { + string_view_t key; + View value; + std::size_t position; + }; + std::vector all; + all.reserve(m_view.size()); + std::size_t position = 0; + for (auto it = m_view.begin(); it != m_view.end(); ++it) + { + all.push_back(member{it.key(), it.value(), position++}); + } + std::stable_sort(all.begin(), all.end(), [](const member & a, const member & b) + { + return a.key < b.key; + }); + std::vector unique; + unique.reserve(all.size()); + for (std::size_t i = 0; i < all.size();) + { + std::size_t last = i; + while (last + 1 < all.size() && all[last + 1].key == all[i].key) + { + ++last; + } + unique.push_back(member{all[i].key, all[last].value, all[i].position}); + i = last + 1; + } + if (ordered) + { + std::sort(unique.begin(), unique.end(), [](const member & a, const member & b) + { + return a.position < b.position; + }); + } + out.reserve(unique.size()); + for (const member& m : unique) + { + out.emplace_back(m.key, view_side(m.value)); + } + } + + private: + View m_view; +}; + +/// the other side of a comparison: a basic_json value +template +class json_side +{ + public: + using string_view_t = StringView; + + explicit json_side(const BasicJsonType& j) noexcept + : m_json(&j) + {} + + value_t type() const noexcept + { + return m_json->type(); + } + + std::size_t size() const noexcept + { + return m_json->size(); + } + + string_view_t string() const + { + const auto& s = m_json->template get_ref(); + return string_view_t(s.data(), s.size()); + } + + BasicJsonType scalar() const + { + return *m_json; + } + + void elements(std::vector& out) const + { + out.reserve(m_json->size()); + for (const auto& e : *m_json) + { + out.emplace_back(e); + } + } + + void members(std::vector>& out, bool ordered) const + { + out.reserve(m_json->size()); + for (auto it = m_json->cbegin(); it != m_json->cend(); ++it) + { + out.emplace_back(string_view_t(it.key().data(), it.key().size()), json_side(it.value())); + } + if (!ordered) + { + std::sort(out.begin(), out.end(), [](const std::pair& a, const std::pair& b) + { + return a.first < b.first; + }); + } + } + + private: + const BasicJsonType* m_json; +}; + +inline bool is_number(value_t t) noexcept +{ + return t == value_t::number_integer || t == value_t::number_unsigned || t == value_t::number_float; +} + +/// whether two sides are equal; iterative, so that the nesting depth is +/// limited by memory only +template +bool equal(const A& a0, const B& b0) +{ + using string_view_t = typename A::string_view_t; + const bool ordered = is_ordered_map::value; + + struct frame + { + std::vector elements_a{}; + std::vector elements_b{}; + std::vector> members_a{}; + std::vector> members_b{}; + bool object = false; + std::size_t next = 0; + }; + std::vector stack; + A a = a0; + B b = b0; + for (;;) + { + const value_t ta = a.type(); + const value_t tb = b.type(); + if (ta == value_t::discarded || tb == value_t::discarded) + { + // basic_json decides (JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON) + if (ta != tb || !(BasicJsonType(value_t::discarded) == BasicJsonType(value_t::discarded))) + { + return false; + } + } + else + { + if (ta != tb && !(is_number(ta) && is_number(tb))) // numbers of different types can be equal + { + return false; + } + if (ta == value_t::string) + { + if (a.string() != b.string()) + { + return false; + } + } + else if (ta == value_t::array || ta == value_t::object) + { + if (a.size() != b.size() && ta == value_t::array) + { + return false; + } + frame f; + f.object = ta == value_t::object; + if (f.object) + { + a.members(f.members_a, ordered); + b.members(f.members_b, ordered); + if (f.members_a.size() != f.members_b.size()) + { + return false; + } + } + else + { + a.elements(f.elements_a); + b.elements(f.elements_b); + } + stack.push_back(std::move(f)); + } + else if (a.scalar() != b.scalar()) // numbers (also of different types), null, boolean + { + return false; + } + } + + // the next pair of values + for (;;) + { + if (stack.empty()) + { + return true; + } + frame& f = stack.back(); + const std::size_t count = f.object ? f.members_a.size() : f.elements_a.size(); + if (f.next == count) + { + stack.pop_back(); // (f dangles now, and is bound again after continue) + continue; + } + if (f.object) + { + if (!(f.members_a[f.next].first == f.members_b[f.next].first)) + { + return false; + } + a = f.members_a[f.next].second; + b = f.members_b[f.next].second; + } + else + { + a = f.elements_a[f.next]; + b = f.elements_b[f.next]; + } + ++f.next; + break; + } + } +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/document_data.hpp b/include/nlohmann/detail/view/document_data.hpp new file mode 100644 index 000000000..55f9d8c76 --- /dev/null +++ b/include/nlohmann/detail/view/document_data.hpp @@ -0,0 +1,299 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // array +#include // size_t +#include // uint8_t, uint32_t +#include // memcmp, memcpy +#include // numeric_limits +#include // map +#include // unique_ptr +#include // bad_alloc, operator new, placement new +#include // string +#include // vector + +#include +#include +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// storage of a parsed document; heap-allocated (header and an initial node +/// array in one block) so that views survive moves of the owning document +struct document_data +{ + const char* src = nullptr; + std::size_t size = 0; + node* nodes = nullptr; ///< the node index (see node.hpp) + std::size_t node_count = 0; + std::size_t node_capacity = 0; + node* inline_nodes = nullptr; ///< node array allocated together with this header + std::size_t inline_capacity = 0; + std::size_t max_depth = 0; ///< deepest nesting of arrays and objects (the root container is depth 1) + std::vector arena; ///< decoded strings that contained escapes (no terminator: they are handed out with their length) + std::size_t arena_size = 0; ///< bytes of decoded strings at base[1] (the arena, or those of a loaded image) + std::string owned; ///< owned copy of the input, if any (a std::string, so that its NUL serves as the parser's sentinel) + std::vector owned_image; ///< a loaded image the document owns (the text and the decoded strings point into it) + + // hash indexes of the keys of large objects (see object_index.hpp) + static constexpr std::uint32_t index_min_members = 128; + struct object_index + { + std::size_t start; ///< first slot in index_slots + std::uint32_t mask; ///< slot count - 1 (a power of two minus one) + }; + std::vector indexes; + std::vector index_slots; + std::vector large_objects; ///< positions of the objects to index (noted while parsing) + std::array base = {{nullptr, nullptr, nullptr, nullptr}}; ///< string bases: source, arena, edit arena, unused (indexed by flags & node_flags::storage) + bool discarded = true; + + /// The storage of edits (editable documents only; see edit_storage.hpp). + /// No edit moves or resizes the parsed index, and the nodes of values + /// written by edits never move either, so a view stays valid whenever it + /// was taken, before the first edit or between later ones: an array/object + /// whose elements change gets node_flags::moved, and its elements then live + /// in a separate sequence (a header node, then the entries), whose entries + /// link to the values. + struct edit_state + { + std::vector moved{}; ///< element sequences of moved arrays/objects (header node first) // NOLINT(readability-redundant-member-init) + std::vector moved_cap{}; ///< capacity in nodes of a growable block; 0: a fixed sequence (a new value) // NOLINT(readability-redundant-member-init) + std::vector> chunks{}; ///< storage of new values and blocks; never moved // NOLINT(readability-redundant-member-init,cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + std::map regions{}; ///< new arrays/objects: root -> container that uses it as its element sequence (nullptr: linked from a block) // NOLINT(readability-redundant-member-init) + node* chunk_cur = nullptr; + node* chunk_end = nullptr; + std::size_t chunk_next = 64; + std::vector> texts{}; ///< edit arena, the current buffer last; earlier ones stay alive for string views // NOLINT(readability-redundant-member-init,cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + std::size_t text_used = 0; + std::size_t text_cap = 0; + std::size_t bytes = 0; ///< memory held by edits + }; + std::unique_ptr edits; ///< created by the first edit + + /// One allocation for the header and room for `count` nodes, so that a + /// small document needs one allocation instead of two. Large documents get + /// a separate node array instead (so it can be trimmed). When a document + /// outgrows the inline array, that array stays unused: at most 256 nodes + /// (4 KiB), and only for documents whose estimate was too small. + static document_data* create(std::size_t count) + { + count = count <= 256 ? count : 0; + void* mem = ::operator new (sizeof(document_data) + (count * sizeof(node))); + auto* d = new (mem) document_data(); // NOLINT(cppcoreguidelines-owning-memory): owned by the returned pointer, freed by deleter + // (aligned: sizeof is a multiple of the alignment; through void*, as GCC's -Wcast-align wants) + // (null without inline nodes, not a pointer past the end of the allocation) + d->inline_nodes = count == 0 ? nullptr : static_cast(static_cast(static_cast(mem) + sizeof(document_data))); // NOLINT(bugprone-casting-through-void) + d->inline_capacity = count; + d->nodes = d->inline_nodes; + d->node_capacity = count; + return d; + } + + struct deleter + { + void operator()(document_data* d) const noexcept + { + d->~document_data(); + ::operator delete (d); + } + }; + + /// user-provided so that the class-type members can be initialized in the + /// member initialization list (-Weffc++ asks for it, and old GCC rejects a + /// defaulted constructor whose exception specification differs from the + /// implicit one); they cannot take default member initializers, which old + /// Clang (3.4-3.6) rejects. The other members have default member + /// initializers. noexcept: create() constructs into raw memory and could not + /// release it if this threw (the std::vector and std::string default + /// constructors do not allocate). + document_data() noexcept + : arena() // NOLINT(readability-redundant-member-init) + , owned() // NOLINT(readability-redundant-member-init) + , owned_image() // NOLINT(readability-redundant-member-init) + , indexes() // NOLINT(readability-redundant-member-init) + , index_slots() // NOLINT(readability-redundant-member-init) + , large_objects() // NOLINT(readability-redundant-member-init) + , edits() // NOLINT(readability-redundant-member-init) + {} + document_data(const document_data&) = delete; + document_data(document_data&&) = delete; + document_data& operator=(const document_data&) = delete; + document_data& operator=(document_data&&) = delete; + ~document_data() + { + release(); + } + + void release() noexcept + { + if (nodes != inline_nodes) + { + ::operator delete (nodes); + } + nodes = inline_nodes; + node_capacity = inline_capacity; + } + + /// the largest node count whose size in bytes fits a std::size_t + static constexpr std::size_t max_nodes() noexcept + { + return (std::numeric_limits::max)() / sizeof(node); + } + + [[noreturn]] NLOHMANN_VIEW_NOINLINE static void throw_bad_alloc() + { + NLOHMANN_VIEW_THROW(std::bad_alloc()); + } + + /// make room for n nodes; keeps the first node_count nodes (throws + /// std::bad_alloc for a count that does not fit the address space, + /// instead of wrapping around in n * sizeof(node)) + void reserve(std::size_t n) + { + if (n <= node_capacity) + { + return; + } + if (NLOHMANN_VIEW_UNLIKELY(n > max_nodes())) + { + throw_bad_alloc(); + } + node* fresh = static_cast(::operator new (n * sizeof(node))); + if (node_count != 0) + { + std::memcpy(fresh, nodes, node_count * sizeof(node)); + } + release(); + nodes = fresh; + node_capacity = n; + } + + const char* str(const node& n) const noexcept + { + return base[n.flags & node_flags::storage] + n.off; + } + + /// whether the string of node k (a key) is the n bytes at key + bool key_equals(const node& k, const char* key, std::size_t n) const noexcept + { + return k.len == n && (n == 0 || std::memcmp(str(k), key, n) == 0); + } + + /// the node after n's subtree (containers span `next` nodes, scalars one) + static NLOHMANN_VIEW_ALWAYS_INLINE const node* after(const node* n) noexcept + { + return n + (is_container(*n) ? n->next : 1u); + } + + /// first element (array) or first key (object) of a container + static NLOHMANN_VIEW_ALWAYS_INLINE const node* first_child(const node* n) noexcept + { + return n + 1; + } + + /// end of the elements of a container + static NLOHMANN_VIEW_ALWAYS_INLINE const node* child_end(const node* n) noexcept + { + return n + n->next; + } + + /// (editable documents) first element or key, also of a moved container + NLOHMANN_VIEW_ALWAYS_INLINE const node* first_child_edited(const node* n) const noexcept + { + return NLOHMANN_VIEW_LIKELY((n->flags & node_flags::moved) == 0) ? n + 1 : edits->moved[n->off] + 1; + } + + /// (editable documents) end of the elements, also of a moved container + NLOHMANN_VIEW_ALWAYS_INLINE const node* child_end_edited(const node* n) const noexcept + { + if (NLOHMANN_VIEW_LIKELY((n->flags & node_flags::moved) == 0)) + { + return n + n->next; + } + const node* const h = edits->moved[n->off]; + return h + h->next; + } + + /// (editable documents) the value at an element position: entries of + /// moved sequences are links. The link case is out of line, so that this + /// compiles to a predicted branch rather than a select that delays the + /// following loads. + static NLOHMANN_VIEW_ALWAYS_INLINE const node* deref(const node* n) noexcept + { + return NLOHMANN_VIEW_LIKELY(n->kind != kind_link) ? n : follow_link(n); + } + + static NLOHMANN_VIEW_NOINLINE const node* follow_link(const node* n) noexcept + { + return link_target(*n); + } +}; + +/// How the index is walked: views of read-only documents follow the node +/// array alone and compile without any of the edit handling; views of +/// editable documents also follow moved element sequences and links. +template +struct navigation +{ + /// whether the walk follows edits (the template argument as a runtime + /// condition: `Editable && ...` is a constant condition for MSVC, C4127) + static NLOHMANN_VIEW_ALWAYS_INLINE bool editable() noexcept + { + return false; + } + + static NLOHMANN_VIEW_ALWAYS_INLINE const node* first(const document_data& /*d*/, const node* n) noexcept + { + return n + 1; + } + + static NLOHMANN_VIEW_ALWAYS_INLINE const node* end(const document_data& /*d*/, const node* n) noexcept + { + return n + n->next; + } + + static NLOHMANN_VIEW_ALWAYS_INLINE const node* value(const node* n) noexcept + { + return n; + } +}; + +template<> +struct navigation +{ + static NLOHMANN_VIEW_ALWAYS_INLINE bool editable() noexcept + { + return true; + } + + static NLOHMANN_VIEW_ALWAYS_INLINE const node* first(const document_data& d, const node* n) noexcept + { + return d.first_child_edited(n); + } + + static NLOHMANN_VIEW_ALWAYS_INLINE const node* end(const document_data& d, const node* n) noexcept + { + return d.child_end_edited(n); + } + + static NLOHMANN_VIEW_ALWAYS_INLINE const node* value(const node* n) noexcept + { + return document_data::deref(n); + } +}; + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/edit.hpp b/include/nlohmann/detail/view/edit.hpp new file mode 100644 index 000000000..bad8d9da3 --- /dev/null +++ b/include/nlohmann/detail/view/edit.hpp @@ -0,0 +1,861 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // array +#include // isinf, isnan +#include // size_t +#include // int64_t, uint8_t, uint32_t, uint64_t +#include // memmove +#include // numeric_limits +#include // string, to_string +#include // decay, enable_if, integral_constant, is_arithmetic, is_convertible, is_floating_point, is_same, is_signed +#include // forward +#include // vector + +#include +#include +#include +#include +#include +#include +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN + +template +class basic_json_view; + +namespace detail +{ +namespace view +{ + +/// the index of the first true condition (the number of conditions if none is) +template +struct first_true : std::integral_constant {}; + +template +struct first_true : std::integral_constant < int, 1 + first_true::value > {}; + +/// Checks a string the way basic_json's serializer does when it writes it +/// (type_error.316 with the same message), so that an editable document +/// only holds valid UTF-8: the error is at the first byte that no +/// well-formed sequence can continue with (Unicode, Table 3-7). +inline void check_utf8(const char* s, std::size_t n) +{ + const auto* const p = reinterpret_cast(s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + for (std::size_t i = 0; i < n;) + { + const unsigned char c = p[i]; + if (c < 0x80) + { + ++i; + continue; + } + const utf8_sequence_shape shape = utf8_shape(c); + const std::size_t len = shape.length; + if (len == 0) + { + throw_type_error(exception_id::invalid_utf8, concat("invalid UTF-8 byte at index ", std::to_string(i), ": 0x", hex_byte(c))); + } + for (std::size_t k = 1; k < len; ++k) + { + if (i + k == n) + { + throw_type_error(exception_id::invalid_utf8, concat("incomplete UTF-8 string; last byte: 0x", hex_byte(p[n - 1]))); + } + const unsigned char b = p[i + k]; + if (b < (k == 1 ? shape.second_lo : 0x80) || b > (k == 1 ? shape.second_hi : 0xBF)) + { + throw_type_error(exception_id::invalid_utf8, concat("invalid UTF-8 byte at index ", std::to_string(i + k), ": 0x", hex_byte(b))); + } + } + i += len; + } +} + +/*! +@brief the edits of an editable basic_json_document + +Values are accepted as views (of any document), BasicJsonType values, and +everything BasicJsonType can be constructed from. The source text is never +written: new values go to storage owned by the document (see +edit_storage.hpp). +*/ +template +class editor +{ + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + using string_view_t = typename View::string_view_t; + using nav = navigation; + + public: + explicit editor(document_data& d) noexcept + : m_doc(d) + {} + + /// replace a value; returns its view + template + View set(const View& target, V&& value) + { + node* const slot = own(target); + const encoded e = encode(std::forward(value)); + assign(slot, e, nullptr, false); + return View(&m_doc, slot); + } + + /// set a member (appended if missing; a null value becomes an object); + /// returns a view of the member value + template + View set(const View& object, string_view_t key, V&& value) + { + node* const o = own(object); + if (o->kind != static_cast(value_t::object) && o->kind != static_cast(value_t::null)) + { + throw_type_error(exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", object.type_name()); + } + check_utf8(key.data(), key.size()); + const encoded e = encode(std::forward(value)); + if (o->kind == static_cast(value_t::null)) + { + become_empty(o, value_t::object); + } + // an existing member: assign it (and drop later duplicates, so that + // lookups, iteration, and materialize() agree) + node* slot = nullptr; + bool duplicates = false; + for (const node* k = nav::first(m_doc, o), *end = nav::end(m_doc, o); k != end; k = document_data::after(k + 1)) + { + if (key_equals(*k, key)) + { + if (slot != nullptr) + { + duplicates = true; + break; + } + slot = const_cast(nav::value(k + 1)); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to this document + } + } + if (slot != nullptr) + { + if (duplicates) + { + erase_members(o, key, true); + } + assign(slot, e, o, true); + return View(&m_doc, slot); + } + const node k = string_node(key.data(), key.size()); + slot = new_slot(e); + node* const h = block_of(m_doc, o, 2); + h[h->next] = k; + make_link(h[h->next + 1], slot); + h->next += 2; + ++h->len; + ++o->len; + return View(&m_doc, slot); + } + + /// assign an existing array element; returns a view of it + template + View set(const View& array, std::size_t idx, V&& value) + { + node* const a = own(array); + if (a->kind != static_cast(value_t::array)) + { + throw_type_error(exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", array.type_name()); + } + check_index(idx, a->len); + const encoded e = encode(std::forward(value)); + node* const slot = const_cast(nav::value(element_at(m_doc, a, idx))); // NOLINT(cppcoreguidelines-pro-type-const-cast) + assign(slot, e, a, true); + return View(&m_doc, slot); + } + + /// append to an array (a null value becomes an array); returns a view of + /// the new element + template + View push_back(const View& array, V&& value) + { + node* const a = own(array); + if (a->kind != static_cast(value_t::array) && a->kind != static_cast(value_t::null)) + { + throw_type_error(exception_id::push_back_wrong_type, "cannot use push_back() with ", array.type_name()); + } + const encoded e = encode(std::forward(value)); + if (a->kind == static_cast(value_t::null)) + { + become_empty(a, value_t::array); + } + node* const slot = new_slot(e); + node* const h = block_of(m_doc, a, 1); + make_link(h[h->next], slot); + ++h->next; + ++h->len; + ++a->len; + return View(&m_doc, slot); + } + + /// insert into an array before position idx (idx <= size()); returns a + /// view of the new element + template + View insert(const View& array, std::size_t idx, V&& value) + { + node* const a = own(array); + if (a->kind != static_cast(value_t::array)) + { + throw_type_error(exception_id::insert_wrong_type, "cannot use insert() with ", array.type_name()); + } + check_index(idx, a->len + 1); + const encoded e = encode(std::forward(value)); + node* const slot = new_slot(e); + node* const h = block_of(m_doc, a, 1); + std::memmove(h + 2 + idx, h + 1 + idx, (h->next - 1 - idx) * sizeof(node)); + make_link(h[1 + idx], slot); + ++h->next; + ++h->len; + ++a->len; + return View(&m_doc, slot); + } + + /// remove all members with this key; returns their number + std::size_t erase(const View& object, string_view_t key) + { + node* const o = own(object); + if (o->kind != static_cast(value_t::object)) + { + throw_type_error(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)) + { + if (key_equals(*k, key)) + { + return erase_members(o, key, false); + } + } + return 0; + } + + /// remove an array element + void erase(const View& array, std::size_t idx) + { + node* const a = own(array); + if (a->kind != static_cast(value_t::array)) + { + throw_type_error(exception_id::erase_wrong_type, "cannot use erase() with ", array.type_name()); + } + check_index(idx, a->len); + node* const h = block_of(m_doc, a, 0); + std::memmove(h + 1 + idx, h + 2 + idx, (h->next - 2 - idx) * sizeof(node)); + --h->next; + --h->len; + --a->len; + } + + private: + /// an encoded value: a scalar node, or the root of a new array/object + struct encoded + { + node scalar{}; + node* region = nullptr; + }; + + /// the node of a view of this document + node* own(const View& v) + { + if (NLOHMANN_VIEW_UNLIKELY(v.m_doc != &m_doc || v.m_node == nullptr)) + { + throw_invalid_iterator(exception_id::iterator_from_other_value, "view does not belong to this document"); + } + edit_state_of(m_doc); + return const_cast(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) + { + if (idx >= limit) + { + throw_array_index_out_of_range(idx); + } + } + + bool key_equals(const node& k, string_view_t key) const noexcept + { + return m_doc.key_equals(k, key.data(), key.size()); + } + + /// remove the members with this key (all, or all but the first) from an object + std::size_t erase_members(node* o, string_view_t key, bool keep_first) + { + node* const h = block_of(m_doc, o, 0); + node* w = h + 1; + std::size_t erased = 0; + bool kept = false; + for (const node* r = h + 1, *end = h + h->next; r != end; r += 2) + { + const bool match = key_equals(*r, key); + if (match && (kept || !keep_first)) + { + ++erased; + continue; + } + kept = kept || match; + if (w != r) + { + w[0] = r[0]; + w[1] = r[1]; + } + w += 2; + } + h->next = static_cast(w - h); + h->len -= static_cast(erased); + o->len -= static_cast(erased); + return erased; + } + + /// turn a null into an empty array/object in place + static void become_empty(node* n, value_t k) noexcept + { + const std::uint8_t linked = n->flags & node_flags::linked; + *n = node{}; + n->kind = static_cast(k); + n->flags = static_cast(node_flags::is_new | linked); + n->next = 1; + } + + /// Replace the value at slot; `parent` is the container whose elements + /// include slot (if known). + void assign(node* slot, const encoded& e, node* parent, bool parent_known) + { + // an entry of a moved sequence links to the slot: it can take any extent + const std::uint8_t linked = slot->flags & node_flags::linked; + if (e.region == nullptr) + { + if (is_container(*slot) && slot->next > 1 && slot != m_doc.nodes && linked == 0) + { + // The slot spans its old elements in the enclosing sequence, but + // a scalar is one node: the enclosing container first switches to + // links (then the extent of the slot no longer matters). Looking + // for the container is linear in the size of the document, so + // links (which are marked in the slot) avoid it. + node* const p = parent_known ? parent : find_parent(m_doc, slot); + if (p != nullptr && ((p->flags & node_flags::moved) == 0 || moved_capacity(m_doc, p) == 0)) + { + block_of(m_doc, p, 0); + } + } + *slot = e.scalar; + slot->flags = static_cast(slot->flags | linked); + return; + } + // an array/object: the slot keeps its extent (so that the enclosing + // sequence still steps over it), and the elements come from the new + // sequence + const node* const r = e.region; + const std::uint32_t extent = is_container(*slot) ? slot->next : 1; + const bool was_moved = (slot->flags & node_flags::moved) != 0; + // Everything that can throw happens before the slot changes: a slot + // that is a container without the moved flag would show its old + // elements. reserve_moved() makes the set_moved() below, which sets + // the flag, safe; the entry of `regions` exists already (encode() + // added it), so that the assignment at the end does not allocate. + if (!was_moved) + { + reserve_moved(m_doc); + } + slot->kind = r->kind; + slot->extra = 0; + slot->len = r->len; + slot->next = extent; + // (set_moved() adds the moved flag to a slot that does not have it yet) + slot->flags = static_cast((was_moved ? node_flags::moved | node_flags::is_new : 0) | linked); + set_moved(m_doc, slot, e.region, 0); + edit_state_of(m_doc).regions[e.region] = slot; + } + + /// a node for a new element (links point to it; it never moves) + node* new_slot(const encoded& e) + { + if (e.region != nullptr) + { + return e.region; + } + node* const s = alloc_nodes(m_doc, 1); + *s = e.scalar; + return s; + } + + ////////////// + // encoding // + ////////////// + + template + using encode_tag = std::integral_constant; + + template + struct is_view : std::false_type {}; + + template + struct is_view> : std::true_type {}; + + template + encoded encode(V&& v) + { + using D = typename std::decay::type; + return encode_impl(std::forward(v), encode_tag::value, + std::is_same::value, + std::is_same::value, + std::is_same::value, + std::is_arithmetic::value, + std::is_convertible::value>::value> {}); + } + + /// a view of any document (copied; nothing is shared with it) + template + encoded encode_impl(const basic_json_view& v, encode_tag<0> /*view*/) + { + if (NLOHMANN_VIEW_UNLIKELY(v.m_node == nullptr)) + { + throw_type_error(exception_id::type_mismatch, "type must be a value, but is ", "discarded"); + } + encoded r; + if (!is_container(*v.m_node)) + { + r.scalar = copy_scalar(*v.m_doc, *v.m_node); + return r; + } + r.region = alloc_nodes(m_doc, count_nodes(*v.m_doc, v.m_node)); + fill_nodes(*v.m_doc, v.m_node, r.region); + edit_state_of(m_doc).regions.emplace(r.region, nullptr); + return r; + } + + encoded encode_impl(const BasicJsonType& j, encode_tag<1> /*json*/) + { + encoded r; + if (!j.is_structured()) + { + r.scalar = json_scalar(j); + return r; + } + r.region = alloc_nodes(m_doc, count_nodes(j)); + fill_nodes(j, r.region); + edit_state_of(m_doc).regions.emplace(r.region, nullptr); + return r; + } + + encoded encode_impl(std::nullptr_t /*unused*/, encode_tag<2> /*null*/) + { + encoded r; + r.scalar = plain_node(value_t::null); + return r; + } + + encoded encode_impl(bool b, encode_tag<3> /*boolean*/) + { + encoded r; + r.scalar = plain_node(value_t::boolean); + r.scalar.flags = static_cast(r.scalar.flags | (b ? node_flags::is_true : 0)); + return r; + } + + template + encoded encode_impl(T x, encode_tag<4> /*number*/) + { + encoded r; + r.scalar = number_node(x, std::integral_constant::value, std::is_signed::value>::value> {}); + return r; + } + + template + encoded encode_impl(const T& s, encode_tag<5> /*string*/) + { + const string_view_t sv(s); + check_utf8(sv.data(), sv.size()); + encoded r; + r.scalar = string_node(sv.data(), sv.size()); + return r; + } + + template + encoded encode_impl(T&& x, encode_tag<6> /*other*/) + { + return encode_impl(BasicJsonType(std::forward(x)), encode_tag<1> {}); + } + + static node plain_node(value_t k) noexcept + { + node n{}; + n.kind = static_cast(k); + n.flags = node_flags::is_new; + return n; + } + + template + node number_node(T x, std::integral_constant /*floating-point*/) + { + return float_node(static_cast(x)); + } + + template + node number_node(T x, std::integral_constant /*signed*/) + { + return integer_node(static_cast(static_cast(x)), value_t::number_integer); + } + + template + node number_node(T x, std::integral_constant /*unsigned*/) + { + return integer_node(static_cast(x), value_t::number_unsigned); + } + + /// an integer with its canonical token in the edit arena + node integer_node(std::uint64_t bits, value_t k) + { + const bool negative = k == value_t::number_integer && static_cast(bits) < 0; + std::uint64_t magnitude = negative ? 0 - bits : bits; + std::array buf{}; + char* p = buf.data() + buf.size(); // NOLINT(misc-const-correctness): digits are written through p + do + { + *--p = static_cast('0' + (magnitude % 10)); + magnitude /= 10; + } + while (magnitude != 0); + if (negative) + { + *--p = '-'; + } + const auto len = static_cast(buf.data() + buf.size() - p); + node n = plain_node(k); + n.flags = static_cast(n.flags | node_flags::edited); + n.off = append_text(m_doc, p, len); + // number_length() adds one for the sign of number_integer nodes + n.extra = static_cast(k == value_t::number_integer ? len - 1 : len); + set_integer_bits(n, bits); + return n; + } + + /// a float with its shortest round-trip token (as basic_json::dump() + /// writes it), or nan, inf, -inf, in the edit arena + node float_node(number_float_t x) + { + string_t text; + if (std::isnan(x)) + { + text = "nan"; + } + else if (std::isinf(x)) + { + text = x > 0 ? "inf" : "-inf"; + } + else + { + text = BasicJsonType(x).dump(); + } + node n = plain_node(value_t::number_float); + n.flags = static_cast(n.flags | node_flags::edited); + n.extra = 0xFFFFu; // (the digit layout is not recorded) + n.off = append_text(m_doc, text.data(), text.size()); + n.len = static_cast(text.size()); + return n; + } + + /// a string (or key) in the edit arena + node string_node(const char* s, std::size_t len) + { + if (NLOHMANN_VIEW_UNLIKELY(len >= 0xFFFFFFFFu)) + { + throw_out_of_range(exception_id::document_too_large, "strings of 4 GiB or more are not supported by json_document"); // LCOV_EXCL_LINE + } + node n = plain_node(value_t::string); + n.flags = static_cast(n.flags | node_flags::edited); + n.off = append_text(m_doc, s, len); + n.len = static_cast(len); + return n; + } + + /// a scalar of a view (of any document) as a node of this document + node copy_scalar(const document_data& from, const node& n) + { + if (&from == &m_doc) + { + return n; // the same storage + } + switch (static_cast(n.kind)) + { + case value_t::string: + // (an editable document only holds valid UTF-8, whatever the check of the other document was) + check_utf8(from.str(n), n.len); + return string_node(from.str(n), n.len); + case value_t::number_integer: + case value_t::number_unsigned: + return integer_node(integer_bits(n), static_cast(n.kind)); + case value_t::number_float: + { + node r = plain_node(value_t::number_float); + r.flags = static_cast(r.flags | node_flags::edited); + r.off = append_text(m_doc, from.str(n), n.len); + r.len = n.len; + r.extra = n.extra; + return r; + } + case value_t::boolean: + { + node r = plain_node(value_t::boolean); + r.flags = static_cast(r.flags | (n.flags & node_flags::is_true)); + return r; + } + case value_t::null: + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::discarded: + default: + return plain_node(value_t::null); + } + } + + node json_scalar(const BasicJsonType& j) + { + switch (j.type()) + { + case value_t::null: + return plain_node(value_t::null); + case value_t::boolean: + { + node r = plain_node(value_t::boolean); + r.flags = static_cast(r.flags | (j.template get() ? node_flags::is_true : 0)); + return r; + } + case value_t::number_integer: + return integer_node(static_cast(static_cast(j.template get())), value_t::number_integer); + case value_t::number_unsigned: + return integer_node(static_cast(j.template get()), value_t::number_unsigned); + case value_t::number_float: + return float_node(j.template get()); + case value_t::string: + { + const auto& s = j.template get_ref(); + check_utf8(s.data(), s.size()); + return string_node(s.data(), s.size()); + } + case value_t::binary: + throw_type_error(exception_id::document_binary_value, "cannot store a binary value in a json_document", ""); + case value_t::discarded: + case value_t::object: + case value_t::array: + default: + throw_type_error(exception_id::type_mismatch, "type must be a value, but is ", "discarded"); + } + } + + // The subtrees are walked with an explicit stack (as materialize() does): + // the nesting depth is limited by memory only, not by the call stack. + + /// number of nodes of a subtree (containers, keys, scalars) + template + static std::size_t count_nodes(const document_data& d, const node* n) + { + using walk = navigation; + struct frame + { + const node* pos; ///< next element, or key of the next member + const node* end; + bool object; + }; + std::vector open; + std::size_t r = 0; + for (;;) + { + ++r; + if (is_container(*n)) + { + open.push_back(frame{walk::first(d, n), walk::end(d, n), n->kind == static_cast(value_t::object)}); + } + // the next value: close finished containers, then step over the key + for (;;) + { + if (open.empty()) + { + return r; + } + frame& f = open.back(); + if (f.pos == f.end) + { + open.pop_back(); + continue; + } + const node* v = f.pos; + if (f.object) + { + ++r; // the key + ++v; + } + f.pos = document_data::after(v); + n = walk::value(v); + break; + } + } + } + + /// copy a subtree (of any document) as a contiguous sequence of + /// count_nodes() nodes + template + void fill_nodes(const document_data& d, const node* n, node* out) + { + using walk = navigation; + struct frame + { + const node* pos; ///< next element, or key of the next member + const node* end; + bool object; + node* self; ///< the container in the copy + }; + std::vector open; + for (;;) + { + if (is_container(*n)) + { + node* const self = out++; + *self = plain_node(static_cast(n->kind)); + self->len = n->len; + open.push_back(frame{walk::first(d, n), walk::end(d, n), n->kind == static_cast(value_t::object), self}); + } + else + { + *out++ = copy_scalar(d, *n); + } + // the next value: close finished containers, then copy the key + for (;;) + { + if (open.empty()) + { + return; + } + frame& f = open.back(); + if (f.pos == f.end) + { + f.self->next = static_cast(out - f.self); + open.pop_back(); + continue; + } + const node* v = f.pos; + if (f.object) + { + *out++ = copy_scalar(d, *v); + ++v; + } + f.pos = document_data::after(v); + n = walk::value(v); + break; + } + } + } + + static std::size_t count_nodes(const BasicJsonType& j) + { + using iterator = typename BasicJsonType::const_iterator; + struct frame + { + iterator pos; + iterator end; + bool object; + }; + std::vector open; + const BasicJsonType* n = &j; + std::size_t r = 0; + for (;;) + { + ++r; + if (n->is_structured()) + { + open.push_back(frame{n->cbegin(), n->cend(), n->is_object()}); + } + for (;;) + { + if (open.empty()) + { + return r; + } + frame& f = open.back(); + if (f.pos == f.end) + { + open.pop_back(); + continue; + } + r += f.object ? 1 : 0; // the key + n = &*f.pos; + ++f.pos; + break; + } + } + } + + void fill_nodes(const BasicJsonType& j, node* out) + { + using iterator = typename BasicJsonType::const_iterator; + struct frame + { + iterator pos; + iterator end; + bool object; + node* self; ///< the container in the copy + }; + std::vector open; + const BasicJsonType* n = &j; + for (;;) + { + if (n->is_structured()) + { + node* const self = out++; + *self = plain_node(n->type()); + self->len = static_cast(n->size()); + open.push_back(frame{n->cbegin(), n->cend(), n->is_object(), self}); + } + else + { + *out++ = json_scalar(*n); + } + for (;;) + { + if (open.empty()) + { + return; + } + frame& f = open.back(); + if (f.pos == f.end) + { + f.self->next = static_cast(out - f.self); + open.pop_back(); + continue; + } + if (f.object) + { + const auto& key = f.pos.key(); + check_utf8(key.data(), key.size()); + *out++ = string_node(key.data(), key.size()); + } + n = &*f.pos; + ++f.pos; + break; + } + } + } + + document_data& m_doc; +}; + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/edit_storage.hpp b/include/nlohmann/detail/view/edit_storage.hpp new file mode 100644 index 000000000..9a45bfebd --- /dev/null +++ b/include/nlohmann/detail/view/edit_storage.hpp @@ -0,0 +1,281 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // max, min +#include // size_t +#include // uint8_t, uint32_t +#include // memcpy +#include // less +#include // unique_ptr +#include // move + +#include +#include +#include +#include +#include + +// The storage of edits. Edits never move or resize the parsed index: every +// value keeps its node, so views stay valid. New values and element sequences +// live in chunks that never move; strings and number tokens written by edits +// live in the edit arena. An array/object whose elements change gets +// node_flags::moved: its elements then live in a separate sequence (a header +// node, then the entries), whose entries link to the values (kind_link). + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +inline document_data::edit_state& edit_state_of(document_data& d) +{ + if (!d.edits) + { + d.edits.reset(new document_data::edit_state()); // NOLINT(cppcoreguidelines-owning-memory,modernize-make-unique): owned by the unique_ptr; std::make_unique is C++14 + } + return *d.edits; +} + +/// k consecutive nodes that never move (new values and blocks) +inline node* alloc_nodes(document_data& d, std::size_t k) +{ + document_data::edit_state& e = edit_state_of(d); + if (NLOHMANN_VIEW_UNLIKELY(static_cast(e.chunk_end - e.chunk_cur) < k)) + { + if (k >= e.chunk_next / 2) + { + // a large request (half of the next chunk or more) gets a chunk of + // its own, so that the rest of the current chunk stays available + // for smaller ones + std::unique_ptr own(new node[k]()); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + e.chunks.push_back(std::move(own)); + e.bytes += k * sizeof(node); + return e.chunks.back().get(); + } + // a smaller request leaves the rest of the current chunk unused + // (fewer than k nodes, and k is less than half of the new chunk) + const std::size_t count = e.chunk_next; + std::unique_ptr fresh(new node[count]()); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + e.chunks.push_back(std::move(fresh)); + e.chunk_cur = e.chunks.back().get(); + e.chunk_end = e.chunk_cur + count; + e.chunk_next = (std::min)(e.chunk_next * 2, std::size_t{65536}); + e.bytes += count * sizeof(node); + } + node* const r = e.chunk_cur; + e.chunk_cur += k; + return r; +} + +/// The capacity of the edit arena after it grows by n bytes (`used` of `cap` +/// are taken): doubled, or what is needed plus some room, but never more than +/// the 4 GiB - 1 bytes that the 32-bit offsets of nodes can address. An error +/// if n more bytes do not fit even then. +inline std::size_t text_capacity(std::size_t cap, std::size_t used, std::size_t n) +{ + constexpr std::size_t limit = 0xFFFFFFFFu; + if (NLOHMANN_VIEW_UNLIKELY(used > limit || n > limit - used)) + { + throw_out_of_range(exception_id::document_too_large, "edits of 4 GiB or more are not supported by json_document"); + } + const std::size_t needed = used + n; + const std::size_t wanted = needed + (std::min)(limit - needed, std::size_t{256}); + const std::size_t doubled = cap > limit / 2 ? limit : cap * 2; + return (std::max)(doubled, wanted); +} + +/// copy n bytes into the edit arena and return their offset; a new buffer +/// leaves the old one alive, so that string views into it remain valid; n is +/// 0 for an empty string (written by an edit, or the key of a new member) +inline std::uint32_t append_text(document_data& d, const char* s, std::size_t n) +{ + document_data::edit_state& e = edit_state_of(d); + if (NLOHMANN_VIEW_UNLIKELY(e.text_cap - e.text_used < n)) + { + const std::size_t cap = text_capacity(e.text_cap, e.text_used, n); + std::unique_ptr fresh(new char[cap]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + if (NLOHMANN_VIEW_LIKELY(e.text_used != 0)) // (0 only for the first buffer, and then texts is empty) + { + std::memcpy(fresh.get(), e.texts.back().get(), e.text_used); + } + e.texts.push_back(std::move(fresh)); + e.text_cap = cap; + e.bytes += cap; + d.base[2] = e.texts.back().get(); + } + const auto off = static_cast(e.text_used); + if (n != 0) + { + std::memcpy(e.texts.back().get() + e.text_used, s, n); + } + e.text_used += n; + return off; +} + +/// the capacity in nodes of the block of a moved container (0: a fixed +/// sequence, the elements of a new value) +inline std::size_t moved_capacity(const document_data& d, const node* n) noexcept +{ + return d.edits->moved_cap[n->off]; +} + +/// Make room for one more moved container. This is the part of set_moved() +/// that can throw: a caller that changes a node before it calls set_moved() +/// calls this first, so that a failure leaves the node as it was. +inline void reserve_moved(document_data& d) +{ + document_data::edit_state& e = edit_state_of(d); + if (e.moved.size() >= 0xFFFFFFFFu) + { + throw_out_of_range(exception_id::document_too_large, "more than 4294967295 edited arrays and objects are not supported by json_document"); // LCOV_EXCL_LINE + } + if (e.moved.size() == e.moved.capacity() || e.moved_cap.size() == e.moved_cap.capacity()) + { + // both grow before either changes, so that the push_backs cannot throw + // (both conditions are needed: reserve() may allocate more than asked, + // so the two capacities can differ) + e.moved.reserve((e.moved.size() * 2) + 16); + e.moved_cap.reserve((e.moved.size() * 2) + 16); + } +} + +/// let container n take its elements from `seq` (header node first); cannot +/// throw if n is moved already or reserve_moved() was called +inline void set_moved(document_data& d, node* n, node* seq, std::size_t cap) +{ + document_data::edit_state& e = edit_state_of(d); + if ((n->flags & node_flags::moved) != 0) + { + e.moved[n->off] = seq; + e.moved_cap[n->off] = cap; + return; + } + reserve_moved(d); + e.moved.push_back(seq); + e.moved_cap.push_back(cap); + n->off = static_cast(e.moved.size() - 1); + n->flags = static_cast(n->flags | node_flags::moved | node_flags::is_new); +} + +/// Make the elements of container n a growable block with room for `extra` +/// more nodes, and return its header. The entries link to the existing +/// values, which stay where they are. A block that grows is copied (its old +/// space is not reused). +inline node* block_of(document_data& d, node* n, std::size_t extra) +{ + if ((n->flags & node_flags::moved) != 0 && moved_capacity(d, n) != 0) + { + node* const h = d.edits->moved[n->off]; + if (h->next + extra <= moved_capacity(d, n)) + { + return h; + } + const std::size_t cap = (std::max)(2 * moved_capacity(d, n), h->next + extra); + node* const nh = alloc_nodes(d, cap); + std::memcpy(nh, h, h->next * sizeof(node)); + set_moved(d, n, nh, cap); + return nh; + } + reserve_moved(d); // (so that set_moved() below cannot throw: the links are marked before) + const bool object = n->kind == static_cast(value_t::object); + const std::size_t used = 1 + (static_cast(n->len) * (object ? 2 : 1)); + const std::size_t cap = used + extra; + node* const h = alloc_nodes(d, cap); + *h = node{}; + h->kind = n->kind; + h->len = n->len; + h->next = static_cast(used); + node* o = h + 1; + for (const node* c = d.first_child_edited(n), *e = d.child_end_edited(n); c != e;) + { + if (object) + { + *o++ = *c++; // the key + } + make_link(*o, const_cast(document_data::deref(c))); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to the document + ++o; + c = document_data::after(c); + } + set_moved(d, n, h, cap); + return h; +} + +/// The container whose elements include `target`; nullptr for the root, for +/// a value that is no longer part of the document, and for a value that is +/// only reached through a link. Values never move between allocations, so +/// the path to `target` stays inside the allocation that holds it (the parsed +/// index, or one new value), where the extent of each container (`next`) +/// still covers its original subtree. +inline node* find_parent(const document_data& d, const node* target) +{ + const std::less lt{}; + const node* lo = d.nodes; + const node* hi = d.nodes + d.node_count; + const node* c = d.nodes; + if (lt(target, lo) || !lt(target, hi)) + { + if (!d.edits) + { + return nullptr; // LCOV_EXCL_LINE (nodes outside the index exist only after edits) + } + auto it = d.edits->regions.upper_bound(target); + if (it == d.edits->regions.begin()) + { + return nullptr; // LCOV_EXCL_LINE (an array/object with elements is in the index or a new value) + } + --it; + lo = it->first; + hi = lo + lo->next; + if (!lt(target, hi)) + { + return nullptr; // LCOV_EXCL_LINE (a single-node value, reached through a link) + } + // the root of a new value is the element sequence of its owner, or a linked value + c = it->second != nullptr ? it->second : lo; + } + if (target == lo) + { + return nullptr; + } + for (;;) + { + if (!is_container(*c)) + { + return nullptr; // LCOV_EXCL_LINE (the value is inside c) + } + const bool object = c->kind == static_cast(value_t::object); + const node* down = nullptr; + for (const node* p = d.first_child_edited(c), *e = d.child_end_edited(c); p != e;) + { + const node* const at = object ? p + 1 : p; + const node* const v = document_data::deref(at); + if (v == target) + { + return const_cast(c); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to the document + } + if (is_container(*v) && !lt(v, lo) && lt(v, target) && lt(target, v + v->next)) + { + down = v; + break; + } + p = document_data::after(at); + } + if (down == nullptr) + { + return nullptr; + } + c = down; + } +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/errors.hpp b/include/nlohmann/detail/view/errors.hpp new file mode 100644 index 000000000..590bb2edb --- /dev/null +++ b/include/nlohmann/detail/view/errors.hpp @@ -0,0 +1,198 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // min +#include // size_t +#include // string + +#include +#include +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +// Exceptions are thrown out of line, so that the accessors that may throw stay +// small enough to be inlined. + +[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_type_error(exception_id id, const char* prefix, const char* type) +{ + NLOHMANN_VIEW_THROW(type_error::create(id, concat(prefix, type), nullptr)); +} + +[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_type_error(exception_id id, const std::string& msg) +{ + NLOHMANN_VIEW_THROW(type_error::create(id, msg, nullptr)); +} + +[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_out_of_range(exception_id id, const std::string& msg) +{ + NLOHMANN_VIEW_THROW(out_of_range::create(id, msg, nullptr)); +} + +[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_invalid_iterator(exception_id id, const char* msg) +{ + NLOHMANN_VIEW_THROW(invalid_iterator::create(id, msg, nullptr)); +} + +/// 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) +{ + NLOHMANN_VIEW_THROW(parse_error::create(id, 0, msg, nullptr)); +} + +/// out_of_range.401: the array index @a idx is not below the size +template +[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_out_of_range(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")); +} + +/// 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) +{ + throw_out_of_range(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 +template +[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_key_not_found(const Key& key) +{ + throw_out_of_range(exception_id::key_not_found, concat("key '", key, "' not found")); +} + +/// SAX handler of the error replay: accepts every event without storing it, +/// and throws the exception the parser reports +template +struct throwing_sax +{ + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + using binary_t = typename BasicJsonType::binary_t; + + bool null() + { + return true; + } + + bool boolean(bool /*unused*/) + { + return true; + } + + bool number_integer(number_integer_t /*unused*/) + { + return true; + } + + bool number_unsigned(number_unsigned_t /*unused*/) + { + return true; + } + + bool number_float(number_float_t /*unused*/, const string_t& /*unused*/) + { + return true; + } + + bool string(string_t& /*unused*/) + { + return true; + } + + bool binary(binary_t& /*unused*/) + { + return true; + } + + bool start_object(std::size_t /*unused*/) + { + return true; + } + + bool key(string_t& /*unused*/) + { + return true; + } + + bool end_object() + { + return true; + } + + bool start_array(std::size_t /*unused*/) + { + return true; + } + + bool end_array() + { + return true; + } + + /// (a template, as in json_sax_dom_parser: the parser passes the concrete + /// exception type, which is thrown as is) + template + [[noreturn]] bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, const Exception& ex) + { + NLOHMANN_VIEW_THROW(ex); + } +}; + +/*! +@brief throw the exception BasicJsonType::parse would throw for this input + +The view accepts exactly the inputs parse() accepts, so on a failure the +library parser is run on the same bytes: it throws the exception parse() would +throw, with the same message, position, and "last read" token. The replay +uses a SAX handler that stores nothing, so it only lexes and parses up to the +error, without building values. The error path is cold, so this costs nothing +on valid input. Should parse() accept the input nevertheless (a bug), the +view's own failure is reported. +*/ +template +[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_parse_failure(const parse_failure& f, const char* src, std::size_t size, + bool ignore_comments, bool ignore_trailing_commas) +{ + if (f.code == error_code::input_too_large) + { + // (the limit is detail::view::max_input_size(): 4 GiB minus 16 bytes) + NLOHMANN_VIEW_THROW(out_of_range::create(exception_id::document_too_large, "input of 4294967280 bytes or more is not supported by json_document", nullptr)); + } + throwing_sax sax; + const bool accepted = BasicJsonType::sax_parse(src, src + size, &sax, input_format_t::json, true, ignore_comments, ignore_trailing_commas); + // LCOV_EXCL_START (only if parse() accepts what the view rejects: a bug) + static_cast(accepted); + + position_t pos; + const std::size_t off = (std::min)(f.offset, size); + pos.chars_read_total = off + 1; + std::size_t line_start = 0; + for (std::size_t i = 0; i < off; ++i) + { + if (src[i] == '\n') + { + ++pos.lines_read; + line_start = i + 1; + } + } + pos.chars_read_current_line = off + 1 - line_start; + NLOHMANN_VIEW_THROW(parse_error::create(exception_id::syntax_error, pos, "syntax error while parsing value", nullptr)); + // LCOV_EXCL_STOP +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/image.hpp b/include/nlohmann/detail/view/image.hpp new file mode 100644 index 000000000..af9994712 --- /dev/null +++ b/include/nlohmann/detail/view/image.hpp @@ -0,0 +1,744 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // min, sort +#include // array +#include // offsetof, size_t +#include // int64_t, uint8_t, uint16_t, uint32_t, uint64_t +#include // memcmp, memcpy, memset +#include // numeric_limits +#include // string +#include // vector + +#include +#include +#include +#include +#include +#include +#include +#include + +// Images: a document stored so that loading it needs no parsing. +// +// Layout (little-endian): a 64-byte header, the nodes, the text (the source, +// followed by the number tokens written by edits), a NUL, the decoded strings +// (followed by the strings written by edits), a NUL. The idea is that of +// zero-copy formats such as FlatBuffers (https://github.com/google/flatbuffers) +// and YaFF (https://github.com/yandex/yaff); no code is taken from them. +// check_image follows the idea of FlatBuffers' Verifier (bounds and +// structure) and also checks what the parser guarantees about strings and +// numbers, so that reading and serializing a checked image is safe and yields +// valid JSON. + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// how load() checks an image +enum class image_check +{ + /// everything the parser guarantees: structure and bounds, strings (valid + /// UTF-8; source strings without quotes, backslashes, and control + /// characters), and numbers (well-formed, matching the stored values) + full, + /// structure and bounds only: reading and serializing are safe, but a + /// crafted image can yield invalid UTF-8, strings that serialize to + /// invalid JSON, or numbers that differ from their text + bounds, + /// none: for images from a trusted source only (a damaged image is + /// undefined behavior) + none, +}; + +struct image_header +{ + std::array magic; ///< "NJVI" + std::uint32_t version; ///< 1 + std::uint64_t node_count; + std::uint64_t text_size; + std::uint64_t arena_size; + std::array reserved; ///< zero (for later versions) +}; +static_assert(sizeof(image_header) == 64, "the image header must be 64 bytes"); + +constexpr std::uint32_t image_version = 1; + +/// the largest node count and text or string size of an image (as for parsed +/// documents, offsets and counts must fit 32 bits) +constexpr std::uint64_t image_limit = 0xFFFFFFF0u; + +/// Copy the current structure of an edited document into nodes in document +/// order, as the parser would have written them. Text written by edits is +/// appended to text_tail (number tokens) and arena_tail (strings); floats that +/// are not finite become null, as dump() writes them. +inline void compact_nodes(const document_data& d, std::size_t arena_size, std::vector& out, std::string& text_tail, std::string& arena_tail) +{ + struct frame + { + const node* cur; + const node* end; + std::size_t index; ///< the container's node in out + std::uint32_t count; + bool object; + }; + std::vector stack; + const auto string_node = [&](const node & s) + { + node r = s; + r.extra = 0; + r.flags = static_cast(s.flags & node_flags::storage); + if (r.flags == node_flags::edited) + { + r.off = static_cast(arena_size + arena_tail.size()); + arena_tail.append(d.str(s), s.len); + r.flags = node_flags::escaped; + } + return r; + }; + const auto copy_node = [&](const node * v) + { + node r = *v; + switch (static_cast(v->kind)) + { + case value_t::object: + case value_t::array: + r.flags = 0; + r.extra = 0; + r.off = (v->flags & (node_flags::moved | node_flags::is_new)) != 0 ? 0 : v->off; + r.len = 0; // counted below + r.next = 0; // set when the container is complete + stack.push_back(frame{d.first_child_edited(v), d.child_end_edited(v), out.size(), 0, v->kind == static_cast(value_t::object)}); + break; + case value_t::string: + r = string_node(*v); + break; + case value_t::number_integer: + case value_t::number_unsigned: + if ((v->flags & node_flags::storage) == node_flags::edited) + { + r.off = static_cast(d.size + text_tail.size()); + text_tail.append(d.str(*v), number_length(*v)); + } + r.flags = 0; + break; + case value_t::number_float: + if ((v->flags & node_flags::storage) == node_flags::edited) + { + const char* const t = d.str(*v); + if (t[0] == 'n' || t[0] == 'i' || (v->len > 1 && t[1] == 'i')) + { + r = node{}; // nan and infinity: null, as dump() writes them + r.kind = static_cast(value_t::null); + break; + } + r.off = static_cast(d.size + text_tail.size()); + text_tail.append(t, v->len); + r.extra = 0xFFFFu; // the digit layout is not recorded + } + r.flags = 0; + break; + case value_t::boolean: + r.flags = static_cast(v->flags & node_flags::is_true); + break; + case value_t::null: + case value_t::binary: + case value_t::discarded: + default: + r.flags = 0; + break; + } + out.push_back(r); + }; + copy_node(d.nodes); + while (!stack.empty()) + { + const frame top = stack.back(); + if (top.cur == top.end) + { + node& c = out[top.index]; + c.len = top.count; + c.next = static_cast(out.size() - top.index); + stack.pop_back(); + continue; + } + if (top.object) + { + out.push_back(string_node(*top.cur)); + } + const node* const value = top.cur + (top.object ? 1 : 0); + stack.back().cur = document_data::after(value); + ++stack.back().count; + copy_node(document_data::deref(value)); // may grow the stack + } +} + +/// the document as an image +inline std::vector save_image(const document_data& d) +{ +#if !NLOHMANN_VIEW_LITTLE_ENDIAN + throw_type_error(exception_id::document_image_unsupported, "json_document images need a little-endian target"); // LCOV_EXCL_LINE +#endif + const std::size_t arena_size = d.arena_size; + std::vector compacted; + std::string text_tail; + std::string arena_tail; + if (d.edits) + { + compact_nodes(d, arena_size, compacted, text_tail, arena_tail); + } + const node* const nodes = d.edits ? compacted.data() : d.nodes; + const std::size_t count = d.edits ? compacted.size() : d.node_count; + const std::size_t text_size = d.size + text_tail.size(); + const std::size_t total_arena = arena_size + arena_tail.size(); + if (NLOHMANN_VIEW_UNLIKELY(text_size >= image_limit || total_arena >= image_limit || count >= image_limit)) + { + // LCOV_EXCL_START (4 GiB) + throw_out_of_range(exception_id::document_too_large, "images of 4 GiB or more are not supported by json_document"); + // LCOV_EXCL_STOP + } + image_header h{}; + h.magic = {{'N', 'J', 'V', 'I'}}; + h.version = image_version; + h.node_count = count; + h.text_size = text_size; + h.arena_size = total_arena; + std::vector image(sizeof(h) + (count * sizeof(node)) + text_size + 1 + total_arena + 1); + std::uint8_t* o = image.data(); + std::memcpy(o, &h, sizeof(h)); + o += sizeof(h); + std::memcpy(o, nodes, count * sizeof(node)); + // the hash indexes are rebuilt by load(), whose check rejects any other + // extra of an object, and the same document saves to the same bytes + for (std::size_t i = 0; i < count; ++i) + { + if (nodes[i].kind == static_cast(value_t::object) && nodes[i].extra != 0) + { + std::memset(o + (i * sizeof(node)) + offsetof(node, extra), 0, sizeof(nodes[i].extra)); + } + } + o += count * sizeof(node); + const auto append = [&o](const char* s, std::size_t n) + { + if (n != 0) + { + std::memcpy(o, s, n); + o += n; + } + }; + append(d.src, d.size); + append(text_tail.data(), text_tail.size()); + *o++ = 0; + append(d.base[1], arena_size); + append(arena_tail.data(), arena_tail.size()); + *o = 0; + return image; +} + +/// the parts of a number token that the checks need +struct number_token +{ + const unsigned char* int_start; + std::size_t int_digits; + std::size_t frac_digits; + std::int64_t exponent; + bool negative; + bool is_float; ///< a fraction or an exponent +}; + +/// whether [s, s + len) is a JSON number (the grammar the parser accepts) +inline bool scan_number_token(const unsigned char* s, std::size_t len, number_token& t) +{ + const unsigned char* const e = s + len; + const unsigned char* p = s; + t.negative = p != e && *p == '-'; + p += t.negative ? 1 : 0; + if (p == e) + { + return false; + } + t.int_start = p; + if (*p == '0') + { + ++p; + } + else if (*p >= '1' && *p <= '9') + { + do + { + ++p; + } + while (p != e && is_digit(*p)); + } + else + { + return false; + } + t.int_digits = static_cast(p - t.int_start); + t.frac_digits = 0; + t.is_float = false; + if (p != e && *p == '.') + { + const unsigned char* const f0 = ++p; + while (p != e && is_digit(*p)) + { + ++p; + } + if (p == f0) + { + return false; + } + t.frac_digits = static_cast(p - f0); + t.is_float = true; + } + t.exponent = 0; + if (p != e && (*p | 0x20u) == 'e') + { + ++p; + const bool exp_negative = p != e && *p == '-'; + p += (p != e && (*p == '+' || *p == '-')) ? 1 : 0; + if (p == e || !is_digit(*p)) + { + return false; + } + do + { + t.exponent = t.exponent < 100000 ? (t.exponent * 10) + (*p - '0') : t.exponent; + ++p; + } + while (p != e && is_digit(*p)); + t.exponent = exp_negative ? -t.exponent : t.exponent; + t.is_float = true; + } + return p == e; +} + +/// the digit layout the parser records for a float token (compaction +/// writes "many" instead; the caller accepts both) +inline std::uint16_t float_layout(const number_token& t) +{ + return static_cast((std::min)(t.int_digits, std::size_t{255}) | ((std::min)(t.frac_digits, std::size_t{255}) << 8u)); +} + +/// whether a float token (well-formed, per scan_number_token) is finite as +/// double; parse() rejects floats that overflow, and as there, only a number +/// whose magnitude could reach 1e308 needs the conversion +inline bool float_token_finite(const unsigned char* s, std::size_t len, const number_token& t) +{ + if (static_cast(t.int_digits) + t.exponent > 300) + { + node n{}; + n.kind = static_cast(value_t::number_float); + n.len = static_cast(len); + const auto v = float_value(reinterpret_cast(s), n); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + return v <= (std::numeric_limits::max)() && v >= -(std::numeric_limits::max)(); + } + return true; +} + +/// whether an integer node matches its token the way the parser records it +/// (after the bounds check; the token has at most 256 characters) +inline bool check_integer(const node& n, const unsigned char* text) +{ + const std::size_t len = number_length(n); + const unsigned char* const s = text + n.off; + number_token t{}; + if (!scan_number_token(s, len, t)) + { + return false; + } + // the token's value is the stored one; number_integer nodes of edits can + // be non-negative (as basic_json keeps the type of a value) + const bool integer = n.kind == static_cast(value_t::number_integer); + if (t.is_float || t.int_digits > 20 || (t.negative && !integer)) + { + return false; + } + // (at most 19 digits cannot overflow; 20 digits are compared with 2^64 - 1) + if (t.int_digits == 20 && std::memcmp(t.int_start, "18446744073709551615", 20) > 0) + { + return false; + } + std::uint64_t m = 0; + for (const unsigned char* d = t.int_start; d != t.int_start + t.int_digits; ++d) + { + m = (m * 10) + static_cast(*d - '0'); + } + if (integer && m > (t.negative ? std::uint64_t{1} << 63u : (std::uint64_t{1} << 63u) - 1)) + { + return false; + } + return integer_bits(n) == (t.negative ? 0 - m : m); +} + +/// a byte range of the text or of the decoded strings +struct byte_range +{ + std::uint32_t off; + std::uint32_t len; +}; + +/// a float token and the digit layout its node records +struct float_range +{ + std::uint32_t off; + std::uint32_t len; + std::uint16_t extra; +}; + +/// Run check once for each distinct range of ranges (which are within bounds +/// and not empty). Ranges that are not identical must not overlap: save() +/// writes every string and token to a place of its own, and nodes that share +/// a value (copies within a document) share the whole range. This bounds the +/// work by the size of the text, however many nodes point to the same bytes. +template +bool check_distinct_ranges(std::vector& ranges, Check check) +{ + std::sort(ranges.begin(), ranges.end(), [](const byte_range & a, const byte_range & b) + { + return a.off != b.off ? a.off < b.off : a.len < b.len; + }); + std::size_t end = 0; + for (std::size_t i = 0; i < ranges.size(); ++i) + { + const byte_range r = ranges[i]; + if (i != 0 && r.off == ranges[i - 1].off && r.len == ranges[i - 1].len) + { + continue; + } + if (r.off < end || !check(r)) + { + return false; + } + end = static_cast(r.off) + r.len; + } + return true; +} + +/// Check the float nodes: each token once (nodes of the same token must +/// record layouts that match it), tokens must not overlap. +inline bool check_float_ranges(std::vector& ranges, const unsigned char* text) +{ + std::sort(ranges.begin(), ranges.end(), [](const float_range & a, const float_range & b) + { + if (a.off != b.off) + { + return a.off < b.off; + } + if (a.len != b.len) + { + return a.len < b.len; + } + return a.extra < b.extra; + }); + std::size_t end = 0; + std::size_t i = 0; + while (i < ranges.size()) + { + const float_range r = ranges[i]; + if (r.off < end) + { + return false; + } + number_token t{}; + const unsigned char* const s = text + r.off; + if (!scan_number_token(s, r.len, t) || !float_token_finite(s, r.len, t)) + { + return false; + } + // the digit layout the parser records (or "many", as compaction writes it) + const std::uint16_t layout = float_layout(t); + for (; i < ranges.size() && ranges[i].off == r.off && ranges[i].len == r.len; ++i) + { + if (ranges[i].extra != layout && ranges[i].extra != 0xFFFFu) + { + return false; + } + } + end = static_cast(r.off) + r.len; + } + return true; +} + +/// the content checks of check_image: source strings as the parser leaves +/// them (no quotes, backslashes, or control characters), decoded strings +/// (valid UTF-8), and float tokens +inline bool check_contents(const unsigned char* text, const unsigned char* arena, + std::vector& source_strings, std::vector& decoded_strings, + std::vector& floats) +{ + return check_distinct_ranges(source_strings, [text](const byte_range & r) + { + const unsigned char* const b = text + r.off; + return scan_string_run(b, b + r.len) == b + r.len; + }) + && check_distinct_ranges(decoded_strings, [arena](const byte_range & r) + { + return valid_utf8_prefix(arena + r.off, r.len) == r.len; + }) + && check_float_ranges(floats, text); +} + +/// Check the nodes of a loaded image against its text and decoded strings: +/// kinds, flags, and `extra`; extents and element counts of arrays and +/// objects; keys; bounds; string contents (source strings as the parser +/// leaves them: no quotes, backslashes, or control characters; all strings +/// valid UTF-8); and number tokens. +/// +/// The structure and the bounds are checked node by node. The contents of +/// strings and of float tokens are checked afterwards, once for each distinct +/// range (see check_distinct_ranges), so that the full check is linear in the +/// size of the image plus the sorting of the ranges, and not in the number of +/// nodes times the size of the text. +inline bool check_image(const node* nodes, std::size_t count, const unsigned char* text, std::size_t text_size, + const unsigned char* arena, std::size_t arena_size, bool full) +{ + struct frame + { + std::size_t end; + std::uint32_t len; + std::uint32_t seen; + bool object; + bool expect_key; + }; + std::vector stack; + std::vector source_strings; + std::vector decoded_strings; + std::vector floats; + const auto check_string = [&](const node & n) -> bool + { + if ((n.flags & ~node_flags::escaped) != 0 || n.extra != 0) + { + return false; + } + const bool decoded = (n.flags & node_flags::escaped) != 0; + const std::size_t limit = decoded ? arena_size : text_size; + if (n.off > limit || n.len > limit - n.off) + { + return false; + } + if (full && n.len != 0) + { + (decoded ? decoded_strings : source_strings).push_back(byte_range{n.off, n.len}); + } + return true; + }; + // bounds of a number token; the recorded digit layout must lie within it + const auto number_in_bounds = [&](const node & n) -> bool + { + const std::size_t len = number_length(n); + if (len == 0 || n.off > text_size || len > text_size - n.off) + { + return false; + } + if (n.kind != static_cast(value_t::number_float)) + { + return (n.extra >> 8u) == 0; + } + // float_value() reads the sign, the integer digits, and the point and + // fraction digits the layout records (a layout of more than 19 digits + // means the general conversion, which stays within the token) + const std::size_t int_digits = n.extra & 0xFFu; + const std::size_t frac_digits = n.extra >> 8u; + const std::size_t need = (text[n.off] == '-' ? 1u : 0u) + int_digits + (frac_digits != 0 ? frac_digits + 1 : 0); + return int_digits + frac_digits > 19 || need <= len; + }; + std::size_t i = 0; + for (;;) + { + // close finished arrays and objects + while (!stack.empty() && i == stack.back().end) + { + const frame f = stack.back(); + if (f.seen != f.len || (f.object && !f.expect_key)) + { + return false; + } + stack.pop_back(); + if (!stack.empty()) + { + ++stack.back().seen; + stack.back().expect_key = true; + } + } + if (i == count) + { + return stack.empty() && (!full || check_contents(text, arena, source_strings, decoded_strings, floats)); + } + if (i != 0 && stack.empty()) + { + return false; // nodes after the root + } + const node& n = nodes[i]; + if (!stack.empty() && stack.back().object && stack.back().expect_key) + { + if (n.kind != static_cast(value_t::string) || !check_string(n)) + { + return false; + } + stack.back().expect_key = false; + ++i; + continue; + } + bool complete = true; + switch (static_cast(n.kind)) + { + case value_t::null: + // (the offset of a literal is read to size the output of dump()) + if (n.flags != 0 || n.extra != 0 || n.off > text_size) + { + return false; + } + break; + case value_t::boolean: + if ((n.flags & ~node_flags::is_true) != 0 || n.extra != 0 || n.off > text_size) + { + return false; + } + break; + case value_t::string: + if (!check_string(n)) + { + return false; + } + break; + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + if (n.flags != 0 || !number_in_bounds(n)) + { + return false; + } + if (full) + { + // (the contents of float tokens are checked later; the token of an + // integer has at most 256 characters) + if (n.kind == static_cast(value_t::number_float)) + { + floats.push_back(float_range{n.off, n.len, n.extra}); + } + else if (!check_integer(n, text)) + { + return false; + } + } + break; + case value_t::array: + case value_t::object: + { + const std::size_t limit = stack.empty() ? count : stack.back().end; + if (n.flags != 0 || n.extra != 0 || n.next == 0 || n.next > limit - i || n.off > text_size) + { + return false; + } + stack.push_back(frame{i + n.next, n.len, 0, n.kind == static_cast(value_t::object), true}); + complete = false; + break; + } + case value_t::binary: + case value_t::discarded: + default: + return false; + } + ++i; + if (complete && !stack.empty()) + { + ++stack.back().seen; + stack.back().expect_key = true; + } + } +} + +[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_invalid_image(const char* what) +{ + throw_parse_error(exception_id::invalid_document_image, concat("invalid json_document image: ", what)); +} + +/// Read an image into d. The text and the decoded strings stay in the image; +/// the nodes are copied (so that they are aligned, and edits can change them). +inline void load_image(document_data& d, const std::uint8_t* image, std::size_t size, image_check check) +{ +#if !NLOHMANN_VIEW_LITTLE_ENDIAN + throw_type_error(exception_id::document_image_unsupported, "json_document images need a little-endian target"); // LCOV_EXCL_LINE +#endif + if (image == nullptr || size < sizeof(image_header)) + { + throw_invalid_image("too short"); + } + image_header h{}; + std::memcpy(&h, image, sizeof(h)); + // (the reserved fields are for later versions) + if (std::memcmp(h.magic.data(), "NJVI", 4) != 0 || h.version != image_version + || (h.reserved[0] | h.reserved[1] | h.reserved[2] | h.reserved[3]) != 0) + { + throw_invalid_image("unknown format"); + } + const std::size_t room = size - sizeof(h); + if (h.node_count == 0 || h.node_count > room / sizeof(node) || h.node_count >= image_limit || h.text_size >= image_limit || h.arena_size >= image_limit) + { + throw_invalid_image("sizes out of range"); + } + const auto count = static_cast(h.node_count); + const auto text_size = static_cast(h.text_size); + const auto arena_size = static_cast(h.arena_size); + const std::size_t text_at = sizeof(h) + (count * sizeof(node)); + // the text, a NUL, the decoded strings, a NUL, and nothing after them + if (size - text_at < 2 || text_size > size - text_at - 2 || arena_size != size - text_at - text_size - 2 + || image[text_at + text_size] != 0 || image[size - 1] != 0) + { + throw_invalid_image("sizes out of range"); + } + + d.discarded = true; + d.edits.reset(); + d.base[2] = nullptr; + d.owned.clear(); + if (d.owned_image.empty() || image != d.owned_image.data()) + { + d.owned_image.clear(); + } + d.arena.clear(); + d.indexes.clear(); + d.index_slots.clear(); + d.large_objects.clear(); + d.node_count = 0; + d.reserve(count); + std::memcpy(d.nodes, image + sizeof(h), count * sizeof(node)); + d.node_count = count; + const std::uint8_t* const text = image + text_at; + const std::uint8_t* const arena = text + text_size + 1; + d.src = reinterpret_cast(text); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + d.size = text_size; + d.base[0] = d.src; + d.base[1] = reinterpret_cast(arena); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + d.arena_size = arena_size; + if (check != image_check::none && !check_image(d.nodes, count, text, text_size, arena, arena_size, check == image_check::full)) + { + throw_invalid_image("the check failed"); + } + // the hash indexes of large objects, as after parsing + for (std::size_t i = 0; i < count; ++i) + { + const node& n = d.nodes[i]; + if (n.kind == static_cast(value_t::object) && n.len >= document_data::index_min_members) + { + d.large_objects.push_back(static_cast(i)); + } + } + build_object_indexes(d); + std::vector().swap(d.large_objects); // (only needed while building) + d.discarded = false; +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/input.hpp b/include/nlohmann/detail/view/input.hpp new file mode 100644 index 000000000..d028e81e2 --- /dev/null +++ b/include/nlohmann/detail/view/input.hpp @@ -0,0 +1,193 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // max +#include // size_t +#ifndef JSON_NO_IO + #include // FILE, fread +#endif +#include // istream, streambuf +#include // basic_string, char_traits, string +#include // decay, integral_constant, is_array, is_const, is_integral, is_lvalue_reference, is_pointer, is_same, remove_cv, remove_extent, remove_pointer, remove_reference +#include // forward + +#include +#include + +#if NLOHMANN_VIEW_HAS_CPP_17 + #include // string_view +#endif + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// how a document takes its input +enum class input_kind +{ + move_string, ///< non-const rvalue std::string: owned without a copy + c_string, ///< pointer to single-byte characters (NUL-terminated): borrowed + char_array, ///< array of single-byte characters (e.g. a string literal): borrowed + borrow_range, ///< lvalue contiguous byte container, or std::string_view: borrowed + copy_range, ///< rvalue contiguous byte container (a const rvalue std::string too): copied + wide_c_string, ///< pointer to wide characters (NUL-terminated): converted to UTF-8 into a buffer + adapter, ///< streams, FILE*, wide strings and arrays, and the rest of what the library's input adapter reads: read into a buffer +}; + +/// a single-byte character type (char, signed/unsigned char, char8_t, std::uint8_t, ...) +template +struct is_byte_char : conjunction, std::integral_constant> {}; // NOLINT(bugprone-sizeof-expression): T is any pointee or element type + +/// a character type the library's input adapter converts to UTF-8 +template +struct is_wide_char : disjunction, std::is_same, std::is_same> {}; + +template +struct classify_input +{ + using R = typename std::remove_reference::type; + using D = typename std::decay::type; + using element = typename std::remove_cv::type>::type; + using pointee = typename std::remove_cv::type>::type; + static constexpr bool is_rvalue = !std::is_lvalue_reference::value; + static constexpr bool is_bytes = is_contiguous_byte_container::value; +#if NLOHMANN_VIEW_HAS_CPP_17 + static constexpr bool is_string_view = std::is_same::value; +#else + static constexpr bool is_string_view = false; +#endif + // NOLINTBEGIN(readability-avoid-nested-conditional-operator): a constant expression of C++11 + // (arrays and pointers of other types, such as wide-character arrays and + // FILE*, go to the library's input adapter) + static constexpr input_kind value = + (std::is_array::value && is_byte_char::value) ? input_kind::char_array + : std::is_array::value ? input_kind::adapter + : (std::is_pointer::value && is_byte_char::value) ? input_kind::c_string + : (std::is_pointer::value && is_wide_char::value) ? input_kind::wide_c_string + : std::is_pointer::value ? input_kind::adapter + : (is_rvalue && !std::is_const::value && std::is_same::value) ? input_kind::move_string + : (is_bytes && (!is_rvalue || is_string_view)) ? input_kind::borrow_range + : is_bytes ? input_kind::copy_range + : input_kind::adapter; + // NOLINTEND(readability-avoid-nested-conditional-operator) +}; + +/// an integer type other than bool: a length passed where a flag is expected +template +struct is_integer_not_bool : std::is_integral {}; + +template<> +struct is_integer_not_bool : std::false_type {}; + +/// std::basic_string guarantees a NUL at data()[size()] (the parser's sentinel) +template +struct is_std_string : std::false_type {}; + +template +struct is_std_string> : std::true_type {}; + +/// append what a json input adapter yields (UTF-16/32 inputs arrive as UTF-8) +template +void append_adapter(std::string& buf, Adapter ia) +{ + for (;;) + { + const auto ch = ia.get_character(); + if (ch == std::char_traits::eof()) + { + break; + } + buf.push_back(static_cast(ch)); + } +} + +/// an input adapter made by the document for an input that the library's +/// input_adapter() does not take as one argument (an iterator range, a wide C +/// string), to be appended with append_adapter() +template +struct adapter_input +{ + Adapter ia; +}; + +template +struct is_adapter_input : std::false_type {}; + +template +struct is_adapter_input> : std::true_type {}; + +template +adapter_input::type> make_adapter_input(Adapter&& ia) +{ + return {std::forward(ia)}; +} + +/// append the rest of a stream: its characters need no conversion, so they are +/// read in blocks from the stream buffer, straight into buf (reusing its +/// capacity). As the library's stream adapter, this reads the stream buffer +/// directly and sets eofbit at the end. +inline void append_stream(std::string& buf, std::istream& is) +{ + std::streambuf* const sb = is.rdbuf(); + if (sb == nullptr) + { + return; + } + for (;;) + { + const std::size_t used = buf.size(); + const std::size_t room = (std::max)(buf.capacity() - used, std::size_t{4096}); + buf.resize(used + room); + const std::streamsize got = sb->sgetn(&buf[used], static_cast(room)); + buf.resize(used + static_cast((std::max)(got, std::streamsize{0}))); + if (got <= 0) + { + break; + } + } + is.clear(is.rdstate() | std::ios::eofbit); +} + +/// a pointer to a C stream (never with JSON_NO_IO, which leaves out ) +template +struct is_c_stream +#ifndef JSON_NO_IO + : std::is_same +#else + : std::false_type +#endif +{}; + +#ifndef JSON_NO_IO +/// append the rest of a C stream: in blocks with fread, not character by +/// character (the library's file adapter reads with fgetc); +/// f is not null +inline void append_file(std::string& buf, std::FILE* f) +{ + for (;;) + { + const std::size_t used = buf.size(); + const std::size_t room = (std::max)(buf.capacity() - used, std::size_t{4096}); + buf.resize(used + room); + const std::size_t got = std::fread(&buf[used], 1, room, f); + buf.resize(used + got); + if (got < room) + { + break; // end of file, or an error (as fgetc, which returns EOF for both) + } + } +} +#endif + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/iterator.hpp b/include/nlohmann/detail/view/iterator.hpp new file mode 100644 index 000000000..9d8246210 --- /dev/null +++ b/include/nlohmann/detail/view/iterator.hpp @@ -0,0 +1,265 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // ptrdiff_t, size_t +#include // forward_iterator_tag +#include // string, to_string +#include // enable_if + +#include +#include +#include +#include +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// the result of view_iterator::operator->: keeps the view alive for the +/// duration of the member access +template +class arrow_proxy +{ + public: + explicit arrow_proxy(const View& v) noexcept + : m_view(v) + {} + + const View* operator->() const noexcept + { + return &m_view; + } + + private: + View m_view; +}; + +/*! +@brief forward iterator over the elements of a basic_json_view + +Iterates over the elements of an array or the member values of an object, in +document order; key() gives the key of an object member. As for basic_json, a +primitive value iterates as a range of one element (itself), and null as an +empty range. +*/ +template +class view_iterator +{ + public: + using iterator_category = std::forward_iterator_tag; + using value_type = View; + using difference_type = std::ptrdiff_t; + using pointer = arrow_proxy; + using reference = View; + using string_view_t = typename View::string_view_t; + + view_iterator() noexcept = default; + + /// @param[in] pos the element, or the key of the member + /// @param[in] object whether pos is a key (its value is the next node) + view_iterator(const document_data* d, const node* pos, bool object) noexcept + : m_doc(d), m_pos(pos), m_value_offset(object ? 1 : 0) + {} + + NLOHMANN_VIEW_ALWAYS_INLINE View operator*() const noexcept + { + return View(m_doc, View::navigation::value(m_pos + m_value_offset)); + } + + pointer operator->() const noexcept + { + return pointer(**this); + } + + NLOHMANN_VIEW_ALWAYS_INLINE view_iterator& operator++() noexcept + { + m_pos = document_data::after(m_pos + m_value_offset); + return *this; + } + + view_iterator operator++(int) noexcept + { + const view_iterator r = *this; + ++*this; + return r; + } + + friend bool operator==(const view_iterator& a, const view_iterator& b) noexcept + { + return a.m_pos == b.m_pos; + } + + friend bool operator!=(const view_iterator& a, const view_iterator& b) noexcept + { + return a.m_pos != b.m_pos; + } + + /// the key of the current object member; throws invalid_iterator.207 for + /// other iterators, like basic_json's iterators + string_view_t key() const + { + if (NLOHMANN_VIEW_UNLIKELY(m_value_offset == 0)) + { + throw_invalid_iterator(exception_id::iterator_key_not_object, "cannot use key() for non-object iterators"); + } + return string_view_t(m_doc->str(*m_pos), m_pos->len); + } + + View value() const noexcept + { + return **this; + } + + /// whether the iterator runs over the members of an object + bool is_object_iterator() const noexcept + { + return m_value_offset != 0; + } + + private: + const document_data* m_doc = nullptr; + const node* m_pos = nullptr; + std::size_t m_value_offset = 0; ///< 1 for objects: the value follows its key +}; + +/*! +@brief a (key, value) item of basic_json_view::items() + +The key of an array element is its index, as for basic_json::items(). +Supports structured bindings: for (const auto [key, value] : view.items()) +*/ +template +class view_item +{ + public: + using string_view_t = typename View::string_view_t; + using iterator = view_iterator; + + view_item(const iterator& it, std::size_t index) + : m_it(it) + , m_position(index) + {} + + /// the member key, or the element index for arrays (as text made on the + /// first call, so that iterating over the values pays nothing for it) + string_view_t key() const + { + if (m_it.is_object_iterator()) + { + return m_it.key(); + } + if (m_index.empty()) + { + m_index = std::to_string(m_position); + } + return string_view_t(m_index.data(), m_index.size()); + } + + View value() const noexcept + { + return *m_it; + } + + template::type = 0> + string_view_t get() const + { + return key(); + } + + template::type = 0> + View get() const noexcept + { + return value(); + } + + private: + iterator m_it; + std::size_t m_position; ///< the index of an array element + mutable std::string m_index{}; ///< the text of m_position, once key() was called // NOLINT(readability-redundant-member-init) +}; + +/// the range returned by basic_json_view::items() +template +class view_items +{ + public: + using item = view_item; + + class iterator + { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = item; + using difference_type = std::ptrdiff_t; + using pointer = void; + using reference = item; + + explicit iterator(const view_iterator& it) noexcept + : m_it(it) + {} + + item operator*() const + { + return item(m_it, m_index); + } + + iterator& operator++() noexcept + { + ++m_it; + ++m_index; + return *this; + } + + iterator operator++(int) noexcept + { + const iterator r = *this; + ++*this; + return r; + } + + friend bool operator==(const iterator& a, const iterator& b) noexcept + { + return a.m_it == b.m_it; + } + + friend bool operator!=(const iterator& a, const iterator& b) noexcept + { + return a.m_it != b.m_it; + } + + private: + view_iterator m_it; + std::size_t m_index = 0; + }; + + explicit view_items(const View& v) noexcept + : m_view(v) + {} + + iterator begin() const noexcept + { + return iterator(m_view.begin()); + } + + iterator end() const noexcept + { + return iterator(m_view.end()); + } + + private: + View m_view; +}; + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/lookup.hpp b/include/nlohmann/detail/view/lookup.hpp new file mode 100644 index 000000000..e2038a956 --- /dev/null +++ b/include/nlohmann/detail/view/lookup.hpp @@ -0,0 +1,201 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // size_t +#include // uint16_t, uint32_t, uint64_t +#include // memcmp, memcpy +#include // numeric_limits +#include // integral_constant, is_integral, is_same + +#include +#include +#include +#include +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// equality test for strings of one length n <= 16: two overlapping loads per +/// string (the first and the last 8, 4, or 2 bytes) replace a memcmp, and no +/// byte outside [s, s + n) is read +class short_key +{ + public: + short_key(const unsigned char* k, std::size_t n) noexcept + : m_n(n) + { + load(k, m_a, m_b); + } + + NLOHMANN_VIEW_ALWAYS_INLINE bool matches(const unsigned char* s) const noexcept + { + std::uint64_t a = 0; + std::uint64_t b = 0; + load(s, a, b); + return a == m_a && b == m_b; + } + + private: + template + static NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t load_word(const unsigned char* s) noexcept + { + T w = 0; + std::memcpy(&w, s, sizeof(T)); + return w; + } + + NLOHMANN_VIEW_ALWAYS_INLINE void load(const unsigned char* s, std::uint64_t& a, std::uint64_t& b) const noexcept + { + if (m_n >= 8) + { + a = load_word(s); + b = load_word(s + m_n - 8); + } + else if (m_n >= 4) + { + a = load_word(s); + b = load_word(s + m_n - 4); + } + else if (m_n >= 2) + { + a = load_word(s); + b = load_word(s + m_n - 2); + } + else + { + a = m_n == 1 ? s[0] : 0; + b = 0; + } + } + + std::size_t m_n; + std::uint64_t m_a = 0; + std::uint64_t m_b = 0; +}; + +/// the key node of the first member of an object with the given key, or +/// nullptr (the search stops at the first match; materialize() and parse() +/// keep the last value of a duplicate key instead); most keys are rejected by +/// their length, from the index alone +template +const node* find_member(const document_data& d, const node* object, const char* key, std::size_t n) noexcept +{ + using nav = navigation; + if (NLOHMANN_VIEW_UNLIKELY(object->extra != 0) && (!nav::editable() || (object->flags & node_flags::moved) == 0)) + { + return find_indexed(d, object, key, n); // a large object (whose members have not been edited) + } + const node* const end = nav::end(d, object); + const auto* const k = reinterpret_cast(key); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + if (NLOHMANN_VIEW_LIKELY(n <= 16)) + { + const short_key probe(k, n); + for (const node* m = nav::first(d, object); m != end; m = document_data::after(m + 1)) + { + if (m->len == n && probe.matches(reinterpret_cast(d.str(*m)))) // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + { + return m; + } + } + return nullptr; + } + for (const node* m = nav::first(d, object); m != end; m = document_data::after(m + 1)) + { + if (d.key_equals(*m, key, n)) + { + return m; + } + } + return nullptr; +} + +/// the number of members of an object with this key (duplicates included) +template +std::size_t count_members(const document_data& d, const node* object, const char* key, std::size_t n) noexcept +{ + using nav = navigation; + std::size_t result = 0; + const node* const end = nav::end(d, object); + for (const node* m = nav::first(d, object); m != end; m = document_data::after(m + 1)) + { + if (d.key_equals(*m, key, n)) + { + ++result; + } + } + return result; +} + +/// whether an integer type is accepted as an array index by the view's +/// operator[] and at(): every integer type but bool and size_t, which has its +/// own overload +template +struct is_index_type : std::integral_constant < bool, + std::is_integral::value && !std::is_same::value && !std::is_same::value > +{}; + +/// an integer as an index: negative values, and values that do not fit a +/// size_t, map to the largest size_t (out of range for every array) +template +SizeType to_index(IntegerType idx) noexcept +{ + const IntegerType zero = 0; + const auto result = static_cast(idx); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): negative values are mapped below + return (idx < zero || static_cast(result) != idx) ? (std::numeric_limits::max)() : result; +} + +/// the entry of the element of an array at an index below its size (a link +/// in the moved sequences of editable documents) +template +const node* element_at(const document_data& d, const node* array, std::size_t idx) noexcept +{ + using nav = navigation; + const node* e = nav::first(d, array); + if (nav::editable() && (array->flags & node_flags::moved) != 0 && d.edits->moved_cap[array->off] != 0) + { + return e + idx; // a growable block: one link per element + } + if (array->next == array->len + 1u && (!nav::editable() || (array->flags & node_flags::moved) == 0)) + { + return e + idx; // no element has a subtree: the elements are the nodes after the array + } + for (std::size_t i = 0; i < idx; ++i) + { + e = document_data::after(e); + } + return e; +} + +/// the entry of the last element of a non-empty array, or the key of the +/// last member of a non-empty object +template +const node* last_child(const document_data& d, const node* container) noexcept +{ + const std::size_t value_offset = container->kind == static_cast(value_t::object) ? 1 : 0; + const node* const end = navigation::end(d, container); + const node* last = navigation::first(d, container); + if (value_offset == 0 && container->next == container->len + 1u && (!navigation::editable() || (container->flags & node_flags::moved) == 0)) + { + return last + (container->len - 1u); // an array without subtrees: the elements are the nodes after it + } + for (const node* c = document_data::after(last + value_offset); c != end; c = document_data::after(c + value_offset)) + { + last = c; + } + return last; +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/macro_scope.hpp b/include/nlohmann/detail/view/macro_scope.hpp new file mode 100644 index 000000000..bbe1a240b --- /dev/null +++ b/include/nlohmann/detail/view/macro_scope.hpp @@ -0,0 +1,80 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +// Macros of json_view.hpp and its detail headers. json.hpp undefines its own +// macros at its end (macro_unscope.hpp), so the view defines the few it needs +// under its own prefix; json_view.hpp undefines them all at its end +// (detail/view/macro_unscope.hpp). Configuration that json.hpp undefines is +// read from detail::abi_config instead. + +#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) + #define NLOHMANN_VIEW_HAS_CPP_17 1 +#else + #define NLOHMANN_VIEW_HAS_CPP_17 0 +#endif + +#if defined(__GNUC__) || defined(__clang__) + #define NLOHMANN_VIEW_LIKELY(x) __builtin_expect(!!(x), 1) + #define NLOHMANN_VIEW_UNLIKELY(x) __builtin_expect(!!(x), 0) + #define NLOHMANN_VIEW_ALWAYS_INLINE inline __attribute__((always_inline)) + #define NLOHMANN_VIEW_NOINLINE __attribute__((noinline)) +#elif defined(_MSC_VER) + #define NLOHMANN_VIEW_LIKELY(x) (x) + #define NLOHMANN_VIEW_UNLIKELY(x) (x) + // plain inline: __forceinline makes MSVC report C4714 (not inlined) for + // function templates it cannot inline, which is an error under /WX; the + // forced inlining is only a performance hint + #define NLOHMANN_VIEW_ALWAYS_INLINE inline + #define NLOHMANN_VIEW_NOINLINE __declspec(noinline) +#else + #define NLOHMANN_VIEW_LIKELY(x) (x) + #define NLOHMANN_VIEW_UNLIKELY(x) (x) + #define NLOHMANN_VIEW_ALWAYS_INLINE inline + #define NLOHMANN_VIEW_NOINLINE +#endif + +#if defined(__GNUC__) || defined(__clang__) + #define NLOHMANN_VIEW_NODISCARD __attribute__((warn_unused_result)) +#elif defined(_MSC_VER) + #define NLOHMANN_VIEW_NODISCARD _Check_return_ +#else + #define NLOHMANN_VIEW_NODISCARD +#endif + +// exceptions as in json.hpp (JSON_NOEXCEPTION, JSON_THROW_USER) +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) + #define NLOHMANN_VIEW_THROW(exception) throw exception +#else + #include + // (the exception is built first, so that the arguments of the throwing + // helpers count as used; the program ends anyway) + #define NLOHMANN_VIEW_THROW(exception) (static_cast(exception), std::abort()) +#endif +#if defined(JSON_THROW_USER) + #undef NLOHMANN_VIEW_THROW + #define NLOHMANN_VIEW_THROW JSON_THROW_USER +#endif + +// the parser stores a node's first word at once where the layout of `node` is +// known to be little-endian (MSVC targets are); elsewhere field by field +#if (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) || defined(_MSC_VER) + #define NLOHMANN_VIEW_LITTLE_ENDIAN 1 +#else + #define NLOHMANN_VIEW_LITTLE_ENDIAN 0 +#endif + +/// eight checks at fixed offsets 0..7 +#define NLOHMANN_VIEW_REPEAT_FIRST_8(X) X(0) X(1) X(2) X(3) X(4) X(5) X(6) X(7) + +/// eight checks at fixed offsets 8..15 +#define NLOHMANN_VIEW_REPEAT_SECOND_8(X) X(8) X(9) X(10) X(11) X(12) X(13) X(14) X(15) + +/// sixteen checks at fixed offsets 0..15 +#define NLOHMANN_VIEW_REPEAT16(X) NLOHMANN_VIEW_REPEAT_FIRST_8(X) NLOHMANN_VIEW_REPEAT_SECOND_8(X) diff --git a/include/nlohmann/detail/view/macro_unscope.hpp b/include/nlohmann/detail/view/macro_unscope.hpp new file mode 100644 index 000000000..a9aec867b --- /dev/null +++ b/include/nlohmann/detail/view/macro_unscope.hpp @@ -0,0 +1,30 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +// undefine the macros of detail/view/macro_scope.hpp (at the end of json_view.hpp) + +#undef NLOHMANN_VIEW_HAS_CPP_17 +#undef NLOHMANN_VIEW_LIKELY +#undef NLOHMANN_VIEW_UNLIKELY +#undef NLOHMANN_VIEW_ALWAYS_INLINE +#undef NLOHMANN_VIEW_NOINLINE +#undef NLOHMANN_VIEW_NODISCARD +#undef NLOHMANN_VIEW_THROW +#undef NLOHMANN_VIEW_LITTLE_ENDIAN +#undef NLOHMANN_VIEW_REPEAT16 +#undef NLOHMANN_VIEW_REPEAT_SECOND_8 +#undef NLOHMANN_VIEW_REPEAT_FIRST_8 +#undef NLOHMANN_VIEW_NEON +#undef NLOHMANN_VIEW_SSE2 +#undef NLOHMANN_VIEW_SSSE3 +#undef NLOHMANN_VIEW_SSSE3_DISPATCH +#undef NLOHMANN_VIEW_SSSE3_TARGET +#undef NLOHMANN_VIEW_VECTOR +#undef NLOHMANN_VIEW_VECTOR_UTF8 diff --git a/include/nlohmann/detail/view/materialize.hpp b/include/nlohmann/detail/view/materialize.hpp new file mode 100644 index 000000000..62adb30a9 --- /dev/null +++ b/include/nlohmann/detail/view/materialize.hpp @@ -0,0 +1,189 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // array +#include // size_t +#include // int64_t, uint8_t +#include // string +#include // vector + +#include +#include +#include +#include +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/*! +@brief the basic_json value of the subtree at n + +The subtree is replayed into the SAX handler that parse() uses to build its +values, so the result is the value parse() would produce: duplicate keys keep +the last value, and with JSON_DIAGNOSTICS the parent pointers are set. It is +iterative, so the nesting depth is limited by memory only, as for parse(). +Without a lexer the handler records no source positions +(JSON_DIAGNOSTIC_POSITIONS). +*/ +template +BasicJsonType materialize(const document_data& d, const node* n) +{ + using string_t = typename BasicJsonType::string_t; + using sax_t = json_sax_dom_parser, true>; + using nav = navigation; + + // the open containers: their next element (or key of the next member), + // their end, and whether they are objects; for the first 16 levels on the + // stack, else sized once from the document's deepest nesting (for a + // subtree an upper bound) + struct frame + { + const node* pos; + const node* end; + bool object; + }; + + // a scalar needs neither the handler nor the stack of open containers + switch (static_cast(n->kind)) + { + case value_t::string: + return BasicJsonType(string_t(d.str(*n), n->len)); + case value_t::number_integer: + return BasicJsonType(static_cast(static_cast(integer_bits(*n)))); + case value_t::number_unsigned: + return BasicJsonType(static_cast(integer_bits(*n))); + case value_t::number_float: + return BasicJsonType(float_value(d, *n)); + case value_t::boolean: + return BasicJsonType((n->flags & node_flags::is_true) != 0); + case value_t::null: + return BasicJsonType(nullptr); + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::discarded: + default: + break; + } + + BasicJsonType result; + sax_t sax(result, true); + const string_t no_token{}; + // (shallow subtrees on the stack: a call for a small value then allocates only for the value) + std::array shallow; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): filled as containers open + std::vector deep{}; + frame* open = shallow.data(); + std::size_t capacity = shallow.size(); + if (d.max_depth > capacity) + { + deep.resize(d.max_depth); + open = deep.data(); + capacity = deep.size(); + } + std::size_t depth = 0; + for (;;) + { + // false positive: n comes from nav::value(), which never returns null for a valid index + // @infer-ignore NULLPTR_DEREFERENCE + switch (static_cast(n->kind)) + { + case value_t::object: + case value_t::array: + { + const bool object = n->kind == static_cast(value_t::object); + if (object) + { + sax.start_object(n->len); + } + else + { + sax.start_array(n->len); + } + if (NLOHMANN_VIEW_UNLIKELY(depth == capacity)) + { + // (deeper than the recorded nesting, e.g. after edits) + deep.assign(open, open + depth); + deep.resize(depth * 2); + open = deep.data(); + capacity = deep.size(); + } + open[depth++] = frame{nav::first(d, n), nav::end(d, n), object}; + break; + } + case value_t::string: + { + string_t s(d.str(*n), n->len); + sax.string(s); + break; + } + case value_t::number_integer: + sax.number_integer(static_cast(static_cast(integer_bits(*n)))); + break; + case value_t::number_unsigned: + sax.number_unsigned(static_cast(integer_bits(*n))); + break; + case value_t::number_float: + sax.number_float(float_value(d, *n), no_token); + break; + case value_t::boolean: + sax.boolean((n->flags & node_flags::is_true) != 0); + break; + case value_t::null: + case value_t::binary: + case value_t::discarded: + default: + // false positive: the parser's frame stack points into its own storage (never null) + // @infer-ignore NULLPTR_DEREFERENCE + sax.null(); + break; + } + + // the next value: close finished containers, then read the key + for (;;) + { + if (depth == 0) + { + return result; + } + frame& f = open[depth - 1]; + if (f.pos == f.end) + { + if (f.object) + { + sax.end_object(); + } + else + { + sax.end_array(); + } + --depth; + continue; + } + const node* entry = f.pos; + if (f.object) + { + string_t key(d.str(*entry), entry->len); + sax.key(key); + ++entry; + } + n = nav::value(entry); + f.pos = document_data::after(entry); + break; + } + } +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/node.hpp b/include/nlohmann/detail/view/node.hpp new file mode 100644 index 000000000..7a05c8e0a --- /dev/null +++ b/include/nlohmann/detail/view/node.hpp @@ -0,0 +1,140 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // size_t +#include // uint8_t, uint16_t, uint32_t, uint64_t +#include // memcpy + +#include +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// The largest input a document accepts, in bytes. Offsets and node counts are +/// 32 bits wide; the limit keeps 16 bytes (the width of the scanner's steps) +/// below 2^32, so that a position one step past the end of the text fits. +constexpr std::size_t max_input_size() noexcept +{ + return 0xFFFFFFEFu; +} + +/// node flags +struct node_flags +{ + static constexpr std::uint8_t escaped = 1; ///< string payload lives in the decode arena, not the source + static constexpr std::uint8_t edited = 2; ///< string or number token lives in the edit arena (editable documents) + static constexpr std::uint8_t storage = escaped | edited; ///< mask: where a string or number token lives (index into document_data::base) + static constexpr std::uint8_t is_true = 4; ///< boolean value + static constexpr std::uint8_t moved = 8; ///< array/object: the elements live in a separate sequence (editable documents) + static constexpr std::uint8_t is_new = 16; ///< written by an edit: no source position + static constexpr std::uint8_t linked = 32; ///< an entry of a moved sequence links to this value (editable documents): its extent in the parsed layout no longer matters +}; + +/// kind of an entry of an edited sequence that stands for a value stored +/// elsewhere (the address of the value's node is kept in the len/next bytes) +constexpr std::uint8_t kind_link = 10; + +/// One entry of the flat index, in document order. An object's members are +/// stored as key node followed by the value's subtree. Integers keep their +/// converted 64-bit value in the len/next bytes (the node after a scalar is +/// always the next one, and the token length follows from `extra`). +struct node +{ + std::uint8_t kind; ///< value_t, or kind_link + std::uint8_t flags; ///< node_flags + std::uint16_t extra; ///< numbers: integer digits (low byte) and fraction digits (high byte), 255 = "many"; objects: number of the object's key hash index (1-based, 0 = none); otherwise 0 + std::uint32_t off; ///< source offset (string content, number token, literal, bracket); arena offset if escaped/edited; number of the element sequence if moved + std::uint32_t len; ///< string: decoded bytes; float: token bytes; array/object: element count + std::uint32_t next; ///< array/object: number of nodes of the subtree (its extent in the enclosing sequence) +}; +static_assert(sizeof(node) == 16, "node must stay 16 bytes"); + +// the node kinds are value_t values; the test below (object and array are 1 +// and 2) and those of the number kinds depend on this numbering +static_assert(static_cast(value_t::null) == 0 && static_cast(value_t::object) == 1 + && static_cast(value_t::array) == 2 && static_cast(value_t::string) == 3 + && static_cast(value_t::boolean) == 4 && static_cast(value_t::number_integer) == 5 + && static_cast(value_t::number_unsigned) == 6 && static_cast(value_t::number_float) == 7, + "the node format depends on the numbering of value_t"); + +NLOHMANN_VIEW_ALWAYS_INLINE bool is_container(const node& n) noexcept +{ + return static_cast(n.kind) - 1u <= 1u; +} + +/// the value a link node stands for +NLOHMANN_VIEW_ALWAYS_INLINE const node* link_target(const node& n) noexcept +{ + const node* t = nullptr; + std::memcpy(static_cast(&t), reinterpret_cast(&n) + 8, sizeof(const node*)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + return t; +} + +/// let the entry n stand for the value at target (and mark the value) +inline void make_link(node& n, node* target) noexcept +{ + target->flags = static_cast(target->flags | node_flags::linked); + n = node{}; + n.kind = kind_link; + std::memcpy(reinterpret_cast(&n) + 8, static_cast(&target), sizeof(const node*)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) +} + +/// the converted value of an integer node: len is its low half, next its high +/// half (on little-endian targets the two words are the value in memory) +NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t integer_bits(const node& n) noexcept +{ +#if NLOHMANN_VIEW_LITTLE_ENDIAN + std::uint64_t v = 0; + std::memcpy(&v, reinterpret_cast(&n) + 8, 8); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + return v; +#else + return static_cast(n.len) | (static_cast(n.next) << 32); +#endif +} + +NLOHMANN_VIEW_ALWAYS_INLINE void set_integer_bits(node& n, std::uint64_t v) noexcept +{ +#if NLOHMANN_VIEW_LITTLE_ENDIAN + std::memcpy(reinterpret_cast(&n) + 8, &v, 8); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) +#else + n.len = static_cast(v); + n.next = static_cast(v >> 32); +#endif +} + +/// token length of a number node +NLOHMANN_VIEW_ALWAYS_INLINE std::uint32_t number_length(const node& n) noexcept +{ + return n.kind == static_cast(value_t::number_float) ? n.len + : (n.extra & 0xFFu) + (n.kind == static_cast(value_t::number_integer) ? 1u : 0u); +} + +/// estimated number of nodes for an input of `size` bytes (one node per ~12 +/// bytes covers typical documents without regrowth) +inline std::size_t estimate_nodes(std::size_t size) noexcept +{ + return (size / 12) + 16; +} + +/// estimated number of nodes for the input [src, src + size): pretty-printed +/// input (whitespace after the first byte) needs about a node per 12 bytes, +/// minified input up to one per 4 (yyjson tells the two apart the same way) +inline std::size_t estimate_nodes(const char* src, std::size_t size) noexcept +{ + return size >= 2 && (src[1] == ' ' || src[1] == '\n' || src[1] == '\r' || src[1] == '\t') ? estimate_nodes(size) : (size / 4) + 16; +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/number.hpp b/include/nlohmann/detail/view/number.hpp new file mode 100644 index 000000000..10e2aa85d --- /dev/null +++ b/include/nlohmann/detail/view/number.hpp @@ -0,0 +1,222 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // size_t +#include // int64_t, uint64_t +#include // numeric_limits +#include // string +#include // integral_constant + +#include +#include +#include +#include +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/*! +@brief locate the decimal point and the end of the mantissa of a float token + +Also checks that the token is a JSON number. Tokens of the parser and of edits +always are; an image loaded with image_check::bounds can hold any bytes, which +must not reach the conversion (it expects a well-formed token). +*/ +inline bool float_token_layout(const char* first, const char* last, std::size_t& dot, std::size_t& mantissa_end) noexcept +{ + const auto digit = [last](const char* q) + { + return q != last && is_digit(static_cast(*q)); + }; + const char* p = first; + p += (p != last && *p == '-') ? 1 : 0; + if (!digit(p) || (*p == '0' && digit(p + 1))) + { + return false; + } + do + { + ++p; + } + while (digit(p)); + dot = std::string::npos; + if (p != last && *p == '.') + { + dot = static_cast(p - first); + if (!digit(++p)) + { + return false; + } + do + { + ++p; + } + while (digit(p)); + } + mantissa_end = static_cast(p - first); + if (p != last && (*p == 'e' || *p == 'E')) + { + ++p; + p += (p != last && (*p == '+' || *p == '-')) ? 1 : 0; + if (!digit(p)) + { + return false; + } + do + { + ++p; + } + while (digit(p)); + } + return p == last; +} + +/*! +@brief the value of the float token of a node, as parse() converts it + +Uses the lexer's conversion (detail::convert_float), so that the values are +bit-identical to parse(): float and double are converted without allocation +and independent of the locale. A token that is not a JSON number (only in a +damaged image loaded with image_check::bounds) yields 0. +*/ +template +NLOHMANN_VIEW_NOINLINE FloatType float_value(const char* first, const node& n) +{ + const char* const last = first + n.len; + std::size_t dot = 0; + std::size_t mantissa_end = 0; + if (NLOHMANN_VIEW_UNLIKELY(!float_token_layout(first, last, dot, mantissa_end))) + { + return FloatType{}; + } + return convert_float(first, last, dot, mantissa_end); +} + +/*! +@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 +fraction digits, and the exponent are, so the digits are read eight at a +time without scanning. + +@param[in] p first character of the token +@param[in] e end of the token +@param[in] limit end of the readable memory (the source text) +*/ +NLOHMANN_VIEW_ALWAYS_INLINE float_significand layout_decimal(const unsigned char* p, const unsigned char* e, unsigned int_digits, unsigned frac_digits, const unsigned char* limit) noexcept +{ + const bool negative = *p == '-'; + p += negative ? 1 : 0; + std::uint64_t w = parse_upto19(p, int_digits, limit); + p += int_digits; + std::int64_t q = 0; + if (frac_digits != 0) + { + w = (w * int_pow10(frac_digits)) + parse_upto19(p + 1, frac_digits, limit); + p += 1 + frac_digits; + q = -static_cast(frac_digits); + } + if (p != e) + { + // [eE][+-]digits; huge exponents saturate (the parser rejected + // overflow). The token is not read beyond e, and the digits are taken + // as unsigned, so that a token that is not well-formed (a damaged + // image loaded with image_check::bounds) yields a wrong value, but no + // overflow. + ++p; + const bool exp_negative = p != e && *p == '-'; + p += (p != e && (*p == '-' || *p == '+')) ? 1 : 0; + std::int64_t exp_value = 0; + for (; p != e; ++p) + { + if (exp_value < 0x10000000) + { + exp_value = (exp_value * 10) + static_cast(*p - '0'); + } + } + q += exp_negative ? -exp_value : exp_value; + } + + float_significand d; + d.w = w; + d.exponent = q; + d.negative = negative; + return d; +} + +/*! +@brief the value of a float token with at most 19 digits, from its layout + +The result is correctly rounded by the lexer's conversion +(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 +digits), so it is the value parse() produces. +*/ +template +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(layout_decimal(p, e, int_digits, frac_digits, limit)); +} + +/// the value of a float set by an edit: its token (the shortest round-trip +/// text, or "nan", "inf", "-inf") in the edit arena +template +NLOHMANN_VIEW_NOINLINE FloatType edited_float(const char* token, const node& n) +{ + if (token[0] == 'n') + { + return std::numeric_limits::quiet_NaN(); + } + if (token[0] == 'i' || (token[0] == '-' && token[1] == 'i')) + { + return token[0] == 'i' ? std::numeric_limits::infinity() : -std::numeric_limits::infinity(); + } + return float_value(token, n); +} + +/// 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 +template +FloatType float_value(const document_data& d, const node& n) +{ + if (NLOHMANN_VIEW_UNLIKELY((n.flags & node_flags::storage) == node_flags::edited)) + { + return edited_float(d.str(n), n); + } + return float_value(d, n, std::integral_constant::value> {}); +} + +template +FloatType float_value(const document_data& d, const node& n, std::true_type /*binary32 or binary64*/) +{ + const unsigned int_digits = n.extra & 0xFFu; + const unsigned frac_digits = n.extra >> 8u; + if (NLOHMANN_VIEW_LIKELY(int_digits + frac_digits <= 19)) // (255 marks "many") + { + // (a float token not written by an edit is in the text) + const auto* const first = reinterpret_cast(d.src + n.off); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + return layout_float(first, first + n.len, int_digits, frac_digits, reinterpret_cast(d.src + d.size)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + } + return float_value(d.str(n), n); +} + +template +FloatType float_value(const document_data& d, const node& n, std::false_type /*other*/) +{ + return float_value(d.str(n), n); +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/object_index.hpp b/include/nlohmann/detail/view/object_index.hpp new file mode 100644 index 000000000..9b0ba63ea --- /dev/null +++ b/include/nlohmann/detail/view/object_index.hpp @@ -0,0 +1,244 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // size_t +#include // uint32_t, uint64_t + +#include +#include +#include +#include + +// Hash indexes of the keys of large objects, so that a lookup does not compare thousands +// of keys (as Boost.JSON switches from a linear search to a hash table for +// large objects). An object with document_data::index_min_members members or +// more gets an open-addressing table after parsing; its node stores the +// number of the table (1-based) in `extra`. A slot holds the offset of a key +// node from its object node (0: empty). Of duplicate keys, the first is kept, +// as for the linear search. +// +// The hash is not seeded, so keys chosen to collide could make the build +// quadratic. A key therefore sits at most index_max_displacement slots away +// from its home slot; if a key would sit further away, the table is dropped +// and the object is searched linearly (like a small one). For the same +// reason, a lookup visits at most index_max_displacement + 1 slots. + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// the farthest a key may sit from its home slot (a table with at most half of +/// its slots in use gives random keys a distance of about 50 for millions of +/// members; and every member costs at most this many steps while building) +constexpr std::size_t index_max_displacement = 64; + +// a table has at least 2 * len slots (len >= index_min_members), so the probe +// window of index_max_displacement + 1 slots stays far smaller than the table +// and a probe never wraps around onto its own start +static_assert(document_data::index_min_members >= 2 * index_max_displacement, + "a table must be much larger than the probe window of a lookup"); + +/// hash of a key: its bytes, eight at a time (in a fixed byte order), where the +/// last word may overlap the one before (so that no loop over single bytes is +/// needed); a key of fewer than eight bytes is read as two overlapping words of +/// four or two bytes (in the native byte order: the hash is not stored, and the +/// same on both sides of a lookup) +inline std::uint64_t key_hash(const char* s, std::size_t n) noexcept +{ + std::uint64_t h = 0x9E3779B97F4A7C15u * (n + 1); + const auto* p = reinterpret_cast(s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + std::uint64_t w = 0; + if (n >= 8) + { + const unsigned char* const last = p + n - 8; + while (p < last) + { + h = (h ^ read_eight_bytes(p)) * 0xBF58476D1CE4E5B9u; + h ^= h >> 29u; + p += 8; + } + w = read_eight_bytes(last); + } + else if (n >= 4) + { + std::uint32_t head = 0; + std::uint32_t tail = 0; + std::memcpy(&head, p, 4); + std::memcpy(&tail, p + n - 4, 4); + w = static_cast(head) | (static_cast(tail) << 32u); + } + else if (n >= 2) + { + std::uint16_t head = 0; + std::uint16_t tail = 0; + std::memcpy(&head, p, 2); + std::memcpy(&tail, p + n - 2, 2); + w = static_cast(head) | (static_cast(tail) << 16u); + } + else if (n == 1) + { + w = p[0]; + } + // (a product only carries bits upwards, and the slot is taken from the low + // bits: the high bits of the last word are folded down before the multiplication) + h = (h ^ w ^ (w >> 29u)) * 0x94D049BB133111EBu; + return h ^ (h >> 31u); +} + +/// whether the n <= 16 bytes at a and b are equal, as two overlapping words +/// (the first and the last 8, 4, or 2 bytes) and without a call +NLOHMANN_VIEW_ALWAYS_INLINE bool same_short_bytes(const unsigned char* a, const unsigned char* b, std::size_t n) noexcept +{ + if (n >= 8) + { + std::uint64_t x0 = 0; + std::uint64_t x1 = 0; + std::uint64_t y0 = 0; + std::uint64_t y1 = 0; + std::memcpy(&x0, a, 8); + std::memcpy(&x1, a + n - 8, 8); + std::memcpy(&y0, b, 8); + std::memcpy(&y1, b + n - 8, 8); + return x0 == y0 && x1 == y1; + } + if (n >= 4) + { + std::uint32_t x0 = 0; + std::uint32_t x1 = 0; + std::uint32_t y0 = 0; + std::uint32_t y1 = 0; + std::memcpy(&x0, a, 4); + std::memcpy(&x1, a + n - 4, 4); + std::memcpy(&y0, b, 4); + std::memcpy(&y1, b + n - 4, 4); + return x0 == y0 && x1 == y1; + } + if (n >= 2) + { + std::uint16_t x0 = 0; + std::uint16_t x1 = 0; + std::uint16_t y0 = 0; + std::uint16_t y1 = 0; + std::memcpy(&x0, a, 2); + std::memcpy(&x1, a + n - 2, 2); + std::memcpy(&y0, b, 2); + std::memcpy(&y1, b + n - 2, 2); + return x0 == y0 && x1 == y1; + } + return n == 0 || *a == *b; +} + +/// build the table of a large object +inline void build_object_index(document_data& d, node* obj) +{ + if (d.indexes.size() >= 0xFFFFu) + { + return; // LCOV_EXCL_LINE (the number must fit `extra`; more large objects are searched linearly) + } + std::size_t cap = 16; + while (cap < 2 * static_cast(obj->len)) + { + cap *= 2; + } + const std::size_t start = d.index_slots.size(); + d.index_slots.resize(start + cap, 0); + std::uint32_t* const table = d.index_slots.data() + start; + const std::size_t mask = cap - 1; + for (const node* k = document_data::first_child(obj), *end = document_data::child_end(obj); k != end; k = document_data::after(k + 1)) + { + const char* const key = d.str(*k); + std::size_t slot = conditional_static_cast(key_hash(key, k->len)) & mask; + bool duplicate = false; + std::size_t distance = 0; + while (table[slot] != 0) + { + const node* const other = obj + table[slot]; + if (d.key_equals(*other, key, k->len)) + { + duplicate = true; // keep the first + break; + } + if (distance == index_max_displacement) + { + // too many keys share a home region + d.index_slots.resize(start); // no table: the object is searched linearly + return; + } + ++distance; + slot = (slot + 1) & mask; + } + if (!duplicate) + { + table[slot] = static_cast(k - obj); + } + } + d.indexes.push_back(document_data::object_index{start, static_cast(mask)}); + obj->extra = static_cast(d.indexes.size()); +} + +/// build the tables of the large objects the parser noted +inline void build_object_indexes(document_data& d) +{ + for (const std::uint32_t i : d.large_objects) + { + build_object_index(d, d.nodes + i); + } +} + +/// the key node of the first member with this key of an indexed object, or +/// nullptr +inline const node* find_indexed(const document_data& d, const node* obj, const char* key, std::size_t n) noexcept +{ + const document_data::object_index& ix = d.indexes[obj->extra - 1u]; + const std::uint32_t* const table = d.index_slots.data() + ix.start; + std::size_t slot = conditional_static_cast(key_hash(key, n)) & ix.mask; + const auto* const k_bytes = reinterpret_cast(key); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + // (the loop for longer keys comes first, as the code is laid out; it is not the more common case) + if (NLOHMANN_VIEW_UNLIKELY(n <= 16)) + { + // (up to 16 bytes, the bytes are compared as two words, without a call) + for (std::size_t distance = 0; distance <= index_max_displacement; ++distance) + { + const std::uint32_t s = table[slot]; + if (s == 0) + { + return nullptr; + } + const node* const k = obj + s; + if (k->len == n && same_short_bytes(reinterpret_cast(d.str(*k)), k_bytes, n)) // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + { + return k; + } + slot = (slot + 1) & ix.mask; + } + return nullptr; + } + for (std::size_t distance = 0; distance <= index_max_displacement; ++distance) + { + const std::uint32_t s = table[slot]; + if (s == 0) + { + return nullptr; + } + const node* const k = obj + s; + if (d.key_equals(*k, key, n)) + { + return k; + } + slot = (slot + 1) & ix.mask; + } + return nullptr; // (no key sits further from its home slot) +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/pointer.hpp b/include/nlohmann/detail/view/pointer.hpp new file mode 100644 index 000000000..be5aa14de --- /dev/null +++ b/include/nlohmann/detail/view/pointer.hpp @@ -0,0 +1,137 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // size_t +#include // uint64_t +#include // numeric_limits +#include // string, to_string + +#include +#include +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// what resolving a JSON pointer does where it cannot continue +enum class pointer_mode +{ + unchecked, ///< as const basic_json::operator[]: a discarded view where basic_json's behavior is undefined + checked, ///< as basic_json::at(): out_of_range.401/403 + value, ///< as basic_json::value(): no out_of_range exceptions (the default value is used) + contains, ///< as basic_json::contains(): no exceptions at all +}; + +/// the outcome of reading an array index: detail::parse_array_index(), shared with json_pointer +using index_status = detail::array_index_status; + +/// reads an array index like json_pointer::array_index (RFC 6901, Sect. 4), +/// but reports errors instead of throwing them +template +index_status array_index(const StringType& s, std::size_t& idx) noexcept +{ + return detail::parse_array_index(s, idx); +} + +/// throws the exception json_pointer::array_index throws for this status +template +[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_error(index_status status, const StringType& s) +{ + switch (status) + { + case index_status::leading_zero: + throw_parse_error(exception_id::pointer_index_leading_zero, concat("array index '", s, "' must not begin with '0'")); + case index_status::not_a_number: + throw_parse_error(exception_id::pointer_index_not_a_number, concat("array index '", s, "' is not a number")); + case index_status::exceeds_size_type: + throw_out_of_range(exception_id::value_out_of_range, concat("array index ", s, " exceeds size_type")); // LCOV_EXCL_LINE + case index_status::unresolved: + case index_status::ok: + default: + throw_out_of_range(exception_id::pointer_unresolved, concat("unresolved reference token '", s, "'")); + } +} + +/*! +@brief resolve the reference tokens of a JSON pointer, starting at a view + +The exceptions are those basic_json throws for the same pointer; where +basic_json's behavior is undefined (a missing key or an index out of range +with const operator[]), the result is a discarded view. +*/ +template +View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode) +{ + using string_view_t = typename View::string_view_t; + const bool throwing = mode == pointer_mode::unchecked || mode == pointer_mode::checked; + for (const auto& token : tokens) + { + if (cur.is_object()) + { + const auto it = cur.find(string_view_t(token.data(), token.size())); + if (it == cur.end()) + { + if (mode == pointer_mode::checked) + { + throw_key_not_found(token); + } + return View(); + } + cur = *it; + } + else if (cur.is_array()) + { + if (token.size() == 1 && token[0] == '-') + { + if (throwing) + { + throw_array_index_past_end(cur.size()); + } + return View(); + } + std::size_t idx = 0; + const index_status status = array_index(token, idx); + if (status != index_status::ok) + { + const bool parse_error = status == index_status::leading_zero || status == index_status::not_a_number; + if (throwing || (mode == pointer_mode::value && parse_error)) + { + throw_array_index_error(status, token); + } + return View(); + } + if (idx >= cur.size()) + { + if (mode == pointer_mode::checked) + { + throw_array_index_out_of_range(idx); + } + return View(); + } + cur = cur[idx]; + } + else + { + if (throwing) + { + throw_out_of_range(exception_id::pointer_unresolved, concat("unresolved reference token '", token, "'")); + } + return View(); + } + } + return cur; +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/scan.hpp b/include/nlohmann/detail/view/scan.hpp new file mode 100644 index 000000000..e230afbdb --- /dev/null +++ b/include/nlohmann/detail/view/scan.hpp @@ -0,0 +1,220 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-FileCopyrightText: 2020 YaoYuan +// SPDX-License-Identifier: MIT + +#pragma once + +#include // array +#include // size_t +#include // uint8_t, uint16_t, uint64_t +#include // memcpy + +#include +#include +#include + +// Scanning primitives of the view's parser. The unrolled checks at fixed +// offsets follow yyjson (https://github.com/ibireme/yyjson, MIT license): the +// loads do not depend on each other, so the CPU can run ahead. Words are read +// with read_eight_bytes(), so nothing here depends on the byte order. + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// 1 for bytes that may appear verbatim in a string: 0x20..0x7F except '"' and '\\' +inline const std::uint8_t* string_plain() noexcept +{ + static const std::array table = + { + { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x00..0x1F + 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x20..0x3F ('"') + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 0x40..0x5F ('\\') + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60..0x7F + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x80..0x9F + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xA0..0xBF + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xC0..0xDF + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xE0..0xFF + } + }; + return table.data(); +} + +NLOHMANN_VIEW_ALWAYS_INLINE bool is_digit(unsigned char c) noexcept +{ + return static_cast(c - '0') <= 9; +} + +/// two bytes as they are in memory (only compared with byte-symmetric patterns) +NLOHMANN_VIEW_ALWAYS_INLINE std::uint16_t load16(const unsigned char* p) noexcept +{ + std::uint16_t w = 0; + std::memcpy(&w, p, 2); + return w; +} + +/// Advance over plain string bytes and well-formed UTF-8. Stops at a quote, +/// a backslash, a control character, ill-formed UTF-8, or the end. The first +/// bytes are checked one by one, so that the position advances by constants +/// in predicted branches: 16 for keys, whose lengths repeat from record to +/// record, and 8 for string values (Value) where a vector loop follows, as +/// their lengths vary more. Longer runs continue 16 bytes at a time with NEON +/// or SSE2, else eight bytes at a time. With SSE2, the run is checked 16 bytes +/// at a time from its first byte instead: on x86-64, one compare that finds +/// the end of most keys and short values is faster than a branch per byte (on +/// AArch64, where a NEON mask costs more and branches predict well, slower). +template +NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* scan_string_run(const unsigned char* p, const unsigned char* e) noexcept +{ + const std::uint8_t* plain = string_plain(); + for (;;) + { +#if NLOHMANN_VIEW_SSE2 + p = vector_plain_run(p, e); +#else + if (e - p >= 16) + { +#define NLOHMANN_VIEW_STEP(i) if (NLOHMANN_VIEW_LIKELY(plain[p[i]] != 0)) {} else { p += (i); goto stop; } + NLOHMANN_VIEW_REPEAT_FIRST_8(NLOHMANN_VIEW_STEP) +#if NLOHMANN_VIEW_VECTOR + if (!Value) // (a plain `!Value || !NLOHMANN_VIEW_VECTOR` is a constant condition for MSVC, C4127) +#endif + { + NLOHMANN_VIEW_REPEAT_SECOND_8(NLOHMANN_VIEW_STEP) + p += 8; + } +#undef NLOHMANN_VIEW_STEP + p += 8; +#if NLOHMANN_VIEW_VECTOR + p = vector_plain_run(p, e); + if (p != e && plain[*p] == 0) + { + goto stop; + } +#else + while (e - p >= 8) + { + const std::uint64_t special = swar_string_special(read_eight_bytes(p)); + if (special != 0) + { + p += count_trailing_zeros(special) / 8; + goto stop; + } + p += 8; + } +#endif + continue; + } +#endif + while (p != e && plain[*p] != 0) + { + ++p; + } + if (p == e) + { + return p; + } +#if !NLOHMANN_VIEW_SSE2 +stop: +#endif + if (*p < 0x80) + { + return p; // quote, backslash, or control character + } +#if NLOHMANN_VIEW_VECTOR_UTF8 +#if NLOHMANN_VIEW_SSSE3_DISPATCH + if (NLOHMANN_VIEW_LIKELY(cpu_has_ssse3())) +#endif + { + // non-ASCII: the vector check, out of line + return scan_string_vector(p, e, plain); + } +#endif +#if !NLOHMANN_VIEW_VECTOR_UTF8 || NLOHMANN_VIEW_SSSE3_DISPATCH + // non-ASCII: a run of well-formed sequences (the library's check, so + // that exactly what json::parse accepts is accepted) + do + { + const std::size_t n = validate_one_utf8(p, static_cast(e - p)); + if (n == 0) + { + return p; + } + p += n; + } + while (p != e && *p >= 0x80); +#endif + } +} + +/// advance over ASCII digits +NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* skip_digits(const unsigned char* p, const unsigned char* e) noexcept +{ + while (e - p >= 16) + { +#define NLOHMANN_VIEW_STEP(i) if (NLOHMANN_VIEW_LIKELY(is_digit(p[i]))) {} else { return p + (i); } + NLOHMANN_VIEW_REPEAT16(NLOHMANN_VIEW_STEP) +#undef NLOHMANN_VIEW_STEP + p += 16; + } + while (p != e && is_digit(*p)) + { + ++p; + } + return p; +} + +/// powers of ten up to 10^19 as integers +inline std::uint64_t int_pow10(unsigned k) noexcept +{ + return ::nlohmann::detail::dtoa_impl::powers_of_ten()[k]; +} + +/// value of 0 < k < 8 digits at p in one step if [p, p + 8) lies below +/// limit, else one digit at a time (whole blocks of eight digits are read by +/// parse_upto19() directly) +NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t parse_upto8(const unsigned char* p, unsigned k, const unsigned char* limit) noexcept +{ + if (NLOHMANN_VIEW_LIKELY(limit - p >= 8)) + { + // move the k digits to the top and pad the vacated low bytes with '0' + const unsigned shift = 8 * (8 - k); + return parse_eight_digits((read_eight_bytes(p) << shift) | (0x3030303030303030u >> (8 * k))); + } + std::uint64_t v = 0; + for (unsigned i = 0; i < k; ++i) + { + v = (v * 10) + static_cast(p[i] - '0'); + } + return v; +} + +/// value of k <= 19 digits at p +NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t parse_upto19(const unsigned char* p, unsigned k, const unsigned char* limit) noexcept +{ + std::uint64_t w = 0; + while (k >= 8) + { + // (eight digits of the token: they lie below limit) + w = (w * 100000000u) + parse_eight_digits(read_eight_bytes(p)); + p += 8; + k -= 8; + } + if (k != 0) + { + w = (w * int_pow10(k)) + parse_upto8(p, k, limit); + } + return w; +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/serializer.hpp b/include/nlohmann/detail/view/serializer.hpp new file mode 100644 index 000000000..76df8f89b --- /dev/null +++ b/include/nlohmann/detail/view/serializer.hpp @@ -0,0 +1,1159 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // max +#include // array +#include // isfinite +#include // size_t +#include // uint8_t, uint32_t +#include // memcpy, memset +#include // numeric_limits +#include // integral_constant +#include // vector + +#include +#include +#include +#include +#include +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// append-only output buffer: writes through a raw pointer into a string that +/// is resized ahead, and trimmed by finish(). The estimate is reserved, and the +/// string grows in steps of 64 KiB within it: resize() fills the new bytes +/// with zeros (before C++23, a string cannot grow without), and a small step +/// is filled while the writer is about to use it, in the cache, instead of +/// filling the whole estimate in memory first. +template +class output_buffer +{ + public: + output_buffer(StringType& out, std::size_t estimate) + : m_out(sized(out, estimate)) + , m_pos(&m_out[0]) + , m_end(m_pos + m_out.size()) + {} + + void finish() + { + const auto size = static_cast(m_pos - m_out.data()); + m_out.resize(size); + } + + NLOHMANN_VIEW_ALWAYS_INLINE void reserve(std::size_t n) + { + if (NLOHMANN_VIEW_UNLIKELY(static_cast(m_end - m_pos) < n)) + { + grow(n); + } + } + + NLOHMANN_VIEW_ALWAYS_INLINE void put(char c) + { + reserve(1); + *m_pos++ = c; + } + + NLOHMANN_VIEW_ALWAYS_INLINE void put(const char* s, std::size_t n) + { + reserve(n); + std::memcpy(m_pos, s, n); + m_pos += n; + } + + /// a string literal, without its terminating NUL + template + NLOHMANN_VIEW_ALWAYS_INLINE void put(const char (&s)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + { + put(s, N - 1); + } + + /// the write position and the end of the writable space, for a writer + /// that keeps the position in a local variable (set_cursor() hands it back) + char* cursor() const noexcept + { + return m_pos; + } + + char* limit() const noexcept + { + return m_end; + } + + void set_cursor(char* p) noexcept + { + m_pos = p; + } + + private: + /// the size of a growth step + static constexpr std::size_t step = 0x10000; + + static StringType& sized(StringType& out, std::size_t estimate) + { + out.reserve(estimate); + out.resize(detail::clamp(estimate, std::size_t{64}, step)); + return out; + } + + NLOHMANN_VIEW_NOINLINE void grow(std::size_t n) + { + const auto used = static_cast(m_pos - m_out.data()); + // (a step does not go beyond the reserved estimate, so that a good + // estimate is never copied to a larger allocation; not a clamp: the + // lower bound used + n + 256 can exceed capacity(), and then it must win) + const std::size_t size = (std::max)((std::min)(m_out.size() + step, m_out.capacity()), used + n + 256); + if (size > m_out.capacity()) + { + m_out.reserve((std::max)(m_out.capacity() * 2, size)); + } + m_out.resize(size); + m_pos = &m_out[0] + used; + m_end = &m_out[0] + m_out.size(); + } + + StringType& m_out; + char* m_pos; + char* m_end; +}; + +/// The length of the run at s that dump() writes unchanged without +/// ensure_ascii: all bytes but quotes, backslashes, and control characters. +/// Unlike detail::string_bulk_run(), non-ASCII bytes are not validated: the +/// strings of a document are valid UTF-8 (a damaged image loaded with +/// image_check::bounds can have others, which are then written unchanged). +inline std::size_t plain_output_run(const unsigned char* s, std::size_t n) noexcept +{ + std::size_t i = 0; +#if NLOHMANN_VIEW_SSE2 + // (16 bytes per step; with NEON, the mask of a step costs more than it saves on the + // short runs that are typical here) + // NOLINTBEGIN(portability-simd-intrinsics) + for (; i + 16 <= n; i += 16) + { + const __m128i in = _mm_loadu_si128(static_cast(static_cast(s + i))); // NOLINT(bugprone-casting-through-void) + // (a byte is below 0x20 if it is its own minimum with 0x1F) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_cmpeq_epi8(_mm_min_epu8(in, _mm_set1_epi8(0x1F)), in)); + const auto mask = static_cast(static_cast(_mm_movemask_epi8(special))); + if (mask != 0) + { + return i + static_cast(count_trailing_zeros(mask)); + } + } + // NOLINTEND(portability-simd-intrinsics) +#endif + constexpr std::uint64_t ones = 0x0101010101010101ull; + constexpr std::uint64_t high = 0x8080808080808080ull; + for (; i + 8 <= n; i += 8) + { + const std::uint64_t v = read_eight_bytes(s + i); + const std::uint64_t q = v ^ 0x2222222222222222ull; // '"' + const std::uint64_t b = v ^ 0x5C5C5C5C5C5C5C5Cull; // '\\' + const std::uint64_t stop = (((q - ones) & ~q) | ((b - ones) & ~b) | ((v - 0x2020202020202020ull) & ~v)) & high; + if (stop != 0) + { + // the lowest flagged byte is the first stop: borrows only flag bytes above a true one + return i + (static_cast(count_trailing_zeros(stop)) / 8); + } + } + for (; i < n; ++i) + { + if (s[i] == '"' || s[i] == '\\' || s[i] < 0x20) + { + return i; + } + } + return n; +} + +/// Whether dump() with ensure_ascii writes the string at s unchanged, for a +/// string of the source without escape sequences (which has no quote, +/// backslash, or control character): no byte is DEL (0x7F) or above. Adding 1 +/// to each byte sets its high bit exactly for 0x7F, and a carry into the next +/// byte only comes from 0xFF, whose own high bit is set anyway. +inline bool is_ascii_without_delete(const char* s, 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(s + i); + if (((v | (v + ones)) & high) != 0) + { + return false; + } + } + for (; i < n; ++i) + { + if (static_cast(s[i]) >= 0x7F) + { + return false; + } + } + return true; +} + +#if NLOHMANN_VIEW_VECTOR +/// the bytes of the 16 at p that ensure_ascii cannot copy (a quote, a +/// backslash, a control character, DEL, or not ASCII; a signed compare with +/// 0x20 finds control characters and non-ASCII bytes at once), one bit per +/// byte (x86) or one nibble per byte (NEON) +NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t ascii_stop_mask(const unsigned char* p) noexcept +{ +#if NLOHMANN_VIEW_NEON + const uint8x16_t in = vld1q_u8(p); + const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))), + vorrq_u8(vceqq_u8(in, vdupq_n_u8(0x7F)), vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20)))); + return vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0); +#else + const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8(0x7F)), _mm_cmplt_epi8(in, _mm_set1_epi8(0x20)))); + return static_cast(static_cast(_mm_movemask_epi8(special))); +#endif +} + +/// the width in bits of a byte in the mask of ascii_stop_mask() +constexpr unsigned ascii_stop_bits() noexcept +{ + return NLOHMANN_VIEW_NEON ? 4u : 1u; +} +#endif + +/// As find_ascii_copyable_run(), 16 bytes per step. end is the end of the +/// buffer that holds the n bytes at p (a short run is read as 16 bytes if +/// they are in the buffer). +inline std::size_t ascii_copyable_run(const unsigned char* p, std::size_t n, const unsigned char* end) noexcept +{ +#if NLOHMANN_VIEW_VECTOR + if (n >= 16) + { + std::size_t i = 0; + for (; i + 16 <= n; i += 16) + { + const std::uint64_t mask = ascii_stop_mask(p + i); + if (mask != 0) + { + return i + (static_cast(count_trailing_zeros(mask)) / ascii_stop_bits()); + } + } + if (i == n) + { + return n; + } + // the last 16 bytes: those before i are known to be copyable + const std::uint64_t mask = ascii_stop_mask(p + n - 16); + return mask != 0 ? n - 16 + (static_cast(count_trailing_zeros(mask)) / ascii_stop_bits()) : n; + } + if (end - p >= 16) + { + // (the bytes after the run are masked out) + const std::uint64_t mask = ascii_stop_mask(p) & ((std::uint64_t{1} << (n * ascii_stop_bits())) - 1u); + return mask != 0 ? static_cast(count_trailing_zeros(mask)) / ascii_stop_bits() : n; + } +#else + static_cast(end); +#endif + return find_ascii_copyable_run(p, n); +} + +/// A stack that starts in a buffer of the caller (a local array) and moves to +/// the heap (a vector of the caller) only when that is full, so that dumps of +/// shallow documents need no allocation. The top is a pointer, as in +/// std::vector. grow() is static and out of line: it gets the vector and the +/// used range and returns the new storage, so the address of the stack never +/// reaches a call. Otherwise the compiler would have to keep the three +/// pointers in memory and load and store them on every push_back() and +/// pop_back(), not just when growing. +template +class small_stack +{ + public: + small_stack(T* buffer, std::size_t capacity, std::vector& heap) noexcept + : m_begin(buffer), m_top(buffer), m_end(buffer + capacity), m_heap(heap) + {} + small_stack(const small_stack&) = delete; + small_stack(small_stack&&) = delete; + small_stack& operator=(const small_stack&) = delete; + small_stack& operator=(small_stack&&) = delete; + ~small_stack() = default; + + NLOHMANN_VIEW_ALWAYS_INLINE void push_back(const T& x) + { + if (NLOHMANN_VIEW_UNLIKELY(m_top == m_end)) + { + const std::size_t used = size(); + const std::size_t capacity = 2 * static_cast(m_end - m_begin); + m_begin = grow(m_heap, m_begin, used, capacity); + m_top = m_begin + used; + m_end = m_begin + capacity; + } + *m_top++ = x; + } + + NLOHMANN_VIEW_ALWAYS_INLINE T& back() noexcept + { + return m_top[-1]; + } + + NLOHMANN_VIEW_ALWAYS_INLINE void pop_back() noexcept + { + --m_top; + } + + NLOHMANN_VIEW_ALWAYS_INLINE bool empty() const noexcept + { + return m_top == m_begin; + } + + NLOHMANN_VIEW_ALWAYS_INLINE std::size_t size() const noexcept + { + return static_cast(m_top - m_begin); + } + + private: + /// the used entries moved to heap storage of the given capacity + NLOHMANN_VIEW_NOINLINE static T* grow(std::vector& heap, const T* begin, std::size_t used, std::size_t capacity) + { + std::vector bigger(capacity); + std::copy(begin, begin + used, bigger.begin()); + heap.swap(bigger); + return heap.data(); + } + + T* m_begin; + T* m_top; + T* m_end; + std::vector& m_heap; +}; + +/// a string literal at w, without its terminating NUL (after room()) +/// @return a pointer past it +template +NLOHMANN_VIEW_ALWAYS_INLINE char* put_literal(char* w, const char (&s)[N]) noexcept // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) +{ + std::memcpy(w, s, N - 1); + return w + N - 1; +} + +/// the depth of the innermost open container, counted only for indented output +template +struct depth_counter +{ + std::size_t value = 0; + void enter() noexcept + { + ++value; + } + std::size_t leave() noexcept + { + return --value; + } + std::size_t get() const noexcept + { + return value; + } +}; + +template<> +struct depth_counter +{ + void enter() noexcept {} + std::size_t leave() noexcept // NOLINT(readability-convert-member-functions-to-static): same interface as depth_counter + { + return 0; + } + std::size_t get() const noexcept // NOLINT(readability-convert-member-functions-to-static): same interface as depth_counter + { + return 0; + } +}; + +/// how the view's dump() writes a value +struct dump_style +{ + bool pretty = false; ///< indent >= 0 + std::size_t indent = 0; ///< characters per level + char indent_char = ' '; + bool ensure_ascii = false; + bool source_numbers = false; ///< copy number tokens from the source +}; + +/*! +@brief write a view's subtree as basic_json::dump() writes the value + +The output of a subtree equals ordered_json::parse(text).dump() of it for +the same arguments (members in document order): strings are escaped by the +same rules, with the library's scanning kernels; floats are written with +the library's conversion; integers are copied from the source, where they +are canonical (except "-0", which parse() reads as 0). The walk is +iterative, so the nesting depth is limited by memory only. +*/ +template +class view_serializer +{ + using nav = navigation; + using string_t = typename BasicJsonType::string_t; + using number_float_t = typename BasicJsonType::number_float_t; + /// doubles that are IEEE 754 binary64 are written from their bits (Zmij) + using fast_double = std::integral_constant < bool, std::is_same::value && ::nlohmann::detail::dtoa_impl::is_binary64::value >; + + public: + view_serializer(const document_data& d, string_t& out, std::size_t estimate, const dump_style& style) + : m_doc(d), m_out(out, estimate), m_style(style) + {} + + void dump(const node* root) + { + if (m_style.ensure_ascii) + { + dump_ascii(root); + } + else if (m_style.pretty) + { + dump_pretty(root); + } + else if (m_style.source_numbers) + { + dump_text(root); + } + else + { + dump_text(root); + } + } + + private: + /// dump_text() with ensure_ascii, out of line: the walk without it is + /// faster when it is not merged with this one into dump() + NLOHMANN_VIEW_NOINLINE void dump_ascii(const node* root) + { + if (m_style.pretty) + { + if (m_style.source_numbers) + { + dump_text(root); + } + else + { + dump_text(root); + } + } + else if (m_style.source_numbers) + { + dump_text(root); + } + else + { + dump_text(root); + } + } + + /// dump_text() with indentation, out of line: the compact walk is faster + /// when it is not merged with the indented ones into dump() + NLOHMANN_VIEW_NOINLINE void dump_pretty(const node* root) + { + if (m_style.source_numbers) + { + dump_text(root); + } + else + { + dump_text(root); + } + } + + /// a string that is not a plain string of the source (decoded, or written + /// by an edit), without ensure_ascii: runs without characters to escape + /// are copied. If validate is set, the string must be valid UTF-8: the + /// runs are found by the lexer's scan, which validates the sequences, and + /// invalid UTF-8 throws what basic_json::dump() throws. (The compact output + /// writes the bytes of a damaged image unchanged, and scans without + /// validating them.) + NLOHMANN_VIEW_NOINLINE void write_decoded(const node& n, bool validate) + { + const auto* const s = reinterpret_cast(m_doc.str(n)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + m_out.put('"'); + for (std::size_t i = 0; i < n.len;) + { + const std::size_t run = validate ? string_bulk_run(s + i, n.len - i) : plain_output_run(s + i, n.len - i); + if (run != 0) + { + m_out.put(reinterpret_cast(s + i), run); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + i += run; + continue; + } + if (validate && s[i] >= 0x80) + { + invalid_utf8(s, n.len); // (the scan stopped at ill-formed UTF-8) + } + write_codepoint(s[i], s + i, 1); // a quote, a backslash, or a control character + ++i; + } + m_out.put('"'); + } + + /// whether dump_text() copies a string or key from the source: one of + /// the source, and with ensure_ascii one without bytes >= 0x7F (an overload + /// each, so that the walk without ensure_ascii is compiled exactly as + /// before: with GCC, a condition that only folds away later changed its + /// block layout and made it 4-6% slower) + static bool copied(const node& n, const char* /*src*/, std::false_type /*ensure_ascii*/) noexcept + { + return (n.flags & node_flags::storage) == 0; + } + + static bool copied(const node& n, const char* src, std::true_type /*ensure_ascii*/) noexcept + { + return (n.flags & node_flags::storage) == 0 && is_ascii_without_delete(src + n.off, n.len); + } + + /*! + @brief a string with ensure_ascii: the characters that are not printable ASCII as \\uXXXX + (a character beyond U+FFFF as a surrogate pair); strings of a document + are valid UTF-8, except in a damaged image loaded with image_check::bounds, + for which this throws what basic_json::dump() throws for the string + */ + NLOHMANN_VIEW_NOINLINE void write_ascii(const node& n) + { + const auto* const s = reinterpret_cast(m_doc.str(n)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + const std::size_t size = n.len; + char* w = m_out.cursor(); + char* lim = m_out.limit(); + // room for k bytes + const auto room = [&](std::size_t k) + { + const auto left = static_cast(lim - w); + if (NLOHMANN_VIEW_UNLIKELY(left < k)) + { + m_out.set_cursor(w); + m_out.reserve(k); + w = m_out.cursor(); + lim = m_out.limit(); + } + }; + static constexpr const char* hex = "0123456789abcdef"; + // \\uXXXX + const auto u_escape = [&](std::uint32_t u) + { + w[0] = '\\'; + w[1] = 'u'; + w[2] = hex[(u >> 12u) & 0xFu]; + w[3] = hex[(u >> 8u) & 0xFu]; + w[4] = hex[(u >> 4u) & 0xFu]; + w[5] = hex[u & 0xFu]; + w += 6; + }; + room(1); + *w++ = '"'; + std::size_t i = 0; + // (a string of the source starts with a run that has nothing to escape, 16 bytes per step) + const std::size_t prefix = (n.flags & node_flags::storage) == 0 ? ascii_copyable_run(s, size, reinterpret_cast(m_doc.src + m_doc.size)) : 0; // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + if (prefix != 0) + { + room(prefix); + std::memcpy(w, s, prefix); + w += prefix; + i = prefix; + } + while (i < size) + { + // (a character that needs an escape sequence is not worth a call) + const std::size_t run = is_ascii_copyable(s[i]) ? find_ascii_copyable_run(s + i, size - i) : 0; + if (run != 0) + { + room(run); + std::memcpy(w, s + i, run); + w += run; + i += run; + continue; + } + room(12); + if (s[i] >= 0x80) + { + const unsigned char lead = s[i]; + // (the two-byte sequences of Latin text and the three-byte sequences of most other + // text, with continuation bytes of any value: the ranges that are restricted (E0, + // ED, and the sequences of four bytes) go through validate_one_utf8()) + if (lead >= 0xE1 && lead <= 0xEF && lead != 0xED && size - i >= 3 && (s[i + 1] & 0xC0u) == 0x80 && (s[i + 2] & 0xC0u) == 0x80) + { + u_escape(((lead & 0x0Fu) << 12u) | ((s[i + 1] & 0x3Fu) << 6u) | (s[i + 2] & 0x3Fu)); + i += 3; + continue; + } + if (lead >= 0xC2 && lead <= 0xDF && size - i >= 2 && (s[i + 1] & 0xC0u) == 0x80) + { + u_escape(((lead & 0x1Fu) << 6u) | (s[i + 1] & 0x3Fu)); + i += 2; + continue; + } + const std::size_t len = validate_one_utf8(s + i, size - i); + if (NLOHMANN_VIEW_UNLIKELY(len == 0)) + { + m_out.set_cursor(w); + invalid_utf8(s, size); + return; // LCOV_EXCL_LINE (invalid_utf8 throws) + } + std::uint32_t codepoint = s[i] & (0xFFu >> (len + 1)); + for (std::size_t k = 1; k < len; ++k) + { + codepoint = (codepoint << 6u) | (s[i + k] & 0x3Fu); + } + i += len; + if (codepoint <= 0xFFFF) + { + u_escape(codepoint); + } + else + { + u_escape(0xD7C0u + (codepoint >> 10u)); + u_escape(0xDC00u + (codepoint & 0x3FFu)); + } + continue; + } + // a quote, a backslash, or a control character (or DEL) + const unsigned char c = s[i++]; + char escape = 0; + switch (c) + { + case 0x08: + escape = 'b'; + break; + case 0x09: + escape = 't'; + break; + case 0x0A: + escape = 'n'; + break; + case 0x0C: + escape = 'f'; + break; + case 0x0D: + escape = 'r'; + break; + case 0x22: + escape = '"'; + break; + case 0x5C: + escape = '\\'; + break; + default: + break; + } + if (escape != 0) + { + w[0] = '\\'; + w[1] = escape; + w += 2; + } + else + { + u_escape(c); + } + } + room(1); + *w++ = '"'; + m_out.set_cursor(w); + } + + /// the copies of dump_text() that are not fixed-size moves (long + /// strings, or near the end of the source); out of line, so that the + /// compiler does not merge the fixed-size moves into this call + NLOHMANN_VIEW_NOINLINE static void copy_long(char* to, const char* from, std::size_t n) noexcept + { + std::memcpy(to, from, n); + } + + /// a compile-time option as a runtime condition: testing the template + /// argument directly makes a condition like `Editable && c` constant when + /// the option is off, which MSVC reports as C4127 + static NLOHMANN_VIEW_ALWAYS_INLINE bool enabled(bool option) noexcept + { + return option; + } + + /*! + @brief the output, compact (the default dump()) or indented, with or without ensure_ascii + + The same walk as dump(), with the write position in a local variable + (stores through char pointers would otherwise force a reload of the + buffer's members after each one), and with strings and number tokens of + the source copied by fixed-size moves of 32 bytes where the source has + that many bytes left, instead of a library call per token. The buffer + keeps 64 bytes of slack for the overshoot. With ensure_ascii (Ascii), a + string of the source is copied only if it has nothing to escape (see + copied()); any other string is written by write_ascii(). + */ + template + void dump_text(const node* root) + { + struct frame + { + const node* pos; ///< (editable documents) next element, or key of the next member + const node* end; + bool object; + }; + std::array buffer; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read + std::vector heap; + small_stack stack(buffer.data(), buffer.size(), heap); + const char* const src = m_doc.src; + const char* const src_end = src + m_doc.size; + char* w = m_out.cursor(); + char* lim = m_out.limit(); + // (indented output) a line break and 63 indentation characters, from + // which the line break of a level of up to 63 characters is a copy + std::array ruler; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): filled below if used + if (Pretty) + { + ruler.fill(m_style.indent_char); + ruler[0] = '\n'; + } + const std::size_t indent = m_style.indent; + depth_counter level; // depth of the innermost open container (indented output) + // room for n bytes and the slack + const auto room = [&](std::size_t n) + { + if (NLOHMANN_VIEW_UNLIKELY(static_cast(lim - w) < n + 64)) + { + m_out.set_cursor(w); + m_out.reserve(n + 64); + w = m_out.cursor(); + lim = m_out.limit(); + } + }; + // copy n bytes of the source (after room(n)) + const auto copy = [&](const char* from, std::size_t n) + { + if (n <= 32 && static_cast(src_end - from) >= 32) + { + std::memcpy(w, from, 32); + } + else if (n <= 256 && static_cast(src_end - from) >= n + 32) + { + for (std::size_t i = 0; i < n; i += 32) + { + std::memcpy(w + i, from + i, 32); + } + } + else + { + copy_long(w, from, n); + } + w += n; + }; + // (indented output) the line break and the indentation of a level + const auto newline = [&](std::size_t depth) + { + const std::size_t count = 1 + (depth * indent); + room(count); + if (count <= ruler.size()) + { + std::memcpy(w, ruler.data(), ruler.size()); + } + else + { + *w = '\n'; + std::memset(w + 1, m_style.indent_char, count - 1); + } + w += count; + }; + // a string that is not a plain string of the source (out of line, so + // that the cursor stays in a register here) + const auto escaped = [&](const node & n) + { + m_out.set_cursor(w); + if (enabled(Ascii)) + { + write_ascii(n); + } + else + { + write_decoded(n, Pretty); + } + w = m_out.cursor(); + lim = m_out.limit(); + }; + + // Read-only documents: the elements of a container follow it in the + // node array, so the walk goes through the array in order, and a + // frame only needs the end of its container. Editable documents: the + // elements of a moved container live elsewhere, so a frame keeps the + // position of the next element (see navigation). + // The innermost open container is kept in registers (cur; end == + // nullptr: none), the stack holds the ones around it. + frame cur{nullptr, nullptr, false}; + const node* n = root; + for (;;) + { + // write the value at n (read-only documents: and advance n) + bool opened = false; + // false positive: n comes from nav::value(), which never returns null for a valid index + // @infer-ignore NULLPTR_DEREFERENCE + switch (static_cast(n->kind)) + { + case value_t::string: + if (copied(*n, src, std::integral_constant())) + { + room(n->len + 2); + *w++ = '"'; + copy(src + n->off, n->len); + *w++ = '"'; + } + else + { + escaped(*n); + } + break; + case value_t::number_integer: + case value_t::number_unsigned: + { + const std::uint32_t len = number_length(*n); + room(len); + if (enabled(Editable) && (n->flags & node_flags::storage) != 0) + { + copy_long(w, m_doc.str(*n), len); // a canonical token written by an edit + w += len; + break; + } + const char* const token = src + n->off; + if (!enabled(SourceNumbers) && NLOHMANN_VIEW_UNLIKELY(len == 2 && token[0] == '-' && token[1] == '0')) + { + *w++ = '0'; // parse() reads -0 as the integer 0 + } + else + { + copy(token, len); + } + break; + } + case value_t::number_float: + if (enabled(SourceNumbers) && (n->flags & node_flags::storage) != node_flags::edited) + { + room(n->len); + copy(src + n->off, n->len); + } + else if (enabled(fast_double::value)) + { + room(64); + w = write_double_at(w, *n); + } + else + { + m_out.set_cursor(w); + write_float_node(*n); + w = m_out.cursor(); + lim = m_out.limit(); + } + break; + case value_t::boolean: + room(8); + if ((n->flags & node_flags::is_true) != 0) + { + w = put_literal(w, "true"); + } + else + { + w = put_literal(w, "false"); + } + break; + case value_t::object: + case value_t::array: + { + const bool object = n->kind == static_cast(value_t::object); + room(8); + if (n->len == 0) + { + w = object ? put_literal(w, "{}") : put_literal(w, "[]"); + } + else + { + *w++ = object ? '{' : '['; + stack.push_back(cur); + level.enter(); + if (Editable) + { + cur = frame{nav::first(m_doc, n), nav::end(m_doc, n), object}; + } + else + { + cur = frame{nullptr, n + n->next, object}; + } + opened = true; + } + break; + } + case value_t::null: + room(8); + w = put_literal(w, "null"); + break; + case value_t::binary: // LCOV_EXCL_LINE (not in a document) + case value_t::discarded: // LCOV_EXCL_LINE + default: // LCOV_EXCL_LINE + break; // LCOV_EXCL_LINE + } + if (!Editable) + { + ++n; // the next node: the first element of an opened container, or the node after a scalar + } + + // go to the next value: close finished containers, then separate + // (a container just opened has an element) + if (!opened) + { + for (;;) + { + if (cur.end == nullptr) + { + m_out.set_cursor(w); + m_out.finish(); + return; + } + if ((Editable ? cur.pos : n) != cur.end) + { + break; + } + if (Pretty) + { + newline(level.leave()); + } + room(1); + *w++ = cur.object ? '}' : ']'; + cur = stack.back(); + stack.pop_back(); + } + room(1); + *w++ = ','; + } + if (Pretty) + { + newline(level.get()); + } + const node* const at = Editable ? cur.pos : n; + if (cur.object) + { + const node& key = *at; + if (copied(key, src, std::integral_constant())) + { + room(key.len + (Pretty ? 4 : 3)); + *w++ = '"'; + copy(src + key.off, key.len); + w[0] = '"'; + w[1] = ':'; + w += 2; + } + else + { + escaped(key); + room(Pretty ? 2 : 1); + *w++ = ':'; + } + if (Pretty) + { + *w++ = ' '; + } + if (Editable) + { + n = nav::value(at + 1); + cur.pos = document_data::after(at + 1); + } + else + { + ++n; + } + } + else if (Editable) + { + n = nav::value(at); + cur.pos = document_data::after(at); + } + } + } + + /// a float node as dump() writes it + void write_float_node(const node& n) + { + write_float_node(n, fast_double {}); + } + + void write_float_node(const node& n, std::false_type /*other*/) + { + write_float(float_value(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) + + A token of at most 15 significant digits is written from its digits, + without a conversion: two decimals of at most 15 digits are farther + apart than the rounding interval of a (normal) double (the argument + behind DBL_DIG), so the token's digits are the shortest ones of its + double, which the library's conversion writes (Zmij). Other tokens are + converted from the digits already read. + */ + char* write_double_at(char* w, const node& n) + { + const unsigned int_digits = n.extra & 0xFFu; + const unsigned frac_digits = n.extra >> 8u; + if ((n.flags & node_flags::storage) != node_flags::edited && int_digits + frac_digits <= 19) + { + const auto* const first = reinterpret_cast(m_doc.src + n.off); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + const float_significand d = layout_decimal(first, first + n.len, int_digits, frac_digits, reinterpret_cast(m_doc.src + m_doc.size)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + // (the exponent keeps the value far from subnormals and overflow) + if (d.w != 0 && d.w < 1000000000000000u && d.exponent >= -290 && d.exponent <= 290) + { + *w = '-'; + w += d.negative ? 1 : 0; + // (without leading zeros, all digits of the token count; the + // check also keeps an image that was only checked for bounds, + // whose token may not be made of digits, from the counted + // overload) + const auto& powers = ::nlohmann::detail::dtoa_impl::powers_of_ten(); + const unsigned count = int_digits + frac_digits; + return count - 1u < 15u && d.w >= powers[count - 1u] && d.w < powers[count] + ? ::nlohmann::detail::dtoa_impl::write_short_decimal(w, d.w, static_cast(count), static_cast(d.exponent)) + : ::nlohmann::detail::dtoa_impl::write_short_decimal(w, d.w, static_cast(d.exponent)); + } + return write_double_value_at(w, decimal_to_float(d)); // (without reading the token again) + } + return write_double_value_at(w, static_cast(float_value(m_doc, n))); + } + + /// n bytes of text at w + static char* write_text_at(char* w, const char* text, std::size_t n) noexcept + { + std::memcpy(w, text, n); + return w + n; + } + + /// a double as dump() writes it, at w (64 bytes of room) + static char* write_double_value_at(char* w, double x) + { + // (from the bits: without the checks of to_chars()) + auto bits = bit_cast(x); + if (NLOHMANN_VIEW_UNLIKELY((bits & 0x7FF0000000000000u) == 0x7FF0000000000000u)) + { + return write_text_at(w, "null", 4); + } + *w = '-'; + w += bits >> 63u; + bits &= ~(std::uint64_t{1} << 63u); + if (bits == 0) + { + return write_text_at(w, "0.0", 3); + } + return ::nlohmann::detail::dtoa_impl::write_shortest(w, ::nlohmann::detail::zmij::to_shortest(bits)); + } + + /// as serializer::dump_float() + void write_float(number_float_t x) + { + if (!std::isfinite(x)) + { + m_out.put("null"); + return; + } + write_float(x, std::integral_constant::value>()); + } + + void write_float(number_float_t x, std::true_type /*has_native_float_format*/) + { + std::array buf{}; + const char* const end = ::nlohmann::detail::to_chars(buf.data(), buf.data() + buf.size(), x); + m_out.put(buf.data(), static_cast(end - buf.data())); + } + + void write_float(number_float_t x, std::false_type /*has_native_float_format*/) + { + // other types (e.g. long double) are rare: the library writes them + const string_t s = BasicJsonType(x).dump(); + m_out.put(s.data(), s.size()); + } + + /// throw what basic_json::dump() throws for a string that is not valid UTF-8 + NLOHMANN_VIEW_NOINLINE static void invalid_utf8(const unsigned char* s, std::size_t n) + { + const string_t dumped = BasicJsonType(string_t(reinterpret_cast(s), n)).dump(); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + static_cast(dumped); + } + + template + void write_codepoint(std::uint32_t codepoint, const unsigned char* bytes, std::size_t len) + { + switch (codepoint) + { + case 0x08: + m_out.put("\\b"); + return; + case 0x09: + m_out.put("\\t"); + return; + case 0x0A: + m_out.put("\\n"); + return; + case 0x0C: + m_out.put("\\f"); + return; + case 0x0D: + m_out.put("\\r"); + return; + case 0x22: + m_out.put("\\\""); + return; + case 0x5C: + m_out.put("\\\\"); + return; + default: + break; + } + if (codepoint <= 0x1F || (EnsureAscii && codepoint >= 0x7F)) + { + 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(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) + { + std::array e = {{'\\', 'u', 0, 0, 0, 0}}; + hex4_lower(static_cast(u), &e[2]); + m_out.put(e.data(), e.size()); + } + + const document_data& m_doc; + output_buffer m_out; + const dump_style m_style; +}; + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/simd.hpp b/include/nlohmann/detail/view/simd.hpp new file mode 100644 index 000000000..7a0248f63 --- /dev/null +++ b/include/nlohmann/detail/view/simd.hpp @@ -0,0 +1,372 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-FileCopyrightText: 2018-2025 The simdjson authors +// SPDX-License-Identifier: MIT + +#pragma once + +#include // array +#include // atomic +#include // size_t +#include // uint8_t, uint64_t + +#include +#include + +// Vector code for long runs of string bytes. NEON (AArch64) and SSE2 (x86-64) +// belong to the baseline instruction sets and are used by default. The vector +// UTF-8 check needs NEON or SSSE3. SSSE3 is not part of x86-64, and the code +// must not depend on the flags of a translation unit (two translation units +// with different flags would have different definitions of the same inline +// functions): the check is compiled for SSSE3 with a function attribute and +// used where the CPU has SSSE3 (all x86-64 CPUs since about 2011), else the +// portable check. JSON_VIEW_USE_SSSE3 skips the CPU check (for code compiled +// for SSSE3 anyway); JSON_VIEW_NO_SIMD selects the portable code. +// NLOHMANN_VIEW_NO_TARGET_ATTRIBUTE (set by the GCC module interface, where GCC +// ignores the target attribute and then rejects the SSSE3 intrinsics) keeps the +// check portable without disabling the SSE2 code. +#if !defined(JSON_VIEW_NO_SIMD) && defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) && NLOHMANN_VIEW_LITTLE_ENDIAN + #include + #define NLOHMANN_VIEW_NEON 1 +#else + #define NLOHMANN_VIEW_NEON 0 +#endif +// (x86 only: other targets can define __SSE2__ as well, e.g., WebAssembly with -msse2, but have no ) +#if !defined(JSON_VIEW_NO_SIMD) && !NLOHMANN_VIEW_NEON && (defined(__x86_64__) || defined(__i386__) || defined(_M_X64) || defined(_M_IX86)) && (defined(__SSE2__) || defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2)) + #include + #define NLOHMANN_VIEW_SSE2 1 +#else + #define NLOHMANN_VIEW_SSE2 0 +#endif +#if NLOHMANN_VIEW_SSE2 && defined(JSON_VIEW_USE_SSSE3) + #include + #define NLOHMANN_VIEW_SSSE3 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) +#else + #define NLOHMANN_VIEW_SSSE3 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) +#endif +#if NLOHMANN_VIEW_SSE2 && !NLOHMANN_VIEW_SSSE3 && !defined(NLOHMANN_VIEW_NO_TARGET_ATTRIBUTE) && ((defined(__clang__) && __clang_major__ >= 4) || (defined(__GNUC__) && !defined(__clang__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 9)))) + // (GCC before 4.9 has no SSSE3 intrinsics without -mssse3) + // of GCC before 11 has no include guard, and json.hpp and + // json_view.hpp both include it + #ifndef NLOHMANN_JSON_CPUID_INCLUDED + #define NLOHMANN_JSON_CPUID_INCLUDED + #include + #endif + #include + #define NLOHMANN_VIEW_SSSE3_DISPATCH 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) + #define NLOHMANN_VIEW_SSSE3_TARGET __attribute__((target("ssse3"))) +#elif NLOHMANN_VIEW_SSE2 && !NLOHMANN_VIEW_SSSE3 && defined(_MSC_VER) + // (MSVC compiles intrinsics of any instruction set) + #include + #include + #define NLOHMANN_VIEW_SSSE3_DISPATCH 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) + #define NLOHMANN_VIEW_SSSE3_TARGET +#else + #define NLOHMANN_VIEW_SSSE3_DISPATCH 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) + #define NLOHMANN_VIEW_SSSE3_TARGET +#endif +#define NLOHMANN_VIEW_VECTOR (NLOHMANN_VIEW_NEON || NLOHMANN_VIEW_SSE2) +#define NLOHMANN_VIEW_VECTOR_UTF8 (NLOHMANN_VIEW_NEON || NLOHMANN_VIEW_SSSE3 || NLOHMANN_VIEW_SSSE3_DISPATCH) + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +#if NLOHMANN_VIEW_VECTOR +/*! +@brief the first byte of a string run that is a quote, a backslash, a control +character, or not ASCII, 16 bytes per step + +Stops at such a byte, or where fewer than 16 bytes are left (the caller tells +the two apart). A signed compare with 0x20 finds control characters and +non-ASCII bytes at once. +*/ +NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* vector_plain_run(const unsigned char* p, const unsigned char* e) noexcept +{ + while (e - p >= 16) + { +#if NLOHMANN_VIEW_NEON + const uint8x16_t in = vld1q_u8(p); + const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))), + vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20))); + // one nibble per byte (the usual NEON replacement of x86's movemask, see + // D. Kutenin, "Porting x86 vector bitmask optimizations to Arm NEON", 2022) + const std::uint64_t bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0); + if (bits != 0) + { + return p + (count_trailing_zeros(bits) >> 2u); + } +#else + const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_cmplt_epi8(in, _mm_set1_epi8(0x20))); + const auto bits = static_cast(static_cast(_mm_movemask_epi8(special))); + if (bits != 0) + { + return p + count_trailing_zeros(bits); + } +#endif + p += 16; + } + return p; +} + +/*! +@brief copy 16 bytes from p to w, and find the first quote, backslash, +control character, or non-ASCII byte among them + +The bytes are copied whether or not they are all plain (the caller has room +for 16 bytes and keeps only the plain ones). + +@return the offset of that byte, or 16 if there is none +*/ +NLOHMANN_VIEW_ALWAYS_INLINE unsigned copy_plain_run16(const unsigned char* p, char* w) noexcept +{ +#if NLOHMANN_VIEW_NEON + const uint8x16_t in = vld1q_u8(p); + vst1q_u8(reinterpret_cast(w), in); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))), + vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20))); + const std::uint64_t bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0); + return bits != 0 ? static_cast(count_trailing_zeros(bits) >> 2u) : 16u; +#else + const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) + _mm_storeu_si128(static_cast<__m128i*>(static_cast(w)), in); // NOLINT(bugprone-casting-through-void) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_cmplt_epi8(in, _mm_set1_epi8(0x20))); + const auto bits = static_cast(static_cast(_mm_movemask_epi8(special))); + return bits != 0 ? static_cast(count_trailing_zeros(bits)) : 16u; +#endif +} +#endif + +#if NLOHMANN_VIEW_SSSE3_DISPATCH +/// whether the CPU has SSSE3 (CPUID leaf 1, ECX bit 9) +inline bool cpu_ssse3() noexcept +{ +#if defined(_MSC_VER) && !defined(__clang__) + std::array regs {{}}; + __cpuid(regs.data(), 1); + return (static_cast(regs[2]) & (1u << 9u)) != 0; +#else + unsigned eax = 0; + unsigned ebx = 0; + unsigned ecx = 0; + unsigned edx = 0; + return __get_cpuid(1, &eax, &ebx, &ecx, &edx) != 0 && (ecx & (1u << 9u)) != 0; +#endif +} + +/// whether the CPU has SSSE3, asked once: the answer is kept in an atomic +/// that is initialized at compile time, so that neither a guard of a local +/// static nor a global constructor is needed (threads that ask at the same +/// time all store the same answer) +NLOHMANN_VIEW_ALWAYS_INLINE bool cpu_has_ssse3() noexcept +{ + static std::atomic known{0}; // 0: not asked yet, 1: no, 2: yes + int state = known.load(std::memory_order_relaxed); + if (NLOHMANN_VIEW_UNLIKELY(state == 0)) + { + state = cpu_ssse3() ? 2 : 1; + known.store(state, std::memory_order_relaxed); + } + return state == 2; +} +#endif + +#if NLOHMANN_VIEW_VECTOR_UTF8 +/// Tables of the UTF-8 check of J. Keiser and D. Lemire, "Validating UTF-8 In +/// Less Than One Instruction Per Byte" (2021), as in simdjson ("lookup4"): each +/// maps a nibble (high and low nibble of the previous byte, high nibble of the +/// current byte) to the errors it allows; a byte pair is ill-formed if all +/// three have an error bit in common. +template +struct utf8_lookup4 +{ + static constexpr std::uint8_t too_short = 1u << 0u, too_long = 1u << 1u, overlong_3 = 1u << 2u, too_large = 1u << 3u; + static constexpr std::uint8_t surrogate = 1u << 4u, overlong_2 = 1u << 5u, too_large_1000 = 1u << 6u, overlong_4 = 1u << 6u; + static constexpr std::uint8_t two_conts = 1u << 7u, carry = too_short | too_long | two_conts; + static const std::array byte_1_high; + static const std::array byte_1_low; + static const std::array byte_2_high; +}; + +template +const std::array utf8_lookup4::byte_1_high = +{ + { + too_long, too_long, too_long, too_long, too_long, too_long, too_long, too_long, + two_conts, two_conts, two_conts, two_conts, + too_short | overlong_2, too_short, too_short | overlong_3 | surrogate, too_short | too_large | too_large_1000 | overlong_4 + } +}; + +template +const std::array utf8_lookup4::byte_1_low = +{ + { + carry | overlong_3 | overlong_2 | overlong_4, carry | overlong_2, carry, carry, + carry | too_large, carry | too_large | too_large_1000, carry | too_large | too_large_1000, carry | too_large | too_large_1000, + carry | too_large | too_large_1000, carry | too_large | too_large_1000, carry | too_large | too_large_1000, carry | too_large | too_large_1000, + carry | too_large | too_large_1000, carry | too_large | too_large_1000 | surrogate, carry | too_large | too_large_1000, carry | too_large | too_large_1000 + } +}; + +template +const std::array utf8_lookup4::byte_2_high = +{ + { + too_short, too_short, too_short, too_short, too_short, too_short, too_short, too_short, + static_cast(too_long | overlong_2 | two_conts | overlong_3 | too_large_1000 | overlong_4), + static_cast(too_long | overlong_2 | two_conts | overlong_3 | too_large), + static_cast(too_long | overlong_2 | two_conts | surrogate | too_large), + static_cast(too_long | overlong_2 | two_conts | surrogate | too_large), + too_short, too_short, too_short, too_short + } +}; + +/// the end of scan_string_vector from block, where the vector loop stopped +/// (ill-formed UTF-8, or fewer than 16 bytes left): one byte or sequence at a +/// time, from the start of a sequence that crosses into the block +inline const unsigned char* scan_string_finish(const unsigned char* p, const unsigned char* block, const unsigned char* e, const std::uint8_t* plain) noexcept +{ + for (int i = 1; i <= 3 && block - i >= p; ++i) + { + const unsigned char c = block[-i]; + if (c < 0x80) + { + break; + } + if (c >= 0xC0) + { + const int len = 2 + static_cast(c >= 0xE0) + static_cast(c >= 0xF0); + if (len > i) + { + block -= i; + } + break; + } + } + for (p = block; p != e;) + { + if (*p < 0x80) + { + if (plain[*p] == 0) + { + return p; + } + ++p; + continue; + } + const std::size_t n = validate_one_utf8(p, static_cast(e - p)); + if (n == 0) + { + return p; + } + p += n; + } + return p; +} + +/*! +@brief the rest of a string from p (a character boundary), 16 bytes per step + +The first quote, backslash, or control character is found with vector +compares, and the UTF-8 check covers the bytes up to it. Returns where 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 +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()). +*/ +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 +{ + using lookup = utf8_lookup4<>; + const unsigned char* block = p; +#if NLOHMANN_VIEW_NEON + const uint8x16_t t1h = vld1q_u8(lookup::byte_1_high.data()); + const uint8x16_t t1l = vld1q_u8(lookup::byte_1_low.data()); + const uint8x16_t t2h = vld1q_u8(lookup::byte_2_high.data()); + uint8x16_t prev = vdupq_n_u8(0); + while (e - block >= 16) + { + const uint8x16_t in = vld1q_u8(block); + const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))), vcltq_u8(in, vdupq_n_u8(0x20))); + const uint8x16_t prev1 = vextq_u8(prev, in, 15); + const uint8x16_t sc = vandq_u8(vandq_u8(vqtbl1q_u8(t1h, vshrq_n_u8(prev1, 4)), vqtbl1q_u8(t1l, vandq_u8(prev1, vdupq_n_u8(0x0F)))), vqtbl1q_u8(t2h, vshrq_n_u8(in, 4))); + const uint8x16_t must23 = vorrq_u8(vqsubq_u8(vextq_u8(prev, in, 14), vdupq_n_u8(0xE0 - 0x80)), vqsubq_u8(vextq_u8(prev, in, 13), vdupq_n_u8(0xF0 - 0x80))); + const uint8x16_t err = veorq_u8(vandq_u8(must23, vdupq_n_u8(0x80)), sc); + const std::uint64_t special_bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0); + const std::uint64_t err_bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(vtstq_u8(err, err)), 4)), 0); + if (special_bits != 0) + { + // errors up to the special byte count (an incomplete sequence + // before a quote shows at the quote); the bytes after it do not + const unsigned k = static_cast(count_trailing_zeros(special_bits)) >> 2u; + const std::uint64_t upto = k == 15 ? ~std::uint64_t{0} : + (std::uint64_t{1} << (4u * (k + 1u))) - 1u; + if ((err_bits & upto) == 0) + { + return block + k; + } + break; + } + if (err_bits != 0) + { + break; + } + prev = in; + block += 16; + } +#else + // the same with SSSE3 (pshufb for the table lookups; nibbles from 16-bit + // shifts, as there are no byte shifts) + const __m128i t1h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_high.data()))); // NOLINT(bugprone-casting-through-void) + const __m128i t1l = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_low.data()))); // NOLINT(bugprone-casting-through-void) + const __m128i t2h = _mm_loadu_si128(static_cast(static_cast(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(static_cast(block))); // NOLINT(bugprone-casting-through-void) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_cmpeq_epi8(_mm_subs_epu8(in, _mm_set1_epi8(0x1F)), zero)); // in < 0x20 + const __m128i prev1 = _mm_alignr_epi8(in, prev, 15); + const __m128i sc = _mm_and_si128(_mm_and_si128(_mm_shuffle_epi8(t1h, _mm_and_si128(_mm_srli_epi16(prev1, 4), nibble)), + _mm_shuffle_epi8(t1l, _mm_and_si128(prev1, nibble))), + _mm_shuffle_epi8(t2h, _mm_and_si128(_mm_srli_epi16(in, 4), nibble))); + const __m128i must23 = _mm_or_si128(_mm_subs_epu8(_mm_alignr_epi8(in, prev, 14), _mm_set1_epi8(0xE0 - 0x80)), + _mm_subs_epu8(_mm_alignr_epi8(in, prev, 13), _mm_set1_epi8(0xF0 - 0x80))); + const __m128i err = _mm_xor_si128(_mm_and_si128(must23, _mm_set1_epi8(static_cast(-128))), sc); + const auto special_bits = static_cast(_mm_movemask_epi8(special)); + const auto err_bits = ~static_cast(_mm_movemask_epi8(_mm_cmpeq_epi8(err, zero))) & 0xFFFFu; + if (special_bits != 0) + { + const auto k = static_cast(count_trailing_zeros(static_cast(special_bits))); + if ((err_bits & ((2u << k) - 1u)) == 0) + { + return block + k; + } + break; + } + if (err_bits != 0) + { + break; + } + prev = in; + block += 16; + } +#endif + return scan_string_finish(p, block, e, plain); +} +#endif + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/string_ref.hpp b/include/nlohmann/detail/view/string_ref.hpp new file mode 100644 index 000000000..d401500ec --- /dev/null +++ b/include/nlohmann/detail/view/string_ref.hpp @@ -0,0 +1,113 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // min +#include // size_t +#include // memcmp, strlen +#include // basic_string + +#include +#include + +#if NLOHMANN_VIEW_HAS_CPP_17 + #include // string_view +#endif +#ifndef JSON_NO_IO + #include // ostream +#endif + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +#if NLOHMANN_VIEW_HAS_CPP_17 +using string_ref = std::string_view; +#else +/// minimal C++11 stand-in for std::string_view +class string_ref +{ + public: + using size_type = std::size_t; + using const_iterator = const char*; + + string_ref() noexcept = default; + // s must be null-terminated, as for std::string_view(const char*) + // flawfinder: ignore + string_ref(const char* s) : m_data(s), m_size(std::strlen(s)) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) + string_ref(const char* s, std::size_t n) noexcept : m_data(s), m_size(n) {} + template + string_ref(const std::basic_string& s) noexcept : m_data(s.data()), m_size(s.size()) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) + + const char* data() const noexcept + { + return m_data; + } + std::size_t size() const noexcept + { + return m_size; + } + std::size_t length() const noexcept + { + return m_size; + } + bool empty() const noexcept + { + return m_size == 0; + } + const char* begin() const noexcept + { + return m_data; + } + const char* end() const noexcept + { + return m_data + m_size; + } + char operator[](std::size_t i) const noexcept + { + return m_data[i]; + } + + template + explicit operator std::basic_string() const + { + return std::basic_string(m_data, m_size); + } + + friend bool operator==(string_ref a, string_ref b) noexcept + { + return a.m_size == b.m_size && (a.m_size == 0 || std::memcmp(a.m_data, b.m_data, a.m_size) == 0); + } + friend bool operator!=(string_ref a, string_ref b) noexcept + { + return !(a == b); + } + friend bool operator<(string_ref a, string_ref b) noexcept + { + const int c = std::memcmp(a.m_data, b.m_data, (std::min)(a.m_size, b.m_size)); + return c != 0 ? c < 0 : a.m_size < b.m_size; + } +#ifndef JSON_NO_IO + friend std::ostream& operator<<(std::ostream& o, string_ref s) + { + return o.write(s.m_data, static_cast(s.m_size)); + } +#endif + + private: + const char* m_data = ""; + std::size_t m_size = 0; +}; +#endif + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/value.hpp b/include/nlohmann/detail/view/value.hpp new file mode 100644 index 000000000..945071705 --- /dev/null +++ b/include/nlohmann/detail/view/value.hpp @@ -0,0 +1,144 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // nullptr_t, size_t +#include // int64_t +#include // map +#include // basic_string +#include // decay, enable_if, is_arithmetic, is_constructible, is_integral, is_same +#include // unordered_map +#include // vector + +#include +#include +#include +#include +#include +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// selects a conversion by its target type +template +struct value_tag {}; + +/*! +@brief the number or boolean of a node converted to an arithmetic type + +As basic_json's get() for arithmetic types: integers and floats are +converted with static_cast, booleans give 0 or 1, and other types throw +type_error.302. +*/ +template +NLOHMANN_VIEW_ALWAYS_INLINE T arithmetic_value(const document_data& d, const node& n) +{ + switch (static_cast(n.kind)) + { + case value_t::number_unsigned: + return static_cast(static_cast(integer_bits(n))); + case value_t::number_integer: + return static_cast(static_cast(static_cast(integer_bits(n)))); + case value_t::number_float: + return static_cast(float_value(d, n)); + case value_t::boolean: + return static_cast((n.flags & node_flags::is_true) != 0); + 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: + throw_type_error(exception_id::type_mismatch, "type must be number, but is ", value_type_name(static_cast(n.kind))); + } +} + +/// std::vector from an array, element by element (type_error.302 otherwise) +template +std::vector vector_value(const View& v) +{ + if (NLOHMANN_VIEW_UNLIKELY(!v.is_array())) + { + throw_type_error(exception_id::type_mismatch, "type must be array, but is ", v.type_name()); + } + std::vector r; + r.reserve(v.size()); + for (const View e : v) + { + r.push_back(e.template get()); + } + return r; +} + +/// a map with string keys from an object; with duplicate keys, the last +/// value is kept, as parse() does (type_error.302 for other types) +template +Map map_value(const View& v) +{ + if (NLOHMANN_VIEW_UNLIKELY(!v.is_object())) + { + throw_type_error(exception_id::type_mismatch, "type must be object, but is ", v.type_name()); + } + Map r; + for (auto it = v.begin(); it != v.end(); ++it) + { + const auto key = it.key(); + r[typename Map::key_type(key.data(), key.size())] = it.value().template get(); + } + return r; +} + +/// whether a map type is read member by member (its keys are made from +/// characters and a length); other maps go through basic_json +template +struct is_string_key : std::is_constructible {}; + +/// the arithmetic types arithmetic_value() converts to (bool has its own +/// conversion) +template +struct is_non_bool_arithmetic : conjunction, negation>> {}; + +/// the default values of value() other than C strings, which have their own +/// overload (returning string_t) +template +struct is_non_c_string_default : negation::type, const char*>> {}; + +/// the integral types accepted as an array index by the edits of a document +template +struct is_non_bool_integral : conjunction, negation>> {}; + +/// whether View::get() converts to T without materialize(); get_to() +/// assigns these, and passes the others to from_json +template +struct is_direct_value : std::integral_constant < bool, + std::is_arithmetic::value || std::is_same::value + || std::is_same::value + || std::is_same::value || std::is_same::value > +{}; + +template +struct is_direct_value, BasicJsonType, View> : std::true_type {}; + +template +struct is_direct_value, BasicJsonType, View> : std::true_type {}; + +template +struct is_direct_value, BasicJsonType, View> : is_string_key {}; + +template +struct is_direct_value, BasicJsonType, View> : is_string_key {}; + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 9c89ab0bc..3d40682d9 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -19,6 +19,7 @@ #define INCLUDE_NLOHMANN_JSON_HPP_ #include // all_of, find, for_each, none_of +#include // array #include // isnan #include // nullptr_t, ptrdiff_t, size_t #include // uint8_t @@ -29,6 +30,7 @@ #endif // JSON_NO_IO #include // make_move_iterator, random_access_iterator_tag #include // numeric_limits +#include // map #include // unique_ptr #include // swap, operator!= #include // length_error @@ -42,12 +44,18 @@ // each of these, not only the forward declarations from json_fwd.hpp, so IWYU's suggestion to // drop them (nothing in this file otherwise names the type) would break every downstream // translation unit that relies on basic_json<>'s defaults actually being usable. -#include // IWYU pragma: keep +// export: client code names what these headers declare (nlohmann::json and ordered_json from +// json_fwd.hpp, adl_serializer for specializations, json_ref behind brace initialization, the +// NLOHMANN_DEFINE_* and NLOHMANN_JSON_NAMESPACE_* macros, and the literals at the end of this file), and include analyzers must +// attribute it to this header, not tell clients to include the internal ones (issue #5812). +// tests/iwyu_consumer checks this. +#include // IWYU pragma: export #include -#include +#include +#include // IWYU pragma: export #include // IWYU pragma: keep #include // IWYU pragma: keep -#include +#include // IWYU pragma: export #include #include #include @@ -61,8 +69,8 @@ #include #include #include -#include -#include +#include // IWYU pragma: export +#include // IWYU pragma: export #include #include #include @@ -74,7 +82,7 @@ #include #include #include -#include +#include // IWYU pragma: export #include // IWYU pragma: keep #include @@ -151,9 +159,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec friend class ::nlohmann::detail::binary_writer; template friend class ::nlohmann::detail::binary_reader; - template + template friend class ::nlohmann::detail::json_sax_dom_parser; template + friend class ::nlohmann::detail::json_sax_dom_binary_parser; + template friend class ::nlohmann::detail::json_sax_dom_callback_parser; #if JSON_DIAGNOSTIC_POSITIONS friend struct ::nlohmann::detail::diagnostic_positions; @@ -366,9 +376,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec result["name"] = "JSON for Modern C++"; result["url"] = "https://github.com/nlohmann/json"; result["version"]["string"] = - detail::concat(std::to_string(NLOHMANN_JSON_VERSION_MAJOR), '.', - std::to_string(NLOHMANN_JSON_VERSION_MINOR), '.', - std::to_string(NLOHMANN_JSON_VERSION_PATCH)); + detail::concat(detail::to_string(static_cast(NLOHMANN_JSON_VERSION_MAJOR)), '.', + detail::to_string(static_cast(NLOHMANN_JSON_VERSION_MINOR)), '.', + detail::to_string(static_cast(NLOHMANN_JSON_VERSION_PATCH))); result["version"]["major"] = NLOHMANN_JSON_VERSION_MAJOR; result["version"]["minor"] = NLOHMANN_JSON_VERSION_MINOR; result["version"]["patch"] = NLOHMANN_JSON_VERSION_PATCH; @@ -390,10 +400,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec #elif defined(__clang__) result["compiler"] = {{"family", "clang"}, {"version", __clang_version__}}; #elif defined(__GNUC__) || defined(__GNUG__) - result["compiler"] = {{"family", "gcc"}, {"version", detail::concat( - std::to_string(__GNUC__), '.', - std::to_string(__GNUC_MINOR__), '.', - std::to_string(__GNUC_PATCHLEVEL__)) + result["compiler"] = {{"family", "gcc"}, {"version", detail::concat( + detail::to_string(static_cast(__GNUC__)), '.', + detail::to_string(static_cast(__GNUC_MINOR__)), '.', + detail::to_string(static_cast(__GNUC_PATCHLEVEL__))) } }; #elif defined(__HP_aCC) @@ -411,9 +421,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec #endif #if defined(_MSVC_LANG) - result["compiler"]["c++"] = std::to_string(_MSVC_LANG); + result["compiler"]["c++"] = detail::to_string(static_cast(_MSVC_LANG)); #elif defined(__cplusplus) - result["compiler"]["c++"] = std::to_string(__cplusplus); + result["compiler"]["c++"] = detail::to_string(static_cast(__cplusplus)); #else result["compiler"]["c++"] = "unknown"; #endif @@ -495,8 +505,22 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec "NumberUnsignedType must be at least as wide as NumberIntegerType, " "because it has to hold the absolute value of every NumberIntegerType value"); + // NumberUnsignedType may be the same type as NumberIntegerType (#2573), but + // a repeated NumberFloatType would give overloads that collide, so it gets + // a readable diagnostic instead. + static_assert(!std::is_same::value && !std::is_same::value, + "NumberFloatType must be a different type than NumberIntegerType and NumberUnsignedType"); + private: + /// the kind a value of kind @a t is stored as: if number_unsigned_t is the + /// same type as number_integer_t, every integer is stored as number_integer + /// (#2573); every other kind is stored as itself + static constexpr value_t stored_kind(const value_t t) noexcept + { + return (std::is_same::value && t == value_t::number_unsigned) ? value_t::number_integer : t; + } + /// helper for exception-safe object creation template JSON_HEDLEY_RETURNS_NON_NULL @@ -570,8 +594,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec json_value(boolean_t v) noexcept : boolean(v) {} /// constructor for numbers (integer) json_value(number_integer_t v) noexcept : number_integer(v) {} - /// constructor for numbers (unsigned) - json_value(number_unsigned_t v) noexcept : number_unsigned(v) {} + /// constructor for numbers (unsigned); takes a placeholder if number_unsigned_t is number_integer_t + json_value(detail::number_unsigned_overload_type v) noexcept : number_unsigned(v) {} /// constructor for numbers (floating-point) json_value(number_float_t v) noexcept : number_float(v) {} /// constructor for empty values of a given type @@ -580,26 +604,12 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec switch (t) { case value_t::object: - { - object = create(); - break; - } - case value_t::array: - { - array = create(); - break; - } - case value_t::string: - { - string = create(""); - break; - } - case value_t::binary: { - binary = create(); + // allocating: kept out of line, so the scalar cases below inline + init_owned(t); break; } @@ -639,13 +649,41 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec object = nullptr; // silence warning, see #821 if (JSON_HEDLEY_UNLIKELY(t == value_t::null)) { - JSON_THROW(other_error::create(500, "961c151d2e87f2686a955a9be24d316f1362bf21 3.12.0", nullptr)); // LCOV_EXCL_LINE + detail::throw_literal(detail::exception_id::internal_error, "961c151d2e87f2686a955a9be24d316f1362bf21 3.12.0", nullptr); // LCOV_EXCL_LINE } break; } } } + JSON_HEDLEY_NEVER_INLINE + void init_owned(value_t t) + { + switch (t) + { + case value_t::object: + object = create(); + break; + case value_t::array: + array = create(); + break; + case value_t::string: + string = create(""); + break; + case value_t::binary: + binary = create(); + break; + case value_t::null: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::discarded: + default: + break; + } + } + /// constructor for strings json_value(const string_t& value) : string(create(value)) {} @@ -696,23 +734,37 @@ private: // true if v is not an array/object, or is an already-empty one static bool has_no_children(const basic_json& v) noexcept { - switch (v.m_data.m_type) + if (v.m_data.m_type == value_t::array) { - case value_t::array: - return v.m_data.m_value.array->empty(); - case value_t::object: - return v.m_data.m_value.object->empty(); - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - return true; + return v.m_data.m_value.array->empty(); } + if (v.m_data.m_type == value_t::object) + { + return v.m_data.m_value.object->empty(); + } + return true; + } + + // true if no child of the non-empty array/object v has children of + // its own, i.e. v can be freed in one go (its destructor then + // destroys scalars and empty containers only, so it recurses at most + // one level deep); stops at the first structured child it meets + static bool all_children_leaves(const basic_json& v) noexcept + { + // array_t only needs forward iterators (see custom_array_type) + if (v.m_data.m_type == value_t::array) + { + const auto& a = *v.m_data.m_value.array; + return std::all_of(a.begin(), a.end(), [](const basic_json & el) + { + return has_no_children(el); + }); + } + const auto& o = *v.m_data.m_value.object; + return std::all_of(o.begin(), o.end(), [](const typename object_t::value_type & el) + { + return has_no_children(el.second); + }); } // The walk in destroy_container() may take the children of a @@ -770,15 +822,16 @@ private: } } - // deallocates the (already empty) array/object held by v; this is - // the same allocator-based free the old recursive implementation - // used, just factored out so every level of the walk in destroy() - // can share it + // deallocates the array/object held by v, which is either empty or + // holds only scalars and empty containers (so its destructor + // recurses at most one level deep); this is the same allocator-based + // free the old recursive implementation used, just factored out so + // every level of the walk in destroy() can share it static void free_container(basic_json& v) noexcept { + JSON_ASSERT(has_no_children(v) || all_children_leaves(v)); if (v.m_data.m_type == value_t::array) { - JSON_ASSERT(v.m_data.m_value.array->empty()); AllocatorType alloc; std::allocator_traits::destroy(alloc, v.m_data.m_value.array); std::allocator_traits::deallocate(alloc, v.m_data.m_value.array, 1); @@ -786,7 +839,6 @@ private: else { JSON_ASSERT(v.m_data.m_type == value_t::object); - JSON_ASSERT(v.m_data.m_value.object->empty()); AllocatorType alloc; std::allocator_traits::destroy(alloc, v.m_data.m_value.object); std::allocator_traits::deallocate(alloc, v.m_data.m_value.object, 1); @@ -795,28 +847,17 @@ private: } public: - void destroy_string() noexcept + template + static void destroy_pointer(T*& ptr) noexcept { - if (string == nullptr) + if (ptr == nullptr) { // not initialized (e.g., due to exception in the ctor) return; } - AllocatorType alloc; - std::allocator_traits::destroy(alloc, string); - std::allocator_traits::deallocate(alloc, string, 1); - } - - void destroy_binary() noexcept - { - if (binary == nullptr) - { - // not initialized (e.g., due to exception in the ctor) - return; - } - AllocatorType alloc; - std::allocator_traits::destroy(alloc, binary); - std::allocator_traits::deallocate(alloc, binary, 1); + AllocatorType alloc; + std::allocator_traits::destroy(alloc, ptr); + std::allocator_traits::deallocate(alloc, ptr, 1); } // t must be value_t::array or value_t::object @@ -831,40 +872,99 @@ public: return; } - // Destroy the tree without recursing per nesting level and - // without any heap allocation: a heap-allocated flattening - // stack (the previous implementation) can itself throw - // bad_alloc, which would escape this noexcept destructor and - // terminate the program (#5135). - // - // Instead, walk down a chain of children (always the one - // walk_child() picks), reversing links as we go: cur is the - // container currently being emptied, and prev is its parent - // (value_t::null when there is none). Each parent's - // walk_child() slot doubles as storage for that parent's own - // parent link while we are below it, so no extra memory is - // needed; the parent is not modified meanwhile, so - // walk_child() finds that same slot again on the way up. We - // only ever remove a child once it is a scalar or an empty - // array/object, which neither allocates nor recurses more - // than one level deep. - // - // This json_value is not itself a basic_json, so the - // top-level container is first moved into a local stand-in - // ("cur"); a default-constructed basic_json has a null - // pointer in its m_value (see data::m_value's initializer), - // so swapping it with *this leaves this union's own pointer - // null, and it is never looked at or freed a second time. + // This json_value is not itself a basic_json, so the top-level + // container is first moved into a local stand-in ("cur"); a + // default-constructed basic_json has a null pointer in its + // m_value (see data::m_value's initializer), so swapping it + // with *this leaves this union's own pointer null, and it is + // never looked at or freed a second time. basic_json cur; cur.m_data.m_type = t; using std::swap; swap(cur.m_data.m_value, *this); + destroy_bounded(cur, 0); + } + + // Destroy the array/object v, whose children are destroyed first. + // The first recursion_depth_limit() levels are destroyed by plain + // recursion, which is the fastest way and cannot throw; anything + // nested deeper is handed to destroy_walk(), which uses no call stack. + static void destroy_bounded(basic_json& v, std::size_t depth) noexcept + { + if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) + { + destroy_walk(v); + return; + } + + if (v.m_data.m_type == value_t::array) + { + for (auto& child : *v.m_data.m_value.array) + { + if (child.m_data.m_type == value_t::array || child.m_data.m_type == value_t::object) + { + destroy_bounded(child, depth + 1); + } + } + } + else + { + for (auto& child : *v.m_data.m_value.object) + { + if (child.second.m_data.m_type == value_t::array || child.second.m_data.m_type == value_t::object) + { + destroy_bounded(child.second, depth + 1); + } + } + } + + // only leaves and already destroyed (null) children remain + free_container(v); + } + + // Destroy the tree below top without recursing per nesting level and + // without any heap allocation: a heap-allocated flattening + // stack (the previous implementation) can itself throw + // bad_alloc, which would escape this noexcept destructor and + // terminate the program (#5135). + // + // Instead, walk down a chain of children (always the one + // walk_child() picks), reversing links as we go: cur is the + // container currently being emptied, and prev is its parent + // (value_t::null when there is none). Each parent's + // walk_child() slot doubles as storage for that parent's own + // parent link while we are below it, so no extra memory is + // needed; the parent is not modified meanwhile, so + // walk_child() finds that same slot again on the way up. We + // only ever remove a child once it is a scalar or an empty + // array/object, which neither allocates nor recurses more + // than one level deep. + // + // This json_value is not itself a basic_json, so the + // top-level container is first moved into a local stand-in + // ("cur"); a default-constructed basic_json has a null + // pointer in its m_value (see data::m_value's initializer), + // so swapping it with *this leaves this union's own pointer + // null, and it is never looked at or freed a second time. + static void destroy_walk(basic_json& top) noexcept + { + basic_json cur; + take(cur, top); + basic_json prev; // value_t::null: no parent + // true right after cur was entered: its children have not been + // looked at yet. If none of them has children of its own, cur is + // freed in one go instead of child by child; this is tried once + // per container, so the walk stays linear. + bool fresh = true; + while (true) { - if (has_no_children(cur)) + const bool fresh_now = fresh; + fresh = false; + if (has_no_children(cur) || (fresh_now && all_children_leaves(cur))) { if (prev.m_data.m_type == value_t::null) { @@ -903,19 +1003,31 @@ public: take(cur_child_ref, prev); take(prev, cur); take(cur, tmp); + fresh = true; } } + // keep the check whether t owns memory inline so destroying a scalar + // is a compare and a branch, and leave the actual work to an + // out-of-line function void destroy(value_t t) + { + if (JSON_HEDLEY_UNLIKELY(detail::owns_memory(t))) + { + destroy_owned(t); + } + } + + JSON_HEDLEY_NEVER_INLINE void destroy_owned(value_t t) { switch (t) { case value_t::string: - destroy_string(); + destroy_pointer(string); break; case value_t::binary: - destroy_binary(); + destroy_pointer(binary); break; case value_t::object: @@ -977,36 +1089,19 @@ public: void set_parents() { #if JSON_DIAGNOSTICS - switch (m_data.m_type) + if (m_data.m_type == value_t::array) { - case value_t::array: + for (auto& element : *m_data.m_value.array) { - for (auto& element : *m_data.m_value.array) - { - element.m_parent = this; - } - break; + element.m_parent = this; } - - case value_t::object: + } + else if (m_data.m_type == value_t::object) + { + for (auto& element : *m_data.m_value.object) { - for (auto& element : *m_data.m_value.object) - { - element.second.m_parent = this; - } - break; + element.second.m_parent = this; } - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - break; } #endif } @@ -1074,11 +1169,16 @@ public: return next; } - /// @brief the capacity of the stored array, or unknown_size() - /// Only JSON_DIAGNOSTICS uses the value, to detect a reallocation that - /// would invalidate the parent pointers. Array types that do not have a - /// capacity() member function report unknown_size(), which is treated as - /// "the elements may have moved". + /// @brief the capacity of the stored array or object, or unknown_size() + /// Only JSON_DIAGNOSTICS uses the value, to detect an insertion that may + /// have moved the other elements and so invalidated their parent pointers. + /// unknown_size() means "the elements may have moved". Arrays only grow at + /// the end, so their capacity tells whether they reallocated; arrays + /// without a capacity() member report unknown_size(). For objects, only + /// types known to keep their elements in place (std::map and the other + /// node-based standard containers) report a fixed 0, and only + /// nlohmann::ordered_map, which appends members, reports its capacity; any + /// other object type (a sorted flat map, say) reports unknown_size(). #if JSON_DIAGNOSTICS template < typename A = array_t, detail::enable_if_t < detail::has_capacity::value, int > = 0 > std::size_t array_capacity() const noexcept @@ -1091,57 +1191,63 @@ public: { return detail::unknown_size(); } + + template < typename O = object_t, detail::enable_if_t < detail::has_stable_elements::value, int > = 0 > + std::size_t object_capacity() const noexcept + { + return 0; + } + + template < typename O = object_t, detail::enable_if_t < !detail::has_stable_elements::value && detail::is_nlohmann_ordered_map::value, int > = 0 > + std::size_t object_capacity() const noexcept + { + return m_data.m_value.object->capacity(); + } + + template < typename O = object_t, detail::enable_if_t < !detail::has_stable_elements::value && !detail::is_nlohmann_ordered_map::value, int > = 0 > + std::size_t object_capacity() const noexcept + { + return detail::unknown_size(); + } + + std::size_t container_capacity() const noexcept + { + return is_array() ? array_capacity() : object_capacity(); + } #else - static constexpr std::size_t array_capacity() noexcept + static constexpr std::size_t container_capacity() noexcept { return detail::unknown_size(); } #endif - /// @brief set the parent of a value that has just been added to an array - /// @param j the added value - /// @param old_capacity the value @ref array_capacity() returned before the + /// @brief set the parent of a value that has just been added to an array or object + /// @param j the added (or looked-up) value + /// @param old_capacity the value @ref container_capacity() returned before the /// insertion - reference set_parent_after_array_insert(reference j, std::size_t old_capacity) + reference set_parent_after_insert(reference j, std::size_t old_capacity) { #if JSON_DIAGNOSTICS // see https://github.com/nlohmann/json/issues/2838 - JSON_ASSERT(type() == value_t::array); if (JSON_HEDLEY_UNLIKELY(old_capacity == detail::unknown_size() - || array_capacity() != old_capacity)) + || container_capacity() != old_capacity)) { - // the capacity has changed, or the array type does not let us tell: - // the elements may have moved, so update all parents + // the capacity has changed, or the container type does not let us + // tell: the elements may have moved, so update all parents set_parents(); return j; } #else static_cast(old_capacity); #endif - return set_parent(j); + return set_parent_at(j); } - reference set_parent(reference j) + /// @brief set the parent of an element accessed via at(); never moves elements + reference set_parent_at(reference j) noexcept { #if JSON_DIAGNOSTICS - // ordered_json uses a vector internally, so pointers could have - // been invalidated; see https://github.com/nlohmann/json/issues/2962 -#ifdef JSON_HEDLEY_MSVC_VERSION -#pragma warning(push ) -#pragma warning(disable : 4127) // ignore warning to replace if with if constexpr -#endif - if (detail::is_ordered_map::value) - { - set_parents(); - return j; - } -#ifdef JSON_HEDLEY_MSVC_VERSION -#pragma warning( pop ) -#endif - j.m_parent = this; -#else - static_cast(j); #endif return j; } @@ -1421,6 +1527,24 @@ public: return create(first, last); } + /// @brief compare two object keys for equality, if the key type supports it + /// @note object_t only needs operator< for its keys (std::map), so operator== + /// may not exist; the keys are then reported as different, which makes + /// copy_object_level pair the values via object_t::find() + template::value, int> = 0> + static bool copy_keys_equal(const K& a, const K& b) + { + return a == b; + } + + template < typename K = typename object_t::key_type, + detail::enable_if_t < !detail::is_detected::value, int > = 0 > + static bool copy_keys_equal(const K& /*a*/, const K& /*b*/) + { + return false; + } + /// @brief create the copy of the object @a src in @a dst /// @note structured values are appended to @a worklist instead static void copy_object_level(const basic_json& src, basic_json& dst, @@ -1453,7 +1577,7 @@ public: auto src_it = src_object.cbegin(); for (auto& element : *dst.m_data.m_value.object) { - if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && src_it->first == element.first)) + if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && copy_keys_equal(src_it->first, element.first))) { copy_shallow(src_it->second, element.second, worklist); ++src_it; @@ -2113,9 +2237,11 @@ public: /// @brief restore the parent pointers after erasing from an object - /// ordered_json keeps its members in a vector, and erasing a member - /// re-constructs every member after it in place, which resets their - /// parent pointers + /// Unless the object type is known to keep its elements in place (see + /// detail::has_stable_elements), erasing a member may move the members + /// after it; ordered_json, for instance, keeps its members in a vector + /// and re-constructs every member after the erased one in place, which + /// resets their parent pointers void set_parents_after_object_erase() { #if JSON_DIAGNOSTICS @@ -2123,7 +2249,7 @@ public: #pragma warning(push ) #pragma warning(disable : 4127) // ignore warning to replace if with if constexpr #endif - if (detail::is_ordered_map::value) + if (!detail::has_stable_elements::value) { set_parents(); } @@ -2158,7 +2284,7 @@ public: /// @brief create an empty value with a given type /// @sa https://json.nlohmann.me/api/basic_json/basic_json/ basic_json(const value_t v) - : m_data(v) + : m_data(stored_kind(v)) { assert_invariant(); } @@ -2239,7 +2365,7 @@ public: { convert_leaf(val); } - JSON_ASSERT(m_data.m_type == val.type()); + JSON_ASSERT(m_data.m_type == stored_kind(val.type())); set_parents(); assert_invariant(); @@ -2287,7 +2413,7 @@ public: // if an object is wanted but impossible, throw an exception if (JSON_HEDLEY_UNLIKELY(manual_type == value_t::object && !is_an_object)) { - JSON_THROW(type_error::create(301, "cannot create object from initializer list", nullptr)); + detail::throw_literal(detail::exception_id::object_from_non_pairs, "cannot create object from initializer list", nullptr); } } @@ -2407,7 +2533,7 @@ public: // make sure the iterator fits the current value if (JSON_HEDLEY_UNLIKELY(first.m_object != last.m_object)) { - JSON_THROW(invalid_iterator::create(201, "iterators are not compatible", nullptr)); + detail::throw_literal(detail::exception_id::iterators_incompatible, "iterators are not compatible", nullptr); } // copy type from the first iterator @@ -2423,10 +2549,9 @@ public: case value_t::string: case value_t::binary: { - if (JSON_HEDLEY_UNLIKELY(!first.m_it.primitive_iterator.is_begin() - || !last.m_it.primitive_iterator.is_end())) + if (JSON_HEDLEY_UNLIKELY(!primitive_range_is_complete(first, last))) { - JSON_THROW(invalid_iterator::create(204, "iterators out of range", first.m_object)); + detail::throw_literal(detail::exception_id::iterator_range_out_of_range, "iterators out of range", first.m_object); } break; } @@ -2494,7 +2619,7 @@ public: case value_t::null: case value_t::discarded: default: - JSON_THROW(invalid_iterator::create(206, detail::concat("cannot construct with iterators from ", first.m_object->type_name()), first.m_object)); + throw_iterator_range_of_null(first.m_object); } set_parents(); @@ -2523,18 +2648,16 @@ public: // check of passed value is valid other.assert_invariant(); - if (m_data.m_type == value_t::object || m_data.m_type == value_t::array) + if (JSON_HEDLEY_LIKELY(!detail::owns_memory(m_data.m_type))) { - // copying the container directly would call this constructor again - // for every element, once per nesting level - copy_structured(other); + // null, boolean, numbers, discarded: plain copy of the union + m_data.m_value = other.m_data.m_value; } else { - copy_leaf_value(other, *this); + copy_owning(other); } - set_parents(); assert_invariant(); } @@ -2764,6 +2887,22 @@ public: // value access // ////////////////// + /// out-of-line part of the copy constructor for owning types + JSON_HEDLEY_NEVER_INLINE + void copy_owning(const basic_json& other) + { + if (m_data.m_type == value_t::object || m_data.m_type == value_t::array) + { + // copying the container directly would call this constructor again + // for every element, once per nesting level + copy_structured(other); + } + else + { + copy_leaf_value(other, *this); + } + } + /// get a pointer to the value (object) object_t* get_impl_ptr(object_t* /*unused*/) noexcept { @@ -2824,14 +2963,16 @@ public: return m_data.m_type == value_t::number_integer ? &m_data.m_value.number_integer : nullptr; } - /// get a pointer to the value (unsigned number) - number_unsigned_t* get_impl_ptr(number_unsigned_t* /*unused*/) noexcept + /// get a pointer to the value (unsigned number); takes a placeholder if + /// number_unsigned_t is number_integer_t (the integer overload handles it then) + number_unsigned_t* get_impl_ptr(detail::number_unsigned_overload_type* /*unused*/) noexcept { return is_number_unsigned() ? &m_data.m_value.number_unsigned : nullptr; } - /// get a pointer to the value (unsigned number) - constexpr const number_unsigned_t* get_impl_ptr(const number_unsigned_t* /*unused*/) const noexcept + /// get a pointer to the value (unsigned number); takes a placeholder if + /// number_unsigned_t is number_integer_t (the integer overload handles it then) + constexpr const number_unsigned_t* get_impl_ptr(const detail::number_unsigned_overload_type* /*unused*/) const noexcept { return is_number_unsigned() ? &m_data.m_value.number_unsigned : nullptr; } @@ -2860,6 +3001,175 @@ public: return is_binary() ? m_data.m_value.binary : nullptr; } + /// out-of-line throw helpers (message construction kept out of the hot paths) + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_iterator_range_of_null(const basic_json* obj) + { + static_cast(obj); // unused when JSON_NOEXCEPTION is defined + JSON_THROW(invalid_iterator::create(detail::exception_id::iterator_range_of_null, detail::concat("cannot construct with iterators from ", obj->type_name()), obj)); + } + + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_incompatible_reference_type(const basic_json& obj) + { + static_cast(obj); // unused when JSON_NOEXCEPTION is defined + JSON_THROW(type_error::create(detail::exception_id::incompatible_reference_type, detail::concat("incompatible ReferenceType for get_ref, actual type is ", obj.type_name()), &obj)); + } + + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_array_index_exceeds_size_type(size_type idx) + { + static_cast(idx); // unused when JSON_NOEXCEPTION is defined + JSON_THROW(std::length_error(detail::concat("array index ", std::to_string(idx), " exceeds size_type"))); + } + + /// @throw type_error.309 always, as insert() is not defined for the type of @a j + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_insert_wrong_type(const basic_json& j) + { + detail::throw_cannot_use_with(detail::exception_id::insert_wrong_type, "insert()", j); + } + + /// @throw invalid_iterator.202 always, as an iterator belongs to another value than @a obj + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_iterator_from_other_value(const basic_json* obj) + { + detail::throw_literal(detail::exception_id::iterator_from_other_value, "iterator does not fit current value", obj); + } + + /// @throw type_error.310 always, as swap() with the given operand is not defined for the type of @a j + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_swap_wrong_type(const char* operation, const basic_json& j) + { + detail::throw_cannot_use_with(detail::exception_id::swap_wrong_type, operation, j); + } + + /// exchange the stored @a Stored value (if the value has that type) with @a other + template + void swap_impl(Other& other, const char* operation) + { + Stored* const stored = get_impl_ptr(static_cast(nullptr)); + if (JSON_HEDLEY_UNLIKELY(stored == nullptr)) + { + throw_swap_wrong_type(operation, *this); + } + + using std::swap; + swap(*stored, other); + } + + /// whether [first, last) spans the whole of a primitive value + template + JSON_HEDLEY_ALWAYS_INLINE + static bool primitive_range_is_complete(const IteratorType& first, const IteratorType& last) noexcept + { + return first.m_it.primitive_iterator.is_begin() && last.m_it.primitive_iterator.is_end(); + } + + /// destroy the current (primitive) value and become null + void reset_to_null_impl() + { + m_data.m_value.destroy(m_data.m_type); + m_data.m_value = {}; + m_data.m_type = value_t::null; + assert_invariant(); + } + + /// append @a val (moved or copied) to the array; a null value becomes an array + template + JSON_HEDLEY_ALWAYS_INLINE + void push_back_array_impl(ValueType&& val) + { + // push_back only works for null objects or arrays + if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_array()))) + { + detail::throw_cannot_use_with(detail::exception_id::push_back_wrong_type, "push_back()", *this); + } + + // transform a null object into an array + if (is_null()) + { + convert_null_to(); + } + + // add the element to the array + const auto old_capacity = container_capacity(); + m_data.m_value.array->push_back(std::forward(val)); + set_parent_after_insert(m_data.m_value.array->back(), old_capacity); + } + + /// parse the input behind an already created adapter + template + static basic_json parse_impl(InputAdapterType&& adapter, + parser_callback_t cb, + const bool allow_exceptions, + const bool ignore_comments, + const bool ignore_trailing_commas) + { + basic_json result; + parser(std::forward(adapter), std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas).parse(true, result); + return result; + } + + /// check the input behind an already created adapter + template + static bool accept_impl(InputAdapterType&& adapter, + const bool ignore_comments, + const bool ignore_trailing_commas) + { + return parser(std::forward(adapter), nullptr, false, ignore_comments, ignore_trailing_commas, true).accept(true); + } + + /// generate SAX events from the input behind an already created adapter + template + static bool sax_parse_impl(InputAdapterType ia, SAX* sax, + const input_format_t format, + const bool strict, + const bool ignore_comments, + const bool ignore_trailing_commas, + const cbor_tag_handler_t tag_handler) + { + if (format == input_format_t::json) + { + return parser(std::move(ia), nullptr, true, ignore_comments, ignore_trailing_commas).sax_parse(sax, strict); + } + return detail::binary_reader(std::move(ia), format).sax_parse(sax, strict, tag_handler); + } + + /// serialize @a j into a new vector; @a write receives the vector-backed writer + template + static std::vector to_binary_vector(const basic_json& j, const error_handler_t error_handler, WriteFunction write) + { + std::vector result; + result.reserve(detail::binary_initial_capacity(j)); + { + // the sink sets the size of result when it is destroyed, which + // must happen before result is returned (without NRVO, it is moved) + auto writer = vector_writer(result, error_handler); + write(writer); + } + return result; + } + + /// @throw out_of_range.401 always, as @a idx is out of range of @a j + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_array_index_out_of_range(size_type idx, const basic_json& j) + { + static_cast(idx); // unused when JSON_NOEXCEPTION is defined + static_cast(j); + JSON_THROW(out_of_range::create(detail::exception_id::array_index_out_of_range, detail::concat("array index ", std::to_string(idx), " is out of range"), &j)); + } + + /// @throw out_of_range.403 always, as @a key is not found in @a j + template + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_key_not_found(const KeyType& key, const basic_json& j) + { + static_cast(key); // unused when JSON_NOEXCEPTION is defined + static_cast(j); + JSON_THROW(out_of_range::create(detail::exception_id::key_not_found, detail::concat("key '", key, "' not found"), &j)); + } + /*! @brief helper function to implement get_ref() @@ -2882,7 +3192,7 @@ public: return *ptr; } - JSON_THROW(type_error::create(303, detail::concat("incompatible ReferenceType for get_ref, actual type is ", obj.type_name()), &obj)); + throw_incompatible_reference_type(obj); } public: @@ -3266,7 +3576,7 @@ public: { if (!is_binary()) { - JSON_THROW(type_error::create(302, detail::concat("type must be binary, but is ", type_name()), this)); + detail::throw_type_must_be("binary", *this); } return *get_ptr(); @@ -3278,7 +3588,7 @@ public: { if (!is_binary()) { - JSON_THROW(type_error::create(302, detail::concat("type must be binary, but is ", type_name()), this)); + detail::throw_type_must_be("binary", *this); } return *get_ptr(); @@ -3320,7 +3630,7 @@ public: // at only works for objects if (JSON_HEDLEY_UNLIKELY(!j.is_object())) { - JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", j.type_name()), &j)); + detail::throw_cannot_use_with(detail::exception_id::at_wrong_type, "at()", j); } auto it = object_lookup(j, std::forward(key)); @@ -3330,11 +3640,28 @@ public: // std::map or ordered_map) never moves from its argument, so key is still // valid here regardless of whether KeyType was deduced as an rvalue reference // NOLINTNEXTLINE(bugprone-use-after-move,hicpp-invalid-access-moved) - JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(key), "' not found"), &j)); + throw_key_not_found(key_for_message(key), j); } return it->second; } + /// @brief key as it is passed to detail::concat for an error message + /// @note keys with data() and size() (such as string_t itself or a string + /// view) are passed through unchanged, so a miss does not copy them; + /// other keys (such as string literals or key types that only convert + /// to string_t) are converted to string_t + template < typename KeyType, detail::enable_if_t < detail::detect_string_can_append_data::value, int > = 0 > + static const KeyType & key_for_message(const KeyType& key) + { + return key; // NOLINT(bugprone-return-const-ref-from-parameter): the result is only passed to concat() within the full-expression that holds key + } + + template < typename KeyType, detail::enable_if_t < !detail::detect_string_can_append_data::value, int > = 0 > + static string_t key_for_message(const KeyType& key) + { + return string_t(key); + } + /// @brief checked array element access used by the at() overloads taking an index /// @throw type_error.304 if @a j is not an array /// @throw out_of_range.401 if @a idx is out of range @@ -3345,12 +3672,12 @@ public: // at only works for arrays if (JSON_HEDLEY_UNLIKELY(!j.is_array())) { - JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", j.type_name()), &j)); + detail::throw_cannot_use_with(detail::exception_id::at_wrong_type, "at()", j); } if (JSON_HEDLEY_UNLIKELY(idx >= j.m_data.m_value.array->size())) { - JSON_THROW(out_of_range::create(401, detail::concat("array index ", std::to_string(idx), " is out of range"), &j)); + throw_array_index_out_of_range(idx, j); } return (*j.m_data.m_value.array)[idx]; @@ -3383,6 +3710,15 @@ public: m_data.m_type = value_t::object; } + /// @brief throws because operator[] is not supported for the type of this value + /// @param[in] argument the kind of the operator's argument, "numeric" or "string" + /// @throw type_error.305 always + JSON_HEDLEY_NO_RETURN void throw_subscript_wrong_type(const char* argument) const + { + detail::throw_cannot_use_with(detail::exception_id::subscript_wrong_type, + detail::concat("operator[] with a ", argument, " argument").c_str(), *this); + } + public: //////////////////// // element access // @@ -3396,7 +3732,7 @@ public: /// @sa https://json.nlohmann.me/api/basic_json/at/ reference at(size_type idx) { - return set_parent(array_at(*this, idx)); + return set_parent_at(array_at(*this, idx)); } /// @brief access specified array element with bounds checking @@ -3410,7 +3746,7 @@ public: /// @sa https://json.nlohmann.me/api/basic_json/at/ reference at(const typename object_t::key_type& key) { - return set_parent(object_at(*this, key)); + return set_parent_at(object_at(*this, key)); } /// @brief access specified object element with bounds checking @@ -3419,7 +3755,7 @@ public: detail::is_usable_as_basic_json_key_type::value, int> = 0> reference at(KeyType && key) { - return set_parent(object_at(*this, std::forward(key))); + return set_parent_at(object_at(*this, std::forward(key))); } /// @brief access specified object element with bounds checking @@ -3459,18 +3795,18 @@ public: // resize() rejects other indices that are too large to represent if (JSON_HEDLEY_UNLIKELY(idx == (std::numeric_limits::max)())) { - JSON_THROW(std::length_error(detail::concat("array index ", std::to_string(idx), " exceeds size_type"))); + throw_array_index_exceeds_size_type(idx); } #if JSON_DIAGNOSTICS // remember array size & capacity before resizing const auto old_size = m_data.m_value.array->size(); - const auto old_capacity = array_capacity(); + const auto old_capacity = container_capacity(); #endif m_data.m_value.array->resize(idx + 1); #if JSON_DIAGNOSTICS if (JSON_HEDLEY_UNLIKELY(old_capacity == detail::unknown_size() - || array_capacity() != old_capacity)) + || container_capacity() != old_capacity)) { // capacity has changed: update all parents set_parents(); @@ -3487,7 +3823,7 @@ public: return m_data.m_value.array->operator[](idx); } - JSON_THROW(type_error::create(305, detail::concat("cannot use operator[] with a numeric argument with ", type_name()), this)); + throw_subscript_wrong_type("numeric"); } /// @brief access specified array element @@ -3501,7 +3837,7 @@ public: return m_data.m_value.array->operator[](idx); } - JSON_THROW(type_error::create(305, detail::concat("cannot use operator[] with a numeric argument with ", type_name()), this)); + throw_subscript_wrong_type("numeric"); } /// @brief access specified object element @@ -3517,11 +3853,12 @@ public: // operator[] only works for objects if (JSON_HEDLEY_LIKELY(is_object())) { + const auto old_capacity = container_capacity(); auto result = m_data.m_value.object->emplace(std::move(key), nullptr); - return set_parent(result.first->second); + return set_parent_after_insert(result.first->second, old_capacity); } - JSON_THROW(type_error::create(305, detail::concat("cannot use operator[] with a string argument with ", type_name()), this)); + throw_subscript_wrong_type("string"); } /// @brief access specified object element @@ -3536,7 +3873,7 @@ public: return it->second; } - JSON_THROW(type_error::create(305, detail::concat("cannot use operator[] with a string argument with ", type_name()), this)); + throw_subscript_wrong_type("string"); } // these two functions resolve a (const) char * ambiguity affecting Clang and MSVC @@ -3568,11 +3905,12 @@ public: // operator[] only works for objects if (JSON_HEDLEY_LIKELY(is_object())) { + const auto old_capacity = container_capacity(); auto result = m_data.m_value.object->emplace(lookup_key(std::forward(key)), nullptr); - return set_parent(result.first->second); + return set_parent_after_insert(result.first->second, old_capacity); } - JSON_THROW(type_error::create(305, detail::concat("cannot use operator[] with a string argument with ", type_name()), this)); + throw_subscript_wrong_type("string"); } /// @brief access specified object element @@ -3589,7 +3927,7 @@ public: return it->second; } - JSON_THROW(type_error::create(305, detail::concat("cannot use operator[] with a string argument with ", type_name()), this)); + throw_subscript_wrong_type("string"); } private: @@ -3613,7 +3951,7 @@ public: // value only works for objects if (JSON_HEDLEY_UNLIKELY(!is_object())) { - JSON_THROW(type_error::create(306, detail::concat("cannot use value() with ", type_name()), this)); + detail::throw_cannot_use_with(detail::exception_id::value_wrong_type, "value()", *this); } const auto it = find(std::forward(key)); @@ -3628,7 +3966,7 @@ public: // value only works for arrays and objects if (JSON_HEDLEY_UNLIKELY(!is_structured())) { - JSON_THROW(type_error::create(306, detail::concat("cannot use value() with ", type_name()), this)); + detail::throw_cannot_use_with(detail::exception_id::value_wrong_type, "value()", *this); } return ptr.get_checked_or_null(this); @@ -3795,7 +4133,7 @@ public: // make sure the iterator fits the current value if (JSON_HEDLEY_UNLIKELY(this != pos.m_object)) { - JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value", this)); + throw_iterator_from_other_value(this); } IteratorType result = end(); @@ -3811,13 +4149,10 @@ public: { if (JSON_HEDLEY_UNLIKELY(!pos.m_it.primitive_iterator.is_begin())) { - JSON_THROW(invalid_iterator::create(205, "iterator out of range", this)); + detail::throw_literal(detail::exception_id::iterator_out_of_range, "iterator out of range", this); } - m_data.m_value.destroy(m_data.m_type); - m_data.m_value = {}; - m_data.m_type = value_t::null; - assert_invariant(); + reset_to_null_impl(); break; } @@ -3837,7 +4172,7 @@ public: case value_t::null: case value_t::discarded: default: - JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this)); + detail::throw_cannot_use_with(detail::exception_id::erase_wrong_type, "erase()", *this); } return result; @@ -3853,7 +4188,7 @@ public: // make sure the iterator fits the current value if (JSON_HEDLEY_UNLIKELY(this != first.m_object || this != last.m_object)) { - JSON_THROW(invalid_iterator::create(203, "iterators do not fit current value", this)); + detail::throw_literal(detail::exception_id::iterator_range_from_other_value, "iterators do not fit current value", this); } IteratorType result = end(); @@ -3867,16 +4202,12 @@ public: case value_t::string: case value_t::binary: { - if (JSON_HEDLEY_UNLIKELY(!first.m_it.primitive_iterator.is_begin() - || !last.m_it.primitive_iterator.is_end())) + if (JSON_HEDLEY_UNLIKELY(!primitive_range_is_complete(first, last))) { - JSON_THROW(invalid_iterator::create(204, "iterators out of range", this)); + detail::throw_literal(detail::exception_id::iterator_range_out_of_range, "iterators out of range", this); } - m_data.m_value.destroy(m_data.m_type); - m_data.m_value = {}; - m_data.m_type = value_t::null; - assert_invariant(); + reset_to_null_impl(); break; } @@ -3898,7 +4229,7 @@ public: case value_t::null: case value_t::discarded: default: - JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this)); + detail::throw_cannot_use_with(detail::exception_id::erase_wrong_type, "erase()", *this); } return result; @@ -3912,7 +4243,7 @@ public: // this erase only works for objects if (JSON_HEDLEY_UNLIKELY(!is_object())) { - JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this)); + detail::throw_cannot_use_with(detail::exception_id::erase_wrong_type, "erase()", *this); } const auto erased = m_data.m_value.object->erase(std::forward(key)); @@ -3927,7 +4258,7 @@ public: // this erase only works for objects if (JSON_HEDLEY_UNLIKELY(!is_object())) { - JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this)); + detail::throw_cannot_use_with(detail::exception_id::erase_wrong_type, "erase()", *this); } const auto it = object_lookup(*this, std::forward(key)); @@ -3969,14 +4300,14 @@ public: { if (JSON_HEDLEY_UNLIKELY(idx >= size())) { - JSON_THROW(out_of_range::create(401, detail::concat("array index ", std::to_string(idx), " is out of range"), this)); + throw_array_index_out_of_range(idx, *this); } m_data.m_value.array->erase(m_data.m_value.array->begin() + static_cast(idx)); } else { - JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this)); + detail::throw_cannot_use_with(detail::exception_id::erase_wrong_type, "erase()", *this); } } @@ -4357,34 +4688,20 @@ public: JSON_HEDLEY_WARN_UNUSED_RESULT size_type max_size() const noexcept { - switch (m_data.m_type) + if (m_data.m_type == value_t::array) { - case value_t::array: - { - // delegate call to array_t::max_size() - return m_data.m_value.array->max_size(); - } - - case value_t::object: - { - // delegate call to object_t::max_size() - return m_data.m_value.object->max_size(); - } - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - { - // all other types have max_size() == size() - return size(); - } + // delegate call to array_t::max_size() + return m_data.m_value.array->max_size(); } + + if (m_data.m_type == value_t::object) + { + // delegate call to object_t::max_size() + return m_data.m_value.object->max_size(); + } + + // all other types have max_size() == size() + return size(); } /// @} @@ -4462,22 +4779,7 @@ public: /// @sa https://json.nlohmann.me/api/basic_json/push_back/ void push_back(basic_json&& val) { - // push_back only works for null objects or arrays - if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_array()))) - { - JSON_THROW(type_error::create(308, detail::concat("cannot use push_back() with ", type_name()), this)); - } - - // transform a null object into an array - if (is_null()) - { - convert_null_to(); - } - - // add the element to the array (move semantics) - const auto old_capacity = array_capacity(); - m_data.m_value.array->push_back(std::move(val)); - set_parent_after_array_insert(m_data.m_value.array->back(), old_capacity); + push_back_array_impl(std::move(val)); // if val is moved from, basic_json move constructor marks it null, so we do not call the destructor } @@ -4493,22 +4795,7 @@ public: /// @sa https://json.nlohmann.me/api/basic_json/push_back/ void push_back(const basic_json& val) { - // push_back only works for null objects or arrays - if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_array()))) - { - JSON_THROW(type_error::create(308, detail::concat("cannot use push_back() with ", type_name()), this)); - } - - // transform a null object into an array - if (is_null()) - { - convert_null_to(); - } - - // add the element to the array - const auto old_capacity = array_capacity(); - m_data.m_value.array->push_back(val); - set_parent_after_array_insert(m_data.m_value.array->back(), old_capacity); + push_back_array_impl(val); } /// @brief add an object to an array @@ -4526,7 +4813,7 @@ public: // push_back only works for null objects or objects if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_object()))) { - JSON_THROW(type_error::create(308, detail::concat("cannot use push_back() with ", type_name()), this)); + detail::throw_cannot_use_with(detail::exception_id::push_back_wrong_type, "push_back()", *this); } // transform a null object into an object @@ -4536,8 +4823,9 @@ public: } // add the element to the object + const auto old_capacity = container_capacity(); auto res = m_data.m_value.object->insert(val); - set_parent(res.first->second); + set_parent_after_insert(res.first->second, old_capacity); } /// @brief add an object to an object @@ -4580,7 +4868,7 @@ public: // emplace_back only works for null objects or arrays if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_array()))) { - JSON_THROW(type_error::create(311, detail::concat("cannot use emplace_back() with ", type_name()), this)); + detail::throw_cannot_use_with(detail::exception_id::emplace_wrong_type, "emplace_back()", *this); } // transform a null object into an array @@ -4590,9 +4878,9 @@ public: } // add the element to the array (perfect forwarding) - const auto old_capacity = array_capacity(); + const auto old_capacity = container_capacity(); m_data.m_value.array->emplace_back(std::forward(args)...); - return set_parent_after_array_insert(m_data.m_value.array->back(), old_capacity); + return set_parent_after_insert(m_data.m_value.array->back(), old_capacity); } /// @brief add an object to an object if key does not exist @@ -4603,7 +4891,7 @@ public: // emplace only works for null objects or arrays if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_object()))) { - JSON_THROW(type_error::create(311, detail::concat("cannot use emplace() with ", type_name()), this)); + detail::throw_cannot_use_with(detail::exception_id::emplace_wrong_type, "emplace()", *this); } // transform a null object into an object @@ -4613,8 +4901,9 @@ public: } // add the element to the array (perfect forwarding) + const auto old_capacity = container_capacity(); auto res = m_data.m_value.object->emplace(std::forward(args)...); - set_parent(res.first->second); + set_parent_after_insert(res.first->second, old_capacity); // create a result iterator and set iterator to the result of emplace auto it = begin(); @@ -4650,19 +4939,19 @@ public: iterator insert(const_iterator pos, const basic_json& val) // NOLINT(performance-unnecessary-value-param) { // insert only works for arrays - if (JSON_HEDLEY_LIKELY(is_array())) + if (JSON_HEDLEY_UNLIKELY(!is_array())) { - // check if iterator pos fits to this JSON value - if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) - { - JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value", this)); - } - - // insert to array and return iterator - return insert_iterator(pos, val); + throw_insert_wrong_type(*this); } - JSON_THROW(type_error::create(309, detail::concat("cannot use insert() with ", type_name()), this)); + // check if iterator pos fits to this JSON value + if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) + { + throw_iterator_from_other_value(this); + } + + // insert to array and return iterator + return insert_iterator(pos, val); } /// @brief inserts element into array @@ -4670,21 +4959,21 @@ public: iterator insert(const_iterator pos, basic_json&& val) // NOLINT(performance-unnecessary-value-param) { // insert only works for arrays - if (JSON_HEDLEY_LIKELY(is_array())) + if (JSON_HEDLEY_UNLIKELY(!is_array())) { - // check if iterator pos fits to this JSON value - if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) - { - JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value", this)); - } - - // moving into a local first keeps this safe even if val aliases - // an element of this array - basic_json tmp(std::move(val)); - return insert_iterator(pos, std::move(tmp)); + throw_insert_wrong_type(*this); } - JSON_THROW(type_error::create(309, detail::concat("cannot use insert() with ", type_name()), this)); + // check if iterator pos fits to this JSON value + if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) + { + throw_iterator_from_other_value(this); + } + + // moving into a local first keeps this safe even if val aliases + // an element of this array + basic_json tmp(std::move(val)); + return insert_iterator(pos, std::move(tmp)); } /// @brief inserts copies of element into array @@ -4692,19 +4981,19 @@ public: iterator insert(const_iterator pos, size_type cnt, const basic_json& val) // NOLINT(performance-unnecessary-value-param) { // insert only works for arrays - if (JSON_HEDLEY_LIKELY(is_array())) + if (JSON_HEDLEY_UNLIKELY(!is_array())) { - // check if iterator pos fits to this JSON value - if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) - { - JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value", this)); - } - - // insert to array and return iterator - return insert_iterator(pos, cnt, val); + throw_insert_wrong_type(*this); } - JSON_THROW(type_error::create(309, detail::concat("cannot use insert() with ", type_name()), this)); + // check if iterator pos fits to this JSON value + if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) + { + throw_iterator_from_other_value(this); + } + + // insert to array and return iterator + return insert_iterator(pos, cnt, val); } /// @brief inserts range of elements into array @@ -4714,30 +5003,30 @@ public: // insert only works for arrays if (JSON_HEDLEY_UNLIKELY(!is_array())) { - JSON_THROW(type_error::create(309, detail::concat("cannot use insert() with ", type_name()), this)); + throw_insert_wrong_type(*this); } // check if iterator pos fits to this JSON value if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) { - JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value", this)); + throw_iterator_from_other_value(this); } // check if range iterators belong to the same JSON object if (JSON_HEDLEY_UNLIKELY(first.m_object != last.m_object)) { - JSON_THROW(invalid_iterator::create(210, "iterators do not fit", this)); + detail::throw_literal(detail::exception_id::insert_range_incompatible, "iterators do not fit", this); } if (JSON_HEDLEY_UNLIKELY(first.m_object == this)) { - JSON_THROW(invalid_iterator::create(211, "passed iterators may not belong to container", this)); + detail::throw_literal(detail::exception_id::insert_range_into_itself, "passed iterators may not belong to container", this); } // passed iterators must belong to arrays if (JSON_HEDLEY_UNLIKELY(!first.m_object->is_array())) { - JSON_THROW(invalid_iterator::create(202, "iterators first and last must point to arrays", this)); + detail::throw_literal(detail::exception_id::iterator_from_other_value, "iterators first and last must point to arrays", this); } // insert to array and return iterator @@ -4751,13 +5040,13 @@ public: // insert only works for arrays if (JSON_HEDLEY_UNLIKELY(!is_array())) { - JSON_THROW(type_error::create(309, detail::concat("cannot use insert() with ", type_name()), this)); + throw_insert_wrong_type(*this); } // check if iterator pos fits to this JSON value if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) { - JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value", this)); + throw_iterator_from_other_value(this); } // copy the values first: ilist may refer to elements of this array @@ -4779,19 +5068,19 @@ public: // insert only works for objects if (JSON_HEDLEY_UNLIKELY(!is_object())) { - JSON_THROW(type_error::create(309, detail::concat("cannot use insert() with ", type_name()), this)); + throw_insert_wrong_type(*this); } // check if range iterators belong to the same JSON object if (JSON_HEDLEY_UNLIKELY(first.m_object != last.m_object)) { - JSON_THROW(invalid_iterator::create(210, "iterators do not fit", this)); + detail::throw_literal(detail::exception_id::insert_range_incompatible, "iterators do not fit", this); } // passed iterators must belong to objects if (JSON_HEDLEY_UNLIKELY(!first.m_object->is_object())) { - JSON_THROW(invalid_iterator::create(202, "iterators first and last must point to objects", this)); + detail::throw_literal(detail::exception_id::iterator_from_other_value, "iterators first and last must point to objects", this); } m_data.m_value.object->insert(first.m_it.object_iterator, last.m_it.object_iterator); @@ -4808,7 +5097,7 @@ public: // j, not the copy made below) if (JSON_HEDLEY_UNLIKELY(!j.is_object())) { - JSON_THROW(type_error::create(312, detail::concat("cannot use update() with ", j.type_name()), &j)); + detail::throw_cannot_use_with(detail::exception_id::update_wrong_type, "update()", j); } // copy first: j may be *this or one of its descendants, and is @@ -4826,13 +5115,13 @@ public: // check if range iterators belong to the same JSON object if (JSON_HEDLEY_UNLIKELY(first.m_object != last.m_object)) { - JSON_THROW(invalid_iterator::create(210, "iterators do not fit", this)); + detail::throw_literal(detail::exception_id::insert_range_incompatible, "iterators do not fit", this); } // passed iterators must belong to objects if (JSON_HEDLEY_UNLIKELY(!first.m_object->is_object())) { - JSON_THROW(type_error::create(312, detail::concat("cannot use update() with ", first.m_object->type_name()), first.m_object)); + detail::throw_cannot_use_with(detail::exception_id::update_wrong_type, "update()", *first.m_object); } // copy first: the range may belong to *this or one of its @@ -4868,7 +5157,7 @@ public: if (JSON_HEDLEY_UNLIKELY(!is_object())) { - JSON_THROW(type_error::create(312, detail::concat("cannot use update() with ", type_name()), this)); + detail::throw_cannot_use_with(detail::exception_id::update_wrong_type, "update()", *this); } } @@ -4913,9 +5202,9 @@ public: continue; } } - // set_parent() also repairs the other members, which ordered_json - // relocates when adding a key makes its vector grow - set_parent(m_data.m_value.object->operator[](it.key()) = std::move(it.value())); + // operator[] repairs the other members if the object type moves + // them; assigning through it keeps this O(1) per key + operator[](it.key()) = std::move(it.value()); } } @@ -4965,9 +5254,8 @@ public: continue; } } - // set_parent() also repairs the other members, which ordered_json - // relocates when adding a key makes its vector grow - target->set_parent(target->m_data.m_value.object->operator[](first.key()) = std::move(first.value())); + // operator[] repairs the other members if the object type moves them + target->operator[](first.key()) = std::move(first.value()); ++first; } } @@ -5022,82 +5310,37 @@ public: /// @sa https://json.nlohmann.me/api/basic_json/swap/ void swap(array_t& other) // NOLINT(bugprone-exception-escape,cppcoreguidelines-noexcept-swap,performance-noexcept-swap) { - // swap only works for arrays - if (JSON_HEDLEY_LIKELY(is_array())) - { - using std::swap; - swap(*(m_data.m_value.array), other); - set_parents(); - } - else - { - JSON_THROW(type_error::create(310, detail::concat("cannot use swap(array_t&) with ", type_name()), this)); - } + swap_impl(other, "swap(array_t&)"); + set_parents(); } /// @brief exchanges the values /// @sa https://json.nlohmann.me/api/basic_json/swap/ void swap(object_t& other) // NOLINT(bugprone-exception-escape,cppcoreguidelines-noexcept-swap,performance-noexcept-swap) { - // swap only works for objects - if (JSON_HEDLEY_LIKELY(is_object())) - { - using std::swap; - swap(*(m_data.m_value.object), other); - set_parents(); - } - else - { - JSON_THROW(type_error::create(310, detail::concat("cannot use swap(object_t&) with ", type_name()), this)); - } + swap_impl(other, "swap(object_t&)"); + set_parents(); } /// @brief exchanges the values /// @sa https://json.nlohmann.me/api/basic_json/swap/ void swap(string_t& other) // NOLINT(bugprone-exception-escape,cppcoreguidelines-noexcept-swap,performance-noexcept-swap) { - // swap only works for strings - if (JSON_HEDLEY_LIKELY(is_string())) - { - using std::swap; - swap(*(m_data.m_value.string), other); - } - else - { - JSON_THROW(type_error::create(310, detail::concat("cannot use swap(string_t&) with ", type_name()), this)); - } + swap_impl(other, "swap(string_t&)"); } /// @brief exchanges the values /// @sa https://json.nlohmann.me/api/basic_json/swap/ void swap(binary_t& other) // NOLINT(bugprone-exception-escape,cppcoreguidelines-noexcept-swap,performance-noexcept-swap) { - // swap only works for binary values - if (JSON_HEDLEY_LIKELY(is_binary())) - { - using std::swap; - swap(*(m_data.m_value.binary), other); - } - else - { - JSON_THROW(type_error::create(310, detail::concat("cannot use swap(binary_t&) with ", type_name()), this)); - } + swap_impl(other, "swap(binary_t&)"); } /// @brief exchanges the values /// @sa https://json.nlohmann.me/api/basic_json/swap/ void swap(typename binary_t::container_type& other) // NOLINT(bugprone-exception-escape) { - // swap only works for binary values - if (JSON_HEDLEY_LIKELY(is_binary())) - { - using std::swap; - swap(*(m_data.m_value.binary), other); - } - else - { - JSON_THROW(type_error::create(310, detail::concat("cannot use swap(binary_t::container_type&) with ", type_name()), this)); - } + swap_impl(other, "swap(binary_t::container_type&)"); } /// @} @@ -5109,7 +5352,22 @@ public: /// @name lexicographical comparison operators /// @{ - // note parentheses around operands are necessary; see + // the same-type cases for non-owning scalars, shared by JSON_IMPLEMENT_OPERATOR + // and JSON_IMPLEMENT_SCALAR_FAST +#define JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \ + case value_t::null: \ + return (null_result); \ + case value_t::boolean: \ + return lhs.m_data.m_value.boolean op rhs.m_data.m_value.boolean; \ + case value_t::number_integer: \ + return lhs.m_data.m_value.number_integer op rhs.m_data.m_value.number_integer; \ + case value_t::number_unsigned: \ + return lhs.m_data.m_value.number_unsigned op rhs.m_data.m_value.number_unsigned; \ + case value_t::number_float: \ + return lhs.m_data.m_value.number_float op rhs.m_data.m_value.number_float; + + // note parentheses around the container operands are necessary, because + // GCC otherwise parses `array <` as std::array; see // https://github.com/nlohmann/json/issues/1530 // Mixed signed/unsigned integer comparisons check whether the signed value // is negative before casting. If it is, the comparison is performed with @@ -5145,24 +5403,11 @@ public: return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \ } \ \ - case value_t::null: \ - return (null_result); \ - \ + JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \ + \ case value_t::string: \ return (*lhs.m_data.m_value.string) op (*rhs.m_data.m_value.string); \ \ - case value_t::boolean: \ - return (lhs.m_data.m_value.boolean) op (rhs.m_data.m_value.boolean); \ - \ - case value_t::number_integer: \ - return (lhs.m_data.m_value.number_integer) op (rhs.m_data.m_value.number_integer); \ - \ - case value_t::number_unsigned: \ - return (lhs.m_data.m_value.number_unsigned) op (rhs.m_data.m_value.number_unsigned); \ - \ - case value_t::number_float: \ - return (lhs.m_data.m_value.number_float) op (rhs.m_data.m_value.number_float); \ - \ case value_t::binary: \ return (*lhs.m_data.m_value.binary) op (*rhs.m_data.m_value.binary); \ \ @@ -5206,6 +5451,24 @@ public: \ return (default_result); + // inline fast path for two values of the same non-owning scalar type; anything + // else falls through to the out-of-line general comparison +#define JSON_IMPLEMENT_SCALAR_FAST(op, null_result) \ + if (lhs.m_data.m_type == rhs.m_data.m_type) /* NOLINT(readability/braces) */ \ + { \ + switch (lhs.m_data.m_type) \ + { \ + JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \ + case value_t::object: \ + case value_t::array: \ + case value_t::string: \ + case value_t::binary: \ + case value_t::discarded: \ + default: \ + break; \ + } \ + } + JSON_PRIVATE_UNLESS_TESTED: // returns true if: // - any operand is NaN and the other operand is of number type @@ -5233,6 +5496,173 @@ public: return compares_unordered(*this, rhs, inverse); } + /// @brief whether objects and arrays are the standard containers, whose + /// elements equal_bounded() may therefore compare itself + using standard_object_t = std::map>>; + using standard_array_t = std::vector>; + using equal_bounded_containers = detail::conjunction, std::is_same>; + + // out-of-line general comparisons; the operators below handle same-type + // scalars inline and fall through to these + JSON_HEDLEY_NEVER_INLINE + static bool equal_general(const_reference lhs, const_reference rhs) noexcept + { +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_PUSH + JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") +#endif + JSON_IMPLEMENT_OPERATOR( ==, true, false, false, + compare_iteratively(lhs, rhs, false) == compare_result::equal, true) +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_POP +#endif + } + +#if JSON_HAS_THREE_WAY_COMPARISON + JSON_HEDLEY_NEVER_INLINE + static std::partial_ordering three_way_general(const_reference lhs, const_reference rhs) noexcept // *NOPAD* + { + // default_result is used if we cannot compare values. In that case, + // we compare types. + JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD* + std::partial_ordering::equivalent, + std::partial_ordering::unordered, + lhs_type <=> rhs_type, // *NOPAD* + to_partial_ordering(compare_iteratively(lhs, rhs, false)), true) + } +#else + JSON_HEDLEY_NEVER_INLINE + static bool less_general(const_reference lhs, const_reference rhs) noexcept + { + // default_result is used if we cannot compare values. In that case, + // we compare types. Note we have to call the operator explicitly, + // because MSVC has problems otherwise. + JSON_IMPLEMENT_OPERATOR( <, false, false, operator<(lhs_type, rhs_type), + compare_iteratively(lhs, rhs, true) == compare_result::less, false) + } +#endif + + /// @brief compare two leaves of the same type inside equal_structured() + /// @return 1 if they are equal, 0 if they are not, and -1 if the values are + /// of different types or are not handled here, which this leaves to + /// equal_structured() + /// @note Unlike JSON_IMPLEMENT_SCALAR_FAST, this also compares strings: + /// they are the most common leaves in a document, so comparing them + /// inline pays off in the container walk, while the operators keep + /// their inlined code at each call site small. Binary values are + /// rare and left to equal_general(). + JSON_HEDLEY_ALWAYS_INLINE + static int equal_leaf(const_reference lhs, const_reference rhs) noexcept + { +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_PUSH + JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") +#endif + if (lhs.m_data.m_type == rhs.m_data.m_type) + { + switch (lhs.m_data.m_type) + { + case value_t::null: + return 1; + case value_t::boolean: + return lhs.m_data.m_value.boolean == rhs.m_data.m_value.boolean; + case value_t::number_integer: + return lhs.m_data.m_value.number_integer == rhs.m_data.m_value.number_integer; + case value_t::number_unsigned: + return lhs.m_data.m_value.number_unsigned == rhs.m_data.m_value.number_unsigned; + case value_t::number_float: + return lhs.m_data.m_value.number_float == rhs.m_data.m_value.number_float; + case value_t::string: + return *lhs.m_data.m_value.string == *rhs.m_data.m_value.string; + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::discarded: + default: + break; + } + } +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_POP +#endif + return -1; + } + + /// @brief compare for equality, walking objects and arrays here instead of + /// in their containers so that the depth is a parameter and needs no + /// thread-local count; only used for the standard containers + static bool equal_bounded(const_reference lhs, const_reference rhs, std::size_t depth) noexcept + { + const int leaf = equal_leaf(lhs, rhs); + return leaf < 0 ? equal_structured(lhs, rhs, depth) : leaf != 0; + } + + static bool equal_structured(const_reference lhs, const_reference rhs, std::size_t depth) noexcept + { + const auto type = lhs.m_data.m_type; + if (type != rhs.m_data.m_type || (type != value_t::array && type != value_t::object)) + { + return equal_general(lhs, rhs); + } + + if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) + { + return compare_iteratively(lhs, rhs, false) == compare_result::equal; + } + + if (type == value_t::array) + { + const auto& a = *lhs.m_data.m_value.array; + const auto& b = *rhs.m_data.m_value.array; + if (a.size() != b.size()) + { + return false; + } + auto it_b = b.cbegin(); + for (auto it_a = a.cbegin(); it_a != a.cend(); ++it_a, ++it_b) + { + const int leaf = equal_leaf(*it_a, *it_b); + if (leaf < 0 ? !equal_structured(*it_a, *it_b, depth + 1) : leaf == 0) + { + return false; + } + } + return true; + } + + const auto& a = *lhs.m_data.m_value.object; + const auto& b = *rhs.m_data.m_value.object; + if (a.size() != b.size()) + { + return false; + } + auto it_b = b.cbegin(); + for (auto it_a = a.cbegin(); it_a != a.cend(); ++it_a, ++it_b) + { + if (!(it_a->first == it_b->first)) + { + return false; + } + const int leaf = equal_leaf(it_a->second, it_b->second); + if (leaf < 0 ? !equal_structured(it_a->second, it_b->second, depth + 1) : leaf == 0) + { + return false; + } + } + return true; + } + + static bool equal_dispatch(const_reference lhs, const_reference rhs, std::true_type /*standard containers*/) noexcept + { + return equal_bounded(lhs, rhs, 0); + } + + static bool equal_dispatch(const_reference lhs, const_reference rhs, std::false_type /*other containers*/) noexcept + { + return equal_general(lhs, rhs); + } + public: #if JSON_HAS_THREE_WAY_COMPARISON /// @brief comparison: equal @@ -5244,11 +5674,11 @@ public: JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") #endif const_reference lhs = *this; - JSON_IMPLEMENT_OPERATOR( ==, true, false, false, - compare_iteratively(lhs, rhs, false) == compare_result::equal, true) + JSON_IMPLEMENT_SCALAR_FAST( ==, true) #ifdef __GNUC__ JSON_HEDLEY_DIAGNOSTIC_POP #endif + return equal_dispatch(lhs, rhs, equal_bounded_containers {}); } /// @brief comparison: equal @@ -5265,13 +5695,8 @@ public: std::partial_ordering operator<=>(const_reference rhs) const noexcept // *NOPAD* { const_reference lhs = *this; - // default_result is used if we cannot compare values. In that case, - // we compare types. - JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD* - std::partial_ordering::equivalent, - std::partial_ordering::unordered, - lhs_type <=> rhs_type, // *NOPAD* - to_partial_ordering(compare_iteratively(lhs, rhs, false)), true) + JSON_IMPLEMENT_SCALAR_FAST(<=>, std::partial_ordering::equivalent) // *NOPAD* + return three_way_general(lhs, rhs); } /// @brief comparison: 3-way @@ -5359,11 +5784,11 @@ public: JSON_HEDLEY_DIAGNOSTIC_PUSH JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") #endif - JSON_IMPLEMENT_OPERATOR( ==, true, false, false, - compare_iteratively(lhs, rhs, false) == compare_result::equal, true) + JSON_IMPLEMENT_SCALAR_FAST( ==, true) #ifdef __GNUC__ JSON_HEDLEY_DIAGNOSTIC_POP #endif + return equal_dispatch(lhs, rhs, equal_bounded_containers {}); } // The friend comparisons with a scalar name the JSON type via decltype of @@ -5418,11 +5843,8 @@ public: /// @sa https://json.nlohmann.me/api/basic_json/operator_lt/ friend bool operator<(const_reference lhs, const_reference rhs) noexcept { - // default_result is used if we cannot compare values. In that case, - // we compare types. Note we have to call the operator explicitly, - // because MSVC has problems otherwise. - JSON_IMPLEMENT_OPERATOR( <, false, false, operator<(lhs_type, rhs_type), - compare_iteratively(lhs, rhs, true) == compare_result::less, false) + JSON_IMPLEMENT_SCALAR_FAST( <, false) + return less_general(lhs, rhs); } /// @brief comparison: less than @@ -5533,6 +5955,8 @@ public: #endif #undef JSON_IMPLEMENT_OPERATOR +#undef JSON_IMPLEMENT_SCALAR_FAST +#undef JSON_IMPLEMENT_SCALAR_CASES /// @} @@ -5597,10 +6021,7 @@ public: const bool ignore_comments = false, const bool ignore_trailing_commas = false) { - basic_json result; - auto p = parser(detail::input_adapter(std::forward(i)), std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas); - p.parse(true, result); - return result; + return parse_impl(detail::input_adapter(std::forward(i)), std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas); } /// @brief deserialize from a pair of character iterators (or an iterator+sentinel pair, C++20 ranges support) @@ -5615,10 +6036,7 @@ public: const bool ignore_comments = false, const bool ignore_trailing_commas = false) { - basic_json result; - auto p = parser(detail::input_adapter(std::move(first), std::move(last)), std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas); - p.parse(true, result); - return result; + return parse_impl(detail::input_adapter(std::move(first), std::move(last)), std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas); } JSON_HEDLEY_WARN_UNUSED_RESULT @@ -5632,10 +6050,7 @@ public: = delete; #else { - basic_json result; - auto p = parser(i.get(), std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas); - p.parse(true, result); - return result; + return parse_impl(i.get(), std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas); } #endif @@ -5647,7 +6062,7 @@ public: const bool ignore_comments = false, const bool ignore_trailing_commas = false) { - return parser(detail::input_adapter(std::forward(i)), nullptr, false, ignore_comments, ignore_trailing_commas, true).accept(true); + return accept_impl(detail::input_adapter(std::forward(i)), ignore_comments, ignore_trailing_commas); } /// @brief check if the input is valid JSON (iterator pair, or iterator+sentinel pair for C++20 ranges support) @@ -5659,7 +6074,7 @@ public: const bool ignore_comments = false, const bool ignore_trailing_commas = false) { - return parser(detail::input_adapter(std::move(first), std::move(last)), nullptr, false, ignore_comments, ignore_trailing_commas, true).accept(true); + return accept_impl(detail::input_adapter(std::move(first), std::move(last)), ignore_comments, ignore_trailing_commas); } JSON_HEDLEY_WARN_UNUSED_RESULT @@ -5671,7 +6086,7 @@ public: = delete; #else { - return parser(i.get(), nullptr, false, ignore_comments, ignore_trailing_commas, true).accept(true); + return accept_impl(i.get(), ignore_comments, ignore_trailing_commas); } #endif @@ -5686,10 +6101,7 @@ public: const bool ignore_trailing_commas = false, const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error) { - auto ia = detail::input_adapter(std::forward(i)); - return format == input_format_t::json - ? parser(std::move(ia), nullptr, true, ignore_comments, ignore_trailing_commas).sax_parse(sax, strict) - : detail::binary_reader(std::move(ia), format).sax_parse(sax, strict, tag_handler); + return sax_parse_impl(detail::input_adapter(std::forward(i)), sax, format, strict, ignore_comments, ignore_trailing_commas, tag_handler); } /// @brief generate SAX events (iterator pair, or iterator+sentinel pair for C++20 ranges support) @@ -5704,10 +6116,7 @@ public: const bool ignore_trailing_commas = false, const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error) { - auto ia = detail::input_adapter(std::move(first), std::move(last)); - return format == input_format_t::json - ? parser(std::move(ia), nullptr, true, ignore_comments, ignore_trailing_commas).sax_parse(sax, strict) - : detail::binary_reader(std::move(ia), format).sax_parse(sax, strict, tag_handler); + return sax_parse_impl(detail::input_adapter(std::move(first), std::move(last)), sax, format, strict, ignore_comments, ignore_trailing_commas, tag_handler); } /// @brief generate SAX events @@ -5741,12 +6150,7 @@ public: = delete; #else { - auto ia = i.get(); - return format == input_format_t::json - // NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg) - ? parser(std::move(ia), nullptr, true, ignore_comments, ignore_trailing_commas).sax_parse(sax, strict) - // NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg) - : detail::binary_reader(std::move(ia), format).sax_parse(sax, strict, tag_handler); + return sax_parse_impl(i.get(), sax, format, strict, ignore_comments, ignore_trailing_commas, tag_handler); } #endif #if defined(__clang__) @@ -5792,29 +6196,7 @@ public: JSON_HEDLEY_RETURNS_NON_NULL const char* type_name() const noexcept { - switch (m_data.m_type) - { - case value_t::null: - return "null"; - case value_t::object: - return "object"; - case value_t::array: - return "array"; - case value_t::string: - return "string"; - case value_t::boolean: - return "boolean"; - case value_t::binary: - return "binary"; - case value_t::discarded: - return "discarded"; - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - return "number"; - default: - return "invalid"; - } + return detail::value_type_name(m_data.m_type); } JSON_PRIVATE_UNLESS_TESTED: @@ -5883,7 +6265,7 @@ public: from_ubjson/from_bjdata/from_bon8/from_bson overloads Building the @ref binary_reader as a named local, rather than as a - temporary that @ref detail::json_sax_dom_parser::parse is called on in + temporary that @ref detail::json_sax_dom_binary_parser::parse is called on in the same expression, avoids a false-positive cppcheck accessMoved warning in each of the 16 callers. */ @@ -5894,7 +6276,7 @@ public: const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error) { basic_json result; - detail::json_sax_dom_parser sdp(result, allow_exceptions); + detail::json_sax_dom_binary_parser sdp(result, allow_exceptions); binary_reader reader(std::move(ia), format, error_handler); if (!reader.sax_parse(&sdp, strict, tag_handler)) { @@ -5916,10 +6298,10 @@ public: static std::vector to_cbor(const basic_json& j, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - std::vector result; - result.reserve(detail::binary_reserve_hint(j)); - vector_writer(result, error_handler).write_cbor(j); - return result; + return to_binary_vector(j, error_handler, [&](vector_binary_writer& w) + { + w.write_cbor(j); + }); } /// @brief create a CBOR serialization of a given JSON value @@ -5927,7 +6309,9 @@ public: static void to_cbor(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_cbor(j); + binary_writer writer(o, error_handler); + writer.write_cbor(j); + writer.flush(); } /// @brief create a CBOR serialization of a given JSON value @@ -5935,7 +6319,9 @@ public: static void to_cbor(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_cbor(j); + binary_writer writer(o, error_handler); + writer.write_cbor(j); + writer.flush(); } /// @brief create a MessagePack serialization of a given JSON value @@ -5943,10 +6329,10 @@ public: static std::vector to_msgpack(const basic_json& j, const error_handler_t error_handler = error_handler_t::keep) { - std::vector result; - result.reserve(detail::binary_reserve_hint(j)); - vector_writer(result, error_handler).write_msgpack(j); - return result; + return to_binary_vector(j, error_handler, [&](vector_binary_writer& w) + { + w.write_msgpack(j); + }); } /// @brief create a MessagePack serialization of a given JSON value @@ -5954,7 +6340,9 @@ public: static void to_msgpack(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = error_handler_t::keep) { - binary_writer(o, error_handler).write_msgpack(j); + binary_writer writer(o, error_handler); + writer.write_msgpack(j); + writer.flush(); } /// @brief create a MessagePack serialization of a given JSON value @@ -5962,7 +6350,9 @@ public: static void to_msgpack(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = error_handler_t::keep) { - binary_writer(o, error_handler).write_msgpack(j); + binary_writer writer(o, error_handler); + writer.write_msgpack(j); + writer.flush(); } /// @brief create a UBJSON serialization of a given JSON value @@ -5972,10 +6362,10 @@ public: const bool use_type = false, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - std::vector result; - result.reserve(detail::binary_reserve_hint(j)); - vector_writer(result, error_handler).write_ubjson(j, use_size, use_type); - return result; + return to_binary_vector(j, error_handler, [&](vector_binary_writer& w) + { + w.write_ubjson(j, use_size, use_type); + }); } /// @brief create a UBJSON serialization of a given JSON value @@ -5984,7 +6374,9 @@ public: const bool use_size = false, const bool use_type = false, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_ubjson(j, use_size, use_type); + binary_writer writer(o, error_handler); + writer.write_ubjson(j, use_size, use_type); + writer.flush(); } /// @brief create a UBJSON serialization of a given JSON value @@ -5993,7 +6385,9 @@ public: const bool use_size = false, const bool use_type = false, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_ubjson(j, use_size, use_type); + binary_writer writer(o, error_handler); + writer.write_ubjson(j, use_size, use_type); + writer.flush(); } /// @brief create a BJData serialization of a given JSON value @@ -6004,10 +6398,10 @@ public: const bjdata_version_t version = bjdata_version_t::draft2, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - std::vector result; - result.reserve(detail::binary_reserve_hint(j)); - vector_writer(result, error_handler).write_ubjson(j, use_size, use_type, true, true, version); - return result; + return to_binary_vector(j, error_handler, [&](vector_binary_writer& w) + { + w.write_ubjson(j, use_size, use_type, true, true, version); + }); } /// @brief create a BJData serialization of a given JSON value @@ -6017,7 +6411,9 @@ public: const bjdata_version_t version = bjdata_version_t::draft2, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_ubjson(j, use_size, use_type, true, true, version); + binary_writer writer(o, error_handler); + writer.write_ubjson(j, use_size, use_type, true, true, version); + writer.flush(); } /// @brief create a BJData serialization of a given JSON value @@ -6027,7 +6423,9 @@ public: const bjdata_version_t version = bjdata_version_t::draft2, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_ubjson(j, use_size, use_type, true, true, version); + binary_writer writer(o, error_handler); + writer.write_ubjson(j, use_size, use_type, true, true, version); + writer.flush(); } /// @brief create a BSON serialization of a given JSON value @@ -6035,10 +6433,10 @@ public: static std::vector to_bson(const basic_json& j, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - std::vector result; - result.reserve(detail::binary_reserve_hint(j)); - vector_writer(result, error_handler).write_bson(j); - return result; + return to_binary_vector(j, error_handler, [&](vector_binary_writer& w) + { + w.write_bson(j); + }); } /// @brief create a BSON serialization of a given JSON value @@ -6046,7 +6444,9 @@ public: static void to_bson(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_bson(j); + binary_writer writer(o, error_handler); + writer.write_bson(j); + writer.flush(); } /// @brief create a BSON serialization of a given JSON value @@ -6054,31 +6454,37 @@ public: static void to_bson(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_bson(j); + binary_writer writer(o, error_handler); + writer.write_bson(j); + writer.flush(); } /// @brief create a BON8 serialization of a given JSON value /// @sa https://json.nlohmann.me/api/basic_json/to_bon8/ static std::vector to_bon8(const basic_json& j) { - std::vector result; - result.reserve(detail::binary_reserve_hint(j)); - vector_writer(result).write_bon8(j); - return result; + return to_binary_vector(j, detail::binary_writer_default_error_handler(), [&](vector_binary_writer& w) + { + w.write_bon8(j); + }); } /// @brief create a BON8 serialization of a given JSON value /// @sa https://json.nlohmann.me/api/basic_json/to_bon8/ static void to_bon8(const basic_json& j, detail::output_adapter o) { - binary_writer(o).write_bon8(j); + binary_writer writer(o); + writer.write_bon8(j); + writer.flush(); } /// @brief create a BON8 serialization of a given JSON value /// @sa https://json.nlohmann.me/api/basic_json/to_bon8/ static void to_bon8(const basic_json& j, detail::output_adapter o) { - binary_writer(o).write_bon8(j); + binary_writer writer(o); + writer.write_bon8(j); + writer.flush(); } /// @brief create a JSON value from an input in CBOR format @@ -6474,6 +6880,72 @@ public: /// @name JSON Patch functions /// @{ + private: + /// the members of an operation of a JSON patch that are of interest + struct patch_members + { + basic_json* op = nullptr; + basic_json* path = nullptr; + basic_json* value = nullptr; + basic_json* from = nullptr; + }; + + /// @brief find the members "op", "path", "value", and "from" of the object @a val in one pass + static patch_members find_patch_members(const basic_json& val, std::true_type /*key_type is string_t*/) + { + patch_members members; + for (auto& member : *val.m_data.m_value.object) + { + const string_t& key = member.first; + switch (key.size()) + { + case 2: + if (key == "op") + { + members.op = &member.second; + } + break; + case 4: + if (key == "path") + { + members.path = &member.second; + } + else if (key == "from") + { + members.from = &member.second; + } + break; + case 5: + if (key == "value") + { + members.value = &member.second; + } + break; + default: + break; + } + } + return members; + } + + /// @brief find the members by lookup for key types other than string_t + static patch_members find_patch_members(const basic_json& val, std::false_type /*key_type is not string_t*/) + { + const auto find_member = [&val](const char* name) -> basic_json * + { + const auto it = val.m_data.m_value.object->find(string_t(name)); + return it != val.m_data.m_value.object->end() ? &it->second : nullptr; + }; + + patch_members members; + members.op = find_member("op"); + members.path = find_member("path"); + members.value = find_member("value"); + members.from = find_member("from"); + return members; + } + + public: /// @brief applies a JSON patch in-place without copying the object /// @sa https://json.nlohmann.me/api/basic_json/patch/ void patch_inplace(const basic_json& json_patch) @@ -6513,24 +6985,18 @@ public: }; // wrapper for "add" operation; add value at ptr - const auto operation_add = [&result](json_pointer & ptr, const basic_json & val) + const auto operation_add = [&result](json_pointer & ptr, basic_json val) { // adding to the root of the target document means replacing it if (ptr.empty()) { - result = val; + result = std::move(val); return; } - // make sure the top element of the pointer exists - json_pointer const top_pointer = ptr.top(); - if (top_pointer != ptr) - { - result.at(top_pointer); - } - - // get reference to the parent of the JSON pointer ptr - const auto last_path = ptr.back(); + // get reference to the parent of the JSON pointer ptr; a missing + // parent (or one of its ancestors) is reported by at() + auto last_path = std::move(ptr.reference_tokens.back()); ptr.pop_back(); // parent must exist when performing patch add per RFC6902 specs basic_json& parent = result.at(ptr); @@ -6541,7 +7007,7 @@ public: case value_t::object: { // use operator[] to add value - parent[last_path] = val; + parent[std::move(last_path)] = std::move(val); break; } @@ -6550,7 +7016,7 @@ public: if (last_path == "-") { // special case: append to back - parent.push_back(val); + parent.push_back(std::move(val)); } else { @@ -6558,11 +7024,11 @@ public: if (JSON_HEDLEY_UNLIKELY(idx > parent.size())) { // avoid undefined behavior - JSON_THROW(out_of_range::create(401, detail::concat("array index ", std::to_string(idx), " is out of range"), &parent)); + throw_array_index_out_of_range(idx, parent); } // default case: insert add offset - parent.insert(parent.begin() + static_cast(idx), val); + parent.insert(parent.begin() + static_cast(idx), std::move(val)); } break; } @@ -6577,7 +7043,7 @@ public: case value_t::binary: case value_t::discarded: default: - JSON_THROW(out_of_range::create(411, detail::concat("cannot add value: the JSON Patch 'add' target's parent is of type ", parent.type_name(), ", but must be an object or array"), &parent)); + detail::throw_patch_add_parent(parent); } }; @@ -6585,7 +7051,8 @@ public: const auto operation_remove = [this, & result](json_pointer & ptr) { // get reference to the parent of the JSON pointer ptr - const auto last_path = ptr.back(); + static_cast(ptr.back()); // throws out_of_range.405 if ptr is the root + const auto last_path = std::move(ptr.reference_tokens.back()); ptr.pop_back(); basic_json& parent = result.at(ptr); @@ -6600,7 +7067,7 @@ public: } else { - JSON_THROW(out_of_range::create(403, detail::concat("key '", last_path, "' not found"), this)); + throw_key_not_found(last_path, *this); } } else if (parent.is_array()) @@ -6612,7 +7079,7 @@ public: { // the parent of a "remove" target must be an object or array // (see #5396) - JSON_THROW(out_of_range::create(413, detail::concat("cannot remove value: the JSON Patch 'remove' target's parent is of type ", parent.type_name(), ", but must be an object or array"), &parent)); + detail::throw_patch_remove_parent(parent); } }; @@ -6643,57 +7110,190 @@ public: // type check: top level value must be an array if (JSON_HEDLEY_UNLIKELY(!json_patch.is_array())) { - JSON_THROW(parse_error::create(104, 0, "JSON patch must be an array of objects", &json_patch)); + detail::throw_patch_not_an_array(json_patch); } + // the usual patch operations "add", "remove", "replace" and "test" on + // a pointer without escapes that denotes existing array elements and + // object members (as "add" does for the parent of the new value), as + // a diff creates them: apply the operation and return true; return + // false, with nothing changed, for all others + string_t token; + typename json_pointer::template plain_steps steps; + const auto try_plain = [&result, &token, &steps](patch_operations operation, const string_t& path, const patch_members & members, const basic_json & val) + { + if (path.empty()) + { + return false; + } + + // a missing "value" is reported below + const basic_json* const value_ptr = members.value; + const basic_json& value = value_ptr != nullptr ? *value_ptr : val; + if (operation != patch_operations::remove && value_ptr == nullptr) + { + return false; + } + + switch (operation) + { + case patch_operations::replace: + { + basic_json* const target = json_pointer::resolve_plain(&result, path, path.size(), token, steps); + if (target == nullptr) + { + return false; + } + *target = value; + return true; + } + + case patch_operations::test: + { + const basic_json* const target = json_pointer::resolve_plain(&result, path, path.size(), token, steps); + // a failing test is reported below + return target != nullptr && *target == value; + } + + case patch_operations::add: + case patch_operations::remove: + { + // the parent of the pointer and its last reference token + std::size_t slash = path.size(); + while (slash > 0 && path[slash - 1] != '/') + { + --slash; + } + if (slash == 0) + { + return false; + } + --slash; + + basic_json* const parent = json_pointer::resolve_plain(&result, path, slash, token, steps); + if (parent == nullptr) + { + return false; + } + token.clear(); + token.append(path.data() + slash + 1, path.size() - slash - 1); + if (token.find_first_of('~') != string_t::npos) + { + return false; + } + + if (operation == patch_operations::add) + { + if (parent->is_object()) + { + (*parent)[token] = value; + return true; + } + if (parent->is_array()) + { + typename basic_json::size_type idx{}; + if (token == "-") + { + parent->push_back(value); + return true; + } + if (json_pointer::template parse_array_index(token, idx) == json_pointer::array_index_status::ok && idx <= parent->size()) + { + parent->insert(parent->begin() + static_cast(idx), value); + return true; + } + } + return false; + } + + if (parent->is_object()) + { + const auto it = parent->find(token); + if (it == parent->end()) + { + return false; + } + parent->erase(it); + return true; + } + if (parent->is_array()) + { + typename basic_json::size_type idx{}; + if (json_pointer::template parse_array_index(token, idx) == json_pointer::array_index_status::ok && idx < parent->size()) + { + parent->erase(idx); + return true; + } + } + return false; + } + + case patch_operations::move: + case patch_operations::copy: + case patch_operations::invalid: + default: + return false; + } + }; + + // the name of the first member to check + const string_t op_member = "op"; + // iterate and apply the operations for (const auto& val : json_patch) { // wrapper to get a value for an operation - const auto get_value = [&val](const string_t& op, - const string_t& member, + const auto get_value = [&val](basic_json * found, + const string_t& op, + const char* member, bool string_type) -> basic_json & { - // find value - auto it = val.m_data.m_value.object->find(member); - - // context-sensitive error message - const auto error_msg = (op == "op") ? "operation" : detail::concat("operation '", op, '\''); // NOLINT(bugprone-unused-local-non-trivial-variable) - // check if the desired value is present - if (JSON_HEDLEY_UNLIKELY(it == val.m_data.m_value.object->end())) + if (JSON_HEDLEY_UNLIKELY(found == nullptr)) { - // NOLINTNEXTLINE(performance-inefficient-string-concatenation) - JSON_THROW(parse_error::create(105, 0, detail::concat(error_msg, " must have member '", member, "'"), &val)); + detail::throw_patch_missing_member(op, string_t(member), false, val); } // check if the result is of type string - if (JSON_HEDLEY_UNLIKELY(string_type && !it->second.is_string())) + if (JSON_HEDLEY_UNLIKELY(string_type && !found->is_string())) { - // NOLINTNEXTLINE(performance-inefficient-string-concatenation) - JSON_THROW(parse_error::create(105, 0, detail::concat(error_msg, " must have string member '", member, "'"), &val)); + detail::throw_patch_missing_member(op, string_t(member), true, val); } // no error: return value - return it->second; + return *found; }; // type check: every element of the array must be an object if (JSON_HEDLEY_UNLIKELY(!val.is_object())) { - JSON_THROW(parse_error::create(104, 0, "JSON patch must be an array of objects", &val)); + detail::throw_patch_not_an_array(val); } + const patch_members members = find_patch_members(val, std::is_same {}); + // collect mandatory members - const auto op = get_value("op", "op", true).template get(); - const auto path = get_value(op, "path", true).template get(); + const string_t& op = *get_value(members.op, op_member, "op", true).m_data.m_value.string; + const string_t& path = *get_value(members.path, op, "path", true).m_data.m_value.string; + const patch_operations operation = get_op(op); + + // the usual operations on a plain pointer are applied without + // building the pointer's reference tokens; anything else (and + // every error) is handled below + if (try_plain(operation, path, members, val)) + { + continue; + } + + // the general operations may change any container + steps.reset(); json_pointer ptr(path); - switch (get_op(op)) + switch (operation) { case patch_operations::add: { - operation_add(ptr, get_value("add", "value", false)); + operation_add(ptr, get_value(members.value, "add", "value", false)); break; } @@ -6706,44 +7306,43 @@ public: case patch_operations::replace: { // the "path" location must exist - use at() - result.at(ptr) = get_value("replace", "value", false); + result.at(ptr) = get_value(members.value, "replace", "value", false); break; } case patch_operations::move: { - const auto from_path = get_value("move", "from", true).template get(); + const string_t& from_path = *get_value(members.from, "move", "from", true).m_data.m_value.string; json_pointer from_ptr(from_path); if (JSON_HEDLEY_UNLIKELY(is_proper_prefix(from_ptr, ptr))) { - JSON_THROW(out_of_range::create(414, detail::concat("cannot move value: 'from' path '", from_path, "' is a proper prefix of 'path' '", path, "'"), &result)); + detail::throw_patch_move_into_child(from_path, path, result); } // the "from" location must exist - use at() - basic_json const v = result.at(from_ptr); + // a root "from" is rejected by operation_remove below, which must leave result intact + basic_json v = from_ptr.empty() ? basic_json(result) : std::move(result.at(from_ptr)); // The move operation is functionally identical to a // "remove" operation on the "from" location, followed // immediately by an "add" operation at the target // location with the value that was just removed. operation_remove(from_ptr); - operation_add(ptr, v); + operation_add(ptr, std::move(v)); break; } case patch_operations::copy: { - const auto from_path = get_value("copy", "from", true).template get(); + const string_t& from_path = *get_value(members.from, "copy", "from", true).m_data.m_value.string; const json_pointer from_ptr(from_path); - // the "from" location must exist - use at() - basic_json const v = result.at(from_ptr); - // The copy is functionally identical to an "add" // operation at the target location using the value - // specified in the "from" member. - operation_add(ptr, v); + // specified in the "from" member; the "from" location + // must exist - use at() + operation_add(ptr, result.at(from_ptr)); break; } @@ -6754,7 +7353,7 @@ public: { // check if "value" matches the one at "path" // the "path" location must exist - use at() - success = (result.at(ptr) == get_value("test", "value", false)); + success = (result.at(ptr) == get_value(members.value, "test", "value", false)); } JSON_INTERNAL_CATCH (out_of_range&) { @@ -6764,7 +7363,7 @@ public: // throw an exception if the test fails if (JSON_HEDLEY_UNLIKELY(!success)) { - JSON_THROW(other_error::create(501, detail::concat("unsuccessful: ", val.dump()), &val)); + detail::throw_patch_test_failed(val); } break; @@ -6775,7 +7374,7 @@ public: { // op must be "add", "remove", "replace", "move", "copy", or // "test" - JSON_THROW(parse_error::create(105, 0, detail::concat("operation value '", op, "' is invalid"), &val)); + detail::throw_patch_invalid_operation(op, val); } } } @@ -6798,7 +7397,8 @@ public: { // the patch basic_json result(value_t::array); - diff_recursively(result, source, target, path, 0); + string_t buffer = path; + diff_recursively(result, source, target, buffer, 0); return result; } @@ -6829,12 +7429,11 @@ public: std::size_t index = 0; /// objects: the next member of source to look at const_iterator member{}; // NOLINT(readability-redundant-member-init) - /// objects: the keys common to both, in source's order - std::vector common_keys{}; // NOLINT(readability-redundant-member-init) - /// objects: the next entry of common_keys - std::size_t next_common = 0; - /// objects: the "add" operations for keys only target has - basic_json added_ops{}; // NOLINT(readability-redundant-member-init) + /// objects: for each member of source (in order), its value in + /// target, or nullptr if target has no such key; `index` is the next + std::vector target_values{}; // NOLINT(readability-redundant-member-init) + /// objects: the members only target has, in target's order + std::vector added{}; // NOLINT(readability-redundant-member-init) }; // The operations of a diff are built by the functions below rather than @@ -6843,31 +7442,130 @@ public: // function it appears in. In diff_recursively, which is on the call stack // once per nesting level, that made every level cost kilobytes of stack. + /// @brief append an operation @a op for @a path to @a result, with @a value if it is not null + static void diff_operation(basic_json& result, const char* op, const string_t& path, const basic_json* value) + { + // the keys are distinct and in the order of std::map; the members are + // added without looking them up + basic_json entry(value_t::object); + auto& members = *entry.m_data.m_value.object; + auto hint = members.end(); + json_pointer::add_flattened(members, hint, string_t("op"), string_t(op), detail::priority_tag<2> {}); + json_pointer::add_flattened(members, hint, string_t("path"), path, detail::priority_tag<2> {}); + if (value != nullptr) + { + json_pointer::add_flattened(members, hint, string_t("value"), *value, detail::priority_tag<2> {}); + } + entry.set_parents(); + result.push_back(std::move(entry)); + } + /// @brief append a "replace" operation for @a path with @a value to @a result static void diff_replace(basic_json& result, const string_t& path, const basic_json& value) { - result.push_back( - { - {"op", "replace"}, {"path", path}, {"value", value} - }); + diff_operation(result, "replace", path, &value); } /// @brief append a "remove" operation for @a path to @a result static void diff_remove(basic_json& result, const string_t& path) { - result.push_back(object( - { - {"op", "remove"}, {"path", path} - })); + diff_operation(result, "remove", path, nullptr); } /// @brief append an "add" operation for @a path with @a value to @a result static void diff_add(basic_json& result, const string_t& path, const basic_json& value) { - result.push_back( + diff_operation(result, "add", path, &value); + } + + /// @brief the values in the target of the members of a source object, for the + /// objects of up to 16 members without allocating + /// + /// reserve() must be called with the number of values that are added. + class diff_target_values + { + public: + diff_target_values() noexcept + : inline_items() // NOLINT(readability-redundant-member-init) + {} + diff_target_values(const diff_target_values&) = delete; + diff_target_values& operator=(const diff_target_values&) = delete; + diff_target_values(diff_target_values&&) = delete; + diff_target_values& operator=(diff_target_values&&) = delete; + ~diff_target_values() = default; + + void reserve(const std::size_t n) { - {"op", "add"}, {"path", path}, {"value", value} - }); + if (n > inline_size) + { + large.resize(n); + items = large.data(); + } + } + + void push_back(const basic_json* value) + { + items[count++] = value; + } + + std::size_t size() const noexcept + { + return count; + } + + const basic_json* operator[](const std::size_t i) const noexcept + { + return items[i]; + } + + private: + static constexpr std::size_t inline_size = 16; + /// the values of a small object, left uninitialized: they are written + /// before they are read (the user-provided constructor keeps the + /// member initialization, which -Weffc++ asks for, from zeroing them) + struct uninitialized_items : std::array + { + uninitialized_items() noexcept {} // NOLINT(modernize-use-equals-default,hicpp-use-equals-default,cppcoreguidelines-pro-type-member-init,hicpp-member-init): see above + }; + + std::size_t count = 0; + std::vector large{}; // NOLINT(readability-redundant-member-init) + uninitialized_items inline_items; + const basic_json** items = inline_items.data(); + }; + + /// @brief append "/" and the decimal representation of @a index to @a path + static void diff_append_index(string_t& path, std::size_t index) + { + path.push_back('/'); + json_pointer::append_index(path, index); + } + + /// @brief append "/" and the escaped @a key to @a path + static void diff_append_key(string_t& path, const string_t& key) + { + path.push_back('/'); + detail::escape_into(path, key); + } + + /// @brief the path of the member @a key of the value at @a path + static string_t diff_key_path(const string_t& path, const string_t& key) + { + string_t result; + result.reserve(path.size() + key.size() + 1); + result.append(path.data(), path.size()); + diff_append_key(result, key); + return result; + } + + /// @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) + { + string_t result; + result.reserve(path.size() + 8); + result.append(path.data(), path.size()); + diff_append_index(result, index); + return result; } /// @brief append the "remove" operations for the elements of array @@ -6880,13 +7578,27 @@ public: // in that order avoids the quadratic reinsertion done before for (std::size_t j = source.size(); j > index; --j) { - diff_remove(result, detail::concat(path, '/', detail::to_string(j - 1))); + diff_remove(result, diff_index_path(path, j - 1)); } // add other remaining elements - for (std::size_t i = source.size(); i < target.size(); ++i) + if (source.size() < target.size()) { - diff_add(result, detail::concat(path, "/-"), target[i]); + string_t tail_path = path; + tail_path.append("/-", 2); + for (std::size_t i = source.size(); i < target.size(); ++i) + { + diff_add(result, tail_path, target[i]); + } + } + } + + /// @brief appends the "add" operations for the members @a added to @a result + static void diff_added(basic_json& result, const std::vector& added, const string_t& path) + { + for (const auto& it : added) + { + diff_add(result, diff_key_path(path, it.key()), it.value()); } } @@ -6895,106 +7607,128 @@ public: If object_t does not keep its members in insertion order, or if the keys both objects have are in the same order in both, and the keys only - @a target has come after them, stores the keys common to both in - source's order in @a common_keys, stores the "add" operations for the keys - only @a target has in @a added_ops, and returns true: the caller then diffs - the objects member by member. Otherwise, appends operations that remove - every member of @a source and add every member of @a target to @a result, - and returns false. + @a target has come after them, stores in @a target_values, for each member + of source in order, its value in target (nullptr for a key target does not + have), stores the members only target has in @a added, and returns true: + the caller then diffs the objects member by member. Otherwise, appends + operations that remove every member of @a source and add every member of + @a target to @a result, and returns false. */ + template static bool diff_object_keys(basic_json& result, const basic_json& source, const basic_json& target, - const string_t& path, std::vector& common_keys, - basic_json& added_ops) + const string_t& path, TargetValues& target_values, + std::vector& added) { - // first pass: record, for every source key, whether it is - // common to both objects (in source's iteration order) or - // was deleted (i.e., in source but not in target) -- this is - // a by-product of the target.find() call already needed to - // tell the two cases apart, so it adds no extra lookups. The - // "remove" ops themselves are emitted later, interleaved - // with the per-key diffs in the caller's fast path, to match - // source's original iteration order (as the original, - // pre-reordering-aware implementation did) instead of - // grouping all removes before all per-key diffs. - std::vector common_keys_source_order; - for (auto it = source.cbegin(); it != source.cend(); ++it) + // first pass: one lookup per source key + // Objects of the same size usually have the same keys, in the same + // order: walk them together as long as that holds, which takes no lookups. + target_values.reserve(source.size()); + std::size_t common = 0; + bool together = source.size() == target.size(); + for (auto it = source.cbegin(), partner = target.cbegin(); it != source.cend(); ++it) { - if (target.find(it.key()) != target.end()) + if (together) { - common_keys_source_order.push_back(it.key()); + if (it.key() == partner.key()) + { + target_values.push_back(&partner.value()); + ++common; + ++partner; + continue; + } + together = false; } - } - // second pass: find keys that were added (i.e., in target but - // not in source), and record the keys common to both, in - // target's iteration order -- again a by-product of the - // source.find() call already needed to detect added keys. At - // the same time, determine whether every added key comes - // after every common key in target's order (a precondition - // for the fast path, which only ever appends new keys - // at the very end). Both are only needed for an object_t that - // keeps its members in insertion order, such as the one - // backing `ordered_json`; for any other object_t, the fast - // path is always taken and they are not computed. - // The patch ops for keys that were added (i.e., in target but not - // in source) are built here so the fast path can reuse - // them without a second source.find() per target key. Only - // used by the fast path -- the slow (reordering) path - // rebuilds "add" ops for every key itself. - std::vector common_keys_target_order; - bool new_keys_form_suffix = true; - bool seen_new_key = false; - for (auto it = target.cbegin(); it != target.cend(); ++it) - { - if (source.find(it.key()) == source.end()) + const auto found = target.find(it.key()); + if (found != target.cend()) { - seen_new_key = true; - diff_add(added_ops, detail::concat(path, '/', detail::escape(it.key())), it.value()); + target_values.push_back(&found.value()); + ++common; } else { -#ifdef JSON_HEDLEY_MSVC_VERSION -#pragma warning(push ) -#pragma warning(disable : 4127) // ignore warning to replace if with if constexpr -#endif - if (detail::is_ordered_map::value) - { - common_keys_target_order.push_back(it.key()); - if (seen_new_key) - { - new_keys_form_suffix = false; - } - } -#ifdef JSON_HEDLEY_MSVC_VERSION -#pragma warning( pop ) -#endif + target_values.push_back(nullptr); } } - // Only an object type that keeps its members in insertion - // order, such as nlohmann::ordered_map, can need reordering: - // patch() appends a new member at the end of such an object. - // Any other object type places its members itself - std::map - // in key order, a hash map in an order its operator== ignores - - // so a member-by-member diff always reproduces target there. + // the same keys in the same order + if (together) + { + return true; + } + + // second pass: find the keys only target has. An object type that + // keeps its members in insertion order, such as the one backing + // ordered_json, also needs the common keys to be in the same order + // in both objects, and the new keys to come after them, as patch() + // appends a new member at the end of such an object. Any other + // object type places its members itself, so a member-by-member diff + // always reproduces target there. #ifdef JSON_HEDLEY_MSVC_VERSION #pragma warning(push ) #pragma warning(disable : 4127) // ignore warning to replace if with if constexpr #endif - if (!detail::is_ordered_map::value - || (common_keys_source_order == common_keys_target_order && new_keys_form_suffix)) + bool fast_path = true; + if (detail::is_ordered_map::value) { - // fast path: order of common keys already matches (or the - // object_t's iteration order does not depend on - // insertion history), so a plain per-key diff is correct - // and minimal, as before - common_keys = std::move(common_keys_source_order); - return true; + // walk the common keys of source in lockstep with target: a key + // of target that is not the next one is either new or out of order + auto src = source.cbegin(); + std::size_t next = 0; + bool seen_new = false; + for (auto it = target.cbegin(); it != target.cend() && fast_path; ++it) + { + while (next < target_values.size() && target_values[next] == nullptr) + { + ++src; + ++next; + } + + if (next < target_values.size() && src.key() == it.key()) + { + // a common key after a new one: new keys are no suffix + fast_path = !seen_new; + ++src; + ++next; + } + else if (source.find(it.key()) == source.cend()) + { + seen_new = true; + added.push_back(it); + } + else + { + // a common key out of order + fast_path = false; + } + } + } + else if (target.size() != common) + { + for (auto it = target.cbegin(); it != target.cend(); ++it) + { + if (source.find(it.key()) == source.cend()) + { + added.push_back(it); + } + } } #ifdef JSON_HEDLEY_MSVC_VERSION #pragma warning( pop ) #endif + if (fast_path) + { + return true; + } + + // equal objects, whose members are in a different order, need no + // operations + if (source == target) + { + return false; + } + // slow path: the common keys are in a different relative // order in source and target (only possible for a // reorderable object_t like ordered_map). Building a @@ -7011,7 +7745,7 @@ public: // it moves it to the end, fixing its position. for (auto it = source.cbegin(); it != source.cend(); ++it) { - diff_remove(result, detail::concat(path, '/', detail::escape(it.key()))); + diff_remove(result, diff_key_path(path, it.key())); } // add every key that is either common (just removed @@ -7020,7 +7754,7 @@ public: // target exactly for (auto it = target.cbegin(); it != target.cend(); ++it) { - diff_add(result, detail::concat(path, '/', detail::escape(it.key())), it.value()); + diff_add(result, diff_key_path(path, it.key()), it.value()); } return false; } @@ -7029,6 +7763,9 @@ public: @brief @ref diff, for values at nesting level @a depth, appending the operations to @a result + @a path is the path of the values; it is a buffer that is extended and + shortened again below, and is the same when the function returns. + Diffing two arrays or objects calls this function again, once per nesting level, so values nested deeply enough used to exhaust the call stack and terminate the process. The descent is bounded here: once @ref @@ -7036,7 +7773,7 @@ public: diff_iteratively diffs what is left without the call stack. */ static void diff_recursively(basic_json& result, const basic_json& source, const basic_json& target, - const string_t& path, const std::size_t depth) + string_t& path, const std::size_t depth) { // if the values are the same, there is nothing to do if (source == target) @@ -7044,6 +7781,14 @@ public: return; } + diff_unequal(result, source, target, path, depth); + } + + /// @brief @ref diff_recursively for values known to be unequal; callers + /// compare first so they extend the path only for unequal ones + static void diff_unequal(basic_json& result, const basic_json& source, const basic_json& target, + string_t& path, const std::size_t depth) + { if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) { diff_iteratively(result, source, target, path); @@ -7057,75 +7802,68 @@ public: return; } - switch (source.type()) + if (detail::is_primitive(source.type())) { - case value_t::array: - { - // first pass: traverse common elements - std::size_t i = 0; - while (i < source.size() && i < target.size()) - { - // recursive call to compare array values at index i - diff_recursively(result, source[i], target[i], detail::concat(path, '/', detail::to_string(i)), depth + 1); - ++i; - } + // both primitive types: replace value + diff_replace(result, path, target); + return; + } - // We now reached the end of at least one array - // in a second pass, traverse the remaining elements - diff_array_tails(result, source, target, path, i); - break; + if (source.is_array()) + { + const std::size_t path_length = path.size(); + + // first pass: traverse common elements + std::size_t i = 0; + while (i < source.size() && i < target.size()) + { + // recursive call to compare array values at index i + if (!(source[i] == target[i])) + { + diff_append_index(path, i); + diff_unequal(result, source[i], target[i], path, depth + 1); + path.resize(path_length); + } + ++i; } - case value_t::object: + // We now reached the end of at least one array + // in a second pass, traverse the remaining elements + diff_array_tails(result, source, target, path, i); + } + else + { + diff_target_values target_values; + std::vector added; + if (diff_object_keys(result, source, target, path, target_values, added)) { - std::vector common_keys; - basic_json added_ops(value_t::array); - if (diff_object_keys(result, source, target, path, common_keys, added_ops)) + // fast path: target_values holds, for every member of + // source in order, its value in target -- so no further + // lookup is needed. Deleted keys are interleaved here + // too, in source's original order, to match the + // historical (pre-reordering-aware) output order. + const std::size_t path_length = path.size(); + std::size_t i = 0; + for (auto it = source.cbegin(); it != source.cend(); ++it, ++i) { - // fast path: common_keys is, by construction, the - // subsequence of source's keys that are common to both - // objects, in source's iteration order -- so it can be - // walked in lockstep with `source` using a cheap key - // comparison instead of another lookup. Deleted keys - // (those source keys not in common_keys) are interleaved - // here too, in source's original order, to match the - // historical (pre-reordering-aware) output order. - auto common_it = common_keys.cbegin(); - for (auto it = source.cbegin(); it != source.cend(); ++it) + if (target_values[i] != nullptr) { - if (common_it != common_keys.cend() && it.key() == *common_it) + if (!(it.value() == *target_values[i])) { - diff_recursively(result, it.value(), target[it.key()], detail::concat(path, '/', detail::escape(it.key())), depth + 1); - ++common_it; - } - else - { - // found a key that is not in target -> remove it - diff_remove(result, detail::concat(path, '/', detail::escape(it.key()))); + diff_append_key(path, it.key()); + diff_unequal(result, it.value(), *target_values[i], path, depth + 1); + path.resize(path_length); } } - - // append the "add" ops for brand-new keys collected by - // diff_object_keys -- no second source.find() per target - // key needed - result.insert(result.end(), added_ops.begin(), added_ops.end()); + else + { + // found a key that is not in target -> remove it + diff_remove(result, diff_key_path(path, it.key())); + } } - break; - } - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - { - // both primitive types: replace value - diff_replace(result, path, target); - break; + // append the "add" operations for brand-new keys + diff_added(result, added, path); } } } @@ -7171,46 +7909,31 @@ public: return; } - switch (s.type()) + if (detail::is_primitive(s.type())) { - case value_t::array: - { - stack.emplace_back(&s, &t, current_path.size()); - return; - } - - case value_t::object: - { - std::vector common_keys; - basic_json added_ops(value_t::array); - if (diff_object_keys(result, s, t, current_path, common_keys, added_ops)) - { - // fast path: the frame walks source in lockstep with - // common_keys, as diff_recursively does, and appends - // added_ops once all members are done - stack.emplace_back(&s, &t, current_path.size()); - stack.back().member = s.cbegin(); - stack.back().common_keys = std::move(common_keys); - stack.back().added_ops = std::move(added_ops); - } - return; - } - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - { - // both primitive types: replace value - diff_replace(result, current_path, t); - return; - } + // both primitive types: replace value + diff_replace(result, current_path, t); + return; } + + if (s.is_array()) + { + stack.emplace_back(&s, &t, current_path.size()); + return; + } + std::vector target_values; + std::vector added; + if (diff_object_keys(result, s, t, current_path, target_values, added)) + { + // fast path: the frame walks source in lockstep with + // common_keys, as diff_recursively does, and appends + // added_ops once all members are done + stack.emplace_back(&s, &t, current_path.size()); + stack.back().member = s.cbegin(); + stack.back().target_values = std::move(target_values); + stack.back().added = std::move(added); + } + return; }; enter(source, target); @@ -7234,7 +7957,7 @@ public: if (i < source_array.size() && i < target_array.size()) { ++stack.back().index; - detail::concat_into(current_path, '/', detail::to_string(i)); + diff_append_index(current_path, i); enter(source_array[i], target_array[i]); if (stack.size() == depth) { @@ -7253,12 +7976,11 @@ public: if (it != s->cend()) { ++stack.back().member; - const std::size_t next_common = stack.back().next_common; - if (next_common < stack.back().common_keys.size() && it.key() == stack.back().common_keys[next_common]) + const basic_json* const target_value_ptr = stack.back().target_values[stack.back().index++]; + if (target_value_ptr != nullptr) { - ++stack.back().next_common; - const basic_json& target_value = (*t)[it.key()]; - detail::concat_into(current_path, '/', detail::escape(it.key())); + const basic_json& target_value = *target_value_ptr; + diff_append_key(current_path, it.key()); enter(it.value(), target_value); if (stack.size() == depth) { @@ -7268,14 +7990,14 @@ public: else { // found a key that is not in target -> remove it - diff_remove(result, detail::concat(current_path, '/', detail::escape(it.key()))); + diff_remove(result, diff_key_path(current_path, it.key())); } continue; } // append the "add" ops for brand-new keys collected when the // object was entered - result.insert(result.end(), stack.back().added_ops.begin(), stack.back().added_ops.end()); + diff_added(result, stack.back().added, current_path); } // this array or object is done: continue with the one it is in @@ -7405,6 +8127,176 @@ public: } public: + /// @brief creates a diff as a JSON Merge Patch + /// @sa https://json.nlohmann.me/api/basic_json/merge_diff/ + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json merge_diff(const basic_json& source, const basic_json& target) + { + // unless both are objects, the patch is the target itself + if (!source.is_object() || !target.is_object()) + { + return target; + } + + return merge_diff_recursively(source, target, 0); + } + + private: + /// @brief two objects @ref merge_diff_iteratively is diffing + struct merge_diff_frame + { + merge_diff_frame(const basic_json* source_, const basic_json* target_) + : source(source_), target(target_), member(source_->cbegin()), patch(value_t::object) + {} + + // declared for GCC's -Weffc++, as in diff_frame + merge_diff_frame(const merge_diff_frame&) = default; + merge_diff_frame(merge_diff_frame&&) = default; + merge_diff_frame& operator=(const merge_diff_frame&) = default; + merge_diff_frame& operator=(merge_diff_frame&&) = default; + ~merge_diff_frame() = default; + + /// the objects being diffed + const basic_json* source; + const basic_json* target; + /// the member of source being diffed; advanced once it is done + const_iterator member; + /// the patch for the members of source diffed so far + basic_json patch; + }; + + /// @brief add the members of object @a target that @a source does not + /// have to the merge patch @a result + static void merge_diff_added_members(basic_json& result, const basic_json& source, const basic_json& target) + { + for (auto it = target.begin(); it != target.end(); ++it) + { + auto itf = source.find(it.key()); + if (itf == source.end()) + { + result[it.key()] = it.value(); + } + } + } + + /*! + @brief @ref merge_diff of two objects at nesting level @a depth + + Diffing two nested objects calls this function again, once per nesting + level, so values nested deeply enough would exhaust the call stack and + terminate the process. The descent is bounded here: once @ref + detail::recursion_depth_limit levels have been entered, @ref + merge_diff_iteratively diffs what is left without the call stack. + */ + static basic_json merge_diff_recursively(const basic_json& source, const basic_json& target, const std::size_t depth) + { + JSON_ASSERT(source.is_object() && target.is_object()); + + if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) + { + return merge_diff_iteratively(source, target); + } + + basic_json result(value_t::object); + + for (auto it = source.begin(); it != source.end(); ++it) + { + auto itf = target.find(it.key()); + if (itf != target.end()) + { + if (it.value().is_object() && itf.value().is_object()) + { + auto diff = merge_diff_recursively(it.value(), itf.value(), depth + 1); + if (!diff.empty()) + { + result[it.key()] = std::move(diff); + } + } + else if (it.value() != itf.value()) + { + result[it.key()] = itf.value(); + } + } + else + { + result[it.key()] = value_t::null; + } + } + + merge_diff_added_members(result, source, target); + return result; + } + + /*! + @brief @ref merge_diff of two objects without the call stack + + Produces the same patch as @ref merge_diff_recursively, keeping the objects + being diffed on an explicit stack instead of descending into them. Two + nested objects are still diffed completely before the next member, so the + patch has its members in the same order as in the recursive version. Only + reached for objects nested more deeply than @ref + detail::recursion_depth_limit. + */ + static basic_json merge_diff_iteratively(const basic_json& source, const basic_json& target) + { + // the patch of source and target, set once their frame is popped + basic_json result; + std::vector stack; + stack.emplace_back(&source, &target); + while (!stack.empty()) + { + // the frame's members are copied out and it is changed through + // stack.back(): pushing a frame for nested objects or popping + // this one would invalidate a reference to it + const basic_json* const s = stack.back().source; + const basic_json* const t = stack.back().target; + const const_iterator it = stack.back().member; + + if (it != s->cend()) + { + const const_iterator itf = t->find(it.key()); + if (itf != t->cend()) + { + if (it.value().is_object() && itf.value().is_object()) + { + // member is advanced once their patch is under its key + stack.emplace_back(&it.value(), &itf.value()); + continue; + } + if (it.value() != itf.value()) + { + stack.back().patch[it.key()] = itf.value(); + } + } + else + { + stack.back().patch[it.key()] = value_t::null; + } + ++stack.back().member; + continue; + } + + merge_diff_added_members(stack.back().patch, *s, *t); + + // these objects are done: continue with the ones they are in + basic_json patch = std::move(stack.back().patch); + stack.pop_back(); + if (stack.empty()) + { + result = std::move(patch); + } + else + { + if (!patch.empty()) + { + stack.back().patch[stack.back().member.key()] = std::move(patch); + } + ++stack.back().member; + } + } + return result; + } + /// @} }; @@ -7581,7 +8473,7 @@ struct formatter // NOLINT(cert-dcl58-c // where needed. #ifndef JSON_NO_AUTOMATIC_UDLS // NOLINTNEXTLINE(misc-header-include-cycle): json_literals.hpp includes this header - #include + #include // IWYU pragma: export #endif #endif // INCLUDE_NLOHMANN_JSON_HPP_ diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp new file mode 100644 index 000000000..85026008a --- /dev/null +++ b/include/nlohmann/json_view.hpp @@ -0,0 +1,1682 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +/****************************************************************************\ + * Zero-copy view of a parsed JSON text. * + * * + * json_document::parse() builds a flat index of the values of a JSON text * + * (16 bytes per value) instead of a tree of basic_json values. Strings and * + * numbers stay in the source text; only strings with escapes are decoded, * + * into one buffer. json_view is a handle to one value of the document, with * + * the read-only part of the basic_json interface; materialize() turns a * + * subtree into the basic_json value that parse() would produce. An editable * + * document (json_editable_document) also has set(), push_back(), insert(), * + * and erase(): edits never write to the source text, and views stay valid. * + * * + * The source text must outlive a document that borrows it (lvalue byte * + * containers, C strings); rvalue strings, streams, and other inputs are * + * owned by the document. * +\****************************************************************************/ + +#ifndef INCLUDE_NLOHMANN_JSON_VIEW_HPP_ +#define INCLUDE_NLOHMANN_JSON_VIEW_HPP_ + +#include // all_of, min +#include // array +#include // nullptr_t, size_t // IWYU pragma: keep +#include // uint8_t, uint32_t +#include // FILE +#include // memcpy, strlen +#include // istream // IWYU pragma: keep +#include // distance, input_iterator_tag, iterator_traits +#include // map +#include // unique_ptr +#ifndef JSON_NO_IO + #include // ostream // IWYU pragma: keep +#endif +#include // string +#include // tuple_element, tuple_size // IWYU pragma: keep +#include // decay, enable_if, integral_constant, is_arithmetic, is_base_of, is_integral, is_same, remove_cv, remove_extent +#include // unordered_map +#include // forward, move +#include // vector + +#include // IWYU pragma: export + +// json.hpp provides the library's types and macros used below (it includes +// the headers that define them); json_view.hpp must not include them again, +// because the amalgamated json_view.hpp only includes json.hpp +// IWYU pragma: no_include +// IWYU pragma: no_include "nlohmann/detail/abi_config.hpp" +// IWYU pragma: no_include "nlohmann/detail/abi_macros.hpp" +// IWYU pragma: no_include "nlohmann/detail/input/input_adapters.hpp" +// IWYU pragma: no_include "nlohmann/detail/json_pointer.hpp" +// IWYU pragma: no_include "nlohmann/detail/meta/cpp_future.hpp" +// IWYU pragma: no_include "nlohmann/detail/string_concat.hpp" +// IWYU pragma: no_include "nlohmann/detail/value_t.hpp" +// IWYU pragma: no_include "nlohmann/json.hpp" +// IWYU pragma: no_include "nlohmann/json_fwd.hpp" + +// the view builds on internals of the library: both must be the same version +#if NLOHMANN_JSON_VERSION_MAJOR != 3 || NLOHMANN_JSON_VERSION_MINOR != 12 || NLOHMANN_JSON_VERSION_PATCH != 0 + #error "json_view.hpp requires json.hpp of the same version (3.12.0)" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN + +/*! +@brief read-only handle to one value of a basic_json_document + +Trivially copyable (two pointers). Valid as long as the document is alive and +has not been re-parsed, and as long as a borrowed source text is alive. +*/ +template +class basic_json_view +{ + using node = detail::view::node; + using document_data = detail::view::document_data; + /// how the index is walked (with edits only for editable documents) + using navigation = detail::view::navigation; + + public: + using value_t = detail::value_t; + using string_t = typename BasicJsonType::string_t; + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using json_pointer = typename BasicJsonType::json_pointer; + using size_type = std::size_t; + /// std::string_view from C++17 on + using string_view_t = detail::view::string_ref; + /// forward iterator over elements (arrays) or member values (objects) + using iterator = detail::view::view_iterator; + using const_iterator = iterator; + /// a (key, value) item of items() + using item = detail::view::view_item; + + /// an invalid view (type() == value_t::discarded) + basic_json_view() noexcept = default; + + ////////// + // type // + ////////// + + NLOHMANN_VIEW_ALWAYS_INLINE value_t type() const noexcept + { + return m_node != nullptr ? static_cast(m_node->kind) : value_t::discarded; + } + + bool is_null() const noexcept + { + return type() == value_t::null; + } + + bool is_boolean() const noexcept + { + return type() == value_t::boolean; + } + + bool is_number() const noexcept + { + return is_number_integer() || is_number_float(); + } + + bool is_number_integer() const noexcept + { + return type() == value_t::number_integer || type() == value_t::number_unsigned; + } + + bool is_number_unsigned() const noexcept + { + return type() == value_t::number_unsigned; + } + + bool is_number_float() const noexcept + { + return type() == value_t::number_float; + } + + bool is_string() const noexcept + { + return type() == value_t::string; + } + + bool is_array() const noexcept + { + return type() == value_t::array; + } + + bool is_object() const noexcept + { + return type() == value_t::object; + } + + /// always false: JSON text has no binary values + bool is_binary() const noexcept + { + return false; + } + + bool is_primitive() const noexcept + { + return is_null() || is_string() || is_boolean() || is_number(); + } + + bool is_structured() const noexcept + { + return is_array() || is_object(); + } + + /// the root of a failed parse with allow_exceptions == false, or a + /// default-constructed view + bool is_discarded() const noexcept + { + return type() == value_t::discarded; + } + + /// the name of the type, as basic_json::type_name() + const char* type_name() const noexcept + { + return detail::value_type_name(type()); + } + + ////////////// + // capacity // + ////////////// + + /// the number of elements (arrays, objects), 0 for null and discarded, + /// 1 otherwise, as basic_json::size() + size_type size() const noexcept + { + switch (type()) + { + case value_t::null: + case value_t::discarded: + return 0; + case value_t::array: + case value_t::object: + return m_node->len; + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + default: + return 1; + } + } + + /// as basic_json::empty() + bool empty() const noexcept + { + switch (type()) + { + case value_t::null: + case value_t::discarded: + return true; + case value_t::array: + case value_t::object: + return m_node->len == 0; + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + default: + return false; + } + } + + //////////////////// + // element access // + //////////////////// + + /// the value of the member with this key (the first one, should the key + /// occur more than once); a discarded view if there is none, or if this + /// is a discarded view (so that v["a"]["b"] is safe). Throws type_error.305 + /// if this is any other value but an object. + NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view operator[](string_view_t key) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_object())) + { + if (is_discarded()) + { + return {}; + } + detail::view::throw_type_error(detail::exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", type_name()); + } + return lookup(key); + } + + basic_json_view operator[](const char* key) const + { + return operator[](string_view_t(key)); + } + + basic_json_view operator[](const string_t& key) const + { + return operator[](string_view_t(key.data(), key.size())); + } + + /// the element at this index; a discarded view if the index is out of + /// range, or if this is a discarded view. Throws type_error.305 if this is + /// any other value but an array. + basic_json_view operator[](size_type idx) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_array())) + { + if (is_discarded()) + { + return {}; + } + detail::view::throw_type_error(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(); + } + + /// any other integer type (int, unsigned, long, std::int64_t, ...; a + /// single overload for size_type alone would be ambiguous for all of them + /// and for const char*); negative values are out of range + template < typename IntegerType, typename std::enable_if < detail::view::is_index_type::value, int >::type = 0 > + basic_json_view operator[](IntegerType idx) const + { + return operator[](detail::view::to_index(idx)); + } + + /// the value a JSON pointer refers to; a discarded view if a key is + /// missing or an index is out of range, or if this is a discarded view. + /// Other errors throw what const basic_json::operator[] throws. + basic_json_view operator[](const json_pointer& ptr) const + { + if (NLOHMANN_VIEW_UNLIKELY(is_discarded())) + { + return {}; + } + return detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::unchecked); + } + + /// the value of the member with this key (the first one, should the key + /// occur more than once). Throws type_error.304 if this is not an object, + /// and out_of_range.403 if there is no such member. + basic_json_view at(string_view_t key) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_object())) + { + detail::view::throw_type_error(detail::exception_id::at_wrong_type, "cannot use at() with ", type_name()); + } + const basic_json_view r = lookup(key); + if (NLOHMANN_VIEW_UNLIKELY(r.is_discarded())) + { + detail::view::throw_key_not_found(std::string(key.data(), key.size())); + } + return r; + } + + basic_json_view at(const char* key) const + { + return at(string_view_t(key)); + } + + basic_json_view at(const string_t& key) const + { + return at(string_view_t(key.data(), key.size())); + } + + /// the element at this index. Throws type_error.304 if this is not an + /// array, and out_of_range.401 if the index is out of range. + basic_json_view at(size_type idx) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_array())) + { + detail::view::throw_type_error(detail::exception_id::at_wrong_type, "cannot use at() with ", type_name()); + } + if (NLOHMANN_VIEW_UNLIKELY(idx >= m_node->len)) + { + detail::view::throw_array_index_out_of_range(idx); + } + return element(idx); + } + + /// any other integer type, see operator[]; negative values are out of + /// range + template < typename IntegerType, typename std::enable_if < detail::view::is_index_type::value, int >::type = 0 > + basic_json_view at(IntegerType idx) const + { + return at(detail::view::to_index(idx)); + } + + /// the value a JSON pointer refers to; throws what basic_json::at() + /// throws if it cannot be resolved + basic_json_view at(const json_pointer& ptr) const + { + return detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::checked); + } + + /// the member with this key converted to T, or the default value if there + /// is no such member (the first one, should the key occur more than + /// once). Throws type_error.306 if this is not an object. + template::value, int>::type = 0> + T value(string_view_t key, const T& default_value) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_object())) + { + detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name()); + } + const basic_json_view r = lookup(key); + return r.is_discarded() ? default_value : r.template get(); + } + + string_t value(string_view_t key, const char* default_value) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_object())) + { + detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name()); + } + // (the default is made only if there is no such member) + const basic_json_view r = lookup(key); + return r.is_discarded() ? string_t(default_value) : r.template get(); + } + + /// the value a JSON pointer refers to converted to T, or the default + /// value if the pointer cannot be resolved. Throws type_error.306 if this + /// is neither an object nor an array. + template::value, int>::type = 0> + T value(const json_pointer& ptr, const T& default_value) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_structured())) + { + detail::view::throw_type_error(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); + return r.is_discarded() ? default_value : r.template get(); + } + + string_t value(const json_pointer& ptr, const char* default_value) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_structured())) + { + detail::view::throw_type_error(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); + return r.is_discarded() ? string_t(default_value) : r.template get(); + } + + /// the first element or member value; a primitive value itself. Throws + /// invalid_iterator.214 for null, discarded views, and empty containers. + basic_json_view front() const + { + const iterator it = begin(); + if (NLOHMANN_VIEW_UNLIKELY(it == end())) + { + detail::view::throw_invalid_iterator(detail::exception_id::iterator_value_unavailable, "cannot get value"); + } + return *it; + } + + /// the last element or member value (linear in the size); a primitive + /// value itself. Throws invalid_iterator.214 for null, discarded views, + /// and empty containers. + basic_json_view back() const + { + if (is_structured() && m_node->len != 0) + { + return unedited() ? basic_json_view(m_doc, detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0)) + : basic_json_view(m_doc, navigation::value(detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0))); + } + return front(); + } + + //////////// + // lookup // + //////////// + + /// an iterator to the member with this key (the first one, should the + /// key occur more than once), or end(); end() also for non-objects + iterator find(string_view_t key) const + { + if (!is_object()) + { + return end(); + } + const node* const k = find_key(key); + return k != nullptr ? iterator(m_doc, k, true) : end(); + } + + iterator find(const char* key) const + { + return find(string_view_t(key)); + } + + iterator find(const string_t& key) const + { + return find(string_view_t(key.data(), key.size())); + } + + /// whether this is an object with a member with this key + bool contains(string_view_t key) const + { + return is_object() && find_key(key) != nullptr; + } + + bool contains(const char* key) const + { + return contains(string_view_t(key)); + } + + bool contains(const string_t& key) const + { + return contains(string_view_t(key.data(), key.size())); + } + + /// whether a JSON pointer can be resolved (never throws, as + /// basic_json::contains()) + bool contains(const json_pointer& ptr) const + { + return !detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::contains).is_discarded(); + } + + /// the number of members with this key (more than 1 if the key occurs + /// more than once); 0 if this is not an object + size_type count(string_view_t key) const + { + return is_object() ? detail::view::count_members(*m_doc, m_node, key.data(), key.size()) : 0; + } + + size_type count(const char* key) const + { + return count(string_view_t(key)); + } + + size_type count(const string_t& key) const + { + return count(string_view_t(key.data(), key.size())); + } + + /////////////// + // iteration // + /////////////// + + /// the first element or member value, in document order; a primitive + /// value is a range of one element (itself), null an empty range + NLOHMANN_VIEW_ALWAYS_INLINE iterator begin() const noexcept + { + if (NLOHMANN_VIEW_LIKELY(is_structured())) + { + return iterator(m_doc, navigation::first(*m_doc, m_node), is_object()); + } + return iterator(m_doc, m_node, false); + } + + NLOHMANN_VIEW_ALWAYS_INLINE iterator end() const noexcept + { + if (NLOHMANN_VIEW_LIKELY(is_structured())) + { + return iterator(m_doc, navigation::end(*m_doc, m_node), is_object()); + } + return iterator(m_doc, (is_null() || is_discarded()) ? m_node : m_node + 1, false); + } + + iterator cbegin() const noexcept + { + return begin(); + } + + iterator cend() const noexcept + { + return end(); + } + + /// (key, value) items; the key of an array element is its index + detail::view::view_items items() const noexcept + { + return detail::view::view_items(*this); + } + + //////////////// + // conversion // + //////////////// + + /// the value converted to T, as BasicJsonType::get(): arithmetic types, + /// strings (string_view_t without a copy), std::nullptr_t, std::vector, + /// maps with string keys, and views are converted directly; other types + /// through materialize().get() + template + NLOHMANN_VIEW_ALWAYS_INLINE T get() const + { + return get_impl(detail::view::value_tag {}, detail::priority_tag<2> {}); + } + + /// as BasicJsonType::get_to(): the types get() converts directly are + /// assigned, all others are passed to from_json with v as it is + template + T& get_to(T& v) const + { + get_to_impl(v, detail::view::is_direct_value {}); + return v; + } + + /// the string, without a copy; valid as long as the view is. Throws + /// type_error.302 for other types. + string_view_t get_string() const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_string())) + { + detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be string, but is ", type_name()); + } + return {m_doc->str(*m_node), m_node->len}; + } + + /// the text of a number as it appears in the source (e.g. "1.50", "1E2", + /// or an integer with more digits than any number type holds). Throws + /// type_error.302 for other types. + string_view_t number_token() const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_number())) + { + detail::view::throw_type_error(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)}; + } + + /////////////////// + // serialization // + /////////////////// + + /// how dump() writes numbers + enum class number_format + { + /// as basic_json::dump(): integers canonically, floats with the + /// library's shortest round-trip digits ("1.5", "100.0", "1e+100") + shortest, + /// the number text of the source as it is ("1.50", "1E2", "-0", all + /// digits of a long integer) + source, + }; + + /// the text of this value; with number_format::shortest, the output of + /// ordered_json::parse(text).dump() with the same arguments (members in + /// document order, all of them should a key occur more than once) + string_t dump(const int indent = -1, const char indent_char = ' ', const bool ensure_ascii = false, + const number_format numbers = number_format::shortest) const + { + string_t out; + if (m_node == nullptr) + { + out = ""; // as basic_json::dump() of a discarded value + return out; + } + detail::view::dump_style style; + style.pretty = indent >= 0; + style.indent = indent >= 0 ? static_cast(indent) : 0; + style.indent_char = indent_char; + style.ensure_ascii = ensure_ascii; + style.source_numbers = numbers == number_format::source; + // the compact text is about as long as the source text of the value; + // the compact writer keeps 64 bytes of slack, so that it does not grow + // the buffer just before the end + const std::size_t estimate = source_extent() + (style.pretty ? source_extent() / 2 : 0) + 160; + if (unedited()) + { + detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + } + else + { + detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + } + return out; + } + +#ifndef JSON_NO_IO + /// as operator<< of basic_json: a stream width > 0 is the indentation, + /// the fill character the indentation character + friend std::ostream& operator<<(std::ostream& o, const basic_json_view& v) + { + const bool pretty = o.width() > 0; + const auto indentation = pretty ? o.width() : 0; + o.width(0); + const string_t s = v.dump(pretty ? static_cast(indentation) : -1, o.fill()); + return o.write(s.data(), static_cast(s.size())); + } +#endif + + //////////////// + // comparison // + //////////////// + + /// whether the values parse() would produce for two views are equal, as + /// by BasicJsonType's operator== (numbers by value, objects by their + /// members with duplicate keys resolved as parse() resolves them) + friend bool operator==(const basic_json_view& a, const basic_json_view& b) + { + return detail::view::equal(side(a), side(b)); + } + + friend bool operator!=(const basic_json_view& a, const basic_json_view& b) + { + return !(a == b); + } + + /// a view of an editable document compares with one of a read-only document + template < bool E, typename std::enable_if < E != Editable, int >::type = 0 > + friend bool operator==(const basic_json_view& a, const basic_json_view& b) + { + return detail::view::equal(side(a), detail::view::view_side>(b)); + } + + template < bool E, typename std::enable_if < E != Editable, int >::type = 0 > + friend bool operator!=(const basic_json_view& a, const basic_json_view& b) + { + return !(a == b); + } + + /// whether the value parse() would produce for a view equals a value + friend bool operator==(const basic_json_view& a, const BasicJsonType& j) + { + return detail::view::equal(side(a), json_side_t(j)); + } + + friend bool operator==(const BasicJsonType& j, const basic_json_view& a) + { + return a == j; + } + + friend bool operator!=(const basic_json_view& a, const BasicJsonType& j) + { + return !(a == j); + } + + friend bool operator!=(const BasicJsonType& j, const basic_json_view& a) + { + return !(a == j); + } + + ///////////////// + // materialize // + ///////////////// + + /// the basic_json value of this subtree, as parse() would produce it + /// (a discarded value for a discarded view) + BasicJsonType materialize() const + { + if (m_node == nullptr) + { + return BasicJsonType(value_t::discarded); + } + return unedited() ? detail::view::materialize(*m_doc, m_node) : detail::view::materialize(*m_doc, m_node); + } + + /// byte offset of this value in the source text (for strings: of the + /// first byte after the opening quote); static_cast(-1) for + /// a discarded view and for strings with escapes, which are decoded + std::size_t source_offset() const noexcept + { + return m_node != nullptr && (m_node->flags & (detail::view::node_flags::storage | detail::view::node_flags::moved | detail::view::node_flags::is_new)) == 0 + ? m_node->off : static_cast(-1); + } + + private: + template friend class basic_json_document; + template friend class basic_json_view; + template friend class detail::view::editor; + friend iterator; + + basic_json_view(const document_data* d, const node* n) noexcept + : m_doc(d), m_node(n) + {} + + using json_side_t = detail::view::json_side; + + static detail::view::view_side side(const basic_json_view& v) noexcept + { + return detail::view::view_side(v); + } + + /// the template argument as a runtime condition: testing it directly in + /// `Editable && ...` makes the condition constant, which MSVC reports as + /// C4127 + static bool editable() noexcept + { + return Editable; + } + + /// the number of source bytes of this value (estimated for values with + /// decoded strings); the estimate sizes the output buffer of dump() + std::size_t source_extent() const noexcept + { + if (editable() && m_doc->edits != nullptr) + { + // positions of moved and new values are not source offsets; the root + // (the first node) spans the source text and every edit + const bool is_root = m_node == m_doc->nodes; + return is_root ? m_doc->size + m_doc->edits->text_used : 64; + } + const node* const end = m_doc->nodes + m_doc->node_count; + if ((m_node->flags & detail::view::node_flags::storage) != 0) + { + return m_node->len; + } + // the value ends where the next node in the source begins; nodes + // with decoded strings (their offset is in the arena) are skipped, + // but only a few of them, to keep the walk short + const node* next = document_data::after(m_node); + for (int skipped = 0; next != end && skipped < 16; ++skipped, ++next) + { + if ((next->flags & detail::view::node_flags::storage) == 0) + { + return next->off >= m_node->off ? next->off - m_node->off : 0; + } + } + if (next == end) + { + return m_doc->size - m_node->off; + } + // the end is unknown: assume a few bytes per node, the output buffer + // grows should the value be larger + const auto nodes = static_cast(document_data::after(m_node) - m_node); + return (std::min)(m_doc->size - m_node->off, static_cast(1024) + (nodes * 16)); + } + + /// the value of the first member with this key, or a discarded view + /// (object required) + NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view lookup(string_view_t key) const noexcept + { + if (unedited()) + { + const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + return k != nullptr ? basic_json_view(m_doc, k + 1) : basic_json_view(); + } + const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + return k != nullptr ? basic_json_view(m_doc, navigation::value(k + 1)) : basic_json_view(); + } + + /// whether the document has not been edited yet: until the first edit, its index is walked like that of a + /// read-only document (nothing is moved or linked), which is cheaper + NLOHMANN_VIEW_ALWAYS_INLINE bool unedited() const noexcept + { + return !navigation::editable() || m_doc->edits == nullptr; + } + + /// the member of this object with @a key (the last one if the key occurs more than once), or nullptr + NLOHMANN_VIEW_ALWAYS_INLINE const node* find_key(string_view_t key) const + { + return find_key(key, std::integral_constant {}); + } + + NLOHMANN_VIEW_ALWAYS_INLINE const node* find_key(string_view_t key, std::true_type /*editable*/) const + { + // (an editable document without edits is looked up as one that cannot be edited) + return unedited() ? detail::view::find_member(*m_doc, m_node, key.data(), key.size()) + : detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + } + + NLOHMANN_VIEW_ALWAYS_INLINE const node* find_key(string_view_t key, std::false_type /*editable*/) const + { + return detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + } + + /// the element of an array at an index below its size + NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view element(size_type idx) const + { + if (unedited()) + { + return basic_json_view(m_doc, detail::view::element_at(*m_doc, m_node, idx)); + } + return basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))); + } + + // --- get() dispatch --- + + bool get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_boolean())) + { + detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be boolean, but is ", type_name()); + } + return (m_node->flags & detail::view::node_flags::is_true) != 0; + } + + template::value, int>::type = 0> + NLOHMANN_VIEW_ALWAYS_INLINE T get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const + { + 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()); + } + return detail::view::arithmetic_value(*m_doc, *m_node); + } + + std::nullptr_t get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_null())) + { + detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be null, but is ", type_name()); + } + return nullptr; + } + + string_view_t get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const + { + return get_string(); + } + + template + std::basic_string get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const + { + const string_view_t s = get_string(); + return std::basic_string(s.data(), s.size()); + } + + BasicJsonType get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const + { + return materialize(); + } + + basic_json_view get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const noexcept + { + return *this; + } + + template + std::vector get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const + { + return detail::view::vector_value(*this); + } + + template::value, int>::type = 0> + std::map get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const + { + return detail::view::map_value>(*this); + } + + template::value, int>::type = 0> + std::unordered_map get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const + { + return detail::view::map_value>(*this); + } + + /// everything else through the BasicJsonType value (from_json included) + template + T get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<0> /*unused*/) const + { + return materialize().template get(); + } + + // --- get_to() dispatch --- + + template + void get_to_impl(T& v, std::true_type /*direct*/) const + { + v = get(); + } + + /// from_json may keep parts of v (members a JSON object lacks, say) + template + void get_to_impl(T& v, std::false_type /*direct*/) const + { + materialize().get_to(v); + } + + const document_data* m_doc = nullptr; + const node* m_node = nullptr; +}; + +/*! +@brief a parsed JSON text: owns the node index (and, optionally, the text) + +Borrowed parses keep a pointer to the caller's text, which must outlive the +document. Owned parses (parse_copy, rvalue std::string, streams, and inputs +that are not contiguous byte ranges) keep their own copy. +*/ +template +class basic_json_document +{ + using document_data = detail::view::document_data; + + static_assert(sizeof(typename BasicJsonType::number_integer_t) == 8 && sizeof(typename BasicJsonType::number_unsigned_t) == 8, + "json_view supports 64-bit integer types only"); + + public: + using view_type = basic_json_view; + using value_t = detail::value_t; + + /// an empty (discarded) document + basic_json_document() = default; + basic_json_document(basic_json_document&&) noexcept = default; + basic_json_document& operator=(basic_json_document&&) noexcept = default; + basic_json_document(const basic_json_document&) = delete; + basic_json_document& operator=(const basic_json_document&) = delete; + ~basic_json_document() = default; + + ///////////// + // parsing // + ///////////// + + /// parse a JSON text; contiguous byte inputs are borrowed, everything else + /// (and rvalue std::string) is owned + template + NLOHMANN_VIEW_NODISCARD + static basic_json_document parse(InputType&& input, + const bool allow_exceptions = true, + const bool ignore_comments = false, + const bool ignore_trailing_commas = false) + { + basic_json_document d; + d.read(std::forward(input), allow_exceptions, ignore_comments, ignore_trailing_commas); + return d; + } + + /// parse(ptr, len) does not compile: len would convert to allow_exceptions + /// and ptr be read as a C string (as for the overloads of parse_copy, + /// accept, and read below) + template + static typename std::enable_if::value, basic_json_document>::type + parse(InputType&& input, IntegerType value, Flags&&... flags) = delete; + + /// parse [first, last) + template::iterator_category>::value, int>::type = 0> + NLOHMANN_VIEW_NODISCARD + static basic_json_document parse(IteratorType first, IteratorType last, + const bool allow_exceptions = true, + const bool ignore_comments = false, + const bool ignore_trailing_commas = false) + { + basic_json_document d; + d.read_range(first, last, allow_exceptions, ignore_comments, ignore_trailing_commas); + return d; + } + + /// parse a copy of the input; the document does not depend on it afterwards + template + NLOHMANN_VIEW_NODISCARD + static basic_json_document parse_copy(InputType&& input, + const bool allow_exceptions = true, + const bool ignore_comments = false, + const bool ignore_trailing_commas = false) + { + basic_json_document d; + std::string buf; + append_input(buf, std::forward(input)); + d.build_owned(std::move(buf), allow_exceptions, ignore_comments, ignore_trailing_commas); + return d; + } + + template + static typename std::enable_if::value, basic_json_document>::type + parse_copy(InputType&& input, IntegerType value, Flags&&... flags) = delete; + + /// check whether the input is valid JSON (the result of basic_json::accept) + template + static bool accept(InputType&& input, const bool ignore_comments = false, const bool ignore_trailing_commas = false) + { + basic_json_document d; + d.read(std::forward(input), false, ignore_comments, ignore_trailing_commas); + return !d.is_discarded(); + } + + template + static typename std::enable_if::value, bool>::type + accept(InputType&& input, IntegerType value, Flags&&... flags) = delete; + + /// parse into this document, reusing its memory + template + // flawfinder: ignore (a member function, not POSIX read()) + void read(InputType&& input, + const bool allow_exceptions = true, + const bool ignore_comments = false, + const bool ignore_trailing_commas = false) + { + read_kind(std::forward(input), allow_exceptions, ignore_comments, ignore_trailing_commas, + std::integral_constant::value> {}); + } + + template + typename std::enable_if::value, void>::type + // flawfinder: ignore (a member function, not POSIX read()) + read(InputType&& input, IntegerType value, Flags&&... flags) = delete; + + //////////// + // access // + //////////// + + /// the root value (discarded if parsing failed without exceptions) + view_type root() const& noexcept + { + if (!m_data || m_data->discarded) + { + return view_type(); + } + return view_type(m_data.get(), m_data->nodes); + } + + /// deleted: the view of a temporary document would dangle + view_type root() const&& = delete; + + bool is_discarded() const noexcept + { + return !m_data || m_data->discarded; + } + + /// the parsed text + typename view_type::string_view_t source() const noexcept + { + return m_data ? typename view_type::string_view_t(m_data->src, m_data->size) : typename view_type::string_view_t(); + } + + /// whether the document holds its own copy of the text + bool owns_source() const noexcept + { + return m_data && ((!m_data->owned.empty() && m_data->src == m_data->owned.data()) || !m_data->owned_image.empty()); + } + + /// number of index nodes (values plus object keys) + std::size_t node_count() const noexcept + { + return m_data ? m_data->node_count : 0; + } + + /// bytes held by the document (index, decoded strings, and owned text or image) + std::size_t memory_usage() const noexcept + { + if (!m_data) + { + return 0; + } + return sizeof(document_data) + (m_data->inline_capacity * sizeof(detail::view::node)) + + (m_data->nodes != m_data->inline_nodes ? m_data->node_capacity * sizeof(detail::view::node) : 0) + + m_data->arena.capacity() + m_data->owned.capacity() + m_data->owned_image.capacity() + + (m_data->indexes.capacity() * sizeof(document_data::object_index)) + (m_data->index_slots.capacity() * sizeof(std::uint32_t)) + + (m_data->large_objects.capacity() * sizeof(std::uint32_t)) + + (m_data->edits != nullptr ? m_data->edits->bytes : 0); + } + + /// release unused capacity of the index and the decoded strings; like + /// std::vector::shrink_to_fit, this invalidates the views of the document + /// (take new ones from root()) + void shrink_to_fit() + { + if (!m_data) + { + return; + } + using detail::view::node; + document_data& d = *m_data; + + // allocate everything first, so that an exception leaves the document + // unchanged + // (the decoded strings of a loaded image stay in the image) + const bool arena_in_use = d.base[1] == d.arena.data(); + const bool shrink_arena = d.arena.capacity() > d.arena.size(); + std::vector arena(shrink_arena && arena_in_use ? d.arena : std::vector()); + // (edits link to the nodes of the index, which then stays in place) + const bool shrink_nodes = d.nodes != d.inline_nodes && d.node_count != d.node_capacity && d.edits == nullptr; + const bool into_header = d.node_count <= d.inline_capacity; + std::vector indexes(d.indexes.capacity() > d.indexes.size() ? d.indexes : std::vector()); + std::vector index_slots(d.index_slots.capacity() > d.index_slots.size() ? d.index_slots : std::vector()); + node* fresh = (shrink_nodes && !into_header) ? static_cast(::operator new (d.node_count * sizeof(node))) : d.inline_nodes; + + if (shrink_nodes) + { + if (d.node_count != 0) + { + std::memcpy(fresh, d.nodes, d.node_count * sizeof(node)); + } + ::operator delete (d.nodes); + d.nodes = fresh; + d.node_capacity = into_header ? d.inline_capacity : d.node_count; + } + if (shrink_arena) + { + d.arena.swap(arena); + if (arena_in_use) + { + d.base[1] = d.arena.data(); + } + } + if (d.indexes.capacity() > d.indexes.size()) + { + d.indexes.swap(indexes); + } + if (d.index_slots.capacity() > d.index_slots.size()) + { + d.index_slots.swap(index_slots); + } + } + + //////////// + // images // + //////////// + + /// how load() checks an image (full, bounds, or none) + using image_check = detail::view::image_check; + + /// The document as an image that load() reads without parsing: the node + /// index, the text, and the decoded strings. An edited document is + /// written in its current state (floats that are not finite become null, + /// as in dump()). + std::vector save() const + { + if (NLOHMANN_VIEW_UNLIKELY(!m_data || m_data->discarded)) + { + detail::view::throw_type_error(detail::exception_id::document_image_unsupported, "cannot save a discarded json_document"); + } + return detail::view::save_image(*m_data); + } + + /// Read an image written by save(). The image is borrowed: it must stay + /// alive and unchanged while the document is used. + NLOHMANN_VIEW_NODISCARD + static basic_json_document load(const std::uint8_t* image, std::size_t size, const image_check check = image_check::full) + { + basic_json_document d; + d.ensure_data(nullptr, 0); + detail::view::load_image(*d.m_data, image, size, check); + return d; + } + + /// read an image (borrowed) + NLOHMANN_VIEW_NODISCARD + static basic_json_document load(const std::vector& image, const image_check check = image_check::full) + { + return load(image.data(), image.size(), check); + } + + /// read an image and keep it (no copy) + NLOHMANN_VIEW_NODISCARD + static basic_json_document load(std::vector&& image, const image_check check = image_check::full) + { + basic_json_document d; + d.ensure_data(nullptr, 0); + d.m_data->owned_image = std::move(image); + detail::view::load_image(*d.m_data, d.m_data->owned_image.data(), d.m_data->owned_image.size(), check); + return d; + } + + /////////// + // edits // + /////////// + + // The source text is never written; new values go to storage owned by + // the document. A view keeps referring to the same value: after an + // assignment it sees the new value, and edits elsewhere do not affect it. + // A view of an erased value keeps its last value. An edit of an + // array/object invalidates the iterators over it. Values are accepted as + // views (of any document), BasicJsonType values, and everything + // BasicJsonType can be constructed from. + + using string_view_t = typename view_type::string_view_t; + using json_pointer = typename BasicJsonType::json_pointer; + + /// replace a value (a view of this document); returns a view of it + template + view_type set(view_type target, V&& value) + { + return editor().set(target, std::forward(value)); + } + + /// set a member (added if missing; a null value becomes an object); + /// returns a view of the member value + template + view_type set(view_type object, string_view_t key, V&& value) + { + return editor().set(object, key, std::forward(value)); + } + + /// assign an existing array element; returns a view of it + template::value, int>::type = 0> + view_type set(view_type array, I idx, V && value) + { + return editor().set(array, index(idx), std::forward(value)); + } + + /// set the value at a JSON pointer: its parent must exist; an object + /// member is set (added if missing), an array element assigned, and "-" + /// or the size of the array appends. A null parent becomes what + /// basic_json's operator[](json_pointer) makes of it: an array for "-" + /// and for digits (padded with nulls up to the index), an object otherwise. + template + view_type set(const json_pointer& ptr, V&& value) + { + if (ptr.empty()) + { + return set(root(), std::forward(value)); + } + const view_type parent = root().at(ptr.parent_pointer()); + const auto& token = ptr.back(); + if (parent.is_null()) + { + if (token == "-") + { + return push_back(parent, std::forward(value)); + } + const bool digits = std::all_of(token.begin(), token.end(), [](const char c) + { + return c >= '0' && c <= '9'; + }); + if (digits) + { + // (an invalid index is an error before the parent changes) + const std::size_t idx = pointer_index(token); + if (idx >= 0xFFFFFFFFu) + { + detail::view::throw_array_index_out_of_range(idx); + } + for (std::size_t i = 0; i < idx; ++i) + { + push_back(parent, nullptr); + } + return push_back(parent, std::forward(value)); + } + } + if (parent.is_array()) + { + const std::size_t idx = token == "-" ? parent.size() : pointer_index(token); + if (idx == parent.size()) + { + return push_back(parent, std::forward(value)); + } + return set(parent, idx, std::forward(value)); + } + return set(parent, string_view_t(token.data(), token.size()), std::forward(value)); + } + + /// append to an array (a null value becomes an array); returns a view of + /// the new element + template + view_type push_back(view_type array, V&& value) + { + return editor().push_back(array, std::forward(value)); + } + + /// insert into an array before position idx (idx <= size()); returns a + /// view of the new element + template::value, int>::type = 0> + view_type insert(view_type array, I idx, V && value) + { + return editor().insert(array, index(idx), std::forward(value)); + } + + /// remove all members with this key; returns their number + std::size_t erase(view_type object, string_view_t key) + { + return editor().erase(object, key); + } + + /// remove an array element + template::value, int>::type = 0> + void erase(view_type array, I idx) + { + editor().erase(array, index(idx)); + } + + /// remove the value at a JSON pointer; returns the number of removed + /// values + std::size_t erase(const json_pointer& ptr) + { + if (ptr.empty()) + { + detail::view::throw_out_of_range(detail::exception_id::patch_on_root, "JSON pointer has no parent"); + } + const view_type parent = root().at(ptr.parent_pointer()); + const auto& token = ptr.back(); + if (parent.is_array()) + { + erase(parent, pointer_index(token)); + return 1; + } + return erase(parent, string_view_t(token.data(), token.size())); + } + + private: + using input_kind = detail::view::input_kind; + + detail::view::editor editor() + { + static_assert(Editable, "only an editable document can be edited: use basic_json_document (json_editable_document)"); + if (NLOHMANN_VIEW_UNLIKELY(!m_data || m_data->discarded)) + { + detail::view::throw_invalid_iterator(detail::exception_id::iterator_from_other_value, "view does not belong to this document"); + } + return detail::view::editor(*m_data); + } + + /// an index (out_of_range.401 if negative) + template + static std::size_t index(I idx) + { + return index(idx, std::is_signed {}); + } + + template + static std::size_t index(I idx, std::true_type /*signed*/) + { + if (idx < 0) + { + detail::view::throw_array_index_out_of_range(idx); + } + return static_cast(idx); + } + + template + static std::size_t index(I idx, std::false_type /*unsigned*/) + { + return static_cast(idx); + } + + /// the array index of a JSON pointer token (json_pointer's rules) + template + static std::size_t pointer_index(const StringType& token) + { + std::size_t idx = 0; + const detail::view::index_status status = detail::view::array_index(token, idx); + if (status != detail::view::index_status::ok) + { + detail::view::throw_array_index_error(status, token); + } + return idx; + } + + /// create the storage (sized for the input) on first use + void ensure_data(const char* src, std::size_t size) + { + if (!m_data) + { + m_data.reset(document_data::create(detail::view::estimate_nodes(src, size))); + } + } + + /// parse a buffer the document takes ownership of (it replaces the + /// previous owned copy) + void build_owned(std::string&& buf, bool allow_exceptions, bool comments, bool trailing_commas) + { + ensure_data(buf.data(), buf.size()); + m_data->owned = std::move(buf); + build(m_data->owned.data(), m_data->owned.size(), allow_exceptions, comments, trailing_commas, true, true); + } + + /// parse a copy of the input; the copy is appended to the cleared owned + /// buffer, so that its capacity is reused + template + void build_copy(T&& input, bool allow_exceptions, bool comments, bool trailing_commas) + { + if (!m_data) + { + std::string buf; + append_input(buf, std::forward(input)); + build_owned(std::move(buf), allow_exceptions, comments, trailing_commas); + return; + } + document_data& d = *m_data; + // (the previous text is gone from here on: should reading the input + // throw, the document is discarded) + d.discarded = true; + d.src = ""; + d.size = 0; + d.owned.clear(); + append_input(d.owned, std::forward(input)); + build(d.owned.data(), d.owned.size(), allow_exceptions, comments, trailing_commas, true, true); + } + + /// 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) + { + ensure_data(src, size); + document_data& d = *m_data; + if (!owned) + { + d.owned.clear(); + } + d.owned_image.clear(); + d.src = src; + d.size = size; + d.node_count = 0; + d.edits.reset(); // (views of the previous text end here anyway) + d.base[2] = nullptr; + d.arena.clear(); + d.indexes.clear(); + d.index_slots.clear(); + d.large_objects.clear(); + d.discarded = true; + detail::view::parse_failure failure; + bool ok = false; + if (NLOHMANN_VIEW_UNLIKELY(size > detail::view::max_input_size())) + { + failure.code = detail::view::error_code::input_too_large; + } + else + { + ok = detail::view::build < typename BasicJsonType::number_float_t, !detail::abi_config::strict_nul_handling > (d, src, size, comments, trailing_commas, sentinel, failure); + } + if (NLOHMANN_VIEW_LIKELY(ok)) + { + d.base[0] = d.src; + d.base[1] = d.arena.data(); + d.arena_size = d.arena.size(); + detail::view::build_object_indexes(d); + std::vector().swap(d.large_objects); // (only needed while parsing) + d.discarded = false; + return; + } + std::vector().swap(d.large_objects); + if (allow_exceptions) + { + detail::view::throw_parse_failure(failure, src, size, comments, trailing_commas); + } + } + + // --- input dispatch (see detail::view::input_kind) --- + + void read_kind(std::string&& s, bool ae, bool c, bool tc, std::integral_constant /*unused*/) + { + build_owned(std::move(s), ae, c, tc); + } + + template + void read_kind(CharT* s, bool ae, bool c, bool tc, std::integral_constant /*unused*/) + { + if (s == nullptr) + { + build("", 0, ae, c, tc, false, true); + return; + } + const char* cs = reinterpret_cast(s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + // C strings are null-terminated, as for json::parse(const char*) + // flawfinder: ignore + build(cs, std::strlen(cs), ae, c, tc, false, true); + } + + template + void read_kind(Array& a, bool ae, bool c, bool tc, std::integral_constant /*unused*/) + { + using CharT = typename std::remove_cv::type>::type; + const std::size_t n = std::extent::value; + // a trailing NUL (string literals) is not part of the text, as for + // parse(), and serves as sentinel + const bool terminated = n > 0 && a[n - 1] == 0 && (!detail::abi_config::strict_nul_handling || std::is_same::value); + build(reinterpret_cast(&a[0]), terminated ? n - 1 : n, ae, c, tc, false, terminated); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + } + + template + void read_kind(const T& s, bool ae, bool c, bool tc, std::integral_constant /*unused*/) + { + // std::basic_string guarantees data()[size()] == 0: use it as sentinel + const auto size = static_cast(s.size()); + build(size == 0 ? "" : reinterpret_cast(s.data()), size, ae, c, tc, false, // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + detail::view::is_std_string::value || size == 0); + } + + template + void read_kind(const T& s, bool ae, bool c, bool tc, std::integral_constant /*unused*/) + { + build_copy(s, ae, c, tc); + } + + template + void read_kind(CharT* s, bool ae, bool c, bool tc, std::integral_constant /*unused*/) + { + if (s == nullptr) + { + build("", 0, ae, c, tc, false, true); + return; + } + // NUL-terminated, as for the single-byte C strings + build_copy(detail::view::make_adapter_input(detail::input_adapter(s, s + std::char_traits::type>::length(s))), ae, c, tc); + } + + template + void read_kind(T&& input, bool ae, bool c, bool tc, std::integral_constant /*unused*/) + { + build_copy(std::forward(input), ae, c, tc); + } + + template + void read_range(IteratorType first, IteratorType last, bool ae, bool c, bool tc) + { + using value_type = typename std::remove_cv::value_type>::type; + // borrow the range where the library's input adapter scans it as one + // block: pointers and, in C++20, contiguous iterators (std::vector, + // std::string, ...) over single bytes + read_range_impl(first, last, ae, c, tc, std::integral_constant < bool, std::is_integral::value + && detail::iterator_input_adapter::supports_bulk_scan > {}); + } + + template + void read_range_impl(IteratorType first, IteratorType last, bool ae, bool c, bool tc, std::true_type /*contiguous bytes*/) + { + const auto size = static_cast(std::distance(first, last)); + build(size == 0 ? "" : reinterpret_cast(&*first), size, ae, c, tc, false, size == 0); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + } + + template + void read_range_impl(IteratorType first, IteratorType last, bool ae, bool c, bool tc, std::false_type /*other*/) + { + build_copy(detail::view::make_adapter_input(detail::input_adapter(first, last)), ae, c, tc); + } + + /// append the text of an input to buf: a contiguous byte input as one + /// block, a stream in blocks, an input adapter (made here for any other + /// input) character by character + template + static void append_input(std::string& buf, T&& input) + { + using D = typename std::decay::type; + // NOLINTBEGIN(readability-avoid-nested-conditional-operator): a constant expression of C++11 + append_input_impl(buf, std::forward(input), std::integral_constant < int, detail::is_contiguous_byte_container::value ? 0 + : std::is_base_of::value ? 1 + : detail::view::is_adapter_input::value ? 2 + : detail::view::is_c_stream::value ? 4 : 3 > {}); + // NOLINTEND(readability-avoid-nested-conditional-operator) + } + + template + static void append_input_impl(std::string& buf, const T& s, std::integral_constant /*contiguous bytes*/) + { + buf.append(reinterpret_cast(s.data()), static_cast(s.size())); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + } + + template + static void append_input_impl(std::string& buf, Stream&& is, std::integral_constant /*stream*/) // NOLINT(cppcoreguidelines-missing-std-forward): read in place, as by the library's stream adapter + { + detail::view::append_stream(buf, is); + } + + template + static void append_input_impl(std::string& buf, detail::view::adapter_input&& input, std::integral_constant /*input adapter*/) + { + detail::view::append_adapter(buf, std::move(input.ia)); + } + +#ifndef JSON_NO_IO + static void append_input_impl(std::string& buf, std::FILE* file, std::integral_constant /*C stream*/) + { + if (file == nullptr) + { + // (as before: the library's adapter decides what a null stream yields) + detail::view::append_adapter(buf, detail::input_adapter(file)); + return; + } + detail::view::append_file(buf, file); + } +#endif + + template + static void append_input_impl(std::string& buf, T&& input, std::integral_constant /*other*/) + { + detail::view::append_adapter(buf, detail::input_adapter(std::forward(input))); + } + + std::unique_ptr m_data{}; // NOLINT(readability-redundant-member-init) +}; + +/// a parsed JSON text for json +using json_document = basic_json_document; +/// a value of a json_document +using json_view = basic_json_view; +/// a parsed JSON text for ordered_json +using ordered_json_document = basic_json_document; +/// a value of an ordered_json_document +using ordered_json_view = basic_json_view; +/// an editable parsed JSON text for json +using json_editable_document = basic_json_document; +/// a value of a json_editable_document +using json_editable_view = basic_json_view; +/// an editable parsed JSON text for ordered_json +using ordered_json_editable_document = basic_json_document; +/// a value of an ordered_json_editable_document +using ordered_json_editable_view = basic_json_view; + +NLOHMANN_JSON_NAMESPACE_END + +// tuple protocol for the items of basic_json_view::items() (structured bindings) +namespace std // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) +{ + +#if defined(__clang__) + // Fix: https://github.com/nlohmann/json/issues/1401 + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wmismatched-tags" +#endif +template +class tuple_size<::nlohmann::detail::view::view_item> // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) + : public std::integral_constant {}; + +template +class tuple_element> // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) +{ + public: + using type = decltype(std::declval<::nlohmann::detail::view::view_item>().template get()); +}; +#if defined(__clang__) + #pragma clang diagnostic pop +#endif + +} // namespace std + +#include // IWYU pragma: keep + +#endif // INCLUDE_NLOHMANN_JSON_VIEW_HPP_ diff --git a/include/nlohmann/ordered_map.hpp b/include/nlohmann/ordered_map.hpp index 74539d376..831261219 100644 --- a/include/nlohmann/ordered_map.hpp +++ b/include/nlohmann/ordered_map.hpp @@ -23,6 +23,7 @@ #include #include #include +#include NLOHMANN_JSON_NAMESPACE_BEGIN @@ -90,6 +91,28 @@ private: return self.end(); } + /// @brief throw the exception of a failed at() lookup + /// @note kept out of line so that at_impl() stays small enough to inline + /// @throw std::out_of_range always + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_key_not_found() + { + JSON_THROW(std::out_of_range("key not found")); + } + + /// @brief shared implementation of the const and non-const at() overloads + /// @throw std::out_of_range if @a key is not found + template + static auto at_impl(Self& self, const KeyType& key) -> decltype((self.begin()->second)) + { + const auto it = find_impl(self, key); + if (it == self.end()) + { + throw_key_not_found(); + } + return it->second; + } + /// @brief remove the entry @a it points to, preserving order /// @note keys are not movable, so the tail is destroyed and re-constructed in place void erase_at(iterator it) @@ -102,6 +125,19 @@ private: Container::pop_back(); } + /// @brief remove the entry with the given key; returns the number of removed entries + template + size_type erase_impl(const K& key) + { + const auto it = find_impl(*this, key); + if (it != this->end()) + { + erase_at(it); + return 1; + } + return 0; + } + public: template::value, int> = 0> @@ -130,6 +166,17 @@ public: return {std::prev(this->end()), true}; } + /// @brief add an element at the end without checking whether the key exists + /// @note The caller must guarantee that @a key is not yet contained, otherwise + /// the container holds duplicate keys. This avoids the linear search of + /// emplace() and operator[] when many keys known to be new are added. + template::value, int> = 0> + void emplace_new_key(const key_type& key, V && t) + { + append(key, std::forward(t)); + } + T& operator[](const key_type& key) { return emplace(key, T{}).first->second; @@ -156,70 +203,38 @@ public: T& at(const key_type& key) { - const auto it = find_impl(*this, key); - if (it == this->end()) - { - JSON_THROW(std::out_of_range("key not found")); - } - return it->second; + return at_impl(*this, key); } template::value, int> = 0> T & at(KeyType && key) // NOLINT(cppcoreguidelines-missing-std-forward) { - const auto it = find_impl(*this, key); - if (it == this->end()) - { - JSON_THROW(std::out_of_range("key not found")); - } - return it->second; + return at_impl(*this, key); } const T& at(const key_type& key) const { - const auto it = find_impl(*this, key); - if (it == this->end()) - { - JSON_THROW(std::out_of_range("key not found")); - } - return it->second; + return at_impl(*this, key); } template::value, int> = 0> const T & at(KeyType && key) const // NOLINT(cppcoreguidelines-missing-std-forward) { - const auto it = find_impl(*this, key); - if (it == this->end()) - { - JSON_THROW(std::out_of_range("key not found")); - } - return it->second; + return at_impl(*this, key); } size_type erase(const key_type& key) { - const auto it = find_impl(*this, key); - if (it != this->end()) - { - erase_at(it); - return 1; - } - return 0; + return erase_impl(key); } template::value, int> = 0> size_type erase(KeyType && key) // NOLINT(cppcoreguidelines-missing-std-forward) { - const auto it = find_impl(*this, key); - if (it != this->end()) - { - erase_at(it); - return 1; - } - return 0; + return erase_impl(key); } iterator erase(iterator pos) @@ -247,6 +262,14 @@ public: // ^ ^ // first last + // Note on conformance: before C++20, [basic.life]/8 did not allow an + // object of a type with a const member (like value_type's const Key) + // to transparently replace the destroyed one, so strictly, accessing + // it through the vector's existing pointers would have required + // std::launder (which does not exist before C++17). C++20 dropped that + // condition (P1971R0, NB comment US 041). Compilers have always treated + // this pattern as intended, so it is kept deliberately. + // Since we cannot move const Keys, we re-construct them in place. // We start at first and re-construct (viz. copy) the elements from // the back of the vector. Example for the first iteration: diff --git a/nlohmann_json.natvis b/nlohmann_json.natvis index 02e9d28ed..75898cd7d 100644 --- a/nlohmann_json.natvis +++ b/nlohmann_json.natvis @@ -1411,6 +1411,746 @@ + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + null @@ -2447,6 +3187,746 @@ + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + null @@ -3335,6 +4815,746 @@ + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + null @@ -4075,6 +6295,746 @@ + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + null @@ -4667,6 +7627,746 @@ + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + null @@ -5111,6 +8811,746 @@ + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + null @@ -5407,6 +9847,746 @@ + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded + + + *(m_data.m_value.object),view(simple) + + + *(m_data.m_value.array),view(simple) + + + + null @@ -5555,22 +10735,22 @@ - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -5578,7 +10758,7 @@ - + {second} second @@ -5587,64 +10767,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_sbu8_dsbs after + 3.12.0 (#5238) and in nlohmann::json_abi_sbu8_dsbs::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -5652,7 +10832,7 @@ - + {second} second @@ -5661,64 +10841,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_sbu8_dsbs_v3_12_0 after + 3.12.0 (#5238) and in nlohmann::json_abi_sbu8_dsbs_v3_12_0::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -5726,7 +10906,7 @@ - + {second} second @@ -5735,64 +10915,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_sbu8_dufio after + 3.12.0 (#5238) and in nlohmann::json_abi_sbu8_dufio::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -5800,7 +10980,7 @@ - + {second} second @@ -5809,64 +10989,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_sbu8_dufio_v3_12_0 after + 3.12.0 (#5238) and in nlohmann::json_abi_sbu8_dufio_v3_12_0::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -5874,7 +11054,7 @@ - + {second} second @@ -5883,64 +11063,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_sbu8_ns after + 3.12.0 (#5238) and in nlohmann::json_abi_sbu8_ns::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -5948,7 +11128,7 @@ - + {second} second @@ -5957,64 +11137,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_sbu8_ns_v3_12_0 after + 3.12.0 (#5238) and in nlohmann::json_abi_sbu8_ns_v3_12_0::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -6022,7 +11202,7 @@ - + {second} second @@ -6031,64 +11211,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_sbu8_vns after + 3.12.0 (#5238) and in nlohmann::json_abi_sbu8_vns::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -6096,7 +11276,7 @@ - + {second} second @@ -6105,64 +11285,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_sbu8_vns_v3_12_0 after + 3.12.0 (#5238) and in nlohmann::json_abi_sbu8_vns_v3_12_0::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -6170,7 +11350,7 @@ - + {second} second @@ -6179,64 +11359,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_sbu8_vssse3 after + 3.12.0 (#5238) and in nlohmann::json_abi_sbu8_vssse3::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -6244,7 +11424,7 @@ - + {second} second @@ -6253,64 +11433,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_sbu8_vssse3_v3_12_0 after + 3.12.0 (#5238) and in nlohmann::json_abi_sbu8_vssse3_v3_12_0::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -6318,7 +11498,7 @@ - + {second} second @@ -6327,64 +11507,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_ekmo_dsbs after + 3.12.0 (#5238) and in nlohmann::json_abi_ekmo_dsbs::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -6392,7 +11572,7 @@ - + {second} second @@ -6401,64 +11581,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_ekmo_dsbs_v3_12_0 after + 3.12.0 (#5238) and in nlohmann::json_abi_ekmo_dsbs_v3_12_0::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -6466,7 +11646,7 @@ - + {second} second @@ -6475,64 +11655,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_ekmo_dufio after + 3.12.0 (#5238) and in nlohmann::json_abi_ekmo_dufio::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -6540,7 +11720,7 @@ - + {second} second @@ -6549,64 +11729,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_ekmo_dufio_v3_12_0 after + 3.12.0 (#5238) and in nlohmann::json_abi_ekmo_dufio_v3_12_0::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -6614,7 +11794,7 @@ - + {second} second @@ -6623,64 +11803,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_ekmo_ns after + 3.12.0 (#5238) and in nlohmann::json_abi_ekmo_ns::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -6688,7 +11868,7 @@ - + {second} second @@ -6697,64 +11877,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_ekmo_ns_v3_12_0 after + 3.12.0 (#5238) and in nlohmann::json_abi_ekmo_ns_v3_12_0::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -6762,7 +11942,7 @@ - + {second} second @@ -6771,64 +11951,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_ekmo_vns after + 3.12.0 (#5238) and in nlohmann::json_abi_ekmo_vns::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -6836,7 +12016,7 @@ - + {second} second @@ -6845,64 +12025,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_ekmo_vns_v3_12_0 after + 3.12.0 (#5238) and in nlohmann::json_abi_ekmo_vns_v3_12_0::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -6910,7 +12090,7 @@ - + {second} second @@ -6919,64 +12099,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_ekmo_vssse3 after + 3.12.0 (#5238) and in nlohmann::json_abi_ekmo_vssse3::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -6984,7 +12164,7 @@ - + {second} second @@ -6993,64 +12173,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_ekmo_vssse3_v3_12_0 after + 3.12.0 (#5238) and in nlohmann::json_abi_ekmo_vssse3_v3_12_0::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -7058,7 +12238,7 @@ - + {second} second @@ -7067,64 +12247,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_dsbs_dufio after + 3.12.0 (#5238) and in nlohmann::json_abi_dsbs_dufio::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -7132,7 +12312,7 @@ - + {second} second @@ -7141,64 +12321,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_dsbs_dufio_v3_12_0 after + 3.12.0 (#5238) and in nlohmann::json_abi_dsbs_dufio_v3_12_0::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -7206,7 +12386,7 @@ - + {second} second @@ -7215,64 +12395,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_dsbs_ns after + 3.12.0 (#5238) and in nlohmann::json_abi_dsbs_ns::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -7280,7 +12460,7 @@ - + {second} second @@ -7289,64 +12469,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_dsbs_ns_v3_12_0 after + 3.12.0 (#5238) and in nlohmann::json_abi_dsbs_ns_v3_12_0::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -7354,7 +12534,7 @@ - + {second} second @@ -7363,64 +12543,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_dsbs_vns after + 3.12.0 (#5238) and in nlohmann::json_abi_dsbs_vns::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -7428,7 +12608,7 @@ - + {second} second @@ -7437,64 +12617,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_dsbs_vns_v3_12_0 after + 3.12.0 (#5238) and in nlohmann::json_abi_dsbs_vns_v3_12_0::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -7502,7 +12682,7 @@ - + {second} second @@ -7511,64 +12691,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_dsbs_vssse3 after + 3.12.0 (#5238) and in nlohmann::json_abi_dsbs_vssse3::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -7576,7 +12756,7 @@ - + {second} second @@ -7585,64 +12765,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_dsbs_vssse3_v3_12_0 after + 3.12.0 (#5238) and in nlohmann::json_abi_dsbs_vssse3_v3_12_0::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -7650,7 +12830,7 @@ - + {second} second @@ -7659,64 +12839,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_dufio_ns after + 3.12.0 (#5238) and in nlohmann::json_abi_dufio_ns::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -7724,7 +12904,7 @@ - + {second} second @@ -7733,64 +12913,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_dufio_ns_v3_12_0 after + 3.12.0 (#5238) and in nlohmann::json_abi_dufio_ns_v3_12_0::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -7798,7 +12978,7 @@ - + {second} second @@ -7807,64 +12987,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_dufio_vns after + 3.12.0 (#5238) and in nlohmann::json_abi_dufio_vns::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -7872,7 +13052,7 @@ - + {second} second @@ -7881,64 +13061,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_dufio_vns_v3_12_0 after + 3.12.0 (#5238) and in nlohmann::json_abi_dufio_vns_v3_12_0::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -7946,7 +13126,7 @@ - + {second} second @@ -7955,64 +13135,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_dufio_vssse3 after + 3.12.0 (#5238) and in nlohmann::json_abi_dufio_vssse3::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -8020,7 +13200,7 @@ - + {second} second @@ -8029,64 +13209,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_dufio_vssse3_v3_12_0 after + 3.12.0 (#5238) and in nlohmann::json_abi_dufio_vssse3_v3_12_0::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -8094,7 +13274,7 @@ - + {second} second @@ -8103,64 +13283,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_ns_vns after + 3.12.0 (#5238) and in nlohmann::json_abi_ns_vns::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -8168,7 +13348,7 @@ - + {second} second @@ -8177,64 +13357,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_ns_vns_v3_12_0 after + 3.12.0 (#5238) and in nlohmann::json_abi_ns_vns_v3_12_0::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -8242,7 +13422,7 @@ - + {second} second @@ -8251,64 +13431,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_ns_vssse3 after + 3.12.0 (#5238) and in nlohmann::json_abi_ns_vssse3::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -8316,7 +13496,7 @@ - + {second} second @@ -8325,64 +13505,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_ns_vssse3_v3_12_0 after + 3.12.0 (#5238) and in nlohmann::json_abi_ns_vssse3_v3_12_0::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -8390,7 +13570,7 @@ - + {second} second @@ -8399,64 +13579,64 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_vns_vssse3 after + 3.12.0 (#5238) and in nlohmann::json_abi_vns_vssse3::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) @@ -8464,7 +13644,7 @@ - + {second} second @@ -8473,29495 +13653,43 @@ - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + against the derived object, so m_data is accessible here. The class lives in nlohmann::json_abi_vns_vssse3_v3_12_0 after + 3.12.0 (#5238) and in nlohmann::json_abi_vns_vssse3_v3_12_0::detail up to 3.12.0, so both names are listed. --> + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - + *(m_data.m_value.array),view(simple) - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded + + null + {*(m_data.m_value.object)} + {*(m_data.m_value.array)} + {*(m_data.m_value.string)} + {m_data.m_value.boolean} + {m_data.m_value.number_integer} + {m_data.m_value.number_unsigned} + {m_data.m_value.number_float} + discarded - + *(m_data.m_value.object),view(simple) - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - - {second} - - second - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - - *(m_data.m_value.array),view(simple) - - - - - - null - {*(m_data.m_value.object)} - {*(m_data.m_value.array)} - {*(m_data.m_value.string)} - {m_data.m_value.boolean} - {m_data.m_value.number_integer} - {m_data.m_value.number_unsigned} - {m_data.m_value.number_float} - discarded - - - *(m_data.m_value.object),view(simple) - - + *(m_data.m_value.array),view(simple) diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index efeb06dd8..668236b42 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -19,6 +19,7 @@ #define INCLUDE_NLOHMANN_JSON_HPP_ #include // all_of, find, for_each, none_of +#include // array #include // isnan #include // nullptr_t, ptrdiff_t, size_t #include // uint8_t @@ -29,6 +30,7 @@ #endif // JSON_NO_IO #include // make_move_iterator, random_access_iterator_tag #include // numeric_limits +#include // map #include // unique_ptr #include // swap, operator!= #include // length_error @@ -42,6 +44,11 @@ // each of these, not only the forward declarations from json_fwd.hpp, so IWYU's suggestion to // drop them (nothing in this file otherwise names the type) would break every downstream // translation unit that relies on basic_json<>'s defaults actually being usable. +// export: client code names what these headers declare (nlohmann::json and ordered_json from +// json_fwd.hpp, adl_serializer for specializations, json_ref behind brace initialization, the +// NLOHMANN_DEFINE_* and NLOHMANN_JSON_NAMESPACE_* macros, and the literals at the end of this file), and include analyzers must +// attribute it to this header, not tell clients to include the internal ones (issue #5812). +// tests/iwyu_consumer checks this. // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ @@ -112,6 +119,14 @@ #define JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS 0 #endif +#ifndef JSON_DISABLE_STREAM_BULK_SCAN + #define JSON_DISABLE_STREAM_BULK_SCAN 0 +#endif + +#ifndef JSON_DISABLE_UNLOCKED_FILE_IO + #define JSON_DISABLE_UNLOCKED_FILE_IO 0 +#endif + #if JSON_DIAGNOSTICS #define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS _diag #else @@ -160,14 +175,44 @@ #define NLOHMANN_JSON_ABI_TAG_OBJECTS_FOR_ENUM_KEYED_MAPS #endif +#if JSON_DISABLE_STREAM_BULK_SCAN + #define NLOHMANN_JSON_ABI_TAG_DISABLE_STREAM_BULK_SCAN _dsbs +#else + #define NLOHMANN_JSON_ABI_TAG_DISABLE_STREAM_BULK_SCAN +#endif + +#if JSON_DISABLE_UNLOCKED_FILE_IO + #define NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO _dufio +#else + #define NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO +#endif + +#ifdef JSON_NO_SIMD + #define NLOHMANN_JSON_ABI_TAG_NO_SIMD _ns +#else + #define NLOHMANN_JSON_ABI_TAG_NO_SIMD +#endif + +#ifdef JSON_VIEW_NO_SIMD + #define NLOHMANN_JSON_ABI_TAG_VIEW_NO_SIMD _vns +#else + #define NLOHMANN_JSON_ABI_TAG_VIEW_NO_SIMD +#endif + +#ifdef JSON_VIEW_USE_SSSE3 + #define NLOHMANN_JSON_ABI_TAG_VIEW_USE_SSSE3 _vssse3 +#else + #define NLOHMANN_JSON_ABI_TAG_VIEW_USE_SSSE3 +#endif + #ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION #define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0 #endif // Construct the namespace ABI tags component -#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h) json_abi ## a ## b ## c ## d ## e ## f ## g ## h -#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c, d, e, f, g, h) \ - NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h) +#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l, m) json_abi ## a ## b ## c ## d ## e ## f ## g ## h ## i ## j ## k ## l ## m +#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c, d, e, f, g, h, i, j, k, l, m) \ + NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l, m) #define NLOHMANN_JSON_ABI_TAGS \ NLOHMANN_JSON_ABI_TAGS_CONCAT( \ @@ -178,7 +223,12 @@ NLOHMANN_JSON_ABI_TAG_PRECISE_STREAM_POSITION, \ NLOHMANN_JSON_ABI_TAG_STRICT_NUL_HANDLING, \ NLOHMANN_JSON_ABI_TAG_STRICT_BINARY_UTF8, \ - NLOHMANN_JSON_ABI_TAG_OBJECTS_FOR_ENUM_KEYED_MAPS) + NLOHMANN_JSON_ABI_TAG_OBJECTS_FOR_ENUM_KEYED_MAPS, \ + NLOHMANN_JSON_ABI_TAG_DISABLE_STREAM_BULK_SCAN, \ + NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO, \ + NLOHMANN_JSON_ABI_TAG_NO_SIMD, \ + NLOHMANN_JSON_ABI_TAG_VIEW_NO_SIMD, \ + NLOHMANN_JSON_ABI_TAG_VIEW_USE_SSSE3) // Construct the namespace version component #define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \ @@ -234,7 +284,7 @@ -#include // transform +#include // min, transform #include // array #include // forward_list #include // inserter, front_inserter, end @@ -284,7 +334,6 @@ #include // uint8_t #include // numeric_limits #include // string -#include // is_signed // #include // __ _____ _____ _____ @@ -2524,6 +2573,22 @@ JSON_HEDLEY_DIAGNOSTIC_POP #endif #endif +// the byte order of the target where the compiler tells it (std::endian is +// C++20 and cannot select code in #if): JSON_LITTLE_ENDIAN_TARGET or +// JSON_BIG_ENDIAN_TARGET is 1, or both are 0 where it is unknown (code then +// handles the bytes one at a time); MSVC and x86 targets are little-endian +#if defined(_MSC_VER) || defined(__x86_64__) || defined(__i386__) \ + || (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) + #define JSON_LITTLE_ENDIAN_TARGET 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) + #define JSON_BIG_ENDIAN_TARGET 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) +#elif defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + #define JSON_LITTLE_ENDIAN_TARGET 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) + #define JSON_BIG_ENDIAN_TARGET 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) +#else + #define JSON_LITTLE_ENDIAN_TARGET 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) + #define JSON_BIG_ENDIAN_TARGET 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) +#endif + #ifdef JSON_HAS_CPP_17 #define JSON_INLINE_VARIABLE inline #else @@ -2649,7 +2714,7 @@ JSON_HEDLEY_DIAGNOSTIC_POP return ej_pair.first == e; \ }); \ if (it != std::end(m)) j = it->second; \ - else ::nlohmann::detail::templated_json_throw(nlohmann::detail::out_of_range::create(410,"enum value out of range for " #ENUM_TYPE, nullptr)); \ + else ::nlohmann::detail::templated_json_throw(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range,"enum value out of range for " #ENUM_TYPE, nullptr)); \ } \ template \ inline void from_json(const BasicJsonType& j, ENUM_TYPE& e) \ @@ -2664,7 +2729,7 @@ JSON_HEDLEY_DIAGNOSTIC_POP return ej_pair.second == j; \ }); \ if (it != std::end(m)) e = it->first; \ - else ::nlohmann::detail::templated_json_throw(nlohmann::detail::out_of_range::create(410, nlohmann::detail::concat("enum value out of range for " #ENUM_TYPE ": ", j.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace)), &j)); \ + else ::nlohmann::detail::templated_json_throw(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 ": ", j.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace)), &j)); \ } // Ugly macros to avoid uglier copy-paste when specializing basic_json. They @@ -3283,6 +3348,39 @@ enum class value_t : std::uint8_t discarded ///< discarded by the parser callback function }; +/*! +@brief the name of a JSON type, as returned by basic_json::type_name() + +Used in exception messages; also by code that reports types without a +basic_json value at hand (such as the zero-copy view). +*/ +inline const char* value_type_name(const value_t t) noexcept +{ + switch (t) + { + case value_t::null: + return "null"; + case value_t::object: + return "object"; + case value_t::array: + return "array"; + case value_t::string: + return "string"; + case value_t::boolean: + return "boolean"; + case value_t::binary: + return "binary"; + case value_t::discarded: + return "discarded"; + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + return "number"; + default: + return "invalid"; + } +} + /*! @brief comparison operator for JSON types @@ -3333,6 +3431,64 @@ inline bool operator<(const value_t lhs, const value_t rhs) noexcept } #endif +/*! +@brief whether a value of the given type owns heap memory + +Strings, binaries, objects, and arrays are stored as pointers to allocated +values; all other types are stored directly in the union. Every enumerator is +listed so that -Wswitch-enum flags this function when a new type is added. +*/ +inline bool owns_memory(const value_t t) noexcept +{ + switch (t) + { + case value_t::object: + case value_t::array: + case value_t::string: + case value_t::binary: + return true; + + case value_t::null: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::discarded: + default: + return false; + } +} + +/*! +@brief whether a value of the given type is neither an object nor an array + +Unlike basic_json::is_primitive(), this includes value_t::discarded: it names +the types that iterators walk with a primitive_iterator_t (a single value, or no +value for null and discarded) and that JSON Pointer resolution and the +serializer treat as leaves. Every enumerator is listed so that -Wswitch-enum +flags this function when a new type is added. +*/ +inline bool is_primitive(const value_t t) noexcept +{ + switch (t) + { + case value_t::object: + case value_t::array: + return false; + + case value_t::null: + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + return true; + } +} + /*! @brief compare an integer with a floating point number without precision loss @@ -3363,13 +3519,16 @@ FloatType compare_integer_with_float(const IntegerType i, const FloatType f) noe // values of IntegerType lie in [-bound, bound) when signed and in // [0, bound) when unsigned; digits excludes the sign bit, so bound is a - // power of two that the float represents exactly - const FloatType bound = std::ldexp(static_cast(1), std::numeric_limits::digits); + // power of two that the float represents exactly; the signedness comes + // from numeric_limits as well, because std::is_signed is false for class + // types such as 128-bit or multiprecision integers + using limits = std::numeric_limits; + const FloatType bound = std::ldexp(static_cast(1), limits::digits); if (f >= bound) { return ordered(-1); } - if (std::is_signed::value ? (f < -bound) : (f < static_cast(0))) + if (limits::is_signed ? (f < -bound) : (f < static_cast(0))) { return ordered(1); } @@ -3410,6 +3569,7 @@ NLOHMANN_JSON_NAMESPACE_END #include // size_t +#include // move // #include @@ -3418,6 +3578,25 @@ NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail { +/// append @a s to @a out, escaped like @ref escape does +template +inline void escape_into(StringType& out, const StringType& s) +{ + const std::size_t size = s.size(); + std::size_t run = 0; + for (std::size_t i = 0; i < size; ++i) + { + const char c = s[i]; + if (c == '~' || c == '/') + { + out.append(s.data() + run, i - run); + out.append(c == '~' ? "~0" : "~1", 2); + run = i + 1; + } + } + out.append(s.data() + run, size - run); +} + /*! * @brief string escaping as described in RFC 6901 (Sect. 4) * @param[in] s string to escape @@ -3426,39 +3605,28 @@ namespace detail * Note the order of escaping "~" to "~0" and "/" to "~1" is important. * * The string is rebuilt in a single pass, appending whole runs between the - * characters that need escaping. Scanning with find_first_of() keeps the - * common case -- nothing to escape -- as fast as a single search, while - * repeated replace() calls would move the tail of the string once per - * escaped character. + * characters that need escaping, while repeated replace() calls would move + * the tail of the string once per escaped character. @ref escape_into appends + * the escaped string to an existing one. */ template inline StringType escape(const StringType& s) { - auto next_special = [&s](std::size_t from) + // a single scan for both special characters; nothing to escape is the common case + const std::size_t size = s.size(); + std::size_t i = 0; + while (i < size && s[i] != '~' && s[i] != '/') { - const auto tilde = s.find_first_of('~', from); - const auto slash = s.find_first_of('/', from); - return tilde < slash ? tilde : slash; // npos is the largest value - }; - - auto pos = next_special(0); - if (pos == StringType::npos) + ++i; + } + if (i == size) { return s; } StringType result; - result.reserve(s.size() + 2); - - std::size_t run = 0; - while (pos != StringType::npos) - { - result.append(s.data() + run, pos - run); - result.append(s[pos] == '~' ? "~0" : "~1", 2); - run = pos + 1; - pos = next_special(run); - } - result.append(s.data() + run, s.size() - run); + result.reserve(size + 2); + escape_into(result, s); return result; } @@ -3503,7 +3671,7 @@ inline void unescape(StringType& s) pos = s.find_first_of('~', run); } result.append(s.data() + run, s.size() - run); - s = result; + s = std::move(result); } } // namespace detail @@ -3564,18 +3732,27 @@ NLOHMANN_JSON_NAMESPACE_END #include // size_t -#include // conditional, enable_if, false_type, integral_constant, is_constructible, is_integral, is_same, remove_cv, remove_reference, true_type +#include // conditional, enable_if, false_type, integral_constant, is_arithmetic, is_constructible, is_integral, is_same, remove_cv, remove_reference, true_type #include // index_sequence, make_index_sequence, index_sequence_for // #include +#ifdef JSON_HAS_CPP_17 + #include // clamp +#endif + NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail { -template -using uncvref_t = typename std::remove_cv::type>::type; +#ifdef __cpp_lib_remove_cvref + template + using uncvref_t = std::remove_cvref_t; +#else + template + using uncvref_t = typename std::remove_cv::type>::type; +#endif #ifdef JSON_HAS_CPP_14 @@ -3715,6 +3892,20 @@ struct static_const constexpr T static_const::value; #endif +/// @a v bounded to [@a lo, @a hi] (requires !(hi < lo)); like std::clamp, +/// but by value, so that static constexpr members can be passed without an +/// out-of-line definition before C++17 +template::value, int> = 0> +constexpr T clamp(T v, T lo, T hi) noexcept +{ +#ifdef __cpp_lib_clamp + return std::clamp(v, lo, hi); +#else + // NOLINTNEXTLINE(readability-avoid-nested-conditional-operator): a constant expression of C++11 + return v < lo ? lo : (hi < v ? hi : v); +#endif +} + } // namespace detail NLOHMANN_JSON_NAMESPACE_END @@ -3730,9 +3921,11 @@ NLOHMANN_JSON_NAMESPACE_END #include // numeric_limits +#include // map, multimap #include // char_traits #include // tuple #include // false_type, is_constructible, is_integral, is_same, true_type +#include // unordered_map, unordered_multimap #include // declval #include // vector #if defined(__cpp_lib_byte) && __cpp_lib_byte >= 201603L @@ -4171,6 +4364,10 @@ struct has_to_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> template using detect_key_compare = typename T::key_compare; +// detects whether two values of type T can be compared with operator== +template +using detect_equal_comparable = decltype(static_cast(std::declval() == std::declval())); + // obtains the actual object key comparator: object_t::key_compare if the // object type defines it, and default_object_comparator_t otherwise // @@ -4306,6 +4503,14 @@ struct char_traits : std::char_traits // is_ functions // /////////////////// +#ifdef __cpp_lib_logical_traits + +using std::conjunction; +using std::disjunction; +using std::negation; + +#else + // https://en.cppreference.com/w/cpp/types/conjunction template struct conjunction : std::true_type { }; template struct conjunction : B { }; @@ -4323,6 +4528,8 @@ struct disjunction // https://en.cppreference.com/w/cpp/types/negation template struct negation : std::integral_constant < bool, !B::value > { }; +#endif + // Reimplementation of is_constructible and is_default_constructible, due to them being broken for // std::pair and std::tuple until LWG 2367 fix (see https://cplusplus.github.io/LWG/lwg-defects.html#2367). // This causes compile errors in e.g., Clang 3.5 or GCC 4.9. @@ -4678,6 +4885,22 @@ struct is_compatible_integer_type : is_compatible_integer_type_impl {}; +// whether to_json() stores an integer of type CompatibleNumberIntegerType as +// number_integer +template +struct is_compatible_number_integer_type + : is_compatible_integer_type {}; + +// if number_unsigned_t is the same type as number_integer_t, unsigned integers +// are stored as number_integer as well (#2573) +template +struct is_compatible_number_integer_type, + std::is_integral, + std::is_unsigned, + negation>>::value>> + : std::is_constructible {}; + template struct is_compatible_type_impl: std::false_type {}; @@ -4892,6 +5115,31 @@ struct has_capacity : std::integral_constant struct is_ordered_map : has_capacity {}; +// whether T is a specialization of nlohmann::ordered_map, which only ever +// appends members, so they can only move when its capacity changes +template +struct is_nlohmann_ordered_map : std::false_type {}; + +template +struct is_nlohmann_ordered_map> : std::true_type {}; + +// whether adding or erasing elements never moves the other elements of the +// container T; only node-based standard containers are known to guarantee this +template +struct has_stable_elements : std::false_type {}; + +template +struct has_stable_elements> : std::true_type {}; + +template +struct has_stable_elements> : std::true_type {}; + +template +struct has_stable_elements> : std::true_type {}; + +template +struct has_stable_elements> : std::true_type {}; + // to avoid useless casts (see https://github.com/nlohmann/json/issues/2893#issuecomment-889152324) template < typename T, typename U, enable_if_t < !std::is_same::value, int > = 0 > T conditional_static_cast(U value) @@ -5012,8 +5260,31 @@ constexpr bool value_in_range_of(T val) return value_in_range_of_impl1::test(val); } -template -using bool_constant = std::integral_constant; +#ifdef __cpp_lib_bool_constant + using std::bool_constant; +#else + template + using bool_constant = std::integral_constant; +#endif + +// Stands in for number_unsigned_t in overload sets if it is the same type as +// number_integer_t, so that the unsigned overloads do not collide with the +// integer ones. Never constructed. Such a basic_json stores every integer as +// number_integer (#2573). +struct same_as_number_integer_type {}; + +// NumberUnsignedType, or same_as_number_integer_type if it is the same type as +// NumberIntegerType; basic_json uses this one, as it cannot name its own member +// types through an incomplete basic_json (MSVC reports a recursive alias, and +// old GCC versions crash) +template +using number_unsigned_overload_type = typename std::conditional::value, + same_as_number_integer_type, NumberUnsignedType>::type; + +// number_unsigned_t of BasicJsonType, or same_as_number_integer_type if it is +// the same type as number_integer_t +template +using number_unsigned_overload_t = number_unsigned_overload_type; /////////////////////////////////////////////////////////////////////////////// // is_c_string @@ -5237,6 +5508,88 @@ namespace detail // exceptions // //////////////// +/*! +@brief the ids of the exceptions thrown by the library +@note The values are part of the public API: they are the `id` member of the + exceptions and appear in their `what()` messages. +@sa https://json.nlohmann.me/home/exceptions/ +*/ +enum class exception_id : int +{ + // parse_error + syntax_error = 101, ///< unexpected token or invalid literal while parsing JSON + patch_not_an_array = 104, ///< a JSON Patch document is not an array of objects + patch_invalid_operation = 105, ///< a JSON Patch operation is malformed + pointer_index_leading_zero = 106, ///< a JSON Pointer array index has a leading zero + pointer_missing_slash = 107, ///< a JSON Pointer does not start with '/' + pointer_invalid_escape = 108, ///< a JSON Pointer contains an escape other than ~0 and ~1 + pointer_index_not_a_number = 109, ///< a JSON Pointer array index is not a number + unexpected_end_of_input = 110, ///< a binary input ends early, or has bytes left at its end + unexpected_byte = 112, ///< a binary input contains an unexpected byte or invalid length + invalid_string_or_size = 113, ///< a binary input contains an invalid string or size specification + bson_unsupported_type = 114, ///< a BSON record type is not supported + invalid_high_precision_number = 115, ///< a UBJSON/BJData high-precision number cannot be parsed + invalid_document_image = 116, ///< json_document::load() rejects an image + // invalid_iterator + iterators_incompatible = 201, ///< the iterators of a range belong to different values + iterator_from_other_value = 202, ///< an iterator does not belong to the value it is used with + iterator_range_from_other_value = 203, ///< an iterator range does not belong to the value it is used with + iterator_range_out_of_range = 204, ///< an iterator range of a primitive value is not [begin, end) + iterator_out_of_range = 205, ///< an iterator of a primitive value is not begin() + iterator_range_of_null = 206, ///< an iterator range belongs to a null value + iterator_key_not_object = 207, ///< key() is called on an iterator of a non-object + iterator_subscript_on_object = 208, ///< operator[] is called on an iterator of an object + iterator_arithmetic_on_object = 209, ///< an offset operator is used on an iterator of an object + insert_range_incompatible = 210, ///< the iterators of an inserted range belong to different values + insert_range_into_itself = 211, ///< an inserted range belongs to the value it is inserted into + iterators_compare_different_values = 212, ///< iterators of different values are compared + iterator_order_on_object = 213, ///< iterators of an object are compared by order + iterator_value_unavailable = 214, ///< an iterator does not refer to a value + // type_error + object_from_non_pairs = 301, ///< an object is created from an initializer list that is not a list of pairs + type_mismatch = 302, ///< a value has the wrong type for a conversion + incompatible_reference_type = 303, ///< get_ref() is called with a reference type that does not match the value + at_wrong_type = 304, ///< at() is called on a value of the wrong type + subscript_wrong_type = 305, ///< operator[] is called on a value of the wrong type + value_wrong_type = 306, ///< value() is called on a value of the wrong type + erase_wrong_type = 307, ///< erase() is called on a value of the wrong type + push_back_wrong_type = 308, ///< push_back() or operator+= is called on a value of the wrong type + insert_wrong_type = 309, ///< insert() is called on a value of the wrong type + swap_wrong_type = 310, ///< swap() is called on a value of the wrong type + emplace_wrong_type = 311, ///< emplace() or emplace_back() is called on a value of the wrong type + update_wrong_type = 312, ///< update() is called on a value of the wrong type + unflatten_invalid_value = 313, ///< unflatten() finds conflicting paths + unflatten_not_object = 314, ///< unflatten() is called on a non-object + unflatten_value_not_primitive = 315, ///< unflatten() is called on an object with non-primitive values + invalid_utf8 = 316, ///< dump() finds a string that is not valid UTF-8 + type_not_serializable = 317, ///< a value cannot be serialized to the requested binary format + enum_key_duplicate = 318, ///< an enum-keyed map has two keys that serialize to the same string + document_binary_value = 319, ///< a binary value is stored in a json_document + document_image_unsupported = 320, ///< a json_document image cannot be saved or loaded (discarded document, big-endian target) + discarded_value_used = 321, ///< a discarded value is used + // out_of_range + array_index_out_of_range = 401, ///< an array index is out of range + pointer_past_the_end_index = 402, ///< a JSON Pointer uses the array index '-' + key_not_found = 403, ///< an object key is not found + pointer_unresolved = 404, ///< a JSON Pointer reference token cannot be resolved + patch_on_root = 405, ///< the root of a value has no parent, e.g. for a JSON Patch 'remove' or 'add' at the root + number_overflow = 406, ///< a number cannot be stored without overflowing to NaN or INF + integer_too_large = 407, ///< an integer cannot be represented in the binary format + container_too_large = 408, ///< the size of a container in a binary input exceeds the maximal capacity + bson_key_with_null = 409, ///< a BSON key contains U+0000 + value_out_of_range = 410, ///< an enum value is undefined, or a JSON Pointer array index exceeds size_type + patch_add_parent_not_container = 411, ///< the parent of a JSON Patch 'add' target is not a container + length_too_large = 412, ///< a length does not fit into the length field of a binary format + patch_remove_parent_not_container = 413, ///< the parent of a JSON Patch 'remove' target is not a container + patch_move_into_child = 414, ///< a JSON Patch 'move' moves a value into one of its children + subtype_out_of_range = 415, ///< a binary subtype does not fit into one byte + document_too_large = 416, ///< a json_document input, its edit storage, or an image reaches the 32-bit (4 GiB) limit + // other_error + internal_error = 500, ///< unreachable code was reached + patch_test_failed = 501, ///< a JSON Patch 'test' operation failed + size_marker_required = 502 ///< UBJSON/BJData output with type markers requires size markers +}; + /// @brief general exception of the @ref basic_json class /// @sa https://json.nlohmann.me/api/basic_json/exception/ class exception : public std::exception @@ -5260,6 +5613,13 @@ class exception : public std::exception return concat("[json.exception.", ename, '.', std::to_string(id_), "] "); } + /// build the message "[json.exception..] " + template + static std::string what_impl(const std::string& ename, int id_, const std::string& what_arg, BasicJsonContext context) + { + return concat(name(ename, id_), diagnostics(context), what_arg); + } + static std::string diagnostics(std::nullptr_t /*leaf_element*/) { return ""; @@ -5272,48 +5632,33 @@ class exception : public std::exception std::vector tokens; for (const auto* current = leaf_element; current != nullptr && current->m_parent != nullptr; current = current->m_parent) { - switch (current->m_parent->type()) + const auto* parent = current->m_parent; + if (parent->is_array()) { - case value_t::array: + for (std::size_t i = 0; i < parent->m_data.m_value.array->size(); ++i) { - for (std::size_t i = 0; i < current->m_parent->m_data.m_value.array->size(); ++i) + if (&parent->m_data.m_value.array->operator[](i) == current) { - if (¤t->m_parent->m_data.m_value.array->operator[](i) == current) - { - tokens.emplace_back(std::to_string(i)); - break; - } + tokens.emplace_back(std::to_string(i)); + break; } - break; } - - case value_t::object: - { - for (const auto& element : *current->m_parent->m_data.m_value.object) - { - if (&element.second == current) - { - // data() is null-terminated, so a key containing - // a null byte is cut short here rather than - // truncating the whole message at what() - tokens.emplace_back(element.first.data()); - break; - } - } - break; - } - - case value_t::null: // LCOV_EXCL_LINE - case value_t::string: // LCOV_EXCL_LINE - case value_t::boolean: // LCOV_EXCL_LINE - case value_t::number_integer: // LCOV_EXCL_LINE - case value_t::number_unsigned: // LCOV_EXCL_LINE - case value_t::number_float: // LCOV_EXCL_LINE - case value_t::binary: // LCOV_EXCL_LINE - case value_t::discarded: // LCOV_EXCL_LINE - default: // LCOV_EXCL_LINE - break; // LCOV_EXCL_LINE } + else if (parent->is_object()) + { + for (const auto& element : *parent->m_data.m_value.object) + { + if (&element.second == current) + { + // data() is null-terminated, so a key containing + // a null byte is cut short here rather than + // truncating the whole message at what() + tokens.emplace_back(element.first.data()); + break; + } + } + } + // all other parent types (primitives) contribute no token } if (tokens.empty()) @@ -5387,6 +5732,18 @@ class parse_error : public exception return {id_, byte_, w.c_str()}; } + template::value, int> = 0> + static parse_error create(exception_id id_, const position_t& pos, const std::string& what_arg, BasicJsonContext context) + { + return create(static_cast(id_), pos, what_arg, context); + } + + template::value, int> = 0> + static parse_error create(exception_id id_, std::size_t byte_, const std::string& what_arg, BasicJsonContext context) + { + return create(static_cast(id_), byte_, what_arg, context); + } + /*! @brief byte index of the parse error @@ -5417,10 +5774,16 @@ class invalid_iterator : public exception template::value, int> = 0> static invalid_iterator create(int id_, const std::string& what_arg, BasicJsonContext context) { - const std::string w = concat(exception::name("invalid_iterator", id_), exception::diagnostics(context), what_arg); + const std::string w = exception::what_impl("invalid_iterator", id_, what_arg, context); return {id_, w.c_str()}; } + template::value, int> = 0> + static invalid_iterator create(exception_id id_, const std::string& what_arg, BasicJsonContext context) + { + return create(static_cast(id_), what_arg, context); + } + private: JSON_HEDLEY_NON_NULL(3) invalid_iterator(int id_, const char* what_arg) @@ -5435,10 +5798,16 @@ class type_error : public exception template::value, int> = 0> static type_error create(int id_, const std::string& what_arg, BasicJsonContext context) { - const std::string w = concat(exception::name("type_error", id_), exception::diagnostics(context), what_arg); + const std::string w = exception::what_impl("type_error", id_, what_arg, context); return {id_, w.c_str()}; } + template::value, int> = 0> + static type_error create(exception_id id_, const std::string& what_arg, BasicJsonContext context) + { + return create(static_cast(id_), what_arg, context); + } + private: JSON_HEDLEY_NON_NULL(3) type_error(int id_, const char* what_arg) : exception(id_, what_arg) {} @@ -5452,10 +5821,16 @@ class out_of_range : public exception template::value, int> = 0> static out_of_range create(int id_, const std::string& what_arg, BasicJsonContext context) { - const std::string w = concat(exception::name("out_of_range", id_), exception::diagnostics(context), what_arg); + const std::string w = exception::what_impl("out_of_range", id_, what_arg, context); return {id_, w.c_str()}; } + template::value, int> = 0> + static out_of_range create(exception_id id_, const std::string& what_arg, BasicJsonContext context) + { + return create(static_cast(id_), what_arg, context); + } + private: JSON_HEDLEY_NON_NULL(3) out_of_range(int id_, const char* what_arg) : exception(id_, what_arg) {} @@ -5469,10 +5844,16 @@ class other_error : public exception template::value, int> = 0> static other_error create(int id_, const std::string& what_arg, BasicJsonContext context) { - const std::string w = concat(exception::name("other_error", id_), exception::diagnostics(context), what_arg); + const std::string w = exception::what_impl("other_error", id_, what_arg, context); return {id_, w.c_str()}; } + template::value, int> = 0> + static other_error create(exception_id id_, const std::string& what_arg, BasicJsonContext context) + { + return create(static_cast(id_), what_arg, context); + } + private: JSON_HEDLEY_NON_NULL(3) other_error(int id_, const char* what_arg) : exception(id_, what_arg) {} @@ -5499,6 +5880,106 @@ void templated_json_throw(ExceptionType exception) (void)exception; } +/*! +@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] j the value with the wrong type +@throw type_error.302 always +*/ +template +JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_type_must_be(const char* expected, const BasicJsonType& j) +{ + static_cast(expected); // unused when JSON_NOEXCEPTION is defined + static_cast(j); + JSON_THROW(type_error::create(exception_id::type_mismatch, concat("type must be ", expected, ", but is ", j.type_name()), &j)); +} + +/*! +@brief throws because an operation is not supported for the type of @a j +@param[in] id_ the id of the type_error exception (at_wrong_type..update_wrong_type) +@param[in] operation the operation, e.g. "erase()" +@param[in] j the value the operation was called on +@throw type_error always +*/ +template +JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_cannot_use_with(const exception_id id_, const char* operation, const BasicJsonType& j) +{ + static_cast(id_); // unused when JSON_NOEXCEPTION is defined + static_cast(operation); + static_cast(j); + JSON_THROW(type_error::create(id_, concat("cannot use ", operation, " with ", j.type_name()), &j)); +} + +/*! +@brief throws ExceptionType with a fixed message; kept out of line so that callers do not carry the exception construction +*/ +template +JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_literal(const exception_id id_, const char* what_arg, BasicJsonContext context) +{ + static_cast(id_); // unused when JSON_NOEXCEPTION is defined + static_cast(what_arg); + static_cast(context); + JSON_THROW(ExceptionType::create(id_, what_arg, context)); +} + +// JSON Patch errors; constructed out of line so that patch_inplace() does not carry them + +template +JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_patch_add_parent(const BasicJsonType& parent) +{ + static_cast(parent); + JSON_THROW(out_of_range::create(exception_id::patch_add_parent_not_container, concat("cannot add value: the JSON Patch 'add' target's parent is of type ", parent.type_name(), ", but must be an object or array"), &parent)); +} + +template +JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_patch_remove_parent(const BasicJsonType& parent) +{ + static_cast(parent); + JSON_THROW(out_of_range::create(exception_id::patch_remove_parent_not_container, concat("cannot remove value: the JSON Patch 'remove' target's parent is of type ", parent.type_name(), ", but must be an object or array"), &parent)); +} + +template +JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_patch_not_an_array(const BasicJsonType& j) +{ + static_cast(j); + JSON_THROW(parse_error::create(exception_id::patch_not_an_array, 0, "JSON patch must be an array of objects", &j)); +} + +template +JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_patch_missing_member(const StringType& op, const StringType& member, const bool string_type, const BasicJsonType& val) +{ + static_cast(op); + static_cast(member); + static_cast(string_type); + static_cast(val); + // context-sensitive error message + JSON_THROW(parse_error::create(exception_id::patch_invalid_operation, 0, concat((op == "op") ? std::string("operation") : concat("operation '", op, '\''), string_type ? " must have string member '" : " must have member '", member, "'"), &val)); +} + +template +JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_patch_move_into_child(const StringType& from_path, const StringType& path, const BasicJsonType& result) +{ + static_cast(from_path); + static_cast(path); + static_cast(result); + JSON_THROW(out_of_range::create(exception_id::patch_move_into_child, concat("cannot move value: 'from' path '", from_path, "' is a proper prefix of 'path' '", path, "'"), &result)); +} + +template +JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_patch_test_failed(const BasicJsonType& val) +{ + static_cast(val); + JSON_THROW(other_error::create(exception_id::patch_test_failed, concat("unsuccessful: ", val.dump()), &val)); +} + +template +JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_patch_invalid_operation(const StringType& op, const BasicJsonType& val) +{ + static_cast(op); + static_cast(val); + JSON_THROW(parse_error::create(exception_id::patch_invalid_operation, 0, concat("operation value '", op, "' is invalid"), &val)); +} + } // namespace detail NLOHMANN_JSON_NAMESPACE_END @@ -5591,7 +6072,7 @@ inline void from_json(const BasicJsonType& j, typename std::nullptr_t& n) { if (JSON_HEDLEY_UNLIKELY(!j.is_null())) { - JSON_THROW(type_error::create(302, concat("type must be null, but is ", j.type_name()), &j)); + throw_type_must_be("null", j); } n = nullptr; } @@ -5612,29 +6093,31 @@ void from_json(const BasicJsonType& j, std::optional& opt) } #endif // JSON_HAS_CPP_17 -// overloads for basic_json template parameters -template < typename BasicJsonType, typename ArithmeticType, - enable_if_t < std::is_arithmetic::value&& - !std::is_same::value, - int > = 0 > -void get_arithmetic_value(const BasicJsonType& j, ArithmeticType& val) +// read a number value of any of the three number types into val; +// returns false (leaving val untouched) if j is not a number +template +bool get_number_value(const BasicJsonType& j, ArithmeticType& val) { switch (static_cast(j)) { case value_t::number_unsigned: { - val = static_cast(*j.template get_ptr()); - break; + // never null: a basic_json whose number_unsigned_t is number_integer_t + // stores no number_unsigned values (#2573) + const auto* ptr = j.template get_ptr(); + JSON_ASSERT(ptr != nullptr); + val = static_cast(*ptr); + return true; } case value_t::number_integer: { val = static_cast(*j.template get_ptr()); - break; + return true; } case value_t::number_float: { val = static_cast(*j.template get_ptr()); - break; + return true; } case value_t::null: @@ -5645,7 +6128,20 @@ void get_arithmetic_value(const BasicJsonType& j, ArithmeticType& val) case value_t::binary: case value_t::discarded: default: - JSON_THROW(type_error::create(302, concat("type must be number, but is ", j.type_name()), &j)); + return false; + } +} + +// overloads for basic_json template parameters +template < typename BasicJsonType, typename ArithmeticType, + enable_if_t < std::is_arithmetic::value&& + !std::is_same::value, + int > = 0 > +void get_arithmetic_value(const BasicJsonType& j, ArithmeticType& val) +{ + if (JSON_HEDLEY_UNLIKELY(!get_number_value(j, val))) + { + throw_type_must_be("number", j); } } @@ -5654,7 +6150,7 @@ inline void from_json(const BasicJsonType& j, typename BasicJsonType::boolean_t& { if (JSON_HEDLEY_UNLIKELY(!j.is_boolean())) { - JSON_THROW(type_error::create(302, concat("type must be boolean, but is ", j.type_name()), &j)); + throw_type_must_be("boolean", j); } b = *j.template get_ptr(); } @@ -5664,7 +6160,7 @@ inline void from_json(const BasicJsonType& j, typename BasicJsonType::string_t& { if (JSON_HEDLEY_UNLIKELY(!j.is_string())) { - JSON_THROW(type_error::create(302, concat("type must be string, but is ", j.type_name()), &j)); + throw_type_must_be("string", j); } s = *j.template get_ptr(); } @@ -5680,7 +6176,7 @@ inline void from_json(const BasicJsonType& j, StringType& s) { if (JSON_HEDLEY_UNLIKELY(!j.is_string())) { - JSON_THROW(type_error::create(302, concat("type must be string, but is ", j.type_name()), &j)); + throw_type_must_be("string", j); } s = *j.template get_ptr(); @@ -5693,7 +6189,7 @@ inline void from_json(const BasicJsonType& j, typename BasicJsonType::number_flo } template -inline void from_json(const BasicJsonType& j, typename BasicJsonType::number_unsigned_t& val) +inline void from_json(const BasicJsonType& j, number_unsigned_overload_t& val) { get_arithmetic_value(j, val); } @@ -5726,7 +6222,7 @@ inline void from_json(const BasicJsonType& j, std::forward_list& l { if (JSON_HEDLEY_UNLIKELY(!j.is_array())) { - JSON_THROW(type_error::create(302, concat("type must be array, but is ", j.type_name()), &j)); + throw_type_must_be("array", j); } l.clear(); std::transform(j.rbegin(), j.rend(), @@ -5743,7 +6239,7 @@ inline void from_json(const BasicJsonType& j, std::valarray& l) { if (JSON_HEDLEY_UNLIKELY(!j.is_array())) { - JSON_THROW(type_error::create(302, concat("type must be array, but is ", j.type_name()), &j)); + throw_type_must_be("array", j); } l.resize(j.size()); std::transform(j.begin(), j.end(), std::begin(l), @@ -5784,31 +6280,62 @@ inline void from_json_array_impl(const BasicJsonType& j, typename BasicJsonType: arr = *j.template get_ptr(); } +// fill a std::array from the JSON array j: the size is checked once and the +// elements are read in order, so a short array converts the elements it has and +// then throws the out_of_range error at() would throw for the first missing one +template +void from_json_std_array(const BasicJsonType& j, std::array& arr) +{ + const auto& inner_array = *j.template get_ptr(); + const std::size_t size = (std::min)(N, inner_array.size()); + for (std::size_t i = 0; i < size; ++i) + { + arr[i] = inner_array[i].template get(); + } + if (JSON_HEDLEY_UNLIKELY(size < N)) + { + static_cast(j.at(size)); + } +} + template auto from_json_array_impl(const BasicJsonType& j, std::array& arr, priority_tag<2> /*unused*/) -> decltype(j.template get(), void()) { - for (std::size_t i = 0; i < N; ++i) - { - arr[i] = j.at(i).template get(); - } + from_json_std_array(j, arr); } -// reserve() is called through this pair (modeled on from_json_object_reserve) -// so from_json_array_impl below has a single body for both ConstructibleArrayType -// that support reserve() and those that don't. -template -auto from_json_array_reserve(ConstructibleArrayType& arr, typename ConstructibleArrayType::size_type size, priority_tag<1> /*unused*/) --> decltype(arr.reserve(size), void()) +// reserve() is called through this pair (used for arrays and objects alike) +// so from_json_array_impl and the object from_json have a single body for both +// containers that support reserve() and those that don't. +template +auto reserve_if_possible(ConstructibleContainerType& c, typename ConstructibleContainerType::size_type size, priority_tag<1> /*unused*/) +-> decltype(c.reserve(size), void()) { - arr.reserve(size); + c.reserve(size); } -template -void from_json_array_reserve(ConstructibleArrayType& /*arr*/, std::size_t /*size*/, priority_tag<0> /*unused*/) +template +void reserve_if_possible(ConstructibleContainerType& /*c*/, std::size_t /*size*/, priority_tag<0> /*unused*/) {} +// append through push_back() where available; containers without it (such as +// std::set) fall back to insert(end(), ...), which is what std::inserter did +template +auto from_json_array_append(ConstructibleArrayType& arr, ValueType&& value, priority_tag<1> /*unused*/) +-> decltype(arr.push_back(std::forward(value)), void()) +{ + arr.push_back(std::forward(value)); +} + +template +void from_json_array_append(ConstructibleArrayType& arr, ValueType&& value, priority_tag<0> /*unused*/) +{ + using std::end; + arr.insert(end(arr), std::forward(value)); +} + template::value, @@ -5818,17 +6345,15 @@ auto from_json_array_impl(const BasicJsonType& j, ConstructibleArrayType& arr, p j.template get(), void()) { - using std::end; - ConstructibleArrayType ret; - from_json_array_reserve(ret, j.size(), priority_tag<1> {}); - std::transform(j.begin(), j.end(), - std::inserter(ret, end(ret)), [](const BasicJsonType & i) + const auto* inner_array = j.template get_ptr(); + reserve_if_possible(ret, inner_array->size(), priority_tag<1> {}); + for (const auto& elem : *inner_array) { // get() returns *this, this won't call a from_json // method when value_type is BasicJsonType - return i.template get(); - }); + from_json_array_append(ret, elem.template get(), priority_tag<1> {}); + } arr = std::move(ret); } @@ -5848,7 +6373,7 @@ void()) { if (JSON_HEDLEY_UNLIKELY(!j.is_array())) { - JSON_THROW(type_error::create(302, concat("type must be array, but is ", j.type_name()), &j)); + throw_type_must_be("array", j); } from_json_array_impl(j, arr, priority_tag<3> {}); @@ -5861,16 +6386,47 @@ std::array from_json_inplace_array_impl(const BasicJsonType& return { { j.at(Idx).template get()... } }; } +// std::is_trivial is deprecated in C++26 (see binary_writer::to_char_type); +// a struct rather than an alias, as astyle indents an #ifdef block of aliases +template +struct is_trivial_element +#ifdef JSON_HAS_CPP_26 + : conjunction, std::is_trivially_default_constructible> +#else + : std::is_trivial +#endif +{}; + +// the element type may not be default constructible: construct every element +// in place (a pack expansion that is unrolled N times) +template +std::array from_json_make_array(const BasicJsonType& j, identity_tag> tag, + std::false_type /*is_trivial_element*/) +{ + return from_json_inplace_array_impl(j, tag, make_index_sequence {}); +} + +// a trivial element type cannot tell default construction plus assignment from +// construction in place, so use a loop instead of unrolling N conversions +template +std::array from_json_make_array(const BasicJsonType& j, identity_tag> /*unused*/, + std::true_type /*is_trivial_element*/) +{ + std::array arr; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): every element is assigned or an exception is thrown + from_json_std_array(j, arr); + return arr; +} + template < typename BasicJsonType, typename T, std::size_t N > auto from_json(const BasicJsonType& j, identity_tag> tag) -> decltype(from_json_inplace_array_impl(j, tag, make_index_sequence {})) { if (JSON_HEDLEY_UNLIKELY(!j.is_array())) { - JSON_THROW(type_error::create(302, concat("type must be array, but is ", j.type_name()), &j)); + throw_type_must_be("array", j); } - return from_json_inplace_array_impl(j, tag, make_index_sequence {}); + return from_json_make_array(j, tag, is_trivial_element {}); } template @@ -5878,7 +6434,7 @@ inline void from_json(const BasicJsonType& j, typename BasicJsonType::binary_t& { if (JSON_HEDLEY_UNLIKELY(!j.is_binary())) { - JSON_THROW(type_error::create(302, concat("type must be binary, but is ", j.type_name()), &j)); + throw_type_must_be("binary", j); } bin = *j.template get_ptr(); @@ -5899,20 +6455,26 @@ inline void from_json(const BasicJsonType& j, CompatibleArrayType& bin) } else { - JSON_THROW(type_error::create(302, concat("type must be binary or array, but is ", j.type_name()), &j)); + throw_type_must_be("binary or array", j); } } -template -auto from_json_object_reserve(ConstructibleObjectType& obj, typename ConstructibleObjectType::size_type size, priority_tag<1> /*unused*/) --> decltype(obj.reserve(size), void()) +// the keys of the source are sorted for the usual object types, so hinting the +// end makes every insertion constant-time (for containers that order their keys +// the same way); containers without emplace_hint() (such as ordered_map) fall +// back to emplace() +template +auto from_json_object_emplace(ConstructibleObjectType& obj, Key&& key, Value&& value, priority_tag<1> /*unused*/) +-> decltype(obj.emplace_hint(obj.end(), std::forward(key), std::forward(value)), void()) { - obj.reserve(size); + obj.emplace_hint(obj.end(), std::forward(key), std::forward(value)); } -template -inline void from_json_object_reserve(ConstructibleObjectType& /*obj*/, std::size_t /*size*/, priority_tag<0> /*unused*/) -{} +template +void from_json_object_emplace(ConstructibleObjectType& obj, Key&& key, Value&& value, priority_tag<0> /*unused*/) +{ + obj.emplace(std::forward(key), std::forward(value)); +} template::value, int> = 0> @@ -5920,15 +6482,15 @@ inline void from_json(const BasicJsonType& j, ConstructibleObjectType& obj) { if (JSON_HEDLEY_UNLIKELY(!j.is_object())) { - JSON_THROW(type_error::create(302, concat("type must be object, but is ", j.type_name()), &j)); + throw_type_must_be("object", j); } ConstructibleObjectType ret; const auto* inner_object = j.template get_ptr(); - from_json_object_reserve(ret, inner_object->size(), priority_tag<1> {}); + reserve_if_possible(ret, inner_object->size(), priority_tag<1> {}); for (const auto& p : *inner_object) { - ret.emplace(p.first, p.second.template get()); + from_json_object_emplace(ret, p.first, p.second.template get(), priority_tag<1> {}); } obj = std::move(ret); } @@ -5945,38 +6507,17 @@ template < typename BasicJsonType, typename ArithmeticType, int > = 0 > inline void from_json(const BasicJsonType& j, ArithmeticType& val) { - switch (static_cast(j)) + if (get_number_value(j, val)) { - case value_t::number_unsigned: - { - val = static_cast(*j.template get_ptr()); - break; - } - case value_t::number_integer: - { - val = static_cast(*j.template get_ptr()); - break; - } - case value_t::number_float: - { - val = static_cast(*j.template get_ptr()); - break; - } - case value_t::boolean: - { - val = static_cast(*j.template get_ptr()); - break; - } - - 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: - JSON_THROW(type_error::create(302, concat("type must be number, but is ", j.type_name()), &j)); + return; } + + // unlike get_arithmetic_value(), a boolean is accepted here + if (JSON_HEDLEY_UNLIKELY(!j.is_boolean())) + { + throw_type_must_be("number", j); + } + val = static_cast(*j.template get_ptr()); } template @@ -6047,7 +6588,7 @@ auto from_json(const BasicJsonType& j, TupleRelated&& t) { if (JSON_HEDLEY_UNLIKELY(!j.is_array())) { - JSON_THROW(type_error::create(302, concat("type must be array, but is ", j.type_name()), &j)); + throw_type_must_be("array", j); } return from_json_tuple_impl(j, std::forward(t), priority_tag<3> {}); @@ -6060,14 +6601,14 @@ void from_json_pair_array_to_map(const BasicJsonType& j, MapType& m) { if (JSON_HEDLEY_UNLIKELY(!j.is_array())) { - JSON_THROW(type_error::create(302, concat("type must be array, but is ", j.type_name()), &j)); + throw_type_must_be("array", j); } m.clear(); for (const auto& p : j) { if (JSON_HEDLEY_UNLIKELY(!p.is_array())) { - JSON_THROW(type_error::create(302, concat("type must be array, but is ", p.type_name()), &p)); + throw_type_must_be("array", p); } m.emplace(p.at(0).template get(), p.at(1).template get()); } @@ -6134,7 +6675,7 @@ inline void from_json(const BasicJsonType& j, std_fs::path& p) { if (JSON_HEDLEY_UNLIKELY(!j.is_string())) { - JSON_THROW(type_error::create(302, concat("type must be string, but is ", j.type_name()), &j)); + throw_type_must_be("string", j); } const auto& s = *j.template get_ptr(); // Checking for C++20 standard or later can be insufficient in case the @@ -6193,7 +6734,7 @@ NLOHMANN_JSON_NAMESPACE_END #include // optional #endif -#include // begin, end +#include // begin, end, make_move_iterator, move_iterator #include // allocator_traits #include // basic_string, char_traits #include // tuple, get @@ -6335,6 +6876,24 @@ inline std::string hex_byte(const std::uint8_t byte) return result; } +/*! +@brief write a UTF-16 code unit as four lowercase hexadecimal digits + +The lowercase counterpart of hex_byte(), used for the `\\uXXXX` escapes of the +serializers, whose output is lowercase. Writes exactly four characters to +@a out and returns the pointer past them. +*/ +JSON_HEDLEY_ALWAYS_INLINE char* hex4_lower(const std::uint16_t codeunit, char* out) noexcept +{ + constexpr const char* nibble_to_hex = "0123456789abcdef"; + const unsigned int unit = codeunit; + out[0] = nibble_to_hex[(unit >> 12u) & 0x0Fu]; + out[1] = nibble_to_hex[(unit >> 8u) & 0x0Fu]; + out[2] = nibble_to_hex[(unit >> 4u) & 0x0Fu]; + out[3] = nibble_to_hex[unit & 0x0Fu]; + return out + 4; +} + /////////////////// // UTF-8 encoding // /////////////////// @@ -6390,13 +6949,73 @@ void encode_utf8(std::uint32_t cp, const Out& out) } } +/*! +@brief the shape of a well-formed UTF-8 sequence, by its lead byte + +@a length is 1 for ASCII, 2 to 4 for a valid lead byte, and 0 for a byte that +cannot start a well-formed sequence (a continuation byte, 0xC0, 0xC1, or 0xF5 +to 0xFF). For a multibyte sequence, the second byte must lie in +[@a second_lo, @a second_hi], and every later byte in [0x80, 0xBF] +(RFC 3629, section 4; Unicode Table 3-7). The narrower second-byte ranges +exclude overlong forms (0xE0, 0xF0), surrogates (0xED), and code points +beyond U+10FFFF (0xF4). +*/ +struct utf8_sequence_shape +{ + std::uint8_t length; + std::uint8_t second_lo; + std::uint8_t second_hi; +}; + +/// @return the shape of a UTF-8 sequence starting with @a lead, see utf8_sequence_shape +inline utf8_sequence_shape utf8_shape(const std::uint8_t lead) noexcept +{ + if (lead < 0x80u) + { + return {1, 0, 0}; + } + if (lead < 0xC2u) + { + return {0, 0, 0}; + } + if (lead <= 0xDFu) + { + return {2, 0x80u, 0xBFu}; + } + if (lead == 0xE0u) + { + return {3, 0xA0u, 0xBFu}; + } + if (lead == 0xEDu) + { + return {3, 0x80u, 0x9Fu}; + } + if (lead <= 0xEFu) + { + return {3, 0x80u, 0xBFu}; + } + if (lead == 0xF0u) + { + return {4, 0x90u, 0xBFu}; + } + if (lead <= 0xF3u) + { + return {4, 0x80u, 0xBFu}; + } + if (lead == 0xF4u) + { + return {4, 0x80u, 0x8Fu}; + } + return {0, 0, 0}; +} + /////////////////// // UTF-8 decoding // /////////////////// // UTF-8 decoder states used by decode() below -static constexpr std::uint8_t UTF8_ACCEPT = 0; -static constexpr std::uint8_t UTF8_REJECT = 1; +JSON_INLINE_VARIABLE constexpr std::uint8_t UTF8_ACCEPT = 0; +JSON_INLINE_VARIABLE constexpr std::uint8_t UTF8_REJECT = 1; /*! @brief process a byte of a UTF-8 sequence @@ -6405,7 +7024,7 @@ This is a single-byte step of a "shift-based" UTF-8 decoder originally written by Björn Hoehrmann. See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for details. -The library checks UTF-8 well-formedness (RFC 3629, section 4) in three +The library checks UTF-8 well-formedness (RFC 3629, section 4) in five places, which differ in speed, diagnostics, and how they read the input: - decode() below: the serializer, to escape and, in strict mode, reject @@ -6419,10 +7038,15 @@ places, which differ in speed, diagnostics, and how they read the input: bulk string scan, the bulk path of the BON8 reader, and the BON8 writer. They must accept exactly what the lexer's switch accepts. - the byte path of binary_reader::get_bon8_string(): BON8 input without bulk - access, and the bytes the bulk path leaves to it. + access, and the bytes the bulk path leaves to it. It keeps its own ranges: + taking them from utf8_shape() made BON8 decoding 2-3% slower (#5891). +- check_utf8() in detail/view/edit.hpp: strings stored in an editable + json_document, with the index of the offending byte. It also uses + utf8_shape(). -All four must accept the same set of sequences, so a change to one needs a -matching change to the others. +All five must accept the same set of sequences, which the test "UTF-8 +validators accept the same sequences" in unit-class_lexer.cpp checks +exhaustively against decode() and utf8_shape(). @param[in,out] state the current decoder state @param[in,out] codep codepoint (valid only if resulting state is UTF8_ACCEPT) @@ -6688,35 +7312,25 @@ template class iteration_proxy_value { JSON_ASSERT(anchor.m_object != nullptr); - switch (anchor.m_object->type()) + if (anchor.m_object->is_array()) { // use integer array index as key - case value_t::array: + if (array_index != array_index_last) { - if (array_index != array_index_last) - { - int_to_string( array_index_str, array_index ); - array_index_last = array_index; - } - return array_index_str; + int_to_string( array_index_str, array_index ); + array_index_last = array_index; } - - // use key from the object - case value_t::object: - return anchor.key(); - - // use an empty key for all primitive types - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - return empty_str; + return array_index_str; } + + if (anchor.m_object->is_object()) + { + // use key from the object + return anchor.key(); + } + + // use an empty key for all primitive types + return empty_str; } /// return value of the iterator @@ -6842,6 +7456,32 @@ namespace detail * leave j with the type of the new value, but the pointer to the destroyed old one. */ +// a move iterator over the elements of an rvalue container, unless they are +// const (e.g., std::set): libstdc++ before GCC 5 defines the reference of a +// std::move_iterator as value_type&&, which cannot bind to a const element +template +std::move_iterator make_move_iterator_if_mutable_impl(const Iterator& it, std::true_type /*mutable*/) +{ + return std::make_move_iterator(it); +} + +template +Iterator make_move_iterator_if_mutable_impl(const Iterator& it, std::false_type /*mutable*/) +{ + return it; +} + +template +using is_mutable_iterator = std::integral_constant < bool, + !std::is_const())>::type>::value >; + +template +auto make_move_iterator_if_mutable(const Iterator& it) +-> decltype(make_move_iterator_if_mutable_impl(it, is_mutable_iterator {})) +{ + return make_move_iterator_if_mutable_impl(it, is_mutable_iterator {}); +} + template struct external_constructor; template<> @@ -6961,6 +7601,17 @@ struct external_constructor j.replace_value(value_t::array, value); } + /// an rvalue container: move its elements + template + static void construct_moving(BasicJsonType& j, CompatibleArrayType& arr) + { + using std::begin; + using std::end; + + const typename BasicJsonType::json_value value(j.template create(make_move_iterator_if_mutable(begin(arr)), make_move_iterator_if_mutable(end(arr)))); + j.replace_value(value_t::array, value); + } + template static void construct(BasicJsonType& j, const std::vector& arr) { @@ -7011,6 +7662,17 @@ struct external_constructor j.replace_value(value_t::object, value); } + /// an rvalue container: move its values + template + static void construct_moving(BasicJsonType& j, CompatibleObjectType& obj) + { + using std::begin; + using std::end; + + const typename BasicJsonType::json_value value(j.template create(make_move_iterator_if_mutable(begin(obj)), make_move_iterator_if_mutable(end(obj)))); + j.replace_value(value_t::object, value); + } + template < typename BasicJsonType, typename CompatibleObjectType, enable_if_t < !std::is_same::value, int > = 0 > static void construct(BasicJsonType& j, const CompatibleObjectType& obj) @@ -7097,14 +7759,14 @@ inline void to_json(BasicJsonType& j, FloatType val) noexcept } template::value, int> = 0> + enable_if_t, CompatibleNumberUnsignedType>::value, int> = 0> inline void to_json(BasicJsonType& j, CompatibleNumberUnsignedType val) noexcept { external_constructor::construct(j, static_cast(val)); } template::value, int> = 0> + enable_if_t::value, int> = 0> inline void to_json(BasicJsonType& j, CompatibleNumberIntegerType val) noexcept { external_constructor::construct(j, static_cast(val)); @@ -7148,6 +7810,68 @@ inline void to_json(BasicJsonType& j, const CompatibleArrayType& arr) external_constructor::construct(j, arr); } +// types that have a dedicated to_json overload and must not be treated as generic containers +template +struct has_dedicated_to_json : std::false_type {}; + +#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM +template<> +struct has_dedicated_to_json : std::true_type {}; +#endif + +#if defined(__cpp_lib_char8_t) +template +struct has_dedicated_to_json> : std::true_type {}; +#endif + +#if JSON_HAS_RANGE_VIEW_CONVERSION +template +struct is_range_view_type : is_compatible_range_view {}; +#else +template +struct is_range_view_type : std::false_type {}; +#endif + +#if JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS +template +struct is_enum_keyed_map_type : is_enum_keyed_map {}; +#else +template +struct is_enum_keyed_map_type : std::false_type {}; +#endif + +// rvalue (non-const) container: the generic to_json takes its elements by move +template +using is_movable_container_rvalue = negation, + std::is_const, + has_dedicated_to_json, + is_basic_json, + is_enum_keyed_map_type, + std::is_same>>>; + +// rvalue array-like containers; object types are ruled out before views are +// checked, as std::ranges::view for the object_t of an ordered_json would +// need that ordered_json to be complete (GCC 11, see #3312) +template +using is_rvalue_array_to_json = conjunction, + negation>, + negation>, + is_compatible_array_type, + negation, + std::is_same, + is_compatible_string_type, + is_compatible_binary_type>>>; + +// rvalue containers: their elements are moved instead of copied +template::value, int> = 0> +inline void to_json(BasicJsonType& j, CompatibleArrayType && arr) // NOLINT(cppcoreguidelines-missing-std-forward) +{ + external_constructor::construct_moving(j, arr); +} + #if JSON_HAS_RANGE_VIEW_CONVERSION template < typename BasicJsonType, typename T, enable_if_t < is_compatible_range_view>::value @@ -7208,19 +7932,33 @@ inline void to_json(BasicJsonType& j, const EnumKeyedMap& map) BasicJsonType key = p.first; if (JSON_HEDLEY_UNLIKELY(!key.is_string())) { - JSON_THROW(type_error::create(302, concat("type must be string, but is ", key.type_name()), &key)); + throw_type_must_be("string", key); } auto& key_string = *key.template get_ptr(); if (JSON_HEDLEY_UNLIKELY(!obj.emplace(key_string, BasicJsonType(p.second)).second)) { - JSON_THROW(type_error::create(318, concat("duplicate object key '", key_string, "'"), &key)); + JSON_THROW(type_error::create(exception_id::enum_key_duplicate, concat("duplicate object key '", key_string, "'"), &key)); } } external_constructor::construct(j, std::move(obj)); } #endif +// rvalue object-like containers (views have no key_type and mapped_type, so +// they are no compatible object type) +template +using is_rvalue_object_to_json = conjunction, + negation>, + is_compatible_object_type>; + +template::value, int> = 0> +inline void to_json(BasicJsonType& j, CompatibleObjectType && obj) // NOLINT(cppcoreguidelines-missing-std-forward) +{ + external_constructor::construct_moving(j, obj); +} + template inline void to_json(BasicJsonType& j, typename BasicJsonType::object_t&& obj) { @@ -7385,7 +8123,7 @@ struct adl_serializer }; NLOHMANN_JSON_NAMESPACE_END -// IWYU pragma: keep +// IWYU pragma: export // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ @@ -7492,14 +8230,50 @@ class byte_container_with_subtype : public BinaryType NLOHMANN_JSON_NAMESPACE_END +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + // #include + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +/*! +@brief the configuration macros that json_view.hpp reads + +json.hpp undefines these macros at its end (see macro_unscope.hpp), so code +that builds on the library after it (json_view.hpp) reads them here. A macro +is added when the view starts to depend on it. Like the +macros, they are part of the ABI namespace, so they always match the +basic_json they are used with. +*/ +struct abi_config +{ + /// JSON_STRICT_NUL_HANDLING: a null byte is an error, not the end of input + static constexpr bool strict_nul_handling = JSON_STRICT_NUL_HANDLING != 0; +}; + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include +// IWYU pragma: export // #include // IWYU pragma: keep // #include // IWYU pragma: keep // #include - +// IWYU pragma: export // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ @@ -7792,7 +8566,6 @@ NLOHMANN_JSON_NAMESPACE_END #include // ldexp #include // size_t #include // uint8_t, uint16_t, uint32_t, uint64_t, uintmax_t -#include // snprintf #include // memcpy #include // back_inserter #include // numeric_limits @@ -7816,16 +8589,19 @@ NLOHMANN_JSON_NAMESPACE_END -#include // min +#include // count, find, max, min #include // array #include // size_t #include // uint32_t #include // strlen -#include // begin, end, iterator_traits, random_access_iterator_tag, distance, next +#include // begin, end, iterator_traits, random_access_iterator_tag, distance, next, reverse_iterator +#include // numeric_limits +#include // unique_ptr #include // streambuf #include // string, char_traits #include // enable_if, is_base_of, is_pointer, is_integral, remove_pointer -#include // pair, declval +#include // declval, move, pair +#include // vector #ifndef JSON_NO_IO #include // FILE * @@ -7843,6 +8619,33 @@ NLOHMANN_JSON_NAMESPACE_END // #include +#ifndef JSON_NO_IO + // Which unlocked stdio functions file_input_adapter uses (see + // JSON_DISABLE_UNLOCKED_FILE_IO): 1 = POSIX flockfile/getc_unlocked, + // 2 = Microsoft CRT _lock_file/_fgetc_nolock, 0 = none (std::fgetc, which + // locks the file for every character). Only C libraries known to declare + // the functions are listed; the macros are tested after , which + // defines __GLIBC__. MemorySanitizer builds always use std::fgetc: glibc's + // inline getc_unlocked reads the stdio buffer directly, which glibc (not + // instrumented) filled, so MemorySanitizer would report a false positive. + #if defined(__has_feature) + #if __has_feature(memory_sanitizer) + #define JSON_UNLOCKED_FILE_IO_MSAN 1 + #endif + #endif + + #if JSON_DISABLE_UNLOCKED_FILE_IO || defined(JSON_UNLOCKED_FILE_IO_MSAN) + #define JSON_UNLOCKED_FILE_IO 0 + #elif defined(_MSC_VER) + #define JSON_UNLOCKED_FILE_IO 2 + #elif defined(__GLIBC__) || defined(__APPLE__) || defined(__ANDROID__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__DragonFly__) + #define JSON_UNLOCKED_FILE_IO 1 + #else + #define JSON_UNLOCKED_FILE_IO 0 + #endif + #undef JSON_UNLOCKED_FILE_IO_MSAN +#endif // JSON_NO_IO + NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail { @@ -7850,12 +8653,118 @@ namespace detail /// the supported input formats enum class input_format_t { json, cbor, msgpack, ubjson, bson, bjdata, bon8 }; +/// @return the name of a binary input format, as used in exception messages +inline const char* input_format_name(const input_format_t format) noexcept +{ + switch (format) + { + case input_format_t::cbor: + return "CBOR"; + case input_format_t::msgpack: + return "MessagePack"; + case input_format_t::ubjson: + return "UBJSON"; + case input_format_t::bson: + return "BSON"; + case input_format_t::bjdata: + return "BJData"; + case input_format_t::bon8: + return "BON8"; + case input_format_t::json: // LCOV_EXCL_LINE + default: // LCOV_EXCL_LINE + JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE + return ""; // LCOV_EXCL_LINE + } +} + +/// @return the message of the parse_error thrown or reported for an empty input +inline const char* empty_input_message() noexcept +{ + return "attempting to parse an empty input; check that your input string or stream contains the expected JSON"; +} + //////////////////// // input adapters // //////////////////// #ifndef JSON_NO_IO /*! +A growable array of characters for the part of a token that an input adapter +copied out of its window: unlike std::vector::push_back(), appending a +character takes no reference to a local, and is a few instructions when there +is room. +*/ +class token_carry_buffer +{ + public: + token_carry_buffer() = default; + token_carry_buffer(const token_carry_buffer&) = delete; + token_carry_buffer& operator=(const token_carry_buffer&) = delete; + token_carry_buffer(token_carry_buffer&& rhs) noexcept + : m_data(std::move(rhs.m_data)), m_end(rhs.m_end), m_capacity_end(rhs.m_capacity_end) + { + rhs.m_end = rhs.m_capacity_end = nullptr; + } + token_carry_buffer& operator=(token_carry_buffer&&) = delete; + ~token_carry_buffer() = default; + + void clear() noexcept + { + m_end = m_data.get(); + } + + void push_back(char c) + { + if (JSON_HEDLEY_UNLIKELY(m_end == m_capacity_end)) + { + grow(1); + } + *m_end++ = c; + } + + void append(const char* first, const char* last) + { + const auto n = static_cast(last - first); + const auto room = static_cast(m_capacity_end - m_end); + if (n > room) + { + grow(n); + } + if (n != 0) + { + std::memcpy(m_end, first, n); + } + m_end += n; + } + + /// replace the contents of @a out + void copy_to(std::vector& out) const + { + out.assign(m_data.get(), m_end); + } + + private: + /// make room for @a extra more characters + JSON_HEDLEY_NEVER_INLINE void grow(std::size_t extra) + { + const auto size = static_cast(m_end - m_data.get()); + const auto capacity = static_cast(m_capacity_end - m_data.get()); + const std::size_t new_capacity = (std::max)(capacity < 32 ? static_cast(32) : 2 * capacity, size + extra); + std::unique_ptr data(new char[new_capacity]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + if (size != 0) + { + std::memcpy(data.get(), m_data.get(), size); + } + m_data = std::move(data); + m_end = m_data.get() + size; + m_capacity_end = m_data.get() + new_capacity; + } + + std::unique_ptr m_data{}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays,readability-redundant-member-init) + char* m_end = nullptr; + char* m_capacity_end = nullptr; +}; +/*! Input adapter for stdio file access. This adapter read only 1 byte and do not use any buffer. This adapter is a very low level adapter. */ @@ -7869,18 +8778,35 @@ class file_input_adapter : m_file(f) { JSON_ASSERT(m_file != nullptr); + lock(); } // make class move-only file_input_adapter(const file_input_adapter&) = delete; - file_input_adapter(file_input_adapter&&) noexcept = default; + file_input_adapter(file_input_adapter&& rhs) noexcept + : m_file(rhs.m_file) + { + // the lock is owned by the thread, not the adapter: hand it over + rhs.m_file = nullptr; + } file_input_adapter& operator=(const file_input_adapter&) = delete; file_input_adapter& operator=(file_input_adapter&&) = delete; - ~file_input_adapter() = default; + ~file_input_adapter() + { + unlock(); + } std::char_traits::int_type get_character() noexcept { + // the file is locked for the lifetime of the adapter, so the + // per-character lock of std::fgetc can be skipped +#if JSON_UNLOCKED_FILE_IO == 1 + return getc_unlocked(m_file); +#elif JSON_UNLOCKED_FILE_IO == 2 + return _fgetc_nolock(m_file); +#else return std::fgetc(m_file); +#endif } // returns the number of characters successfully read @@ -7891,6 +8817,32 @@ class file_input_adapter } private: + // Lock the file once for the whole parse instead of once per character. + // The lock is recursive, so fread() and the user's own stdio calls on the + // same thread keep working. + void lock() noexcept + { +#if JSON_UNLOCKED_FILE_IO == 1 + flockfile(m_file); +#elif JSON_UNLOCKED_FILE_IO == 2 + _lock_file(m_file); +#endif + } + + void unlock() noexcept + { + // a moved-from adapter has no file and holds no lock + if (m_file == nullptr) + { + return; + } +#if JSON_UNLOCKED_FILE_IO == 1 + funlockfile(m_file); +#elif JSON_UNLOCKED_FILE_IO == 2 + _unlock_file(m_file); +#endif + } + /// the file pointer to read from std::FILE* m_file; }; @@ -7940,15 +8892,27 @@ class input_stream_adapter input_stream_adapter& operator=(input_stream_adapter&) = delete; input_stream_adapter& operator=(input_stream_adapter&&) = delete; -#if JSON_PRECISE_STREAM_POSITION input_stream_adapter(input_stream_adapter&& rhs) noexcept - : is(rhs.is), sb(rhs.sb), lookahead(rhs.lookahead) + : is(rhs.is), sb(rhs.sb) +#if JSON_PRECISE_STREAM_POSITION + , lookahead(rhs.lookahead) +#endif +#if !JSON_DISABLE_STREAM_BULK_SCAN + , token_carry(std::move(rhs.token_carry)) + , token_segment(rhs.token_segment) + , current_in_get_area(rhs.current_in_get_area) + , tracking_token(rhs.tracking_token) + , no_get_area(rhs.no_get_area) +#endif { rhs.is = nullptr; rhs.sb = nullptr; +#if JSON_PRECISE_STREAM_POSITION rhs.lookahead = false; +#endif } +#if JSON_PRECISE_STREAM_POSITION // Whether the character last returned by get_character() can be given back // to the input with release_lookahead(). static constexpr bool supports_lookahead = true; @@ -7968,17 +8932,41 @@ class input_stream_adapter sb->sbumpc(); } +#if !JSON_DISABLE_STREAM_BULK_SCAN + // sgetc() refills the get area if it is exhausted + const bool refill = streambuf_access::get_ptr(sb) == streambuf_access::get_end(sb); + if (JSON_HEDLEY_UNLIKELY(refill)) + { + save_token_segment(); + } +#endif + auto res = sb->sgetc(); // set eof manually, as we don't use the istream interface. if (JSON_HEDLEY_UNLIKELY(res == std::char_traits::eof())) { // there is nothing to step over next time lookahead = false; +#if !JSON_DISABLE_STREAM_BULK_SCAN + current_in_get_area = false; +#endif is->clear(is->rdstate() | std::ios::eofbit); } else { lookahead = true; +#if !JSON_DISABLE_STREAM_BULK_SCAN + if (JSON_HEDLEY_UNLIKELY(refill)) + { + // the peeked character is at gptr(), unless the streambuf has + // no get area + after_refill(res, streambuf_access::get_ptr(sb) != streambuf_access::get_end(sb)); + } + else + { + current_in_get_area = true; + } +#endif } return res; } @@ -7992,13 +8980,6 @@ class input_stream_adapter lookahead = false; } #else - input_stream_adapter(input_stream_adapter&& rhs) noexcept - : is(rhs.is), sb(rhs.sb) - { - rhs.is = nullptr; - rhs.sb = nullptr; - } - // std::istream/std::streambuf use std::char_traits::to_int_type, to // ensure that std::char_traits::eof() and the character 0xFF do not // end up as the same value, e.g., 0xFFFFFFFF. @@ -8007,13 +8988,21 @@ class input_stream_adapter // stays consumed after parsing; see JSON_PRECISE_STREAM_POSITION. std::char_traits::int_type get_character() { - auto res = sb->sbumpc(); - // set eof manually, as we don't use the istream interface. - if (JSON_HEDLEY_UNLIKELY(res == std::char_traits::eof())) +#if !JSON_DISABLE_STREAM_BULK_SCAN + // the character is in the get area: step over it, as sbumpc() does + const char* const next = streambuf_access::get_ptr(sb); + if (JSON_HEDLEY_LIKELY(next != streambuf_access::get_end(sb))) { - is->clear(is->rdstate() | std::ios::eofbit); + current_in_get_area = true; + streambuf_access::bump(sb, 1); + return std::char_traits::to_int_type(*next); } - return res; + if (no_get_area) + { + return get_character_no_get_area(); + } +#endif + return get_character_slow(); } #endif @@ -8022,6 +9011,18 @@ class input_stream_adapter { #if JSON_PRECISE_STREAM_POSITION commit_lookahead(); +#endif +#if !JSON_DISABLE_STREAM_BULK_SCAN + // the elements are in the get area: copy them, as sgetn() does, but + // without the virtual call + const char* const first = streambuf_access::get_ptr(sb); + const auto available = static_cast(streambuf_access::get_end(sb) - first); + if (JSON_HEDLEY_LIKELY(count * sizeof(T) <= available && count * sizeof(T) <= static_cast((std::numeric_limits::max)()))) + { + std::memcpy(dest, first, count * sizeof(T)); + streambuf_access::bump(sb, static_cast(count * sizeof(T))); + return count * sizeof(T); + } #endif auto res = static_cast(sb->sgetn(reinterpret_cast(dest), static_cast(count * sizeof(T)))); if (JSON_HEDLEY_UNLIKELY(res < count * sizeof(T))) @@ -8031,7 +9032,248 @@ class input_stream_adapter return res; } +#if !JSON_DISABLE_STREAM_BULK_SCAN + // The lexer may scan the characters in the get area of the streambuf + // directly (SWAR fast paths). The get area is only a window into the + // input: it ends wherever the streambuf's buffer ends, and is refilled by + // the next get_character(). The lexer therefore keeps counting the + // position itself and treats the end of the window as "unknown", not as + // the end of the input. + static constexpr bool supports_bulk_scan = true; + static constexpr bool bulk_window = true; + + // Pointer to the next unread character; only valid when + // bulk_remaining() > 0. The character before it is the one last returned + // by get_character(). + const char* bulk_data() const + { + return current_end(); + } + + // Number of unread characters in the get area. + std::size_t bulk_remaining() const + { + if (!current_in_get_area) + { + return 0; + } + return static_cast(streambuf_access::get_end(sb) - current_end()); + } + + // Consume @a n characters previously inspected via bulk_data(); the last + // of them becomes the character last returned by get_character(). + void bulk_skip(std::size_t n) + { + JSON_ASSERT(n <= bulk_remaining()); + // gbump() takes an int, but a get area (e.g., of a std::stringbuf) can + // be larger + constexpr auto max_bump = static_cast((std::numeric_limits::max)()); + for (; n > max_bump; n -= max_bump) + { + streambuf_access::bump(sb, static_cast(max_bump)); + } + streambuf_access::bump(sb, static_cast(n)); + } + + // The adapter keeps the current token for the lexer's error messages: the + // part in the get area is only remembered as a pointer, and copied out + // when the get area is about to be refilled. + static constexpr bool supports_token_tracking = true; + + // Start a token with the characters read from now on. Until this is + // called, nothing is tracked (the binary readers never call it). + void begin_token() noexcept + { + tracking_token = true; + token_carry.clear(); + token_segment = current_end(); + } + + // Start a new token with the character @a c last returned by + // get_character(). + void mark_token_start(std::char_traits::int_type c) + { + tracking_token = true; + token_carry.clear(); + if (current_in_get_area) + { + token_segment = current_end() - 1; + } + else + { + token_segment = nullptr; + token_carry.push_back(std::char_traits::to_char_type(c)); + } + } + + // Copy the characters read since mark_token_start() to @a out, without the + // last one if @a drop_last. + void copy_token(std::vector& out, bool drop_last) const + { + token_carry.copy_to(out); + if (token_segment != nullptr) + { + out.insert(out.end(), token_segment, current_end()); + } + if (drop_last && !out.empty()) + { + out.pop_back(); + } + } +#endif + private: +#if !JSON_PRECISE_STREAM_POSITION + // the get area is exhausted, or there is none +#if !JSON_DISABLE_STREAM_BULK_SCAN + JSON_HEDLEY_NEVER_INLINE +#endif + std::char_traits::int_type get_character_slow() + { +#if !JSON_DISABLE_STREAM_BULK_SCAN + // sbumpc() refills the get area if it is exhausted + const bool refill = streambuf_access::get_ptr(sb) == streambuf_access::get_end(sb); + if (JSON_HEDLEY_UNLIKELY(refill)) + { + save_token_segment(); + } +#endif + + auto res = sb->sbumpc(); + // set eof manually, as we don't use the istream interface. + if (JSON_HEDLEY_UNLIKELY(res == std::char_traits::eof())) + { +#if !JSON_DISABLE_STREAM_BULK_SCAN + current_in_get_area = false; +#endif + is->clear(is->rdstate() | std::ios::eofbit); + } +#if !JSON_DISABLE_STREAM_BULK_SCAN + else if (JSON_HEDLEY_UNLIKELY(refill)) + { + // the consumed character is at gptr() - 1, unless the streambuf + // has no get area and returned it from uflow() + const char* const next = streambuf_access::get_ptr(sb); + after_refill(res, next != nullptr && next != streambuf_access::get_begin(sb)); + } + else + { + current_in_get_area = true; + } +#endif + return res; + } +#endif +#if !JSON_DISABLE_STREAM_BULK_SCAN + // Reads the protected get-area pointers of any std::streambuf. Naming the + // members through a derived class makes forming the member pointers + // accessible; calling them on another streambuf object is then ordinary + // member access through a pointer to member. + struct streambuf_access : std::streambuf + { + static const char* get_ptr(std::streambuf* s) + { + return (s->*(&streambuf_access::gptr))(); + } + static const char* get_end(std::streambuf* s) + { + return (s->*(&streambuf_access::egptr))(); + } + static const char* get_begin(std::streambuf* s) + { + return (s->*(&streambuf_access::eback))(); + } + static void bump(std::streambuf* s, int n) + { + (s->*(&streambuf_access::gbump))(n); + } + // sbumpc() for an exhausted get area + static std::streambuf::int_type next_character(std::streambuf* s) + { + return (s->*(&streambuf_access::uflow))(); + } + }; + + // one past the character last returned by get_character(), if that is in + // the get area + const char* current_end() const + { +#if JSON_PRECISE_STREAM_POSITION + // the character is peeked at gptr() and not stepped over yet, unless + // it was given back with release_lookahead() + return streambuf_access::get_ptr(sb) + (lookahead ? 1 : 0); +#else + return streambuf_access::get_ptr(sb); +#endif + } + + // the get area is about to be replaced: copy the token's part in it + void save_token_segment() + { + if (token_segment != nullptr) + { + token_carry.append(token_segment, streambuf_access::get_end(sb)); + token_segment = nullptr; + } + } + +#if !JSON_PRECISE_STREAM_POSITION + // The streambuf has no get area (e.g., std::cin synchronized with stdio): + // every character comes from uflow(), and the token is kept by value. + JSON_HEDLEY_NEVER_INLINE std::char_traits::int_type get_character_no_get_area() + { + // the get area is exhausted, so this is what sbumpc() does + const auto res = streambuf_access::next_character(sb); + if (JSON_HEDLEY_UNLIKELY(res == std::char_traits::eof() || streambuf_access::get_end(sb) != nullptr)) + { + return get_character_no_get_area_other(res); + } + // current_in_get_area is false: only the first character of a get + // area, or the end of the input, is read here + if (tracking_token) + { + token_carry.push_back(std::char_traits::to_char_type(res)); + } + return res; + } + + // @a res came from uflow() of a streambuf without a get area: it is the + // end of the input, or the streambuf has a get area now + JSON_HEDLEY_NEVER_INLINE std::char_traits::int_type get_character_no_get_area_other(std::char_traits::int_type res) + { + if (res == std::char_traits::eof()) + { + current_in_get_area = false; + is->clear(is->rdstate() | std::ios::eofbit); + return res; + } + // the consumed character is at gptr() - 1 + after_refill(res, streambuf_access::get_ptr(sb) != streambuf_access::get_begin(sb)); + return res; + } +#endif + + // character @a c was just read into a new get area (@a in_get_area) or + // returned without one + void after_refill(std::char_traits::int_type c, bool in_get_area) + { + current_in_get_area = in_get_area; + no_get_area = !in_get_area; + if (!tracking_token) + { + return; + } + if (in_get_area) + { + token_segment = current_end() - 1; + } + else + { + token_carry.push_back(std::char_traits::to_char_type(c)); + } + } +#endif + #if JSON_PRECISE_STREAM_POSITION // Step over the character last returned by get_character(). The character // has already been peeked successfully, so for every streambuf with a get @@ -8053,6 +9295,19 @@ class input_stream_adapter /// whether get_character() peeked a character that is not consumed yet bool lookahead = false; #endif +#if !JSON_DISABLE_STREAM_BULK_SCAN + /// the part of the current token that was in earlier get areas + token_carry_buffer token_carry{}; // NOLINT(readability-redundant-member-init) + /// start of the current token's part in the get area, or nullptr + const char* token_segment = nullptr; + /// whether the character last returned by get_character() is in the get area + bool current_in_get_area = false; + /// whether a token is tracked at all; the binary readers never start one + bool tracking_token = false; + /// whether the streambuf had no get area when it was last asked for a + /// character + bool no_get_area = false; +#endif }; #endif // JSON_NO_IO @@ -8110,6 +9365,7 @@ class iterator_input_adapter return result; } + ++eof_reads; return char_traits::eof(); } @@ -8119,6 +9375,26 @@ class iterator_input_adapter return static_cast(std::distance(begin, current)); } + // number of get_character() calls so far, including those that returned + // EOF; the lexer derives its position from it (see lexer::lazy_position) + std::size_t get_read_count() const + { + return get_consumed_count() + eof_reads; + } + + // number of newlines in the consumed range [0, last_index); @a line_start + // is set to the index after the last of them (0 if there is none); only + // valid when supports_seek + std::size_t count_consumed_lines(std::size_t last_index, std::size_t& line_start) const + { + const auto to = std::next(begin, static_cast::difference_type>(last_index)); + const auto newline = char_traits::to_char_type('\n'); + using reverse_it = std::reverse_iterator; + const auto last_newline = std::find(reverse_it(to), reverse_it(begin), newline); + line_start = static_cast(std::distance(begin, last_newline.base())); + return static_cast(std::count(begin, last_newline.base(), newline)); + } + // append the already-consumed characters in the half-open range // [first_index, last_index) to @a out; only valid when supports_seek template @@ -8235,6 +9511,8 @@ class iterator_input_adapter IteratorType begin; IteratorType current; SentinelType end; + /// number of get_character() calls that returned EOF + std::size_t eof_reads = 0; template friend struct wide_string_input_helper; @@ -8394,7 +9672,7 @@ class wide_string_input_adapter template JSON_HEDLEY_NO_RETURN std::size_t get_elements(T* /*dest*/, std::size_t /*count*/ = 1) { - JSON_THROW(parse_error::create(112, 1, "wide string type cannot be interpreted as binary data", nullptr)); + JSON_THROW(parse_error::create(exception_id::unexpected_byte, 1, "wide string type cannot be interpreted as binary data", nullptr)); } private: @@ -8606,7 +9884,7 @@ inline file_input_adapter input_adapter(std::FILE* file) { if (file == nullptr) { - JSON_THROW(parse_error::create(101, 0, "attempting to parse an empty input; check that your input string or stream contains the expected JSON", nullptr)); + JSON_THROW(parse_error::create(exception_id::syntax_error, 0, empty_input_message(), nullptr)); } return file_input_adapter(file); } @@ -8615,7 +9893,7 @@ inline input_stream_adapter input_adapter(std::istream& stream) { if (stream.rdbuf() == nullptr) { - JSON_THROW(parse_error::create(101, 0, "attempting to parse an empty input; check that your input string or stream contains the expected JSON", nullptr)); + JSON_THROW(parse_error::create(exception_id::syntax_error, 0, empty_input_message(), nullptr)); } return input_stream_adapter(stream); } @@ -8640,7 +9918,7 @@ contiguous_bytes_input_adapter input_adapter(CharT b) { if (b == nullptr) { - JSON_THROW(parse_error::create(101, 0, "attempting to parse an empty input; check that your input string or stream contains the expected JSON", nullptr)); + JSON_THROW(parse_error::create(exception_id::syntax_error, 0, empty_input_message(), nullptr)); } auto length = std::strlen(reinterpret_cast(b)); const auto* ptr = reinterpret_cast(b); @@ -8725,9 +10003,13 @@ NLOHMANN_JSON_NAMESPACE_END -#include // find_if, min +#include // copy, find_if, min +#include // array #include +#include // hash #include // numeric_limits +#include // unique_ptr +#include // operator new #include // string #include // enable_if_t #include // move, pair @@ -8750,35 +10032,13 @@ NLOHMANN_JSON_NAMESPACE_END #include // size_t #include // uint32_t #include // snprintf +#include // memcmp #include // initializer_list +#include // numeric_limits #include // char_traits, string #include // move #include // vector -// #include - -// #include -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.12.0 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2021 The fast_float authors -// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann -// SPDX-License-Identifier: MIT - - - -#include // array -#include // FLT_EVAL_METHOD -#include // localeconv -#include // size_t -#include // int64_t, uint64_t -#include // strtof, strtod, strtold -#include // memcpy -#include // numeric_limits -#include // string - // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ @@ -8791,16 +10051,30 @@ NLOHMANN_JSON_NAMESPACE_END #include // uint64_t -#if !defined(__SIZEOF_INT128__) && defined(_MSC_VER) && (defined(_M_X64) || defined(_M_ARM64)) - #include // __umulh, _umul128 +#include // memcpy +#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_ARM64)) && (!defined(__SIZEOF_INT128__) || (!defined(__GNUC__) && !defined(__clang__))) + #include // __umulh, _umul128, _BitScanForward64, _BitScanReverse64 #endif -// #include +#ifdef _MSC_VER + #include // _byteswap_ushort, _byteswap_ulong, _byteswap_uint64 +#endif +#if defined(_MSC_VER) && !defined(__clang__) && (defined(_M_IX86) || defined(_M_ARM)) + #include // _BitScanForward, _BitScanReverse +#endif +// #include +// JSON_HEDLEY_ALWAYS_INLINE, NLOHMANN_JSON_NAMESPACE_BEGIN + +#if defined(__cpp_lib_bit_cast) || defined(__cpp_lib_bitops) || defined(__cpp_lib_byteswap) + #include // bit_cast, byteswap, countl_zero, countr_zero +#endif // Portable bit-level helpers for the number and string scanners. They use -// compiler builtins or platform-specific intrinsics where available and plain -// C++ otherwise, so they work regardless of byte order. +// compiler builtins or platform-specific intrinsics where available, the +// C++20 functions next, and plain C++ otherwise, so they work regardless +// of byte order. (The builtins and intrinsics come first: MSVC's +// std::countl_zero checks for LZCNT at run time unless AVX2 is enabled.) NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail @@ -8811,6 +10085,20 @@ inline int count_leading_zeros(std::uint64_t x) noexcept { #if defined(__GNUC__) || defined(__clang__) return __builtin_clzll(x); +#elif defined(_MSC_VER) && (defined(_M_X64) || defined(_M_ARM64)) + unsigned long index = 0; // NOLINT(runtime/int): the type _BitScan*64 takes + _BitScanReverse64(&index, x); + return 63 - static_cast(index); +#elif defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_ARM)) + unsigned long index = 0; // NOLINT(runtime/int): the type _BitScan* takes + if (_BitScanReverse(&index, static_cast(x >> 32u))) // NOLINT(runtime/int) + { + return 31 - static_cast(index); + } + _BitScanReverse(&index, static_cast(x)); // NOLINT(runtime/int) + return 63 - static_cast(index); +#elif defined(__cpp_lib_bitops) + return std::countl_zero(x); #else int n = 0; for (int shift = 32; shift != 0; shift >>= 1) @@ -8825,6 +10113,100 @@ inline int count_leading_zeros(std::uint64_t x) noexcept #endif } +/// number of trailing zero bits of x (x != 0) +inline int count_trailing_zeros(std::uint64_t x) noexcept +{ +#if defined(__GNUC__) || defined(__clang__) + return __builtin_ctzll(x); +#elif defined(_MSC_VER) && (defined(_M_X64) || defined(_M_ARM64)) + unsigned long index = 0; // NOLINT(runtime/int): the type _BitScan*64 takes + _BitScanForward64(&index, x); + return static_cast(index); +#elif defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_ARM)) + unsigned long index = 0; // NOLINT(runtime/int): the type _BitScan* takes + if (_BitScanForward(&index, static_cast(x))) // NOLINT(runtime/int) + { + return static_cast(index); + } + _BitScanForward(&index, static_cast(x >> 32u)); // NOLINT(runtime/int) + return 32 + static_cast(index); +#elif defined(__cpp_lib_bitops) + return std::countr_zero(x); +#else + int n = 0; + for (int shift = 32; shift != 0; shift >>= 1) + { + if ((x << (64 - shift)) == 0) + { + n += shift; + x >>= shift; + } + } + return n; +#endif +} + +/// the object representation of @a from as a @a To (std::bit_cast in C++20) +template +JSON_HEDLEY_ALWAYS_INLINE To bit_cast(const From& from) noexcept +{ + static_assert(sizeof(To) == sizeof(From), "bit_cast needs types of the same size"); +#if defined(__cpp_lib_bit_cast) + return std::bit_cast(from); +#else + To to {}; + std::memcpy(&to, &from, sizeof(To)); + return to; +#endif +} + +/// x with its bytes in reverse order (std::byteswap in C++23); compilers lower +/// it to a single bswap/rev/movbe +inline std::uint16_t byteswap(std::uint16_t x) noexcept +{ +#if defined(__GNUC__) || defined(__clang__) + return __builtin_bswap16(x); +#elif defined(_MSC_VER) + return _byteswap_ushort(x); +#elif defined(__cpp_lib_byteswap) + return std::byteswap(x); +#else + return static_cast((x >> 8u) | (x << 8u)); +#endif +} + +/// x with its bytes in reverse order (std::byteswap in C++23) +inline std::uint32_t byteswap(std::uint32_t x) noexcept +{ +#if defined(__GNUC__) || defined(__clang__) + return __builtin_bswap32(x); +#elif defined(_MSC_VER) + return _byteswap_ulong(x); +#elif defined(__cpp_lib_byteswap) + return std::byteswap(x); +#else + return ((x & 0x000000FFu) << 24u) | ((x & 0x0000FF00u) << 8u) + | ((x & 0x00FF0000u) >> 8u) | ((x & 0xFF000000u) >> 24u); +#endif +} + +/// x with its bytes in reverse order (std::byteswap in C++23) +inline std::uint64_t byteswap(std::uint64_t x) noexcept +{ +#if defined(__GNUC__) || defined(__clang__) + return __builtin_bswap64(x); +#elif defined(_MSC_VER) + return _byteswap_uint64(x); +#elif defined(__cpp_lib_byteswap) + return std::byteswap(x); +#else + x = ((x & 0x00000000FFFFFFFFull) << 32u) | ((x & 0xFFFFFFFF00000000ull) >> 32u); + x = ((x & 0x0000FFFF0000FFFFull) << 16u) | ((x & 0xFFFF0000FFFF0000ull) >> 16u); + x = ((x & 0x00FF00FF00FF00FFull) << 8u) | ((x & 0xFF00FF00FF00FF00ull) >> 8u); + return x; +#endif +} + /// the 128-bit product of two 64-bit numbers struct uint128_parts { @@ -8858,20 +10240,60 @@ inline uint128_parts full_multiplication(std::uint64_t a, std::uint64_t b) noexc #endif } -/// eight bytes as a little-endian word (compilers fold this into one load on -/// little-endian targets) -inline std::uint64_t read_eight_bytes(const char* p) noexcept +/// eight bytes as a little-endian word (a single load on little-endian +/// targets; always inlined, as GCC otherwise calls it in the number loops) +JSON_HEDLEY_ALWAYS_INLINE std::uint64_t read_eight_bytes(const unsigned char* b) noexcept { - const auto* b = reinterpret_cast(p); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) +#if JSON_LITTLE_ENDIAN_TARGET + // the byte order already matches + std::uint64_t result = 0; + std::memcpy(&result, b, sizeof(result)); + return result; +#else return static_cast(b[0]) | (static_cast(b[1]) << 8u) | (static_cast(b[2]) << 16u) | (static_cast(b[3]) << 24u) | (static_cast(b[4]) << 32u) | (static_cast(b[5]) << 40u) | (static_cast(b[6]) << 48u) | (static_cast(b[7]) << 56u); +#endif +} + +/// eight bytes as a little-endian word +JSON_HEDLEY_ALWAYS_INLINE std::uint64_t read_eight_bytes(const char* p) noexcept +{ + return read_eight_bytes(reinterpret_cast(p)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) } } // namespace detail NLOHMANN_JSON_NAMESPACE_END +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2021 The fast_float authors +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // any_of +#include // array +#include // FLT_EVAL_METHOD +#include // localeconv +#include // size_t +#include // int64_t, uint64_t +#include // strtof, strtod, strtold +#include // numeric_limits +#include // string +#include // conditional, integral_constant, is_signed, true_type, false_type +#include // move + +// #include + // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ @@ -8898,6 +10320,10 @@ namespace detail constexpr std::int64_t pow5_128_smallest_power = -342; constexpr std::int64_t pow5_128_largest_power = 308; +// every entry of pow5_128() holds two 64-bit halves of 5^q, one per covered power of 5 +static_assert((pow5_128_largest_power - pow5_128_smallest_power + 1) * 2 == 1302, + "pow5_128_smallest_power/pow5_128_largest_power must match the size of the pow5_128() table"); + /*! @brief 128-bit approximations of 5^q for q in [-342, 308] @@ -9261,15 +10687,32 @@ NLOHMANN_JSON_NAMESPACE_END #endif // This file contains the value-conversion helpers used by the lexer to turn an -// already-validated number token into a value, without the locale/errno -// overhead of std::strtoull/std::strtod where possible. They are free functions -// so the lexer stays focused on scanning (see lexer::convert_number()) and so -// that other parsers of JSON text can convert tokens exactly like it does. +// already-validated number token into a value. Integers and binary32/binary64 +// floats (float, double, and long double where it is binary64) are converted +// by the library itself, without the locale/errno overhead of +// std::strtoull/std::strtod and correctly rounded; other long double formats +// use std::from_chars or std::strtold. They are free functions so the lexer +// stays focused on scanning (see lexer::convert_number()) and so that other +// parsers of JSON text can convert tokens exactly like it does. NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail { +// whether an integer of a signed type is negative; the overload for other types +// avoids comparing an unsigned value with zero +template +constexpr bool is_negative_integer(NumberType x, std::true_type /*is_signed*/) noexcept +{ + return x < 0; +} + +template +constexpr bool is_negative_integer(NumberType /*unused*/, std::false_type /*is_signed*/) noexcept +{ + return false; +} + /*! @brief fast integer parser for an already-validated unsigned integer @@ -9303,8 +10746,10 @@ bool parse_integer_unsigned(const char* first, const char* last, NumberUnsignedT x = (x * 10u) + digit; } value = static_cast(x); - // reject values that do not round-trip into a narrower NumberUnsignedType - return static_cast(value) == x; + // reject values that do not round-trip into a narrower NumberUnsignedType, + // and values that wrap around to negative numbers if it is a signed type + // (number_unsigned_t may be the same type as number_integer_t, #2573) + return static_cast(value) == x && !is_negative_integer(value, std::is_signed()); } /*! @@ -9341,233 +10786,337 @@ bool parse_integer_signed(const char* first, const char* last, NumberIntegerType } /*! -@brief exact fast path for parsing a `double` (Clinger's algorithm) - -For the common case - at most 19 significant digits, a decimal exponent in -[-22, 22], and a significand below 2^53 - the value equals significand * -10^exp computed in IEEE-754 double arithmetic, which is exact under -round-to-nearest because both operands are exactly representable. This is the -same fast path used by fast_float/simdjson; the general cases are left to -std::strtod. The parser only activates for number_float_t == double; float and -long double keep the std::strtof/std::strtold paths (see the templated overload -below). - -@param[in] first pointer to the first character of the number -@param[in] last pointer past the last character -@param[out] out the parsed value on success -@return true if the value was parsed exactly; false to fall back to strtod +@brief parameters of the IEEE-754 binary32 and binary64 formats for the float + conversion (after fast_float's binary_format) */ -inline bool parse_float_fast(const char* first, const char* last, double& out) noexcept +template +struct ieee_binary_format; + +template<> +struct ieee_binary_format<24> // binary32 { -#if defined(FLT_EVAL_METHOD) && FLT_EVAL_METHOD != 0 - // Clinger's fast path is only exact when double operations are evaluated in - // true double precision. On platforms that keep intermediates in extended - // precision (e.g. the x87 FPU on 32-bit x86, where FLT_EVAL_METHOD == 2) the - // single significand * 10^scale step is double-rounded and can be 1 ULP off, - // so decline and let the caller fall back to the correctly-rounded - // std::from_chars / std::strtod path. - static_cast(first); - static_cast(last); - static_cast(out); - return false; -#else - static const std::array powers_of_ten = + static constexpr int mantissa_bits() noexcept { - { - 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, - 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22 - } - }; + return 23; + } + static constexpr int sign_bit() noexcept + { + return 31; + } + static constexpr int minimum_exponent() noexcept + { + return -127; + } + static constexpr int infinite_power() noexcept + { + return 0xFF; + } + // w * 10^q with w < 2^64 is below half the smallest subnormal number for + // q < smallest_power_of_ten() and at least infinity for q > largest_power_of_ten() + static constexpr int smallest_power_of_ten() noexcept + { + return -64; + } + static constexpr int largest_power_of_ten() noexcept + { + return 38; + } + // w * 10^q can only be exactly between two numbers for q in this range + static constexpr int min_exponent_round_to_even() noexcept + { + return -17; + } + static constexpr int max_exponent_round_to_even() noexcept + { + return 10; + } + // Clinger's fast path: w and 10^|q| are exact + static constexpr int max_exponent_fast_path() noexcept + { + return 10; + } + static constexpr std::uint64_t max_mantissa_fast_path() noexcept + { + return std::uint64_t{2} << 23u; + } + // a midpoint between two numbers has at most this many significant digits + static constexpr std::int64_t max_digits() noexcept + { + return 114; + } +}; - const char* p = first; - bool negative = false; - if (p != last && (*p == '-' || *p == '+')) - { - negative = (*p == '-'); - ++p; - } - - std::uint64_t significand = 0; - int num_digits = 0; - int fractional_digits = 0; - bool seen_dot = false; - bool any_digit = false; - for (; p != last; ++p) - { - const char c = *p; - if (c >= '0' && c <= '9') - { - any_digit = true; - if (JSON_HEDLEY_UNLIKELY(num_digits >= 19)) - { - return false; // significand may not fit into uint64_t - } - significand = (significand * 10u) + static_cast(c - '0'); - ++num_digits; - fractional_digits += static_cast(seen_dot); - } - else if (c == '.') - { - if (JSON_HEDLEY_UNLIKELY(seen_dot)) - { - return false; - } - seen_dot = true; - } - else if (c == 'e' || c == 'E') - { - ++p; - break; - } - else - { - return false; - } - } - if (JSON_HEDLEY_UNLIKELY(!any_digit)) - { - return false; - } - - int exponent = 0; - if (p != last) // an exponent part remains - { - bool exp_negative = false; - if (p != last && (*p == '-' || *p == '+')) - { - exp_negative = (*p == '-'); - ++p; - } - bool any_exp_digit = false; - for (; p != last; ++p) - { - if (JSON_HEDLEY_UNLIKELY(*p < '0' || *p > '9')) - { - return false; - } - exponent = (exponent * 10) + (*p - '0'); - any_exp_digit = true; - if (JSON_HEDLEY_UNLIKELY(exponent > 9999)) - { - return false; - } - } - if (JSON_HEDLEY_UNLIKELY(!any_exp_digit)) - { - return false; - } - if (exp_negative) - { - exponent = -exponent; - } - } - - const int scale = exponent - fractional_digits; - if (JSON_HEDLEY_UNLIKELY(significand >= (static_cast(1) << 53))) - { - return false; // significand not exactly representable as double - } - - auto result = static_cast(significand); - if (scale >= 0) - { - if (JSON_HEDLEY_UNLIKELY(scale > 22)) - { - return false; - } - result *= powers_of_ten[static_cast(scale)]; - } - else - { - if (JSON_HEDLEY_UNLIKELY(-scale > 22)) - { - return false; - } - result /= powers_of_ten[static_cast(-scale)]; - } - out = negative ? -result : result; - return true; -#endif -} - -/// fast float path is only exact for `double`; decline for float/long double -template -bool parse_float_fast(const char* /*first*/, const char* /*last*/, FloatType& /*out*/) noexcept +template<> +struct ieee_binary_format<53> // binary64 { - return false; -} + static constexpr int mantissa_bits() noexcept + { + return 52; + } + static constexpr int sign_bit() noexcept + { + return 63; + } + static constexpr int minimum_exponent() noexcept + { + return -1023; + } + static constexpr int infinite_power() noexcept + { + return 0x7FF; + } + static constexpr int smallest_power_of_ten() noexcept + { + return -342; + } + static constexpr int largest_power_of_ten() noexcept + { + return 308; + } + static constexpr int min_exponent_round_to_even() noexcept + { + return -4; + } + static constexpr int max_exponent_round_to_even() noexcept + { + return 23; + } + static constexpr int max_exponent_fast_path() noexcept + { + return 22; + } + static constexpr std::uint64_t max_mantissa_fast_path() noexcept + { + return std::uint64_t{2} << 52u; + } + static constexpr std::int64_t max_digits() noexcept + { + return 769; + } +}; /*! -@brief parse a float with std::from_chars (Eisel-Lemire) when available +@brief whether @a FloatType is IEEE-754 binary32 or binary64 -std::from_chars is locale-independent, correctly rounded, and - via the -Eisel-Lemire algorithm in modern standard libraries - much faster than strtod -over the whole value range (not just the Clinger subset). It is used only when -__cpp_lib_to_chars indicates full floating-point support and only when it -consumes the entire token ([first, last)). An under-/overflow (result_out_of_range) also declines, so -the caller's strtod fallback supplies the well-defined ±inf/0 result the parser -expects (side-stepping the P4168 divergence between implementations). - -@return true if the value was parsed exactly and fully; false to fall back +These formats (float, double, and long double where it is binary64, e.g. +with MSVC or on Apple arm64) are converted by parse_float_native(). The +predicate is the one the serializer uses to choose Grisu2. */ template -bool parse_float_from_chars(const char* first, const char* last, FloatType& out) noexcept +struct has_native_float_format { - // JSON_HAS_CPP_17 must gate the use as well as the include above: - // some standard libraries (e.g. libstdc++ 15) define __cpp_lib_to_chars even - // in C++14 mode, where is not included. -#if defined(JSON_HAS_CPP_17) && defined(__cpp_lib_to_chars) - const auto result = std::from_chars(first, last, out); - return result.ec == std::errc() && result.ptr == last; -#else - static_cast(first); - static_cast(last); - static_cast(out); - return false; -#endif -} + static constexpr bool value = + (std::numeric_limits::is_iec559 && std::numeric_limits::digits == 24 && std::numeric_limits::max_exponent == 128) || + (std::numeric_limits::is_iec559 && std::numeric_limits::digits == 53 && std::numeric_limits::max_exponent == 1024); +}; -/// whether the eight bytes of @a v (see read_eight_bytes()) are ASCII digits -/// (after fast_float's is_made_of_eight_digits_fast) -inline bool is_eight_digits(std::uint64_t v) noexcept -{ - return ((v & 0xF0F0F0F0F0F0F0F0u) | (((v + 0x0606060606060606u) & 0xF0F0F0F0F0F0F0F0u) >> 4u)) == 0x3333333333333333u; -} +/// the C++ type (float or double) that holds a binary32 or binary64 @a FloatType +template +using native_float_t = typename std::conditional::digits == 24, float, double>::type; /// the value of the eight ASCII digits in @a v (see read_eight_bytes()), three -/// multiplications instead of eight (after simdjson and fast_float) -inline std::uint32_t parse_eight_digits(std::uint64_t v) noexcept +/// multiplications instead of eight (after simdjson and fast_float); always +/// inlined, as GCC otherwise calls it in the number loops +JSON_HEDLEY_ALWAYS_INLINE std::uint32_t parse_eight_digits(std::uint64_t v) noexcept { v = ((v & 0x0F0F0F0F0F0F0F0Fu) * 2561u) >> 8u; v = ((v & 0x00FF00FF00FF00FFu) * 6553601u) >> 16u; return static_cast(((v & 0x0000FFFF0000FFFFu) * 42949672960001u) >> 32u); } +/// whether the eight bytes in @a v (see read_eight_bytes()) are all ASCII +/// digits (after fast_float) +JSON_HEDLEY_ALWAYS_INLINE bool is_eight_digits(std::uint64_t v) noexcept +{ + return ((v & 0xF0F0F0F0F0F0F0F0u) | (((v + 0x0606060606060606u) & 0xF0F0F0F0F0F0F0F0u) >> 4u)) == 0x3333333333333333u; +} + /*! -@brief the double nearest to w * 10^q (Eisel-Lemire) +@brief skip the ASCII digits starting at data[i] and accumulate their value + +Eight digits are taken at a time while at least eight bytes remain. @a w wraps +around after 19 digits; the caller only uses it for shorter digit runs. + +@return the index of the first byte at or after @a i that is not a digit +*/ +JSON_HEDLEY_ALWAYS_INLINE std::size_t accumulate_digits(const char* data, std::size_t i, std::size_t avail, std::uint64_t& w) noexcept +{ + while (avail - i >= 8) + { + const std::uint64_t v = read_eight_bytes(data + i); + if (!is_eight_digits(v)) + { + break; + } + w = (w * 100000000u) + parse_eight_digits(v); + i += 8; + } + while (i < avail && data[i] >= '0' && data[i] <= '9') + { + w = (w * 10u) + static_cast(data[i] - '0'); + ++i; + } + return i; +} + +/// whether [first, last) contains a digit other than '0'; a plain loop, as +/// GCC calls std::any_of out of line even for the usual empty range +JSON_HEDLEY_ALWAYS_INLINE bool has_nonzero_digit(const char* first, const char* last) noexcept +{ + for (; first != last; ++first) + { + if (*first != '0') + { + return true; + } + } + return false; +} + +/*! +@brief accumulate the validated digits [p, last) into @a w, as long as + @a remaining digits still fit into it + +Eight digits are taken at a time while at least eight remain, and @a remaining +is decreased by the number of digits taken. + +@return a pointer past the last digit taken +*/ +JSON_HEDLEY_ALWAYS_INLINE const char* accumulate_significant_digits(const char* p, const char* last, int& remaining, std::uint64_t& w) noexcept +{ + while (remaining >= 8 && last - p >= 8) + { + w = (w * 100000000u) + parse_eight_digits(read_eight_bytes(p)); + p += 8; + remaining -= 8; + } + for (; remaining > 0 && p != last; ++p, --remaining) + { + w = (w * 10u) + static_cast(*p - '0'); + } + return p; +} + +/// the value of the validated exponent digits [+-]?[0-9]+ in [first, last), +/// saturated far beyond every range +inline std::int64_t parse_float_exponent(const char* first, const char* last) noexcept +{ + const bool negative = *first == '-'; + first += (*first == '-' || *first == '+') ? 1 : 0; + constexpr std::int64_t saturation = 100000000000000000; // 10^17 + std::int64_t value = 0; + for (; first != last; ++first) + { + if (value < saturation) + { + value = (value * 10) + (*first - '0'); + } + } + return negative ? -value : value; +} + +/// a float token as w * 10^exponent, see parse_float_significand() +struct float_significand +{ + std::uint64_t w = 0; ///< the first (at most 19) significant digits + std::int64_t exponent = 0; ///< the decimal exponent of the last digit in w + bool negative = false; ///< whether the token starts with '-' + bool truncated = false; ///< whether nonzero digits follow the ones in w +}; + +/*! +@brief split a validated number token into sign, significand, and exponent + +The lexer has validated the token against the JSON grammar and knows where its +parts are, so this needs no character classification: the integer part ends at +@a decimal_point_position (or @a mantissa_end), the fraction at @a mantissa_end, +and an exponent follows. At most 19 significant digits are kept; the value then +lies in [w, w + 1) * 10^exponent, and is exactly w * 10^exponent unless +truncated is set. + +@param[in] first pointer to the first character of the token +@param[in] last pointer past the last character +@param[in] decimal_point_position index of the '.' in the token, or + std::string::npos if there is none +@param[in] mantissa_end index of the 'e'/'E', or the token length +*/ +JSON_HEDLEY_ALWAYS_INLINE float_significand parse_float_significand(const char* first, const char* last, + std::size_t decimal_point_position, std::size_t mantissa_end) noexcept +{ + float_significand s; + const char* p = first; + s.negative = *p == '-'; + p += s.negative ? 1 : 0; + const bool has_dot = decimal_point_position != std::string::npos; + const char* const mantissa_last = first + mantissa_end; + const char* const integer_last = has_dot ? first + decimal_point_position : mantissa_last; + + std::uint64_t w = 0; + int remaining = 19; // digits that still fit into w + if (*p != '0') // the integer part is "0" or [1-9][0-9]* + { + p = accumulate_significant_digits(p, integer_last, remaining, w); + s.exponent = integer_last - p; + s.truncated = has_nonzero_digit(p, integer_last); + } + + if (has_dot) + { + p = integer_last + 1; + if (w == 0) + { + // zeros after the decimal point of "0." are not significant + const char* const zeros = p; + while (p != mantissa_last && *p == '0') + { + ++p; + } + s.exponent -= p - zeros; + } + const char* const digits = p; + p = accumulate_significant_digits(p, mantissa_last, remaining, w); + s.exponent -= p - digits; + s.truncated = s.truncated || has_nonzero_digit(p, mantissa_last); + } + + if (mantissa_last != last) + { + s.exponent += parse_float_exponent(mantissa_last + 1, last); + } + s.w = w; + return s; +} + +/*! +@brief the bits of the float nearest to w * 10^q (Eisel-Lemire) The algorithm of Daniel Lemire, "Number Parsing at a Gigabyte per Second" (Software: Practice and Experience, 2021), after fast_float's compute_float (used under the MIT license). With a 128-bit approximation of 5^q, the product -is always sufficient to round correctly for w with at most 19 digits (Noble -Mushtak and Daniel Lemire, "Fast number parsing without fallback", Software: -Practice and Experience, 2023). Only integer arithmetic is used, so the result -does not depend on the floating-point environment. +is always sufficient to round correctly for w < 2^64 (Noble Mushtak and Daniel +Lemire, "Fast number parsing without fallback", Software: Practice and +Experience, 2023). Only integer arithmetic is used, so the result does not +depend on the floating-point environment. +It is always inlined, like decimal_to_float(), so that hot loops of callers +keep the whole conversion inline. + +@tparam Format ieee_binary_format<24> (binary32) or ieee_binary_format<53> (binary64) @param[in] q decimal exponent -@param[in] w significand, w != 0 +@param[in] w significand @return the IEEE-754 bits of the positive result (0 for underflow, infinity for overflow) */ -inline std::uint64_t eisel_lemire(std::int64_t q, std::uint64_t w) noexcept +template +JSON_HEDLEY_ALWAYS_INLINE std::uint64_t eisel_lemire(std::int64_t q, std::uint64_t w) noexcept { - constexpr int mantissa_bits = 52; - constexpr std::uint64_t infinity = std::uint64_t{0x7FF} << mantissa_bits; - if (q < pow5_128_smallest_power) + constexpr int mantissa_bits = Format::mantissa_bits(); + constexpr std::uint64_t infinity = static_cast(Format::infinite_power()) << mantissa_bits; + if (w == 0 || q < Format::smallest_power_of_ten()) { return 0; } - if (q > pow5_128_largest_power) + if (q > Format::largest_power_of_ten()) { return infinity; } @@ -9591,8 +11140,8 @@ inline std::uint64_t eisel_lemire(std::int64_t q, std::uint64_t w) noexcept const auto upperbit = static_cast(product.high >> 63u); const int shift = upperbit + 64 - mantissa_bits - 3; std::uint64_t mantissa = product.high >> static_cast(shift); - // floor(log2(10^q)) + 63 + 1023, with log2(10) ~ 217706 / 2^16 - std::int64_t power2 = (((152170 + 65536) * q) >> 16) + 63 + upperbit - lz + 1023; + // floor(log2(10^q)) + 63 + bias, with log2(10) ~ 217706 / 2^16 + std::int64_t power2 = (((152170 + 65536) * q) >> 16) + 63 + upperbit - lz - Format::minimum_exponent(); if (power2 <= 0) // subnormal { @@ -9601,17 +11150,18 @@ inline std::uint64_t eisel_lemire(std::int64_t q, std::uint64_t w) noexcept return 0; } mantissa >>= static_cast(-power2 + 1); + // no tie is possible here: that needs a small |q| mantissa += (mantissa & 1u); mantissa >>= 1u; // rounding up may produce the smallest normal number power2 = (mantissa < (std::uint64_t{1} << mantissa_bits)) ? 0 : 1; - return mantissa | (static_cast(power2) << mantissa_bits); + return (mantissa & ((std::uint64_t{1} << mantissa_bits) - 1)) | (static_cast(power2) << mantissa_bits); } - // a value exactly between two doubles rounds to even; this can only + // a value exactly between two floats rounds to even; this can only // happen for small |q|, where 5^q is exact - if (product.low <= 1 && q >= -4 && q <= 23 && (mantissa & 3u) == 1 - && (mantissa << static_cast(shift)) == product.high) + if (product.low <= 1 && q >= Format::min_exponent_round_to_even() && q <= Format::max_exponent_round_to_even() + && (mantissa & 3u) == 1 && (mantissa << static_cast(shift)) == product.high) { mantissa &= ~std::uint64_t{1}; } @@ -9623,198 +11173,419 @@ inline std::uint64_t eisel_lemire(std::int64_t q, std::uint64_t w) noexcept ++power2; } mantissa &= ~(std::uint64_t{1} << mantissa_bits); - if (power2 >= 0x7FF) + if (power2 >= Format::infinite_power()) { return infinity; } return mantissa | (static_cast(power2) << mantissa_bits); } -/*! -@brief parse a validated float token with the Eisel-Lemire algorithm - -The significand is accumulated eight digits at a time where possible. A token -with more than 19 significant digits is truncated to w; the value then lies -in [w, w + 1) * 10^q, and it is only returned if both ends round to the same -double, which covers all but a few such tokens. - -@param[in] first pointer to the first character of the token -@param[in] last pointer past the last character -@param[out] out the correctly rounded value on success (±infinity if it - overflows, like strtod) -@return true on success; false if strtod must decide -*/ -inline bool parse_float_eisel_lemire(const char* first, const char* last, double& out) noexcept +/// an unsigned integer of up to 4096 bits for digit_comparison() (32-bit limbs, +/// so only 32x32->64-bit multiplications are needed) +class float_bigint { - const char* p = first; - const bool negative = (p != last && *p == '-'); - if (negative) + public: + explicit float_bigint(std::uint64_t value) noexcept { - ++p; + for (; value != 0; value >>= 32u) + { + limbs[count++] = static_cast(value); + } } - std::uint64_t w = 0; - unsigned int digits = 0; // significant digits in w - std::int64_t exponent = 0; - bool truncated = false; - bool in_fraction = false; - for (;;) + /// *this = *this * factor + summand + void multiply_add(std::uint32_t factor, std::uint32_t summand) noexcept { - // eight digits at a time, as long as they fit into w - while (w != 0 && digits <= 19u - 8u && last - p >= 8) + std::uint64_t carry = summand; + for (std::size_t i = 0; i < count; ++i) { - const std::uint64_t v = read_eight_bytes(p); - if (!is_eight_digits(v)) - { - break; - } - w = (w * 100000000u) + parse_eight_digits(v); - digits += 8u; - exponent -= in_fraction ? 8 : 0; - p += 8; + const std::uint64_t product = (static_cast(limbs[i]) * factor) + carry; + limbs[i] = static_cast(product); + carry = product >> 32u; } - if (p == last) + if (carry != 0) { - break; + JSON_ASSERT(count < limbs.size()); + limbs[count++] = static_cast(carry); } - const char c = *p; - if (c >= '0' && c <= '9') + } + + /// *this = *this * 5^n + void multiply_power_of_five(std::int64_t n) noexcept + { + static const std::array powers = { - if (w == 0 && c == '0') - { - // leading zeros are not significant, but scale a fraction - exponent -= in_fraction ? 1 : 0; - } - else if (digits < 19u) - { - w = (w * 10u) + static_cast(c - '0'); - ++digits; - exponent -= in_fraction ? 1 : 0; - } - else - { - // dropped: the value lies between w and w + 1 (in units of - // the last kept digit) unless all dropped digits are zero - truncated = truncated || c != '0'; - exponent += in_fraction ? 0 : 1; - } - ++p; + {1u, 5u, 25u, 125u, 625u, 3125u, 15625u, 78125u, 390625u, 1953125u, 9765625u, 48828125u, 244140625u, 1220703125u} + }; + for (; n >= 13; n -= 13) + { + multiply_add(powers[13], 0); } - else if (c == '.') + multiply_add(powers[static_cast(n)], 0); + } + + /// *this = *this * 2^n + void shift_left(std::int64_t n) noexcept + { + if (count == 0) { - in_fraction = true; - ++p; + return; + } + const auto limb_shift = static_cast(n / 32); + const auto bit_shift = static_cast(n % 32); + JSON_ASSERT(count + limb_shift + 1 <= limbs.size()); + if (bit_shift != 0) + { + std::uint32_t carry = 0; + for (std::size_t i = 0; i < count; ++i) + { + const std::uint32_t limb = limbs[i]; + limbs[i] = (limb << bit_shift) | carry; + carry = limb >> (32u - bit_shift); + } + if (carry != 0) + { + limbs[count++] = carry; + } + } + if (limb_shift != 0) + { + for (std::size_t i = count; i-- > 0;) + { + limbs[i + limb_shift] = limbs[i]; + } + for (std::size_t i = 0; i < limb_shift; ++i) + { + limbs[i] = 0; + } + count += limb_shift; + } + } + + /// -1, 0, or 1 if *this is less than, equal to, or greater than @a other + int compare(const float_bigint& other) const noexcept + { + if (count != other.count) + { + return count < other.count ? -1 : 1; + } + for (std::size_t i = count; i-- > 0;) + { + if (limbs[i] != other.limbs[i]) + { + return limbs[i] < other.limbs[i] ? -1 : 1; + } + } + return 0; + } + + private: + std::array limbs{{}}; + std::size_t count = 0; +}; + +/*! +@brief round a token exactly when eisel_lemire() cannot decide (slow path) + +The value v of the token lies strictly between two adjacent floats, whose +lower one has the bits @a lower, and the result depends on whether v is below, +at, or above the midpoint m between them. Both are compared exactly as big +integers: v = D * 10^s with the significant digits D (at most +Format::max_digits() of them, more than any midpoint has; further nonzero +digits only put v above m) and m = (2 * mantissa + 1) * 2^(e - 1). This is the +digit comparison of fast_float (Daniel Lemire and contributors, used under the +MIT license), simplified by starting from the two candidates. + +@param[in] first pointer to the first character of the token +@param[in] last pointer past the last character +@param[in] lower the bits of the float below v +@return the bits of the correctly rounded result +*/ +template +std::uint64_t digit_comparison(const char* first, const char* last, std::uint64_t lower) noexcept +{ + const char* p = first + ((*first == '-') ? 1 : 0); + + // D, in chunks of up to 9 digits, and s + float_bigint digits(0); + std::int64_t count = 0; + std::int64_t point = 0; // the value is 0.D... * 10^point + bool truncated = false; + std::uint32_t chunk = 0; + int chunk_digits = 0; + static const std::array powers_of_ten = {{1u, 10u, 100u, 1000u, 10000u, 100000u, 1000000u, 10000000u, 100000000u, 1000000000u}}; + const auto append = [&](char c) noexcept + { + if (count < Format::max_digits()) + { + chunk = (chunk * 10u) + static_cast(c - '0'); + ++count; + if (++chunk_digits == 9) + { + digits.multiply_add(powers_of_ten[9], chunk); + chunk = 0; + chunk_digits = 0; + } } else { - break; // 'e' or 'E' + truncated = truncated || c != '0'; + } + }; + bool significant = false; + for (; p != last && *p >= '0' && *p <= '9'; ++p) + { + significant = significant || *p != '0'; + if (significant) + { + append(*p); + ++point; } } - - if (p != last) + if (p != last && *p == '.') { - ++p; // 'e' or 'E' - bool exp_negative = false; - if (p != last && (*p == '-' || *p == '+')) + for (++p; p != last && *p >= '0' && *p <= '9'; ++p) { - exp_negative = (*p == '-'); - ++p; - } - std::int64_t exp_value = 0; - for (; p != last; ++p) - { - // saturate: any exponent beyond this under- or overflows anyway - if (exp_value < 100000) + significant = significant || *p != '0'; + if (significant) { - exp_value = (exp_value * 10) + (*p - '0'); + append(*p); + } + else + { + --point; } } - exponent += exp_negative ? -exp_value : exp_value; } - - std::uint64_t bits = 0; - if (w != 0) + if (chunk_digits != 0) { - bits = eisel_lemire(exponent, w); - if (truncated && (w + 1 == 0 || eisel_lemire(exponent, w + 1) != bits)) - { - return false; - } + digits.multiply_add(powers_of_ten[static_cast(chunk_digits)], chunk); } - bits |= negative ? (std::uint64_t{1} << 63u) : 0u; - static_assert(sizeof(double) == sizeof(std::uint64_t), "double must have 64 bits"); - std::memcpy(&out, &bits, sizeof(out)); - return true; + if (p != last) + { + point += parse_float_exponent(p + 1, last); + } + const std::int64_t s = point - count; // v = D * 10^s + + // the midpoint above the lower candidate + constexpr int mantissa_bits = Format::mantissa_bits(); + const std::uint64_t exponent_field = lower >> mantissa_bits; + std::uint64_t mantissa = lower & ((std::uint64_t{1} << mantissa_bits) - 1); + std::int64_t e = 1 + Format::minimum_exponent() - mantissa_bits; // of the smallest subnormal number + if (exponent_field != 0) + { + mantissa |= std::uint64_t{1} << mantissa_bits; + e += static_cast(exponent_field) - 1; + } + float_bigint midpoint((2 * mantissa) + 1); + const std::int64_t midpoint_exponent = e - 1; // m = midpoint * 2^midpoint_exponent + + // compare D * 5^s * 2^s with midpoint * 2^midpoint_exponent + if (s >= 0) + { + digits.multiply_power_of_five(s); + } + else + { + midpoint.multiply_power_of_five(-s); + } + const std::int64_t shift = s - midpoint_exponent; + if (shift >= 0) + { + digits.shift_left(shift); + } + else + { + midpoint.shift_left(-shift); + } + const int order = digits.compare(midpoint); + const bool round_up = order > 0 || (order == 0 && (truncated || (mantissa & 1u) != 0)); + return lower + (round_up ? 1u : 0u); } -/// Eisel-Lemire is only implemented for `double` -template -bool parse_float_eisel_lemire(const char* /*first*/, const char* /*last*/, FloatType& /*out*/) noexcept +/// the double with the IEEE-754 bits @a bits +inline void float_from_bits(std::uint64_t bits, double& value) noexcept { - return false; + static_assert(sizeof(double) == sizeof(std::uint64_t), "double must have 64 bits"); + value = bit_cast(bits); +} + +/// the float with the IEEE-754 bits @a bits (the lower 32) +inline void float_from_bits(std::uint64_t bits, float& value) noexcept +{ + static_assert(sizeof(float) == sizeof(std::uint32_t), "float must have 32 bits"); + value = bit_cast(static_cast(bits)); +} + +/// the powers of ten that are exact in binary64 (up to 10^22) +inline double exact_power_of_ten(std::int64_t n, double /*tag*/) noexcept +{ + static const std::array powers = + { + { + 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, + 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22 + } + }; + return powers[static_cast(n)]; +} + +/// the powers of ten that are exact in binary32 (up to 10^10) +inline float exact_power_of_ten(std::int64_t n, float /*tag*/) noexcept +{ + static const std::array powers = + { + {1e0f, 1e1f, 1e2f, 1e3f, 1e4f, 1e5f, 1e6f, 1e7f, 1e8f, 1e9f, 1e10f} + }; + return powers[static_cast(n)]; } /*! -@brief check whether Clinger's fast path can still succeed for a float token +@brief the binary32/binary64 value of a significand that was not truncated -parse_float_fast() needs a significand below 2^53. A mantissa with 17 or -more significant digits is at least 10^16 and therefore always exceeds it, -so calling the fast path would walk the token one extra time only to -decline before strtod has to run anyway. +The result is (-1)^negative * w * 10^exponent, correctly rounded (ties to +even): with Clinger's fast path where w and 10^|exponent| are exact, so that a +single floating-point operation rounds (only where intermediate results are not +kept in extended precision, see FLT_EVAL_METHOD), and with eisel_lemire() +otherwise. A value too large for the type becomes ±infinity, a value too small +±0. -Significant digits are the mantissa's digits from the first nonzero one on; -the sign, the decimal point, leading zeros, and the exponent do not count. -The answer is derived from indices - the digits are not scanned again - so -this stays off the hot path of the number scanners. +This is the core of the conversion that other parsers of JSON text share: they +can split a token themselves and still get the lexer's result. It is always +inlined, so that their hot loops keep the whole conversion inline. -@param[in] token the validated number token ('.' as decimal point) -@param[in] decimal_point_position index of the '.' in @a token, or - std::string::npos if there is none -@param[in] mantissa_end offset just past the last mantissa byte -@return false if parse_float_fast() is guaranteed to decline +@param[in] s the significand, with s.truncated == false */ -inline bool mantissa_fits_clinger(const char* token, std::size_t decimal_point_position, std::size_t mantissa_end) noexcept +template +JSON_HEDLEY_ALWAYS_INLINE FloatType decimal_to_float(const float_significand& s) noexcept { - // 10^16 already exceeds 2^53, so 17 digits can never fit - constexpr std::size_t limit = 17; + using result_type = native_float_t; + using format = ieee_binary_format::digits>; + JSON_ASSERT(!s.truncated); - const std::size_t neg = (token[0] == '-') ? 1u : 0u; - const std::size_t has_dot = (decimal_point_position != std::string::npos) ? 1u : 0u; - // the JSON grammar restricts the integer part to "0" or [1-9][0-9]*, so - // a leading zero can only be a lone "0", which is not significant - const std::size_t lead_zero = (token[neg] == '0') ? 1u : 0u; - JSON_ASSERT(mantissa_end >= neg + has_dot + lead_zero); - std::size_t digits = mantissa_end - neg - has_dot - lead_zero; - - if (JSON_HEDLEY_LIKELY(digits < limit)) +#if !defined(FLT_EVAL_METHOD) || FLT_EVAL_METHOD == 0 + if (s.exponent >= -format::max_exponent_fast_path() && s.exponent <= format::max_exponent_fast_path() + && s.w <= format::max_mantissa_fast_path()) { - return true; - } - - // Only a number below 1 can carry further insignificant zeros, and only - // while the count stays at the limit does removing them change the - // answer - so this loop is skipped for all but a few tokens. The - // fraction is located through decimal_point_position rather than by - // searching '.'. - if (lead_zero != 0) - { - JSON_ASSERT(has_dot != 0); // an integer "0" cannot reach the limit - for (std::size_t i = decimal_point_position + 1; - digits >= limit && i < mantissa_end && token[i] == '0'; ++i) + auto value = static_cast(s.w); + if (s.exponent < 0) { - --digits; + value /= exact_power_of_ten(-s.exponent, result_type{}); } + else + { + value *= exact_power_of_ten(s.exponent, result_type{}); + } + const FloatType result = s.negative ? -value : value; + return result; + } +#endif + + result_type value{}; + float_from_bits(eisel_lemire(s.exponent, s.w) | (s.negative ? (std::uint64_t{1} << format::sign_bit()) : 0u), value); + const FloatType result = value; + return result; +} + +/*! +@brief convert a validated number token to the nearest binary32/binary64 value + +The conversion is correctly rounded (ties to even) and independent of the +locale and of the C and C++ libraries: +1. parse_float_significand() splits the token into w * 10^q. +2. If no digits were dropped, decimal_to_float() rounds w * 10^q (Clinger's + fast path or Eisel-Lemire). +3. Otherwise, the value lies in [w, w + 1) * 10^q: if eisel_lemire() rounds + both ends to the same value, so does the token (in all but rare cases). +4. Otherwise, digit_comparison() compares the token exactly with the midpoint + between the two candidates. +A value too large for the type becomes ±infinity (the parser reports +out_of_range.406), a value too small ±0. + +@param[in] first pointer to the first character of the token +@param[in] last pointer past the last character +@param[in] decimal_point_position index of the '.' in the token, or + std::string::npos if there is none +@param[in] mantissa_end index of the 'e'/'E', or the token length +*/ +template +FloatType parse_float_native(const char* first, const char* last, + std::size_t decimal_point_position, std::size_t mantissa_end) noexcept +{ + using result_type = native_float_t; + using format = ieee_binary_format::digits>; + static_assert(std::numeric_limits::digits == std::numeric_limits::digits, "unexpected float format"); + + const float_significand s = parse_float_significand(first, last, decimal_point_position, mantissa_end); + if (JSON_HEDLEY_LIKELY(!s.truncated)) + { + return decimal_to_float(s); } - return digits < limit; + std::uint64_t bits = eisel_lemire(s.exponent, s.w); + if (JSON_HEDLEY_UNLIKELY(bits != eisel_lemire(s.exponent, s.w + 1))) + { + bits = digit_comparison(first, last, bits); + } + result_type value{}; + float_from_bits(bits | (s.negative ? (std::uint64_t{1} << format::sign_bit()) : 0u), value); + const FloatType result = value; + return result; +} + +/*! +@brief parse a float with std::from_chars when available + +Only used for the formats parse_float_native() does not convert (long double +formats other than binary64). std::from_chars is locale-independent and +correctly rounded. It is used only when __cpp_lib_to_chars indicates full +floating-point support and only when it consumes the entire token ([first, +last)). An under-/overflow (result_out_of_range) also declines, so the +caller's strtold fallback supplies the well-defined ±inf/0 result the parser +expects (side-stepping the P4168 divergence between implementations). + +@return true if the value was parsed exactly and fully; false to fall back +*/ +template +bool parse_float_from_chars(const char* first, const char* last, FloatType& out) noexcept +{ + // JSON_HAS_CPP_17 must gate the use as well as the include above: + // some standard libraries (e.g. libstdc++ 15) define __cpp_lib_to_chars even + // in C++14 mode, where is not included. +#if defined(JSON_HAS_CPP_17) && defined(__cpp_lib_to_chars) + const auto result = std::from_chars(first, last, out); + return result.ec == std::errc() && result.ptr == last; +#else + static_cast(first); + static_cast(last); + static_cast(out); + return false; +#endif +} + +/// binary32 and binary64: the library's own conversion, which always succeeds +template +bool convert_float_fast(const char* first, const char* last, std::size_t decimal_point_position, + std::size_t mantissa_end, FloatType& value, std::true_type /*native*/) noexcept +{ + value = parse_float_native(first, last, decimal_point_position, mantissa_end); + return true; +} + +/// other formats (long double on x87, binary128, double-double): std::from_chars, if available +template +bool convert_float_fast(const char* first, const char* last, std::size_t /*decimal_point_position*/, + std::size_t /*mantissa_end*/, FloatType& value, std::false_type /*native*/) noexcept +{ + return parse_float_from_chars(first, last, value); } /*! @brief convert a validated float token without the C library, if possible -Tries std::from_chars (when available), Clinger's exact fast path (double -only, skipped when it cannot succeed), and the Eisel-Lemire algorithm (double -only). +float, double, and long double where it is binary64 are always converted, by +parse_float_native(). Other long double formats are converted with +std::from_chars where the standard library supports it. @param[in] first pointer to the first character of the token @param[in] last pointer past the last character @@ -9830,19 +11601,8 @@ template bool convert_float_fast(const char* first, const char* last, std::size_t decimal_point_position, std::size_t mantissa_end, FloatType& value) noexcept { - if (parse_float_from_chars(first, last, value)) - { - return true; - } - // Skipping a fast path that cannot succeed is lossless and saves a full - // extra pass over the token's bytes, which otherwise shows up on - // high-precision inputs such as canada.json - if (mantissa_fits_clinger(first, decimal_point_position, mantissa_end) - && parse_float_fast(first, last, value)) - { - return true; - } - return parse_float_eisel_lemire(first, last, value); + return convert_float_fast(first, last, decimal_point_position, mantissa_end, value, + std::integral_constant::value> {}); } /// std::strtof, std::strtod, or std::strtold, chosen by the type of @a f @@ -9866,36 +11626,45 @@ inline void strtof_by_type(long double& f, const char* str, char** endptr) noexc f = std::strtold(str, endptr); } -/// return the decimal point of the current locale -inline char get_decimal_point() noexcept +/// return the decimal point of the current locale (it may be longer than one byte) +inline std::string get_decimal_point() { const auto* loc = localeconv(); JSON_ASSERT(loc != nullptr); - return (loc->decimal_point == nullptr) ? '.' : *(loc->decimal_point); + return (loc->decimal_point == nullptr || *loc->decimal_point == '\0') ? "." : loc->decimal_point; } /*! @brief convert a validated float token with strtof/strtod/strtold +Only used for what convert_float_fast() does not convert: long double formats +other than binary64 where std::from_chars is unavailable or reports an under- +or overflow, and floating-point types that are not IEEE-754 (see +has_native_float_format). + These functions expect the decimal point of the *current* locale, so it is looked up right before the conversion instead of once when the lexer is constructed: a locale change in between (by a parser callback, a SAX -handler, or another thread) must not truncate the value (#5198). The -token has been validated before, so if the conversion stops early and the -decimal point changed in the meantime, the locale changed between the +handler, or another thread) must not truncate the value (#5198). A +single-byte decimal point is substituted in place and restored afterwards, +because the token is also handed to the SAX interface. A longer one (e.g., +the two-byte U+066B of ar_EG.UTF-8 or fa_IR.UTF-8) is put into a copy of the +token instead (#5660). + +The token has been validated before, so if the conversion stops early and +the decimal point changed in the meantime, the locale changed between the lookup and the call, and the conversion is repeated with the new decimal -point. If the decimal point did not change, a retry cannot succeed: the -locale's decimal point is not a single character (e.g., the two-byte -U+066B of ar_EG.UTF-8 or fa_IR.UTF-8) and cannot be substituted in place. -The value strtod parsed up to that point is kept, as before this change. +point. If it did not change, the value strtod parsed up to that point is +kept. Note that changing the locale in another thread *while* strtod runs is undefined behavior of the C library, which this function cannot prevent. -@param[in,out] token the token with '.' as decimal point; its - decimal point is replaced during the - conversion and restored afterwards - (data() must be NUL-terminated) +@param[in,out] token the token with '.' as decimal point; a + single-byte decimal point is put in + place during the conversion and + restored afterwards (data() must be + NUL-terminated) @param[in] decimal_point_position index of the '.' in @a token, or std::string::npos if there is none @param[out] value the converted value @@ -9904,39 +11673,77 @@ template void convert_float_locale_aware(StringType& token, std::size_t decimal_point_position, FloatType& value) { const bool has_dot = decimal_point_position != std::string::npos; - char decimal_point = get_decimal_point(); + std::string decimal_point = get_decimal_point(); for (;;) { - const bool substitute = has_dot && decimal_point != '.'; - if (substitute) - { - token[decimal_point_position] = static_cast(decimal_point); - } - char* endptr = nullptr; // NOLINT(misc-const-correctness,cppcoreguidelines-pro-type-vararg,hicpp-vararg) - strtof_by_type(value, token.data(), &endptr); - - if (substitute) + bool complete = false; + if (!has_dot || decimal_point.size() == 1) { - // the caller hands the token on (e.g. to the SAX interface) with '.' - token[decimal_point_position] = '.'; + const bool substitute = has_dot && decimal_point[0] != '.'; + if (substitute) + { + token[decimal_point_position] = static_cast(decimal_point[0]); + } + strtof_by_type(value, token.data(), &endptr); + if (substitute) + { + // the caller hands the token on (e.g. to the SAX interface) with '.' + token[decimal_point_position] = '.'; + } + complete = endptr == token.data() + token.size(); + } + else + { + std::string buffer(token.data(), token.size()); + buffer.replace(decimal_point_position, 1, decimal_point); + strtof_by_type(value, buffer.data(), &endptr); + complete = endptr == buffer.data() + buffer.size(); } - if (JSON_HEDLEY_LIKELY(endptr == token.data() + token.size())) + if (JSON_HEDLEY_LIKELY(complete)) { return; } // retry only if the locale changed; otherwise, this would loop forever - const char current_decimal_point = get_decimal_point(); + std::string current_decimal_point = get_decimal_point(); if (current_decimal_point == decimal_point) { return; } - decimal_point = current_decimal_point; + decimal_point = std::move(current_decimal_point); } } +/*! +@brief convert a validated float token like the lexer does + +For parsers of JSON text other than the lexer, which converts its own token +buffer in place. float, double, and long double where it is binary64 are +converted without allocation and independent of the locale; only other long +double formats that std::from_chars does not support need a copy of the token +for convert_float_locale_aware(). + +@param[in] first pointer to the first character of the token +@param[in] last pointer past the last character +@param[in] decimal_point_position index of the '.' in the token, or + std::string::npos if there is none +@param[in] mantissa_end index of the 'e'/'E', or the token length +@return the value, ±infinity if it overflows +*/ +template +FloatType convert_float(const char* first, const char* last, std::size_t decimal_point_position, std::size_t mantissa_end) +{ + FloatType value{}; + if (!convert_float_fast(first, last, decimal_point_position, mantissa_end, value)) + { + std::string token(first, last); + convert_float_locale_aware(token, decimal_point_position, value); + } + return value; +} + } // namespace detail NLOHMANN_JSON_NAMESPACE_END @@ -9953,10 +11760,338 @@ NLOHMANN_JSON_NAMESPACE_END +#include // array #include // size_t -#include // uint64_t +#include // uint64_t, uint8_t #include // memcpy +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-FileCopyrightText: 2018-2025 The simdjson authors +// SPDX-License-Identifier: MIT + + + +#include // array +#include // atomic +#include // size_t +#include // uint8_t, uint64_t + +// #include + +// #include + + +// Vector code for runs of string bytes, used by the lexer for contiguous input +// (see string_bulk_run()). NEON (AArch64) and SSE2 (x86-64) belong to the +// baseline instruction sets and are used by default. The vector UTF-8 check +// needs NEON or SSSE3. SSSE3 is not part of x86-64, and the code must not +// depend on the flags of a translation unit (two translation units with +// different flags would have different definitions of the same inline +// functions): the check is compiled for SSSE3 with a function attribute and +// used where the CPU has SSSE3 (all x86-64 CPUs since about 2011), else the +// portable check. Code compiled for SSSE3 anyway (-mssse3 or more) skips the +// CPU check. JSON_NO_SIMD selects the portable code everywhere. +// NLOHMANN_JSON_NO_TARGET_ATTRIBUTE (for compilers that ignore the target +// attribute and then reject the SSSE3 intrinsics) keeps the check portable +// without disabling the SSE2 code. +#if !defined(JSON_NO_SIMD) && defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) && defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + #include + #define JSON_SCAN_NEON 1 +#else + #define JSON_SCAN_NEON 0 +#endif +// (x86 only: other targets can define __SSE2__ as well, e.g., WebAssembly with -msse2, but have no ) +#if !defined(JSON_NO_SIMD) && !JSON_SCAN_NEON && (defined(__x86_64__) || defined(__i386__) || defined(_M_X64) || defined(_M_IX86)) && !defined(_M_ARM64EC) && (defined(__SSE2__) || defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2)) + #include + #define JSON_SCAN_SSE2 1 +#else + #define JSON_SCAN_SSE2 0 +#endif +#if JSON_SCAN_SSE2 && defined(__SSSE3__) + #include + #define JSON_SCAN_SSSE3 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) +#else + #define JSON_SCAN_SSSE3 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) +#endif +#if JSON_SCAN_SSE2 && !JSON_SCAN_SSSE3 && !defined(NLOHMANN_JSON_NO_TARGET_ATTRIBUTE) && ((defined(__clang__) && __clang_major__ >= 4) || (defined(__GNUC__) && !defined(__clang__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 9)) && !defined(__cpp_modules))) + // (GCC before 4.9 has no SSSE3 intrinsics without -mssse3; GCC with C++20 + // modules drops the target attribute from functions it reads from a module + // interface and then rejects the intrinsics: "needs isa option -mssse3") + // of GCC before 11 has no include guard, and json.hpp and + // json_view.hpp both include it + #ifndef NLOHMANN_JSON_CPUID_INCLUDED + #define NLOHMANN_JSON_CPUID_INCLUDED + #include + #endif + #include + #define JSON_SCAN_SSSE3_DISPATCH 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) + #define JSON_SCAN_SSSE3_TARGET __attribute__((target("ssse3"))) +#elif JSON_SCAN_SSE2 && !JSON_SCAN_SSSE3 && defined(_MSC_VER) + // (MSVC compiles intrinsics of any instruction set) + #include + #include + #define JSON_SCAN_SSSE3_DISPATCH 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) + #define JSON_SCAN_SSSE3_TARGET +#else + #define JSON_SCAN_SSSE3_DISPATCH 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) + #define JSON_SCAN_SSSE3_TARGET +#endif +#define JSON_SCAN_VECTOR (JSON_SCAN_NEON || JSON_SCAN_SSE2) +#define JSON_SCAN_VECTOR_UTF8 (JSON_SCAN_NEON || JSON_SCAN_SSSE3 || JSON_SCAN_SSSE3_DISPATCH) + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +#if JSON_SCAN_VECTOR +/*! +@brief the first byte of a string run that is a quote, a backslash, a control +character, or not ASCII, 16 bytes per step + +Stops at such a byte, or where fewer than 16 bytes are left (the caller tells +the two apart). A signed compare with 0x20 finds control characters and +non-ASCII bytes at once. +*/ +JSON_HEDLEY_ALWAYS_INLINE const unsigned char* vector_plain_run(const unsigned char* p, const unsigned char* e) noexcept +{ + while (e - p >= 16) + { +#if JSON_SCAN_NEON + const uint8x16_t in = vld1q_u8(p); + const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))), + vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20))); + // one nibble per byte (the usual NEON replacement of x86's movemask, see + // D. Kutenin, "Porting x86 vector bitmask optimizations to Arm NEON", 2022) + const std::uint64_t bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0); + if (bits != 0) + { + return p + (count_trailing_zeros(bits) >> 2u); + } +#else + const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_cmplt_epi8(in, _mm_set1_epi8(0x20))); + const auto bits = static_cast(static_cast(_mm_movemask_epi8(special))); + if (bits != 0) + { + return p + count_trailing_zeros(bits); + } +#endif + p += 16; + } + return p; +} +#endif + +#if JSON_SCAN_VECTOR +/// vector_plain_run() for runs that are already longer than a few words: +/// inline with SSE2, out of line with NEON, where a larger string loop slows +/// down the rest of the lexer +#if JSON_SCAN_NEON + JSON_HEDLEY_NEVER_INLINE inline +#else + JSON_HEDLEY_ALWAYS_INLINE +#endif +const unsigned char* vector_plain_run_long(const unsigned char* p, const unsigned char* e) noexcept +{ + return vector_plain_run(p, e); +} +#endif + +#if JSON_SCAN_SSSE3_DISPATCH +/// whether the CPU has SSSE3 (CPUID leaf 1, ECX bit 9) +inline bool cpu_ssse3() noexcept +{ +#if defined(_MSC_VER) && !defined(__clang__) + std::array regs {{}}; + __cpuid(regs.data(), 1); + return (static_cast(regs[2]) & (1u << 9u)) != 0; +#else + unsigned eax = 0; + unsigned ebx = 0; + unsigned ecx = 0; + unsigned edx = 0; + return __get_cpuid(1, &eax, &ebx, &ecx, &edx) != 0 && (ecx & (1u << 9u)) != 0; +#endif +} + +/// whether the CPU has SSSE3, asked once: the answer is kept in an atomic +/// that is initialized at compile time, so that neither a guard of a local +/// static nor a global constructor is needed (threads that ask at the same +/// time all store the same answer) +JSON_HEDLEY_ALWAYS_INLINE bool cpu_has_ssse3() noexcept +{ + static std::atomic known{0}; // 0: not asked yet, 1: no, 2: yes + int state = known.load(std::memory_order_relaxed); + if (JSON_HEDLEY_UNLIKELY(state == 0)) + { + state = cpu_ssse3() ? 2 : 1; + known.store(state, std::memory_order_relaxed); + } + return state == 2; +} +#endif + +#if JSON_SCAN_VECTOR_UTF8 +/// Tables of the UTF-8 check of J. Keiser and D. Lemire, "Validating UTF-8 In +/// Less Than One Instruction Per Byte" (2021), as in simdjson ("lookup4"): each +/// maps a nibble (high and low nibble of the previous byte, high nibble of the +/// current byte) to the errors it allows; a byte pair is ill-formed if all +/// three have an error bit in common. +template +struct utf8_lookup4 +{ + static constexpr std::uint8_t too_short = 1u << 0u, too_long = 1u << 1u, overlong_3 = 1u << 2u, too_large = 1u << 3u; + static constexpr std::uint8_t surrogate = 1u << 4u, overlong_2 = 1u << 5u, too_large_1000 = 1u << 6u, overlong_4 = 1u << 6u; + static constexpr std::uint8_t two_conts = 1u << 7u, carry = too_short | too_long | two_conts; + static const std::array byte_1_high; + static const std::array byte_1_low; + static const std::array byte_2_high; +}; + +template +const std::array utf8_lookup4::byte_1_high = +{ + { + too_long, too_long, too_long, too_long, too_long, too_long, too_long, too_long, + two_conts, two_conts, two_conts, two_conts, + too_short | overlong_2, too_short, too_short | overlong_3 | surrogate, too_short | too_large | too_large_1000 | overlong_4 + } +}; + +template +const std::array utf8_lookup4::byte_1_low = +{ + { + carry | overlong_3 | overlong_2 | overlong_4, carry | overlong_2, carry, carry, + carry | too_large, carry | too_large | too_large_1000, carry | too_large | too_large_1000, carry | too_large | too_large_1000, + carry | too_large | too_large_1000, carry | too_large | too_large_1000, carry | too_large | too_large_1000, carry | too_large | too_large_1000, + carry | too_large | too_large_1000, carry | too_large | too_large_1000 | surrogate, carry | too_large | too_large_1000, carry | too_large | too_large_1000 + } +}; + +template +const std::array utf8_lookup4::byte_2_high = +{ + { + too_short, too_short, too_short, too_short, too_short, too_short, too_short, too_short, + static_cast(too_long | overlong_2 | two_conts | overlong_3 | too_large_1000 | overlong_4), + static_cast(too_long | overlong_2 | two_conts | overlong_3 | too_large), + static_cast(too_long | overlong_2 | two_conts | surrogate | too_large), + static_cast(too_long | overlong_2 | two_conts | surrogate | too_large), + too_short, too_short, too_short, too_short + } +}; + +/*! +@brief the run of plain string bytes and well-formed UTF-8 at p (a character + boundary), 16 bytes per step + +The first quote, backslash, or control character is found with vector +compares, and the UTF-8 check of J. Keiser and D. Lemire covers the bytes up +to it. Returns true with @a block at that byte; returns false with @a block at +the start of the 16 bytes where the check found ill-formed UTF-8 or where +fewer than 16 bytes are left, for the caller to finish one sequence at a time. +Out of line, so that no constants of the check occupy registers in the +caller's loop. On x86-64, it is compiled for SSSE3 (see cpu_has_ssse3()). +*/ +JSON_SCAN_SSSE3_TARGET JSON_HEDLEY_NEVER_INLINE inline bool vector_utf8_run(const unsigned char* p, const unsigned char* e, const unsigned char*& block_out) noexcept +{ + using lookup = utf8_lookup4<>; + const unsigned char* block = p; +#if JSON_SCAN_NEON + const uint8x16_t t1h = vld1q_u8(lookup::byte_1_high.data()); + const uint8x16_t t1l = vld1q_u8(lookup::byte_1_low.data()); + const uint8x16_t t2h = vld1q_u8(lookup::byte_2_high.data()); + uint8x16_t prev = vdupq_n_u8(0); + while (e - block >= 16) + { + const uint8x16_t in = vld1q_u8(block); + const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))), vcltq_u8(in, vdupq_n_u8(0x20))); + const uint8x16_t prev1 = vextq_u8(prev, in, 15); + const uint8x16_t sc = vandq_u8(vandq_u8(vqtbl1q_u8(t1h, vshrq_n_u8(prev1, 4)), vqtbl1q_u8(t1l, vandq_u8(prev1, vdupq_n_u8(0x0F)))), vqtbl1q_u8(t2h, vshrq_n_u8(in, 4))); + const uint8x16_t must23 = vorrq_u8(vqsubq_u8(vextq_u8(prev, in, 14), vdupq_n_u8(0xE0 - 0x80)), vqsubq_u8(vextq_u8(prev, in, 13), vdupq_n_u8(0xF0 - 0x80))); + const uint8x16_t err = veorq_u8(vandq_u8(must23, vdupq_n_u8(0x80)), sc); + const std::uint64_t special_bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0); + const std::uint64_t err_bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(vtstq_u8(err, err)), 4)), 0); + if (special_bits != 0) + { + // errors up to the special byte count (an incomplete sequence + // before a quote shows at the quote); the bytes after it do not + const unsigned k = static_cast(count_trailing_zeros(special_bits)) >> 2u; + const std::uint64_t upto = k == 15 ? ~std::uint64_t{0} : + (std::uint64_t{1} << (4u * (k + 1u))) - 1u; + if ((err_bits & upto) == 0) + { + block_out = block + k; + return true; + } + break; + } + if (err_bits != 0) + { + break; + } + prev = in; + block += 16; + } +#else + // the same with SSSE3 (pshufb for the table lookups; nibbles from 16-bit + // shifts, as there are no byte shifts) + const __m128i t1h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_high.data()))); // NOLINT(bugprone-casting-through-void) + const __m128i t1l = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_low.data()))); // NOLINT(bugprone-casting-through-void) + const __m128i t2h = _mm_loadu_si128(static_cast(static_cast(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(static_cast(block))); // NOLINT(bugprone-casting-through-void) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_cmpeq_epi8(_mm_subs_epu8(in, _mm_set1_epi8(0x1F)), zero)); // in < 0x20 + const __m128i prev1 = _mm_alignr_epi8(in, prev, 15); + const __m128i sc = _mm_and_si128(_mm_and_si128(_mm_shuffle_epi8(t1h, _mm_and_si128(_mm_srli_epi16(prev1, 4), nibble)), + _mm_shuffle_epi8(t1l, _mm_and_si128(prev1, nibble))), + _mm_shuffle_epi8(t2h, _mm_and_si128(_mm_srli_epi16(in, 4), nibble))); + const __m128i must23 = _mm_or_si128(_mm_subs_epu8(_mm_alignr_epi8(in, prev, 14), _mm_set1_epi8(0xE0 - 0x80)), + _mm_subs_epu8(_mm_alignr_epi8(in, prev, 13), _mm_set1_epi8(0xF0 - 0x80))); + const __m128i err = _mm_xor_si128(_mm_and_si128(must23, _mm_set1_epi8(static_cast(-128))), sc); + const auto special_bits = static_cast(_mm_movemask_epi8(special)); + const auto err_bits = ~static_cast(_mm_movemask_epi8(_mm_cmpeq_epi8(err, zero))) & 0xFFFFu; + if (special_bits != 0) + { + const auto k = static_cast(count_trailing_zeros(static_cast(special_bits))); + if ((err_bits & ((2u << k) - 1u)) == 0) + { + block_out = block + k; + return true; + } + break; + } + if (err_bits != 0) + { + break; + } + prev = in; + block += 16; + } +#endif + block_out = block; + return false; +} +#endif + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + // #include @@ -9978,7 +12113,7 @@ NLOHMANN_JSON_NAMESPACE_END // This file contains the byte-level string-scanning helpers used by the lexer's // contiguous fast path. They operate purely on raw bytes (no dependency on the // lexer's template parameters) so they are free functions, keeping the lexer -// itself focused on the state machine; see lexer::scan_string_bulk(). +// itself focused on the state machine; see lexer::get_after_string_run(). NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail @@ -10010,23 +12145,17 @@ inline std::uint64_t swar_string_special(std::uint64_t v) noexcept // return the index of the first is_string_special() byte in [data, data+n), or // n if every byte is ordinary; scans 8 bytes at a time -inline std::size_t find_string_special(const unsigned char* data, std::size_t n) noexcept +JSON_HEDLEY_ALWAYS_INLINE std::size_t find_string_special(const unsigned char* data, std::size_t n) noexcept { std::size_t i = 0; for (; i + 8 <= n; i += 8) { - std::uint64_t word = 0; - std::memcpy(&word, data + i, sizeof(word)); - if (swar_string_special(word) != 0) + const std::uint64_t special = swar_string_special(read_eight_bytes(data + i)); + if (special != 0) { - // a special byte is in this word; locate it (endian-agnostic) - for (std::size_t j = 0; j < 8; ++j) - { - if (is_string_special(data[i + j])) - { - return i + j; - } - } + // the lowest flagged byte is the first special one: the borrows of + // the subtractions can only flag bytes above a true hit + return i + (static_cast(count_trailing_zeros(special)) / 8); } } for (; i < n; ++i) @@ -10045,7 +12174,7 @@ inline std::size_t find_string_special(const unsigned char* data, std::size_t n) // printable ASCII except '"' and '\\' - is emitted unchanged. Note this differs // from is_string_special() only in that 0x7F is also a stop (it is escaped as // \u007f under ensure_ascii). -inline bool is_ascii_copyable(unsigned char c) noexcept +constexpr bool is_ascii_copyable(unsigned char c) noexcept { return c >= 0x20u && c < 0x7Fu && c != '"' && c != '\\'; } @@ -10060,8 +12189,7 @@ inline std::size_t find_ascii_copyable_run(const unsigned char* data, std::size_ std::size_t i = 0; for (; i + 8 <= n; i += 8) { - std::uint64_t v = 0; - std::memcpy(&v, data + i, sizeof(v)); + const std::uint64_t v = read_eight_bytes(data + i); const std::uint64_t q = v ^ 0x2222222222222222ull; // '"' (0x22) const std::uint64_t b = v ^ 0x5C5C5C5C5C5C5C5Cull; // '\\' (0x5C) const std::uint64_t d = v ^ 0x7F7F7F7F7F7F7F7Full; // DEL (0x7F) @@ -10072,7 +12200,9 @@ inline std::size_t find_ascii_copyable_run(const unsigned char* data, std::size_ | (v & high); // >= 0x80 if (stop != 0) { - break; + // the lowest flagged byte is the first one to stop at (see + // find_string_special()) + return i + (static_cast(count_trailing_zeros(stop)) / 8); } } for (; i < n; ++i) @@ -10184,29 +12314,143 @@ inline std::size_t valid_utf8_prefix(const unsigned char* data, std::size_t n) n return pos; } -// Scalar (C++11) computation of the bulk run length: the number of leading -// bytes in [data, data+n) that are ordinary ASCII or complete well-formed UTF-8 +#if JSON_SCAN_VECTOR_UTF8 +/// the end of scan_string_vector from block, where the vector loop stopped +/// (ill-formed UTF-8, or fewer than 16 bytes left): one byte or sequence at a +/// time, from the start of a sequence that crosses into the block +inline const unsigned char* scan_string_finish(const unsigned char* p, const unsigned char* block, const unsigned char* e) noexcept +{ + for (int i = 1; i <= 3 && block - i >= p; ++i) + { + const unsigned char c = block[-i]; + if (c < 0x80) + { + break; + } + if (c >= 0xC0) + { + const int len = 2 + static_cast(c >= 0xE0) + static_cast(c >= 0xF0); + if (len > i) + { + block -= i; + } + break; + } + } + for (p = block; p != e;) + { + if (*p < 0x80) + { + if (is_string_special(*p)) + { + return p; + } + ++p; + continue; + } + const std::size_t n = validate_one_utf8(p, static_cast(e - p)); + if (n == 0) + { + return p; + } + p += n; + } + return p; +} + +/// the rest of a string from p (a character boundary), with the vector UTF-8 +/// check and the scalar check for the end of the block where it stopped +inline const unsigned char* scan_string_vector(const unsigned char* p, const unsigned char* e) noexcept +{ + const unsigned char* block = p; + if (vector_utf8_run(p, e, block)) + { + return block; + } + return scan_string_finish(p, block, e); +} +#endif + +#if JSON_SCAN_VECTOR +/// the end of the run of plain string characters at @a p: the first quote, +/// backslash, control character, or non-ASCII byte, or @a e. The first words +/// are checked with SWAR (one; four with NEON, where the vector mask costs +/// more), as most keys and short values end there; longer runs continue 16 +/// bytes at a time. +JSON_HEDLEY_ALWAYS_INLINE const unsigned char* plain_run_end(const unsigned char* p, const unsigned char* e) noexcept +{ + constexpr int prefix_words = JSON_SCAN_NEON ? 4 : 1; + for (int words = prefix_words; words > 0 && e - p >= 8; --words) + { + const std::uint64_t special = swar_string_special(read_eight_bytes(p)); + if (special != 0) + { + return p + (static_cast(count_trailing_zeros(special)) / 8); + } + p += 8; + } + // the vector loop only for 16 bytes or more: the serializer passes a + // string without its closing quote, so most of its runs end at e, and a + // call of the out-of-line NEON loop for a shorter rest made dump() up to + // 10% slower + if (e - p >= 16) + { + p = vector_plain_run_long(p, e); + if (e - p >= 16) + { + return p; // the vector loop stopped at a special byte + } + } + return p + find_string_special(p, static_cast(e - p)); +} +#endif + +// Computation of the bulk run length: the number of leading bytes in +// [data, data+n) that are ordinary ASCII or complete well-formed UTF-8 // sequences, stopping before the first byte that needs individual handling (the // closing quote, an escape, a control character, or an ill-formed/truncated -// sequence). ASCII is skipped 8 bytes at a time. -inline std::size_t scalar_string_bulk_run(const unsigned char* data, std::size_t n) noexcept +// sequence). ASCII is skipped 16 bytes at a time with SSE2 and 8 bytes at a +// time otherwise; runs of non-ASCII text use the vector UTF-8 check where +// available. +JSON_HEDLEY_ALWAYS_INLINE std::size_t scalar_string_bulk_run(const unsigned char* data, std::size_t n) noexcept { - std::size_t pos = 0; - while (pos < n) + const unsigned char* const e = data + n; + const unsigned char* p = data; + while (p != e) { - pos += find_string_special(data + pos, n - pos); - if (pos >= n || data[pos] < 0x80u) +#if JSON_SCAN_VECTOR + p = plain_run_end(p, e); +#else + p += find_string_special(p, static_cast(e - p)); +#endif + if (p == e || *p < 0x80u) { break; // end of buffer, or a quote/escape/control byte } - const std::size_t seq = validate_one_utf8(data + pos, n - pos); - if (seq == 0) +#if JSON_SCAN_VECTOR_UTF8 +#if JSON_SCAN_SSSE3_DISPATCH + if (JSON_HEDLEY_LIKELY(cpu_has_ssse3())) +#endif { - break; // ill-formed or truncated: let the byte path diagnose it + return static_cast(scan_string_vector(p, e) - data); } - pos += seq; +#endif +#if !JSON_SCAN_VECTOR_UTF8 || JSON_SCAN_SSSE3_DISPATCH + // a run of multi-byte sequences (e.g. CJK text) is validated sequence + // by sequence without searching for the next special byte in between + do + { + const std::size_t seq = validate_one_utf8(p, static_cast(e - p)); + if (seq == 0) + { + return static_cast(p - data); // ill-formed or truncated: let the byte path diagnose it + } + p += seq; + } + while (p != e && *p >= 0x80u); +#endif } - return pos; + return static_cast(p - data); } #if defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17) @@ -10219,8 +12463,7 @@ inline std::size_t find_string_delimiter(const unsigned char* data, std::size_t std::size_t i = 0; for (; i + 8 <= n; i += 8) { - std::uint64_t v = 0; - std::memcpy(&v, data + i, sizeof(v)); + const std::uint64_t v = read_eight_bytes(data + i); const std::uint64_t q = v ^ 0x2222222222222222ull; const std::uint64_t b = v ^ 0x5C5C5C5C5C5C5C5Cull; const std::uint64_t hit = ((q - ones) & ~q & high) @@ -10228,14 +12471,8 @@ inline std::size_t find_string_delimiter(const unsigned char* data, std::size_t | ((v - 0x2020202020202020ull) & ~v & high); if (hit != 0) { - for (std::size_t j = 0; j < 8; ++j) - { - const unsigned char c = data[i + j]; - if (c == '\"' || c == '\\' || c < 0x20u) - { - return i + j; - } - } + // the lowest flagged byte is the first delimiter (see find_string_special()) + return i + (static_cast(count_trailing_zeros(hit)) / 8); } } for (; i < n; ++i) @@ -10254,7 +12491,7 @@ inline std::size_t find_string_delimiter(const unsigned char* data, std::size_t // next delimiter is validated in one shot by simdutf; on the rare failure the // scalar helper recomputes the exact valid prefix so the byte path still // produces the precise diagnostic. Without it, the pure scalar path is used. -inline std::size_t string_bulk_run(const unsigned char* data, std::size_t n) noexcept +JSON_HEDLEY_ALWAYS_INLINE std::size_t string_bulk_run(const unsigned char* data, std::size_t n) noexcept { #if defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17) const std::size_t run = find_string_delimiter(data, n); @@ -10266,6 +12503,87 @@ inline std::size_t string_bulk_run(const unsigned char* data, std::size_t n) noe return scalar_string_bulk_run(data, n); } +#if JSON_SCAN_NEON && !(defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17)) +/// scalar_string_bulk_run() from @a start on, out of line (see +/// lexer_string_bulk_run()) +JSON_HEDLEY_NEVER_INLINE inline std::size_t string_bulk_run_from(const unsigned char* data, std::size_t start, std::size_t n) noexcept +{ + return start + scalar_string_bulk_run(data + start, n - start); +} +#endif + +// string_bulk_run() for the lexer. With NEON, only the first four words are +// checked inline (most keys and short values end there), and longer runs and +// non-ASCII text continue out of line. The lexer's scan function runs once per +// token, and the vector code inlined into it needs more registers, which every +// token pays for: 5% of json::accept() on jeopardy.json with Apple Clang on +// Apple M1, even though no string of that file reaches the vector code. The +// serializer keeps string_bulk_run() inline, which is faster there. +JSON_HEDLEY_ALWAYS_INLINE std::size_t lexer_string_bulk_run(const unsigned char* data, std::size_t n) noexcept +{ +#if JSON_SCAN_NEON && !(defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17)) + std::size_t i = 0; + for (int words = 4; words > 0 && n - i >= 8; --words) + { + const std::uint64_t special = swar_string_special(read_eight_bytes(data + i)); + if (special != 0) + { + const std::size_t pos = i + (static_cast(count_trailing_zeros(special)) / 8); + return data[pos] < 0x80u ? pos : string_bulk_run_from(data, pos, n); + } + i += 8; + } + return string_bulk_run_from(data, i, n); +#else + return string_bulk_run(data, n); +#endif +} + +// Decode the 4 hex digits at [data, data+4) - the digits following a `\u` +// escape - into a codepoint 0x0000..0xFFFF via one table lookup per byte +// (after yyjson's read_hex_u16), or return -1 if any of the 4 bytes is not a +// hex digit ('0'..'9', 'A'..'F', 'a'..'f'). The caller must already have +// checked that 4 bytes are available; used by lexer::get_codepoint()'s +// contiguous fast path. On -1 it falls back to the byte-at-a-time loop, which +// stops at the first invalid digit, so the reported error and position are +// unaffected by this fast path. +inline int hex_codepoint(const unsigned char* data) noexcept +{ + static const std::array hex_digit_table = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + { + { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 00..0F + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 10..1F + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 20..2F + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 30..3F ('0'..'9') + 0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 40..4F ('A'..'F') + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 50..5F + 0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 60..6F ('a'..'f') + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 70..7F + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 80..8F + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 90..9F + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // A0..AF + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // B0..BF + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // C0..CF + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // D0..DF + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // E0..EF + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // F0..FF + } + }; + + const std::uint8_t d0 = hex_digit_table[data[0]]; + const std::uint8_t d1 = hex_digit_table[data[1]]; + const std::uint8_t d2 = hex_digit_table[data[2]]; + const std::uint8_t d3 = hex_digit_table[data[3]]; + // every valid digit is <= 0xF; the combined OR only exceeds it if at + // least one of the four bytes was not a hex digit (looked up as 0xFF) + if ((d0 | d1 | d2 | d3) > 0x0F) + { + return -1; + } + return (d0 << 12) | (d1 << 8) | (d2 << 4) | d3; +} + } // namespace detail NLOHMANN_JSON_NAMESPACE_END @@ -10416,6 +12734,43 @@ constexpr bool input_adapter_supports_bulk_scan(std::false_type /*detected*/) return false; } +// Detect whether an input adapter keeps the current token itself and can +// hand it out on demand (see input_stream_adapter::supports_token_tracking), +// so the lexer need not copy every scanned character into token_string. +template +using detect_supports_token_tracking = decltype(InputAdapterType::supports_token_tracking); + +template +constexpr bool input_adapter_supports_token_tracking(std::true_type /*detected*/) +{ + return InputAdapterType::supports_token_tracking; +} + +template +constexpr bool input_adapter_supports_token_tracking(std::false_type /*detected*/) +{ + return false; +} + +// Detect whether the contiguous block of a bulk-scan adapter is only a window +// into the input (see input_stream_adapter::bulk_window): its end is not the +// end of the input, so a token that touches it must be finished character by +// character. +template +using detect_bulk_window = decltype(InputAdapterType::bulk_window); + +template +constexpr bool input_adapter_bulk_window(std::true_type /*detected*/) +{ + return InputAdapterType::bulk_window; +} + +template +constexpr bool input_adapter_bulk_window(std::false_type /*detected*/) +{ + return false; +} + /*! @brief lexical analysis @@ -10434,9 +12789,29 @@ class lexer : public lexer_base /// whether the last read token can be reconstructed from the input adapter /// on demand (in error paths) instead of being copied on every scanned /// character; see input_adapter_supports_seek - static constexpr bool lazy_token_string = + static constexpr bool seekable = input_adapter_supports_seek(is_detected {}); + /// whether the adapter keeps the token itself (mark_token_start() / + /// copy_token()); see input_adapter_supports_token_tracking + static constexpr bool adapter_token = + !seekable && input_adapter_supports_token_tracking(is_detected {}); + + /// whether the last read token is obtained from the input adapter on + /// demand (in error paths) instead of being copied on every scanned + /// character: seekable adapters reconstruct it from the consumed input, + /// token-tracking adapters keep it themselves + static constexpr bool lazy_token_string = seekable || adapter_token; + + /// whether the read position is derived from the input adapter on demand + /// instead of being counted on every scanned character. Seekable adapters + /// know how much input they consumed and can revisit it, so + /// chars_read_total is the adapter's read count (minus a pending unget), + /// and line and column are recomputed from the consumed input only when + /// get_position() is called (errors). Counting per character kept two + /// increments and a newline check in the hot path of every get(). + static constexpr bool lazy_position = seekable; + /// whether a simulated unget can be passed on to the input adapter, which /// then leaves the character in the input; see /// input_adapter_supports_lookahead @@ -10444,12 +12819,17 @@ class lexer : public lexer_base input_adapter_supports_lookahead(is_detected {}); /// whether string scanning may bulk-consume runs of ordinary characters - /// directly from a contiguous input buffer (SWAR fast path). This requires - /// the token to be reconstructible lazily (lazy_token_string), so bypassing - /// the per-character capture in get() cannot lose error diagnostics. + /// directly from a contiguous input buffer (SWAR fast path). Without + /// lazy_token_string / lazy_position, the bulk paths copy the consumed + /// characters into token_string and advance the position counters + /// themselves (see bulk_consume). static constexpr bool bulk_scan = - lazy_token_string - && input_adapter_supports_bulk_scan(is_detected {}); + input_adapter_supports_bulk_scan(is_detected {}); + + /// whether the contiguous block is only a window into the input, so that + /// its end does not end a token; see input_adapter_bulk_window + static constexpr bool bulk_window = + input_adapter_bulk_window(is_detected {}); public: using token_type = typename lexer_base::token_type; @@ -10458,7 +12838,11 @@ class lexer : public lexer_base : ia(std::move(adapter)) , ignore_comments(ignore_comments_) , discard_number_values(discard_number_values_) - {} + { + // the token string of an error before the first token is everything + // read so far + begin_token(std::integral_constant {}); + } // deleted because of pointer members lexer(const lexer&) = delete; @@ -10472,6 +12856,41 @@ class lexer : public lexer_base // scan functions ///////////////////// + /// contiguous input: try to decode the 4 hex digits following `\\u` + /// directly from the input buffer via hex_codepoint(), instead of 4 calls + /// to get(). On success, advances the adapter exactly as those 4 get() + /// calls would (including the position and token bookkeeping of a + /// streaming adapter, see bulk_consume) and leaves @a current holding the last of + /// the 4 digits, just as the last such get() would; the codepoint is + /// written to @a out. Makes no state change and returns false for a + /// pending unget, fewer than 4 remaining bytes, or any of the 4 bytes not + /// being a hex digit, so the caller falls back unchanged to the + /// per-character loop, which then reports the same diagnostic (stopping + /// at the first invalid digit) as before this optimization. + bool get_codepoint_bulk(std::true_type /*bulk*/, int& out) + { + if (next_unget || ia.bulk_remaining() < 4) + { + return false; + } + const char_type* const raw = ia.bulk_data(); + const int codepoint = hex_codepoint(reinterpret_cast(raw)); + if (codepoint < 0) + { + return false; + } + bulk_consume(reinterpret_cast(raw), 4); + current = char_traits::to_int_type(raw[3]); + out = codepoint; + return true; + } + + /// streaming input: no bulk fast path + bool get_codepoint_bulk(std::false_type /*bulk*/, int& /*out*/) const noexcept + { + return false; + } + /*! @brief get codepoint from 4 hex characters following `\\u` @@ -10491,6 +12910,14 @@ class lexer : public lexer_base { // this function only makes sense after reading `\u` JSON_ASSERT(current == 'u'); + + // contiguous input: decode all 4 hex digits directly from the buffer + int fast_codepoint = 0; + if (get_codepoint_bulk(std::integral_constant {}, fast_codepoint)) + { + return fast_codepoint; + } + int codepoint = 0; const auto factors = { 12u, 8u, 4u, 0u }; @@ -10558,38 +12985,79 @@ class lexer : public lexer_base } /// contiguous input: bulk-append the run of ordinary characters and complete - /// well-formed UTF-8 sequences starting at the current read position, leaving - /// the first byte that needs individual handling (the closing quote, an - /// escape, a control character, or an ill-formed UTF-8 byte) for get() - void scan_string_bulk(std::true_type /*bulk*/) + /// well-formed UTF-8 sequences starting at the current read position, then + /// 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(). + /// 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 + /// function, which load the read position again right after it was stored + /// (3% of json::accept() on jeopardy.json on Apple M1). + char_int_type get_after_string_run(std::true_type /*bulk*/) { // a pending unget must be consumed through the normal path first if (next_unget) { - return; + return get(); } const std::size_t remaining = ia.bulk_remaining(); if (remaining == 0) { - return; + return get(); } - const auto* const data = reinterpret_cast(ia.bulk_data()); + const auto* const raw = ia.bulk_data(); + const auto* const data = reinterpret_cast(raw); - const std::size_t pos = string_bulk_run(data, remaining); - if (pos == 0) + const std::size_t pos = lexer_string_bulk_run(data, remaining); + if (pos != 0) { - return; + token_buffer.append(reinterpret_cast(data), pos); + bulk_consume(reinterpret_cast(data), pos); } - token_buffer.append(reinterpret_cast(data), pos); - ia.bulk_skip(pos); - // the run contains no newline (all bytes < 0x20 are treated as special), - // so only the flat character counters advance - position.chars_read_total += pos; - position.chars_read_current_line += pos; + if (pos == remaining) + { + return get(); // end of the contiguous input + } + // what get() does, with the byte at hand + advance_position(); + ia.bulk_skip(1); + current = char_traits::to_int_type(raw[pos]); + return track_after_read(); + } + + /// consume @a n characters at @a data (the adapter's bulk_data()) and do + /// the bookkeeping get() would have done for them; the characters are + /// never newlines + void bulk_consume(const char* data, std::size_t n) + { + bulk_track_position(n, std::integral_constant {}); + bulk_track_token(data, n, std::integral_constant {}); + ia.bulk_skip(n); + } + + /// seekable adapter: the position is derived from the adapter + void bulk_track_position(std::size_t /*n*/, std::true_type /*lazy*/) const noexcept {} + + /// streaming adapter: count the characters + void bulk_track_position(std::size_t n, std::false_type /*lazy*/) noexcept + { + position.chars_read_total += n; + position.chars_read_current_line += n; + } + + /// the token is obtained from the adapter + void bulk_track_token(const char* /*data*/, std::size_t /*n*/, std::true_type /*lazy*/) const noexcept {} + + /// streaming adapter: capture the characters + void bulk_track_token(const char* data, std::size_t n, std::false_type /*lazy*/) + { + token_string.insert(token_string.end(), data, data + n); } /// streaming input: no bulk fast path - void scan_string_bulk(std::false_type /*bulk*/) const noexcept {} + char_int_type get_after_string_run(std::false_type /*bulk*/) + { + return get(); + } /*! @brief scan a string literal @@ -10617,11 +13085,8 @@ class lexer : public lexer_base while (true) { // bulk-consume ordinary characters from contiguous input, then - // handle the next special byte through the switch below - scan_string_bulk(std::integral_constant {}); - - // get the next character - switch (get()) + // get the next character, the one that ends the run + switch (get_after_string_run(std::integral_constant())) { // end of file while parsing the string case char_traits::eof(): @@ -10758,194 +13223,74 @@ class lexer : public lexer_base // invalid control characters case 0x00: - { - error_message = "invalid string: control character U+0000 (NUL) must be escaped to \\u0000"; - return token_type::parse_error; - } - case 0x01: - { - error_message = "invalid string: control character U+0001 (SOH) must be escaped to \\u0001"; - return token_type::parse_error; - } - case 0x02: - { - error_message = "invalid string: control character U+0002 (STX) must be escaped to \\u0002"; - return token_type::parse_error; - } - case 0x03: - { - error_message = "invalid string: control character U+0003 (ETX) must be escaped to \\u0003"; - return token_type::parse_error; - } - case 0x04: - { - error_message = "invalid string: control character U+0004 (EOT) must be escaped to \\u0004"; - return token_type::parse_error; - } - case 0x05: - { - error_message = "invalid string: control character U+0005 (ENQ) must be escaped to \\u0005"; - return token_type::parse_error; - } - case 0x06: - { - error_message = "invalid string: control character U+0006 (ACK) must be escaped to \\u0006"; - return token_type::parse_error; - } - case 0x07: - { - error_message = "invalid string: control character U+0007 (BEL) must be escaped to \\u0007"; - return token_type::parse_error; - } - case 0x08: - { - error_message = "invalid string: control character U+0008 (BS) must be escaped to \\u0008 or \\b"; - return token_type::parse_error; - } - case 0x09: - { - error_message = "invalid string: control character U+0009 (HT) must be escaped to \\u0009 or \\t"; - return token_type::parse_error; - } - case 0x0A: - { - error_message = "invalid string: control character U+000A (LF) must be escaped to \\u000A or \\n"; - return token_type::parse_error; - } - case 0x0B: - { - error_message = "invalid string: control character U+000B (VT) must be escaped to \\u000B"; - return token_type::parse_error; - } - case 0x0C: - { - error_message = "invalid string: control character U+000C (FF) must be escaped to \\u000C or \\f"; - return token_type::parse_error; - } - case 0x0D: - { - error_message = "invalid string: control character U+000D (CR) must be escaped to \\u000D or \\r"; - return token_type::parse_error; - } - case 0x0E: - { - error_message = "invalid string: control character U+000E (SO) must be escaped to \\u000E"; - return token_type::parse_error; - } - case 0x0F: - { - error_message = "invalid string: control character U+000F (SI) must be escaped to \\u000F"; - return token_type::parse_error; - } - case 0x10: - { - error_message = "invalid string: control character U+0010 (DLE) must be escaped to \\u0010"; - return token_type::parse_error; - } - case 0x11: - { - error_message = "invalid string: control character U+0011 (DC1) must be escaped to \\u0011"; - return token_type::parse_error; - } - case 0x12: - { - error_message = "invalid string: control character U+0012 (DC2) must be escaped to \\u0012"; - return token_type::parse_error; - } - case 0x13: - { - error_message = "invalid string: control character U+0013 (DC3) must be escaped to \\u0013"; - return token_type::parse_error; - } - case 0x14: - { - error_message = "invalid string: control character U+0014 (DC4) must be escaped to \\u0014"; - return token_type::parse_error; - } - case 0x15: - { - error_message = "invalid string: control character U+0015 (NAK) must be escaped to \\u0015"; - return token_type::parse_error; - } - case 0x16: - { - error_message = "invalid string: control character U+0016 (SYN) must be escaped to \\u0016"; - return token_type::parse_error; - } - case 0x17: - { - error_message = "invalid string: control character U+0017 (ETB) must be escaped to \\u0017"; - return token_type::parse_error; - } - case 0x18: - { - error_message = "invalid string: control character U+0018 (CAN) must be escaped to \\u0018"; - return token_type::parse_error; - } - case 0x19: - { - error_message = "invalid string: control character U+0019 (EM) must be escaped to \\u0019"; - return token_type::parse_error; - } - case 0x1A: - { - error_message = "invalid string: control character U+001A (SUB) must be escaped to \\u001A"; - return token_type::parse_error; - } - case 0x1B: - { - error_message = "invalid string: control character U+001B (ESC) must be escaped to \\u001B"; - return token_type::parse_error; - } - case 0x1C: - { - error_message = "invalid string: control character U+001C (FS) must be escaped to \\u001C"; - return token_type::parse_error; - } - case 0x1D: - { - error_message = "invalid string: control character U+001D (GS) must be escaped to \\u001D"; - return token_type::parse_error; - } - case 0x1E: - { - error_message = "invalid string: control character U+001E (RS) must be escaped to \\u001E"; - return token_type::parse_error; - } - case 0x1F: { - error_message = "invalid string: control character U+001F (US) must be escaped to \\u001F"; + static const std::array control_character_messages = {{ + "invalid string: control character U+0000 (NUL) must be escaped to \\u0000", + "invalid string: control character U+0001 (SOH) must be escaped to \\u0001", + "invalid string: control character U+0002 (STX) must be escaped to \\u0002", + "invalid string: control character U+0003 (ETX) must be escaped to \\u0003", + "invalid string: control character U+0004 (EOT) must be escaped to \\u0004", + "invalid string: control character U+0005 (ENQ) must be escaped to \\u0005", + "invalid string: control character U+0006 (ACK) must be escaped to \\u0006", + "invalid string: control character U+0007 (BEL) must be escaped to \\u0007", + "invalid string: control character U+0008 (BS) must be escaped to \\u0008 or \\b", + "invalid string: control character U+0009 (HT) must be escaped to \\u0009 or \\t", + "invalid string: control character U+000A (LF) must be escaped to \\u000A or \\n", + "invalid string: control character U+000B (VT) must be escaped to \\u000B", + "invalid string: control character U+000C (FF) must be escaped to \\u000C or \\f", + "invalid string: control character U+000D (CR) must be escaped to \\u000D or \\r", + "invalid string: control character U+000E (SO) must be escaped to \\u000E", + "invalid string: control character U+000F (SI) must be escaped to \\u000F", + "invalid string: control character U+0010 (DLE) must be escaped to \\u0010", + "invalid string: control character U+0011 (DC1) must be escaped to \\u0011", + "invalid string: control character U+0012 (DC2) must be escaped to \\u0012", + "invalid string: control character U+0013 (DC3) must be escaped to \\u0013", + "invalid string: control character U+0014 (DC4) must be escaped to \\u0014", + "invalid string: control character U+0015 (NAK) must be escaped to \\u0015", + "invalid string: control character U+0016 (SYN) must be escaped to \\u0016", + "invalid string: control character U+0017 (ETB) must be escaped to \\u0017", + "invalid string: control character U+0018 (CAN) must be escaped to \\u0018", + "invalid string: control character U+0019 (EM) must be escaped to \\u0019", + "invalid string: control character U+001A (SUB) must be escaped to \\u001A", + "invalid string: control character U+001B (ESC) must be escaped to \\u001B", + "invalid string: control character U+001C (FS) must be escaped to \\u001C", + "invalid string: control character U+001D (GS) must be escaped to \\u001D", + "invalid string: control character U+001E (RS) must be escaped to \\u001E", + "invalid string: control character U+001F (US) must be escaped to \\u001F" + } + }; + error_message = control_character_messages[static_cast(current)]; return token_type::parse_error; } @@ -11295,9 +13640,11 @@ class lexer : public lexer_base token_type::parse_error otherwise @note The scanner is independent of the current locale: token_buffer - always holds `.`. Only the std::strtod fallback of convert_number() - depends on the locale, and it looks up the decimal point right - before converting (see detail::convert_float_locale_aware()). + always holds `.`. The conversion of float and double does not use + the locale either. Only the std::strtold fallback of + convert_number() for long double formats other than binary64 + depends on it, and it looks up the decimal point right before + converting (see detail::convert_float_locale_aware()). */ token_type scan_number() // lgtm [cpp/use-of-goto] `goto` is used in this function to implement the number-parsing state machine described above. By design, any finite input will eventually reach the "done" state or return token_type::parse_error. In each intermediate state, 1 byte of the input is appended to the token_buffer vector, and only the already initialized variables token_buffer, number_type, and error_message are manipulated. { @@ -11310,7 +13657,7 @@ class lexer : public lexer_base // offset just past the last mantissa byte in token_buffer (i.e. the // index of 'e'/'E', or the whole token when there is no exponent). - // convert_number() uses it to count significant digits; npos means + // convert_number() uses it to split the token; npos means // "not seen an exponent yet" and is resolved at scan_number_done std::size_t mantissa_end = std::string::npos; @@ -11640,8 +13987,8 @@ scan_number_done: @param[in] mantissa_end offset just past the last mantissa byte in token_buffer (the index of 'e'/'E', or token_buffer.size() when there is no exponent); - used to skip Clinger's fast path when it cannot - possibly succeed - see detail::mantissa_fits_clinger() + with decimal_point_position, it locates the parts + of a float token without scanning it again */ token_type convert_number(token_type number_type, std::size_t mantissa_end) { @@ -11695,10 +14042,11 @@ scan_number_done: } // this code is reached if we parse a floating-point number or if an - // integer conversion above overflowed. Prefer std::from_chars - // (Eisel-Lemire, locale-independent, correctly rounded) when available; - // otherwise the exact Clinger fast path (double only); otherwise the - // locale-aware strtof/strtod/strtold. + // integer conversion above overflowed. float and double (and long + // double where it is binary64) are converted by the library itself, + // correctly rounded and independent of the locale; other long double + // formats use std::from_chars when available, otherwise the + // locale-aware strtold. if (convert_float_fast(num_begin, num_end, decimal_point_position, mantissa_end, value_float)) { return token_type::value_float; @@ -11738,9 +14086,12 @@ scan_number_done: const std::size_t avail = rem + 1; // validate + classify the number extent (mirrors scan_number()'s grammar) + // and accumulate the mantissa digits on the way, so that tokens with up + // to 19 digits need no second pass for their value std::size_t i = 0; std::size_t dot_index = std::string::npos; token_type number_type = token_type::value_unsigned; + std::uint64_t w = 0; if (data[0] == '-') { number_type = token_type::value_integer; @@ -11750,22 +14101,21 @@ scan_number_done: return token_type::uninitialized; } } + const std::size_t integer_begin = i; if (data[i] == '0') { ++i; } else if (data[i] >= '1' && data[i] <= '9') { - ++i; - while (i < avail && data[i] >= '0' && data[i] <= '9') - { - ++i; - } + i = accumulate_digits(data, i, avail, w); } else { return token_type::uninitialized; } + std::size_t digit_count = i - integer_begin; + std::size_t fraction_digits = 0; if (i < avail && data[i] == '.') { number_type = token_type::value_float; @@ -11775,31 +14125,45 @@ scan_number_done: { return token_type::uninitialized; } - while (i < avail && data[i] >= '0' && data[i] <= '9') - { - ++i; - } + const std::size_t fraction_begin = i; + i = accumulate_digits(data, i, avail, w); + fraction_digits = i - fraction_begin; + digit_count += fraction_digits; } // the mantissa ends here, whether or not an exponent part follows const std::size_t mantissa_end = i; + std::int64_t exponent = 0; if (i < avail && (data[i] == 'e' || data[i] == 'E')) { number_type = token_type::value_float; ++i; + bool negative_exponent = false; if (i < avail && (data[i] == '+' || data[i] == '-')) { + negative_exponent = data[i] == '-'; ++i; } if (i >= avail || !(data[i] >= '0' && data[i] <= '9')) { return token_type::uninitialized; } + // saturated far beyond every range, see parse_float_exponent() + constexpr std::int64_t saturation = 100000000000000000; // 10^17 while (i < avail && data[i] >= '0' && data[i] <= '9') { + if (exponent < saturation) + { + exponent = (exponent * 10) + (data[i] - '0'); + } ++i; } + exponent = negative_exponent ? -exponent : exponent; } const std::size_t len = i; + if (number_may_continue(len == avail, std::integral_constant {})) + { + return token_type::uninitialized; + } // reset() records where this token starts (for diagnostics), so it has // to run before the input position advances below @@ -11814,12 +14178,14 @@ scan_number_done: #if !JSON_DIAGNOSTIC_POSITIONS if (number_type != token_type::value_float) { - const token_type integer_result = convert_integer(number_type, data, data + len); + // up to 18 digits always fit into std::uint64_t and std::int64_t + const token_type integer_result = (digit_count <= 18) + ? store_integer(number_type, w) + : convert_integer(number_type, data, data + len); if (JSON_HEDLEY_LIKELY(integer_result != token_type::uninitialized)) { - ia.bulk_skip(len - 1); - position.chars_read_total += (len - 1); - position.chars_read_current_line += (len - 1); + bulk_consume(data + 1, len - 1); + read_number_terminator(std::integral_constant {}); return integer_result; } // The value does not fit an integer, so this token converts as a @@ -11829,19 +14195,108 @@ scan_number_done: } #endif + decimal_point_position = dot_index; + + bulk_consume(data + 1, len - 1); + read_number_terminator(std::integral_constant {}); + + // a float whose digits all fit into w converts from the accumulated + // value; longer ones (and other float formats) take the general path + if (number_type == token_type::value_float && digit_count <= 19) + { + float_significand s; + s.w = w; + s.exponent = exponent - static_cast(fraction_digits); + s.negative = data[0] == '-'; + if (convert_accumulated_float(s, std::integral_constant::value> {})) + { + // the token text is only copied into token_buffer when + // get_string() asks for it; the library's own SAX handlers + // ignore it. A window into a stream may be refilled by the + // next read, so there the text is copied right away. + if (bulk_window) + { + token_buffer.append(reinterpret_cast(data), len); + } + else + { + pending_number_text = data; + pending_number_length = len; + } + return token_type::value_float; + } + } + // materialize the token exactly as scan_number() would. reset() already // cleared token_buffer, so append() fills it (assign() is avoided // because custom string_t types need not provide it) token_buffer.append(reinterpret_cast(data), len); - decimal_point_position = dot_index; - - ia.bulk_skip(len - 1); - position.chars_read_total += (len - 1); - position.chars_read_current_line += (len - 1); - return convert_number(number_type, mantissa_end); } + /// window adapter: a number that reaches the end of the window may continue + /// after it, so scan_number() has to read it character by character + static constexpr bool number_may_continue(bool at_end, std::true_type /*window*/) noexcept + { + return at_end; + } + + /// the end of the block is the end of the input, which ends the number + static constexpr bool number_may_continue(bool /*at_end*/, std::false_type /*window*/) noexcept + { + return false; + } + + /// window adapter: read the character after a bulk-scanned number and unget + /// it, as scan_number() does. The adapter is a stream that is handed back to + /// the user, who must find the same input consumed either way. + void read_number_terminator(std::true_type /*window*/) + { + get(); + unget(); + } + + /// the input is not handed back, so the terminator can stay unread + void read_number_terminator(std::false_type /*window*/) const noexcept {} + + /// binary32/binary64: the value of a float token from its accumulated + /// significand (see scan_number_bulk_contiguous()) + bool convert_accumulated_float(const float_significand& s, std::true_type /*native*/) noexcept + { + // accept() only needs to know that the value is finite (see the parser): + // 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 + // may overflow has to be converted to find out. + if (JSON_HEDLEY_UNLIKELY(discard_number_values && s.exponent + 19 <= std::numeric_limits::max_exponent10)) + { + value_float = 0; + return true; + } + value_float = decimal_to_float(s); + return true; + } + + /// other formats (e.g. x87 long double): not instantiated for them, as + /// decimal_to_float() only supports binary32/binary64 + static bool convert_accumulated_float(const float_significand& /*s*/, std::false_type /*native*/) noexcept + { + return false; + } + + /// the value of an integer token with at most 18 digits, accumulated in @a w + token_type store_integer(token_type number_type, std::uint64_t w) noexcept + { + if (number_type == token_type::value_unsigned) + { + value_unsigned = static_cast(w); + // reject values that do not round-trip into a narrower type + return (static_cast(value_unsigned) == w) ? token_type::value_unsigned : token_type::uninitialized; + } + const std::int64_t x = -static_cast(w); + value_integer = static_cast(x); + return (static_cast(value_integer) == x) ? token_type::value_integer : token_type::uninitialized; + } + /// contiguous input: try the number fast path, else the byte-path scanner token_type scan_number_dispatch(std::true_type /*bulk*/) { @@ -11856,18 +14311,23 @@ scan_number_done: } /*! - @param[in] literal_text the literal text to expect - @param[in] length the length of the passed literal text - @param[in] return_type the token type to return on success + @param[in] rest the literal text after its first character, which + is current (its length is a template parameter, so + that the comparison of the contiguous fast path is + a fixed-size load, not a call of memcmp) + @param[in] return_type the token type to return on success */ - JSON_HEDLEY_NON_NULL(2) - token_type scan_literal(const char_type* literal_text, const std::size_t length, - token_type return_type) + template + JSON_HEDLEY_NEVER_INLINE token_type scan_literal(const char (&rest)[N], token_type return_type) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) { - JSON_ASSERT(char_traits::to_char_type(current) == literal_text[0]); - for (std::size_t i = 1; i < length; ++i) + constexpr std::size_t length = N - 1; + if (scan_literal_bulk(rest, length, std::integral_constant {})) { - if (JSON_HEDLEY_UNLIKELY(char_traits::to_char_type(get()) != literal_text[i])) + return return_type; + } + for (std::size_t i = 0; i < length; ++i) + { + if (JSON_HEDLEY_UNLIKELY(char_traits::to_char_type(get()) != static_cast(rest[i]))) { error_message = "invalid literal"; return token_type::parse_error; @@ -11876,6 +14336,27 @@ scan_number_done: return return_type; } + /// contiguous input: compare the rest of a literal at once; false (and + /// nothing consumed) if it does not match, so that the byte path reports + /// 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*/) + { + if (next_unget || ia.bulk_remaining() < length + || std::memcmp(ia.bulk_data(), rest, length) != 0) + { + return false; + } + bulk_consume(reinterpret_cast(ia.bulk_data()), length); + current = static_cast(static_cast(rest[length - 1])); + return true; + } + + /// streaming input: no bulk fast path + bool scan_literal_bulk(const char* /*rest*/, const std::size_t /*length*/, std::false_type /*bulk*/) const noexcept + { + return false; + } + ///////////////////// // input management ///////////////////// @@ -11884,11 +14365,12 @@ scan_number_done: void reset() noexcept { token_buffer.clear(); + pending_number_text = nullptr; decimal_point_position = std::string::npos; #if JSON_DIAGNOSTIC_POSITIONS // the first character of the token has already been read, hence the -1 - token_start_position = position.chars_read_total - 1; + token_start_position = get_chars_read_total() - 1; #endif note_token_start(std::integral_constant {}); @@ -11897,11 +14379,30 @@ scan_number_done: /// seekable adapter: remember where the current token starts so it can be /// reconstructed from the input on error; current has already been /// consumed, hence the -1 - void note_token_start(std::true_type /*lazy*/) noexcept + void note_token_start(std::true_type /*lazy*/) + { + note_token_start_lazy(std::integral_constant {}); + } + + void note_token_start_lazy(std::false_type /*adapter_token*/) noexcept { token_string_start = ia.get_consumed_count() - 1; } + /// token-tracking adapter: start tracking the token at the first character + void begin_token(std::true_type /*adapter_token*/) noexcept + { + ia.begin_token(); + } + + void begin_token(std::false_type /*adapter_token*/) const noexcept {} + + /// token-tracking adapter: the token starts with the current character + void note_token_start_lazy(std::true_type /*adapter_token*/) + { + ia.mark_token_start(current); + } + /// streaming adapter: start copying the token eagerly, beginning with the /// already-read first character void note_token_start(std::false_type /*lazy*/) noexcept @@ -11941,6 +14442,15 @@ scan_number_done: /// per-character position counters (line-count-on-'\n' bookkeeping is /// handled afterwards, in track_after_read(), once `current` is known) void advance_position() noexcept + { + advance_position_impl(std::integral_constant {}); + } + + /// seekable adapter: the position is derived from the adapter on demand + void advance_position_impl(std::true_type /*lazy*/) const noexcept {} + + /// streaming adapter: count the character + void advance_position_impl(std::false_type /*lazy*/) noexcept { ++position.chars_read_total; ++position.chars_read_current_line; @@ -11954,7 +14464,16 @@ scan_number_done: // seekable adapters reconstruct the token lazily on error (see // get_token_string), so the eager per-character copy is skipped capture_char(std::integral_constant {}); + track_newline(std::integral_constant {}); + return current; + } + /// seekable adapter: lines are counted on demand in get_position() + void track_newline(std::true_type /*lazy*/) const noexcept {} + + /// streaming adapter: start a new line after reading a newline + void track_newline(std::false_type /*lazy*/) noexcept + { if (current == '\n') { ++position.lines_read; @@ -11963,8 +14482,6 @@ scan_number_done: chars_read_before_newline = position.chars_read_current_line; position.chars_read_current_line = 0; } - - return current; } /*! @@ -12008,12 +14525,22 @@ scan_number_done: We implement unget by setting variable next_unget to true. The input is not changed - we just simulate ungetting by modifying chars_read_total, chars_read_current_line, and token_string. The next call to get() will - behave as if the unget character is read again. + behave as if the unget character is read again. With lazy_position, the + pending next_unget is all get_chars_read_total() needs. */ void unget() { next_unget = true; + unget_position(std::integral_constant {}); + uncapture_char(std::integral_constant {}); + } + /// seekable adapter: get_chars_read_total() accounts for next_unget + void unget_position(std::true_type /*lazy*/) const noexcept {} + + /// streaming adapter: step the counters back over the ungotten character + void unget_position(std::false_type /*lazy*/) noexcept + { --position.chars_read_total; // in case we "unget" a newline, we have to also decrement the lines_read @@ -12036,8 +14563,6 @@ scan_number_done: { --position.chars_read_current_line; } - - uncapture_char(std::integral_constant {}); } /// adapter without lookahead: nothing to do (see release_lookahead) @@ -12100,18 +14625,49 @@ scan_number_done: /// return current string value string_t& get_string() { + if (pending_number_text != nullptr) + { + token_buffer.append(reinterpret_cast(pending_number_text), pending_number_length); + pending_number_text = nullptr; + } // a number token holds '.' regardless of the locale (#4084) return token_buffer; } + /// the value of a string token (key or string), which never has pending + /// number text, so get_string()'s check is not needed + string_t& get_string_token() noexcept + { + JSON_ASSERT(pending_number_text == nullptr); + return token_buffer; + } + + /// the text of a float token for a SAX handler that ignores it (the + /// library's own handlers): empty, without copying the token + const string_t& get_ignored_number_text() const noexcept + { + return token_buffer; + } + ///////////////////// // diagnostics ///////////////////// - /// return position of last read token - constexpr position_t get_position() const noexcept + /*! + @brief return the number of characters read so far + + Unlike get_position(), this is O(1) for all adapters, so it may be called + for every value (JSON_DIAGNOSTIC_POSITIONS). + */ + std::size_t get_chars_read_total() const noexcept { - return position; + return get_chars_read_total_impl(std::integral_constant {}); + } + + /// return position of last read token + position_t get_position() const + { + return get_position_impl(std::integral_constant {}); } /*! @@ -12148,8 +14704,59 @@ scan_number_done: } #endif + private: + /// seekable adapter: every get() either read from the adapter (including + /// reads at EOF, which get() counts as characters as well) or replayed an + /// ungotten character, so the count is the adapter's read count minus a + /// still pending unget + std::size_t get_chars_read_total_impl(std::true_type /*lazy*/) const noexcept + { + return ia.get_read_count() - (next_unget ? 1u : 0u); + } + + /// streaming adapter: the counter is maintained by get() and unget() + std::size_t get_chars_read_total_impl(std::false_type /*lazy*/) const noexcept + { + return position.chars_read_total; + } + + /// seekable adapter: count the lines in the consumed input; O(input read + /// so far), but only called on errors + position_t get_position_impl(std::true_type /*lazy*/) const + { + position_t result; + result.chars_read_total = get_chars_read_total(); + // reads at EOF count as characters but are not part of the input + const std::size_t consumed = (std::min)(result.chars_read_total, ia.get_consumed_count()); + std::size_t line_start = 0; + result.lines_read = ia.count_consumed_lines(consumed, line_start); + result.chars_read_current_line = result.chars_read_total - line_start; + return result; + } + + /// streaming adapter: the position is maintained by get() and unget() + position_t get_position_impl(std::false_type /*lazy*/) const noexcept + { + return position; + } + + public: /// seekable adapter: rebuild the last read token from the input on demand const std::vector& collect_token_chars(std::vector& out, std::true_type /*lazy*/) const + { + return collect_token_chars_lazy(out, std::integral_constant {}); + } + + /// token-tracking adapter: the adapter hands out everything read since + /// mark_token_start(), except a pending unget (see below) + const std::vector& collect_token_chars_lazy(std::vector& out, std::true_type /*adapter_token*/) const + { + const bool pending_real_unget = next_unget && current != char_traits::eof(); + ia.copy_token(out, pending_real_unget); + return out; + } + + const std::vector& collect_token_chars_lazy(std::vector& out, std::false_type /*adapter_token*/) const { // a pending unget of a real (non-EOF) character means that character // was consumed from the input but is not part of the token; EOF is @@ -12255,15 +14862,56 @@ scan_number_done: // into one that reloads the pointer from memory every character do { + if (current == '\n') + { + skip_indentation(std::integral_constant {}); + } get_ignoring_pending_unget(); } while (current_is_whitespace()); } + /*! + @brief contiguous input: skip the space indentation that follows a newline + 8 bytes at a time + + Pretty-printed JSON indents every line with a run of spaces, which the loop + in skip_whitespace() would otherwise read one character at a time. The + spaces are skipped 8 bytes at a time; the last 7 bytes of the input and any + other whitespace are left to that loop. Skipping bypasses get(); + bulk_consume() does its bookkeeping. + */ + void skip_indentation(std::true_type /*bulk*/) + { + JSON_ASSERT(!next_unget); + const std::size_t remaining = ia.bulk_remaining(); + if (remaining < 8) + { + return; + } + const char* const data = reinterpret_cast(ia.bulk_data()); + std::size_t pos = 0; + for (; pos + 8 <= remaining; pos += 8) + { + // little-endian, so the first byte is the lowest + const std::uint64_t non_space = read_eight_bytes(data + pos) ^ 0x2020202020202020ull; + if (non_space != 0) + { + // skip the spaces before the first other byte of this word + pos += static_cast(count_trailing_zeros(non_space)) / 8; + break; + } + } + bulk_consume(data, pos); + } + + /// streaming input: no bulk fast path + void skip_indentation(std::false_type /*bulk*/) const noexcept {} + token_type scan() { // initially, skip the BOM - if (position.chars_read_total == 0 && !skip_bom()) + if (get_chars_read_total() == 0 && !skip_bom()) { error_message = "invalid BOM; must be 0xEF 0xBB 0xBF if given"; return token_type::parse_error; @@ -12290,7 +14938,7 @@ scan_number_done: token_type scan_expecting(token_type expected_type) { JSON_ASSERT(expected_type == token_type::name_separator || expected_type == token_type::value_separator); - JSON_ASSERT(position.chars_read_total > 0); + JSON_ASSERT(get_chars_read_total() > 0); const char_int_type expected_char = static_cast((expected_type == token_type::name_separator) ? ':' : ','); skip_whitespace(); if (JSON_HEDLEY_LIKELY(current == expected_char)) @@ -12334,21 +14982,14 @@ scan_number_done: return token_type::value_separator; // literals + // (the text after the first character; no local arrays, which + // would give this function a stack canary on every token) case 't': - { - std::array true_literal = {{static_cast('t'), static_cast('r'), static_cast('u'), static_cast('e')}}; - return scan_literal(true_literal.data(), true_literal.size(), token_type::literal_true); - } + return scan_literal("rue", token_type::literal_true); case 'f': - { - std::array false_literal = {{static_cast('f'), static_cast('a'), static_cast('l'), static_cast('s'), static_cast('e')}}; - return scan_literal(false_literal.data(), false_literal.size(), token_type::literal_false); - } + return scan_literal("alse", token_type::literal_false); case 'n': - { - std::array null_literal = {{static_cast('n'), static_cast('u'), static_cast('l'), static_cast('l')}}; - return scan_literal(null_literal.data(), null_literal.size(), token_type::literal_null); - } + return scan_literal("ull", token_type::literal_null); // string case '\"': @@ -12396,6 +15037,11 @@ scan_number_done: /// whether the next get() call should just return current bool next_unget = false; + /// the text of the current float token in the input, not yet copied into + /// token_buffer (see get_string()), or nullptr + const char* pending_number_text = nullptr; + std::size_t pending_number_length = 0; + /// the start position of the current token position_t position {}; @@ -12442,10 +15088,66 @@ scan_number_done: } // namespace detail NLOHMANN_JSON_NAMESPACE_END +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // conditional, is_same + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN + +/*! +@brief Default base class of the @ref basic_json class. + +So that the correct implementations of the copy / move ctors / assign operators +of @ref basic_json do not require complex case distinctions +(no base class / custom base class used as customization point), +@ref basic_json always has a base class. +By default, this class is used because it is empty and thus has no effect +on the behavior of @ref basic_json. + +@note This class intentionally lives in namespace @ref nlohmann rather than + @ref nlohmann::detail. Every @ref basic_json specialization derives from + it (via @ref detail::json_base_class) unless a custom base class is + supplied, which makes its namespace an associated namespace of + @ref basic_json for the purpose of argument-dependent lookup (ADL). If + it lived in `nlohmann::detail`, that namespace - and with it the + library's internal `to_json`/`from_json` overloads - would leak into + ADL for any unqualified `to_json`/`from_json` call a user makes + involving a @ref basic_json argument, silently shadowing the user's own + overloads in some cases. +*/ +struct json_default_base {}; + +namespace detail +{ + +template +using json_base_class = typename std::conditional < + std::is_same::value, + ::nlohmann::json_default_base, + T + >::type; + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + // #include // #include +// #include + // #include NLOHMANN_JSON_NAMESPACE_BEGIN @@ -12603,6 +15305,34 @@ template inline void reserve_array(ArrayType& /*arr*/, std::size_t /*len*/, priority_tag<0> /*unused*/) {} +/*! +@brief reports an object or array whose announced size exceeds max_size() + +Shared by json_sax_dom_parser and json_sax_dom_callback_parser. + +@param[in] sax the SAX parser to report the error to +@param[in] len the number of elements announced by the input, or unknown_size() +@param[in] kind "object" or "array" +@param[in] ref the object or array that was just created +@return whether parsing should continue (false after reporting out_of_range.408) +*/ +template +JSON_HEDLEY_NEVER_INLINE +bool container_too_large_error(SAX& sax, std::size_t len, const char* kind, BasicJsonType* ref) +{ + return sax.parse_error(0, "", out_of_range::create(exception_id::container_too_large, concat("excessive ", kind, " size: ", std::to_string(len)), ref)); +} + +template +bool check_container_size(SAX& sax, std::size_t len, const char* kind, BasicJsonType* ref) +{ + if (JSON_HEDLEY_UNLIKELY(len != detail::unknown_size() && len > ref->max_size())) + { + return container_too_large_error(sax, len, kind, ref); + } + return true; +} + #if JSON_DIAGNOSTIC_POSITIONS /*! @brief set the diagnostic positions of a value the DOM SAX parsers just stored @@ -12612,6 +15342,35 @@ befriends this struct, as the position members are private. */ struct diagnostic_positions { + /*! + @param[in,out] v the object or array whose opening brace or bracket was just read + @param[in] lexer the lexer that read it, or nullptr to leave @a v alone + */ + template + static void set_container_start(BasicJsonType& v, LexerType* lexer) + { + if (lexer) + { + // Lexer has read the first character of the container, so + // subtract 1 from the position to get the correct start position. + v.start_position = lexer->get_chars_read_total() - 1; + } + } + + /*! + @param[in,out] v the object or array whose closing brace or bracket was just read + @param[in] lexer the lexer that read it, or nullptr to leave @a v alone + */ + template + static void set_container_end(BasicJsonType& v, LexerType* lexer) + { + if (lexer) + { + // Lexer's position is past the closing brace or bracket, so set that as the end position. + v.end_position = lexer->get_chars_read_total(); + } + } + /*! @param[in,out] v the value that was just parsed @param[in] lexer the lexer that read it, or nullptr to leave @a v alone @@ -12624,7 +15383,7 @@ struct diagnostic_positions // Lexer has read past the current field value, so set the end position to the current position. // The start position will be set below based on the length of the string representation // of the value. - v.end_position = lexer->get_position(); + v.end_position = lexer->get_chars_read_total(); switch (v.type()) { @@ -12685,21 +15444,161 @@ struct diagnostic_positions }; #endif +/*! +@brief error state shared by the DOM-building SAX parsers + +Holds whether a parse error occurred and whether errors are thrown, and +implements the parse_error() and is_errored() members of the SAX interface for +json_sax_dom_parser and json_sax_dom_callback_parser. +*/ +class json_sax_dom_error_state +{ + public: + explicit json_sax_dom_error_state(const bool allow_exceptions_) noexcept + : allow_exceptions(allow_exceptions_) + {} + + // make class move-only + json_sax_dom_error_state(const json_sax_dom_error_state&) = delete; + json_sax_dom_error_state(json_sax_dom_error_state&&) = default; // NOLINT(hicpp-noexcept-move,performance-noexcept-move-constructor) + json_sax_dom_error_state& operator=(const json_sax_dom_error_state&) = delete; + json_sax_dom_error_state& operator=(json_sax_dom_error_state&&) = delete; // the const member prevents assignment + ~json_sax_dom_error_state() = default; + + template + bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, + const Exception& ex) + { + errored = true; + static_cast(ex); + if (allow_exceptions) + { + JSON_THROW(ex); + } + return false; + } + + bool is_errored() const + { + return errored; + } + + private: + /// whether a syntax error occurred + bool errored = false; + /// whether to throw exceptions in case of errors + const bool allow_exceptions = true; +}; + +/*! +@brief a hash table that finds the position of a key in an object whose + elements are kept in a vector (ordered_map) + +The elements stay where they are: the table only holds their hashes and +positions, and the caller compares the keys. +*/ +class object_key_index +{ + public: + static constexpr std::size_t npos = static_cast(-1); + + /// @param[in] element_hashes the hashes of the keys of the elements that are there already + explicit object_key_index(std::vector&& element_hashes) + : hashes(std::move(element_hashes)) + { + std::size_t size = 256; + while (size < 4 * hashes.size()) + { + size *= 2; + } + rebuild(size); + } + + /// @return the position of the element with @a hash for which @a matches(position) holds, or npos + template + std::size_t find(const std::size_t hash, Matches matches) const + { + for (std::size_t slot = hash & mask;; slot = (slot + 1) & mask) + { + const std::size_t entry = table[slot]; + if (entry == 0) + { + return npos; + } + if (hashes[entry - 1] == hash && matches(entry - 1)) + { + return entry - 1; + } + } + } + + /// add the next element, whose position is the number of elements so far + void add(const std::size_t hash) + { + hashes.push_back(hash); + if (hashes.size() * 2 > table.size()) + { + rebuild(table.size() * 2); + } + else + { + place(hash, hashes.size()); + } + } + + private: + void rebuild(const std::size_t size) + { + table.assign(size, 0); + mask = size - 1; + for (std::size_t i = 0; i < hashes.size(); ++i) + { + place(hashes[i], i + 1); + } + } + + void place(const std::size_t hash, const std::size_t entry) + { + std::size_t slot = hash & mask; + while (table[slot] != 0) + { + slot = (slot + 1) & mask; + } + table[slot] = entry; + } + + /// the hash of each element, by position + std::vector hashes; + /// open addressing, a power of two in size: position + 1 of an element, or 0 for a free slot + std::vector table {}; // NOLINT(readability-redundant-member-init) + std::size_t mask = 0; +}; + /*! @brief SAX implementation to create a JSON value from SAX events This class implements the @ref json_sax interface and processes the SAX events to create a JSON value which makes it basically a DOM parser. The structure or -hierarchy of the JSON value is managed by the stack `ref_stack` which contains -a pointer to the respective array or object for each recursion depth. +hierarchy of the JSON value is managed by the stack `frames` which contains +an entry for each open array or object. The parser of the text formats, for +which the size of a container is not known up front, collects the elements of +an array and the members of an `ordered_json` object on a stack and moves them +into a container of the exact size (with @a Collect, as basic_json's own parse +functions use it); the binary readers use the plain +@ref json_sax_dom_binary_parser instead. After successful parsing, the value that is passed by reference to the -constructor contains the parsed value. +constructor contains the parsed value. Without @a Collect, every value is +stored in place as it arrives, so after a parse error the value holds what was +read up to it. @tparam BasicJsonType the JSON type +@tparam Collect whether to collect the values of containers of unknown size on + a stack (only for a caller that does not use the value after a + parse error) */ -template -class json_sax_dom_parser +template +class json_sax_dom_parser : public json_sax_dom_error_state { public: using number_integer_t = typename BasicJsonType::number_integer_t; @@ -12715,7 +15614,7 @@ class json_sax_dom_parser @param[in] allow_exceptions_ whether parse errors yield exceptions */ explicit json_sax_dom_parser(BasicJsonType& r, const bool allow_exceptions_ = true, lexer_t* lexer_ = nullptr) - : root(r), allow_exceptions(allow_exceptions_), m_lexer_ref(lexer_) + : json_sax_dom_error_state(allow_exceptions_), root(r), m_lexer_ref(lexer_) {} // make class move-only @@ -12771,27 +15670,934 @@ class json_sax_dom_parser return true; } + bool start_object(std::size_t len) + { +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + // the values need their final place from the start (their parent + // pointers and positions refer to it), so no buffering + return start_stored_object(len); +#else + return start_object(len, std::integral_constant {}); +#endif + } + + bool key(string_t& val) + { + JSON_ASSERT(!frames.empty()); + return key(val, std::integral_constant {}); + } + + bool end_object() + { + JSON_ASSERT(!frames.empty()); + return end_object(std::integral_constant {}); + } + + bool start_array(std::size_t len) + { + if (JSON_HEDLEY_UNLIKELY(len != detail::unknown_size() && len > array_t().max_size())) + { + // the array does not exist yet: report with an empty one + BasicJsonType too_large(BasicJsonType::value_t::array); + return container_too_large_error(*this, len, "array", &too_large); + } + +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + // the values need their final place from the start (their parent + // pointers and positions refer to it), so no buffering + start_stored_array(len); +#else + if (!collects(len)) + { + // the number of elements is known (or the elements must be in + // place as they arrive): store them directly + start_stored_array(len); + } + else + { + // the number of elements is not known, so growing the array + // would reallocate it (and, being a vector, move its elements) + // again and again. Collect the elements on a stack shared by all + // arrays instead, and copy them to an array of the exact size + // when the closing bracket arrives. + BasicJsonType* slot = nullptr; + std::size_t dest_member = 0; + container_destination(slot, dest_member); + reserve_elements(); + frames.push_back(frame{nullptr, nullptr, slot, elements.size(), ordered_state(dest_member, false)}); + in_object = false; + stored_array = nullptr; + } +#endif + return true; + } + + bool end_array() + { + JSON_ASSERT(!frames.empty()); + JSON_ASSERT(frames.back().object == nullptr); + const frame f = frames.back(); + frames.pop_back(); + reenter_enclosing_container(); + + if (f.array == nullptr) + { + // the elements are the top of the shared elements stack, from + // f.start on (the elements below belong to enclosing arrays that + // are still open): move them into the array and pop them + const auto first = elements.begin() + static_cast(f.start); + auto* const arr = BasicJsonType::template create(std::make_move_iterator(first), std::make_move_iterator(elements.end())); + BasicJsonType value; + value.m_data.m_value.array = arr; + value.m_data.m_type = BasicJsonType::value_t::array; + elements.erase(first, elements.end()); + + place_container(f, frames.empty() ? nullptr : &frames.back(), std::move(value), true); + return true; + } + +#if JSON_DIAGNOSTIC_POSITIONS + diagnostic_positions::set_container_end(*f.slot, m_lexer_ref); +#endif +#if JSON_DIAGNOSTICS + f.slot->set_parents(); +#endif + return true; + } + + private: + using object_t = typename BasicJsonType::object_t; + using array_t = typename BasicJsonType::array_t; + + /// whether objects keep their elements in a vector and a key can be hashed, + /// so that a big object can find the keys it has in a hash table instead of + /// searching all of them + template + using detect_emplace_new_key = decltype(std::declval().emplace_new_key(std::declval(), std::declval())); + template + using detect_hash = decltype(std::hash()(std::declval())); + // std::hash is only named once the object can hold the keys in a vector: + // VS 2015 static-asserts on std::hash of a type without one instead of + // reporting a substitution failure + template::value> + struct detect_indexable_keys : std::false_type {}; + template + struct detect_indexable_keys : is_detected {}; + static constexpr bool indexed_keys = detect_indexable_keys::value; + + /// what a frame holds for objects that are kept in a vector: the key index + /// of an object with many keys, and for an object that is collected on the + /// stack the member of the enclosing object it will become + struct ordered_frame + { + /// the entry of key_indexes of an object that has one, else nullptr + object_key_index* index; + /// object or array without a slot in an object collected on the stack: + /// the index of the member in `members` it goes to + std::size_t dest_member; + /// whether this is an object collected on the stack + bool buffered; + }; + struct no_ordered_frame {}; + using ordered_frame_state = typename std::conditional::type; + + static ordered_frame make_ordered_state(const std::size_t dest_member, const bool buffered, std::true_type /*indexed*/) noexcept + { + ordered_frame state = {nullptr, dest_member, buffered}; + return state; + } + + static no_ordered_frame make_ordered_state(const std::size_t /*dest_member*/, const bool /*buffered*/, std::false_type /*indexed*/) noexcept + { + return no_ordered_frame(); + } + + static ordered_frame_state ordered_state(const std::size_t dest_member, const bool buffered) noexcept + { + return make_ordered_state(dest_member, buffered, std::integral_constant {}); + } + + /// an array or object that is being read + struct frame + { + /// the object, or nullptr for an array + object_t* object; + /// the array if its elements are stored in it as they arrive, or + /// nullptr for an array and an object + array_t* array; + /// array without `array`: where the finished array goes (nullptr: it + /// is an element of the enclosing array); otherwise: the value that + /// holds the object or array + BasicJsonType* slot; + /// array without `array`: the index of its first element in `elements` + std::size_t start; + /// what objects in a vector add (see ordered_frame) + ordered_frame_state ordered; + }; + + /// the open arrays and objects; the first few live in the object itself, so + /// that the common documents do not allocate for them + class frame_stack + { + public: + frame_stack() noexcept + : local() // NOLINT(readability-redundant-member-init) + {} + frame_stack(const frame_stack&) = delete; + frame_stack& operator=(const frame_stack&) = delete; + + frame_stack(frame_stack&& other) noexcept + : local() // NOLINT(readability-redundant-member-init) + { + take(other); + } + + frame_stack& operator=(frame_stack&& other) noexcept + { + if (this != &other) + { + release(); + take(other); + } + return *this; + } + + ~frame_stack() = default; + + bool empty() const noexcept + { + return count == 0; + } + + std::size_t size() const noexcept + { + return count; + } + + frame& back() noexcept + { + return data[count - 1]; + } + + frame& operator[](const std::size_t i) noexcept + { + return data[i]; + } + + void push_back(const frame& f) + { + if (JSON_HEDLEY_UNLIKELY(count == capacity)) + { + grow(); + } + data[count++] = f; + } + + void pop_back() noexcept + { + JSON_ASSERT(count > 0); + --count; + } + + private: + JSON_HEDLEY_NEVER_INLINE + void grow() + { + const std::size_t new_capacity = capacity * 2; + std::unique_ptr new_heap(new frame[new_capacity]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + for (std::size_t i = 0; i < count; ++i) + { + new_heap[i] = data[i]; + } + heap = std::move(new_heap); + data = heap.get(); + capacity = new_capacity; + } + + void release() noexcept + { + heap.reset(); + data = local.data(); + capacity = local_capacity; + } + + /// make the contents of @a other our own + void take(frame_stack& other) noexcept + { + if (other.heap == nullptr) + { + for (std::size_t i = 0; i < other.count; ++i) + { + local[i] = other.local[i]; + } + data = local.data(); + capacity = local_capacity; + } + else + { + heap = std::move(other.heap); + data = heap.get(); + capacity = other.capacity; + other.data = other.local.data(); + other.capacity = local_capacity; + } + count = other.count; + other.count = 0; + } + + static constexpr std::size_t local_capacity = 8; + + /// the first frames, left uninitialized: a frame is written before it is + /// read (the user-provided constructor keeps the member initialization, + /// which -Weffc++ asks for, from zeroing them) + struct local_frames : std::array + { + local_frames() noexcept {} // NOLINT(modernize-use-equals-default,hicpp-use-equals-default,cppcoreguidelines-pro-type-member-init,hicpp-member-init): see above + }; + + local_frames local; + /// the frames once there are more than local_capacity of them + std::unique_ptr heap {}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays,readability-redundant-member-init) + frame* data = local.data(); + std::size_t count = 0; + std::size_t capacity = local_capacity; + }; + + void release_key_index(const frame& f, std::true_type /*indexed*/) + { + if (JSON_HEDLEY_UNLIKELY(f.ordered.index != nullptr)) + { + ordered_data.key_indexes.pop_back(); + } + } + + static void release_key_index(const frame& /*f*/, std::false_type /*indexed*/) noexcept {} + + /// the number of keys from which an object looks them up in a hash table + static constexpr std::size_t key_index_threshold = 64; + + /// add @a key to the object of @a f (or find it, if it is there), searching it in std::map + static BasicJsonType* insert_key(frame& f, string_t&& key, std::false_type /*indexed*/) + { + return &f.object->operator[](std::move(key)); + } + + /// add @a key to the object of @a f (or find it, if it is there), searching it in a vector + BasicJsonType* insert_key(frame& f, string_t&& key, std::true_type /*indexed*/) + { + object_t& obj = *f.object; + if (JSON_HEDLEY_LIKELY(obj.size() < key_index_threshold)) + { + return &obj[std::move(key)]; + } + return insert_key_with_index(f, std::move(key)); + } + + /// add @a key to the big object of @a f (or find it, if it is there), using its key index + JSON_HEDLEY_NEVER_INLINE + BasicJsonType* insert_key_with_index(frame& f, string_t&& key) + { + object_t& obj = *f.object; + const std::hash hasher {}; + const std::size_t hash = hasher(key); + if (JSON_HEDLEY_UNLIKELY(f.ordered.index == nullptr)) + { + std::vector hashes; + hashes.reserve(obj.size()); + for (const auto& element : obj) + { + hashes.push_back(hasher(element.first)); + } + ordered_data.key_indexes.emplace_back(new object_key_index(std::move(hashes))); + f.ordered.index = ordered_data.key_indexes.back().get(); + } + + const std::size_t position = f.ordered.index->find(hash, [&obj, &key](std::size_t i) + { + return (obj.begin() + static_cast(i))->first == key; + }); + if (position != object_key_index::npos) + { + return &(obj.begin() + static_cast(position))->second; + } + + obj.emplace_new_key(key, BasicJsonType()); + f.ordered.index->add(hash); + return &obj.back().second; + } + + /// where an array or object that starts now will go when it is finished: + /// @a slot is the value to assign it to (the root or a member of an object + /// that is stored directly), or nullptr if it is an element of the + /// enclosing array or a member of the enclosing object that is collected on + /// the stack, in which case @a dest_member is the member + void container_destination(BasicJsonType*& slot, std::size_t& dest_member) + { + if (frames.empty()) + { + slot = &root; + } + else if (frames.back().object != nullptr) + { + slot = object_element; + } + else + { + collected_object_member(frames.back(), dest_member, std::integral_constant {}); + } + } + + void collected_object_member(const frame& enclosing, std::size_t& dest_member, std::true_type /*indexed*/) const noexcept + { + if (enclosing.ordered.buffered) + { + dest_member = ordered_data.member_index; + } + } + + static void collected_object_member(const frame& /*enclosing*/, std::size_t& /*dest_member*/, std::false_type /*indexed*/) noexcept {} + + static bool is_collected_object(const frame& f, std::true_type /*indexed*/) noexcept + { + return f.ordered.buffered; + } + + static bool is_collected_object(const frame& /*f*/, std::false_type /*indexed*/) noexcept + { + return false; + } + + /// put a finished array or object that was collected on the stack, or + /// that outgrew it, where it belongs; @a enclosing is the container it is in. + /// @a enclosing_is_innermost tells whether that is the container that gets + /// the next value, so that it can be switched to storing its elements + /// directly if the value was the one too many. + void place_container(const frame& f, frame* enclosing, BasicJsonType&& value, const bool enclosing_is_innermost) + { + // only a container with a slot may have no enclosing container + JSON_ASSERT(f.slot != nullptr || enclosing != nullptr); + if (f.slot != nullptr) + { + *f.slot = std::move(value); + } + else if (is_collected_object(*enclosing, std::integral_constant {})) + { + place_member(f, std::move(value), std::integral_constant {}); + } + else if (enclosing->array != nullptr) + { + enclosing->array->emplace_back(std::move(value)); + } + else + { + elements.emplace_back(std::move(value)); + if (JSON_HEDLEY_UNLIKELY(enclosing_is_innermost && elements.size() - enclosing->start > buffer_limit)) + { + spill(*enclosing); + } + } + } + + void place_member(const frame& f, BasicJsonType&& value, std::true_type /*indexed*/) + { + 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 {} + + /// store the members of a new object directly in it + bool start_stored_object(std::size_t len) + { + BasicJsonType* const ref = store(BasicJsonType::value_t::object); + frames.push_back(frame{ref->m_data.m_value.object, nullptr, ref, 0, ordered_state(0, false)}); + in_object = true; + stored_array = nullptr; + +#if JSON_DIAGNOSTIC_POSITIONS + // Ensure this is after the call to store to ensure correct start position. + diagnostic_positions::set_container_start(*ref, m_lexer_ref); +#endif + + return check_container_size(*this, len, "object", ref); + } + + /// objects in std::map and the like have no use for collecting their members + bool start_object(std::size_t len, std::false_type /*indexed*/) + { + return start_stored_object(len); + } + + /// An object in a vector of members adds a member by appending it to the + /// vector, which grows it again and again (and copies the keys each time). + /// Collect the members of an object of unknown size on a stack shared by + /// all open objects instead, and move them into a vector of the exact size + /// when the closing brace arrives. + bool start_object(std::size_t len, std::true_type /*indexed*/) + { + if (!collects(len)) + { + return start_stored_object(len); + } + + BasicJsonType* slot = nullptr; + std::size_t dest_member = 0; + container_destination(slot, dest_member); + frames.push_back(frame{nullptr, nullptr, slot, ordered_data.members.size(), ordered_state(dest_member, true)}); + in_object = false; + stored_array = nullptr; + return true; + } + + bool key(string_t& val, std::false_type /*indexed*/) + { + JSON_ASSERT(frames.back().object != nullptr); + + // add null at the given key and store the reference for later; the + // key is moved, as the lexer starts the next token with reset() + object_element = insert_key(frames.back(), std::move(val), std::false_type {}); + return true; + } + + bool key(string_t& val, std::true_type /*indexed*/) + { + frame& f = frames.back(); + if (JSON_HEDLEY_UNLIKELY(f.object == nullptr)) + { + JSON_ASSERT(f.ordered.buffered); + if (collect_member(f, val)) + { + return true; + } + // the object has grown big: it is stored directly from here on + } + object_element = insert_key(f, std::move(val), std::true_type {}); + return true; + } + + /*! + @brief add a member to the object that is collected on the stack, or find the one with the key + @return false if the object has collected enough members to be stored + directly, which is done and which leaves the key to be inserted + */ + bool collect_member(frame& f, string_t& key) + { + for (std::size_t i = f.start; i < ordered_data.members.size(); ++i) + { + if (ordered_data.members[i].first == key) + { + ordered_data.member_index = i; + return true; + } + } + + if (JSON_HEDLEY_UNLIKELY(ordered_data.members.size() - f.start >= object_buffer_limit)) + { + spill_object(f); + return false; + } + + reserve_members(); + ordered_data.members.emplace_back(std::move(key), BasicJsonType()); + ordered_data.member_index = ordered_data.members.size() - 1; + return true; + } + + /// the members of an object that is collected on the stack as an object; + /// they are the top of the shared member stack, from f.start on (the + /// members below belong to enclosing objects that are still open) + object_t* members_to_object(const frame& f, BasicJsonType& value) + { + const auto first = ordered_data.members.begin() + static_cast(f.start); + auto* const obj = BasicJsonType::template create(std::make_move_iterator(first), std::make_move_iterator(ordered_data.members.end())); + value.m_data.m_value.object = obj; + value.m_data.m_type = BasicJsonType::value_t::object; + ordered_data.members.erase(first, ordered_data.members.end()); + return obj; + } + + /// switch an object that collects its members on the stack to storing them directly + JSON_HEDLEY_NEVER_INLINE + void spill_object(frame& f) + { + BasicJsonType value; + f.object = members_to_object(f, value); + f.ordered.buffered = false; + in_object = true; + // false positive: an object without an enclosing container is the root, + // which has a slot, so place_container() does not use the nullptr + // @infer-ignore NULLPTR_DEREFERENCE + place_container(f, frames.size() >= 2 ? &frames[frames.size() - 2] : nullptr, std::move(value), false); + } + + bool end_object(std::false_type /*indexed*/) + { + return end_stored_object(); + } + + bool end_object(std::true_type /*indexed*/) + { + if (JSON_HEDLEY_LIKELY(frames.back().object != nullptr)) + { + return end_stored_object(); + } + + // the members are on the stack: make them the object + const frame f = frames.back(); + frames.pop_back(); + reenter_enclosing_container(); + + BasicJsonType value; + members_to_object(f, value); + place_container(f, frames.empty() ? nullptr : &frames.back(), std::move(value), true); + return true; + } + + bool end_stored_object() + { + JSON_ASSERT(frames.back().object != nullptr); + +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + // objects are never collected, so their value is where it was stored + BasicJsonType* const ref = frames.back().slot; +#endif +#if JSON_DIAGNOSTIC_POSITIONS + diagnostic_positions::set_container_end(*ref, m_lexer_ref); +#endif +#if JSON_DIAGNOSTICS + ref->set_parents(); +#endif + + release_key_index(frames.back(), std::integral_constant {}); + frames.pop_back(); + reenter_enclosing_container(); + return true; + } + + /// the number of members an object collects on the stack before it + /// switches to storing them directly + static constexpr std::size_t object_buffer_limit = 32; + + void reserve_members() + { + if (JSON_HEDLEY_UNLIKELY(ordered_data.members.capacity() == 0)) + { + ordered_data.members.reserve(16); + } + } + + /// the number of elements an array collects on the stack before it + /// switches to storing them directly, so that it does not hold up the + /// stack with its elements (and move all of them in the end) + static constexpr std::size_t buffer_limit = 16; + + void reserve_elements() + { + if (JSON_HEDLEY_UNLIKELY(elements.capacity() == 0)) + { + elements.reserve(32); + } + } + + /// store the elements of a new array directly in it + void start_stored_array(const std::size_t len) + { + BasicJsonType* const ref = store(BasicJsonType::value_t::array); + frames.push_back(frame{nullptr, ref->m_data.m_value.array, ref, 0, ordered_state(0, false)}); + in_object = false; + stored_array = ref->m_data.m_value.array; + +#if JSON_DIAGNOSTIC_POSITIONS + // Ensure this is after the call to store to ensure correct start position. + diagnostic_positions::set_container_start(*ref, m_lexer_ref); +#endif + + if (len != detail::unknown_size()) + { + reserve_array(*ref->m_data.m_value.array, len, priority_tag<1> {}); + } + } + + /*! + @brief store a value as the next element of the open array or object (or + as the root) + @return the stored value + */ + template + JSON_HEDLEY_RETURNS_NON_NULL + BasicJsonType* store(Value&& v) + { + if (stored_array != nullptr) + { + stored_array->emplace_back(std::forward(v)); + return &stored_array->back(); + } + + if (in_object) + { + JSON_ASSERT(object_element); + set_member(*object_element, std::forward(v), std::is_same {}); + return object_element; + } + + return store_collected(std::forward(v), std::integral_constant {}); + } + + /// store a value that is the root, an element of an array that is collected + /// on the stack, or (in a vector) a member of an object that is + template + BasicJsonType* store_collected(Value&& v, std::true_type /*indexed*/) + { + if (!frames.empty() && frames.back().ordered.buffered) + { + BasicJsonType& member = ordered_data.members[ordered_data.member_index].second; + set_member(member, std::forward(v), std::is_same {}); + return &member; + } + return store_collected(std::forward(v), std::false_type {}); + } + + template + BasicJsonType* store_collected(Value&& v, std::false_type /*indexed*/) + { + if (frames.empty()) + { + root = BasicJsonType(std::forward(v)); + return &root; + } + + frame& f = frames.back(); + elements.emplace_back(std::forward(v)); + if (JSON_HEDLEY_UNLIKELY(elements.size() - f.start > buffer_limit)) + { + return spill(f); + } + return &elements.back(); + } + + /*! + @brief switch an array that collects its elements on the stack to storing + them directly + @return the last element of the array + */ + JSON_HEDLEY_NEVER_INLINE + BasicJsonType* spill(frame& f) + { + JSON_ASSERT(f.array == nullptr); + const auto first = elements.begin() + static_cast(f.start); + BasicJsonType value(BasicJsonType::value_t::array); + array_t& arr = *value.m_data.m_value.array; + // the capacity a vector that grew by itself has with this many elements + reserve_array(arr, 2 * buffer_limit, priority_tag<1> {}); + arr.insert(arr.end(), std::make_move_iterator(first), std::make_move_iterator(elements.end())); + elements.erase(first, elements.end()); + f.array = &arr; + stored_array = &arr; + + // the array takes the place its first element had on the stack + place_container(f, frames.size() >= 2 ? &frames[frames.size() - 2] : nullptr, std::move(value), false); + return &arr.back(); + } + + /// set the value of the member key() just added + template + JSON_HEDLEY_ALWAYS_INLINE + static void set_member(BasicJsonType& member, Value&& v, std::true_type /*default base*/) + { + if (JSON_HEDLEY_LIKELY(member.is_null())) + { + // a new member is null and holds nothing: construct the value + // over it instead of destroying and swapping + ::new (static_cast(&member)) BasicJsonType(std::forward(v)); + } + else + { + // the key occurred before + member = BasicJsonType(std::forward(v)); + } + } + + /// a custom base class may care about the lifetime of the value + template + static void set_member(BasicJsonType& member, Value&& v, std::false_type /*custom base*/) + { + member = BasicJsonType(std::forward(v)); + } + + /// whether the values of a container of @a len elements are collected on the stack + static bool collects(const std::size_t len) noexcept + { + return collects(len, std::integral_constant {}); + } + + static bool collects(const std::size_t len, std::true_type /*collect*/) noexcept + { + return len == detail::unknown_size(); + } + + static bool collects(const std::size_t /*len*/, std::false_type /*collect*/) noexcept + { + return false; + } + + /// after a container was closed: the one it was in is the one that gets the next value + void reenter_enclosing_container() + { + if (frames.empty()) + { + in_object = false; + stored_array = nullptr; + } + else + { + const frame& f = frames.back(); + in_object = f.object != nullptr; + stored_array = f.array; + } + } + + template + void handle_value(Value&& v) + { +#if JSON_DIAGNOSTIC_POSITIONS + BasicJsonType* const stored = store(std::forward(v)); + diagnostic_positions::set_from_lexer(*stored, m_lexer_ref); +#else + store(std::forward(v)); +#endif + } + + /// the parsed JSON value + BasicJsonType& root; + /// the open arrays and objects + frame_stack frames {}; + /// the elements of the open arrays that are not stored in them yet + std::vector elements {}; + /// what objects in a vector need besides the frames (std::map based objects + /// need nothing, so they do not carry it) + struct ordered_data_t + { + /// the members of the open objects in a vector that are not stored in them yet + std::vector> members {}; + /// the member of the innermost such object that key() added or found last + std::size_t member_index = 0; + /// the key indexes of the open objects that have one, innermost last + std::vector> key_indexes {}; // NOLINT(readability-redundant-member-init) + }; + struct no_ordered_data_t {}; + typename std::conditional::type ordered_data {}; + /// helper to hold the reference for the next object element + BasicJsonType* object_element = nullptr; + /// whether the innermost container is an object (frames.back().object) + bool in_object = false; + /// the array that stores the elements of the innermost array as they + /// arrive (frames.back().array), or nullptr + array_t* stored_array = nullptr; + /// the lexer reference to obtain the current position + lexer_t* m_lexer_ref = nullptr; +}; + +/*! +@brief SAX implementation to create a JSON value from the events of a binary reader + +The DOM parser of the text formats (json_sax_dom_parser) collects the elements +of arrays and the members of objects on a stack and builds them in one +exact-size allocation, which pays off because a text parser does not know the +number of elements up front. Binary formats announce the size of their +containers (CBOR, MessagePack, BSON and BON8 do, and so do the optimized +containers of UBJSON and BJData), so they store each value directly in its +container, and the bookkeeping of the collecting parser would be pure +overhead for them. This is the plain parser: the structure or hierarchy of the +JSON value is managed by the stack `ref_stack` which contains a pointer to the +respective array or object for each recursion depth. + +After successful parsing, the value that is passed by reference to the +constructor contains the parsed value. + +@tparam BasicJsonType the JSON type +*/ +template +class json_sax_dom_binary_parser : public json_sax_dom_error_state +{ + public: + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + using binary_t = typename BasicJsonType::binary_t; + using lexer_t = lexer; + + /*! + @param[in,out] r reference to a JSON value that is manipulated while + parsing + @param[in] allow_exceptions_ whether parse errors yield exceptions + */ + explicit json_sax_dom_binary_parser(BasicJsonType& r, const bool allow_exceptions_ = true, lexer_t* lexer_ = nullptr) + : json_sax_dom_error_state(allow_exceptions_), root(r), m_lexer_ref(lexer_) + {} + + // make class move-only + json_sax_dom_binary_parser(const json_sax_dom_binary_parser&) = delete; + json_sax_dom_binary_parser(json_sax_dom_binary_parser&&) = default; // NOLINT(hicpp-noexcept-move,performance-noexcept-move-constructor) + json_sax_dom_binary_parser& operator=(const json_sax_dom_binary_parser&) = delete; + json_sax_dom_binary_parser& operator=(json_sax_dom_binary_parser&&) = default; // NOLINT(hicpp-noexcept-move,performance-noexcept-move-constructor) + ~json_sax_dom_binary_parser() = default; + + bool null() + { + handle_value(nullptr); + return true; + } + + bool boolean(bool val) + { + handle_value(val); + return true; + } + + bool number_integer(number_integer_t val) + { + handle_value(val); + return true; + } + + bool number_unsigned(number_unsigned_t val) + { + handle_value(val); + return true; + } + + bool number_float(number_float_t val, const string_t& /*unused*/) + { + handle_value(val); + return true; + } + + bool string(string_t& val) + { + // json_sax documents that the passed value may be moved from, + // so hand the buffer over instead of copying it + handle_value(std::move(val)); + return true; + } + + bool binary(binary_t& val) + { + // json_sax documents that the passed value may be moved from, + // so hand the buffer over instead of copying it + handle_value(std::move(val)); + return true; + } + bool start_object(std::size_t len) { ref_stack.push_back(handle_value(BasicJsonType::value_t::object)); #if JSON_DIAGNOSTIC_POSITIONS - // Manually set the start position of the object here. // Ensure this is after the call to handle_value to ensure correct start position. - if (m_lexer_ref) - { - // Lexer has read the first character of the object, so - // subtract 1 from the position to get the correct start position. - ref_stack.back()->start_position = m_lexer_ref->get_position() - 1; - } + diagnostic_positions::set_container_start(*ref_stack.back(), m_lexer_ref); #endif - if (JSON_HEDLEY_UNLIKELY(len != detail::unknown_size() && len > ref_stack.back()->max_size())) - { - return parse_error(0, "", out_of_range::create(408, concat("excessive object size: ", std::to_string(len)), ref_stack.back())); - } - - return true; + return check_container_size(*this, len, "object", ref_stack.back()); } bool key(string_t& val) @@ -12799,8 +16605,9 @@ class json_sax_dom_parser JSON_ASSERT(!ref_stack.empty()); JSON_ASSERT(ref_stack.back()->is_object()); - // add null at the given key and store the reference for later - object_element = &(ref_stack.back()->m_data.m_value.object->operator[](val)); + // add null at the given key and store the reference for later; the + // key is moved, as the lexer starts the next token with reset() + object_element = &(ref_stack.back()->m_data.m_value.object->operator[](std::move(val))); return true; } @@ -12810,11 +16617,7 @@ class json_sax_dom_parser JSON_ASSERT(ref_stack.back()->is_object()); #if JSON_DIAGNOSTIC_POSITIONS - if (m_lexer_ref) - { - // Lexer's position is past the closing brace, so set that as the end position. - ref_stack.back()->end_position = m_lexer_ref->get_position(); - } + diagnostic_positions::set_container_end(*ref_stack.back(), m_lexer_ref); #endif ref_stack.back()->set_parents(); @@ -12827,17 +16630,13 @@ class json_sax_dom_parser ref_stack.push_back(handle_value(BasicJsonType::value_t::array)); #if JSON_DIAGNOSTIC_POSITIONS - // Manually set the start position of the array here. // Ensure this is after the call to handle_value to ensure correct start position. - if (m_lexer_ref) - { - ref_stack.back()->start_position = m_lexer_ref->get_position() - 1; - } + diagnostic_positions::set_container_start(*ref_stack.back(), m_lexer_ref); #endif - if (JSON_HEDLEY_UNLIKELY(len != detail::unknown_size() && len > ref_stack.back()->max_size())) + if (JSON_HEDLEY_UNLIKELY(!check_container_size(*this, len, "array", ref_stack.back()))) { - return parse_error(0, "", out_of_range::create(408, concat("excessive array size: ", std::to_string(len)), ref_stack.back())); + return false; } if (len != detail::unknown_size()) @@ -12854,11 +16653,7 @@ class json_sax_dom_parser JSON_ASSERT(ref_stack.back()->is_array()); #if JSON_DIAGNOSTIC_POSITIONS - if (m_lexer_ref) - { - // Lexer's position is past the closing bracket, so set that as the end position. - ref_stack.back()->end_position = m_lexer_ref->get_position(); - } + diagnostic_positions::set_container_end(*ref_stack.back(), m_lexer_ref); #endif ref_stack.back()->set_parents(); @@ -12866,24 +16661,6 @@ class json_sax_dom_parser return true; } - template - bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, - const Exception& ex) - { - errored = true; - static_cast(ex); - if (allow_exceptions) - { - JSON_THROW(ex); - } - return false; - } - - constexpr bool is_errored() const - { - return errored; - } - private: /*! @@ -12937,16 +16714,12 @@ class json_sax_dom_parser std::vector ref_stack {}; /// helper to hold the reference for the next object element BasicJsonType* object_element = nullptr; - /// whether a syntax error occurred - bool errored = false; - /// whether to throw exceptions in case of errors - const bool allow_exceptions = true; /// the lexer reference to obtain the current position lexer_t* m_lexer_ref = nullptr; }; template -class json_sax_dom_callback_parser +class json_sax_dom_callback_parser : public json_sax_dom_error_state { public: using number_integer_t = typename BasicJsonType::number_integer_t; @@ -12962,7 +16735,7 @@ class json_sax_dom_callback_parser parser_callback_t cb, const bool allow_exceptions_ = true, lexer_t* lexer_ = nullptr) - : root(r), callback(std::move(cb)), allow_exceptions(allow_exceptions_), m_lexer_ref(lexer_) + : json_sax_dom_error_state(allow_exceptions_), root(r), callback(std::move(cb)), m_lexer_ref(lexer_) { keep_stack.push_back(true); } @@ -13038,21 +16811,11 @@ class json_sax_dom_callback_parser { #if JSON_DIAGNOSTIC_POSITIONS - // Manually set the start position of the object here. // Ensure this is after the call to handle_value to ensure correct start position. - if (m_lexer_ref) - { - // Lexer has read the first character of the object, so - // subtract 1 from the position to get the correct start position. - ref_stack.back()->start_position = m_lexer_ref->get_position() - 1; - } + diagnostic_positions::set_container_start(*ref_stack.back(), m_lexer_ref); #endif - // check object limit - if (JSON_HEDLEY_UNLIKELY(len != detail::unknown_size() && len > ref_stack.back()->max_size())) - { - return parse_error(0, "", out_of_range::create(408, concat("excessive object size: ", std::to_string(len)), ref_stack.back())); - } + return check_container_size(*this, len, "object", ref_stack.back()); } return true; } @@ -13065,16 +16828,13 @@ class json_sax_dom_callback_parser // handle_value() without touching the key stacks if (keep_stack.back()) { - BasicJsonType k = BasicJsonType(val); - static_cast(callback(static_cast(ref_stack.size()), parse_event_t::key, k)); + static_cast(callback(static_cast(ref_stack.size()), parse_event_t::key, key_value(val))); } return true; } - BasicJsonType k = BasicJsonType(val); - // check callback for the key - const bool keep = callback(static_cast(ref_stack.size()), parse_event_t::key, k); + const bool keep = callback(static_cast(ref_stack.size()), parse_event_t::key, key_value(val)); key_keep_stack.push_back(keep); // remember the key so a rejected value can be erased without searching // the object for it (kept in lockstep with key_keep_stack) @@ -13084,16 +16844,19 @@ class json_sax_dom_callback_parser if (keep && ref_stack.back()) { auto& obj = *ref_stack.back()->m_data.m_value.object; - const auto it = obj.find(val); - if (it != obj.end()) + // a single lookup: insert the placeholder, or find the existing member + const auto inserted = obj.emplace(std::move(val), discarded); + const auto it = inserted.first; + if (!inserted.second) { // this is a duplicate key (legal in JSON); remember its // current value so it can be restored later if the new // value is rejected by the callback, instead of being // erased together with the discarded placeholder duplicate_key_stash.emplace_back(&(it->second), it->second); + it->second = discarded; } - object_element = &(obj[val] = discarded); + object_element = &(it->second); } return true; @@ -13122,11 +16885,7 @@ class json_sax_dom_callback_parser { #if JSON_DIAGNOSTIC_POSITIONS - if (m_lexer_ref) - { - // Lexer's position is past the closing brace, so set that as the end position. - ref_stack.back()->end_position = m_lexer_ref->get_position(); - } + diagnostic_positions::set_container_end(*ref_stack.back(), m_lexer_ref); #endif ref_stack.back()->set_parents(); @@ -13137,13 +16896,7 @@ class json_sax_dom_callback_parser } } - JSON_ASSERT(!ref_stack.empty()); - JSON_ASSERT(!keep_stack.empty()); - JSON_ASSERT(!container_key_stack.empty()); - ref_stack.pop_back(); - keep_stack.pop_back(); - const string_t object_key = std::move(container_key_stack.back()); - container_key_stack.pop_back(); + const string_t object_key = pop_container(); if (!ref_stack.empty() && ref_stack.back() && ref_stack.back()->is_structured()) { @@ -13169,20 +16922,13 @@ class json_sax_dom_callback_parser { #if JSON_DIAGNOSTIC_POSITIONS - // Manually set the start position of the array here. // Ensure this is after the call to handle_value to ensure correct start position. - if (m_lexer_ref) - { - // Lexer has read the first character of the array, so - // subtract 1 from the position to get the correct start position. - ref_stack.back()->start_position = m_lexer_ref->get_position() - 1; - } + diagnostic_positions::set_container_start(*ref_stack.back(), m_lexer_ref); #endif - // check array limit - if (JSON_HEDLEY_UNLIKELY(len != detail::unknown_size() && len > ref_stack.back()->max_size())) + if (JSON_HEDLEY_UNLIKELY(!check_container_size(*this, len, "array", ref_stack.back()))) { - return parse_error(0, "", out_of_range::create(408, concat("excessive array size: ", std::to_string(len)), ref_stack.back())); + return false; } if (len != detail::unknown_size()) @@ -13206,11 +16952,7 @@ class json_sax_dom_callback_parser { #if JSON_DIAGNOSTIC_POSITIONS - if (m_lexer_ref) - { - // Lexer's position is past the closing bracket, so set that as the end position. - ref_stack.back()->end_position = m_lexer_ref->get_position(); - } + diagnostic_positions::set_container_end(*ref_stack.back(), m_lexer_ref); #endif ref_stack.back()->set_parents(); @@ -13236,13 +16978,7 @@ class json_sax_dom_callback_parser } } - JSON_ASSERT(!ref_stack.empty()); - JSON_ASSERT(!keep_stack.empty()); - JSON_ASSERT(!container_key_stack.empty()); - ref_stack.pop_back(); - keep_stack.pop_back(); - const string_t object_key = std::move(container_key_stack.back()); - container_key_stack.pop_back(); + const string_t object_key = pop_container(); // remove discarded value if (!ref_stack.empty() && ref_stack.back()) @@ -13263,26 +16999,33 @@ class json_sax_dom_callback_parser return true; } - template - bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, - const Exception& ex) - { - errored = true; - static_cast(ex); - if (allow_exceptions) - { - JSON_THROW(ex); - } - return false; - } - - constexpr bool is_errored() const - { - return errored; - } - private: + /*! + @brief the value of a key event for the callback + + A string value of its own for every key would allocate it every time, so + one is kept and set to the key. A callback that changed or moved it gets a + new one, as it would have got a new value for every key anyway. + */ + BasicJsonType& key_value(const string_t& key) + { +#if JSON_DIAGNOSTIC_POSITIONS + // a callback may have left positions on the value + key_scratch = BasicJsonType(key); +#else + if (JSON_HEDLEY_LIKELY(key_scratch.is_string())) + { + *key_scratch.m_data.m_value.string = key; + } + else + { + key_scratch = BasicJsonType(key); + } +#endif + return key_scratch; + } + /// if there is a pending duplicate-key stash entry for this exact slot, /// remove it from the stash; if restore_value is true, the stashed /// previous value is moved back into the slot first (use this when the @@ -13329,6 +17072,23 @@ class json_sax_dom_callback_parser return string_t{}; } + /*! + @brief leave the object or array that end_object()/end_array() just closed + @return the key the container is stored under in its parent (see + container_key_stack) + */ + string_t pop_container() + { + JSON_ASSERT(!ref_stack.empty()); + JSON_ASSERT(!keep_stack.empty()); + JSON_ASSERT(!container_key_stack.empty()); + ref_stack.pop_back(); + keep_stack.pop_back(); + string_t object_key = std::move(container_key_stack.back()); + container_key_stack.pop_back(); + return object_key; + } + /*! @brief remove the discarded value the callback rejected from its parent, unless it is a duplicate key's slot with a stashed previous value, in @@ -13505,14 +17265,12 @@ class json_sax_dom_callback_parser /// value is later rejected by the callback, instead of erasing the /// member entirely std::vector> duplicate_key_stash {}; - /// whether a syntax error occurred - bool errored = false; /// callback function const parser_callback_t callback = nullptr; - /// whether to throw exceptions in case of errors - const bool allow_exceptions = true; /// a discarded value for the callback BasicJsonType discarded = BasicJsonType::value_t::discarded; + /// the value of the key event for the callback (see key_value()) + BasicJsonType key_scratch {}; /// the lexer reference to obtain the current position lexer_t* m_lexer_ref = nullptr; }; @@ -13795,7 +17553,7 @@ JSON_INLINE_VARIABLE constexpr std::size_t max_valueless_container_size = 1 << 2 /*! @brief deserialization of BJData, BON8, BSON, CBOR, MessagePack, and UBJSON values */ -template> +template> class binary_reader { using number_integer_t = typename BasicJsonType::number_integer_t; @@ -13904,8 +17662,7 @@ class binary_reader if (JSON_HEDLEY_UNLIKELY(current != char_traits::eof())) { - return sax->parse_error(chars_read, get_token_string(), parse_error::create(110, chars_read, - exception_message(concat("expected end of input; last byte: 0x", get_token_string()), "value"), nullptr)); + return last_byte_error(exception_id::unexpected_end_of_input, "expected end of input", "value", ""); } } @@ -14003,6 +17760,40 @@ class binary_reader return is_object ? sax->end_object() : sax->end_array(); } + /*! + @brief close the innermost open container and report whether that + completed the value being read + + The value begun by the driver is complete once its container is not + inside another one. + + @param[out] value_complete whether no container is left open + @return whether the SAX parser accepted the end event + */ + bool leave_container_and_check(bool& value_complete) + { + if (JSON_HEDLEY_UNLIKELY(!leave_container())) + { + return false; + } + value_complete = container_stack.empty(); + return true; + } + + /*! + @brief read an object key and pass it to the SAX parser + + @tparam GetKey member function that reads the key into a string + @param[in,out] key string the key is read into; cleared first + @return whether reading the key and the SAX event succeeded + */ + template + bool read_object_key(string_t& key) + { + key.clear(); + return (this->*GetKey)(key) && sax->key(key); + } + ////////// // BSON // ////////// @@ -14027,8 +17818,7 @@ class binary_reader { if (JSON_HEDLEY_UNLIKELY(document_size < 0 || static_cast(document_size) != chars_read - document_start)) { - return sax->parse_error(chars_read, get_token_string(), parse_error::create(112, chars_read, - exception_message(concat("document size ", std::to_string(document_size), " does not match the number of bytes read (", std::to_string(chars_read - document_start), ")"), "document"), nullptr)); + return bson_document_size_error(document_size, chars_read - document_start); } return true; } @@ -14220,9 +18010,7 @@ class binary_reader { if (JSON_HEDLEY_UNLIKELY(len < 1)) { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message(concat("string length must be at least 1, is ", std::to_string(len)), "string"), nullptr)); + return bson_length_error("string length must be at least 1, is ", len, "string"); } if (JSON_HEDLEY_UNLIKELY(!get_string(len - static_cast(1), result))) @@ -14232,10 +18020,7 @@ class binary_reader if (JSON_HEDLEY_UNLIKELY(get() != 0x00)) { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message("BSON string is not null-terminated", - "string"), nullptr)); + return last_byte_error(exception_id::unexpected_byte, "BSON string is not null-terminated", "string"); } return check_string_utf8(result, "string"); @@ -14255,9 +18040,7 @@ class binary_reader { if (JSON_HEDLEY_UNLIKELY(len < 0)) { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message(concat("byte array length cannot be negative, is ", std::to_string(len)), "binary"), nullptr)); + return bson_length_error("byte array length cannot be negative, is ", len, "binary"); } // All BSON binary values have a subtype @@ -14347,11 +18130,7 @@ class binary_reader default: // anything else is not supported (yet) { - std::array cr{{}}; - static_cast((std::snprintf)(cr.data(), cr.size(), "%.2hhX", static_cast(element_type))); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) - const std::string cr_str{cr.data()}; - return sax->parse_error(element_type_parse_position, cr_str, - parse_error::create(114, element_type_parse_position, concat("Unsupported BSON record type 0x", cr_str), nullptr)); + return bson_unsupported_type_error(element_type_parse_position, element_type); } } } @@ -14675,9 +18454,7 @@ class binary_reader { if (tag_handler == cbor_tag_handler_t::error) { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message(concat("invalid byte: 0x", last_token), "value"), nullptr)); + return unexpected_byte("invalid byte", "value"); } // ignore and store: the tag value is already in the head, so @@ -14697,9 +18474,7 @@ class binary_reader { case cbor_tag_handler_t::error: { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message(concat("invalid byte: 0x", last_token), "value"), nullptr)); + return unexpected_byte("invalid byte", "value"); } case cbor_tag_handler_t::ignore: @@ -14773,9 +18548,7 @@ class binary_reader default: // anything else (0xFF is handled inside the other types) { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message(concat("invalid byte: 0x", last_token), "value"), nullptr)); + return unexpected_byte("invalid byte", "value"); } } } @@ -14786,12 +18559,38 @@ class binary_reader @param[in] context parsing context for the error message @return whether the SAX consumer accepts the parse error */ + JSON_HEDLEY_NEVER_INLINE bool cbor_indefinite_string_error(const char* type_name, const char* context) { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, - exception_message(concat("indefinite-length ", type_name, - " is not allowed inside indefinite-length ", type_name, "; last byte: 0x", last_token), context), nullptr)); + return last_byte_error(exception_id::invalid_string_or_size, concat("indefinite-length ", type_name, " is not allowed inside indefinite-length ", type_name, "; last byte: 0x", get_token_string()), context); + } + + /*! + @brief report an object key that is not a string + + @param[in] found description of what was found instead of a string key + @return false + */ + bool unsupported_key_type(const char* found) const + { + return last_byte_error(exception_id::invalid_string_or_size, "only string keys are supported, but found ", "object key", found); + } + + /*! + @brief read the length of a CBOR string or byte array and then its bytes + + @tparam NumberType the type of the length field + @tparam ContainerType the destination container (string_t or binary_t) + @param[in] context further context information (for diagnostics) + @param[out] result container the bytes are appended to + + @return whether reading completed + */ + template + bool get_cbor_sized_bytes(const char* context, ContainerType& result) + { + NumberType len{}; + return get_number(len) && get_bytes(len, context, result); } /*! @@ -14844,33 +18643,27 @@ class binary_reader case 0x78: // UTF-8 string (one-byte uint8_t for n follows) { - std::uint8_t len{}; - return get_number(len) && get_string(len, result); + return get_cbor_sized_bytes("string", result); } case 0x79: // UTF-8 string (two-byte uint16_t for n follow) { - std::uint16_t len{}; - return get_number(len) && get_string(len, result); + return get_cbor_sized_bytes("string", result); } case 0x7A: // UTF-8 string (four-byte uint32_t for n follow) { - std::uint32_t len{}; - return get_number(len) && get_string(len, result); + return get_cbor_sized_bytes("string", result); } case 0x7B: // UTF-8 string (eight-byte uint64_t for n follow) { - std::uint64_t len{}; - return get_number(len) && get_string(len, result); + return get_cbor_sized_bytes("string", result); } default: { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, - exception_message(concat("expected length specification (0x60-0x7B)", inside_indefinite ? "" : " or indefinite string type (0x7F)", "; last byte: 0x", last_token), "string"), nullptr)); + return last_byte_error(exception_id::invalid_string_or_size, "expected length specification (0x60-0x7B)", "string", inside_indefinite ? "" : " or indefinite string type (0x7F)"); } } } @@ -15000,9 +18793,7 @@ class binary_reader break; } - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, - exception_message(concat("only string keys are supported, but found ", found, "; last byte: 0x", last_token), "object key"), nullptr)); + return unsupported_key_type(found); } /*! @@ -15055,37 +18846,27 @@ class binary_reader case 0x58: // Binary data (one-byte uint8_t for n follows) { - std::uint8_t len{}; - return get_number(len) && - get_binary(len, result); + return get_cbor_sized_bytes("binary", result); } case 0x59: // Binary data (two-byte uint16_t for n follow) { - std::uint16_t len{}; - return get_number(len) && - get_binary(len, result); + return get_cbor_sized_bytes("binary", result); } case 0x5A: // Binary data (four-byte uint32_t for n follow) { - std::uint32_t len{}; - return get_number(len) && - get_binary(len, result); + return get_cbor_sized_bytes("binary", result); } case 0x5B: // Binary data (eight-byte uint64_t for n follow) { - std::uint64_t len{}; - return get_number(len) && - get_binary(len, result); + return get_cbor_sized_bytes("binary", result); } default: { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, - exception_message(concat("expected length specification (0x40-0x5B)", inside_indefinite ? "" : " or indefinite binary array type (0x5F)", "; last byte: 0x", last_token), "binary"), nullptr)); + return last_byte_error(exception_id::invalid_string_or_size, "expected length specification (0x40-0x5B)", "binary", inside_indefinite ? "" : " or indefinite binary array type (0x5F)"); } } } @@ -15231,8 +19012,7 @@ class binary_reader { if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(len) || len == detail::unknown_size())) { - return sax->parse_error(chars_read, get_token_string(), out_of_range::create(408, - exception_message(concat("excessive ", context, " size"), "size"), nullptr)); + return container_size_error(context); } result = conditional_static_cast(len); return true; @@ -15296,12 +19076,12 @@ class binary_reader if (at_end) { - if (JSON_HEDLEY_UNLIKELY(!leave_container())) + bool value_complete = false; + if (JSON_HEDLEY_UNLIKELY(!leave_container_and_check(value_complete))) { return false; } - // the value begun here is complete once its container is - if (container_stack.empty()) + if (value_complete) { return true; } @@ -15310,8 +19090,7 @@ class binary_reader if (top.is_object) { - key.clear(); - if (JSON_HEDLEY_UNLIKELY(!get_cbor_object_key(key) || !sax->key(key))) + if (JSON_HEDLEY_UNLIKELY(!read_object_key<&binary_reader::get_cbor_object_key>(key))) { return false; } @@ -15719,9 +19498,7 @@ class binary_reader default: // anything else { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message(concat("invalid byte: 0x", last_token), "value"), nullptr)); + return unexpected_byte("invalid byte", "value"); } } } @@ -15784,27 +19561,22 @@ class binary_reader case 0xD9: // str 8 { - std::uint8_t len{}; - return get_number(len) && get_string(len, result) && check_string_utf8(result, context); + return get_string_with_length(result, context); } case 0xDA: // str 16 { - std::uint16_t len{}; - return get_number(len) && get_string(len, result) && check_string_utf8(result, context); + return get_string_with_length(result, context); } case 0xDB: // str 32 { - std::uint32_t len{}; - return get_number(len) && get_string(len, result) && check_string_utf8(result, context); + return get_string_with_length(result, context); } default: { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, - exception_message(concat("expected length specification (0xA0-0xBF, 0xD9-0xDB); last byte: 0x", last_token), "string"), nullptr)); + return last_byte_error(exception_id::invalid_string_or_size, "expected length specification (0xA0-0xBF, 0xD9-0xDB)", "string", ""); } } } @@ -15896,9 +19668,7 @@ class binary_reader break; } - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, - exception_message(concat("only string keys are supported, but found ", found, "; last byte: 0x", last_token), "object key"), nullptr)); + return unsupported_key_type(found); } /*! @@ -16043,12 +19813,12 @@ class binary_reader if (container_stack.back().remaining == 0) { - if (JSON_HEDLEY_UNLIKELY(!leave_container())) + bool value_complete = false; + if (JSON_HEDLEY_UNLIKELY(!leave_container_and_check(value_complete))) { return false; } - // the value begun here is complete once its container is - if (container_stack.empty()) + if (value_complete) { return true; } @@ -16061,8 +19831,7 @@ class binary_reader if (is_object) { get(); - key.clear(); - if (JSON_HEDLEY_UNLIKELY(!get_msgpack_object_key(key) || !sax->key(key))) + if (JSON_HEDLEY_UNLIKELY(!read_object_key<&binary_reader::get_msgpack_object_key>(key))) { return false; } @@ -16207,8 +19976,7 @@ class binary_reader { if (JSON_HEDLEY_UNLIKELY(len < 0)) { - return sax->parse_error(chars_read, get_token_string(), parse_error::create(113, chars_read, - exception_message("string length must not be negative", "string"), nullptr)); + return last_byte_error(exception_id::invalid_string_or_size, "string length must not be negative", "string"); } return true; } @@ -16247,8 +20015,7 @@ class binary_reader { case 'U': { - std::uint8_t len{}; - return get_number(len) && get_string(len, result) && check_string_utf8(result, context); + return get_string_with_length(result, context); } case 'i': @@ -16281,8 +20048,7 @@ class binary_reader { break; } - std::uint16_t len{}; - return get_number(len) && get_string(len, result) && check_string_utf8(result, context); + return get_string_with_length(result, context); } case 'm': @@ -16291,8 +20057,7 @@ class binary_reader { break; } - std::uint32_t len{}; - return get_number(len) && get_string(len, result) && check_string_utf8(result, context); + return get_string_with_length(result, context); } case 'M': @@ -16301,25 +20066,13 @@ class binary_reader { break; } - std::uint64_t len{}; - return get_number(len) && get_string(len, result) && check_string_utf8(result, context); + return get_string_with_length(result, context); } default: break; } - auto last_token = get_token_string(); - std::string message; - - if (input_format != input_format_t::bjdata) - { - message = "expected length type specification (U, i, I, l, L); last byte: 0x" + last_token; - } - else - { - message = "expected length type specification (U, i, u, I, m, l, M, L); last byte: 0x" + last_token; - } - return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, exception_message(message, "string"), nullptr)); + return length_type_error("", "string"); } /*! @@ -16404,13 +20157,11 @@ class binary_reader } if (JSON_HEDLEY_UNLIKELY(number < 0)) { - return sax->parse_error(chars_read, get_token_string(), parse_error::create(113, chars_read, - exception_message("count in an optimized container must be positive", "size"), nullptr)); + return last_byte_error(exception_id::invalid_string_or_size, "count in an optimized container must be positive", "size"); } if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(number))) { - return sax->parse_error(chars_read, get_token_string(), out_of_range::create(408, - exception_message("integer value overflow", "size"), nullptr)); + return out_of_range_error(exception_id::container_too_large, "integer value overflow", "size"); } result = static_cast(number); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): number is not a char return true; @@ -16507,8 +20258,7 @@ class binary_reader } if (!value_in_range_of(number)) { - return sax->parse_error(chars_read, get_token_string(), out_of_range::create(408, - exception_message("integer value overflow", "size"), nullptr)); + return out_of_range_error(exception_id::container_too_large, "integer value overflow", "size"); } result = detail::conditional_static_cast(number); return true; @@ -16522,7 +20272,7 @@ class binary_reader } if (is_ndarray) // ndarray dimensional vector can only contain integers and cannot embed another array { - return sax->parse_error(chars_read, get_token_string(), parse_error::create(113, chars_read, exception_message("ndarray dimensional vector is not allowed", "size"), nullptr)); + return last_byte_error(exception_id::invalid_string_or_size, "ndarray dimensional vector is not allowed", "size"); } std::vector dim; if (JSON_HEDLEY_UNLIKELY(!get_ubjson_ndarray_size(dim))) @@ -16558,9 +20308,7 @@ class binary_reader const char* type_name = bjd_type_name(ndarray_dtype); if (JSON_HEDLEY_UNLIKELY(type_name == nullptr)) { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message("invalid byte: 0x" + last_token, "type"), nullptr)); + return unexpected_byte("invalid byte", "type"); } string_t type_key = "_ArrayType_"; @@ -16588,7 +20336,7 @@ class binary_reader // or SIZE_MAX. if (JSON_HEDLEY_UNLIKELY(result > (std::numeric_limits::max)() / i)) { - return sax->parse_error(chars_read, get_token_string(), out_of_range::create(408, exception_message("excessive ndarray size caused overflow", "size"), nullptr)); + return out_of_range_error(exception_id::container_too_large, "excessive ndarray size caused overflow", "size"); } result *= i; // the pre-check above already rules out result becoming 0 @@ -16597,7 +20345,7 @@ class binary_reader // unknown-size container (see get_ubjson_size_type()) if (result == npos) { - return sax->parse_error(chars_read, get_token_string(), out_of_range::create(408, exception_message("excessive ndarray size caused overflow", "size"), nullptr)); + return out_of_range_error(exception_id::container_too_large, "excessive ndarray size caused overflow", "size"); } if (JSON_HEDLEY_UNLIKELY(!emit_unsigned(i))) { @@ -16614,18 +20362,7 @@ class binary_reader default: break; } - auto last_token = get_token_string(); - std::string message; - - if (input_format != input_format_t::bjdata) - { - message = "expected length type specification (U, i, I, l, L) after '#'; last byte: 0x" + last_token; - } - else - { - message = "expected length type specification (U, i, u, I, m, l, M, L) after '#'; last byte: 0x" + last_token; - } - return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, exception_message(message, "size"), nullptr)); + return length_type_error(" after '#'", "size"); } /*! @@ -16658,9 +20395,7 @@ class binary_reader if (input_format == input_format_t::bjdata && JSON_HEDLEY_UNLIKELY(is_bjd_excluded_optimized_type(result.second))) { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message(concat("marker 0x", last_token, " is not a permitted optimized array type"), "type"), nullptr)); + return optimized_type_error(); } if (JSON_HEDLEY_UNLIKELY(!unexpect_eof("type"))) @@ -16675,9 +20410,7 @@ class binary_reader { return false; } - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message(concat("expected '#' after type information; last byte: 0x", last_token), "size"), nullptr)); + return last_byte_error(exception_id::unexpected_byte, "expected '#' after type information", "size", ""); } const bool is_error = get_ubjson_size_value(result.first, is_ndarray, 0, result.second); @@ -16696,8 +20429,7 @@ class binary_reader const bool is_error = get_ubjson_size_value(result.first, is_ndarray); if (input_format == input_format_t::bjdata && is_ndarray && !inside_ndarray) { - return sax->parse_error(chars_read, get_token_string(), parse_error::create(112, chars_read, - exception_message("ndarray requires both type and size", "size"), nullptr)); + return last_byte_error(exception_id::unexpected_byte, "ndarray requires both type and size", "size"); } return is_error; } @@ -16829,9 +20561,7 @@ class binary_reader } if (JSON_HEDLEY_UNLIKELY(current > 127)) { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, - exception_message(concat("byte after 'C' must be in range 0x00..0x7F; last byte: 0x", last_token), "char"), nullptr)); + return last_byte_error(exception_id::invalid_string_or_size, "byte after 'C' must be in range 0x00..0x7F", "char", ""); } string_t s(1, static_cast(current)); return sax->string(s); @@ -16852,8 +20582,7 @@ class binary_reader default: // anything else break; } - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, exception_message("invalid byte: 0x" + last_token, "value"), nullptr)); + return unexpected_byte("invalid byte", "value"); } /*! @@ -16916,8 +20645,7 @@ class binary_reader if (JSON_HEDLEY_UNLIKELY((size_and_type.second == 'Z' || size_and_type.second == 'T' || size_and_type.second == 'F') && size_and_type.first > max_valueless_container_size)) { - return sax->parse_error(chars_read, get_token_string(), out_of_range::create(408, - exception_message("excessive array size", "size"), nullptr)); + return out_of_range_error(exception_id::container_too_large, "excessive array size", "size"); } if (JSON_HEDLEY_UNLIKELY(!enter_array(size_and_type.first, size_and_type.second))) @@ -16952,9 +20680,7 @@ class binary_reader // do not accept ND-array size in objects in BJData if (input_format == input_format_t::bjdata && size_and_type.first != npos && (size_and_type.second & (1 << 8)) != 0) { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message("BJData object does not support ND-array size in optimized format", "object"), nullptr)); + return last_byte_error(exception_id::unexpected_byte, "BJData object does not support ND-array size in optimized format", "object"); } if (size_and_type.first != npos) @@ -16991,7 +20717,7 @@ class binary_reader // the lexer would stop at a NUL and accept the digits before it if (JSON_HEDLEY_UNLIKELY(current == '\0')) { - return sax->parse_error(chars_read, "00", parse_error::create(115, chars_read, + return sax->parse_error(chars_read, "00", parse_error::create(exception_id::invalid_high_precision_number, chars_read, exception_message("invalid number text; last byte: 0x00", "high-precision number"), nullptr)); } number_vector.push_back(static_cast(current)); @@ -17008,7 +20734,7 @@ class binary_reader if (JSON_HEDLEY_UNLIKELY(result_remainder != token_type::end_of_input)) { - return sax->parse_error(chars_read, number_string, parse_error::create(115, chars_read, + return sax->parse_error(chars_read, number_string, parse_error::create(exception_id::invalid_high_precision_number, chars_read, exception_message(concat("invalid number text: ", number_lexer.get_token_string()), "high-precision number"), nullptr)); } @@ -17026,7 +20752,7 @@ class binary_reader return sax->parse_error( chars_read, number_string, - out_of_range::create(406, concat("number overflow parsing '", number_string, '\''), nullptr)); + out_of_range::create(exception_id::number_overflow, concat("number overflow parsing '", number_string, '\''), nullptr)); } // number_string is a std::string, while the SAX interface takes a // string_t; convert explicitly, as the two are only implicitly @@ -17048,7 +20774,7 @@ class binary_reader case token_type::end_of_input: case token_type::literal_or_value: default: - return sax->parse_error(chars_read, number_string, parse_error::create(115, chars_read, + return sax->parse_error(chars_read, number_string, parse_error::create(exception_id::invalid_high_precision_number, chars_read, exception_message(concat("invalid number text: ", number_lexer.get_token_string()), "high-precision number"), nullptr)); } } @@ -17107,20 +20833,6 @@ class binary_reader return 0x80 <= c && c <= 0xBF; } - /*! - @brief report a parse error at the last read byte - - @param[in] detail a detailed error message - @param[in] context further context information - @return false - */ - bool bon8_error(const std::string& detail, const char* context) - { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, - exception_message(concat(detail, ": 0x", last_token), context), nullptr)); - } - /*! @brief read a BON8 value and everything nested inside it @@ -17170,12 +20882,12 @@ class binary_reader if (at_end) { - if (JSON_HEDLEY_UNLIKELY(!leave_container())) + bool value_complete = false; + if (JSON_HEDLEY_UNLIKELY(!leave_container_and_check(value_complete))) { return false; } - // the value begun here is complete once its container is - if (container_stack.empty()) + if (value_complete) { return true; } @@ -17184,8 +20896,7 @@ class binary_reader if (top.is_object) { - key.clear(); - if (JSON_HEDLEY_UNLIKELY(!get_bon8_key(key) || !sax->key(key))) + if (JSON_HEDLEY_UNLIKELY(!read_object_key<&binary_reader::get_bon8_key>(key))) { return false; } @@ -17336,7 +21047,7 @@ class binary_reader } // 0xFE: end of container where a value is expected - return bon8_error("invalid byte", "value"); + return unexpected_byte("invalid byte", "value"); } /*! @@ -17463,7 +21174,7 @@ class binary_reader current = byte; } - return bon8_error("expected a string; last byte", "key"); + return unexpected_byte("expected a string; last byte", "key"); } /*! @@ -17586,7 +21297,7 @@ class binary_reader if (JSON_HEDLEY_UNLIKELY(!valid_second)) { - return bon8_error("invalid UTF-8 byte", "string"); + return unexpected_byte("invalid UTF-8 byte", "string"); } result.push_back(static_cast(byte)); @@ -17600,7 +21311,7 @@ class binary_reader } if (JSON_HEDLEY_UNLIKELY(!is_bon8_continuation(current))) { - return bon8_error("invalid UTF-8 byte", "string"); + return unexpected_byte("invalid UTF-8 byte", "string"); } result.push_back(static_cast(current)); } @@ -17645,7 +21356,7 @@ class binary_reader { // in case of failure, advance position by 1 to report the failing location ++chars_read; - sax->parse_error(chars_read, "", parse_error::create(110, chars_read, exception_message("unexpected end of input", context), nullptr)); + static_cast(report_eof(context)); return false; } return true; @@ -17798,8 +21509,7 @@ class binary_reader const auto result = static_cast(number); if (JSON_HEDLEY_UNLIKELY(std::isfinite(number) && !std::isfinite(result))) { - return sax->parse_error(chars_read, get_token_string(), - out_of_range::create(406, exception_message("number overflow", "value"), nullptr)); + return out_of_range_error(exception_id::number_overflow, "number overflow", "value"); } return sax->number_float(result, ""); } @@ -17919,15 +21629,29 @@ class binary_reader if (error_handler == error_handler_t::strict) { - auto last_token = get_token_string(); - return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, - exception_message("invalid string: ill-formed UTF-8 byte", context), nullptr)); + return last_byte_error(exception_id::invalid_string_or_size, "invalid string: ill-formed UTF-8 byte", context); } result = sanitize_utf8(result, error_handler); return true; } + /*! + @brief read a length of type @a NumberType, then a string of that many bytes + + @tparam NumberType the type of the length field + @param[out] result string the bytes are appended to + @param[in] context further context information (for diagnostics) + + @return whether reading and validating the string succeeded + */ + template + bool get_string_with_length(string_t& result, const char* context) + { + NumberType len{}; + return get_number(len) && get_string(len, result) && check_string_utf8(result, context); + } + /*! @brief create a byte array by reading bytes from the input @@ -18016,20 +21740,116 @@ class binary_reader { if (JSON_HEDLEY_UNLIKELY(current == char_traits::eof())) { - return sax->parse_error(chars_read, "", - parse_error::create(110, chars_read, exception_message("unexpected end of input", context), nullptr)); + return report_eof(context); } return true; } + /*! + @brief reports an unexpected end of input (out of line to keep the exception + construction out of the hot reading functions) + @param[in] context further context information (for diagnostics) + @return the result of the SAX parser's parse_error() + */ + JSON_HEDLEY_NON_NULL(2) + JSON_HEDLEY_NEVER_INLINE + bool report_eof(const char* context) const + { + return sax->parse_error(chars_read, "", + parse_error::create(exception_id::unexpected_end_of_input, chars_read, exception_message("unexpected end of input", context), nullptr)); + } + + /*! + @brief reports an out_of_range error at the last read byte (out of line) + */ + JSON_HEDLEY_NEVER_INLINE + bool out_of_range_error(const exception_id id_, const char* detail, const char* context) const + { + return sax->parse_error(chars_read, get_token_string(), out_of_range::create(id_, exception_message(detail, context), nullptr)); + } + + JSON_HEDLEY_NEVER_INLINE + bool container_size_error(const char* what) const + { + return sax->parse_error(chars_read, get_token_string(), out_of_range::create(exception_id::container_too_large, + exception_message(concat("excessive ", what, " size"), "size"), nullptr)); + } + + JSON_HEDLEY_NEVER_INLINE + bool optimized_type_error() const + { + return last_byte_error(exception_id::unexpected_byte, concat("marker 0x", get_token_string(), " is not a permitted optimized array type"), "type"); + } + + template + JSON_HEDLEY_NEVER_INLINE + bool bson_length_error(const char* prefix, const NumberType len, const char* context) const + { + return last_byte_error(exception_id::unexpected_byte, concat(prefix, std::to_string(len)), context); + } + + JSON_HEDLEY_NEVER_INLINE + bool bson_document_size_error(const std::int32_t document_size, const std::size_t bytes_read) const + { + return last_byte_error(exception_id::unexpected_byte, concat("document size ", std::to_string(document_size), " does not match the number of bytes read (", std::to_string(bytes_read), ")"), "document"); + } + + JSON_HEDLEY_NEVER_INLINE + bool bson_unsupported_type_error(const std::size_t position, const char_int_type element_type) const + { + const std::string cr_str = hex_byte(static_cast(element_type)); + return sax->parse_error(position, cr_str, + parse_error::create(exception_id::bson_unsupported_type, position, concat("Unsupported BSON record type 0x", cr_str), nullptr)); + } + + /*! + @brief reports a parse error at the last read byte + @param[in] id_ the id of the parse_error exception + @param[in] detail a detailed error message + @param[in] context further context information + @param[in] tail optional text appended to the detail, followed by "; last byte: 0x" + @return the result of the SAX parser's parse_error() + */ + JSON_HEDLEY_NEVER_INLINE + bool last_byte_error(const exception_id id_, const std::string& detail, const char* context, const char* tail = nullptr) const + { + // with a tail, the message reads "; last byte: 0x" + return sax->parse_error(chars_read, get_token_string(), parse_error::create(id_, chars_read, + exception_message(tail == nullptr ? detail : concat(detail, tail, "; last byte: 0x", get_token_string()), context), nullptr)); + } + + /*! + @brief reports the last read byte as unexpected (parse_error.112) + @param[in] detail what is wrong with the byte, e.g. "invalid byte" + @param[in] context further context information + @return the result of the SAX parser's parse_error() + */ + JSON_HEDLEY_NEVER_INLINE + bool unexpected_byte(const char* detail, const char* context) const + { + return last_byte_error(exception_id::unexpected_byte, concat(detail, ": 0x", get_token_string()), context); + } + + /*! + @brief reports that the last read byte is not a UBJSON/BJData length type (parse_error.113) + @param[in] position where the length type was expected, e.g. " after '#'" + @param[in] context further context information + @return the result of the SAX parser's parse_error() + */ + JSON_HEDLEY_NEVER_INLINE + bool length_type_error(const char* position, const char* context) const + { + const char* types = input_format == input_format_t::bjdata ? "U, i, u, I, m, l, M, L" : "U, i, I, l, L"; + return last_byte_error(exception_id::invalid_string_or_size, + concat("expected length type specification (", types, ")", position, "; last byte: 0x", get_token_string()), context); + } + /*! @return a string representation of the last read byte */ std::string get_token_string() const { - std::array cr{{}}; - static_cast((std::snprintf)(cr.data(), cr.size(), "%.2hhX", static_cast(current))); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) - return std::string{cr.data()}; + return hex_byte(static_cast(current)); } /*! @@ -18041,37 +21861,7 @@ class binary_reader const std::string& context) const { std::string error_msg = "syntax error while parsing "; - - switch (input_format) - { - case input_format_t::cbor: - error_msg += "CBOR"; - break; - - case input_format_t::msgpack: - error_msg += "MessagePack"; - break; - - case input_format_t::ubjson: - error_msg += "UBJSON"; - break; - - case input_format_t::bson: - error_msg += "BSON"; - break; - - case input_format_t::bjdata: - error_msg += "BJData"; - break; - - case input_format_t::bon8: - error_msg += "BON8"; - break; - - case input_format_t::json: // LCOV_EXCL_LINE - default: // LCOV_EXCL_LINE - JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE - } + error_msg += input_format_name(input_format); return concat(error_msg, ' ', context, ": ", detail); } @@ -18241,6 +22031,20 @@ template using parser_callback_t = std::function; +/// whether a SAX handler ignores the text of float tokens (the library's own +/// handlers do), so that the parser need not materialize it +template +struct sax_ignores_number_text : std::false_type {}; + +template +struct sax_ignores_number_text> : std::true_type {}; + +template +struct sax_ignores_number_text> : std::true_type {}; + +template +struct sax_ignores_number_text> : std::true_type {}; + /*! @brief syntax analysis @@ -18307,7 +22111,7 @@ class parser } else { - json_sax_dom_parser sdp(result, allow_exceptions, &m_lexer); + json_sax_dom_parser sdp(result, allow_exceptions, &m_lexer); // in case of an error, return a discarded value if (!parse_dom(sdp, strict)) @@ -18341,28 +22145,63 @@ class parser if (result) { - if (strict) - { - // strict mode: next byte must be EOF - if (get_token() != token_type::end_of_input) - { - return sax->parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), exception_message(token_type::end_of_input, "value"), nullptr)); - } - } - else - { - // the caller keeps using the input: position it right after - // the value by leaving the character that terminated it - m_lexer.release_lookahead(); - } + return finish_input(*sax, strict); } - return result; + return false; } private: + /// the text of the current float token for @a sax (see sax_ignores_number_text) + template + const string_t& number_float_text(SAX* /*sax*/) + { + return number_float_text(std::integral_constant::value> {}); + } + + const string_t& number_float_text(std::true_type /*ignored*/) const noexcept + { + return m_lexer.get_ignored_number_text(); + } + + string_t& number_float_text(std::false_type /*ignored*/) + { + return m_lexer.get_string(); + } + + /*! + @brief apply the end-of-input handling after a value was parsed + + In strict mode, the next token must be the end of the input; otherwise + parse_error.101 is reported through @a sax. In non-strict mode, the + lookahead is released so the caller can keep reading the input right after + the parsed value. + + @param[in,out] sax the SAX parser to report an error to + @param[in] strict whether to expect the last token to be EOF + @return true, or what @a sax returned for the error + */ + template + bool finish_input(SAX& sax, const bool strict) + { + if (strict) + { + // strict mode: next byte must be EOF + if (get_token() != token_type::end_of_input) + { + return syntax_error(sax, exception_message(token_type::end_of_input, "value")); + } + } + else + { + // the caller keeps using the input: position it right after + // the value by leaving the character that terminated it + m_lexer.release_lookahead(); + } + + return true; + } + /*! @brief run a DOM SAX parser to completion and position the lexer @@ -18382,23 +22221,7 @@ class parser { sax_parse_internal(&sdp); - if (strict) - { - // in strict mode, input must be completely read - if (get_token() != token_type::end_of_input) - { - sdp.parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), - exception_message(token_type::end_of_input, "value"), nullptr)); - } - } - else - { - // the caller keeps using the input: position it right after - // the value by leaving the character that terminated it - m_lexer.release_lookahead(); - } + static_cast(finish_input(sdp, strict)); return !sdp.is_errored(); } @@ -18407,12 +22230,20 @@ class parser JSON_HEDLEY_NON_NULL(2) bool sax_parse_internal(SAX* sax) { - // stack to remember the hierarchy of structured values we are parsing - // true = array; false = object - std::vector states; // value to avoid a goto (see comment where set to true) bool skip_to_state_evaluation = false; + // stack to remember the hierarchy of structured values we are parsing + // true = array; false = object (bytes, not std::vector, whose + // push_back() is a call with GCC) + std::vector states; + // a document that starts with a container pushes onto the stack at + // once: allocate it in one step instead of growing it from 1 byte + if (last_token == token_type::begin_object || last_token == token_type::begin_array) + { + states.reserve(32); + } + while (true) { if (!skip_to_state_evaluation) @@ -18440,11 +22271,9 @@ class parser // parse key if (JSON_HEDLEY_UNLIKELY(last_token != token_type::value_string)) { - return sax->parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), exception_message(token_type::value_string, "object key"), nullptr)); + return syntax_error(*sax, exception_message(token_type::value_string, "object key")); } - if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string()))) + if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string_token()))) { return false; } @@ -18452,13 +22281,11 @@ class parser // parse separator (:) if (JSON_HEDLEY_UNLIKELY(!get_token_expecting(token_type::name_separator))) { - return sax->parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), exception_message(token_type::name_separator, "object separator"), nullptr)); + return syntax_error(*sax, exception_message(token_type::name_separator, "object separator")); } // remember we are now inside an object - states.push_back(false); + states.push_back(0); // parse values get_token(); @@ -18483,7 +22310,7 @@ class parser } // remember we are now inside an array - states.push_back(true); + states.push_back(1); // parse values (no need to call get_token) continue; @@ -18497,10 +22324,10 @@ class parser { return sax->parse_error(m_lexer.get_position(), m_lexer.get_token_string(), - out_of_range::create(406, concat("number overflow parsing '", m_lexer.get_token_string(), '\''), nullptr)); + out_of_range::create(exception_id::number_overflow, concat("number overflow parsing '", m_lexer.get_token_string(), '\''), nullptr)); } - if (JSON_HEDLEY_UNLIKELY(!sax->number_float(res, m_lexer.get_string()))) + if (JSON_HEDLEY_UNLIKELY(!sax->number_float(res, number_float_text(sax)))) { return false; } @@ -18546,7 +22373,7 @@ class parser case token_type::value_string: { - if (JSON_HEDLEY_UNLIKELY(!sax->string(m_lexer.get_string()))) + if (JSON_HEDLEY_UNLIKELY(!sax->string(m_lexer.get_string_token()))) { return false; } @@ -18565,23 +22392,16 @@ class parser case token_type::parse_error: { // using "uninitialized" to avoid an "expected" message - return sax->parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), exception_message(token_type::uninitialized, "value"), nullptr)); + return syntax_error(*sax, exception_message(token_type::uninitialized, "value")); } case token_type::end_of_input: { - if (JSON_HEDLEY_UNLIKELY(m_lexer.get_position().chars_read_total == 1)) + if (JSON_HEDLEY_UNLIKELY(m_lexer.get_chars_read_total() == 1)) { - return sax->parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), - "attempting to parse an empty input; check that your input string or stream contains the expected JSON", nullptr)); + return syntax_error(*sax, empty_input_message()); } - return sax->parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), exception_message(token_type::literal_or_value, "value"), nullptr)); + return syntax_error(*sax, exception_message(token_type::literal_or_value, "value")); } case token_type::uninitialized: case token_type::end_array: @@ -18591,9 +22411,7 @@ class parser case token_type::literal_or_value: default: // the last token was unexpected { - return sax->parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), exception_message(token_type::literal_or_value, "value"), nullptr)); + return syntax_error(*sax, exception_message(token_type::literal_or_value, "value")); } } } @@ -18609,7 +22427,7 @@ class parser return true; } - if (states.back()) // array + if (states.back() != 0) // array { // comma -> next value // or end of array (ignore_trailing_commas = true) @@ -18644,9 +22462,7 @@ class parser continue; } - return sax->parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), exception_message(token_type::end_array, "array"), nullptr)); + return syntax_error(*sax, exception_message(token_type::end_array, "array")); } // states.back() is false -> object @@ -18663,12 +22479,10 @@ class parser // parse key if (JSON_HEDLEY_UNLIKELY(last_token != token_type::value_string)) { - return sax->parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), exception_message(token_type::value_string, "object key"), nullptr)); + return syntax_error(*sax, exception_message(token_type::value_string, "object key")); } - if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string()))) + if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string_token()))) { return false; } @@ -18676,9 +22490,7 @@ class parser // parse separator (:) if (JSON_HEDLEY_UNLIKELY(!get_token_expecting(token_type::name_separator))) { - return sax->parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), exception_message(token_type::name_separator, "object separator"), nullptr)); + return syntax_error(*sax, exception_message(token_type::name_separator, "object separator")); } // parse values @@ -18706,9 +22518,7 @@ class parser continue; } - return sax->parse_error(m_lexer.get_position(), - m_lexer.get_token_string(), - parse_error::create(101, m_lexer.get_position(), exception_message(token_type::end_object, "object"), nullptr)); + return syntax_error(*sax, exception_message(token_type::end_object, "object")); } } @@ -18725,6 +22535,23 @@ class parser return (last_token = m_lexer.scan_expecting(expected_type)) == expected_type; } + /*! + @brief reports a syntax error at the current token (parse_error.101) + @param[in] sax the SAX parser to report the error to + @param[in] message the error message + @return the result of the SAX parser's parse_error() + */ + template + bool syntax_error(SAX& sax, const std::string& message) + { + // MSVC 2015 reports C4100 (unreferenced parameter) when SAX::parse_error is static + static_cast(sax); + // computing the position is O(input read so far) for seekable input + const position_t position = m_lexer.get_position(); + return sax.parse_error(position, m_lexer.get_token_string(), + parse_error::create(exception_id::syntax_error, position, message, nullptr)); + } + std::string exception_message(const token_type expected, const std::string& context) { std::string error_msg = "syntax error "; @@ -19060,33 +22887,18 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_data.m_type) + if (m_object->is_object()) { - case value_t::object: - { - m_it.object_iterator = typename object_t::iterator(); - break; - } - - case value_t::array: - { - m_it.array_iterator = typename array_t::iterator(); - break; - } - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - { - m_it.primitive_iterator = primitive_iterator_t(); - break; - } + m_it.object_iterator = typename object_t::iterator(); + } + else if (m_object->is_array()) + { + m_it.array_iterator = typename array_t::iterator(); + } + else + { + // primitive (is_primitive(), including discarded) + m_it.primitive_iterator = primitive_iterator_t(); } } @@ -19201,34 +23013,35 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_data.m_type) + if (m_object->is_object()) { - case value_t::object: - { - m_it.object_iterator = m_object->m_data.m_value.object->end(); - break; - } - - case value_t::array: - { - m_it.array_iterator = m_object->m_data.m_value.array->end(); - break; - } - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - { - m_it.primitive_iterator.set_end(); - break; - } + m_it.object_iterator = m_object->m_data.m_value.object->end(); } + else if (m_object->is_array()) + { + m_it.array_iterator = m_object->m_data.m_value.array->end(); + } + else + { + // primitive (is_primitive(), including discarded) + m_it.primitive_iterator.set_end(); + } + } + + /// @throw invalid_iterator.212 if @a other points to another container + template + JSON_HEDLEY_ALWAYS_INLINE void check_same_container(const IterImpl& other) const + { + if (JSON_HEDLEY_UNLIKELY(m_object != other.m_object)) + { + throw_literal(exception_id::iterators_compare_different_values, "cannot compare iterators of different containers", m_object); + } + } + + /// @throw invalid_iterator.214 always + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_cannot_get_value() const + { + JSON_THROW(invalid_iterator::create(exception_id::iterator_value_unavailable, "cannot get value", m_object)); } public: @@ -19240,40 +23053,25 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_data.m_type) + if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array)) { - case value_t::object: - { - JSON_ASSERT(m_it.object_iterator != m_object->m_data.m_value.object->end()); - return m_it.object_iterator->second; - } - - case value_t::array: - { - JSON_ASSERT(m_it.array_iterator != m_object->m_data.m_value.array->end()); - return *m_it.array_iterator; - } - - case value_t::null: - JSON_THROW(invalid_iterator::create(214, "cannot get value", m_object)); - - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - { - if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.is_begin())) - { - return *m_object; - } - - JSON_THROW(invalid_iterator::create(214, "cannot get value", m_object)); - } + JSON_ASSERT(m_it.array_iterator != m_object->m_data.m_value.array->end()); + return *m_it.array_iterator; } + + if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object)) + { + JSON_ASSERT(m_it.object_iterator != m_object->m_data.m_value.object->end()); + return m_it.object_iterator->second; + } + + // neither array nor object: a primitive value (null has no value to point to) + if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type != value_t::null && m_it.primitive_iterator.is_begin())) + { + return *m_object; + } + + throw_cannot_get_value(); } /*! @@ -19284,38 +23082,25 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_data.m_type) + if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array)) { - case value_t::object: - { - JSON_ASSERT(m_it.object_iterator != m_object->m_data.m_value.object->end()); - return &(m_it.object_iterator->second); - } - - case value_t::array: - { - JSON_ASSERT(m_it.array_iterator != m_object->m_data.m_value.array->end()); - return &*m_it.array_iterator; - } - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - { - if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.is_begin())) - { - return m_object; - } - - JSON_THROW(invalid_iterator::create(214, "cannot get value", m_object)); - } + JSON_ASSERT(m_it.array_iterator != m_object->m_data.m_value.array->end()); + return &*m_it.array_iterator; } + + if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object)) + { + JSON_ASSERT(m_it.object_iterator != m_object->m_data.m_value.object->end()); + return &(m_it.object_iterator->second); + } + + // neither array nor object: a primitive value + if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.is_begin())) + { + return m_object; + } + + throw_cannot_get_value(); } /*! @@ -19337,35 +23122,20 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_data.m_type) + if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array)) { - case value_t::object: - { - std::advance(m_it.object_iterator, 1); - break; - } - - case value_t::array: - { - std::advance(m_it.array_iterator, 1); - break; - } - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - { - ++m_it.primitive_iterator; - break; - } + ++m_it.array_iterator; + return *this; } + if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object)) + { + std::advance(m_it.object_iterator, 1); + return *this; + } + + // neither array nor object: a primitive value + ++m_it.primitive_iterator; return *this; } @@ -19388,35 +23158,20 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_data.m_type) + if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array)) { - case value_t::object: - { - std::advance(m_it.object_iterator, -1); - break; - } - - case value_t::array: - { - std::advance(m_it.array_iterator, -1); - break; - } - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - { - --m_it.primitive_iterator; - break; - } + --m_it.array_iterator; + return *this; } + if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object)) + { + std::advance(m_it.object_iterator, -1); + return *this; + } + + // neither array nor object: a primitive value + --m_it.primitive_iterator; return *this; } @@ -19428,10 +23183,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci bool operator==(const IterImpl& other) const { // if objects are not the same, the comparison is undefined - if (JSON_HEDLEY_UNLIKELY(m_object != other.m_object)) - { - JSON_THROW(invalid_iterator::create(212, "cannot compare iterators of different containers", m_object)); - } + check_same_container(other); // value-initialized forward iterators can be compared, and must compare equal to other value-initialized iterators of the same type #4493 if (m_object == nullptr) @@ -19439,25 +23191,18 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci return true; } - switch (m_object->m_data.m_type) + if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array)) { - case value_t::object: - return (m_it.object_iterator == other.m_it.object_iterator); - - case value_t::array: - return (m_it.array_iterator == other.m_it.array_iterator); - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - return (m_it.primitive_iterator == other.m_it.primitive_iterator); + return (m_it.array_iterator == other.m_it.array_iterator); } + + if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object)) + { + return (m_it.object_iterator == other.m_it.object_iterator); + } + + // neither array nor object: a primitive value + return (m_it.primitive_iterator == other.m_it.primitive_iterator); } /*! @@ -19477,10 +23222,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci bool operator<(const iter_impl& other) const { // if objects are not the same, the comparison is undefined - if (JSON_HEDLEY_UNLIKELY(m_object != other.m_object)) - { - JSON_THROW(invalid_iterator::create(212, "cannot compare iterators of different containers", m_object)); - } + check_same_container(other); // value-initialized forward iterators can be compared, and must compare equal to other value-initialized iterators of the same type #4493 if (m_object == nullptr) @@ -19489,25 +23231,18 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci return false; } - switch (m_object->m_data.m_type) + if (m_object->is_object()) { - case value_t::object: - JSON_THROW(invalid_iterator::create(213, "cannot compare order of object iterators", m_object)); - - case value_t::array: - return (m_it.array_iterator < other.m_it.array_iterator); - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - return (m_it.primitive_iterator < other.m_it.primitive_iterator); + throw_literal(exception_id::iterator_order_on_object, "cannot compare order of object iterators", m_object); } + + if (m_object->is_array()) + { + return (m_it.array_iterator < other.m_it.array_iterator); + } + + // primitive (is_primitive(), including discarded) + return (m_it.primitive_iterator < other.m_it.primitive_iterator); } /*! @@ -19545,30 +23280,19 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_data.m_type) + if (m_object->is_object()) { - case value_t::object: - JSON_THROW(invalid_iterator::create(209, "cannot use offsets with object iterators", m_object)); + throw_literal(exception_id::iterator_arithmetic_on_object, "cannot use offsets with object iterators", m_object); + } - case value_t::array: - { - std::advance(m_it.array_iterator, i); - break; - } - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - { - m_it.primitive_iterator += i; - break; - } + if (m_object->is_array()) + { + std::advance(m_it.array_iterator, i); + } + else + { + // primitive (is_primitive(), including discarded) + m_it.primitive_iterator += i; } return *this; @@ -19624,25 +23348,18 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_data.m_type) + if (m_object->is_object()) { - case value_t::object: - JSON_THROW(invalid_iterator::create(209, "cannot use offsets with object iterators", m_object)); - - case value_t::array: - return m_it.array_iterator - other.m_it.array_iterator; - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - return m_it.primitive_iterator - other.m_it.primitive_iterator; + throw_literal(exception_id::iterator_arithmetic_on_object, "cannot use offsets with object iterators", m_object); } + + if (m_object->is_array()) + { + return m_it.array_iterator - other.m_it.array_iterator; + } + + // primitive (is_primitive(), including discarded) + return m_it.primitive_iterator - other.m_it.primitive_iterator; } /*! @@ -19656,13 +23373,13 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci switch (m_object->m_data.m_type) { case value_t::object: - JSON_THROW(invalid_iterator::create(208, "cannot use operator[] for object iterators", m_object)); + throw_literal(exception_id::iterator_subscript_on_object, "cannot use operator[] for object iterators", m_object); case value_t::array: return *std::next(m_it.array_iterator, n); case value_t::null: - JSON_THROW(invalid_iterator::create(214, "cannot get value", m_object)); + throw_cannot_get_value(); case value_t::string: case value_t::boolean: @@ -19678,7 +23395,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci return *m_object; } - JSON_THROW(invalid_iterator::create(214, "cannot get value", m_object)); + throw_cannot_get_value(); } } } @@ -19691,12 +23408,12 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - if (JSON_HEDLEY_LIKELY(m_object->is_object())) + if (JSON_HEDLEY_UNLIKELY(!m_object->is_object())) { - return m_it.object_iterator->first; + throw_literal(exception_id::iterator_key_not_object, "cannot use key() for non-object iterators", m_object); } - JSON_THROW(invalid_iterator::create(207, "cannot use key() for non-object iterators", m_object)); + return m_it.object_iterator->first; } /*! @@ -19856,58 +23573,6 @@ NLOHMANN_JSON_NAMESPACE_END // #include // #include -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.12.0 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann -// SPDX-License-Identifier: MIT - - - -#include // conditional, is_same - -// #include - - -NLOHMANN_JSON_NAMESPACE_BEGIN - -/*! -@brief Default base class of the @ref basic_json class. - -So that the correct implementations of the copy / move ctors / assign operators -of @ref basic_json do not require complex case distinctions -(no base class / custom base class used as customization point), -@ref basic_json always has a base class. -By default, this class is used because it is empty and thus has no effect -on the behavior of @ref basic_json. - -@note This class intentionally lives in namespace @ref nlohmann rather than - @ref nlohmann::detail. Every @ref basic_json specialization derives from - it (via @ref detail::json_base_class) unless a custom base class is - supplied, which makes its namespace an associated namespace of - @ref basic_json for the purpose of argument-dependent lookup (ADL). If - it lived in `nlohmann::detail`, that namespace - and with it the - library's internal `to_json`/`from_json` overloads - would leak into - ADL for any unqualified `to_json`/`from_json` call a user makes - involving a @ref basic_json argument, silently shadowing the user's own - overloads in some cases. -*/ -struct json_default_base {}; - -namespace detail -{ - -template -using json_base_class = typename std::conditional < - std::is_same::value, - ::nlohmann::json_default_base, - T - >::type; - -} // namespace detail -NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ @@ -19920,16 +23585,18 @@ NLOHMANN_JSON_NAMESPACE_END -#include // all_of +#include // all_of, copy +#include // array #include // isdigit #include // errno, ERANGE +#include // size_t +#include // uint64_t #include // strtoull +#include // memchr, memcmp, memcpy #ifndef JSON_NO_IO #include // ostream #endif // JSON_NO_IO #include // max -#include // accumulate -#include // set #include // string #include // move #include // vector @@ -19938,6 +23605,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include +// #include + +// #include + // #include // #include @@ -19949,6 +23620,107 @@ NLOHMANN_JSON_NAMESPACE_END NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +struct json_pointer_access; + +/*! +@brief result of parse_array_index() + +json_pointer::array_index() maps each value to the corresponding +parse_error/out_of_range exception; json_pointer::contains() and +get_checked_or_null(), which must not throw for an out-of-range or +unrepresentable index, and the json_view pointer resolution switch on it +directly instead. +*/ +enum class array_index_status +{ + ok, ///< @a s is a valid, representable array index + leading_zero, ///< @a s begins with '0' but has more than one character + not_a_number, ///< @a s is neither empty nor "-" and does not begin with a digit + unresolved, ///< @a s could not be converted to an integer + exceeds_size_type ///< @a s converts to an integer that exceeds SizeType +}; + +/*! +@brief convert a JSON Pointer reference token into an array index (RFC 6901, Sect. 4) + +@param[in] s reference token to be converted into an array index; its data() + must be NUL-terminated +@param[out] idx the integer representation of @a s if array_index_status::ok + is returned; left unchanged otherwise + +@return whether @a s is a valid array index, and if not, why + +@note this function never throws; json_pointer::array_index(), the callers that + must not throw, and the json_view pointer resolution build on it instead + of each re-implementing the RFC 6901 digit rules and the range check +@note force-inlined: as a free function template it is no longer inlined into + the pointer resolution, which makes JsonPointerAt up to 17% slower (#5891) +*/ +template +JSON_HEDLEY_ALWAYS_INLINE array_index_status parse_array_index(const StringType& s, SizeType& idx) noexcept +{ + // error condition (cf. RFC 6901, Sect. 4) + if (JSON_HEDLEY_UNLIKELY(s.size() > 1 && s[0] == '0')) + { + return array_index_status::leading_zero; + } + + // error condition (cf. RFC 6901, Sect. 4); this also covers single- + // character tokens, so "/x" and "/xy" fail alike; "-" and the empty + // token are left to the conversion below and are reported as unresolved + if (JSON_HEDLEY_UNLIKELY(!s.empty() && !(s.size() == 1 && s[0] == '-') && !(s[0] >= '0' && s[0] <= '9'))) + { + return array_index_status::not_a_number; + } + + // fast path: up to 18 decimal digits cannot overflow unsigned long long or + // SizeType (9 digits for a 32-bit SizeType), so strtoull and errno are not needed + constexpr std::size_t max_fast_digits = sizeof(SizeType) >= 8 ? 18 : 9; + if (!s.empty() && s.size() <= max_fast_digits) + { + SizeType acc = 0; + for (std::size_t i = 0; i < s.size(); ++i) + { + const char c = s[i]; + if (JSON_HEDLEY_UNLIKELY(c < '0' || c > '9')) + { + break; // not a plain number: let strtoull decide + } + acc = static_cast((acc * 10) + static_cast(c - '0')); + if (JSON_HEDLEY_UNLIKELY(i == s.size() - 1)) + { + idx = acc; + return array_index_status::ok; + } + } + } + + const char* p = s.data(); + char* p_end = nullptr; // NOLINT(misc-const-correctness) + errno = 0; // strtoull doesn't reset errno + const unsigned long long res = std::strtoull(p, &p_end, 10); // NOLINT(runtime/int) + if (p == p_end // invalid input or empty string + || errno == ERANGE // out of range + || JSON_HEDLEY_UNLIKELY(static_cast(p_end - p) != s.size())) // incomplete read + { + return array_index_status::unresolved; + } + + // the index does not fit into SizeType; on 64-bit platforms this is + // only SIZE_MAX itself (see #2203 and #5395) + if (res >= static_cast((std::numeric_limits::max)())) // NOLINT(runtime/int) + { + return array_index_status::exceeds_size_type; + } + + idx = static_cast(res); + return array_index_status::ok; +} + +} // namespace detail + /// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document /// @sa https://json.nlohmann.me/api/json_pointer/ template @@ -19961,6 +23733,8 @@ class json_pointer template friend class json_pointer; + friend struct detail::json_pointer_access; + template struct string_t_helper { @@ -19987,12 +23761,45 @@ class json_pointer /// @sa https://json.nlohmann.me/api/json_pointer/to_string/ string_t to_string() const { - return std::accumulate(reference_tokens.begin(), reference_tokens.end(), - string_t{}, - [](const string_t& a, const string_t& b) + if (reference_tokens.empty()) { - return detail::concat(a, '/', detail::escape(b)); - }); + return string_t(); + } + + // one buffer: every token adds its escaped form (at least its length) and a slash + std::size_t length = 0; + bool plain = true; + for (const auto& reference_token : reference_tokens) + { + length += reference_token.size() + 1; + const char* const data = reference_token.data(); + for (std::size_t i = 0, size = reference_token.size(); plain && i < size; ++i) + { + plain = data[i] != '~' && data[i] != '/'; + } + } + + string_t result; + if (JSON_HEDLEY_LIKELY(plain)) + { + // nothing to escape: the tokens are copied into the string + result.resize(length); + char* out = &result[0]; + for (const auto& reference_token : reference_tokens) + { + *out++ = '/'; + out = std::copy(reference_token.data(), reference_token.data() + reference_token.size(), out); + } + return result; + } + + result.reserve(length); + for (const auto& reference_token : reference_tokens) + { + result.append("/", 1); + detail::escape_into(result, reference_token); + } + return result; } /// @brief return a string representation of the JSON pointer @@ -20047,21 +23854,29 @@ class json_pointer friend json_pointer operator/(const json_pointer& lhs, const json_pointer& rhs) { - return json_pointer(lhs) /= rhs; + json_pointer result; + result.reference_tokens.reserve(lhs.reference_tokens.size() + rhs.reference_tokens.size()); + result.reference_tokens.insert(result.reference_tokens.end(), lhs.reference_tokens.begin(), lhs.reference_tokens.end()); + result.reference_tokens.insert(result.reference_tokens.end(), rhs.reference_tokens.begin(), rhs.reference_tokens.end()); + return result; } /// @brief create a new JSON pointer by appending the unescaped token at the end of the JSON pointer /// @sa https://json.nlohmann.me/api/json_pointer/operator_slash/ friend json_pointer operator/(const json_pointer& lhs, string_t token) // NOLINT(performance-unnecessary-value-param) { - return json_pointer(lhs) /= std::move(token); + json_pointer result; + result.reference_tokens.reserve(lhs.reference_tokens.size() + 1); + result.reference_tokens.insert(result.reference_tokens.end(), lhs.reference_tokens.begin(), lhs.reference_tokens.end()); + result.reference_tokens.push_back(std::move(token)); + return result; } /// @brief create a new JSON pointer by appending the array-index-token at the end of the JSON pointer /// @sa https://json.nlohmann.me/api/json_pointer/operator_slash/ friend json_pointer operator/(const json_pointer& lhs, std::size_t array_idx) { - return json_pointer(lhs) /= array_idx; + return lhs / detail::to_string(array_idx); } /// @brief returns the parent of this JSON pointer @@ -20073,8 +23888,8 @@ class json_pointer return *this; } - json_pointer res = *this; - res.pop_back(); + json_pointer res; + res.reference_tokens = std::vector(reference_tokens.begin(), reference_tokens.end() - 1); return res; } @@ -20084,7 +23899,7 @@ class json_pointer { if (JSON_HEDLEY_UNLIKELY(empty())) { - JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent", nullptr)); + throw_no_parent(); } reference_tokens.erase(reference_tokens.begin()); @@ -20096,7 +23911,7 @@ class json_pointer { if (JSON_HEDLEY_UNLIKELY(empty())) { - JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent", nullptr)); + throw_no_parent(); } return reference_tokens.front(); @@ -20122,7 +23937,7 @@ class json_pointer { if (JSON_HEDLEY_UNLIKELY(empty())) { - JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent", nullptr)); + throw_no_parent(); } reference_tokens.pop_back(); @@ -20134,7 +23949,7 @@ class json_pointer { if (JSON_HEDLEY_UNLIKELY(empty())) { - JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent", nullptr)); + throw_no_parent(); } return reference_tokens.back(); @@ -20162,70 +23977,56 @@ class json_pointer } private: - /*! - @brief result of @ref parse_array_index - - @ref array_index maps each value to the corresponding parse_error/out_of_range - exception; @ref contains and @ref get_checked_or_null, which must not throw for - an out-of-range or unrepresentable index, switch on it directly instead. - */ - enum class array_index_status + /// @throw out_of_range.405 always + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE static void throw_no_parent() { - ok, ///< @a s is a valid, representable array index - leading_zero, ///< @a s begins with '0' but has more than one character - not_a_number, ///< @a s does not begin with a digit - unresolved, ///< @a s could not be converted to an integer - exceeds_size_type ///< @a s converts to an integer that exceeds size_type - }; + JSON_THROW(detail::out_of_range::create(detail::exception_id::patch_on_root, "JSON pointer has no parent", nullptr)); + } - /*! - @param[in] s reference token to be converted into an array index - @param[out] idx the integer representation of @a s if @ref array_index_status::ok - is returned; left unchanged otherwise + /// whether the '~' at @a pos of @a s is not followed by '0' or '1' + JSON_HEDLEY_ALWAYS_INLINE static bool is_invalid_escape(const string_t& s, std::size_t pos) + { + return pos == s.size() - 1 || (s[pos + 1] != '0' && s[pos + 1] != '1'); + } - @return whether @a s is a valid array index, and if not, why + /// @throw parse_error.107 always + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE static void throw_missing_slash(const string_t& pointer) + { + static_cast(pointer); // unused when JSON_NOEXCEPTION is defined + JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_missing_slash, 1, detail::concat("JSON pointer must be empty or begin with '/' - was: '", pointer, "'"), nullptr)); + } - @note this function never throws; @ref array_index and the callers that must not - throw (@ref contains, @ref get_checked_or_null) build on it instead of each - re-implementing the RFC 6901 digit rules and the @a size_type range check - */ + /// @throw parse_error.107 if a nonempty @a pointer does not begin with '/' + JSON_HEDLEY_ALWAYS_INLINE static void check_begins_with_slash(const string_t& pointer) + { + if (JSON_HEDLEY_UNLIKELY(pointer[0] != '/')) + { + throw_missing_slash(pointer); + } + } + + /// @throw out_of_range.404 always + template + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE static void throw_unresolved(const string_t& reference_token, BasicJsonContext context) + { + static_cast(reference_token); // unused when JSON_NOEXCEPTION is defined + static_cast(context); + JSON_THROW(detail::out_of_range::create(detail::exception_id::pointer_unresolved, detail::concat("unresolved reference token '", reference_token, "'"), context)); + } + + using array_index_status = detail::array_index_status; + + /// whether a reference token is "-" (a plain comparison with a string literal is not always inlined) + static bool is_dash(const string_t& s) noexcept + { + return s.size() == 1 && s[0] == '-'; + } + + /// detail::parse_array_index() for the size_type of @a BasicJsonType template static array_index_status parse_array_index(const string_t& s, typename BasicJsonType::size_type& idx) noexcept { - using size_type = typename BasicJsonType::size_type; - - // error condition (cf. RFC 6901, Sect. 4) - if (JSON_HEDLEY_UNLIKELY(s.size() > 1 && s[0] == '0')) - { - return array_index_status::leading_zero; - } - - // error condition (cf. RFC 6901, Sect. 4) - if (JSON_HEDLEY_UNLIKELY(s.size() > 1 && !(s[0] >= '1' && s[0] <= '9'))) - { - return array_index_status::not_a_number; - } - - const char* p = s.data(); - char* p_end = nullptr; // NOLINT(misc-const-correctness) - errno = 0; // strtoull doesn't reset errno - const unsigned long long res = std::strtoull(p, &p_end, 10); // NOLINT(runtime/int) - if (p == p_end // invalid input or empty string - || errno == ERANGE // out of range - || JSON_HEDLEY_UNLIKELY(static_cast(p_end - p) != s.size())) // incomplete read - { - return array_index_status::unresolved; - } - - // the index does not fit into size_type; on 64-bit platforms this is - // only SIZE_MAX itself (see #2203 and #5395) - if (res >= static_cast((std::numeric_limits::max)())) // NOLINT(runtime/int) - { - return array_index_status::exceeds_size_type; - } - - idx = static_cast(res); - return array_index_status::ok; + return detail::parse_array_index(s, idx); } /*! @@ -20247,13 +24048,13 @@ class json_pointer // the branches differ in their messages, not after JSON_THROW's expansion // NOLINTNEXTLINE(bugprone-branch-clone) case array_index_status::leading_zero: - JSON_THROW(detail::parse_error::create(106, 0, detail::concat("array index '", s, "' must not begin with '0'"), nullptr)); + JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_index_leading_zero, 0, detail::concat("array index '", s, "' must not begin with '0'"), nullptr)); case array_index_status::not_a_number: - JSON_THROW(detail::parse_error::create(109, 0, detail::concat("array index '", s, "' is not a number"), nullptr)); + JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_index_not_a_number, 0, detail::concat("array index '", s, "' is not a number"), nullptr)); case array_index_status::unresolved: - JSON_THROW(detail::out_of_range::create(404, detail::concat("unresolved reference token '", s, "'"), nullptr)); + throw_unresolved(s, nullptr); case array_index_status::exceeds_size_type: - JSON_THROW(detail::out_of_range::create(410, detail::concat("array index ", s, " exceeds size_type"), nullptr)); + JSON_THROW(detail::out_of_range::create(detail::exception_id::value_out_of_range, detail::concat("array index ", s, " exceeds size_type"), nullptr)); case array_index_status::ok: default: break; @@ -20262,12 +24063,123 @@ class json_pointer return idx; } + /*! + @brief compare with a JSON pointer string without splitting it into tokens + + @throw parse_error.107 if the string is not empty or begins with '/' + @throw parse_error.108 if character '~' is not followed by '0' or '1' + */ + bool equals_string(const string_t& reference_string) const + { + if (reference_string.empty()) + { + return reference_tokens.empty(); + } + + check_begins_with_slash(reference_string); + + // the whole string is validated, whether or not it matches + const std::size_t size = reference_string.size(); + for (std::size_t pos = reference_string.find_first_of('~'); + pos != string_t::npos; + pos = reference_string.find_first_of('~', pos + 1)) + { + if (JSON_HEDLEY_UNLIKELY(is_invalid_escape(reference_string, pos))) + { + JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_invalid_escape, 0, "escape character '~' must be followed with '0' or '1'", nullptr)); + } + } + + // compare token by token, unescaping the string on the fly + std::size_t pos = 0; // the slash in front of the next token + for (const auto& reference_token : reference_tokens) + { + if (pos >= size) + { + return false; + } + + std::size_t i = pos + 1; + std::size_t j = 0; + const std::size_t token_size = reference_token.size(); + while (i < size && reference_string[i] != '/') + { + char c = reference_string[i++]; + if (c == '~') + { + c = (reference_string[i++] == '0') ? '~' : '/'; + } + if (j == token_size || reference_token[j] != c) + { + return false; + } + ++j; + } + if (j != token_size) + { + return false; + } + pos = i; + } + return pos == size; + } + + /// reserve room for @a n elements of an array type that has reserve() + template + static auto reserve_elements(ArrayType& array, const std::size_t n, detail::priority_tag<1> /*unused*/) + -> decltype(array.reserve(n), void()) + { + array.reserve(n); + } + + template + static void reserve_elements(ArrayType& /*array*/, const std::size_t /*n*/, detail::priority_tag<0> /*unused*/) + {} + + /// @brief object member access with a key that is only copied if it is inserted + /// + /// Objects that can find the position of a key and insert there + /// (std::map) search once, others look the key up and add it if it is missing. + template + static auto object_member_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<1> /*unused*/) + -> decltype(object.m_data.m_value.object->emplace_hint(object.m_data.m_value.object->lower_bound(key), key, nullptr), std::declval()) + { + auto& members = *object.m_data.m_value.object; + auto it = members.lower_bound(key); + if (it != members.end() && !members.key_comp()(key, it->first)) + { + return it->second; + } + + const auto old_capacity = object.container_capacity(); + it = members.emplace_hint(it, key, nullptr); + return object.set_parent_after_insert(it->second, old_capacity); + } + + template + static BasicJsonType& object_member_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<0> /*unused*/) + { + auto& members = *object.m_data.m_value.object; + const auto it = members.find(key); + if (it != members.end()) + { + return it->second; + } + return object.operator[](key); + } + + template + static BasicJsonType& object_member(BasicJsonType& object, const string_t& key) + { + return object_member_impl(object, key, detail::priority_tag<1> {}); + } + JSON_PRIVATE_UNLESS_TESTED: json_pointer top() const { if (JSON_HEDLEY_UNLIKELY(empty())) { - JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent", nullptr)); + throw_no_parent(); } json_pointer result = *this; @@ -20277,91 +24189,357 @@ class json_pointer private: /*! - @brief the reference token sequences that denote arrays + @brief the pointer prefixes of a flattened object that denote arrays - @ref unflatten collects the pointer prefixes that have a reference token 0 - among their children; @ref get_and_create creates arrays exactly below - those prefixes and objects everywhere else. Deciding this up front keeps - the result independent of the order in which the flattened object is - iterated, which is unspecified for some object types. + @ref unflatten creates arrays exactly below the prefixes that have a + reference token 0 among their children, and objects everywhere else. + Deciding this up front keeps the result independent of the order in which + the flattened object is iterated, which is unspecified for some object + types. + + A prefix is identified by the text of the key it is the beginning of: + escaping is unambiguous, so equal prefixes of valid keys are equal texts. + The set refers to the keys, which must outlive it. */ - using array_parents_t = std::set>; - - /*! - @brief create and return a reference to the pointed to value - - Complexity: Linear in the number of reference tokens. - - @throw parse_error.106 if an array index begins with '0' - @throw parse_error.109 if array index is not a number - @throw type_error.313 if value cannot be unflattened - */ - template - BasicJsonType& get_and_create(BasicJsonType& j, const array_parents_t& array_parents) const + class array_prefix_set { - auto* result = &j; - - // the reference tokens that have been consumed so far; used to look up - // whether the value to be created below is an array or an object - std::vector prefix; - - // in case no reference tokens exist, return a reference to the JSON value - // j which will be overwritten by a primitive value - for (const auto& reference_token : reference_tokens) + public: + void insert(const char* data, std::size_t size) { - switch (result->type()) + // flattened objects are usually sorted, so most keys that have a + // 0 yield the prefix of the previous one + if (last_data != nullptr && last_size == size && std::memcmp(last_data, data, size) == 0) { - case detail::value_t::null: - { - if (array_parents.find(prefix) != array_parents.end()) - { - // some reference token below this position is 0, so the - // value is an array - result = &result->operator[](array_index(reference_token)); - } - else - { - // start a new object otherwise - result = &result->operator[](reference_token); - } - break; - } - - case detail::value_t::object: - { - // create an entry in the object - result = &result->operator[](reference_token); - break; - } - - case detail::value_t::array: - { - // create an entry in the array - result = &result->operator[](array_index(reference_token)); - break; - } - - /* - The following code is only reached if there exists a reference - token _and_ the current value is primitive. In this case, we have - an error situation, because primitive values may only occur as - a single value; that is, with an empty list of reference tokens. - */ - case detail::value_t::string: - case detail::value_t::boolean: - case detail::value_t::number_integer: - case detail::value_t::number_unsigned: - case detail::value_t::number_float: - case detail::value_t::binary: - case detail::value_t::discarded: - default: - JSON_THROW(detail::type_error::create(313, "invalid value to unflatten", &j)); + return; } + last_data = data; + last_size = size; - prefix.push_back(reference_token); + if (table.empty()) + { + table.resize(16); + } + const std::size_t hash = hash_of(data, size); + const std::size_t mask = table.size() - 1; + std::size_t i = hash & mask; + while (table[i].data != nullptr) + { + if (table[i].hash == hash && table[i].size == size && std::memcmp(table[i].data, data, size) == 0) + { + return; + } + i = (i + 1) & mask; + } + table[i] = slot(hash, data, size); + + // keep the load factor below 1/2 + if (++used * 2 > table.size()) + { + grow(); + } } - return *result; + bool empty() const noexcept + { + return used == 0; + } + + bool contains(const char* data, std::size_t size) const noexcept + { + if (used == 0) + { + return false; + } + const std::size_t hash = hash_of(data, size); + const std::size_t mask = table.size() - 1; + for (std::size_t i = hash & mask; table[i].data != nullptr; i = (i + 1) & mask) + { + if (table[i].hash == hash && table[i].size == size && std::memcmp(table[i].data, data, size) == 0) + { + return true; + } + } + return false; + } + + private: + struct slot + { + slot() noexcept = default; + + slot(std::size_t h, const char* d, std::size_t s) noexcept + : hash(h), data(d), size(s) + {} + + std::size_t hash = 0; + const char* data = nullptr; // nullptr: the slot is empty + std::size_t size = 0; + }; + + static std::size_t hash_of(const char* data, std::size_t size) noexcept + { + std::uint64_t h = 0x9E3779B97F4A7C15ULL ^ size; + std::size_t i = 0; + for (; i + 8 <= size; i += 8) + { + std::uint64_t w = 0; + std::memcpy(&w, data + i, 8); + h = (h ^ w) * 0x100000001B3ULL; + h ^= h >> 32u; + } + std::uint64_t w = 0; + if (i < size) + { + std::memcpy(&w, data + i, size - i); + } + h = (h ^ w) * 0x9E3779B97F4A7C15ULL; + return detail::conditional_static_cast(h ^ (h >> 29u)); + } + + void grow() + { + std::vector old(table.size() * 2); + old.swap(table); + const std::size_t mask = table.size() - 1; + for (const auto& s : old) + { + if (s.data != nullptr) + { + std::size_t i = s.hash & mask; + while (table[i].data != nullptr) + { + i = (i + 1) & mask; + } + table[i] = s; + } + } + } + + std::vector table{}; + std::size_t used = 0; + const char* last_data = nullptr; + std::size_t last_size = 0; + }; + + /// @brief the length of the longest common prefix of the first @a limit bytes of @a a and @a b + static std::size_t common_prefix_length(const char* a, const char* b, const std::size_t limit) noexcept + { + std::size_t i = 0; + for (; i + 8 <= limit; i += 8) + { + std::uint64_t x = 0; + std::uint64_t y = 0; + std::memcpy(&x, a + i, 8); + std::memcpy(&y, b + i, 8); + if (x != y) + { + break; + } + } + while (i < limit && a[i] == b[i]) + { + ++i; + } + return i; + } + + /*! + @brief check that @a key is a valid JSON pointer, and add the prefixes of + it that denote arrays to @a arrays + + @throw parse_error.107 if the key is not empty or begins with '/' + @throw parse_error.108 if character '~' is not followed by '0' or '1' + + The same checks as @ref split, which throws the same exceptions, but + without building the reference tokens. + */ + static void scan_key(const string_t& key, const string_t* previous, array_prefix_set& arrays) + { + const std::size_t size = key.size(); + if (size == 0) + { + return; + } + + const char* const data = key.data(); + check_begins_with_slash(key); + + // The beginning of the key that it has in common with the previous + // one, which was checked already, is skipped: the scan starts with + // the token after the last slash in it. + std::size_t begin = 1; + if (previous != nullptr) + { + std::size_t same = common_prefix_length(data, previous->data(), (std::min)(size, previous->size())); + while (same > 1 && data[same - 1] != '/') + { + --same; + } + if (same > 1) + { + begin = same; + } + } + + for (std::size_t i = begin; i <= size; ++i) + { + if (i == size || data[i] == '/') + { + // a token 0 makes the pointer before it an array + if (i - begin == 1 && data[begin] == '0') + { + arrays.insert(data, begin - 1); + } + begin = i + 1; + } + else if (data[i] == '~') + { + // ~ must be followed by 0 or 1 + if (JSON_HEDLEY_UNLIKELY(is_invalid_escape(key, i))) + { + JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_invalid_escape, 0, "escape character '~' must be followed with '0' or '1'", nullptr)); + } + } + } + } + + /*! + @brief the values along the pointer last resolved by @ref resolve_plain + + A patch from @ref basic_json::diff lists its operations in document order, + so consecutive pointers share most of their reference tokens; the values + along the previous pointer are kept to resume from the common prefix. + `steps[k]` is the value after the first `k` reference tokens together with + the length of the prefix of `*path` that holds them. A value stays valid as + long as no operation changes its container: callers keep only values up to + the parent of the pointer they modify, or call `reset()`. + */ + template + struct plain_steps + { + const string_t* path = nullptr; + std::vector> steps{}; + + void reset() noexcept + { + path = nullptr; + steps.clear(); + } + }; + + /*! + @brief resolve a plain JSON pointer without building its reference tokens + + @param[in] root the value the pointer starts at + @param[in] path a JSON pointer string + @param[in] length the number of leading bytes of @a path to resolve; 0 or + the position of a slash + @param[in,out] token a string to hold the reference tokens, to reuse its storage + @param[in,out] cache the values along the previously resolved pointer; + @a path must outlive the next call + + @return the pointed to value, or `nullptr` if the pointer does not denote + an existing value, or does so in a way that is not plain: an array + index that is malformed or out of range, or a "-", or a reference + token with an escape. No exception is + thrown for these; callers use @ref json_pointer instead, which + handles them. + */ + template + static BasicJsonType* resolve_plain(BasicJsonType* root, const string_t& path, const std::size_t length, string_t& token, plain_steps& cache) + { + auto& steps = cache.steps; + + // resume from the deepest step that is also a prefix of this path + std::size_t depth = 1; + if (cache.path != nullptr && !steps.empty() && steps[0].second == root) + { + const string_t& previous = *cache.path; + for (depth = steps.size(); depth > 1; --depth) + { + // a step ends at a token boundary of the previous path + const std::size_t prefix = steps[depth - 1].first; + if (prefix <= length && (prefix == length || path[prefix] == '/') && std::memcmp(path.data(), previous.data(), prefix) == 0) + { + break; + } + } + steps.resize(depth); + } + else + { + steps.assign(1, std::make_pair(std::size_t{0}, root)); + } + cache.path = &path; + + BasicJsonType* ptr = steps[depth - 1].second; + for (std::size_t pos = steps[depth - 1].first; pos < length;) + { + if (JSON_HEDLEY_UNLIKELY(path[pos] != '/')) + { + return nullptr; + } + + // the reference token between the slash at pos and the next one, + // which must not have an escape + const char* const begin = path.data() + pos + 1; + const char* const limit = path.data() + length; + const char* end = begin; + while (end < limit && *end != '/') + { + if (JSON_HEDLEY_UNLIKELY(*end == '~')) + { + return nullptr; + } + ++end; + } + + if (ptr->is_object()) + { + token.clear(); + token.append(begin, static_cast(end - begin)); + const auto it = ptr->m_data.m_value.object->find(token); + if (JSON_HEDLEY_UNLIKELY(it == ptr->m_data.m_value.object->end())) + { + return nullptr; + } + ptr = &it->second; + } + else if (ptr->is_array()) + { + // an index of up to 18 digits without a leading zero fits into size_type + // (9 digits for a 32-bit size_type); everything else is left to json_pointer + constexpr std::size_t max_digits = sizeof(typename BasicJsonType::size_type) >= 8 ? 18 : 9; + const auto digits = static_cast(end - begin); + if (JSON_HEDLEY_UNLIKELY(digits == 0 || digits > max_digits || (digits > 1 && *begin == '0'))) + { + return nullptr; + } + typename BasicJsonType::size_type idx = 0; + for (const char* c = begin; c != end; ++c) + { + if (JSON_HEDLEY_UNLIKELY(*c < '0' || *c > '9')) + { + return nullptr; + } + idx = static_cast((idx * 10) + static_cast(*c - '0')); + } + if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size())) + { + return nullptr; + } + ptr = &(*ptr->m_data.m_value.array)[idx]; + } + else + { + // primitive value with a reference token left + return nullptr; + } + pos = static_cast(end - path.data()); + steps.emplace_back(pos, ptr); + } + + return ptr; } /*! @@ -20409,40 +24587,28 @@ class json_pointer : detail::value_t::object; } - switch (ptr->type()) + if (ptr->is_object()) { - case detail::value_t::object: + // use unchecked object access + ptr = &object_member(*ptr, reference_token); + } + else if (ptr->is_array()) + { + if (reference_token == "-") { - // use unchecked object access - ptr = &ptr->operator[](reference_token); - break; + // explicitly treat "-" as index beyond the end + ptr = &ptr->operator[](ptr->m_data.m_value.array->size()); } - - case detail::value_t::array: + else { - if (reference_token == "-") - { - // explicitly treat "-" as index beyond the end - ptr = &ptr->operator[](ptr->m_data.m_value.array->size()); - } - else - { - // convert array index to number; unchecked access - ptr = &ptr->operator[](array_index(reference_token)); - } - break; + // convert array index to number; unchecked access + ptr = &ptr->operator[](array_index(reference_token)); } - - case detail::value_t::null: - case detail::value_t::string: - case detail::value_t::boolean: - case detail::value_t::number_integer: - case detail::value_t::number_unsigned: - case detail::value_t::number_float: - case detail::value_t::binary: - case detail::value_t::discarded: - default: - JSON_THROW(detail::out_of_range::create(404, detail::concat("unresolved reference token '", reference_token, "'"), ptr)); + } + else + { + // primitive value with a reference token left + throw_unresolved(reference_token, ptr); } } @@ -20460,46 +24626,39 @@ class json_pointer { for (const auto& reference_token : reference_tokens) { - switch (ptr->type()) + if (ptr->is_object()) { - case detail::value_t::object: + const auto it = ptr->m_data.m_value.object->find(reference_token); + if (JSON_HEDLEY_UNLIKELY(it == ptr->m_data.m_value.object->end())) { - // note: at performs range check - ptr = &ptr->at(reference_token); - break; + // at() throws out_of_range.403 with the usual message + ptr->at(reference_token); + } + ptr = &it->second; + } + else if (ptr->is_array()) + { + if (JSON_HEDLEY_UNLIKELY(is_dash(reference_token))) + { + // "-" always fails the range check + JSON_THROW(detail::out_of_range::create(detail::exception_id::pointer_past_the_end_index, detail::concat( + "array index '-' (", std::to_string(ptr->m_data.m_value.array->size()), + ") is out of range"), ptr)); } - case detail::value_t::array: + const auto idx = array_index(reference_token); + // Bounds check before access to avoid exception with JSON_NOEXCEPTION + if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size())) { - if (JSON_HEDLEY_UNLIKELY(reference_token == "-")) - { - // "-" always fails the range check - JSON_THROW(detail::out_of_range::create(402, detail::concat( - "array index '-' (", std::to_string(ptr->m_data.m_value.array->size()), - ") is out of range"), ptr)); - } - - const auto idx = array_index(reference_token); - // Bounds check before access to avoid exception with JSON_NOEXCEPTION - if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size())) - { - JSON_THROW(detail::out_of_range::create(401, detail::concat( - "array index ", std::to_string(idx), " is out of range"), ptr)); - } - ptr = &ptr->operator[](idx); - break; + JSON_THROW(detail::out_of_range::create(detail::exception_id::array_index_out_of_range, detail::concat( + "array index ", std::to_string(idx), " is out of range"), ptr)); } - - case detail::value_t::null: - case detail::value_t::string: - case detail::value_t::boolean: - case detail::value_t::number_integer: - case detail::value_t::number_unsigned: - case detail::value_t::number_float: - case detail::value_t::binary: - case detail::value_t::discarded: - default: - JSON_THROW(detail::out_of_range::create(404, detail::concat("unresolved reference token '", reference_token, "'"), ptr)); + ptr = &(*ptr->m_data.m_value.array)[idx]; + } + else + { + // primitive value with a reference token left + throw_unresolved(reference_token, ptr); } } @@ -20528,40 +24687,32 @@ class json_pointer { for (const auto& reference_token : reference_tokens) { - switch (ptr->type()) + if (ptr->is_object()) { - case detail::value_t::object: + // use unchecked object access; the const operator[] + // asserts that the key exists + const auto it = ptr->m_data.m_value.object->find(reference_token); + JSON_ASSERT(it != ptr->m_data.m_value.object->end()); + ptr = &it->second; + } + else if (ptr->is_array()) + { + if (JSON_HEDLEY_UNLIKELY(is_dash(reference_token))) { - // use unchecked object access; the const operator[] - // asserts that the key exists - ptr = &ptr->operator[](reference_token); - break; + // "-" cannot be used for const access + JSON_THROW(detail::out_of_range::create(detail::exception_id::pointer_past_the_end_index, detail::concat("array index '-' (", std::to_string(ptr->m_data.m_value.array->size()), ") is out of range"), ptr)); } - case detail::value_t::array: - { - if (JSON_HEDLEY_UNLIKELY(reference_token == "-")) - { - // "-" cannot be used for const access - JSON_THROW(detail::out_of_range::create(402, detail::concat("array index '-' (", std::to_string(ptr->m_data.m_value.array->size()), ") is out of range"), ptr)); - } - - // use unchecked array access; the const operator[] - // asserts that the index exists - ptr = &ptr->operator[](array_index(reference_token)); - break; - } - - case detail::value_t::null: - case detail::value_t::string: - case detail::value_t::boolean: - case detail::value_t::number_integer: - case detail::value_t::number_unsigned: - case detail::value_t::number_float: - case detail::value_t::binary: - case detail::value_t::discarded: - default: - JSON_THROW(detail::out_of_range::create(404, detail::concat("unresolved reference token '", reference_token, "'"), ptr)); + // use unchecked array access; the const operator[] + // asserts that the index exists + const auto idx = array_index(reference_token); + JSON_ASSERT(idx < ptr->m_data.m_value.array->size()); + ptr = &(*ptr->m_data.m_value.array)[idx]; + } + else + { + // primitive value with a reference token left + throw_unresolved(reference_token, ptr); } } @@ -20585,66 +24736,54 @@ class json_pointer { for (const auto& reference_token : reference_tokens) { - switch (ptr->type()) + if (ptr->is_object()) { - case detail::value_t::object: + const auto it = ptr->m_data.m_value.object->find(reference_token); + if (JSON_HEDLEY_UNLIKELY(it == ptr->m_data.m_value.object->end())) { - const auto it = ptr->find(reference_token); - if (JSON_HEDLEY_UNLIKELY(it == ptr->end())) - { - return nullptr; - } - ptr = &*it; - break; - } - - case detail::value_t::array: - { - if (JSON_HEDLEY_UNLIKELY(reference_token == "-")) - { - // "-" always fails the range check - return nullptr; - } - - // a malformed index throws parse_error.106/109; an - // index that is syntactically valid but cannot be - // represented (out_of_range.404/410) is treated like an - // out-of-range index below - typename BasicJsonType::size_type idx{}; - switch (parse_array_index(reference_token, idx)) - { - // the branches differ in their messages, not after JSON_THROW's expansion - // NOLINTNEXTLINE(bugprone-branch-clone) - case array_index_status::leading_zero: - JSON_THROW(detail::parse_error::create(106, 0, detail::concat("array index '", reference_token, "' must not begin with '0'"), nullptr)); - case array_index_status::not_a_number: - JSON_THROW(detail::parse_error::create(109, 0, detail::concat("array index '", reference_token, "' is not a number"), nullptr)); - case array_index_status::unresolved: - case array_index_status::exceeds_size_type: - return nullptr; - case array_index_status::ok: - default: - break; - } - - if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size())) - { - return nullptr; - } - ptr = &ptr->operator[](idx); - break; - } - - case detail::value_t::null: - case detail::value_t::string: - case detail::value_t::boolean: - case detail::value_t::number_integer: - case detail::value_t::number_unsigned: - case detail::value_t::number_float: - case detail::value_t::binary: - case detail::value_t::discarded: - default: return nullptr; + } + ptr = &it->second; + } + else if (ptr->is_array()) + { + if (JSON_HEDLEY_UNLIKELY(is_dash(reference_token))) + { + // "-" always fails the range check + return nullptr; + } + + // a malformed index throws parse_error.106/109; an + // index that is syntactically valid but cannot be + // represented (out_of_range.404/410) is treated like an + // out-of-range index below + typename BasicJsonType::size_type idx{}; + switch (parse_array_index(reference_token, idx)) + { + // the branches differ in their messages, not after JSON_THROW's expansion + // NOLINTNEXTLINE(bugprone-branch-clone) + case array_index_status::leading_zero: + JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_index_leading_zero, 0, detail::concat("array index '", reference_token, "' must not begin with '0'"), nullptr)); + case array_index_status::not_a_number: + JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_index_not_a_number, 0, detail::concat("array index '", reference_token, "' is not a number"), nullptr)); + case array_index_status::unresolved: + case array_index_status::exceeds_size_type: + return nullptr; + case array_index_status::ok: + default: + break; + } + + if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size())) + { + return nullptr; + } + ptr = &(*ptr->m_data.m_value.array)[idx]; + } + else + { + // primitive value with a reference token left + return nullptr; } } @@ -20660,62 +24799,48 @@ class json_pointer { for (const auto& reference_token : reference_tokens) { - switch (ptr->type()) + if (ptr->is_object()) { - case detail::value_t::object: + const auto it = ptr->m_data.m_value.object->find(reference_token); + if (it == ptr->m_data.m_value.object->end()) { - if (!ptr->contains(reference_token)) - { - // we did not find the key in the object - return false; - } - - ptr = &ptr->operator[](reference_token); - break; - } - - case detail::value_t::array: - { - if (JSON_HEDLEY_UNLIKELY(reference_token == "-")) - { - // "-" always fails the range check - return false; - } - - // any parse failure (malformed index, or one that is syntactically - // valid but not representable as size_type) means the reference - // token cannot denote an existing array element -- contains() must - // not throw (see #5395), so it is treated as "not found" - typename BasicJsonType::size_type idx{}; - if (JSON_HEDLEY_UNLIKELY(parse_array_index(reference_token, idx) != array_index_status::ok)) - { - return false; - } - - if (idx >= ptr->size()) - { - // index out of range - return false; - } - - ptr = &ptr->operator[](idx); - break; - } - - case detail::value_t::null: - case detail::value_t::string: - case detail::value_t::boolean: - case detail::value_t::number_integer: - case detail::value_t::number_unsigned: - case detail::value_t::number_float: - case detail::value_t::binary: - case detail::value_t::discarded: - default: - { - // we do not expect primitive values if there is still a - // reference token to process + // we did not find the key in the object return false; } + + ptr = &it->second; + } + else if (ptr->is_array()) + { + if (JSON_HEDLEY_UNLIKELY(is_dash(reference_token))) + { + // "-" always fails the range check + return false; + } + + // any parse failure (malformed index, or one that is syntactically + // valid but not representable as size_type) means the reference + // token cannot denote an existing array element -- contains() must + // not throw (see #5395), so it is treated as "not found" + typename BasicJsonType::size_type idx{}; + if (JSON_HEDLEY_UNLIKELY(parse_array_index(reference_token, idx) != array_index_status::ok)) + { + return false; + } + + if (idx >= ptr->m_data.m_value.array->size()) + { + // index out of range + return false; + } + + ptr = &(*ptr->m_data.m_value.array)[idx]; + } + else + { + // we do not expect primitive values if there is still a + // reference token to process + return false; } } @@ -20743,123 +24868,296 @@ class json_pointer } // check if a nonempty reference string begins with slash - if (JSON_HEDLEY_UNLIKELY(reference_string[0] != '/')) + check_begins_with_slash(reference_string); + + // every slash starts a reference token + const std::size_t size = reference_string.size(); + std::size_t token_count = 0; + for (std::size_t i = 0; i < size; ++i) { - JSON_THROW(detail::parse_error::create(107, 1, detail::concat("JSON pointer must be empty or begin with '/' - was: '", reference_string, "'"), nullptr)); + token_count += static_cast(reference_string[i] == '/'); } + result.reserve(token_count); - // extract the reference tokens: - // - slash: position of the last read slash (or end of string) - // - start: position after the previous slash - for ( - // search for the first slash after the first character - std::size_t slash = reference_string.find_first_of('/', 1), - // set the beginning of the first reference token - start = 1; - // we can stop if start == 0 (if slash == string_t::npos) - start != 0; - // set the beginning of the next reference token - // (will eventually be 0 if slash == string_t::npos) - start = (slash == string_t::npos) ? 0 : slash + 1, - // find next slash - slash = reference_string.find_first_of('/', start)) + // extract the reference tokens: every one runs from after a slash to + // the next slash or the end of the string; a token without a '~' (the + // common case) is stored as it is, others are checked and unescaped + const char* const data = reference_string.data(); + const char* const end = data + size; + for (const char* start = data + 1;; ++start) { - // use the text between the beginning of the reference token - // (start) and the last slash (slash). - const auto count = (slash == string_t::npos ? reference_string.size() : slash) - start; - auto reference_token = string_t(reference_string.data() + start, count); - - // check reference tokens are properly escaped - for (std::size_t pos = reference_token.find_first_of('~'); - pos != string_t::npos; - pos = reference_token.find_first_of('~', pos + 1)) + const char* slash = start; + bool escaped = false; + while (slash != end && *slash != '/') { - JSON_ASSERT(reference_token[pos] == '~'); - - // ~ must be followed by 0 or 1 - if (JSON_HEDLEY_UNLIKELY(pos == reference_token.size() - 1 || - (reference_token[pos + 1] != '0' && - reference_token[pos + 1] != '1'))) - { - JSON_THROW(detail::parse_error::create(108, 0, "escape character '~' must be followed with '0' or '1'", nullptr)); - } + escaped = escaped || *slash == '~'; + ++slash; } - // finally, store the reference token - detail::unescape(reference_token); - result.push_back(reference_token); + const auto count = static_cast(slash - start); + if (JSON_HEDLEY_LIKELY(!escaped)) + { + result.emplace_back(start, count); + } + else + { + auto reference_token = string_t(start, count); + + // check reference tokens are properly escaped + for (std::size_t pos = reference_token.find_first_of('~'); pos != string_t::npos; pos = reference_token.find_first_of('~', pos + 1)) + { + JSON_ASSERT(reference_token[pos] == '~'); + + // ~ must be followed by 0 or 1 + if (JSON_HEDLEY_UNLIKELY(is_invalid_escape(reference_token, pos))) + { + JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_invalid_escape, 0, "escape character '~' must be followed with '0' or '1'", nullptr)); + } + } + + detail::unescape(reference_token); + result.push_back(std::move(reference_token)); + } + + if (slash == end) + { + break; + } + start = slash; } return result; } private: + // Add the entry (key, v) to the object `obj`. Flattened paths are pairwise + // distinct, so objects that can append a key known to be absent + // (ordered_map) skip the lookup, which is linear and would make flatten + // quadratic. Objects with a positional insert (std::map) insert next to + // the entry added last: paths come out in nearly sorted order, so the + // position is usually right and no search from the root is needed. + // All other object types use operator[]. + template + static auto add_flattened(ObjectType& obj, typename ObjectType::iterator& /*hint*/, const string_t& key, ValueType&& v, detail::priority_tag<2> /*unused*/) + -> decltype(obj.emplace_new_key(key, std::forward(v)), void()) + { + obj.emplace_new_key(key, std::forward(v)); + } + + template + static auto add_flattened(ObjectType& obj, typename ObjectType::iterator& hint, const string_t& key, ValueType&& v, detail::priority_tag<1> /*unused*/) + -> decltype(hint = obj.emplace_hint(hint, key, std::forward(v)), void()) + { + hint = obj.emplace_hint(hint, key, std::forward(v)); + ++hint; + } + + template + static void add_flattened(ObjectType& obj, typename ObjectType::iterator& /*hint*/, const string_t& key, ValueType&& v, detail::priority_tag<0> /*unused*/) + { + obj[key] = std::forward(v); + } + + /// the number of entries that flattening @a value adds: its primitive + /// values and empty containers + template + static std::size_t count_flattened(const BasicJsonType& value) + { + std::size_t count = 0; + std::vector pending(1, &value); + while (!pending.empty()) + { + const BasicJsonType* const v = pending.back(); + pending.pop_back(); + if (v->is_array() && !v->m_data.m_value.array->empty()) + { + for (const auto& element : *v->m_data.m_value.array) + { + pending.push_back(&element); + } + } + else if (v->is_object() && !v->m_data.m_value.object->empty()) + { + for (const auto& member : *v->m_data.m_value.object) + { + pending.push_back(&member.second); + } + } + else + { + ++count; + } + } + return count; + } + + // Reserve the storage for @a value of objects that have a reserve(), so + // that it does not have to be moved while it grows + template + static auto reserve_flattened(ObjectType& obj, const BasicJsonType& value, detail::priority_tag<1> /*unused*/) + -> decltype(obj.reserve(std::size_t {}), void()) + { + obj.reserve(count_flattened(value)); + } + + template + static void reserve_flattened(ObjectType& /*obj*/, const BasicJsonType& /*value*/, detail::priority_tag<0> /*unused*/) + {} + + /// append the decimal representation of @a i to @a out + static void append_index(string_t& out, std::size_t i) + { + std::array buf{}; + std::size_t pos = buf.size(); + do + { + buf[--pos] = static_cast('0' + (i % 10)); + i /= 10; + } + while (i != 0); + out.append(buf.data() + pos, buf.size() - pos); + } + /*! @param[in] reference_string the reference string to the current value @param[in] value the value to consider @param[in,out] result the result object to insert values to @note Empty objects or arrays are flattened to `null`. + + The value is walked with an explicit stack rather than the call stack, so + arbitrarily deeply nested values can be flattened. + + @sa https://github.com/nlohmann/json/issues/5393 */ template static void flatten(const string_t& reference_string, const BasicJsonType& value, BasicJsonType& result) { - switch (value.type()) + using object_const_iterator = typename BasicJsonType::object_t::const_iterator; + + // an array or object being walked: the container, the array index or + // object iterator of the next child, and the length of the path of the + // container itself + struct frame { - case detail::value_t::array: + frame(const BasicJsonType* container_, object_const_iterator member_, const std::size_t path_length_) noexcept + : container(container_), member(std::move(member_)), path_length(path_length_) + {} + + const BasicJsonType* container; + std::size_t index = 0; + object_const_iterator member; + std::size_t path_length; + }; + + // The containers being flattened are kept on an explicit stack, and + // every child is flattened completely before the next one, so the + // entries come out in the same order as with a recursive walk. The + // path of the value being flattened is kept in one buffer that grows + // and shrinks with the stack, rather than in a new string per level. + std::vector stack; + string_t path = reference_string; + + // flatten `v`, whose path is `path`: primitives and empty containers + // are added to the result right away; other containers get a frame + auto& object = *result.m_data.m_value.object; + auto hint = object.end(); + const auto enter = [&stack, &path, &object, &hint](const BasicJsonType & v) + { + if (v.is_array()) { - if (value.m_data.m_value.array->empty()) + if (v.m_data.m_value.array->empty()) { // flatten empty array as null - result[reference_string] = nullptr; + add_flattened(object, hint, path, nullptr, detail::priority_tag<2> {}); } else { - // iterate array and use index as a reference string - for (std::size_t i = 0; i < value.m_data.m_value.array->size(); ++i) - { - flatten(detail::concat(reference_string, '/', std::to_string(i)), - value.m_data.m_value.array->operator[](i), result); - } + stack.emplace_back(&v, object_const_iterator(), path.size()); } - break; } - - case detail::value_t::object: + else if (v.is_object()) { - if (value.m_data.m_value.object->empty()) + if (v.m_data.m_value.object->empty()) { // flatten empty object as null - result[reference_string] = nullptr; + add_flattened(object, hint, path, nullptr, detail::priority_tag<2> {}); } else { - // iterate object and use keys as reference string - for (const auto& element : *value.m_data.m_value.object) - { - flatten(detail::concat(reference_string, '/', detail::escape(element.first)), element.second, result); - } + stack.emplace_back(&v, v.m_data.m_value.object->begin(), path.size()); } - break; } - - case detail::value_t::null: - case detail::value_t::string: - case detail::value_t::boolean: - case detail::value_t::number_integer: - case detail::value_t::number_unsigned: - case detail::value_t::number_float: - case detail::value_t::binary: - case detail::value_t::discarded: - default: + else { // add a primitive value with its reference string - result[reference_string] = value; - break; + add_flattened(object, hint, path, v, detail::priority_tag<2> {}); + } + }; + + reserve_flattened(object, value, detail::priority_tag<1> {}); + enter(value); + while (!stack.empty()) + { + // the frame is changed through stack.back(): enter() may push a + // frame, which would invalidate a reference to it + const BasicJsonType* const container = stack.back().container; + + // the path of the previous child is replaced by the path of the + // next one; for an array element in one resize + const std::size_t path_length = stack.back().path_length; + + if (container->is_array()) + { + const auto& array = *container->m_data.m_value.array; + const std::size_t i = stack.back().index; + if (i == array.size()) + { + stack.pop_back(); + continue; + } + + // iterate array and use index as a reference string + ++stack.back().index; + std::array digits{}; + std::size_t pos = digits.size(); + std::size_t rest = i; + do + { + digits[--pos] = static_cast('0' + (rest % 10)); + rest /= 10; + } + while (rest != 0); + const std::size_t count = digits.size() - pos; + path.resize(path_length + 1 + count); + char* const out = &path[path_length]; + out[0] = '/'; + std::memcpy(out + 1, digits.data() + pos, count); + enter(array[i]); + } + else + { + const object_const_iterator it = stack.back().member; + if (it == container->m_data.m_value.object->end()) + { + stack.pop_back(); + continue; + } + + // iterate object and use keys as reference string + ++stack.back().member; + path.resize(path_length); + path.push_back('/'); + detail::escape_into(path, it->first); + enter(it->second); } } + + // the entries were added without going through basic_json, so the + // parent pointers (JSON_DIAGNOSTICS) have to be set now + result.set_parents(); } /*! @@ -20878,42 +25176,159 @@ class json_pointer { if (JSON_HEDLEY_UNLIKELY(!value.is_object())) { - JSON_THROW(detail::type_error::create(314, "only objects can be unflattened", &value)); + JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_not_object, "only objects can be unflattened", &value)); } BasicJsonType result; - // collect the pointer prefixes that have a reference token 0 among - // their children; the values below them are arrays, all others are - // objects (see array_parents_t) - array_parents_t array_parents; + // check every key, and collect the prefixes that have a reference + // token 0 among their children; the values below them are arrays, + // all others are objects (see array_prefix_set) + array_prefix_set arrays; + const string_t* previous_key = nullptr; for (const auto& element : *value.m_data.m_value.object) { - json_pointer ptr(element.first); - std::vector prefix; - for (auto& reference_token : ptr.reference_tokens) - { - if (reference_token == "0") - { - array_parents.insert(prefix); - } - prefix.push_back(std::move(reference_token)); - } + scan_key(element.first, previous_key, arrays); + previous_key = &element.first; } + // values[i] is the value after the first i reference tokens of the + // previous key, ends[i] the length of that prefix of the key. Keys are + // often sorted, so most of the path to the value of a key is the + // same as for the previous key and need not be looked up again. Only + // the values below the common prefix are dropped, and only they can + // move when a value is added: the ones above are not inside the + // container that grows. + std::vector values(1, &result); + std::vector ends(1, 0); + const string_t* previous = nullptr; + string_t token; + // iterate the JSON object values for (const auto& element : *value.m_data.m_value.object) { if (JSON_HEDLEY_UNLIKELY(!element.second.is_primitive())) { - JSON_THROW(detail::type_error::create(315, "values in object must be primitive", &element.second)); + JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_value_not_primitive, "values in object must be primitive", &element.second)); } + const string_t& key = element.first; + const char* const data = key.data(); + + // the number of reference tokens the key has in common with the + // previous one + std::size_t depth = 0; + if (previous != nullptr) + { + const std::size_t same = common_prefix_length(data, previous->data(), (std::min)(key.size(), previous->size())); + depth = ends.size() - 1; + while (depth > 0 && (ends[depth] > same || (ends[depth] == same && same < key.size() && data[same] != '/'))) + { + --depth; + } + } + values.resize(depth + 1); + ends.resize(depth + 1); + previous = &key; + // Assign the value to the reference pointed to by JSON pointer. Note // that if the JSON pointer is "" (i.e., points to the whole value), - // function get_and_create returns a reference to the result itself. - // An assignment will then create a primitive value. - json_pointer(element.first).get_and_create(result, array_parents) = element.second; + // the value is the result itself. An assignment will then create a + // primitive value. + BasicJsonType* current = values.back(); + for (std::size_t pos = ends.back(); pos < key.size();) + { + // the reference token between the slash at pos and the next + std::size_t end = key.find_first_of('/', pos + 1); + if (end == string_t::npos) + { + end = key.size(); + } + const std::size_t token_size = end - pos - 1; + + // a token that is a plain index of up to 9 digits, which is + // the case for almost all array elements, is not copied + std::size_t index = 0; + bool plain_index = current->type() == detail::value_t::array && token_size != 0 && token_size <= 9 && (token_size == 1 || data[pos + 1] != '0'); + for (std::size_t k = pos + 1; plain_index && k < end; ++k) + { + plain_index = data[k] >= '0' && data[k] <= '9'; + index = (index * 10) + static_cast(data[k] - '0'); + } + if (plain_index) + { + current = ¤t->operator[](static_cast(index)); + values.push_back(current); + ends.push_back(end); + pos = end; + continue; + } + + token.clear(); + token.append(data + pos + 1, token_size); + if (token.find_first_of('~') != string_t::npos) + { + detail::unescape(token); + } + + switch (current->type()) + { + case detail::value_t::null: + { + // a first child 0 makes the value an array without a look-up + if ((token_size == 1 && data[pos + 1] == '0') || arrays.contains(data, pos)) + { + // some reference token below this position is 0, so the + // value is an array + *current = BasicJsonType(detail::value_t::array); + reserve_elements(*current->m_data.m_value.array, 2, detail::priority_tag<1> {}); + current = ¤t->operator[](array_index(token)); + } + else + { + // start a new object otherwise + current = ¤t->operator[](token); + } + break; + } + + case detail::value_t::object: + { + // create an entry in the object + current = &object_member(*current, token); + break; + } + + case detail::value_t::array: + { + // create an entry in the array + current = ¤t->operator[](array_index(token)); + break; + } + + /* + The following code is only reached if there exists a reference + token _and_ the current value is primitive. In this case, we have + an error situation, because primitive values may only occur as + a single value; that is, with an empty list of reference tokens. + */ + case detail::value_t::string: + case detail::value_t::boolean: + case detail::value_t::number_integer: + case detail::value_t::number_unsigned: + case detail::value_t::number_float: + case detail::value_t::binary: + case detail::value_t::discarded: + default: + JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_invalid_value, "invalid value to unflatten", &result)); + } + + values.push_back(current); + ends.push_back(end); + pos = end; + } + + *current = element.second; } return result; @@ -20952,7 +25367,7 @@ class json_pointer = delete; #else { - return *this == json_pointer(rhs); + return equals_string(rhs); } #endif @@ -21036,7 +25451,7 @@ inline bool operator==(const json_pointer& lhs, = delete; #else { - return lhs == json_pointer(rhs); + return lhs.equals_string(rhs); } #endif @@ -21049,7 +25464,7 @@ inline bool operator==(const StringType& lhs, = delete; #else { - return json_pointer(lhs) == rhs; + return rhs.equals_string(lhs); } #endif @@ -21094,6 +25509,20 @@ inline bool operator<(const json_pointer& lhs, } #endif +namespace detail +{ +/// the reference tokens of a json_pointer, for code that resolves pointers +/// without a basic_json value (such as the zero-copy view) +struct json_pointer_access +{ + template + static const std::vector::string_t>& reference_tokens(const json_pointer& ptr) noexcept + { + return ptr.reference_tokens; + } +}; +} // namespace detail + NLOHMANN_JSON_NAMESPACE_END // #include @@ -21186,9 +25615,9 @@ class json_ref } // namespace detail NLOHMANN_JSON_NAMESPACE_END - +// IWYU pragma: export // #include - +// IWYU pragma: export // #include // #include @@ -21212,7 +25641,7 @@ NLOHMANN_JSON_NAMESPACE_END -#include // reverse +#include // max, min, reverse #include // array #include // isnan, isinf #include // uint8_t, uint16_t, uint32_t, uint64_t @@ -21223,12 +25652,12 @@ NLOHMANN_JSON_NAMESPACE_END #include // move #include // vector -#ifdef _MSC_VER - #include // _byteswap_ushort, _byteswap_ulong, _byteswap_uint64 -#endif +// #include // #include +// #include + // #include // #include @@ -21246,7 +25675,10 @@ NLOHMANN_JSON_NAMESPACE_END +#include // max +#include // array #include // size_t +#include // memcpy #include // shared_ptr, make_shared #include // basic_string #include // conditional, integral_constant, is_same @@ -21287,32 +25719,258 @@ template struct output_adapter_protocol template using output_adapter_t = std::shared_ptr>; +/// the largest number of characters a writer may reserve() from a sink at once; +/// output_vector_sink also appends writes of at least this size with insert() +constexpr std::size_t max_reserve_size = 256; + +/// @brief copy @a length characters, at most 32, without a call of memcpy() +/// +/// Short strings are the typical payload of a binary value, and a call of +/// memcpy() costs more than copying them. A copy of 16 to 32 characters is two +/// possibly overlapping 16-byte copies, one of 8 to 15 two 8-byte copies, and +/// so on down to a single character; each is a fixed-size load and store. +/// Nothing outside of [src, src + length) is read or written. +template +inline void copy_short(CharType* dst, const CharType* src, const std::size_t length, std::true_type /*is_byte*/) noexcept +{ + JSON_ASSERT(length <= 32); + if (length >= 16) + { + std::array first; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read + std::array last; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read + std::memcpy(first.data(), src, 16); + std::memcpy(last.data(), src + (length - 16), 16); + std::memcpy(dst, first.data(), 16); + std::memcpy(dst + (length - 16), last.data(), 16); + } + else if (length >= 8) + { + std::array first; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read + std::array last; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read + std::memcpy(first.data(), src, 8); + std::memcpy(last.data(), src + (length - 8), 8); + std::memcpy(dst, first.data(), 8); + std::memcpy(dst + (length - 8), last.data(), 8); + } + else if (length >= 4) + { + std::array first; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read + std::array last; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read + std::memcpy(first.data(), src, 4); + std::memcpy(last.data(), src + (length - 4), 4); + std::memcpy(dst, first.data(), 4); + std::memcpy(dst + (length - 4), last.data(), 4); + } + else if (length != 0) + { + // 1 to 3: the first, the middle, and the last character + const CharType a = src[0]; + const CharType b = src[length >> 1]; + const CharType c = src[length - 1]; + dst[0] = a; + dst[length >> 1] = b; + dst[length - 1] = c; + } +} + +/// @brief copy @a length characters, of a character type wider than a byte +template +inline void copy_short(CharType* dst, const CharType* src, const std::size_t length, std::false_type /*is_byte*/) noexcept +{ + std::memcpy(dst, src, length * sizeof(CharType)); +} + +/// the number of characters an empty vector is grown to by output_vector_sink +constexpr std::size_t min_vector_sink_capacity = 64; + /// @brief non-virtual output sink writing into a std::vector /// /// This sink is not part of the virtual output_adapter_protocol hierarchy: it is /// passed to binary_writer by value as a template parameter, so /// write_character()/write_characters() are ordinary (inlinable) calls with no /// vtable lookup and no shared_ptr. It is used for the common -/// `to_cbor`/`to_msgpack`/... into a std::vector. output_vector_adapter below -/// wraps this same sink to provide the virtual interface. +/// `to_cbor`/`to_msgpack`/... into a std::vector. +/// +/// Most writes are a few bytes - a type marker, or a number of 1, 2, 4, or 8 +/// bytes - and appending those with push_back() or insert() cost more than +/// encoding them: insert() is an out-of-line call into a routine that can insert +/// anywhere, and resize() is one as well with libstdc++. The sink therefore keeps +/// its own write position: it grows the vector geometrically ahead of the +/// position and copies each write to it, so a write is a capacity check and a +/// short copy. The destructor shrinks the vector to the bytes actually written, +/// also when a write throws; shrinking a vector never allocates. The sink owns +/// that trim, so it can be moved but not copied, and the vector must not be used +/// while the sink is alive. template> class output_vector_sink { public: explicit output_vector_sink(std::vector& vec) noexcept + : v(&vec), cursor(vec.data() + vec.size()), limit(cursor) + {} + + output_vector_sink(const output_vector_sink&) = delete; + output_vector_sink& operator=(const output_vector_sink&) = delete; + output_vector_sink& operator=(output_vector_sink&&) = delete; + + output_vector_sink(output_vector_sink&& other) noexcept + : v(other.v), cursor(other.cursor), limit(other.limit) + { + other.v = nullptr; + } + + ~output_vector_sink() + { + if (v != nullptr) + { + v->resize(position()); + } + } + + // always inlined: GCC otherwise keeps these out of line in a large + // translation unit, and a call per byte or number costs more than the write + JSON_HEDLEY_ALWAYS_INLINE + void write_character(CharType c) + { + if (JSON_HEDLEY_UNLIKELY(cursor == limit)) + { + grow(1); + } + *cursor++ = c; + } + + // no JSON_HEDLEY_NON_NULL here: binary_writer legitimately passes a null + // pointer with length 0 for empty strings/binary values; memcpy must not + // see it. + JSON_HEDLEY_ALWAYS_INLINE + void write_characters(const CharType* s, std::size_t length) + { + if (JSON_HEDLEY_UNLIKELY(space() < length)) + { + if (length >= max_reserve_size) + { + append(s, length); + return; + } + grow(length); + } + if (length <= 32) + { + copy_short(cursor, s, length, std::integral_constant()); + cursor += length; + } + else + { + std::memcpy(cursor, s, length * sizeof(CharType)); + cursor += length; + } + } + + /// @brief space for writing up to @a length characters directly + /// + /// Returns a pointer to at least @a length (at most @ref max_reserve_size) + /// writable characters at the write position; @ref commit then keeps the + /// first ones of them. A writer can store whole words there and keep only + /// the bytes it needs, and keep its write pointer in a register across many + /// values instead of updating the sink after each. + JSON_HEDLEY_ALWAYS_INLINE + CharType* reserve(const std::size_t length) + { + JSON_ASSERT(length <= max_reserve_size); + if (JSON_HEDLEY_UNLIKELY(space() < length)) + { + grow(length); + } + return cursor; + } + + /// keep the first @a length characters written to the space from reserve() + JSON_HEDLEY_ALWAYS_INLINE + void commit(const std::size_t length) noexcept + { + cursor += length; + } + + /// nothing to pass on: the characters are in the vector (see output_adapter_sink) + void flush() noexcept + {} + + private: + /// number of characters of *v written so far + std::size_t position() const noexcept + { + return static_cast(cursor - v->data()); + } + + /// number of characters that fit the vector behind the write position + std::size_t space() const noexcept + { + return static_cast(limit - cursor); + } + + /// make room for at least @a length more characters after the position; + /// kept out of line so that the write functions stay small enough for + /// GCC to inline them into the writer + JSON_HEDLEY_NEVER_INLINE + void grow(const std::size_t length) + { + // use the capacity the vector already has, else at least double it; + // never less than min_vector_sink_capacity, so that a small value does not + // reallocate for every few bytes + const std::size_t written = position(); + const std::size_t needed = written + length; + v->resize((std::max)(min_vector_sink_capacity, needed <= v->capacity() + ? v->capacity() + : (std::max)(needed, v->capacity() * 2))); + cursor = v->data() + written; + limit = v->data() + v->size(); + } + + /// append @a length (at least max_reserve_size) characters that do not fit + /// the space ahead of the position: insert() copies them once into the + /// grown vector, where grow() would first zero the new space and then copy + /// over it - noticeable for a large string or binary value + JSON_HEDLEY_NEVER_INLINE + void append(const CharType* s, const std::size_t length) + { + const std::size_t written = position(); + v->resize(written); + if (v->capacity() - written < length) + { + // grow geometrically, with room for the writes that follow, so + // that the next grow() need not reallocate right away + v->reserve((std::max)(v->capacity() * 2, written + length + max_reserve_size)); + } + v->insert(v->end(), s, s + length); + cursor = v->data() + v->size(); + limit = cursor; + } + + std::vector* v; + /// the write position: in *v, at the number of characters written so far + CharType* cursor; + /// the end of *v + CharType* limit; +}; + +/// output adapter for byte vectors +template> +class output_vector_adapter : public output_adapter_protocol +{ + public: + explicit output_vector_adapter(std::vector& vec) noexcept : v(vec) {} - void write_character(CharType c) + void write_character(CharType c) override { v.push_back(c); } // no JSON_HEDLEY_NON_NULL here: binary_writer legitimately passes a null - // pointer with length 0 for empty strings/binary values. Appending an empty - // range is a no-op; the type-erased path tolerates this via the (unattributed) - // virtual base, and the concrete sink must do the same. - void write_characters(const CharType* s, std::size_t length) + // pointer with length 0 for empty strings/binary values; appending an empty + // range is a no-op + void write_characters(const CharType* s, std::size_t length) override { v.insert(v.end(), s, s + length); } @@ -21321,33 +25979,6 @@ class output_vector_sink std::vector& v; }; -/// output adapter for byte vectors -/// -/// The appending itself lives in output_vector_sink; this class only adds the -/// virtual output_adapter_protocol interface on top of it, so both the -/// type-erased and the templated path share one implementation. -template> -class output_vector_adapter : public output_adapter_protocol -{ - public: - explicit output_vector_adapter(std::vector& vec) noexcept - : sink(vec) - {} - - void write_character(CharType c) override - { - sink.write_character(c); - } - - void write_characters(const CharType* s, std::size_t length) override - { - sink.write_characters(s, length); - } - - private: - output_vector_sink sink; -}; - #ifndef JSON_NO_IO /// output adapter for output streams template @@ -21402,9 +26033,17 @@ class output_string_adapter : public output_adapter_protocol /// /// Wraps the polymorphic output_adapter_t so the same binary_writer template can /// also target arbitrary adapters (output streams, strings, user-provided -/// adapters) via the `output_adapter`-based overloads. Each write still goes -/// through one virtual call, exactly as before; only the concrete sinks above -/// avoid it. +/// adapters) via the `output_adapter`-based overloads. +/// +/// A write through the adapter is a virtual call, and for a stream a call with +/// a sentry on top, which costs more than encoding the few bytes of most values. +/// The sink therefore collects what is written in a buffer and passes it on in +/// one piece when it is full, a write is too large for it, or @ref flush is +/// called. Everything written reaches the adapter in the same order and with +/// the same bytes; the destructor passes on what is left, so that a write that +/// throws leaves the adapter with exactly the bytes written before it. Only +/// @ref flush can report an error of the adapter (the destructor must not +/// throw), so it has to be called once the value is written. template class output_adapter_sink { @@ -21415,20 +26054,121 @@ class output_adapter_sink JSON_ASSERT(oa); } + output_adapter_sink(const output_adapter_sink&) = delete; + output_adapter_sink& operator=(const output_adapter_sink&) = delete; + output_adapter_sink& operator=(output_adapter_sink&&) = delete; + + output_adapter_sink(output_adapter_sink&& other) noexcept + : oa(std::move(other.oa)), used(other.used) + { + std::memcpy(buffer.data(), other.buffer.data(), other.used * sizeof(CharType)); + other.used = 0; + } + + ~output_adapter_sink() + { + // a normal return has flushed already, so this is for a write that threw + if (used != 0) + { + JSON_TRY + { + flush(); + } + JSON_CATCH(...) + { + // nothing can be done about an adapter that throws here + } + } + } + + JSON_HEDLEY_ALWAYS_INLINE void write_character(CharType c) { - oa->write_character(c); + if (JSON_HEDLEY_UNLIKELY(used == buffer_size)) + { + flush(); + } + buffer[used++] = c; } // no JSON_HEDLEY_NON_NULL: forwards (null, 0) for empty payloads, exactly as // the type-erased path already did before this sink existed + JSON_HEDLEY_ALWAYS_INLINE void write_characters(const CharType* s, std::size_t length) { - oa->write_characters(s, length); + if (JSON_HEDLEY_UNLIKELY(buffer_size - used < length)) + { + write_large(s, length); + return; + } + if (length <= 32) + { + copy_short(buffer.data() + used, s, length, std::integral_constant()); + used += length; + } + else + { + std::memcpy(buffer.data() + used, s, length * sizeof(CharType)); + used += length; + } + } + + /// space for writing up to @a length characters (see + /// output_vector_sink::reserve) + JSON_HEDLEY_ALWAYS_INLINE + CharType* reserve(const std::size_t length) + { + JSON_ASSERT(length <= max_reserve_size); + if (JSON_HEDLEY_UNLIKELY(buffer_size - used < length)) + { + flush(); + } + return buffer.data() + used; + } + + /// keep the first @a length characters written to the space from reserve() + JSON_HEDLEY_ALWAYS_INLINE + void commit(const std::size_t length) noexcept + { + used += length; + } + + /// pass everything written so far on to the adapter + void flush() + { + if (used != 0) + { + // reset first: the characters count as written even if the adapter throws + const std::size_t length = used; + used = 0; + oa->write_characters(buffer.data(), length); + } } private: + /// a write that does not fit the space left in the buffer + JSON_HEDLEY_NEVER_INLINE + void write_large(const CharType* s, const std::size_t length) + { + flush(); + if (length >= buffer_size) + { + oa->write_characters(s, length); + } + else + { + std::memcpy(buffer.data(), s, length * sizeof(CharType)); + used = length; + } + } + + /// large enough for reserve() at any time + static constexpr std::size_t buffer_size = 4 * max_reserve_size; + output_adapter_t oa; + /// number of characters of @ref buffer not yet passed on + std::size_t used = 0; + std::array buffer{}; }; /// @brief whether std::basic_string has a non-deprecated std::char_traits @@ -21587,6 +26327,21 @@ std::size_t binary_reserve_hint(const BasicJsonType& j) return 0; } +/*! +@brief the capacity to reserve for the result of a binary serialization into a + std::vector + +The hint of @ref binary_reserve_hint, but not less than the 64 characters that +output_vector_sink grows an empty vector to on its first write anyway. A small +document then costs one allocation, instead of an allocation of a few bytes that +the first write replaces with one of 64. +*/ +template +std::size_t binary_initial_capacity(const BasicJsonType& j) +{ + return (std::max)(binary_reserve_hint(j), min_vector_sink_capacity); +} + /*! @brief serialization to BJData, BON8, BSON, CBOR, MessagePack, and UBJSON values */ @@ -21594,6 +26349,12 @@ template::value, + const string_t&, string_t >::type; using binary_t = typename BasicJsonType::binary_t; using number_float_t = typename BasicJsonType::number_float_t; @@ -21629,9 +26390,22 @@ class binary_writer template < typename SinkType = OutputSinkType, typename std::enable_if < std::is_constructible>::value, int >::type = 0 > explicit binary_writer(output_adapter_t adapter, const error_handler_t error_handler_ = binary_writer_default_error_handler()) - : oa(SinkType(std::move(adapter))), error_handler(error_handler_) + : oa(std::move(adapter)), error_handler(error_handler_) {} + /*! + @brief pass on everything written so far to the output adapter + + A writer over an output_adapter_sink buffers what it writes; this has to + be called after the last write, or the buffered end is only passed on, and + an error of the adapter lost, when the writer is destroyed. A writer over + an output_vector_sink writes straight into the vector. + */ + void flush() + { + oa.flush(); + } + /*! @param[in] j JSON value to serialize @throw type_error.316 if a string value or an object key is not valid @@ -21660,7 +26434,7 @@ class binary_writer case value_t::discarded: default: { - JSON_THROW(type_error::create(317, concat("to serialize to BSON, top-level type must be object, but is ", j.type_name()), &j)); + JSON_THROW(type_error::create(exception_id::type_not_serializable, concat("to serialize to BSON, top-level type must be object, but is ", j.type_name()), &j)); } } } @@ -21707,24 +26481,12 @@ class binary_writer } case value_t::number_integer: - { - if (j.m_data.m_value.number_integer >= 0) - { - // CBOR does not differentiate between positive signed - // integers and unsigned integers - write_cbor_head(0x00, static_cast(j.m_data.m_value.number_integer)); - } - else - { - // a negative integer n is encoded as -1 - n - write_cbor_head(0x20, static_cast(-1 - j.m_data.m_value.number_integer)); - } - break; - } - case value_t::number_unsigned: { - write_cbor_head(0x00, j.m_data.m_value.number_unsigned); + // one case for both, so that an array mixing negative + // (number_integer) and non-negative (number_unsigned) values + // does not mispredict the switch + write_cbor_integer(j, std::integral_constant()); break; } @@ -21753,28 +26515,13 @@ class binary_writer case value_t::string: { - string_t storage; - const string_t& value = sanitize_utf8_for_write(*j.m_data.m_value.string, j, storage); - - // step 1: write control byte and the string length - write_cbor_head(0x60, value.size()); - - // step 2: write the string - oa.write_characters( - reinterpret_cast(value.data()), - value.size()); + write_cbor_string(*j.m_data.m_value.string, j); break; } case value_t::array: { - // step 1: write control byte and the array size - write_cbor_head(0x80, j.m_data.m_value.array->size()); - - for (const auto& el : *j.m_data.m_value.array) - { - write_cbor(el, depth + 1); - } + write_cbor_array(j, depth); break; } @@ -21816,40 +26563,20 @@ class binary_writer write_cbor_head(0x40, N); // step 2: write each element - oa.write_characters( - reinterpret_cast(j.m_data.m_value.binary->data()), - N); + write_bytes(j.m_data.m_value.binary->data(), N); break; } case value_t::object: { - // step 1: write control byte and the object size - write_cbor_head(0xA0, j.m_data.m_value.object->size()); - - for (const auto& el : *j.m_data.m_value.object) - { - // el.first is checked here, against the object as - // diagnostics context, because write_cbor(el.first) - // converts it to a temporary basic_json that would be - // used as the context instead; for error_handler_t::keep - // and ::replace/::ignore the recursive write_cbor(el.first) - // call below handles the key like any other string, so no - // separate check is needed here for those - if (error_handler == error_handler_t::strict) - { - check_utf8(el.first, j); - } - write_cbor(el.first); - write_cbor(el.second, depth + 1); - } + write_cbor_object(j, depth); break; } case value_t::discarded: default: - throw_on_discarded(j, "CBOR"); + throw_on_discarded(j, input_format_t::cbor); } } @@ -21863,7 +26590,7 @@ class binary_writer { if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(length))) { - JSON_THROW(out_of_range::create(412, concat("MessagePack length ", std::to_string(length), " exceeds maximum of ", std::to_string((std::numeric_limits::max)())), &j)); + throw_msgpack_length_too_large(length, j); } static_cast(j); @@ -21941,110 +26668,30 @@ class binary_writer } case value_t::number_integer: - { - if (j.m_data.m_value.number_integer >= 0) - { - // MessagePack does not differentiate between positive - // signed integers and unsigned integers. - write_msgpack_unsigned(static_cast(j.m_data.m_value.number_integer)); - } - else - { - if (j.m_data.m_value.number_integer >= -32) - { - // negative fixnum - write_number(static_cast(j.m_data.m_value.number_integer)); - } - else if (j.m_data.m_value.number_integer >= (std::numeric_limits::min)() && - j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) - { - // int 8 - oa.write_character(to_char_type(0xD0)); - write_number(static_cast(j.m_data.m_value.number_integer)); - } - else if (j.m_data.m_value.number_integer >= (std::numeric_limits::min)() && - j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) - { - // int 16 - oa.write_character(to_char_type(0xD1)); - write_number(static_cast(j.m_data.m_value.number_integer)); - } - else if (j.m_data.m_value.number_integer >= (std::numeric_limits::min)() && - j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) - { - // int 32 - oa.write_character(to_char_type(0xD2)); - write_number(static_cast(j.m_data.m_value.number_integer)); - } - else - { - // int 64 - oa.write_character(to_char_type(0xD3)); - write_number(static_cast(j.m_data.m_value.number_integer)); - } - } - break; - } - case value_t::number_unsigned: { - write_msgpack_unsigned(static_cast(j.m_data.m_value.number_unsigned)); + // one case for both, so that an array mixing negative + // (number_integer) and non-negative (number_unsigned) values + // does not mispredict the switch + write_msgpack_integer(j, std::integral_constant()); break; } case value_t::number_float: { - write_compact_float(j.m_data.m_value.number_float, to_char_type(0xCA), to_char_type(0xCB)); + write_msgpack_float(j.m_data.m_value.number_float); break; } case value_t::string: { - string_t storage; - const string_t& value = sanitize_utf8_for_write(*j.m_data.m_value.string, j, storage); - - // step 1: write control byte and the string length - const auto N = to_msgpack_length(value.size(), j); - if (N <= 31) - { - // fixstr - write_number(static_cast(0xA0 | N)); - } - else if (N <= (std::numeric_limits::max)()) - { - // str 8 - oa.write_character(to_char_type(0xD9)); - write_number(static_cast(N)); - } - else if (N <= (std::numeric_limits::max)()) - { - // str 16 - oa.write_character(to_char_type(0xDA)); - write_number(static_cast(N)); - } - else - { - // str 32 - oa.write_character(to_char_type(0xDB)); - write_number(static_cast(N)); - } - - // step 2: write the string - oa.write_characters( - reinterpret_cast(value.data()), - value.size()); + write_msgpack_string(*j.m_data.m_value.string, j); break; } case value_t::array: { - // step 1: write control byte and the array size - write_msgpack_array_prefix(j.m_data.m_value.array->size(), j); - - for (const auto& el : *j.m_data.m_value.array) - { - write_msgpack(el, depth + 1); - } + write_msgpack_array(j, depth); break; } @@ -22122,16 +26769,14 @@ class binary_writer { if (JSON_HEDLEY_UNLIKELY(j.m_data.m_value.binary->subtype() > (std::numeric_limits::max)())) { - JSON_THROW(out_of_range::create(415, concat("subtype ", std::to_string(j.m_data.m_value.binary->subtype()), " is too large for the MessagePack ext type (max 255)"), &j)); + throw_subtype_out_of_range(j.m_data.m_value.binary->subtype(), "the MessagePack ext type", j); } write_number(static_cast(j.m_data.m_value.binary->subtype())); } // step 2: write the byte string - oa.write_characters( - reinterpret_cast(j.m_data.m_value.binary->data()), - N); + write_bytes(j.m_data.m_value.binary->data(), N); break; } @@ -22139,26 +26784,13 @@ class binary_writer case value_t::object: { // step 1: write control byte and the object size - write_msgpack_object_prefix(j.m_data.m_value.object->size(), j); - - for (const auto& el : *j.m_data.m_value.object) - { - // as in write_cbor, el.first is checked here against the - // object as diagnostics context; the recursive call below - // handles keep/replace/ignore like any other string - if (error_handler == error_handler_t::strict) - { - check_utf8(el.first, j); - } - write_msgpack(el.first); - write_msgpack(el.second, depth + 1); - } + write_msgpack_object(j, depth); break; } case value_t::discarded: default: - throw_on_discarded(j, "MessagePack"); + throw_on_discarded(j, input_format_t::msgpack); } } @@ -22214,14 +26846,16 @@ class binary_writer } case value_t::number_integer: - { - write_number_with_ubjson_prefix(j.m_data.m_value.number_integer, add_prefix, use_bjdata); - break; - } - case value_t::number_unsigned: { - write_number_with_ubjson_prefix(j.m_data.m_value.number_unsigned, add_prefix, use_bjdata); + if (j.m_data.m_type == value_t::number_integer) + { + write_number_with_ubjson_prefix(j.m_data.m_value.number_integer, add_prefix, use_bjdata); + } + else + { + write_number_with_ubjson_prefix(j.m_data.m_value.number_unsigned, add_prefix, use_bjdata); + } break; } @@ -22233,28 +26867,28 @@ class binary_writer case value_t::string: { - string_t storage; - const string_t& value = sanitize_utf8_for_write(*j.m_data.m_value.string, j, storage); - - if (add_prefix) - { - oa.write_character(to_char_type('S')); - } - write_number_with_ubjson_prefix(value.size(), true, use_bjdata); - oa.write_characters( - reinterpret_cast(value.data()), - value.size()); + write_ubjson_text(*j.m_data.m_value.string, j, add_prefix, use_bjdata); break; } case value_t::array: { bool prefix_required = true; - const bool write_closer = write_ubjson_start_array(j, use_count, use_type, add_prefix, use_bjdata, prefix_required); + const bool write_closer = write_ubjson_start_container(j, use_count, use_type, add_prefix, use_bjdata, prefix_required); - for (const auto& el : *j.m_data.m_value.array) { - write_ubjson(el, use_count, use_type, prefix_required, use_bjdata, bjdata_version, depth + 1); + const auto& array = *j.m_data.m_value.array; + if (starts_with_ubjson_number(array, use_bjdata, std::integral_constant())) + { + write_ubjson_array_elements(array, use_count, use_type, prefix_required, use_bjdata, bjdata_version, depth); + } + else + { + for (const auto& el : array) + { + write_ubjson(el, use_count, use_type, prefix_required, use_bjdata, bjdata_version, depth + 1); + } + } } if (write_closer) @@ -22276,7 +26910,7 @@ class binary_writer { if (!use_count) { - JSON_THROW(other_error::create(502, "use_type requires use_size = true", &j)); + throw_size_marker_required(j); } oa.write_character(to_char_type('$')); oa.write_character(bjdata_draft3 ? 'B' : 'U'); @@ -22290,19 +26924,11 @@ class binary_writer if (use_type) { - oa.write_characters( - reinterpret_cast(j.m_data.m_value.binary->data()), - j.m_data.m_value.binary->size()); + write_bytes(j.m_data.m_value.binary->data(), j.m_data.m_value.binary->size()); } else { - for (size_t i = 0; i < j.m_data.m_value.binary->size(); ++i) - { - oa.write_character(to_char_type(bjdata_draft3 ? 'B' : 'U')); - // the cast is needed for binary types whose value type - // is not an integer (e.g., std::byte) - oa.write_character(to_char_type(static_cast(j.m_data.m_value.binary->data()[i]))); - } + write_ubjson_binary_bytes(*j.m_data.m_value.binary, bjdata_draft3); } if (!use_count) @@ -22324,18 +26950,9 @@ class binary_writer } bool prefix_required = true; - const bool write_closer = write_ubjson_start_object(j, use_count, use_type, add_prefix, use_bjdata, prefix_required); + const bool write_closer = write_ubjson_start_container(j, use_count, use_type, add_prefix, use_bjdata, prefix_required); - for (const auto& el : *j.m_data.m_value.object) - { - string_t storage; - const string_t& key = sanitize_utf8_for_write(el.first, j, storage); - write_number_with_ubjson_prefix(key.size(), true, use_bjdata); - oa.write_characters( - reinterpret_cast(key.data()), - key.size()); - write_ubjson(el.second, use_count, use_type, prefix_required, use_bjdata, bjdata_version, depth + 1); - } + write_ubjson_members(j, use_count, use_type, prefix_required, use_bjdata, bjdata_version, depth); if (write_closer) { @@ -22347,12 +26964,14 @@ class binary_writer case value_t::discarded: default: - throw_on_discarded(j, use_bjdata ? "BJData" : "UBJSON"); + throw_on_discarded(j, use_bjdata ? input_format_t::bjdata : input_format_t::ubjson); } } /*! @param[in] j JSON value to serialize + + @throw type_error.321 if @a j or a value nested in it is discarded */ void write_bon8(const BasicJsonType& j) { @@ -22371,60 +26990,240 @@ class binary_writer @brief throws because @a j is discarded and cannot be serialized @throw type_error.321 always */ - JSON_HEDLEY_NO_RETURN static void throw_on_discarded(const BasicJsonType& j, const char* format_name) + JSON_HEDLEY_NO_RETURN static void throw_on_discarded(const BasicJsonType& j, const input_format_t format) { static_cast(j); // unused when JSON_NOEXCEPTION is defined - static_cast(format_name); - JSON_THROW(type_error::create(321, concat("cannot serialize discarded value to ", format_name), &j)); + static_cast(format); + JSON_THROW(type_error::create(exception_id::discarded_value_used, concat("cannot serialize discarded value to ", input_format_name(format)), &j)); } + /// how the range of integer type @a NumberType relates to that of @a TargetType + template + struct integer_range + { + /// whether NumberType can hold values above the maximum of TargetType + static constexpr bool may_exceed_max = std::numeric_limits::digits > std::numeric_limits::digits; + /// whether NumberType can hold values below the minimum of TargetType + static constexpr bool may_fall_below_min = std::numeric_limits::is_signed + && (!std::numeric_limits::is_signed || may_exceed_max); + }; + + /*! + @brief whether integer @a n does not exceed the maximum of @a TargetType + + The binary formats store integers in at most 64 bits, but number_integer_t + and number_unsigned_t may be wider (e.g., __int128). The range of the + integer is taken from std::numeric_limits, so a number type whose range + fits into @a TargetType is never checked: the function is then constant + true, and the default int64_t/uint64_t types pay nothing. + */ + template + static constexpr bool integer_fits_max(const NumberType n) noexcept + { + return integer_fits_max(n, std::integral_constant::may_exceed_max>()); + } + + template + static constexpr bool integer_fits_max(const NumberType /*unused*/, std::false_type /*may_exceed*/) noexcept + { + return true; + } + + template + static constexpr bool integer_fits_max(const NumberType n, std::true_type /*may_exceed*/) noexcept + { + // NumberType has more digits than TargetType, so it can hold its maximum + return n <= static_cast((std::numeric_limits::max)()); + } + + /*! + @brief whether integer @a n is not below the minimum of @a TargetType; + constant true if NumberType cannot hold such a value + */ + template + static constexpr bool integer_fits_min(const NumberType n) noexcept + { + return integer_fits_min(n, std::integral_constant::may_fall_below_min>()); + } + + template + static constexpr bool integer_fits_min(const NumberType /*unused*/, std::false_type /*may_fall_below*/) noexcept + { + return true; + } + + template + static constexpr bool integer_fits_min(const NumberType n, std::true_type /*may_fall_below*/) noexcept + { + // NumberType is signed and either TargetType is unsigned (minimum 0) + // or NumberType has more digits, so it can hold the minimum + return n >= static_cast((std::numeric_limits::min)()); + } + + /*! + @brief whether integer @a n lies in the range of @a TargetType + */ + template + static constexpr bool integer_fits(const NumberType n) noexcept + { + return integer_fits_min(n) && integer_fits_max(n); + } + + /*! + @brief the decimal digits of the non-negative integer @a n + */ + template + static std::string integer_to_string(NumberType n, std::false_type /*is_signed*/) + { + std::string result; + do + { + result.push_back(static_cast('0' + (n % 10))); + n = static_cast(n / 10); + } + while (n != 0); + std::reverse(result.begin(), result.end()); + return result; + } + + /*! + @brief the decimal digits of the integer @a n, with a leading '-' if it + is negative + */ + template + static std::string integer_to_string(NumberType n, std::true_type /*is_signed*/) + { + // the remainder of a negative n is not positive; each digit is + // negated on its own, because negating the minimum of NumberType + // as a whole would overflow + const bool negative = n < 0; + std::string result; + do + { + const auto remainder = n % 10; + result.push_back(static_cast('0' + (remainder < 0 ? -remainder : remainder))); + n = static_cast(n / 10); + } + while (n != 0); + if (negative) + { + result.push_back('-'); + } + std::reverse(result.begin(), result.end()); + return result; + } + + /*! + @brief throws because the integer @a j is too large for @a format + @throw out_of_range.407 always + */ + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_integer_out_of_range(const BasicJsonType& j, const input_format_t format, const char* range) + { + // neither std::to_string(), which has no overload for integer types + // wider than 64 bits, nor dump(), which would also instantiate the + // serializer for objects and fail for key types it cannot write + using integer_limits = std::numeric_limits; + using unsigned_limits = std::numeric_limits; + const std::string number = j.is_number_unsigned() + ? integer_to_string(j.m_data.m_value.number_unsigned, std::integral_constant()) + : integer_to_string(j.m_data.m_value.number_integer, std::integral_constant()); + static_cast(number); // unused when JSON_NOEXCEPTION is defined + static_cast(format); + static_cast(range); + JSON_THROW(out_of_range::create(exception_id::integer_too_large, concat("integer number ", number, " cannot be represented by ", input_format_name(format), " as it does not fit ", range), &j)); + } + + // The exceptions below are thrown from functions of their own, out of line: + // the code that builds a message (concat, std::to_string) would otherwise + // make the frame of the function that checks, which most calls never throw. + + /// @throw out_of_range.412 always + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_msgpack_length_too_large(const std::size_t length, const BasicJsonType& j) + { + static_cast(length); // unused when JSON_NOEXCEPTION is defined + static_cast(j); + JSON_THROW(out_of_range::create(exception_id::length_too_large, concat("MessagePack length ", std::to_string(length), " exceeds maximum of ", std::to_string((std::numeric_limits::max)())), &j)); + } + + /// @throw out_of_range.412 always + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_bson_length_too_large(const std::size_t size) + { + static_cast(size); // unused when JSON_NOEXCEPTION is defined + JSON_THROW(out_of_range::create(exception_id::length_too_large, concat("BSON length ", std::to_string(size), " exceeds maximum of ", std::to_string((std::numeric_limits::max)())), nullptr)); + } + + /// @throw out_of_range.415 always + template + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_subtype_out_of_range(const SubtypeType subtype, const char* what, const BasicJsonType& j) + { + static_cast(subtype); // unused when JSON_NOEXCEPTION is defined + static_cast(what); + static_cast(j); + JSON_THROW(out_of_range::create(exception_id::subtype_out_of_range, concat("subtype ", std::to_string(subtype), " is too large for ", what, " (max 255)"), &j)); + } + + /// @throw other_error.407 always + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_size_marker_required(const BasicJsonType& j) + { + static_cast(j); // unused when JSON_NOEXCEPTION is defined + JSON_THROW(other_error::create(exception_id::size_marker_required, "use_type requires use_size = true", &j)); + } + + /// @brief write @a size bytes starting at @a data to the output adapter + JSON_HEDLEY_ALWAYS_INLINE + void write_bytes(const void* data, const std::size_t size) + { + oa.write_characters(static_cast(data), size); + } + + /// @brief write the MessagePack prefix of an array or object with @a N + /// elements: the fix marker @a fix_marker | n, or @a marker_16 or + /// @a marker_32 followed by the length + JSON_HEDLEY_ALWAYS_INLINE + void write_msgpack_container_prefix(const std::size_t N, const BasicJsonType& j, + const std::uint8_t fix_marker, const std::uint8_t marker_16, const std::uint8_t marker_32) + { + const auto n = to_msgpack_length(N, j); + if (n <= 15) + { + // fixarray, fixmap + oa.write_character(to_char_type(static_cast(fix_marker | n))); + } + else if (n <= (std::numeric_limits::max)()) + { + // array 16, map 16 + oa.write_character(to_char_type(marker_16)); + write_number(static_cast(n)); + } + else + { + // array 32, map 32 + oa.write_character(to_char_type(marker_32)); + write_number(static_cast(n)); + } + } + + JSON_HEDLEY_ALWAYS_INLINE void write_msgpack_array_prefix(const std::size_t N, const BasicJsonType& j) { - const auto n = to_msgpack_length(N, j); - if (n <= 15) - { - // fixarray - write_number(static_cast(0x90 | n)); - } - else if (n <= (std::numeric_limits::max)()) - { - // array 16 - oa.write_character(to_char_type(0xDC)); - write_number(static_cast(n)); - } - else - { - // array 32 - oa.write_character(to_char_type(0xDD)); - write_number(static_cast(n)); - } + write_msgpack_container_prefix(N, j, 0x90, 0xDC, 0xDD); } + JSON_HEDLEY_ALWAYS_INLINE void write_msgpack_object_prefix(const std::size_t N, const BasicJsonType& j) { - const auto n = to_msgpack_length(N, j); - if (n <= 15) - { - // fixmap - write_number(static_cast(0x80 | (n & 0xF))); - } - else if (n <= (std::numeric_limits::max)()) - { - // map 16 - oa.write_character(to_char_type(0xDE)); - write_number(static_cast(n)); - } - else - { - // map 32 - oa.write_character(to_char_type(0xDF)); - write_number(static_cast(n)); - } + write_msgpack_container_prefix(N, j, 0x80, 0xDE, 0xDF); } - /// @brief a CBOR or MessagePack array or object whose elements - /// @ref write_cbor_iterative or @ref write_msgpack_iterative is - /// still writing + /// @brief a CBOR, MessagePack, BON8, or (as the base of @ref ubjson_frame) + /// UBJSON array or object whose elements @ref write_container_iterative, + /// @ref write_bon8_iterative, or @ref write_ubjson_iterative is still + /// writing struct binary_container_frame { explicit binary_container_frame(const BasicJsonType* value_) noexcept @@ -22460,19 +27259,82 @@ class binary_writer typename BasicJsonType::array_t::const_iterator array_it{}; }; + /// @brief the CBOR specifics of @ref write_container_iterative + struct cbor_walk + { + static void array_head(binary_writer& w, const BasicJsonType& j) + { + w.write_cbor_head(0x80, j.m_data.m_value.array->size()); + } + + static void object_head(binary_writer& w, const BasicJsonType& j) + { + w.write_cbor_head(0xA0, j.m_data.m_value.object->size()); + } + + static void scalar(binary_writer& w, const BasicJsonType& j) + { + w.write_cbor(j); + } + + static void string(binary_writer& w, const string_t& key, const BasicJsonType& context) + { + w.write_cbor_string(key, context); + } + + template + static void key(binary_writer& w, const KeyType& k, const BasicJsonType& object) + { + w.write_cbor_key(k, object); + } + }; + + /// @brief the MessagePack specifics of @ref write_container_iterative + struct msgpack_walk + { + static void array_head(binary_writer& w, const BasicJsonType& j) + { + w.write_msgpack_array_prefix(j.m_data.m_value.array->size(), j); + } + + static void object_head(binary_writer& w, const BasicJsonType& j) + { + w.write_msgpack_object_prefix(j.m_data.m_value.object->size(), j); + } + + static void scalar(binary_writer& w, const BasicJsonType& j) + { + w.write_msgpack(j); + } + + static void string(binary_writer& w, const string_t& key, const BasicJsonType& context) + { + w.write_msgpack_string(key, context); + } + + template + static void key(binary_writer& w, const KeyType& k, const BasicJsonType& object) + { + w.write_msgpack_key(k, object); + } + }; + /*! - @brief write @a j with @ref write_cbor, or write its header and push a - frame for @ref write_cbor_iterative to continue with its elements + @brief write @a j with @a Walk::scalar, or write its header and push a + frame for @ref write_container_iterative to continue with its + elements A scalar, and an empty array or object, are written out in full: there is - nothing below them for @ref write_cbor_iterative to come back to, so + nothing below them for @ref write_container_iterative to come back to, so nothing is pushed for them. */ - void write_cbor_value_or_push(const BasicJsonType& j, std::vector& stack) + template + JSON_HEDLEY_ALWAYS_INLINE + void write_container_value_or_push(const BasicJsonType& j, std::vector& stack) { if (j.is_array()) { - write_cbor_head(0x80, j.m_data.m_value.array->size()); + Walk::array_head(*this, j); if (!j.m_data.m_value.array->empty()) { stack.emplace_back(&j); @@ -22482,7 +27344,7 @@ class binary_writer if (j.is_object()) { - write_cbor_head(0xA0, j.m_data.m_value.object->size()); + Walk::object_head(*this, j); if (!j.m_data.m_value.object->empty()) { stack.emplace_back(&j); @@ -22490,22 +27352,26 @@ class binary_writer return; } - write_cbor(j); + Walk::scalar(*this, j); } /*! @brief write out @a root and everything below it without the call stack - Emits the same bytes as @ref write_cbor, keeping the containers it has + Emits the same bytes as the recursive writer, keeping the containers it has entered on an explicit stack instead of descending into them. Only reached for values nested deeper than @ref recursion_depth_limit, which is why it - is not written for speed. + is not written for speed. Shared by CBOR and MessagePack, which differ only + in the @a Walk policy (@ref cbor_walk, @ref msgpack_walk); BON8 has its own + walker, as it tracks an open string run and closes a container with a + marker. */ - void write_cbor_iterative(const BasicJsonType& root) + template + void write_container_iterative(const BasicJsonType& root) { - // only a container with elements is ever pushed; see write_cbor_value_or_push + // only a container with elements is ever pushed; see write_container_value_or_push std::vector stack; - write_cbor_value_or_push(root, stack); + write_container_value_or_push(root, stack); while (!stack.empty()) { @@ -22523,7 +27389,7 @@ class binary_writer // read the child before pushing: entering it can move every frame const BasicJsonType* child = &(*current.array_it); ++stack.back().array_it; - write_cbor_value_or_push(*child, stack); + write_container_value_or_push(*child, stack); } else { @@ -22534,117 +27400,54 @@ class binary_writer continue; } - // el.first is checked here, against the object as diagnostics - // context, like the matching check in write_cbor's object case - if (error_handler == error_handler_t::strict) - { - check_utf8(current.object_it->first, *current.value); - } - write_cbor(current.object_it->first); + // the key is written directly (not via a temporary basic_json), + // with the object as diagnostics context, as in the recursive writer + Walk::key(*this, current.object_it->first, *current.value); const BasicJsonType* child = &(current.object_it->second); ++stack.back().object_it; - write_cbor_value_or_push(*child, stack); + write_container_value_or_push(*child, stack); } } } - /*! - @brief write @a j with @ref write_msgpack, or write its header and push a - frame for @ref write_msgpack_iterative to continue with its elements - - @sa @ref write_cbor_value_or_push - */ - void write_msgpack_value_or_push(const BasicJsonType& j, std::vector& stack) + void write_cbor_iterative(const BasicJsonType& root) { - if (j.is_array()) - { - write_msgpack_array_prefix(j.m_data.m_value.array->size(), j); - if (!j.m_data.m_value.array->empty()) - { - stack.emplace_back(&j); - } - return; - } - - if (j.is_object()) - { - write_msgpack_object_prefix(j.m_data.m_value.object->size(), j); - if (!j.m_data.m_value.object->empty()) - { - stack.emplace_back(&j); - } - return; - } - - write_msgpack(j); + write_container_iterative(root); } - /*! - @brief write out @a root and everything below it without the call stack - - @sa @ref write_cbor_iterative - */ void write_msgpack_iterative(const BasicJsonType& root) { - std::vector stack; - write_msgpack_value_or_push(root, stack); - - while (!stack.empty()) - { - const binary_container_frame current = stack.back(); - - if (current.value->is_array()) - { - const auto& array = *current.value->m_data.m_value.array; - if (current.array_it == array.cend()) - { - stack.pop_back(); - continue; - } - - const BasicJsonType* child = &(*current.array_it); - ++stack.back().array_it; - write_msgpack_value_or_push(*child, stack); - } - else - { - const auto& object = *current.value->m_data.m_value.object; - if (current.object_it == object.cend()) - { - stack.pop_back(); - continue; - } - - if (error_handler == error_handler_t::strict) - { - check_utf8(current.object_it->first, *current.value); - } - write_msgpack(current.object_it->first); - const BasicJsonType* child = &(current.object_it->second); - ++stack.back().object_it; - write_msgpack_value_or_push(*child, stack); - } - } + write_container_iterative(root); } - /// @return true when a closing ']' still has to be written after the elements - bool write_ubjson_start_array(const BasicJsonType& j, const bool use_count, const bool use_type, - const bool add_prefix, const bool use_bjdata, bool& prefix_required) + /*! + @brief write the start of an array (@a IsArray) or object: its opening + marker, the optimized type, and the count + + @return true when a closing ']' or '}' still has to be written after the + elements + */ + template + bool write_ubjson_start_container(const BasicJsonType& j, const bool use_count, const bool use_type, + const bool add_prefix, const bool use_bjdata, bool& prefix_required) { prefix_required = true; if (add_prefix) { - oa.write_character(to_char_type('[')); + oa.write_character(to_char_type(IsArray ? '[' : '{')); } - if (use_type && !j.m_data.m_value.array->empty()) + const std::size_t size = IsArray ? j.m_data.m_value.array->size() : j.m_data.m_value.object->size(); + + if (use_type && size != 0) { if (!use_count) { - JSON_THROW(other_error::create(502, "use_type requires use_size = true", &j)); + throw_size_marker_required(j); } const CharType first_prefix = ubjson_prefix(j.front(), use_bjdata); - const bool same_prefix = std::all_of(j.begin() + 1, j.end(), + // the first element of an array has been compared with itself + const bool same_prefix = std::all_of(IsArray ? j.begin() + 1 : j.begin(), j.end(), [this, first_prefix, use_bjdata](const BasicJsonType & v) { return ubjson_prefix(v, use_bjdata) == first_prefix; @@ -22656,8 +27459,9 @@ class binary_writer // those, at one byte per element, so the result can be read back. // Objects are not affected: every element is preceded by its key. const bool valueless_type = (first_prefix == 'Z' || first_prefix == 'T' || first_prefix == 'F'); - const bool excessive_valueless = valueless_type - && j.m_data.m_value.array->size() > detail::max_valueless_container_size; + const bool excessive_valueless = IsArray + && valueless_type + && size > detail::max_valueless_container_size; if (same_prefix && !excessive_valueless && !(use_bjdata && is_bjdata_excluded_type_marker(first_prefix))) @@ -22670,48 +27474,7 @@ class binary_writer if (use_count) { - oa.write_character(to_char_type('#')); - write_number_with_ubjson_prefix(j.m_data.m_value.array->size(), true, use_bjdata); - } - - return !use_count; - } - - /// @return true when a closing '}' still has to be written after the elements - bool write_ubjson_start_object(const BasicJsonType& j, const bool use_count, const bool use_type, - const bool add_prefix, const bool use_bjdata, bool& prefix_required) - { - prefix_required = true; - if (add_prefix) - { - oa.write_character(to_char_type('{')); - } - - if (use_type && !j.m_data.m_value.object->empty()) - { - if (!use_count) - { - JSON_THROW(other_error::create(502, "use_type requires use_size = true", &j)); - } - const CharType first_prefix = ubjson_prefix(j.front(), use_bjdata); - const bool same_prefix = std::all_of(j.begin(), j.end(), - [this, first_prefix, use_bjdata](const BasicJsonType & v) - { - return ubjson_prefix(v, use_bjdata) == first_prefix; - }); - - if (same_prefix && !(use_bjdata && is_bjdata_excluded_type_marker(first_prefix))) - { - prefix_required = false; - oa.write_character(to_char_type('$')); - oa.write_character(first_prefix); - } - } - - if (use_count) - { - oa.write_character(to_char_type('#')); - write_number_with_ubjson_prefix(j.m_data.m_value.object->size(), true, use_bjdata); + write_ubjson_count(size, use_bjdata); } return !use_count; @@ -22739,21 +27502,12 @@ class binary_writer /// @brief an object or array @ref write_ubjson_iterative is still writing /// the elements of - struct ubjson_frame + struct ubjson_frame : binary_container_frame { ubjson_frame(const BasicJsonType* value_, const bool prefix_required_) noexcept - : value(value_) + : binary_container_frame(value_) , prefix_required(prefix_required_) - { - if (value->is_object()) - { - object_it = value->m_data.m_value.object->cbegin(); - } - else - { - array_it = value->m_data.m_value.array->cbegin(); - } - } + {} // declared for GCC's -Weffc++, which asks for them in a class with // pointer members and a non-trivial destructor; the exception @@ -22765,13 +27519,9 @@ class binary_writer ubjson_frame& operator=(ubjson_frame&&) = default; ~ubjson_frame() = default; - /// the array or object being written - const BasicJsonType* value; /// whether value's elements each carry their own type marker; an /// optimized ($type) container writes it once for all of them instead bool prefix_required; - typename BasicJsonType::object_t::const_iterator object_it{}; - typename BasicJsonType::array_t::const_iterator array_it{}; }; /*! @@ -22782,7 +27532,7 @@ class binary_writer elements of an optimized container, and everything below the top level, never repeat it) - @sa @ref write_cbor_value_or_push + @sa @ref write_container_value_or_push */ void write_ubjson_value_or_push(const BasicJsonType& j, const bool add_prefix, const bool use_count, const bool use_type, const bool use_bjdata, const bjdata_version_t bjdata_version, @@ -22805,11 +27555,11 @@ class binary_writer bool prefix_required = true; if (is_array) { - write_ubjson_start_array(j, use_count, use_type, add_prefix, use_bjdata, prefix_required); + write_ubjson_start_container(j, use_count, use_type, add_prefix, use_bjdata, prefix_required); } else { - write_ubjson_start_object(j, use_count, use_type, add_prefix, use_bjdata, prefix_required); + write_ubjson_start_container(j, use_count, use_type, add_prefix, use_bjdata, prefix_required); } const bool empty = is_array ? j.m_data.m_value.array->empty() : j.m_data.m_value.object->empty(); @@ -22819,7 +27569,7 @@ class binary_writer return; } - // write_ubjson_start_array/_object return !use_count, i.e. whether a + // write_ubjson_start_container returns !use_count, i.e. whether a // closer still has to be written; use_count is constant for the whole // document, so that is recomputed here instead of being carried along if (!use_count) @@ -22875,12 +27625,9 @@ class binary_writer continue; } - string_t storage; - const string_t& key = sanitize_utf8_for_write(current.object_it->first, j, storage); - write_number_with_ubjson_prefix(key.size(), true, use_bjdata); - oa.write_characters( - reinterpret_cast(key.data()), - key.size()); + // a converted key must outlive the reference returned by sanitize_utf8_for_write + const object_key_string_t key_string = current.object_it->first; + write_ubjson_text(key_string, j, false, use_bjdata); const BasicJsonType* child = &(current.object_it->second); const bool child_prefix = current.prefix_required; ++stack.back().object_it; @@ -22903,18 +27650,85 @@ class binary_writer */ std::size_t calc_bson_entry_header_size(const string_t& name, const BasicJsonType& j) { - const auto it = name.find(static_cast(0)); + const std::size_t it = find_nul(name); if (JSON_HEDLEY_UNLIKELY(it != BasicJsonType::string_t::npos)) { - JSON_THROW(out_of_range::create(409, concat("BSON key cannot contain code point U+0000 (at byte ", std::to_string(it), ")"), &j)); + throw_bson_key_with_null(it, j); } + if (JSON_HEDLEY_LIKELY(error_handler == error_handler_t::keep)) + { + return /*id*/ 1ul + name.size() + /*zero-terminator*/1u; + } + + return calc_bson_checked_entry_header_size(name, j); + } + + /// @brief throw out_of_range.409 for a name with U+0000 at byte @a index; + /// out of line, so that the message does not burden the check + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_bson_key_with_null(const std::size_t index, const BasicJsonType& j) + { + static_cast(index); // only used when exceptions are enabled + static_cast(j); + JSON_THROW(out_of_range::create(exception_id::bson_key_with_null, concat("BSON key cannot contain code point U+0000 (at byte ", std::to_string(index), ")"), &j)); + } + + /// @brief the size of a BSON document entry header for a name that the + /// error handler checks or sanitizes; out of line, as the string_t + /// for the sanitized copy would otherwise burden every call of + /// @ref calc_bson_entry_header_size + JSON_HEDLEY_NEVER_INLINE + std::size_t calc_bson_checked_entry_header_size(const string_t& name, const BasicJsonType& j) + { string_t storage; const string_t& sanitized = sanitize_utf8_for_write(name, j, storage); return /*id*/ 1ul + sanitized.size() + /*zero-terminator*/1u; } + /*! + @brief the position of the first U+0000 in @a name, or string_t::npos + + The same as `name.find(0)`, without the calls into the standard library for + names of a few characters: it looks at eight characters at a time. + */ + static std::size_t find_nul(const string_t& name) noexcept + { + return find_nul(name, std::integral_constant()); + } + + static std::size_t find_nul(const string_t& name, std::true_type /*is_byte*/) noexcept + { + constexpr std::uint64_t ones = 0x0101010101010101ull; + constexpr std::uint64_t high = 0x8080808080808080ull; + const std::size_t size = name.size(); + const char* const data = reinterpret_cast(name.data()); + std::size_t pos = 0; + for (; pos + 8 <= size; pos += 8) + { + std::uint64_t word = 0; + std::memcpy(&word, data + pos, sizeof(word)); + if (((word - ones) & ~word & high) != 0) + { + break; + } + } + for (; pos < size; ++pos) + { + if (data[pos] == '\0') + { + return pos; + } + } + return BasicJsonType::string_t::npos; + } + + static std::size_t find_nul(const string_t& name, std::false_type /*is_byte*/) noexcept + { + return name.find(static_cast(0)); + } + /*! @brief Checks that @a size fits into the 32-bit length field used by BSON @return The size as a signed 32-bit integer @@ -22924,7 +27738,7 @@ class binary_writer { if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(size))) { - JSON_THROW(out_of_range::create(412, concat("BSON length ", std::to_string(size), " exceeds maximum of ", std::to_string((std::numeric_limits::max)())), nullptr)); + throw_bson_length_too_large(size); } return static_cast(size); @@ -22944,14 +27758,15 @@ class binary_writer { oa.write_character(to_char_type(element_type)); - if (error_handler == error_handler_t::keep || error_handler == error_handler_t::strict || is_valid_utf8(name)) + if (JSON_HEDLEY_LIKELY(error_handler == error_handler_t::keep || error_handler == error_handler_t::strict)) { - oa.write_characters(reinterpret_cast(name.data()), name.size()); + write_bytes(name.data(), name.size()); } else { - const string_t sanitized = sanitize_utf8(name, error_handler); - oa.write_characters(reinterpret_cast(sanitized.data()), sanitized.size()); + string_t storage; + const string_t& written = sanitize_utf8_for_bson_write(name, storage); + write_bytes(written.data(), written.size()); } // the terminating null byte is written explicitly rather than taken @@ -22994,13 +27809,25 @@ class binary_writer { if (JSON_HEDLEY_LIKELY(value_in_range_of(value.size()))) { - string_t storage; - const string_t& sanitized = sanitize_utf8_for_write(value, j, storage); - return sizeof(std::int32_t) + sanitized.size() + 1ul; + if (JSON_HEDLEY_LIKELY(error_handler == error_handler_t::keep)) + { + return sizeof(std::int32_t) + value.size() + 1ul; + } + return calc_bson_checked_string_size(value, j); } return sizeof(std::int32_t) + value.size() + 1ul; } + /// @brief the size of a BSON string that the error handler checks or + /// sanitizes; out of line, like @ref calc_bson_checked_entry_header_size + JSON_HEDLEY_NEVER_INLINE + std::size_t calc_bson_checked_string_size(const string_t& value, const BasicJsonType& j) + { + string_t storage; + const string_t& sanitized = sanitize_utf8_for_write(value, j, storage); + return sizeof(std::int32_t) + sanitized.size() + 1ul; + } + /*! @brief Writes a BSON element with key @a name and string value @a value @@ -23015,16 +27842,11 @@ class binary_writer { write_bson_entry_header(name, 0x02); - const bool sanitize = error_handler != error_handler_t::keep - && error_handler != error_handler_t::strict - && !is_valid_utf8(value); - const string_t sanitized = sanitize ? sanitize_utf8(value, error_handler) : string_t{}; - const string_t& written = sanitize ? sanitized : value; + string_t storage; + const string_t& written = sanitize_utf8_for_bson_write(value, storage); write_number(to_bson_length(written.size() + 1ul), true); - oa.write_characters( - reinterpret_cast(written.data()), - written.size()); + write_bytes(written.data(), written.size()); // the terminating null byte is written explicitly rather than taken // from the buffer, so that string_t::data() need not be null-terminated oa.write_character(to_char_type(0x00)); @@ -23110,7 +27932,7 @@ class binary_writer if (value.has_subtype() && JSON_HEDLEY_UNLIKELY(value.subtype() > (std::numeric_limits::max)())) { - JSON_THROW(out_of_range::create(415, concat("subtype ", std::to_string(value.subtype()), " is too large for the BSON binary subtype (max 255)"), &j)); + throw_subtype_out_of_range(value.subtype(), "the BSON binary subtype", j); } return sizeof(std::int32_t) + value.size() + 1ul; @@ -23130,7 +27952,7 @@ class binary_writer write_number(value.has_subtype() ? static_cast(value.subtype()) : static_cast(0x00)); - oa.write_characters(reinterpret_cast(value.data()), value.size()); + write_bytes(value.data(), value.size()); } /*! @@ -23138,6 +27960,8 @@ class binary_writer is neither an object nor an array @throw out_of_range.415 if @a j is binary with a subtype that does not fit into a byte, before anything is written + @throw out_of_range.407 if @a j is an integer that does not fit the 64 bits + of a BSON integer, before anything is written @throw type_error.316 if @a j is a string that is not valid UTF-8, before anything is written @throw type_error.321 if @a j is discarded @@ -23156,10 +27980,18 @@ class binary_writer return 8ul; case value_t::number_integer: - return calc_bson_integer_size(j.m_data.m_value.number_integer); + if (JSON_HEDLEY_UNLIKELY(!integer_fits(j.m_data.m_value.number_integer))) + { + throw_integer_out_of_range(j, input_format_t::bson, "int64"); + } + return calc_bson_integer_size(static_cast(j.m_data.m_value.number_integer)); case value_t::number_unsigned: - return calc_bson_unsigned_size(j.m_data.m_value.number_unsigned); + if (JSON_HEDLEY_UNLIKELY(!integer_fits_max(j.m_data.m_value.number_unsigned))) + { + throw_integer_out_of_range(j, input_format_t::bson, "uint64"); + } + return calc_bson_unsigned_size(conditional_static_cast(j.m_data.m_value.number_unsigned)); case value_t::string: return calc_bson_string_size(*j.m_data.m_value.string, j); @@ -23168,7 +28000,7 @@ class binary_writer return 0ul; case value_t::discarded: - throw_on_discarded(j, "BSON"); + throw_on_discarded(j, input_format_t::bson); // LCOV_EXCL_START case value_t::object: @@ -23180,12 +28012,198 @@ class binary_writer } } + /// the longest name for which write_bson_scalar() writes an entry through a + /// single reservation: the type, name, terminating null byte, and the length + /// of a string or binary value (4 bytes) or the largest fixed-size value (8 + /// bytes, a double or a 64-bit integer) must fit max_reserve_size + static constexpr std::size_t bson_scalar_max_name_size = max_reserve_size - 2 - 8 - 1; + + /*! + @brief store @a n at @a out in little-endian byte order + */ + template + void store_little_endian(CharType* out, const NumberType n) const noexcept + { + std::memcpy(out, &n, sizeof(NumberType)); + if (!is_little_endian) + { + std::reverse(out, out + sizeof(NumberType)); + } + } + + /*! + @brief encode the type and the value of the BSON document entry for @a j + if it has a fixed size + @param[in] out where the type byte goes + @param[in] value_out where the value goes: the characters after the name + and its terminating null byte + @return the end of the entry, or nullptr (and nothing is written to + @a out) if @a j is not a boolean, number, or null; at most 9 + characters are written to @a out and @a value_out + + The entry is encoded as @ref write_bson_value does. + */ + CharType* encode_bson_fixed_entry(CharType* out, CharType* value_out, const BasicJsonType& j) const noexcept + { + switch (j.type()) + { + case value_t::boolean: + out[0] = to_char_type(0x08); + *value_out++ = j.m_data.m_value.boolean ? to_char_type(0x01) : to_char_type(0x00); + return value_out; + + case value_t::number_float: + out[0] = to_char_type(0x01); + store_little_endian(value_out, static_cast(j.m_data.m_value.number_float)); + return value_out + sizeof(double); + + case value_t::number_integer: + { + const auto value = static_cast(j.m_data.m_value.number_integer); + if ((std::numeric_limits::min)() <= value && value <= (std::numeric_limits::max)()) + { + out[0] = to_char_type(0x10); // int32 + store_little_endian(value_out, static_cast(value)); + return value_out + sizeof(std::int32_t); + } + out[0] = to_char_type(0x12); // int64 + store_little_endian(value_out, value); + return value_out + sizeof(std::int64_t); + } + + case value_t::number_unsigned: + { + const auto value = conditional_static_cast(j.m_data.m_value.number_unsigned); + if (value <= static_cast((std::numeric_limits::max)())) + { + out[0] = to_char_type(0x10); // int32 + store_little_endian(value_out, static_cast(value)); + return value_out + sizeof(std::int32_t); + } + if (value <= static_cast((std::numeric_limits::max)())) + { + out[0] = to_char_type(0x12); // int64 + store_little_endian(value_out, static_cast(value)); + return value_out + sizeof(std::int64_t); + } + out[0] = to_char_type(0x11); // uint64 + store_little_endian(value_out, value); + return value_out + sizeof(std::uint64_t); + } + + case value_t::null: + out[0] = to_char_type(0x0A); + return value_out; + + case value_t::string: + case value_t::binary: + case value_t::object: + case value_t::array: + case value_t::discarded: + default: + return nullptr; + } + } + + /*! + @brief Writes the BSON document entry with key @a name for @a j, which is + neither an object nor an array, with the key and a fixed-size value + (or the header of a string or binary value) in one reservation + @return whether the entry was written; false for a discarded value, a + string or binary value whose length does not fit the length + field, in which case nothing was written + @pre @a name is at most bson_scalar_max_name_size characters long and is + not changed by the error handler + */ + bool write_bson_scalar(const string_t& name, const BasicJsonType& j) + { + const std::size_t name_size = name.size(); + // a short string is written whole: its type, the name and its null + // byte, the length, the characters, and their null byte + const bool whole_string = j.is_string() && j.m_data.m_value.string->size() <= max_reserve_size - name_size - 7; + CharType* const out = oa.reserve(whole_string ? name_size + j.m_data.m_value.string->size() + 7 : name_size + 2 + 8); + // the name and its terminating null byte follow the type byte + CharType* p = out + 1; + copy_characters(p, reinterpret_cast(name.data()), name_size); + p += name_size; + *p++ = to_char_type(0x00); + + if (const CharType* const end = encode_bson_fixed_entry(out, p, j)) + { + oa.commit(static_cast(end - out)); + return true; + } + + if (whole_string) + { + const string_t& value = *j.m_data.m_value.string; + out[0] = to_char_type(0x02); + store_little_endian(p, static_cast(value.size() + 1ul)); + p += sizeof(std::int32_t); + copy_characters(p, reinterpret_cast(value.data()), value.size()); + p += value.size(); + // the terminating null byte is written explicitly rather than + // taken from the buffer, so that string_t::data() need not be + // null-terminated + *p++ = to_char_type(0x00); + oa.commit(static_cast(p - out)); + return true; + } + + if (j.is_string()) + { + const string_t& value = *j.m_data.m_value.string; + if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(value.size() + 1ul))) + { + return false; + } + out[0] = to_char_type(0x02); + store_little_endian(p, static_cast(value.size() + 1ul)); + p += sizeof(std::int32_t); + oa.commit(static_cast(p - out)); + // the terminating null byte is written explicitly rather than + // taken from the buffer, so that string_t::data() need not be + // null-terminated + write_bytes(value.data(), value.size()); + oa.write_character(to_char_type(0x00)); + return true; + } + + if (j.is_binary()) + { + const binary_t& value = *j.m_data.m_value.binary; + if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(value.size()))) + { + return false; + } + out[0] = to_char_type(0x05); + store_little_endian(p, static_cast(value.size())); + p += sizeof(std::int32_t); + *p++ = value.has_subtype() ? to_char_type(static_cast(value.subtype())) : to_char_type(0x00); + oa.commit(static_cast(p - out)); + write_bytes(value.data(), value.size()); + return true; + } + + return false; + } + /*! @brief Writes the BSON document entry with key @a name for @a j, which is neither an object nor an array */ void write_bson_value(const string_t& name, const BasicJsonType& j) { + // The strings are written as they are, as they are neither checked nor + // sanitized again here by these two error handlers. The name has to leave + // room for the larger payloads (see write_bson_scalar). + if (name.size() <= bson_scalar_max_name_size + && (error_handler == error_handler_t::keep || error_handler == error_handler_t::strict) + && write_bson_scalar(name, j)) + { + return; + } + switch (j.type()) { case value_t::binary: @@ -23197,11 +28215,12 @@ class binary_writer case value_t::number_float: return write_bson_double(name, j.m_data.m_value.number_float); + // calc_bson_value_size() checked that integers fit 64 bits case value_t::number_integer: - return write_bson_integer(name, j.m_data.m_value.number_integer); + return write_bson_integer(name, static_cast(j.m_data.m_value.number_integer)); case value_t::number_unsigned: - return write_bson_unsigned(name, j.m_data.m_value.number_unsigned); + return write_bson_unsigned(name, conditional_static_cast(j.m_data.m_value.number_unsigned)); case value_t::string: return write_bson_string(name, *j.m_data.m_value.string); @@ -23210,7 +28229,7 @@ class binary_writer return write_bson_null(name); case value_t::discarded: - throw_on_discarded(j, "BSON"); + throw_on_discarded(j, input_format_t::bson); // LCOV_EXCL_START case value_t::object: @@ -23222,6 +28241,24 @@ class binary_writer } } + /*! + @brief Writes the head of a BSON element with key @a name whose value is + the object or array @a j: its type, key, and @a size + + Called while the key is in scope: an object key is a key_type, which + may only convert to a temporary string_t, so it cannot be kept by + pointer until after the loop over the entries. + */ + void write_bson_nested_head(const string_t& name, const BasicJsonType& j, + const std::size_t size) + { + write_bson_entry_header(name, j.is_object() ? 0x03 : 0x04); + write_number(to_bson_length(size), true); + } + + /// space for the name of an array element, the decimal index, in the frame + static constexpr std::size_t bson_index_name_space = 24; + /// @brief an object or array of the BSON document being sized or written struct bson_frame { @@ -23235,28 +28272,196 @@ class binary_writer } } + /// @ref calc_bson_sizes only: make @ref index the next one and count the + /// digits of its name + void count_index() noexcept + { + if (++index == index_boundary) + { + ++index_name_size; + index_boundary *= 10; + } + } + + /// @ref write_bson_document only: make @ref index the next one and + /// update its decimal name + void next_index() noexcept + { + ++index; + std::size_t i = index_name_size; + while (i > 0 && index_name[i - 1] == '9') + { + index_name[--i] = '0'; + } + if (i > 0) + { + ++index_name[i - 1]; + } + else + { + // all digits were 9: 10, 100, ... + index_name[0] = '1'; + index_name[index_name_size++] = '0'; + } + } + /// the object or array const BasicJsonType* value; /// objects: the next member typename BasicJsonType::object_t::const_iterator member{}; /// arrays: the index of the next element std::size_t index = 0; + /// arrays: the name BSON gives that element, its decimal index (at most + /// 2^64 - 1, which has 20 digits), in space for copying a few characters + /// more; only maintained by @ref write_bson_document + std::array index_name{{'0'}}; + /// arrays: the number of digits of the index + std::size_t index_name_size = 1; + /// arrays: the first index with one more digit; only maintained by + /// @ref calc_bson_sizes + std::uint64_t index_boundary = 10; /// @ref calc_bson_sizes only: where its size goes in the table std::size_t size_slot; /// @ref calc_bson_sizes only: the size of its entries seen so far std::size_t entries_size = 0; }; - /*! - @brief creates the name BSON gives the array element with index @a index - @param[out] name receives the decimal index - */ - static void create_bson_index_name(const std::size_t index, string_t& name) + /// whether the BSON entry of @a j has a fixed size, which is the case for + /// a boolean, a number, and null + static bool is_bson_fixed_size(const BasicJsonType& j) noexcept { - // the index is built as a std::string; convert explicitly, as the - // two are only implicitly convertible for some string types - const auto key = std::to_string(index); - name = string_t(key.data(), key.size()); + return j.is_number() || j.is_boolean() || j.is_null(); + } + + /*! + @brief Writes the entries of the array in @a frame that have a fixed size + (booleans, numbers, and null), from its next element on, until it + reaches another element or the end of the array + + The index of the element and its name are advanced in @a frame. + + The entries are encoded straight into space reserved from the sink for as + many of them as fit, with the index, its digits, and the write pointer + kept in local variables: stores of characters may alias the frame, and + would force them to be loaded from it again for every element. + */ + void write_bson_array_fixed_entries(bson_frame& frame) + { + const auto& array = *frame.value->m_data.m_value.array; + const std::size_t count = array.size(); + std::size_t index = frame.index; + std::size_t name_size = frame.index_name_size; + std::array name = frame.index_name; + + // an entry is at most the type, 20 digits, the null byte, and 8 bytes + constexpr std::size_t max_entry = 1 + 20 + 1 + 8; + while (index < count) + { + CharType* const start = oa.reserve(max_reserve_size); + CharType* out = start; + const CharType* const limit = start + (max_reserve_size - max_entry); + while (out <= limit && index < count) + { + CharType* p = out + 1; + std::memcpy(p, name.data(), name.size()); + p += name_size; + *p++ = to_char_type(0x00); + CharType* const end = encode_bson_fixed_entry(out, p, array[index]); + if (end == nullptr) + { + break; + } + out = end; + + ++index; + std::size_t i = name_size; + while (i > 0 && name[i - 1] == '9') + { + name[--i] = '0'; + } + if (i > 0) + { + ++name[i - 1]; + } + else + { + // all digits were 9: 10, 100, ... + name[0] = '1'; + name[name_size++] = '0'; + } + } + oa.commit(static_cast(out - start)); + if (out <= limit && index < count) + { + // stopped at an element that has no fixed size + break; + } + } + + frame.index = index; + frame.index_name_size = name_size; + frame.index_name = name; + } + + /*! + @brief Calculates the size of the BSON array @a j if all its elements are + booleans, numbers, or null + @param[out] size the size of the array, including its own size field + @return false (and nothing is known about @a size) if an element is not, + or is an integer that does not fit 64 bits (which the generic + path rejects), which is not looked at further + */ + bool calc_bson_fixed_array_size(const BasicJsonType& j, std::size_t& size) const noexcept + { + std::size_t total = sizeof(std::int32_t) + 1ul; + std::size_t index_name_size = 1; + std::uint64_t index_boundary = 10; + std::uint64_t index = 0; + for (const auto& el : *j.m_data.m_value.array) + { + // the type, the decimal index, and the terminating null byte + total += 2ul + index_name_size; + if (++index == index_boundary) + { + ++index_name_size; + index_boundary *= 10; + } + switch (el.type()) + { + case value_t::boolean: + total += 1ul; + break; + case value_t::number_float: + total += 8ul; + break; + case value_t::number_integer: + // an integer beyond 64 bits is left to the generic path, which throws + if (JSON_HEDLEY_UNLIKELY(!integer_fits(el.m_data.m_value.number_integer))) + { + return false; + } + total += calc_bson_integer_size(static_cast(el.m_data.m_value.number_integer)); + break; + case value_t::number_unsigned: + if (JSON_HEDLEY_UNLIKELY(!integer_fits_max(el.m_data.m_value.number_unsigned))) + { + return false; + } + total += calc_bson_unsigned_size(conditional_static_cast(el.m_data.m_value.number_unsigned)); + break; + case value_t::null: + break; + case value_t::string: + case value_t::binary: + case value_t::object: + case value_t::array: + case value_t::discarded: + default: + return false; + } + } + size = total; + return true; } /*! @@ -23290,8 +28495,6 @@ class binary_writer // is in; nothing is allocated unless the document nests bson_frame current(&document); std::vector parents; - // string_t need not be default constructible - string_t index_name("", 0); while (true) { @@ -23322,9 +28525,10 @@ class binary_writer while (nested == nullptr && current.index < array.size()) { const BasicJsonType& el = array[current.index]; - create_bson_index_name(current.index, index_name); - current.entries_size += calc_bson_entry_header_size(index_name, el); - ++current.index; + // the type, the decimal index (which is valid UTF-8 and + // has no U+0000), and the terminating null byte + current.entries_size += 2ul + current.index_name_size; + current.count_index(); if (el.is_structured()) { nested = ⪙ @@ -23338,6 +28542,15 @@ class binary_writer if (nested != nullptr) { + // an array of booleans, numbers and null: sized without a frame + std::size_t fixed_size = 0; + if (nested->is_array() && calc_bson_fixed_array_size(*nested, fixed_size)) + { + nested_sizes.push_back(fixed_size); + current.entries_size += fixed_size; + continue; + } + // its size is added to the current one's once it is done nested_sizes.push_back(0); parents.push_back(std::move(current)); @@ -23379,14 +28592,11 @@ class binary_writer bson_frame current(&document); std::vector parents; std::size_t next_size = 0; - // string_t need not be default constructible - string_t index_name("", 0); while (true) { // write entries until the current object or array is done, or an // entry is an object or array itself - const string_t* nested_name = nullptr; const BasicJsonType* nested = nullptr; if (current.value->is_object()) { @@ -23397,7 +28607,7 @@ class binary_writer ++current.member; if (el.second.is_structured()) { - nested_name = &el.first; + write_bson_nested_head(el.first, el.second, nested_sizes[next_size++]); nested = &el.second; } else @@ -23411,12 +28621,22 @@ class binary_writer 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]; - create_bson_index_name(current.index, index_name); - ++current.index; + const string_t index_name(current.index_name.data(), current.index_name_size); + current.next_index(); if (el.is_structured()) { - nested_name = &index_name; + write_bson_nested_head(index_name, el, nested_sizes[next_size++]); nested = ⪙ } else @@ -23428,8 +28648,20 @@ class binary_writer if (nested != nullptr) { - write_bson_entry_header(*nested_name, nested->is_object() ? 0x03 : 0x04); - write_number(to_bson_length(nested_sizes[next_size++]), true); + if (nested->is_array()) + { + // an array of booleans, numbers and null is written without a frame on the stack + bson_frame leaf(nested); + write_bson_array_fixed_entries(leaf); + if (leaf.index == nested->m_data.m_value.array->size()) + { + oa.write_character(to_char_type(0x00)); + continue; + } + parents.push_back(std::move(current)); + current = std::move(leaf); + continue; + } parents.push_back(std::move(current)); current = bson_frame(nested); continue; @@ -23497,6 +28729,727 @@ class binary_writer } } + /*! + @brief size class of an unsigned integer from its bit length + + 1, 2, 3, or 4 if @a x fits 1, 2, 4, or 8 bytes. Looked up from the number + of leading zero bits instead of found by branches: the sizes of a stream + of numbers are hard to predict, and a mispredicted branch costs more than + writing the number. (A table, as comparisons are vectorized by clang into + slower code.) + */ + static unsigned integer_size_class(const std::uint64_t x) noexcept + { + static constexpr std::array size_class_by_leading_zeros = + { + { + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1 + } + }; + // x | 1 keeps count_leading_zeros() away from 0 + return size_class_by_leading_zeros[static_cast(count_leading_zeros(x | 1u))]; + } + + /*! + @brief encode @a first_byte and the low-order bytes of @a value that + @a size_class calls for (none, or 1, 2, 4, or 8) in network order + to @a out + @return the number of characters of the encoding; up to + @ref integer_encoding_space characters at @a out are overwritten + + The bytes are written as one byte and one 8-byte word whose bytes after + the value are scratch that the caller drops, so that writing them does not + depend on the size class. For class 0 the word is all scratch; it is + shifted by 60 bits rather than 64, which would be undefined. + */ + std::size_t encode_first_byte_and_tail(CharType* out, const std::uint8_t first_byte, const unsigned size_class, const std::uint64_t value) const noexcept + { + out[0] = to_char_type(first_byte); + const unsigned unused_bits = 64u - ((8u << size_class) >> 1u); + const std::uint64_t tail = is_little_endian ? byteswap(value) >> unused_bits : value << unused_bits; + std::memcpy(out + 1, &tail, sizeof(tail)); + return ((std::size_t{1} << size_class) >> 1u) + 1; + } + + /// characters encode_first_byte_and_tail() may overwrite: the first byte + /// and one 8-byte word + static constexpr std::size_t integer_encoding_space = 9; + + /*! + @brief the 64 bits of a number_integer or number_unsigned value, and a mask + that is all ones if it is negative, else zero + + Selected through masks rather than branches, as the type and the sign of + the values in an array of mixed signs alternate unpredictably (non-negative + integers are number_unsigned after parsing). Up to 64 bits, the sign is bit + 63 of the result (a narrower integer is sign-extended); computing it with + masks keeps GCC from branching on the type. A wider integer has to be + compared. + */ + static std::uint64_t integer_bits(const BasicJsonType& j, std::uint64_t& sign_mask) noexcept + { + const bool is_signed = j.m_data.m_type == value_t::number_integer; + const std::uint64_t raw = is_signed + ? static_cast(j.m_data.m_value.number_integer) + : static_cast(j.m_data.m_value.number_unsigned); + sign_mask = sizeof(typename BasicJsonType::number_integer_t) <= sizeof(std::uint64_t) + ? (std::uint64_t{0} - static_cast(is_signed)) & (std::uint64_t{0} - (raw >> 63u)) + : std::uint64_t{0} - static_cast(is_signed && j.m_data.m_value.number_integer < 0); + return raw; + } + + /*! + @brief encode the head of a CBOR integer to @a out without branching on its + size + @return the number of characters of the head + + Same output as @ref write_cbor_head. The lengths of strings and containers + are mostly predictable, so their heads keep the branches; the sizes of a + run of integers are not, so an integer's head is assembled without them. + */ + std::size_t encode_cbor_integer_head(CharType* out, const std::uint8_t major_type, const std::uint64_t argument) const noexcept + { + // an argument of at most 23 is in size class 1 (one byte) and is moved + // to class 0, the first byte, by subtracting 1 + const auto is_immediate = static_cast(argument <= 0x17u); + const unsigned size_class = integer_size_class(argument) - is_immediate; + + // the first byte: the argument itself if it is immediate, else + // 0x17 + size_class, selected through a mask + const std::uint64_t immediate_mask = std::uint64_t{0} - is_immediate; + const auto first_byte = static_cast(major_type + ((argument & immediate_mask) | ((0x17u + size_class) & ~immediate_mask))); + return encode_first_byte_and_tail(out, first_byte, size_class, argument); + } + + /// write the head of an array or object: one byte for a size of up to 23, else as + /// @ref write_cbor_head + void write_cbor_container_head(const std::uint8_t major_type, const std::uint64_t size) + { + if (JSON_HEDLEY_LIKELY(size <= 0x17)) + { + oa.write_character(to_char_type(static_cast(major_type + size))); + return; + } + write_cbor_head(major_type, size); + } + + /*! + @brief write an object value (head and members) as CBOR + + A member whose value is null, a boolean, an integer, or a string is written + without a call of @ref write_cbor. Kept out of line, like @ref write_cbor_array. + */ + JSON_HEDLEY_NEVER_INLINE + void write_cbor_object(const BasicJsonType& j, const std::size_t depth) + { + // step 1: write control byte and the object size + write_cbor_container_head(0xA0, j.m_data.m_value.object->size()); + + for (const auto& el : *j.m_data.m_value.object) + { + // el.first is written directly (not via a temporary + // basic_json), with the object as diagnostics context + write_cbor_key(el.first, j); + + const BasicJsonType& value = el.second; + switch (value.type()) + { + case value_t::null: + oa.write_character(to_char_type(0xF6)); + break; + + case value_t::boolean: + oa.write_character(value.m_data.m_value.boolean ? to_char_type(0xF5) : to_char_type(0xF4)); + break; + + case value_t::number_integer: + case value_t::number_unsigned: + write_cbor_integer(value, std::integral_constant()); + break; + + case value_t::string: + write_cbor_string(*value.m_data.m_value.string, value); + break; + + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::number_float: + case value_t::discarded: + default: + write_cbor(value, depth + 1); + break; + } + } + } + + /*! + @brief write an array value (head and elements) as CBOR + + Integer elements, the most common scalar, are written without a call to + @ref write_cbor. The loop is kept out of line: inlined into write_cbor, its + registers would be saved and restored on every call of write_cbor, which + is one call per value nested in an object. + */ + JSON_HEDLEY_NEVER_INLINE + void write_cbor_array(const BasicJsonType& j, const std::size_t depth) + { + write_cbor_container_head(0x80, j.m_data.m_value.array->size()); + if (!j.m_data.m_value.array->empty()) + { + write_cbor_array_elements(j, depth, std::integral_constant()); + } + } + + JSON_HEDLEY_NEVER_INLINE + void write_cbor_array_elements(const BasicJsonType& j, const std::size_t depth, std::true_type /*fits_64_bits*/) + { + write_array_elements(j, [this](CharType * out, const BasicJsonType & el) + { + return encode_cbor_integer(out, el); + }, [this](CharType * out, const number_float_t n) + { + return encode_cbor_float(out, n); + }, [this, depth](const BasicJsonType & el) + { + write_cbor(el, depth + 1); + }); + } + + void write_cbor_array_elements(const BasicJsonType& j, const std::size_t depth, std::false_type /*fits_64_bits*/) + { + for (const auto& el : *j.m_data.m_value.array) + { + write_cbor(el, depth + 1); + } + } + + /*! + @brief write the elements of the array @a j, runs of integers with + @a encode_integer, runs of floats with @a encode_float, and all + other values with @a write_value + + Runs of numbers are encoded straight into space reserved from the sink + for a whole run: the write pointer stays a local, where it would otherwise + be reloaded from the sink for every value, as a store of a character may + alias it. + */ + template + void write_array_elements(const BasicJsonType& j, EncodeInteger encode_integer, EncodeFloat encode_float, WriteValue write_value) + { + constexpr std::size_t max_run = max_reserve_size / integer_encoding_space; + auto it = j.m_data.m_value.array->begin(); + const auto end = j.m_data.m_value.array->end(); + while (it != end) + { + if (!it->is_number()) + { + write_value(*it); + ++it; + } + else if (!it->is_number_float()) + { + CharType* const start = oa.reserve(max_run * integer_encoding_space); + CharType* out = start; + for (std::size_t n = 0; n < max_run && it != end && it->is_number_integer(); ++n, ++it) + { + out += encode_integer(out, *it); + } + oa.commit(static_cast(out - start)); + } + else + { + it = write_float_elements(it, end, encode_float); + } + } + } + + /*! + @brief write the run of floats that starts at @a it with @a encode_float + @return the first element after the run + + Out of line, so that the loop of integers in @ref write_array_elements is + not changed by it. + */ + template + JSON_HEDLEY_NEVER_INLINE + Iterator write_float_elements(Iterator it, const Iterator end, EncodeFloat encode_float) // NOLINT(performance-unnecessary-value-param): iterators are passed by value + { + constexpr std::size_t max_run = max_reserve_size / integer_encoding_space; + CharType* const start = oa.reserve(max_run * integer_encoding_space); + CharType* out = start; + for (std::size_t n = 0; n < max_run && it != end && it->is_number_float(); ++n, ++it) + { + out += encode_float(out, it->m_data.m_value.number_float); + } + oa.commit(static_cast(out - start)); + return it; + } + + /*! + @brief write a number_integer or number_unsigned value as a CBOR integer + + A negative integer n has major type 1 and the argument -1 - n, which is ~n. + Kept out of line, like the other per-value paths, so that write_cbor stays + small (runs of integers in an array are encoded inline instead). + */ + JSON_HEDLEY_NEVER_INLINE + void write_cbor_integer(const BasicJsonType& j, std::true_type /*fits_64_bits*/) + { + oa.commit(encode_cbor_integer(oa.reserve(integer_encoding_space), j)); + } + + /// write a number_integer or number_unsigned value of a number type wider + /// than 64 bits as a CBOR integer, with range checks on the whole value + void write_cbor_integer(const BasicJsonType& j, std::false_type /*fits_64_bits*/) + { + if (j.m_data.m_type == value_t::number_unsigned) + { + if (JSON_HEDLEY_UNLIKELY(!integer_fits_max(j.m_data.m_value.number_unsigned))) + { + throw_integer_out_of_range(j, input_format_t::cbor, "uint64"); + } + write_cbor_head(0x00, conditional_static_cast(j.m_data.m_value.number_unsigned)); + return; + } + + if (j.m_data.m_value.number_integer >= 0) + { + if (JSON_HEDLEY_UNLIKELY(!integer_fits_max(j.m_data.m_value.number_integer))) + { + throw_integer_out_of_range(j, input_format_t::cbor, "[-2^64, 2^64-1]"); + } + // CBOR does not differentiate between positive signed + // integers and unsigned integers + write_cbor_head(0x00, static_cast(j.m_data.m_value.number_integer)); + } + else + { + // a negative integer n is encoded as -1 - n + if (JSON_HEDLEY_UNLIKELY(!integer_fits_max(-1 - j.m_data.m_value.number_integer))) + { + throw_integer_out_of_range(j, input_format_t::cbor, "[-2^64, 2^64-1]"); + } + write_cbor_head(0x20, static_cast(-1 - j.m_data.m_value.number_integer)); + } + } + + /// encode a number_integer or number_unsigned value to @a out (see + /// @ref write_cbor_integer) and return the number of characters + std::size_t encode_cbor_integer(CharType* out, const BasicJsonType& j) const noexcept + { + std::uint64_t sign_mask = 0; + const std::uint64_t raw = integer_bits(j, sign_mask); + // ~raw is -1 - n in the 64 bits that are written + return encode_cbor_integer_head(out, static_cast(sign_mask & 0x20u), raw ^ sign_mask); + } + + /*! + @brief whether the CBOR and MessagePack integers of BasicJsonType are + written by @ref encode_cbor_integer and @ref encode_msgpack_integer + + These pick the format from the low 64 bits; a wider number type is written + with range checks on the whole value instead. + */ + static constexpr bool integers_fit_64_bits() noexcept + { + return sizeof(typename BasicJsonType::number_integer_t) <= sizeof(std::uint64_t) + && sizeof(typename BasicJsonType::number_unsigned_t) <= sizeof(std::uint64_t); + } + + /*! + @brief write a number_integer or number_unsigned value as a MessagePack + integer without branching on its sign or size + + Same output as @ref write_msgpack: a non-negative integer as positive fixint + (< 128) or uint 8/16/32/64, a negative one as negative fixint (>= -32) or + int 8/16/32/64. Kept out of line (see @ref write_cbor_integer). + */ + JSON_HEDLEY_NEVER_INLINE + void write_msgpack_integer(const BasicJsonType& j, std::true_type /*fits_64_bits*/) + { + oa.commit(encode_msgpack_integer(oa.reserve(integer_encoding_space), j)); + } + + /// encode a number_integer or number_unsigned value to @a out (see + /// @ref write_msgpack_integer) and return the number of characters + std::size_t encode_msgpack_integer(CharType* out, const BasicJsonType& j) const noexcept + { + std::uint64_t sign_mask = 0; + const std::uint64_t raw = integer_bits(j, sign_mask); + + // A negative n fits int 8/16/32 exactly if 2 * (-1 - n) + 1 fits + // uint 8/16/32, so x gives the size class of either sign. + const std::uint64_t is_negative = sign_mask & 1u; + const std::uint64_t x = ((raw ^ sign_mask) << is_negative) | is_negative; + + // A fixint is at most 127, or at least -32 (x at most 63). It is in + // size class 1 and is moved to class 0, the first byte, by subtracting 1. + const auto is_fixint = static_cast(x <= (127u ^ (sign_mask & 64u))); + const unsigned size_class = integer_size_class(x) - is_fixint; + + // the first byte: the low byte of the integer for a fixint, else the + // marker 0xCC..0xCF (uint) or 0xD0..0xD3 (int), selected through a mask + const std::uint64_t fixint_mask = std::uint64_t{0} - is_fixint; + const std::uint64_t marker = 0xCBu + size_class + (sign_mask & 4u); + const auto first_byte = static_cast((raw & fixint_mask) | (marker & ~fixint_mask)); + return encode_first_byte_and_tail(out, first_byte, size_class, raw); + } + + /// write a number_integer or number_unsigned value of a number type wider + /// than 64 bits as a MessagePack integer, with range checks on the whole value + void write_msgpack_integer(const BasicJsonType& j, std::false_type /*fits_64_bits*/) + { + if (j.m_data.m_type == value_t::number_unsigned) + { + if (JSON_HEDLEY_UNLIKELY(!integer_fits_max(j.m_data.m_value.number_unsigned))) + { + throw_integer_out_of_range(j, input_format_t::msgpack, "uint64"); + } + write_msgpack_unsigned(static_cast(j.m_data.m_value.number_unsigned)); + return; + } + + if (j.m_data.m_value.number_integer >= 0) + { + if (JSON_HEDLEY_UNLIKELY(!integer_fits_max(j.m_data.m_value.number_integer))) + { + throw_integer_out_of_range(j, input_format_t::msgpack, "[-2^63, 2^64-1]"); + } + // MessagePack does not differentiate between positive + // signed integers and unsigned integers. + write_msgpack_unsigned(static_cast(j.m_data.m_value.number_integer)); + } + else + { + if (JSON_HEDLEY_UNLIKELY(!integer_fits_min(j.m_data.m_value.number_integer))) + { + throw_integer_out_of_range(j, input_format_t::msgpack, "[-2^63, 2^64-1]"); + } + if (j.m_data.m_value.number_integer >= -32) + { + // negative fixnum + write_number(static_cast(j.m_data.m_value.number_integer)); + } + else if (j.m_data.m_value.number_integer >= (std::numeric_limits::min)() && + j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) + { + // int 8 + oa.write_character(to_char_type(0xD0)); + write_number(static_cast(j.m_data.m_value.number_integer)); + } + else if (j.m_data.m_value.number_integer >= (std::numeric_limits::min)() && + j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) + { + // int 16 + oa.write_character(to_char_type(0xD1)); + write_number(static_cast(j.m_data.m_value.number_integer)); + } + else if (j.m_data.m_value.number_integer >= (std::numeric_limits::min)() && + j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) + { + // int 32 + oa.write_character(to_char_type(0xD2)); + write_number(static_cast(j.m_data.m_value.number_integer)); + } + else + { + // int 64 + oa.write_character(to_char_type(0xD3)); + write_number(static_cast(j.m_data.m_value.number_integer)); + } + } + } + + /// write a float as MessagePack, into space reserved from the sink + JSON_HEDLEY_NEVER_INLINE + void write_msgpack_float(const number_float_t n) + { + oa.commit(encode_compact_float(oa.reserve(integer_encoding_space), n, to_char_type(0xCA), to_char_type(0xCB))); + } + + /*! + @brief write an object value (head and members) as MessagePack + + See @ref write_cbor_object. + */ + JSON_HEDLEY_NEVER_INLINE + void write_msgpack_object(const BasicJsonType& j, const std::size_t depth) + { + write_msgpack_object_prefix(j.m_data.m_value.object->size(), j); + + for (const auto& el : *j.m_data.m_value.object) + { + // as in write_cbor, el.first is written directly with the + // object as diagnostics context + write_msgpack_key(el.first, j); + + const BasicJsonType& value = el.second; + switch (value.type()) + { + case value_t::null: + oa.write_character(to_char_type(0xC0)); + break; + + case value_t::boolean: + oa.write_character(value.m_data.m_value.boolean ? to_char_type(0xC3) : to_char_type(0xC2)); + break; + + case value_t::number_integer: + case value_t::number_unsigned: + write_msgpack_integer(value, std::integral_constant()); + break; + + case value_t::number_float: + write_msgpack_float(value.m_data.m_value.number_float); + break; + + case value_t::string: + write_msgpack_string(*value.m_data.m_value.string, value); + break; + + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::discarded: + default: + write_msgpack(value, depth + 1); + break; + } + } + } + + /*! + @brief write an array value (head and elements) as MessagePack + + See @ref write_cbor_array. + */ + JSON_HEDLEY_NEVER_INLINE + void write_msgpack_array(const BasicJsonType& j, const std::size_t depth) + { + write_msgpack_array_prefix(j.m_data.m_value.array->size(), j); + if (!j.m_data.m_value.array->empty()) + { + write_msgpack_array_elements(j, depth, std::integral_constant()); + } + } + + JSON_HEDLEY_NEVER_INLINE + void write_msgpack_array_elements(const BasicJsonType& j, const std::size_t depth, std::true_type /*fits_64_bits*/) + { + write_array_elements(j, [this](CharType * out, const BasicJsonType & el) + { + return encode_msgpack_integer(out, el); + }, [this](CharType * out, const number_float_t n) + { + return encode_compact_float(out, n, to_char_type(0xCA), to_char_type(0xCB)); + }, [this, depth](const BasicJsonType & el) + { + write_msgpack(el, depth + 1); + }); + } + + void write_msgpack_array_elements(const BasicJsonType& j, const std::size_t depth, std::false_type /*fits_64_bits*/) + { + for (const auto& el : *j.m_data.m_value.array) + { + write_msgpack(el, depth + 1); + } + } + + /*! + @brief write an object key as a CBOR or MessagePack string (@a Walk) + + A key convertible to string_t is written directly. Other key types (only + an explicit conversion, or only a to_json overload) go through a temporary + basic_json, as in version 3.12.0; the temporary is then the diagnostics + context for strict UTF-8 checks. + */ + template::value, int> = 0> + void write_object_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& context) + { + Walk::string(*this, key, context); + } + + template>::value, int> = 0> + void write_object_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& /*context*/) + { + Walk::scalar(*this, BasicJsonType(key)); + } + + /// @brief write an object key as a CBOR text string + void write_cbor_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& context) + { + write_object_key(key, context); + } + + /// @brief write an object key as a MessagePack str + void write_msgpack_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& context) + { + write_object_key(key, context); + } + + /// the longest string that is written, with its head of one or two + /// characters, in a single reservation from the sink + static constexpr std::size_t max_text_in_reservation = max_reserve_size - 2; + + /// copy the characters of a string to space in the sink; a short one + /// without a call of memcpy() + static void copy_characters(CharType* dst, const CharType* src, const std::size_t length) noexcept + { + if (length <= 32) + { + copy_short(dst, src, length, std::integral_constant()); + } + else + { + std::memcpy(dst, src, length * sizeof(CharType)); + } + } + + /*! + @brief write a CBOR text string + + @a value is checked or sanitized according to @ref error_handler, with + @a context (the string value itself, or the object a key belongs to) used + as diagnostics context; this avoids converting object keys to a temporary + basic_json just to write them + + @note When object_t::key_type is not string_t, @a value is a temporary + string_t converted from the key, which lives only until the end of + the caller's statement. The reference returned by + @ref sanitize_utf8_for_write may refer to it, so it must not escape + this function. + */ + void write_cbor_string(const string_t& value, const BasicJsonType& context) + { + if (JSON_HEDLEY_LIKELY(error_handler == error_handler_t::keep)) + { + // the string is written as it is: head and characters in one reservation + if (JSON_HEDLEY_LIKELY(value.size() <= max_text_in_reservation)) + { + CharType* const out = oa.reserve(value.size() + 2); + CharType* p = out; + if (value.size() <= 0x17) + { + *p++ = to_char_type(static_cast(0x60 + value.size())); + } + else + { + *p++ = to_char_type(0x78); + *p++ = to_char_type(static_cast(value.size())); + } + copy_characters(p, reinterpret_cast(value.data()), value.size()); + oa.commit(static_cast(p - out) + value.size()); + return; + } + } + + write_cbor_checked_string(value, context); + } + + /// @brief write a CBOR text string checked or sanitized by the error handler; + /// out of line, as the string_t for the sanitized copy would make + /// every call of @ref write_cbor_string set up a frame for it + JSON_HEDLEY_NEVER_INLINE + void write_cbor_checked_string(const string_t& value, const BasicJsonType& context) + { + string_t storage; + const string_t& sanitized = sanitize_utf8_for_write(value, context, storage); + + // step 1: write control byte and the string length + write_cbor_head(0x60, sanitized.size()); + + // step 2: write the string + write_bytes(sanitized.data(), sanitized.size()); + } + + ///////////// + // MsgPack // + ///////////// + + /*! + @brief write a MessagePack str + + @a value is checked or sanitized according to @ref error_handler, with + @a context used as diagnostics context, as in @ref write_cbor_string + + @note As in @ref write_cbor_string, @a value may be a temporary string_t + converted from a key, so the reference returned by + @ref sanitize_utf8_for_write must not escape this function. + */ + void write_msgpack_string(const string_t& value, const BasicJsonType& context) + { + if (JSON_HEDLEY_LIKELY(error_handler == error_handler_t::keep)) + { + // the string is written as it is: head and characters in one reservation + if (JSON_HEDLEY_LIKELY(value.size() <= max_text_in_reservation)) + { + CharType* const out = oa.reserve(value.size() + 2); + CharType* p = out; + if (value.size() <= 31) + { + // fixstr + *p++ = to_char_type(static_cast(0xA0 | value.size())); + } + else + { + // str 8 + *p++ = to_char_type(0xD9); + *p++ = to_char_type(static_cast(value.size())); + } + copy_characters(p, reinterpret_cast(value.data()), value.size()); + oa.commit(static_cast(p - out) + value.size()); + return; + } + } + + write_msgpack_checked_string(value, context); + } + + /// @brief write a MessagePack str checked or sanitized by the error handler; + /// out of line, like @ref write_cbor_checked_string + JSON_HEDLEY_NEVER_INLINE + void write_msgpack_checked_string(const string_t& value, const BasicJsonType& context) + { + string_t storage; + const string_t& sanitized = sanitize_utf8_for_write(value, context, storage); + + // step 1: write control byte and the string length + const auto N = to_msgpack_length(sanitized.size(), context); + if (N <= 31) + { + // fixstr + write_number(static_cast(0xA0 | N)); + } + else if (N <= (std::numeric_limits::max)()) + { + // str 8 + oa.write_character(to_char_type(0xD9)); + write_number(static_cast(N)); + } + else if (N <= (std::numeric_limits::max)()) + { + // str 16 + oa.write_character(to_char_type(0xDA)); + write_number(static_cast(N)); + } + else + { + // str 32 + oa.write_character(to_char_type(0xDB)); + write_number(static_cast(N)); + } + + // step 2: write the string + write_bytes(sanitized.data(), sanitized.size()); + } + //////////// // UBJSON // //////////// @@ -23530,6 +29483,373 @@ class binary_writer write_ubjson_integer_payload(prefix, n, use_bjdata); } + /*! + @brief markers and size classes of UBJSON/BJData integers, by the bit length + of the magnitude + + A non-negative integer has the magnitude n, a negative one ~n = -1 - n; the + bit length of the magnitude is 1 to 64. The first 130 characters of the + table are the markers: 65 for a non-negative integer, then 65 for a negative + one (index + 65). The marker is the first of `i`, `U`, `I`, `u` (BJData), + `l`, `m` (BJData), `L`, `M` (BJData) whose range contains the integer; `H` + (a high-precision number) is for a non-negative integer of 64 bits in + UBJSON. The following 130 characters are the size classes at index + 130: + the digit 0, 1, 2, or 3 for 1, 2, 4, or 8 bytes. Looked up, like + @ref integer_size_class, instead of found by branches or comparisons. + + @sa ubjson_integer_prefix for the integers of types of more than 64 bits + */ + static const char* ubjson_integer_table(const bool use_bjdata) noexcept + { + // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays): initialized from string literals + static constexpr char ubjson_table[] = + // the markers for n >= 0, then n < 0 + "i" "iiiiiii" "U" "IIIIIII" "llllllllllllllll" "LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL" "H" + "i" "iiiiiii" "IIIIIIII" "llllllllllllllll" "LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL" "L" + // the size classes + "000000000" "1111111" "2222222222222222" "333333333333333333333333333333333" + "00000000" "11111111" "2222222222222222" "333333333333333333333333333333333"; + // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays): initialized from string literals + static constexpr char bjdata_table[] = + "i" "iiiiiii" "U" "IIIIIII" "u" "lllllllllllllll" "m" "LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL" "M" + "i" "iiiiiii" "IIIIIIII" "llllllllllllllll" "LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL" "L" + "000000000" "11111111" "2222222222222222" "33333333333333333333333333333333" + "00000000" "11111111" "2222222222222222" "333333333333333333333333333333333"; + static_assert(sizeof(ubjson_table) == 261 && sizeof(bjdata_table) == 261, "two tables of 130 markers and 130 size classes"); + return use_bjdata ? bjdata_table : ubjson_table; + } + + /// the index in ubjson_integer_table() of the integer with the 64 bits + /// @a raw and the sign mask @a sign_mask (all ones if it is negative) + static std::size_t ubjson_integer_index(const std::uint64_t raw, const std::uint64_t sign_mask) noexcept + { + const std::size_t sign_offset = sign_mask != 0 ? std::size_t{65} : + std::size_t{0}; + return static_cast(64 - count_leading_zeros((raw ^ sign_mask) | 1u)) + sign_offset; + } + + /*! + @brief encode a UBJSON/BJData integer without branching on its size + @param[in] out where the encoding goes; at most 9 characters are written + @param[in] raw the 64 bits of the integer (two's complement) + @param[in] sign_mask all ones if the integer is negative, else zero + @param[in] table ubjson_integer_table(use_bjdata) + @return the end of the encoding, or nullptr if the integer is a high-precision + number (a non-negative one of 64 bits in UBJSON), which is not written + */ + CharType* encode_ubjson_integer(CharType* out, const std::uint64_t raw, const std::uint64_t sign_mask, + const bool add_prefix, const bool use_bjdata, const char* table) const noexcept + { + const std::size_t index = ubjson_integer_index(raw, sign_mask); + const char marker = table[index]; + if (JSON_HEDLEY_UNLIKELY(marker == 'H')) + { + return nullptr; + } + const unsigned size_class = static_cast(table[index + 130]) & 3u; + + // the marker (or its place, if not written) and one 8-byte word, whose + // bytes after the integer are scratch that the caller drops + out[0] = to_char_type(static_cast(marker)); + out += add_prefix ? 1 : 0; + const unsigned unused_bits = 64u - (8u << size_class); + // UBJSON in network order, BJData in little-endian + std::uint64_t tail = 0; + if (use_bjdata) + { + tail = is_little_endian ? raw : byteswap(raw); + } + else + { + tail = is_little_endian ? byteswap(raw) >> unused_bits : raw << unused_bits; + } + std::memcpy(out, &tail, sizeof(tail)); + return out + (std::size_t{1} << size_class); + } + + /// @brief whether the integers of BasicJsonType are written by + /// @ref encode_ubjson_integer + static constexpr bool ubjson_integers_fit_64_bits() noexcept + { + return integers_fit_64_bits(); + } + + /// space for the largest encoding of encode_ubjson_integer(): the marker and 8 bytes + static constexpr std::size_t ubjson_integer_encoding_space = 9; + + /*! + @brief write the bytes of a binary value as an array of UBJSON uint8 values + + Every byte is written with its marker, in blocks that fit the space reserved + from the sink. Kept out of line, so that write_ubjson does not carry the + registers of its loop. + */ + JSON_HEDLEY_NEVER_INLINE + void write_ubjson_binary_bytes(const binary_t& binary, const bool bjdata_draft3) + { + const CharType marker = to_char_type(bjdata_draft3 ? 'B' : 'U'); + const auto* const data = binary.data(); + const std::size_t size = binary.size(); + for (std::size_t i = 0; i < size;) + { + const std::size_t block = (std::min)(size - i, max_reserve_size / 2); + CharType* const out = oa.reserve(2 * block); + for (std::size_t k = 0; k < block; ++k) + { + out[2 * k] = marker; + // the cast is needed for binary types whose value type + // is not an integer (e.g., std::byte) + out[(2 * k) + 1] = to_char_type(static_cast(data[i + k])); + } + i += block; + oa.commit(2 * block); + } + } + + /*! + @brief write the members of the object @a j of a UBJSON/BJData container + + A member whose value is null, a boolean, an integer, a float, or a string is + written without a call of @ref write_ubjson. Kept out of line, like + @ref write_cbor_object. + */ + JSON_HEDLEY_NEVER_INLINE + void write_ubjson_members(const BasicJsonType& j, const bool use_count, const bool use_type, + const bool prefix_required, const bool use_bjdata, const bjdata_version_t bjdata_version, + const std::size_t depth) + { + for (const auto& el : *j.m_data.m_value.object) + { + // a converted key must outlive the reference returned by sanitize_utf8_for_write + const object_key_string_t key_string = el.first; + write_ubjson_text(key_string, j, false, use_bjdata); + + const BasicJsonType& value = el.second; + switch (value.type()) + { + case value_t::null: + if (prefix_required) + { + oa.write_character(to_char_type('Z')); + } + break; + + case value_t::boolean: + if (prefix_required) + { + oa.write_character(value.m_data.m_value.boolean ? to_char_type('T') : to_char_type('F')); + } + break; + + case value_t::number_integer: + write_number_with_ubjson_prefix(value.m_data.m_value.number_integer, prefix_required, use_bjdata); + break; + + case value_t::number_unsigned: + write_number_with_ubjson_prefix(value.m_data.m_value.number_unsigned, prefix_required, use_bjdata); + break; + + case value_t::number_float: + write_number_with_ubjson_prefix(value.m_data.m_value.number_float, prefix_required, use_bjdata); + break; + + case value_t::string: + write_ubjson_text(*value.m_data.m_value.string, value, prefix_required, use_bjdata); + break; + + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::discarded: + default: + write_ubjson(value, use_count, use_type, prefix_required, use_bjdata, bjdata_version, depth + 1); + break; + } + } + } + + /// write the count marker `#` and the number of elements of a container as + /// an integer; a count of up to 127, which is `i` and a byte, in one reservation + void write_ubjson_count(const std::size_t count, const bool use_bjdata) + { + if (JSON_HEDLEY_LIKELY(count <= 127)) + { + CharType* const out = oa.reserve(3); + out[0] = to_char_type('#'); + out[1] = to_char_type('i'); + out[2] = to_char_type(static_cast(count)); + oa.commit(3); + return; + } + oa.write_character(to_char_type('#')); + write_number_with_ubjson_prefix(count, true, use_bjdata); + } + + /*! + @brief write a UBJSON/BJData string or object key: the optional string + marker `S`, the length as an integer with its marker, and the characters + + With the default error handler (keep) and a string of up to 253 characters, + which has a length of the marker `i` or `U` and a single byte, everything is + written into space reserved from the sink in one go. + + @param[in] value the string; checked or sanitized according to + @ref error_handler, with @a context as diagnostics context + @param[in] add_marker whether `S` is written first (not for an object key) + */ + void write_ubjson_text(const string_t& value, const BasicJsonType& context, const bool add_marker, const bool use_bjdata) + { + const std::size_t size = value.size(); + if (JSON_HEDLEY_LIKELY(error_handler == error_handler_t::keep) && JSON_HEDLEY_LIKELY(size <= max_text_in_reservation - 1)) + { + CharType* const out = oa.reserve(size + 3); + CharType* p = out; + if (add_marker) + { + *p++ = to_char_type('S'); + } + *p++ = to_char_type(size <= 127 ? 'i' : 'U'); + *p++ = to_char_type(static_cast(size)); + copy_characters(p, reinterpret_cast(value.data()), size); + oa.commit(static_cast(p - out) + size); + return; + } + + write_ubjson_checked_text(value, context, add_marker, use_bjdata); + } + + /// @brief write a UBJSON/BJData string checked or sanitized by the error + /// handler; out of line, like @ref write_cbor_checked_string + JSON_HEDLEY_NEVER_INLINE + void write_ubjson_checked_text(const string_t& value, const BasicJsonType& context, const bool add_marker, const bool use_bjdata) + { + string_t storage; + const string_t& sanitized = sanitize_utf8_for_write(value, context, storage); + if (add_marker) + { + oa.write_character(to_char_type('S')); + } + write_number_with_ubjson_prefix(sanitized.size(), true, use_bjdata); + write_bytes(sanitized.data(), sanitized.size()); + } + + /*! + @brief encode a UBJSON/BJData float, with its marker if @a add_prefix + @param[in] out where the encoding goes; at most 9 characters are written + @return the end of the encoding + */ + CharType* encode_ubjson_float(CharType* out, const number_float_t n, const bool add_prefix, const bool use_bjdata) const noexcept + { + if (add_prefix) + { + *out++ = get_ubjson_float_prefix(); + } + return store_number(out, n, use_bjdata); + } + + /// whether @a array starts with a number that @ref write_ubjson_array_elements + /// can encode (all but an integer that UBJSON writes as a high-precision + /// number); an array that does not is written by write_ubjson one element at a time + template + static bool starts_with_ubjson_number(const ArrayType& array, const bool use_bjdata, std::true_type /*fits_64_bits*/) noexcept + { + if (array.empty() || !array.front().is_number()) + { + return false; + } + const BasicJsonType& first = array.front(); + return use_bjdata || first.m_data.m_type != value_t::number_unsigned + || (static_cast(first.m_data.m_value.number_unsigned) >> 63u) == 0; + } + + template + static bool starts_with_ubjson_number(const ArrayType& /*array*/, const bool /*use_bjdata*/, std::false_type /*fits_64_bits*/) noexcept + { + return false; + } + + /*! + @brief write the elements of an array of a UBJSON/BJData container that + starts with a number, with runs of numbers encoded without a call of + @ref write_ubjson for each + + Same output as calling write_ubjson for each element. The numbers of a + run are encoded straight into space reserved from the sink, as in + @ref write_array_elements; all other values, and an integer that UBJSON + writes as a high-precision number, are written with write_ubjson. Kept out + of line, like @ref write_cbor_array. + */ + template + JSON_HEDLEY_NEVER_INLINE + void write_ubjson_array_elements(const ArrayType& array, const bool use_count, const bool use_type, + const bool prefix_required, const bool use_bjdata, const bjdata_version_t bjdata_version, + const std::size_t depth) + { + constexpr std::size_t max_run = max_reserve_size / ubjson_integer_encoding_space; + const char* const table = ubjson_integer_table(use_bjdata); + auto it = array.begin(); + const auto end = array.end(); + while (it != end) + { + if (!it->is_number()) + { + write_ubjson(*it, use_count, use_type, prefix_required, use_bjdata, bjdata_version, depth + 1); + ++it; + continue; + } + + CharType* const start = oa.reserve(max_run * ubjson_integer_encoding_space); + CharType* out = start; + bool unencodable = false; + for (std::size_t n = 0; n < max_run && it != end && it->is_number(); ++n) + { + CharType* next = nullptr; + if (it->is_number_float()) + { + next = encode_ubjson_float(out, it->m_data.m_value.number_float, prefix_required, use_bjdata); + } + else + { + std::uint64_t sign_mask = 0; + const std::uint64_t raw = integer_bits(*it, sign_mask); + next = encode_ubjson_integer(out, raw, sign_mask, prefix_required, use_bjdata, table); + } + if (JSON_HEDLEY_UNLIKELY(next == nullptr)) + { + unencodable = true; + break; + } + out = next; + ++it; + } + oa.commit(static_cast(out - start)); + + if (unencodable) + { + write_ubjson(*it, use_count, use_type, prefix_required, use_bjdata, bjdata_version, depth + 1); + ++it; + } + } + } + + /*! + @brief store @a n at @a out in the byte order of the output + @param[in] output_little_endian whether the output is little-endian + @return the end of the number + */ + template + CharType* store_number(CharType* out, const NumberType n, const bool output_little_endian) const noexcept + { + std::array vec{}; + std::memcpy(vec.data(), &n, sizeof(NumberType)); + if (is_little_endian != output_little_endian) + { + reverse_bytes(vec); + } + std::memcpy(out, vec.data(), sizeof(NumberType)); + return out + sizeof(NumberType); + } + /*! @brief determine the UBJSON/BJData type marker of an integer @@ -23577,7 +29897,7 @@ class binary_writer { return 'L'; } - if (use_bjdata && std::is_unsigned::value) + if (use_bjdata && std::is_unsigned::value && integer_fits_max(n)) { return 'M'; } @@ -23620,19 +29940,58 @@ class binary_writer break; default: { - // high-precision number: the decimal digits as a string JSON_ASSERT(prefix == 'H'); - const auto number = BasicJsonType(n).dump(); - write_number_with_ubjson_prefix(number.size(), true, use_bjdata); - for (std::size_t i = 0; i < number.size(); ++i) - { - oa.write_character(to_char_type(static_cast(number[i]))); - } + write_ubjson_high_precision(n, use_bjdata); break; } } } + /// @brief write an integer as a high-precision number: its decimal digits as + /// a string; out of line, as the string would otherwise burden + /// @ref write_ubjson_integer_payload for every integer + template + JSON_HEDLEY_NEVER_INLINE + void write_ubjson_high_precision(const NumberType n, const bool use_bjdata) + { + write_ubjson_high_precision(n, use_bjdata, std::integral_constant < bool, std::is_unsigned::value && sizeof(NumberType) <= sizeof(std::uint64_t) > ()); + } + + /// an unsigned integer of at most 64 bits: its decimal digits are written directly + template + void write_ubjson_high_precision(const NumberType n, const bool use_bjdata, std::true_type /*is_unsigned_64_bits*/) + { + std::array digits; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read + std::size_t first = digits.size(); + auto value = static_cast(n); + do + { + digits[--first] = static_cast('0' + (value % 10)); + value /= 10; + } + while (value != 0); + const std::size_t length = digits.size() - first; + write_number_with_ubjson_prefix(length, true, use_bjdata); + CharType* const out = oa.reserve(digits.size()); + for (std::size_t i = 0; i < length; ++i) + { + out[i] = to_char_type(static_cast(digits[first + i])); + } + oa.commit(length); + } + + /// any other integer: its decimal digits from basic_json::dump() + template + void write_ubjson_high_precision(const NumberType n, const bool use_bjdata, std::false_type /*is_unsigned_64_bits*/) + { + const auto number = BasicJsonType(n).dump(); + write_number_with_ubjson_prefix(number.size(), true, use_bjdata); + for (std::size_t i = 0; i < number.size(); ++i) + { + oa.write_character(to_char_type(static_cast(number[i]))); + } + } + /*! @brief determine the type prefix of container values */ @@ -23702,14 +30061,16 @@ class binary_writer @brief checks whether a JSON number fits into @a TargetType @param[in] el a JSON number of either the signed or unsigned integer kind @return whether @a el's value can be represented by @a TargetType without - wrapping, regardless of which of the two kinds it is stored as + wrapping, regardless of which of the two kinds it is stored as; + false for a value that does not even fit the 64-bit type it is + read as (possible for number types wider than 64 bits) */ template static bool bjdata_ndarray_value_in_range(const BasicJsonType& el) { return el.is_number_unsigned() - ? value_in_range_of(el.template get()) - : value_in_range_of(el.template get()); + ? integer_fits_max(el.m_data.m_value.number_unsigned) && value_in_range_of(static_cast(el.m_data.m_value.number_unsigned)) + : integer_fits(el.m_data.m_value.number_integer) && value_in_range_of(static_cast(el.m_data.m_value.number_integer)); } /*! @@ -23798,7 +30159,7 @@ class binary_writer */ bool write_bjdata_ndarray_float_element(const BasicJsonType& el, const bool dry_run) { - const auto dval = el.template get(); + const auto dval = static_cast(el.m_data.m_value.number_float); if (dry_run) { #ifdef __GNUC__ @@ -23822,6 +30183,90 @@ class binary_writer return true; } + /*! + @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 + in range by a dry run of @ref write_bjdata_ndarray_elements + + The elements are encoded in blocks into space reserved from the sink. + */ + void write_bjdata_ndarray_values(const BasicJsonType& array_data, const CharType dtype) + { + switch (dtype) + { + case 'U': + case 'C': + case 'B': + write_bjdata_ndarray_block(array_data); + break; + case 'i': + write_bjdata_ndarray_block(array_data); + break; + case 'u': + write_bjdata_ndarray_block(array_data); + break; + case 'I': + write_bjdata_ndarray_block(array_data); + break; + case 'm': + write_bjdata_ndarray_block(array_data); + break; + case 'l': + write_bjdata_ndarray_block(array_data); + break; + case 'M': + write_bjdata_ndarray_block(array_data); + break; + case 'L': + write_bjdata_ndarray_block(array_data); + break; + case 'd': + write_bjdata_ndarray_block(array_data); + break; + case 'D': + default: + // 'D' (double) already spans the full range of number_float_t + write_bjdata_ndarray_block(array_data); + break; + } + } + + /// the value of an ND-array element that is an integer, as the integer type that @a T is converted from + template + static T bjdata_ndarray_integer(const BasicJsonType& el, std::true_type /*is_integral*/) noexcept + { + using storage_type = typename std::conditional::value, std::uint64_t, std::int64_t>::type; + return static_cast(el.is_number_unsigned() + ? static_cast(el.m_data.m_value.number_unsigned) + : static_cast(el.m_data.m_value.number_integer)); + } + + /// the value of an ND-array element that is a float + template + static T bjdata_ndarray_integer(const BasicJsonType& el, std::false_type /*is_integral*/) noexcept + { + return static_cast(static_cast(el.m_data.m_value.number_float)); + } + + /// write the elements of an ND-array as @a T, little-endian, in blocks + template + void write_bjdata_ndarray_block(const BasicJsonType& array_data) + { + constexpr std::size_t per_block = max_reserve_size / sizeof(T); + auto it = array_data.m_data.m_value.array->begin(); + const auto end = array_data.m_data.m_value.array->end(); + while (it != end) + { + CharType* const start = oa.reserve(per_block * sizeof(T)); + CharType* out = start; + for (std::size_t n = 0; n < per_block && it != end; ++n, ++it) + { + out = store_number(out, bjdata_ndarray_integer(*it, std::is_integral()), true); + } + oa.commit(static_cast(out - start)); + } + } + /*! @brief validate or write every element of a BJData ND-array's `_ArrayData_` @param[in] array_data the `_ArrayData_` array @@ -23831,6 +30276,12 @@ class binary_writer */ bool write_bjdata_ndarray_elements(const BasicJsonType& array_data, const CharType dtype, const bool dry_run) { + if (!dry_run) + { + write_bjdata_ndarray_values(array_data, dtype); + return true; + } + for (const auto& el : array_data) { bool ok = true; @@ -23941,10 +30392,11 @@ class binary_writer for (const auto& el : dims) { // a dimension is read as an unsigned value below, so anything that - // is not a non-negative integer is rejected: a non-integer entry - // would pun unrelated bytes as the dimension, and a negative one - // would wrap into a nonsensical length - if (!el.is_number_integer() || (!el.is_number_unsigned() && el.template get() < 0)) + // is not a non-negative integer in the range of std::uint64_t is + // rejected: a non-integer entry would pun unrelated bytes as the + // dimension, and a negative or wider one would wrap into a + // nonsensical length + if (!el.is_number_integer() || !bjdata_ndarray_value_in_range(el)) { return true; } @@ -24039,9 +30491,27 @@ class binary_writer @param[in] j JSON value to serialize @param[in,out] string_open whether the output ends with a non-empty string that has not been terminated with 0xFF + @param[in] depth nesting level of @a j, counted from the + top-level value passed to @ref basic_json::to_bon8 + @throw type_error.316 if a string value or an object key is not valid + UTF-8 + @throw out_of_range.407 if an unsigned integer does not fit int64 + @throw type_error.321 if @a j or a value nested in it is discarded + + Values nested deeper than @ref recursion_depth_limit are written by + @ref write_bon8_iterative without the call stack. + + @sa @ref write_cbor + @sa https://github.com/nlohmann/json/issues/5392 */ - void write_bon8_value(const BasicJsonType& j, bool& string_open) + void write_bon8_value(const BasicJsonType& j, bool& string_open, const std::size_t depth = 0) { + if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit()) && (j.is_array() || j.is_object())) + { + write_bon8_iterative(j, string_open); + return; + } + switch (j.type()) { case value_t::null: @@ -24058,9 +30528,9 @@ class binary_writer case value_t::number_unsigned: { - if (j.m_data.m_value.number_unsigned > static_cast((std::numeric_limits::max)())) + if (JSON_HEDLEY_UNLIKELY(!integer_fits_max(j.m_data.m_value.number_unsigned))) { - JSON_THROW(out_of_range::create(407, concat("integer number ", std::to_string(j.m_data.m_value.number_unsigned), " cannot be represented by BON8 as it does not fit int64"), &j)); + throw_integer_out_of_range(j, input_format_t::bon8, "int64"); } write_bon8_integer(static_cast(j.m_data.m_value.number_unsigned)); string_open = false; @@ -24069,6 +30539,10 @@ class binary_writer case value_t::number_integer: { + if (JSON_HEDLEY_UNLIKELY(!integer_fits(j.m_data.m_value.number_integer))) + { + throw_integer_out_of_range(j, input_format_t::bon8, "int64"); + } write_bon8_integer(static_cast(j.m_data.m_value.number_integer)); string_open = false; break; @@ -24093,9 +30567,16 @@ class binary_writer // 0x80..0x84: array with 0..4 elements; 0x85: array ended by 0xFE write_bon8_marker(static_cast(N <= 4 ? 0x80 + N : 0x85), string_open); - for (const auto& el : *j.m_data.m_value.array) + if (N != 0 && j.m_data.m_value.array->front().is_number()) { - write_bon8_value(el, string_open); + write_bon8_array_elements(*j.m_data.m_value.array, string_open, depth); + } + else + { + for (const auto& el : *j.m_data.m_value.array) + { + write_bon8_value(el, string_open, depth + 1); + } } if (N > 4) @@ -24111,11 +30592,7 @@ class binary_writer // 0x86..0x8A: object with 0..4 members; 0x8B: object ended by 0xFE write_bon8_marker(static_cast(N <= 4 ? 0x86 + N : 0x8B), string_open); - for (const auto& el : *j.m_data.m_value.object) - { - write_bon8_string(el.first, string_open, j); - write_bon8_value(el.second, string_open); - } + write_bon8_members(j, string_open, depth); if (N > 4) { @@ -24126,28 +30603,365 @@ class binary_writer case value_t::binary: { - // BON8 has no binary type: write the bytes as an array of - // integers, like UBJSON and BJData do - const auto N = j.m_data.m_value.binary->size(); - write_bon8_marker(static_cast(N <= 4 ? 0x80 + N : 0x85), string_open); - - for (std::size_t i = 0; i < N; ++i) - { - // the cast is needed for binary types whose value type - // is not an integer (e.g., std::byte) - write_bon8_integer(static_cast(j.m_data.m_value.binary->data()[i])); - } - - if (N > 4) - { - write_bon8_marker(0xFE, string_open); - } + write_bon8_binary(*j.m_data.m_value.binary, string_open); break; } case value_t::discarded: default: - break; + throw_on_discarded(j, input_format_t::bon8); + } + } + + /*! + @brief write @a j with @ref write_bon8_value, or write its marker and push + a frame for @ref write_bon8_iterative to continue with its elements + + A scalar, and an empty array or object, are written out in full and not + pushed. + + @sa @ref write_container_value_or_push + */ + void write_bon8_value_or_push(const BasicJsonType& j, bool& string_open, std::vector& stack) + { + if (j.is_array() || j.is_object()) + { + // arrays use the markers 0x80..0x85, objects 0x86..0x8B; the last + // one stands for more than four elements, closed later with 0xFE + const auto N = j.size(); + const std::size_t base = j.is_array() ? 0x80 : 0x86; + write_bon8_marker(static_cast(base + (N <= 4 ? N : 5)), string_open); + if (N != 0) + { + stack.emplace_back(&j); + } + return; + } + + write_bon8_value(j, string_open); + } + + /*! + @brief write out @a root and everything below it without the call stack + + A container with more than four elements is closed with 0xFE once its + last element is written, as in @ref write_bon8_value. + + @sa @ref write_cbor_iterative + */ + void write_bon8_iterative(const BasicJsonType& root, bool& string_open) + { + // only a container with elements is ever pushed; see write_bon8_value_or_push + std::vector stack; + write_bon8_value_or_push(root, string_open, stack); + + while (!stack.empty()) + { + const binary_container_frame current = stack.back(); + const bool is_array = current.value->is_array(); + const bool at_end = is_array + ? current.array_it == current.value->m_data.m_value.array->cend() + : current.object_it == current.value->m_data.m_value.object->cend(); + + if (at_end) + { + if (current.value->size() > 4) + { + write_bon8_marker(0xFE, string_open); + } + stack.pop_back(); + continue; + } + + // read the child before pushing: entering it can move every frame + const BasicJsonType* child = nullptr; + if (is_array) + { + child = &(*current.array_it); + ++stack.back().array_it; + } + else + { + write_bon8_string(current.object_it->first, string_open, *current.value); + child = &(current.object_it->second); + ++stack.back().object_it; + } + write_bon8_value_or_push(*child, string_open, stack); + } + } + + /*! + @brief encode an integer as @ref write_bon8_integer writes it + @param[in] out where the encoding goes; at most 9 characters are written + @return the end of the encoding + */ + CharType* encode_bon8_integer(CharType* out, std::int64_t value) const noexcept + { + if (value < (std::numeric_limits::min)() || value > (std::numeric_limits::max)()) + { + *out++ = to_char_type(0x8D); + return store_number(out, value, false); + } + if (value < -33818506 || value > 67637031) + { + *out++ = to_char_type(0x8C); + return store_number(out, static_cast(value), false); + } + if (value <= -264075) + { + value = -(value + 264075); + return put_bon8_bytes(out, 0xF0 + ((value >> 22) & 0x07), 0xC0 + ((value >> 16) & 0x3F), value >> 8, value); + } + if (value <= -1931) + { + value = -(value + 1931); + return put_bon8_bytes(out, 0xE0 + ((value >> 14) & 0x0F), 0xC0 + ((value >> 8) & 0x3F), value); + } + if (value <= -11) + { + value = -(value + 11); + return put_bon8_bytes(out, 0xC2 + ((value >> 6) & 0x1F), 0xC0 + (value & 0x3F)); + } + if (value <= -1) + { + return put_bon8_bytes(out, 0xB8 - (value + 1)); + } + if (value <= 39) + { + return put_bon8_bytes(out, 0x90 + value); + } + if (value <= 3879) + { + value -= 40; + return put_bon8_bytes(out, 0xC2 + ((value >> 7) & 0x1F), value & 0x7F); + } + if (value <= 528167) + { + value -= 3880; + return put_bon8_bytes(out, 0xE0 + ((value >> 15) & 0x0F), (value >> 8) & 0x7F, value); + } + value -= 528168; + return put_bon8_bytes(out, 0xF0 + ((value >> 23) & 0x07), (value >> 16) & 0x7F, value >> 8, value); + } + + /// store the low byte of each argument at @a out; return the end + template + CharType* put_bon8_bytes(CharType* out, const Bytes... bytes) const noexcept + { + const std::array buffer{{to_char_type(static_cast(bytes & 0xFF))...}}; + std::memcpy(out, buffer.data(), buffer.size()); + return out + buffer.size(); + } + + /*! + @brief encode a float as @ref write_bon8_float writes it + @param[in] out where the encoding goes; at most 9 characters are written + @return the end of the encoding + */ + CharType* encode_bon8_float(CharType* out, const number_float_t n) const noexcept + { +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_PUSH + JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") +#endif + if (n == static_cast(-1)) + { + *out++ = to_char_type(0xFB); + return out; + } + if (n == static_cast(0) && !std::signbit(n)) + { + *out++ = to_char_type(0xFC); + return out; + } + if (n == static_cast(1)) + { + *out++ = to_char_type(0xFD); + return out; + } + if (std::isnan(n)) + { + return put_bon8_bytes(out, 0x8E, 0x7F, 0x80, 0x00, 0x01); + } + return out + encode_compact_float(out, n, to_char_type(0x8E), to_char_type(0x8F)); +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_POP +#endif + } + + /*! + @brief write the elements of an array that starts with a number, with runs + of numbers encoded without a call of @ref write_bon8_value for each + + Same output as calling write_bon8_value for each element. The numbers of a + run are encoded into space reserved from the sink, as in + @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 + from write_bon8_value, like any other value that is not a number. Kept out + of line, like @ref write_cbor_array. + */ + template + JSON_HEDLEY_NEVER_INLINE + void write_bon8_array_elements(const ArrayType& array, bool& string_open, const std::size_t depth) + { + constexpr std::size_t max_run = max_reserve_size / 9; + auto it = array.begin(); + const auto end = array.end(); + while (it != end) + { + if (!it->is_number()) + { + write_bon8_value(*it, string_open, depth + 1); + ++it; + continue; + } + + CharType* const start = oa.reserve(max_run * 9); + CharType* out = start; + bool unencodable = false; + for (std::size_t n = 0; n < max_run && it != end && it->is_number(); ++n) + { + if (it->is_number_float()) + { + out = encode_bon8_float(out, it->m_data.m_value.number_float); + } + else if (JSON_HEDLEY_UNLIKELY((it->m_data.m_type == value_t::number_unsigned + && !integer_fits_max(it->m_data.m_value.number_unsigned)) + || (it->m_data.m_type == value_t::number_integer + && !integer_fits(it->m_data.m_value.number_integer)))) + { + unencodable = true; + break; + } + else + { + out = encode_bon8_integer(out, it->m_data.m_type == value_t::number_integer + ? static_cast(it->m_data.m_value.number_integer) + : static_cast(it->m_data.m_value.number_unsigned)); + } + ++it; + } + oa.commit(static_cast(out - start)); + if (out != start) + { + // a number follows a string without terminating it + string_open = false; + } + + if (unencodable) + { + write_bon8_value(*it, string_open, depth + 1); + ++it; + } + } + } + + /*! + @brief write the members of the object @a j as BON8 + + A member whose value is null, a boolean, a number, or a string is written + without a call of @ref write_bon8_value. Kept out of line, like + @ref write_cbor_object. + */ + JSON_HEDLEY_NEVER_INLINE + void write_bon8_members(const BasicJsonType& j, bool& string_open, const std::size_t depth) + { + for (const auto& el : *j.m_data.m_value.object) + { + write_bon8_string(el.first, string_open, j); + + const BasicJsonType& value = el.second; + switch (value.type()) + { + case value_t::null: + write_bon8_marker(0xFA, string_open); + break; + + case value_t::boolean: + write_bon8_marker(value.m_data.m_value.boolean ? 0xF9 : 0xF8, string_open); + break; + + case value_t::string: + write_bon8_string(*value.m_data.m_value.string, string_open, value); + break; + + case value_t::number_integer: + if (JSON_HEDLEY_LIKELY(integer_fits(value.m_data.m_value.number_integer))) + { + write_bon8_integer(static_cast(value.m_data.m_value.number_integer)); + string_open = false; + break; + } + // beyond 64 bits (number types wider than that): throws + write_bon8_value(value, string_open, depth + 1); + break; + + case value_t::number_float: + write_bon8_float(value.m_data.m_value.number_float); + string_open = false; + break; + + case value_t::number_unsigned: + if (JSON_HEDLEY_LIKELY(integer_fits_max(value.m_data.m_value.number_unsigned))) + { + write_bon8_integer(static_cast(value.m_data.m_value.number_unsigned)); + string_open = false; + break; + } + // too large for BON8: throws + write_bon8_value(value, string_open, depth + 1); + break; + + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::discarded: + default: + write_bon8_value(value, string_open, depth + 1); + break; + } + } + } + + /*! + @brief write a binary value as BON8 + + BON8 has no binary type: the bytes are written as an array of integers, + like UBJSON and BJData do. Kept out of line, so that write_bon8_value does + not carry the registers of its loop. + */ + JSON_HEDLEY_NEVER_INLINE + void write_bon8_binary(const binary_t& binary, bool& string_open) + { + const auto N = binary.size(); + write_bon8_marker(static_cast(N <= 4 ? 0x80 + N : 0x85), string_open); + + // a byte is an integer of 0 to 255: one byte up to 39, two bytes above + const auto* const data = binary.data(); + for (std::size_t i = 0; i < N;) + { + const std::size_t block = (std::min)(N - i, max_reserve_size / 2); + CharType* const start = oa.reserve(2 * block); + CharType* out = start; + for (std::size_t k = 0; k < block; ++k, ++i) + { + // the cast is needed for binary types whose value type + // is not an integer (e.g., std::byte) + const auto value = static_cast(static_cast(data[i])); + // both bytes are stored, the second only counts for a value above 39: + // 0xC2 + ((value - 40) >> 7), (value - 40) & 0x7F + const unsigned wide = value - 40u; + const bool is_wide = value > 39u; + out[0] = to_char_type(static_cast(is_wide ? 0xC2u + (wide >> 7u) : 0x90u + value)); + out[1] = to_char_type(static_cast(wide & 0x7Fu)); + out += is_wide ? 2 : 1; + } + oa.commit(static_cast(out - start)); + } + + if (N > 4) + { + write_bon8_marker(0xFE, string_open); } } @@ -24192,7 +31006,7 @@ class binary_writer } else { - oa.write_characters(reinterpret_cast(s.data()), s.size()); + write_bytes(s.data(), s.size()); string_open = true; } } @@ -24208,15 +31022,25 @@ class binary_writer */ static void check_utf8(const string_t& s, const BasicJsonType& context) { - static_cast(context); // only used when exceptions are enabled const auto* data = reinterpret_cast(s.data()); const std::size_t valid = valid_utf8_prefix(data, s.size()); if (JSON_HEDLEY_UNLIKELY(valid != s.size())) { - JSON_THROW(type_error::create(316, concat("invalid UTF-8 byte at index ", std::to_string(valid), ": 0x", detail::hex_byte(data[valid])), &context)); + throw_invalid_utf8(data, valid, context); } } + /// @brief throw type_error.316 for the invalid byte at @a index of @a data; + /// out of line, so that the message does not burden check_utf8() + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_invalid_utf8(const unsigned char* data, const std::size_t index, const BasicJsonType& context) + { + static_cast(data); // only used when exceptions are enabled + static_cast(index); + static_cast(context); + JSON_THROW(type_error::create(exception_id::invalid_utf8, concat("invalid UTF-8 byte at index ", std::to_string(index), ": 0x", detail::hex_byte(data[index])), &context)); + } + /*! @brief return @a s as it should be written, honoring @ref error_handler @@ -24239,6 +31063,11 @@ class binary_writer itself in every case but a sanitized `replace`/`ignore` one, so @a storage must outlive the returned reference only then. + @a s must be an lvalue that outlives the returned reference. An object key + whose `key_type` is not @ref string_t must therefore first be converted + into a named string_t (see @ref object_key_string_t); the deleted overload + below enforces this at compile time. + @param[in] s the string (value or object key) to write @param[in] context the value @a s belongs to (for diagnostics) @param[out] storage backing storage for a sanitized copy @@ -24268,6 +31097,35 @@ class binary_writer } } + /*! + @brief the write-pass counterpart of @ref sanitize_utf8_for_write for BSON + + The size pass (@ref calc_bson_entry_header_size, @ref + calc_bson_string_size) has already validated @a s, and thrown for @ref + error_handler_t::strict if it was ill-formed. Here, @ref + error_handler_t::strict therefore behaves like @ref error_handler_t::keep, + and @ref error_handler_t::replace / @ref error_handler_t::ignore produce + exactly the bytes the size pass measured. Hence no context is needed. + + @param[in] s the string (value or object key) to write + @param[out] storage backing storage for a sanitized copy + + @return a reference to @a s, or to @a storage once it holds a sanitized copy + */ + const string_t& sanitize_utf8_for_bson_write(const string_t& s, string_t& storage) const + { + if (error_handler == error_handler_t::keep || error_handler == error_handler_t::strict || is_valid_utf8(s)) + { + return s; // NOLINT(bugprone-return-const-ref-from-parameter): callers pass lvalues that outlive the call + } + storage = sanitize_utf8(s, error_handler); + return storage; + } + + /// deleted: anything but a string_t would bind a temporary that dies before the returned reference is used + template < typename T, enable_if_t < !std::is_same::value, int > = 0 > + const string_t& sanitize_utf8_for_write(const T& /*s*/, const BasicJsonType& /*context*/, string_t& /*storage*/) const = delete; // NOLINT(hicpp-use-equals-delete,modernize-use-equals-delete): a private helper's guard, not part of the interface + /*! @brief write an integer in the shortest encoding @@ -24384,53 +31242,14 @@ class binary_writer // Utility functions // /////////////////////// - // single-instruction byte swaps (compilers lower these to bswap/rev/movbe); - // used to emit big-endian numbers without a per-byte std::reverse loop - static std::uint16_t byte_swap(std::uint16_t x) noexcept - { -#if defined(__GNUC__) || defined(__clang__) - return __builtin_bswap16(x); -#elif defined(_MSC_VER) - return _byteswap_ushort(x); -#else - return static_cast((x >> 8) | (x << 8)); -#endif - } - - static std::uint32_t byte_swap(std::uint32_t x) noexcept - { -#if defined(__GNUC__) || defined(__clang__) - return __builtin_bswap32(x); -#elif defined(_MSC_VER) - return _byteswap_ulong(x); -#else - return ((x & 0x000000FFu) << 24) | ((x & 0x0000FF00u) << 8) - | ((x & 0x00FF0000u) >> 8) | ((x & 0xFF000000u) >> 24); -#endif - } - - static std::uint64_t byte_swap(std::uint64_t x) noexcept - { -#if defined(__GNUC__) || defined(__clang__) - return __builtin_bswap64(x); -#elif defined(_MSC_VER) - return _byteswap_uint64(x); -#else - x = ((x & 0x00000000FFFFFFFFull) << 32) | ((x & 0xFFFFFFFF00000000ull) >> 32); - x = ((x & 0x0000FFFF0000FFFFull) << 16) | ((x & 0xFFFF0000FFFF0000ull) >> 16); - x = ((x & 0x00FF00FF00FF00FFull) << 8) | ((x & 0xFF00FF00FF00FF00ull) >> 8); - return x; -#endif - } - /*! @brief reverse the bytes of a buffer by byte-swapping it as UIntType Loading the buffer into an unsigned integer of the same width and swapping that is what lets the compiler emit a single bswap/rev/movbe; reversing the buffer element by element does not reliably get there (clang keeps a scalar - shuffle). The two memcpy calls are the only portable way to reinterpret the - bytes and are folded away by every optimizer. + shuffle). (The two memcpy calls, rather than bit_cast, keep GCC from + inlining differently in C++20, which made writing slower.) */ template static void byte_swap_buffer(std::array& a) noexcept @@ -24438,11 +31257,11 @@ class binary_writer static_assert(sizeof(UIntType) == N, "swap width must match the buffer size"); UIntType v{}; std::memcpy(&v, a.data(), sizeof(v)); - v = byte_swap(v); + v = byteswap(v); std::memcpy(a.data(), &v, sizeof(v)); } - // reverse the bytes of a fixed-size buffer; a single byte_swap() for the + // reverse the bytes of a fixed-size buffer; a single byteswap() for the // common 2/4/8-byte number payloads, std::reverse for any other size static void reverse_bytes(std::array& a) noexcept { @@ -24495,6 +31314,67 @@ class binary_writer oa.write_characters(vec.data(), sizeof(NumberType)); } + /// whether @a n is written as a float32 by @ref write_compact_float + static bool fits_float32(const number_float_t n) noexcept + { + static_assert(std::is_same::value || std::is_same::value, + "number_float_t must be float or double for the CBOR/MessagePack/BON8 writer"); +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_PUSH + JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") +#endif +#if defined(__GNUC__) && !defined(__clang__) && (__GNUC__ >= 7) + JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wduplicated-branches") +#endif + return !std::isfinite(n) || ((static_cast(n) >= static_cast(std::numeric_limits::lowest()) && + static_cast(n) <= static_cast((std::numeric_limits::max)()) && + static_cast(static_cast(n)) == static_cast(n))); +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_POP +#endif + } + + /*! + @brief encode @a n to @a out as @ref write_compact_float writes it + @return the number of characters, 5 or 9 + */ + std::size_t encode_compact_float(CharType* out, const number_float_t n, const CharType float32_marker, const CharType float64_marker) const noexcept + { + if (fits_float32(n)) + { + *out = float32_marker; + store_number(out + 1, static_cast(n), false); + return 1 + sizeof(float); + } + *out = float64_marker; + store_number(out + 1, n, false); + return 1 + sizeof(number_float_t); + } + + /*! + @brief encode a float as @ref write_cbor writes it + @return the number of characters, 3, 5 or 9 + */ + std::size_t encode_cbor_float(CharType* out, const number_float_t n) const noexcept + { + if (JSON_HEDLEY_UNLIKELY(!std::isfinite(n))) + { + // NaN is 0xf97e00, Infinity is 0xf97c00, -Infinity is 0xf9fc00 + out[0] = to_char_type(0xF9); + if (std::isnan(n)) + { + out[1] = to_char_type(0x7E); + } + else + { + out[1] = n > 0 ? to_char_type(0x7C) : to_char_type(0xFC); + } + out[2] = to_char_type(0x00); + return 3; + } + return encode_compact_float(out, n, to_char_type(0xFA), to_char_type(0xFB)); + } + /// @brief write @a n using @a float32_marker if it round-trips through /// float, otherwise using @a float64_marker /// @@ -24504,24 +31384,7 @@ class binary_writer /// no longer needs to be passed in. void write_compact_float(const number_float_t n, const CharType float32_marker, const CharType float64_marker) { - static_assert(std::is_same::value || std::is_same::value, - "number_float_t must be float or double for the CBOR/MessagePack/BON8 writer"); -#ifdef __GNUC__ - JSON_HEDLEY_DIAGNOSTIC_PUSH - JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") -#endif - // When number_float_t is float, static_cast(n) is the identity and - // both branches below are intentionally identical (the "compact" float - // representation is the value itself). Only GCC diagnoses this, and only - // when the sink calls are inlined; clang has no such warning. - // (-Wduplicated-branches only exists from GCC 7 on; naming it on an older - // GCC would itself warn under -Wpragmas) -#if defined(__GNUC__) && !defined(__clang__) && (__GNUC__ >= 7) - JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wduplicated-branches") -#endif - if (!std::isfinite(n) || ((static_cast(n) >= static_cast(std::numeric_limits::lowest()) && - static_cast(n) <= static_cast((std::numeric_limits::max)()) && - static_cast(static_cast(n)) == static_cast(n)))) + if (fits_float32(n)) { oa.write_character(float32_marker); write_number(static_cast(n)); @@ -24531,9 +31394,6 @@ class binary_writer oa.write_character(float64_marker); write_number(n); } -#ifdef __GNUC__ - JSON_HEDLEY_DIAGNOSTIC_POP -#endif } public: @@ -24567,9 +31427,7 @@ class binary_writer #endif static_assert(sizeof(std::uint8_t) == sizeof(CharType), "size of CharType must be equal to std::uint8_t"); - CharType result; - std::memcpy(&result, &x, sizeof(x)); - return result; + return bit_cast(x); } template +// SPDX-FileCopyrightText: 2025 Victor Zverovich // SPDX-FileCopyrightText: 2013-2026 Niels Lohmann // SPDX-License-Identifier: MIT @@ -24646,11 +31505,258 @@ NLOHMANN_JSON_NAMESPACE_END #include // array #include // signbit, isfinite +#include // size_t #include // intN_t, uintN_t #include // memcpy, memmove #include // numeric_limits #include // conditional + +// SSE2 (every x86-64 CPU) and NEON (every 64-bit Arm CPU) convert the 16 +// digits of a double at once (unless JSON_NO_SIMD selects the portable code) +#if !defined(JSON_NO_SIMD) && (defined(__x86_64__) || (defined(_M_X64) && !defined(_M_ARM64EC))) + #include + #define JSON_DTOA_SSE2 1 + #define JSON_DTOA_NEON 0 +#elif !defined(JSON_NO_SIMD) && (defined(__aarch64__) || defined(_M_ARM64)) && !defined(_M_ARM64EC) && !defined(__ARM_BIG_ENDIAN) + #include + #define JSON_DTOA_SSE2 0 + #define JSON_DTOA_NEON 1 +#else + #define JSON_DTOA_SSE2 0 + #define JSON_DTOA_NEON 0 +#endif + +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2025 Victor Zverovich +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // array +#include // size_t +#include // uint32_t, uint64_t + +// #include + +// #include + +// #include + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +/*! +@brief the shortest decimal representation of a double + +A C++11 port of the conversion of Zmij by Victor Zverovich +(https://github.com/vitaut/zmij, MIT license): the shortest decimal in the +rounding interval of a double, the closest one if there are several. Zmij +credits Xiang JunBo (producing the shorter candidate without a division) and +Dougall Johnson (the compressed powers of ten). The powers of ten are taken +from the table for number parsing (pow5_table.hpp) where it holds them, and +computed from the compressed tables of Zmij beyond it. +*/ +namespace zmij +{ + +/// the compressed powers of ten of Zmij +inline const std::array& pow10_minor() noexcept +{ + static const std::array table = + { + { + 0x8000000000000000u, 0xa000000000000000u, 0xc800000000000000u, 0xfa00000000000000u, 0x9c40000000000000u, + 0xc350000000000000u, 0xf424000000000000u, 0x9896800000000000u, 0xbebc200000000000u, 0xee6b280000000000u, + 0x9502f90000000000u, 0xba43b74000000000u, 0xe8d4a51000000000u, 0x9184e72a00000000u, 0xb5e620f480000000u, + 0xe35fa931a0000000u, 0x8e1bc9bf04000000u, 0xb1a2bc2ec5000000u, 0xde0b6b3a76400000u, 0x8ac7230489e80000u, + 0xad78ebc5ac620000u, 0xd8d726b7177a8000u, 0x878678326eac9000u, 0xa968163f0a57b400u, 0xd3c21bcecceda100u, + 0x84595161401484a0u, 0xa56fa5b99019a5c8u, 0xcecb8f27f4200f3au + } + }; + return table; +} + +/// (high, low) pairs +inline const std::array& pow10_major() noexcept +{ + static const std::array table = + { + { + 0xaddcb9e83c6b1793u, 0xdf4abe242a1bbf3eu, 0xaf8e5410288e1b6fu, 0x07ecf0ae5ee44ddau, 0xb1442798f49ffb4au, 0x99cd11cfdf41779du, + 0xb2fe3f0b8599ef07u, 0x861fa7e6dcb4aa15u, 0xb4bca50b065abe63u, 0x0fed077a756b53aau, 0xb67f6455292cbf08u, 0x1a3bc84c17b1d543u, + 0xb84687c269ef3bfbu, 0x3d5d514f40eea742u, 0xba121a4650e4ddebu, 0x92f34d62616ce413u, 0xbbe226efb628afeau, 0x890489f70a55368cu, + 0xbdb6b8e905cb600fu, 0x5400e987bbc1c921u, 0xbf8fdb78849a5f96u, 0xde98520472bdd034u, 0xc16d9a0095928a27u, 0x75b7053c0f178294u, + 0xc350000000000000u, 0x0000000000000000u, 0xc5371912364ce305u, 0x6c28000000000000u, 0xc722f0ef9d80aad6u, 0x424d3ad2b7b97ef6u, + 0xc913936dd571c84cu, 0x03bc3a19cd1e38eau, 0xcb090c8001ab551cu, 0x5cadf5bfd3072cc6u, 0xcd036837130890a1u, 0x36dba887c37a8c10u, + 0xcf02b2c21207ef2eu, 0x94f967e45e03f4bcu, 0xd106f86e69d785c7u, 0xe13336d701beba52u, 0xd31045a8341ca07cu, 0x1ede48111209a051u, + 0xd51ea6fa85785631u, 0x552a74227f3ea566u, 0xd732290fbacaf133u, 0xa97c177947ad4096u, 0xd94ad8b1c7380874u, 0x18375281ae7822bdu, + 0xdb68c2ca82ed2a05u, 0xa67398db9f6820e1u + } + }; + return table; +} + +/// one bit per power: whether the computed value is one unit too large +inline const std::array& pow10_fixups() noexcept +{ + static const std::array table = + { + { + 0x8d8fc810u, 0x06100293u, 0x19000000u, 0x00100000u, 0x00000908u, 0x00000000u, 0x04e00300u, 0x3807e0b2u, 0x3d83d793u, 0x0006f5ccu, + 0x00000000u, 0xffff0000u, 0x8076337du, 0x4ff45ba0u, 0x09405033u, 0x034376d9u, 0x09000000u, 0x4e100501u, 0x076d14dcu, 0xf964f45eu, + 0x0000003du + } + }; + return table; +} + +/// the 128-bit significand of 10^k, rounded down, for k in [-307, 341] +/// (compute_pow10 of Zmij) +inline uint128_parts compute_pow10(int k) noexcept +{ + const auto i = static_cast(k + 307); + const std::uint64_t m = pow10_minor()[(i + 24) % 28]; + const std::size_t j = 2 * static_cast((i + 24) / 28); + const std::uint64_t h_hi = pow10_major()[j]; + const std::uint64_t h_lo = pow10_major()[j + 1]; + const std::uint64_t h1 = full_multiplication(h_lo, m).high; + const std::uint64_t c0 = h_lo * m; + const std::uint64_t c1 = h1 + (h_hi * m); + const std::uint64_t c2 = (c1 < h1 ? 1u : 0u) + full_multiplication(h_hi, m).high; + uint128_parts r{}; + if ((c2 >> 63u) != 0) + { + r.high = c2; + r.low = c1; + } + else + { + r.high = (c2 << 1u) | (c1 >> 63u); + r.low = (c1 << 1u) | (c0 >> 63u); + } + r.low -= (pow10_fixups()[i >> 5u] >> (i & 31u)) & 1u; + return r; +} + +/// The 128-bit significand of 10^k, rounded down, for k in [-342, 341]. +/// Up to 10^308, the table for number parsing holds the same significands +/// (those of 5^k), except for k in [-27, -1], where it holds them one unit +/// larger (as the Eisel-Lemire algorithm needs them). +inline uint128_parts pow10(int k) noexcept +{ + if (k > pow5_128_largest_power) + { + return compute_pow10(k); // (only for the smallest doubles) + } + const auto i = 2 * static_cast(k - pow5_128_smallest_power); + uint128_parts r{pow5_128()[i + 1], pow5_128()[i]}; + const std::uint64_t adjust = static_cast(k + 27) < 27u ? 1u : 0u; + r.high -= r.low < adjust ? 1u : 0u; + r.low -= adjust; + return r; +} + +/// (x_hi * 2^64 + x_lo) * y >> 64, as 128 bits +inline uint128_parts umul192_hi128(std::uint64_t x_hi, std::uint64_t x_lo, std::uint64_t y) noexcept +{ + const uint128_parts p = full_multiplication(x_hi, y); + uint128_parts r{}; + r.low = p.low + full_multiplication(x_lo, y).high; + r.high = p.high + (r.low < p.low ? 1u : 0u); + return r; +} + +/// (x * y + c) >> 64 +inline std::uint64_t umul128_add_hi64(std::uint64_t x, std::uint64_t y, std::uint64_t c) noexcept +{ + const uint128_parts p = full_multiplication(x, y); + return p.high + (p.low + c < p.low ? 1u : 0u); +} + +/// the result of Zmij: the shorter candidate and, if that is outside the +/// rounding interval, the digit after it (16 bytes: returned in registers) +struct shortest_decimal +{ + std::uint64_t integral; ///< the shorter candidate (15 or 16 digits for normal doubles) + int exponent; ///< the decimal exponent of the digit after it + unsigned char digit; ///< the digit after it (if has_digit) + bool has_digit; ///< whether the shortest decimal is integral * 10 + digit +}; + +/// The shortest decimal in the rounding interval of a positive finite double +/// given by its bits, the closest one if there are several (to_decimal of +/// Zmij, which keeps the last digit apart: the 15 or 16 digits before it can be +/// converted without a multiplication by 10 first). Always inlined: GCC +/// otherwise calls it, and its result goes through memory. +JSON_HEDLEY_ALWAYS_INLINE shortest_decimal to_shortest(std::uint64_t bits) noexcept +{ + constexpr int extra_shift = 9; + const auto raw_exp = static_cast((bits >> 52u) & 0x7FFu); + std::uint64_t bin_sig = bits & ((std::uint64_t{1} << 52u) - 1); + // a power of two has a narrower interval below (except the smallest normal) + const bool regular = bin_sig != 0 || raw_exp <= 1; + const int bin_exp = (raw_exp == 0 ? 1 : raw_exp) - 1075; + if (raw_exp != 0) + { + bin_sig |= std::uint64_t{1} << 52u; + } + // floor(log10(2^bin_exp)), or floor(log10(3/4 * 2^bin_exp)) for the irregular case + const int dec_exp = ((bin_exp * 315653) - (regular ? 0 : 131072)) >> 20; + // scaled by 10^(-dec_exp - 1): the integral part is the shorter candidate + const int shift = bin_exp + ((-(dec_exp + 1) * 217707) >> 16) + 1 + extra_shift; + const uint128_parts p10 = pow10(-dec_exp - 1); + const uint128_parts p = umul192_hi128(p10.high, p10.low, bin_sig << static_cast(shift)); + std::uint64_t integral = p.high >> static_cast(extra_shift); + // (no casts in the shifts: MSVC 2015 warns C4554 on them despite the parentheses) + constexpr auto fractional_low_shift = static_cast(extra_shift); + constexpr unsigned fractional_high_shift = 64u - fractional_low_shift; + const std::uint64_t fractional = (p.high << fractional_high_shift) | (p.low >> fractional_low_shift); + std::uint64_t digit = 0; + bool round_up = false; + bool round_down = false; + if (JSON_HEDLEY_LIKELY(regular)) + { + const std::uint64_t half_ulp = (p10.high >> static_cast(extra_shift + 1 - shift)) + (1 - (bin_sig & 1u)); + round_up = fractional + half_ulp < fractional; + round_down = half_ulp > fractional; + // the last digit of the longer candidate, rounded to nearest + digit = umul128_add_hi64(fractional, 10, (std::uint64_t{1} << 63u) + 6); + if (fractional == (std::uint64_t{1} << 62u)) + { + digit = 2; // 2.5 rounds to 2 + } + } + else + { + const std::uint64_t half_ulp = p10.high >> static_cast(extra_shift + 1 - shift); + round_up = half_ulp > ~std::uint64_t{0} - fractional; + round_down = (half_ulp >> 1u) > fractional; + digit = umul128_add_hi64(fractional, 10, (std::uint64_t{1} << 63u) - 1); + const std::uint64_t lowest = umul128_add_hi64(fractional - (half_ulp >> 1u), 10, ~std::uint64_t{0}); + digit = digit < lowest ? lowest : digit; + } + integral += round_up ? 1u : 0u; + // if the shorter candidate is outside the rounding interval: one digit more + return shortest_decimal{integral, dec_exp, static_cast(digit), !round_up && !round_down}; +} + +} // namespace zmij +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + // #include @@ -24680,16 +31786,6 @@ For a detailed description of the algorithm see: namespace dtoa_impl { -template -Target reinterpret_bits(const Source source) -{ - static_assert(sizeof(Target) == sizeof(Source), "size mismatch"); - - Target target; - std::memcpy(&target, &source, sizeof(Source)); - return target; -} - struct diyfp // f * 2^e { static constexpr int kPrecision = 64; // = q @@ -24844,7 +31940,7 @@ boundaries compute_boundaries(FloatType value) using bits_type = typename std::conditional::type; - const auto bits = static_cast(reinterpret_bits(value)); + const auto bits = static_cast(bit_cast(value)); const std::uint64_t E = bits >> (kPrecision - 1); const std::uint64_t F = bits & (kHiddenBit - 1); @@ -25683,6 +32779,437 @@ inline char* format_buffer(char* buf, int len, int decimal_exponent, return append_exponent(buf, n - 1); } +/// eight decimal digits (a value below 10^8) as bytes 0..9, the first digit +/// in the most significant byte: three steps that divide all lanes at once +/// by a multiplication (the conversion of Xiang JunBo, as in Zmij) +inline std::uint64_t eight_digit_bytes(std::uint64_t abcdefgh) noexcept +{ + const std::uint64_t abcd_efgh = abcdefgh + (((std::uint64_t{1} << 32u) - 10000u) * ((abcdefgh * (((std::uint64_t{1} << 40u) / 10000u) + 1u)) >> 40u)); + const std::uint64_t ab_cd_ef_gh = abcd_efgh + (((std::uint64_t{1} << 16u) - 100u) * (((abcd_efgh * (((std::uint64_t{1} << 19u) / 100u) + 1u)) >> 19u) & 0x7F0000007Fu)); + return ab_cd_ef_gh + (((std::uint64_t{1} << 8u) - 10u) * (((ab_cd_ef_gh * (((std::uint64_t{1} << 10u) / 10u) + 1u)) >> 10u) & 0x000F000F000F000Fu)); +} + +/// store the bytes of v, the most significant one first (one byte swap and +/// one store where the byte order is known: compilers do not reliably merge +/// the byte stores once this is inlined) +inline void store_msb_first(char* p, std::uint64_t v) noexcept +{ +#if JSON_LITTLE_ENDIAN_TARGET + v = byteswap(v); + std::memcpy(p, &v, sizeof(v)); +#elif JSON_BIG_ENDIAN_TARGET + std::memcpy(p, &v, sizeof(v)); +#else + for (unsigned i = 0; i < 8; ++i) + { + p[i] = static_cast(v >> (56u - (8u * i))); + } +#endif +} + +/*! +@brief digits * 10^exp for a double, in the layout of format_buffer() + +The layout is that of format_buffer() with min_exp -4 and max_exp 15 (the +digits10 of double). The digits are converted eight at a time and placed +with fixed-size moves instead of per-digit loops and moves of the buffer. + +@param[in] digits the digits (not 0, at most 17 digits; trailing zeros allowed) +@param[in] exp the decimal exponent of the last digit +@return a pointer past the text; up to 41 bytes at @a first are written + (some beyond the returned end) +*/ +JSON_HEDLEY_NON_NULL(1) +JSON_HEDLEY_RETURNS_NON_NULL +inline char* write_decimal(char* first, std::uint64_t digits, int exp) noexcept +{ + JSON_ASSERT(digits != 0 && digits < 100000000000000000u); + const std::uint64_t upper = digits / 100000000u; + const std::uint64_t b0 = upper / 100000000u; // (one digit: it is its own byte) + const std::uint64_t b1 = eight_digit_bytes(upper % 100000000u); + const std::uint64_t b2 = eight_digit_bytes(digits % 100000000u); + // leading and trailing zero digits: zero bytes, counted without division + int leading = 16; + int zeros = 16; + if (b0 != 0) + { + leading = count_leading_zeros(b0) / 8; + } + else if (b1 != 0) + { + leading = 8 + (count_leading_zeros(b1) / 8); + } + else + { + leading += count_leading_zeros(b2) / 8; + } + if (b2 != 0) + { + zeros = count_trailing_zeros(b2) / 8; + } + else if (b1 != 0) + { + zeros = 8 + (count_trailing_zeros(b1) / 8); + } + // (else: 16, b0 is the one digit that is not 0) + // the digits as text at text + leading, then '0's, so that fixed-size + // moves need not check how many digits there are + std::array text; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read + store_msb_first(text.data(), b0 + 0x3030303030303030u); + store_msb_first(text.data() + 8, b1 + 0x3030303030303030u); + store_msb_first(text.data() + 16, b2 + 0x3030303030303030u); + std::memset(text.data() + 24, '0', 40); + const int k = 24 - leading - zeros; // significant digits + const int n = k + exp + zeros; // position of the decimal point after the first digit + const char* const s0 = text.data() + leading; + + if (-4 < n && n <= 15) + { + // "0.[000]digits" (n <= 0) is the digits after 1 - n leading '0's + // with the point after the first; "digits[000].0" (n >= k) and + // "dig.its" put the point after n characters + const int pad = n <= 0 ? 1 - n : 0; + const char* const s = s0 - pad; + const int len = k + pad; + const int point = n + pad; + std::memcpy(first, s, 16); + std::memcpy(first + point + 1, s + point, 24); + first[point] = '.'; + return first + (point >= len ? point + 2 : len + 1); + } + + // d.igitse+XX, with at least two exponent digits (as append_exponent()) + std::memcpy(first, s0, 16); + std::memcpy(first + 2, s0 + 1, 16); + first[1] = '.'; + char* const end = first + (k == 1 ? 1 : k + 1); + const int e = n - 1; + const auto ea = e < 0 ? 0u - static_cast(e) : static_cast(e); // (unsigned: no signed overflow to assume) + const bool three = ea >= 100; + end[0] = 'e'; + end[1] = e < 0 ? '-' : '+'; + end[2] = static_cast('0' + (three ? ea / 100 : (ea / 10) % 10)); + end[3] = static_cast('0' + (three ? (ea / 10) % 10 : ea % 10)); + end[4] = static_cast('0' + (ea % 10)); + return end + (three ? 5 : 4); +} + +/*! +@brief the shortest decimal of a positive double (Zmij), as write_decimal() +writes it + +For a normal double, the shorter candidate has 15 or 16 digits: they are +converted at once (two halves of eight digits) and followed by the digit +after them, if there is one, without the multiplication and division by 10 +that counting the digits of one number would take. The fixed layouts move +the digits after the point by one byte. + +@return a pointer past the text; up to 41 bytes at @a first are written + (some beyond the returned end) +*/ +JSON_HEDLEY_NON_NULL(1) +JSON_HEDLEY_RETURNS_NON_NULL +inline char* write_shortest(char* first, const zmij::shortest_decimal d) noexcept +{ + const std::uint64_t sig = d.integral; + if (JSON_HEDLEY_UNLIKELY(sig < 100000000000000u || sig >= 10000000000000000u)) + { + // (subnormals) + return d.has_digit ? write_decimal(first, (sig * 10) + d.digit, d.exponent) : write_decimal(first, sig, d.exponent + 1); + } + const bool sixteen = sig >= 1000000000000000u; // (else 15 digits) + const int last = d.has_digit ? d.digit : 0; + const std::uint64_t upper = sig / 100000000u; +#if JSON_DTOA_SSE2 + // NOLINTBEGIN(portability-simd-intrinsics) + // the two halves in the 64-bit lanes, each as abcd * 2^32 + efgh, then as + // bytes (as eight_digit_bytes(), one lane each) + const __m128i x = _mm_set_epi64x(static_cast(sig - (upper * 100000000u)), static_cast(upper)); // NOLINT(runtime/int) + const __m128i abcd = _mm_srli_epi64(_mm_mul_epu32(x, _mm_set1_epi64x(109951163)), 40); // 2^40 / 10000 + 1 + const __m128i abcd_efgh = _mm_add_epi64(x, _mm_mul_epu32(abcd, _mm_set1_epi64x(4294957296))); // 2^32 - 10000 + // 32-bit lanes in the order of the text: abcd, efgh of both halves + const __m128i fours = _mm_shuffle_epi32(abcd_efgh, _MM_SHUFFLE(2, 3, 0, 1)); + const __m128i ab = _mm_srli_epi16(_mm_mulhi_epu16(fours, _mm_set1_epi32(5243)), 3); + const __m128i ab_cd = _mm_or_si128(_mm_slli_epi32(_mm_sub_epi16(fours, _mm_mullo_epi16(ab, _mm_set1_epi32(100))), 16), ab); + // 16-bit lanes ab (< 100) -> bytes a, b: 256 * ab - 2559 * (ab / 10) + const __m128i bytes = _mm_sub_epi16(_mm_slli_epi16(ab_cd, 8), _mm_mullo_epi16(_mm_set1_epi16(2559), _mm_mulhi_epu16(ab_cd, _mm_set1_epi16(6554)))); + // the last digit that is not 0 (sig is not 0) + const auto nonzero = static_cast(_mm_movemask_epi8(_mm_cmpgt_epi8(bytes, _mm_setzero_si128()))); + const int digits = 63 - count_leading_zeros(nonzero) + (sixteen ? 1 : 0); // without trailing zeros + const __m128i chars = _mm_add_epi8(bytes, _mm_set1_epi8('0')); + // the 16 characters from the first digit + const __m128i s = sixteen ? chars : _mm_or_si128(_mm_srli_si128(chars, 1), _mm_slli_si128(_mm_cvtsi32_si128('0' + last), 15)); + const char s16 = static_cast(sixteen ? '0' + last : '0'); // the 17th + const auto store_16 = [&s](char* p) noexcept + { + std::memcpy(p, &s, 16); + }; + const char first_digit = static_cast(_mm_cvtsi128_si32(s)); + // NOLINTEND(portability-simd-intrinsics) +#elif JSON_DTOA_NEON + // as with SSE2: the halves in 32-bit lanes, then abcd, efgh of both + const uint32x2_t halves = vcreate_u32(upper | ((sig - (upper * 100000000u)) << 32u)); + const uint32x2_t abcd = vmovn_u64(vshrq_n_u64(vmull_n_u32(halves, static_cast(((std::uint64_t{1} << 40u) / 10000u) + 1u)), 40)); + const uint32x2_t efgh = vmls_n_u32(halves, abcd, 10000u); + const uint32x4_t fours = vcombine_u32(vzip1_u32(abcd, efgh), vzip2_u32(abcd, efgh)); + const uint32x4_t ab = vshrq_n_u32(vmulq_n_u32(fours, 5243u), 19); + const uint16x8_t ab_cd = vreinterpretq_u16_u32(vorrq_u32(ab, vshlq_n_u32(vmlsq_n_u32(fours, ab, 100u), 16))); + const uint16x8_t tens = vshrq_n_u16(vmulq_n_u16(ab_cd, 103u), 10); + const uint8x16_t bytes = vreinterpretq_u8_u16(vorrq_u16(tens, vshlq_n_u16(vmlsq_n_u16(ab_cd, tens, 10u), 8))); + // the last digit that is not 0 (sig is not 0): a nibble per byte + const std::uint64_t nonzero = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(vtstq_u8(bytes, bytes)), 4)), 0); + const int digits = ((63 - count_leading_zeros(nonzero)) / 4) + (sixteen ? 1 : 0); // without trailing zeros + const uint8x16_t chars = vaddq_u8(bytes, vdupq_n_u8('0')); + // the 16 characters from the first digit + const uint8x16_t s = sixteen ? chars : vextq_u8(chars, vdupq_n_u8(static_cast('0' + last)), 1); + const char s16 = static_cast(sixteen ? '0' + last : '0'); // the 17th + const auto store_16 = [&s](char* p) noexcept + { + vst1q_u8(reinterpret_cast(p), s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + }; + const auto first_digit = static_cast(vgetq_lane_u8(s, 0)); +#else + const std::uint64_t hi = eight_digit_bytes(upper); + const std::uint64_t lo = eight_digit_bytes(sig - (upper * 100000000u)); + // trailing zero digits: zero bytes (sig is not 0) + const int zeros = lo != 0 ? count_trailing_zeros(lo) / 8 : 8 + (count_trailing_zeros(hi) / 8); + const int digits = 15 - zeros + (sixteen ? 1 : 0); // without trailing zeros + // the 16 characters from the first digit + const std::uint64_t s_hi = (sixteen ? hi : (hi << 8u) | (lo >> 56u)) + 0x3030303030303030u; + const std::uint64_t s_lo = (sixteen ? lo : (lo << 8u) | static_cast(last)) + 0x3030303030303030u; + const char s16 = static_cast(sixteen ? '0' + last : '0'); // the 17th + const auto store_16 = [s_hi, s_lo](char* p) noexcept + { + store_msb_first(p, s_hi); + store_msb_first(p + 8, s_lo); + }; + const auto first_digit = static_cast(s_hi >> 56u); +#endif + const int len = d.has_digit ? 16 + (sixteen ? 1 : 0) : digits; // significant digits + const int n = 16 + (sixteen ? 1 : 0) + d.exponent; // digits before the point + + if (JSON_HEDLEY_LIKELY(n >= 1 && n <= 15)) + { + // "dig.its" and "digits[000].0": the digits after the point move by + // one byte ('0's follow the digits) +#if JSON_DTOA_SSE2 + // NOLINTBEGIN(portability-simd-intrinsics) + // (in the register: reading the digits back from memory right after + // storing them waits until the stores are done) + const __m128i at = _mm_set1_epi8(static_cast(n)); + const __m128i index = _mm_setr_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15); + const __m128i before = _mm_cmpgt_epi8(at, index); + const __m128i after = _mm_cmpgt_epi8(index, at); + const __m128i text = _mm_or_si128(_mm_or_si128(_mm_and_si128(s, before), _mm_and_si128(_mm_slli_si128(s, 1), after)), + _mm_andnot_si128(_mm_or_si128(before, after), _mm_set1_epi8('.'))); + std::memcpy(first, &text, 16); + first[16] = static_cast(_mm_extract_epi16(s, 7) >> 8); + first[17] = s16; + // NOLINTEND(portability-simd-intrinsics) +#elif JSON_DTOA_NEON + const uint8x16_t index = vcombine_u8(vcreate_u8(0x0706050403020100u), vcreate_u8(0x0F0E0D0C0B0A0908u)); + const uint8x16_t at = vdupq_n_u8(static_cast(n)); + const uint8x16_t after_point = vbslq_u8(vcgtq_u8(index, at), vextq_u8(vdupq_n_u8(0), s, 15), vdupq_n_u8('.')); + vst1q_u8(reinterpret_cast(first), vbslq_u8(vcltq_u8(index, at), s, after_point)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + first[16] = static_cast(vgetq_lane_u8(s, 15)); + first[17] = s16; +#else + store_16(first); + first[16] = s16; + std::uint64_t after_point[2]; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays,cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read + std::memcpy(after_point, first + n, 16); + std::memcpy(first + n + 1, after_point, 16); + first[n] = '.'; +#endif + return first + (n >= len ? n + 2 : len + 1); + } + if (n <= 0 && n > -4) + { + // "0.[000]digits" + std::memset(first, '0', 8); + first[1] = '.'; + store_16(first + 2 - n); + first[18 - n] = s16; + return first + 2 - n + len; + } + // d.igitse+XX, with at least two exponent digits (as append_exponent()) + store_16(first + 1); + first[17] = s16; + first[0] = first_digit; + first[1] = '.'; + char* const end = first + (len == 1 ? 1 : len + 1); + const int e = n - 1; + const auto ea = e < 0 ? 0u - static_cast(e) : static_cast(e); // (unsigned: no signed overflow to assume) + const bool three = ea >= 100; + end[0] = 'e'; + end[1] = e < 0 ? '-' : '+'; + end[2] = static_cast('0' + (three ? ea / 100 : (ea / 10) % 10)); + end[3] = static_cast('0' + (three ? (ea / 10) % 10 : ea % 10)); + end[4] = static_cast('0' + (ea % 10)); + return end + (three ? 5 : 4); +} + +/// the powers of ten up to 10^19 (all that fit in std::uint64_t) +inline const std::array& powers_of_ten() noexcept +{ + static const std::array powers = + { + { + 1u, + 10u, + 100u, + 1000u, + 10000u, + 100000u, + 1000000u, + 10000000u, + 100000000u, + 1000000000u, + 10000000000u, + 100000000000u, + 1000000000000u, + 10000000000000u, + 100000000000000u, + 1000000000000000u, + 10000000000000000u, + 100000000000000000u, + 1000000000000000000u, + 10000000000000000000u + } + }; + return powers; +} + +/*! +@brief digits * 10^exp, as write_decimal() writes it, for the digits of a +double that need no conversion (count digits, at most 15, the first not 0; +trailing zeros allowed): extended to 16 digits and written by write_shortest() + +@return a pointer past the text; up to 41 bytes at @a first are written + (some beyond the returned end) +*/ +JSON_HEDLEY_NON_NULL(1) +JSON_HEDLEY_RETURNS_NON_NULL +inline char* write_short_decimal(char* first, std::uint64_t digits, int count, int exp) noexcept +{ + JSON_ASSERT(digits >= powers_of_ten()[static_cast(count - 1)] && count <= 15); + const int scale = 16 - count; + return write_shortest(first, zmij::shortest_decimal{digits * powers_of_ten()[static_cast(scale)], exp - scale - 1, 0, false}); +} + +/// as write_short_decimal(), counting the digits (not 0, less than 10^15) +JSON_HEDLEY_NON_NULL(1) +JSON_HEDLEY_RETURNS_NON_NULL +inline char* write_short_decimal(char* first, std::uint64_t digits, int exp) noexcept +{ + JSON_ASSERT(digits != 0 && digits < 1000000000000000u); + // floor(log10(2^bits)) + 1 digits, or one less + const int log2_bound = ((64 - count_leading_zeros(digits)) * 1233) >> 12; + const int count = log2_bound + (digits >= powers_of_ten()[static_cast(log2_bound)] ? 1 : 0); + return write_short_decimal(first, digits, count, exp); +} + +/*! +@brief whether FloatType is an IEEE 754 binary64 type (a double, or a long double +that has the same format, as with MSVC and on Apple's Arm CPUs) + +These are the types the conversion of Zmij (see zmij.hpp) is used for; all +others (binary32, or a format the library does not know) use Grisu2. +*/ +template +constexpr bool has_binary64_format() noexcept +{ + return std::numeric_limits::is_iec559 + && std::numeric_limits::digits == 53 + && std::numeric_limits::max_exponent == 1024 + && sizeof(FloatType) == sizeof(std::uint64_t); +} + +template +struct is_binary64 : std::integral_constant()> {}; + +/// a positive finite float (other than binary64): Grisu2 and format_buffer() +template +JSON_HEDLEY_NON_NULL(1, 2) +JSON_HEDLEY_RETURNS_NON_NULL +char* write_positive_grisu2(char* first, const char* last, FloatType value) +{ + JSON_ASSERT(last - first >= std::numeric_limits::max_digits10); + static_cast(last); // (only used in the assertion) + + // Compute v = buffer * 10^decimal_exponent. + // The decimal digits are stored in the buffer, which needs to be interpreted + // as an unsigned decimal integer. + // len is the length of the buffer, i.e., the number of decimal digits. + int len = 0; + int decimal_exponent = 0; + grisu2(first, len, decimal_exponent, value); + + JSON_ASSERT(len <= std::numeric_limits::max_digits10); + + // Format the buffer like printf("%.*g", prec, value) + constexpr int kMinExp = -4; + // Use digits10 here to increase compatibility with version 2. + constexpr int kMaxExp = std::numeric_limits::digits10; + + JSON_ASSERT(last - first >= kMaxExp + 2); + JSON_ASSERT(last - first >= 2 + (-kMinExp - 1) + std::numeric_limits::max_digits10); + JSON_ASSERT(last - first >= std::numeric_limits::max_digits10 + 6); + + return format_buffer(first, len, decimal_exponent, kMinExp, kMaxExp); +} + +/// a positive finite binary64 number: the shortest digits (Zmij), laid out by +/// write_shortest() (through a local buffer if [first, last) is shorter than +/// the 41 bytes it may write) +template +JSON_HEDLEY_NON_NULL(1, 2) +JSON_HEDLEY_RETURNS_NON_NULL +char* write_positive_zmij(char* first, const char* last, FloatType value) +{ + static_assert(is_binary64::value, + "internal error: the conversion of Zmij needs IEEE 754 binary64 numbers"); + const auto bits = bit_cast(value); + const zmij::shortest_decimal d = zmij::to_shortest(bits); + if (JSON_HEDLEY_LIKELY(last - first >= 41)) + { + return write_shortest(first, d); + } + std::array buf; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read + const auto len = static_cast(write_shortest(buf.data(), d) - buf.data()); + JSON_ASSERT(last - first >= static_cast(len)); + std::memcpy(first, buf.data(), len); + return first + len; +} + +/// a positive finite binary64 number: Zmij (as a long double has the format of +/// a double here, its bits are those of the double of the same value) +template +JSON_HEDLEY_NON_NULL(1, 2) +JSON_HEDLEY_RETURNS_NON_NULL +char* write_positive(char* first, const char* last, FloatType value, std::true_type /*is_binary64*/) +{ + return write_positive_zmij(first, last, value); +} + +/// a positive finite float of any other format: Grisu2 +template +JSON_HEDLEY_NON_NULL(1, 2) +JSON_HEDLEY_RETURNS_NON_NULL +char* write_positive(char* first, const char* last, FloatType value, std::false_type /*is_binary64*/) +{ + return write_positive_grisu2(first, last, value); +} + +/// a positive finite float: Zmij for binary64 numbers, Grisu2 otherwise +template +JSON_HEDLEY_NON_NULL(1, 2) +JSON_HEDLEY_RETURNS_NON_NULL +char* write_positive(char* first, const char* last, FloatType value) +{ + return write_positive(first, last, value, is_binary64 {}); +} + } // namespace dtoa_impl /*! @@ -25700,7 +33227,6 @@ JSON_HEDLEY_NON_NULL(1, 2) JSON_HEDLEY_RETURNS_NON_NULL char* to_chars(char* first, const char* last, FloatType value) { - static_cast(last); // maybe unused - fix warning JSON_ASSERT(std::isfinite(value)); // Use signbit(value) instead of (value < 0) since signbit works for -0. @@ -25726,28 +33252,7 @@ char* to_chars(char* first, const char* last, FloatType value) JSON_HEDLEY_DIAGNOSTIC_POP #endif - JSON_ASSERT(last - first >= std::numeric_limits::max_digits10); - - // Compute v = buffer * 10^decimal_exponent. - // The decimal digits are stored in the buffer, which needs to be interpreted - // as an unsigned decimal integer. - // len is the length of the buffer, i.e., the number of decimal digits. - int len = 0; - int decimal_exponent = 0; - dtoa_impl::grisu2(first, len, decimal_exponent, value); - - JSON_ASSERT(len <= std::numeric_limits::max_digits10); - - // Format the buffer like printf("%.*g", prec, value) - constexpr int kMinExp = -4; - // Use digits10 here to increase compatibility with version 2. - constexpr int kMaxExp = std::numeric_limits::digits10; - - JSON_ASSERT(last - first >= kMaxExp + 2); - JSON_ASSERT(last - first >= 2 + (-kMinExp - 1) + std::numeric_limits::max_digits10); - JSON_ASSERT(last - first >= std::numeric_limits::max_digits10 + 6); - - return dtoa_impl::format_buffer(first, len, decimal_exponent, kMinExp, kMaxExp); + return dtoa_impl::write_positive(first, last, value); } } // namespace detail @@ -25789,6 +33294,12 @@ class serializer using number_float_t = typename BasicJsonType::number_float_t; using number_integer_t = typename BasicJsonType::number_integer_t; using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + /// type of the absolute value of an integer: number_unsigned_t, or its + /// unsigned counterpart if it is signed (it may be number_integer_t, #2573) + using number_magnitude_t = typename std::conditional < + std::is_integral::value && std::is_signed::value, + std::make_unsigned, + std::enable_if>::type::type; using binary_char_t = typename BasicJsonType::binary_t::value_type; public: @@ -25805,7 +33316,7 @@ class serializer None of @a pretty_print_, @a ensure_ascii_ and @a indent_step_ change over the life of the serializer, so they are captured once here instead of - being threaded through every call to @ref dump, @ref dump_internal and + being threaded through every call to @ref dump, @ref dump_container and @ref dump_iteratively. */ serializer(output_adapter_protocol& s, const char ichar, @@ -25814,11 +33325,14 @@ class serializer const std::size_t indent_step_ = 0, error_handler_t error_handler_ = error_handler_t::strict) : o(&s) + , number_buffer() // NOLINT(readability-redundant-member-init) + , string_buffer() // NOLINT(readability-redundant-member-init) , indent_char(ichar) , pretty_print(pretty_print_) , ensure_ascii(ensure_ascii_) , indent_step(indent_step_) , error_handler(error_handler_) + , write_buffer() // NOLINT(readability-redundant-member-init) {} // deleted because of pointer members @@ -25849,18 +33363,18 @@ class serializer void dump(const BasicJsonType& val, const std::size_t current_indent = 0) { - dump_internal(val, current_indent); + dump_element(val, current_indent, 0); flush(); } JSON_PRIVATE_UNLESS_TESTED: /*! - @brief worker for @ref dump + @brief serialize the object or array @a val - Identical in behavior to the historical @ref dump, but writes into the - serializer's internal @ref write_buffer instead of issuing a virtual call - per token. The public @ref dump wraps this and flushes the buffer once the - top-level value has been serialized. + Writes into the serializer's internal @ref write_buffer instead of issuing + a virtual call per token; the public @ref dump flushes the buffer once the + top-level value has been serialized. Scalars are written by @ref dump_scalar, + which @ref dump_element calls without entering this function. Serializing a container descends into its elements, so a value nested deeply enough used to exhaust the call stack and terminate the process with no @@ -25871,167 +33385,149 @@ class serializer @sa https://github.com/nlohmann/json/issues/5387 */ - void dump_internal(const BasicJsonType& val, - const std::size_t current_indent = 0, - const std::size_t depth = 0) + void dump_container(const BasicJsonType& val, + const std::size_t current_indent, + const std::size_t depth) { - switch (val.m_data.m_type) + JSON_ASSERT(val.m_data.m_type == value_t::object || val.m_data.m_type == value_t::array); + + if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit())) { - case value_t::object: + dump_iteratively(val, current_indent); + return; + } + + if (val.m_data.m_type == value_t::object) + { + if (val.m_data.m_value.object->empty()) { - if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit())) - { - dump_iteratively(val, current_indent); - return; - } - - if (val.m_data.m_value.object->empty()) - { - put_literal("{}"); - return; - } - - if (pretty_print) - { - put_literal("{\n"); - - // variable to hold indentation for recursive calls - const auto new_indent = next_indent(current_indent, indent_step); - - // first n-1 elements - auto i = val.m_data.m_value.object->cbegin(); - for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i) - { - put_indent(new_indent); - put_char('"'); - dump_escaped(i->first); - put_literal("\": "); - dump_internal(i->second, new_indent, depth + 1); - put_literal(",\n"); - } - - // last element - JSON_ASSERT(i != val.m_data.m_value.object->cend()); - JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend()); - put_indent(new_indent); - put_char('"'); - dump_escaped(i->first); - put_literal("\": "); - dump_internal(i->second, new_indent, depth + 1); - - put_char('\n'); - put_indent(current_indent); - put_char('}'); - } - else - { - put_char('{'); - - // first n-1 elements - auto i = val.m_data.m_value.object->cbegin(); - for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i) - { - put_char('"'); - dump_escaped(i->first); - put_literal("\":"); - dump_internal(i->second, current_indent, depth + 1); - put_char(','); - } - - // last element - JSON_ASSERT(i != val.m_data.m_value.object->cend()); - JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend()); - put_char('"'); - dump_escaped(i->first); - put_literal("\":"); - dump_internal(i->second, current_indent, depth + 1); - - put_char('}'); - } - + put_literal("{}"); return; } - case value_t::array: + if (pretty_print) { - if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit())) + // variable to hold indentation for recursive calls + const auto new_indent = next_indent(current_indent, indent_step); + + put_literal_indent("{\n", new_indent); + + // first n-1 elements + auto i = val.m_data.m_value.object->cbegin(); + for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i) { - dump_iteratively(val, current_indent); - return; + put_object_key(i->first, "\": "); + dump_element(i->second, new_indent, depth + 1); + put_literal_indent(",\n", new_indent); } - if (val.m_data.m_value.array->empty()) + // last element + JSON_ASSERT(i != val.m_data.m_value.object->cend()); + JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend()); + put_object_key(i->first, "\": "); + dump_element(i->second, new_indent, depth + 1); + + put_literal_indent("\n", current_indent); + put_char('}'); + } + else + { + put_char('{'); + + // first n-1 elements + auto i = val.m_data.m_value.object->cbegin(); + for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i) { - put_literal("[]"); - return; + put_object_key(i->first, "\":"); + dump_element(i->second, 0, depth + 1); + put_char(','); } - if (pretty_print) - { - put_literal("[\n"); + // last element + JSON_ASSERT(i != val.m_data.m_value.object->cend()); + JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend()); + put_object_key(i->first, "\":"); + dump_element(i->second, 0, depth + 1); - // variable to hold indentation for recursive calls - const auto new_indent = next_indent(current_indent, indent_step); + put_char('}'); + } + return; + } - // first n-1 elements - for (auto i = val.m_data.m_value.array->cbegin(); - i != val.m_data.m_value.array->cend() - 1; ++i) - { - put_indent(new_indent); - dump_internal(*i, new_indent, depth + 1); - put_literal(",\n"); - } + if (val.m_data.m_value.array->empty()) + { + put_literal("[]"); + return; + } - // last element - JSON_ASSERT(!val.m_data.m_value.array->empty()); - put_indent(new_indent); - dump_internal(val.m_data.m_value.array->back(), new_indent, depth + 1); + if (pretty_print) + { + // variable to hold indentation for recursive calls + const auto new_indent = next_indent(current_indent, indent_step); - put_char('\n'); - put_indent(current_indent); - put_char(']'); - } - else - { - put_char('['); + put_literal_indent("[\n", new_indent); - // first n-1 elements - for (auto i = val.m_data.m_value.array->cbegin(); - i != val.m_data.m_value.array->cend() - 1; ++i) - { - dump_internal(*i, current_indent, depth + 1); - put_char(','); - } - - // last element - JSON_ASSERT(!val.m_data.m_value.array->empty()); - dump_internal(val.m_data.m_value.array->back(), current_indent, depth + 1); - - put_char(']'); - } - - return; + // first n-1 elements + for (auto i = val.m_data.m_value.array->cbegin(); + i != val.m_data.m_value.array->cend() - 1; ++i) + { + dump_element(*i, new_indent, depth + 1); + put_literal_indent(",\n", new_indent); } - case value_t::string: - case value_t::binary: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::discarded: - case value_t::null: - default: - dump_scalar(val, current_indent); - return; + // last element + JSON_ASSERT(!val.m_data.m_value.array->empty()); + dump_element(val.m_data.m_value.array->back(), new_indent, depth + 1); + + put_literal_indent("\n", current_indent); + put_char(']'); + } + else + { + put_char('['); + + // first n-1 elements + for (auto i = val.m_data.m_value.array->cbegin(); + i != val.m_data.m_value.array->cend() - 1; ++i) + { + dump_element(*i, 0, depth + 1); + put_char(','); + } + + // last element + JSON_ASSERT(!val.m_data.m_value.array->empty()); + dump_element(val.m_data.m_value.array->back(), 0, depth + 1); + + put_char(']'); } } private: + /*! + @brief serialize an element of a container + + Most elements are scalars, which need none of the state dump_container() keeps + for a container; writing them from here spares each the entry and exit of + that function. + */ + JSON_HEDLEY_ALWAYS_INLINE void dump_element(const BasicJsonType& val, + const std::size_t current_indent, + const std::size_t depth) + { + if (val.m_data.m_type == value_t::object || val.m_data.m_type == value_t::array) + { + dump_container(val, current_indent, depth); + } + else + { + dump_scalar(val, current_indent); + } + } + /*! @brief write out @a val and everything below it without the call stack - Emits the same bytes as @ref dump_internal, keeping the containers it has + Emits the same bytes as @ref dump_container, keeping the containers it has entered on an explicit stack instead of descending into them. Only reached for values nested deeper than @ref recursion_depth_limit, which is why it is not written for speed: walking every value this way measured up to 20% slower on @@ -26059,8 +33555,7 @@ class serializer { if (pretty_print) { - put_char('\n'); - put_indent(frame.current_indent); + put_literal_indent("\n", frame.current_indent); } put_char('}'); @@ -26087,16 +33582,13 @@ class serializer put_indent(frame.child_indent); } - put_char('"'); - dump_escaped(frame.object_it->first); - if (pretty_print) { - put_literal("\": "); + put_object_key(frame.object_it->first, "\": "); } else { - put_literal("\":"); + put_object_key(frame.object_it->first, "\":"); } const BasicJsonType& element = frame.object_it->second; @@ -26115,8 +33607,7 @@ class serializer { if (pretty_print) { - put_char('\n'); - put_indent(frame.current_indent); + put_literal_indent("\n", frame.current_indent); } put_char(']'); @@ -26257,7 +33748,7 @@ class serializer /*! @brief serialize the value @a val, which is neither an object nor an array - Shared by @ref dump_internal and @ref dump_value, so that a value is written + Shared by @ref dump_element and @ref dump_value, so that a value is written the same way however deeply it is nested. A binary value is written out here in full: it looks like an object, but has no elements to descend into. @@ -26271,78 +33762,13 @@ class serializer { case value_t::string: { - put_char('"'); - dump_escaped(*val.m_data.m_value.string); - put_char('"'); + dump_string_value(*val.m_data.m_value.string); return; } case value_t::binary: { - if (pretty_print) - { - put_literal("{\n"); - - // variable to hold indentation for the bytes - const auto new_indent = next_indent(current_indent, indent_step); - - put_indent(new_indent); - - put_literal("\"bytes\": ["); - - if (!val.m_data.m_value.binary->empty()) - { - for (auto i = val.m_data.m_value.binary->cbegin(); - i != val.m_data.m_value.binary->cend() - 1; ++i) - { - dump_byte(*i); - put_literal(", "); - } - dump_byte(val.m_data.m_value.binary->back()); - } - - put_literal("],\n"); - put_indent(new_indent); - - put_literal("\"subtype\": "); - if (val.m_data.m_value.binary->has_subtype()) - { - dump_integer(val.m_data.m_value.binary->subtype()); - } - else - { - put_literal("null"); - } - put_char('\n'); - put_indent(current_indent); - put_char('}'); - } - else - { - put_literal("{\"bytes\":["); - - if (!val.m_data.m_value.binary->empty()) - { - for (auto i = val.m_data.m_value.binary->cbegin(); - i != val.m_data.m_value.binary->cend() - 1; ++i) - { - dump_byte(*i); - put_char(','); - } - dump_byte(val.m_data.m_value.binary->back()); - } - - put_literal("],\"subtype\":"); - if (val.m_data.m_value.binary->has_subtype()) - { - dump_integer(val.m_data.m_value.binary->subtype()); - put_char('}'); - } - else - { - put_literal("null}"); - } - } + dump_binary_value(current_indent, *val.m_data.m_value.binary); return; } @@ -26361,19 +33787,19 @@ class serializer case value_t::number_integer: { - dump_integer(val.m_data.m_value.number_integer); + dump_signed_value(val.m_data.m_value.number_integer); return; } case value_t::number_unsigned: { - dump_integer(val.m_data.m_value.number_unsigned); + dump_unsigned_value(val.m_data.m_value.number_unsigned); return; } case value_t::number_float: { - dump_float(val.m_data.m_value.number_float); + dump_float_value(val.m_data.m_value.number_float); return; } @@ -26396,6 +33822,100 @@ class serializer } } + /// @brief write the string @a s in quotes; kept out of line, so that the + /// other scalars do not pay for its register needs (and the other way round) + JSON_HEDLEY_NEVER_INLINE void dump_string_value(const string_t& s) + { +#if JSON_LITTLE_ENDIAN_TARGET + if (s.size() <= short_string_max && put_short_quoted(s, "\"")) + { + return; + } +#endif + put_char('"'); + dump_escaped(s); + put_char('"'); + } + + // The numbers, and the rest of what dump_scalar() writes, are left to + // functions of their own as well: a single function holding all of them + // saves and restores every register any of them needs, on every call. Now + // dump_scalar() only dispatches, and each writer pays for what it uses. + JSON_HEDLEY_NEVER_INLINE void dump_signed_value(const number_integer_t x) + { + dump_integer(x); + } + + JSON_HEDLEY_NEVER_INLINE void dump_unsigned_value(const number_unsigned_t x) + { + dump_integer(x); + } + + JSON_HEDLEY_NEVER_INLINE void dump_float_value(const number_float_t x) + { + dump_float(x); + } + + JSON_HEDLEY_NEVER_INLINE void dump_binary_value(const std::size_t current_indent, const typename BasicJsonType::binary_t& binary) + { + if (pretty_print) + { + // variable to hold indentation for the bytes + const auto new_indent = next_indent(current_indent, indent_step); + + put_literal_indent("{\n", new_indent); + put_literal("\"bytes\": ["); + + if (!binary.empty()) + { + for (auto i = binary.cbegin(); i != binary.cend() - 1; ++i) + { + dump_byte(*i); + put_literal(", "); + } + dump_byte(binary.back()); + } + + put_literal_indent("],\n", new_indent); + + put_literal("\"subtype\": "); + if (binary.has_subtype()) + { + dump_integer(binary.subtype()); + } + else + { + put_literal("null"); + } + put_literal_indent("\n", current_indent); + put_char('}'); + } + else + { + put_literal("{\"bytes\":["); + + if (!binary.empty()) + { + for (auto i = binary.cbegin(); i != binary.cend() - 1; ++i) + { + dump_byte(*i); + put_char(','); + } + dump_byte(binary.back()); + } + + put_literal("],\"subtype\":"); + if (binary.has_subtype()) + { + dump_integer(binary.subtype()); + put_char('}'); + } + else + { + put_literal("null}"); + } + } + } /*! @brief the indentation level to use for the children of the current value @@ -26412,6 +33932,220 @@ class serializer return new_indent; } + private: + /*! + @brief append the object key @a s in quotes, followed by @a tail + + @a tail is the closing quote and the colon, followed by a space when + pretty-printing. The callers pass it because each of them already sits on + the pretty-printing or the compact path; choosing it here would test + @ref pretty_print once more for every key. Most keys are short and plain + and are written in one go by put_short_quoted(); everything else takes the + general path. + */ + template + void put_object_key(const string_t& s, const char (&tail)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + { +#if JSON_LITTLE_ENDIAN_TARGET + if (put_short_quoted(s, tail)) + { + return; + } +#endif + put_char('"'); + dump_escaped(s); + put_literal(tail); + } + + // put_short_quoted() assembles words from little-endian loads +#if JSON_LITTLE_ENDIAN_TARGET + /// the longest string put_short_quoted() takes + static constexpr std::size_t short_string_max = JSON_SCAN_VECTOR ? 32 : 16; + + /// the high bit of every byte of @a v that is not printable ASCII or is a + /// quote, a backslash, or DEL (zero if the bytes can be copied as they are) + static std::uint64_t swar_not_plain(const std::uint64_t v) noexcept + { + constexpr std::uint64_t ones = 0x0101010101010101ull; + const std::uint64_t q = v ^ 0x2222222222222222ull; // '"' + const std::uint64_t b = v ^ 0x5C5C5C5C5C5C5C5Cull; // '\\' + return (((q - ones) & ~q) | ((b - ones) & ~b) // == '"', == '\\' + | ((v - 0x2020202020202020ull) & ~v) // < 0x20 + | v | (v + ones)) // >= 0x7F + & 0x8080808080808080ull; + } + + template + static WordType load_word(const char* p) noexcept + { + WordType w; + std::memcpy(&w, p, sizeof(w)); + return w; + } + + template + static void store_word(char* p, const WordType w) noexcept + { + std::memcpy(p, &w, sizeof(w)); + } + + /*! + @brief write `"`, @a s and @a tail if @a s is short and needs no escaping + + A string of up to 16 (32 with vector instructions) printable ASCII + characters (no quote, backslash, or DEL) is read with two overlapping loads + and written with two overlapping stores, instead of scanning it and calling + memcpy with a length that is only known at run time. Returns false, having + changed nothing, if @a s does not qualify or the write buffer lacks the + room. + */ + template + bool put_short_quoted(const string_t& s, const char (&tail)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + { + const std::size_t n = s.size(); + if (n > short_string_max || write_buffer_pos + n + N > write_buffer.size()) + { + return false; + } + + char* d = write_buffer.data() + write_buffer_pos + 1; + const char* const p = s.data(); + + if (n >= 8) + { +#if JSON_SCAN_VECTOR + if (!(n >= 16 ? copy_plain_vectors(d, p, n) : copy_plain_halves(d, p, n))) + { + return false; + } +#else + const auto a = load_word(p); + const auto b = load_word(p + n - 8); + if ((swar_not_plain(a) | swar_not_plain(b)) != 0) + { + return false; + } + store_word(d, a); + store_word(d + n - 8, b); +#endif + } + else if (n >= 4) + { + const auto a = load_word(p); + const auto b = load_word(p + n - 4); + // the bytes above the string are filled with a plain character + const std::uint64_t w = a | (static_cast(b) << (8 * (n - 4))) + | (0x4141414141414141ull << (8 * n)); + if (swar_not_plain(w) != 0) + { + return false; + } + store_word(d, a); + store_word(d + n - 4, b); + } + else if (n >= 2) + { + const auto a = load_word(p); + const auto b = load_word(p + n - 2); + const std::uint64_t w = a | (static_cast(b) << (8 * (n - 2))) + | (0x4141414141414141ull << (8 * n)); + if (swar_not_plain(w) != 0) + { + return false; + } + store_word(d, a); + store_word(d + n - 2, b); + } + else if (n == 1) + { + const std::uint64_t w = static_cast(static_cast(p[0])) | (0x4141414141414141ull << 8u); + if (swar_not_plain(w) != 0) + { + return false; + } + *d = p[0]; + } + + d[-1] = '"'; + std::memcpy(d + n, tail, N - 1); + write_buffer_pos += n + N; + return true; + } + +#if JSON_SCAN_VECTOR + /// copy the 8 to 15 bytes [@a p, @a p + @a n) to @a d with two overlapping + /// words, tested together as one vector, see copy_plain_vectors() + static bool copy_plain_halves(char* d, const char* p, const std::size_t n) noexcept + { +#if JSON_SCAN_NEON + const uint8x8_t lo = vld1_u8(reinterpret_cast(p)); + const uint8x8_t hi = vld1_u8(reinterpret_cast(p + n - 8)); + const uint8x16_t in = vcombine_u8(lo, hi); + const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))), + vorrq_u8(vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20)), vceqq_u8(in, vdupq_n_u8(0x7F)))); + if (vmaxvq_u8(special) != 0) + { + return false; + } + vst1_u8(reinterpret_cast(d), lo); + vst1_u8(reinterpret_cast(d + n - 8), hi); +#else + const __m128i lo = _mm_loadl_epi64(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) + const __m128i hi = _mm_loadl_epi64(static_cast(static_cast(p + n - 8))); // NOLINT(bugprone-casting-through-void) + const __m128i in = _mm_unpacklo_epi64(lo, hi); + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_or_si128(_mm_cmplt_epi8(in, _mm_set1_epi8(0x20)), _mm_cmpeq_epi8(in, _mm_set1_epi8(0x7F)))); + if (_mm_movemask_epi8(special) != 0) + { + return false; + } + _mm_storel_epi64(static_cast<__m128i*>(static_cast(d)), lo); // NOLINT(bugprone-casting-through-void) + _mm_storel_epi64(static_cast<__m128i*>(static_cast(d + n - 8)), hi); // NOLINT(bugprone-casting-through-void) +#endif + return true; + } + + /// copy the 16 to 32 bytes [@a p, @a p + @a n) to @a d with two overlapping + /// vectors, unless one of them is a quote, a backslash, a control + /// character, DEL or not ASCII + static bool copy_plain_vectors(char* d, const char* p, const std::size_t n) noexcept + { +#if JSON_SCAN_NEON + const uint8x16_t a = vld1q_u8(reinterpret_cast(p)); + const uint8x16_t b = vld1q_u8(reinterpret_cast(p + n - 16)); + const uint8x16_t special = vorrq_u8(vorrq_u8(vorrq_u8(vceqq_u8(a, vdupq_n_u8('"')), vceqq_u8(a, vdupq_n_u8('\\'))), + vorrq_u8(vceqq_u8(b, vdupq_n_u8('"')), vceqq_u8(b, vdupq_n_u8('\\')))), + vorrq_u8(vorrq_u8(vcltq_s8(vreinterpretq_s8_u8(a), vdupq_n_s8(0x20)), vcltq_s8(vreinterpretq_s8_u8(b), vdupq_n_s8(0x20))), + vorrq_u8(vceqq_u8(a, vdupq_n_u8(0x7F)), vceqq_u8(b, vdupq_n_u8(0x7F))))); + if (vmaxvq_u8(special) != 0) + { + return false; + } + vst1q_u8(reinterpret_cast(d), a); + vst1q_u8(reinterpret_cast(d + n - 16), b); +#else + const __m128i a = _mm_loadu_si128(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) + const __m128i b = _mm_loadu_si128(static_cast(static_cast(p + n - 16))); // NOLINT(bugprone-casting-through-void) + const __m128i quote = _mm_set1_epi8('"'); + const __m128i backslash = _mm_set1_epi8('\\'); + const __m128i space = _mm_set1_epi8(0x20); + const __m128i del = _mm_set1_epi8(0x7F); + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(a, quote), _mm_cmpeq_epi8(a, backslash)), + _mm_or_si128(_mm_cmpeq_epi8(b, quote), _mm_cmpeq_epi8(b, backslash))), + _mm_or_si128(_mm_or_si128(_mm_cmplt_epi8(a, space), _mm_cmplt_epi8(b, space)), + _mm_or_si128(_mm_cmpeq_epi8(a, del), _mm_cmpeq_epi8(b, del)))); + if (_mm_movemask_epi8(special) != 0) + { + return false; + } + _mm_storeu_si128(static_cast<__m128i*>(static_cast(d)), a); // NOLINT(bugprone-casting-through-void) + _mm_storeu_si128(static_cast<__m128i*>(static_cast(d + n - 16)), b); // NOLINT(bugprone-casting-through-void) +#endif + return true; + } +#endif +#endif + JSON_PRIVATE_UNLESS_TESTED: /*! @brief dump escaped string @@ -26424,9 +34158,46 @@ class serializer @param[in] s the string to escape Complexity: Linear in the length of string @a s. + + @note kept out of line: with the short-string attempts in front of it gone + from here, compilers would otherwise inline it into @ref put_object_key + and then no longer into the loops that call that */ - void dump_escaped(const string_t& s) + JSON_HEDLEY_NEVER_INLINE void dump_escaped(const string_t& s) { + // Short strings of printable ASCII - most object keys and many values - + // are checked byte by byte against a table and then copied in one go. + // For them, this is cheaper than starting the vector scanner below, + // whose setup costs more than such a string takes to scan. The same + // check serves both values of ensure_ascii. + // + // Where put_short_quoted() exists and takes strings of up to 32 bytes, + // the callers have tried it already, and repeating the attempt only + // costs time for strings that need escaping or are not ASCII. +#if !JSON_LITTLE_ENDIAN_TARGET || !JSON_SCAN_VECTOR + if (s.size() <= 32) + { + const auto& copyable = ascii_copyable(); + const auto* const data = reinterpret_cast(s.data()); + const std::size_t n = s.size(); + std::size_t k = 0; + while (k + 4 <= n && copyable[data[k]] != 0 && copyable[data[k + 1]] != 0 + && copyable[data[k + 2]] != 0 && copyable[data[k + 3]] != 0) + { + k += 4; + } + while (k < n && copyable[data[k]] != 0) + { + ++k; + } + if (k == n) + { + put_string(s, 0, n); + return; + } + } +#endif + // dispatch once here rather than test the flag inside the loop: it does // not change while a string is written, and folding it lets each of the // two scanners be inlined into a loop of its own @@ -26440,6 +34211,28 @@ class serializer } } + /// write out the bytes pending in @ref string_buffer and reset @a bytes + JSON_HEDLEY_ALWAYS_INLINE void emit_pending(std::size_t& bytes) + { + if (bytes != 0) + { + put_buffer(string_buffer, bytes); + bytes = 0; + } + } + + /// write buffer and reset index if fewer than 13 bytes are left, as this + /// is the maximal number of bytes to be written ("\uxxxx\uxxxx\0") for one + /// code point + JSON_HEDLEY_ALWAYS_INLINE void reserve_string_buffer(std::size_t& bytes) + { + if (string_buffer.size() - bytes < 13) + { + put_buffer(string_buffer, bytes); + bytes = 0; + } + } + /*! @brief worker for @ref dump_escaped @@ -26498,12 +34291,8 @@ class serializer { // emit any bytes still pending in string_buffer first to // preserve output order, then write the run directly - if (bytes != 0) - { - put_buffer(string_buffer, bytes); - bytes = 0; - } - put_string(s, i, i + run); + emit_pending(bytes); + put_run(s, i, run); bytes_after_last_accept = 0; undumped_chars = 0; i += run; @@ -26512,110 +34301,97 @@ class serializer break; } } + + // A byte below 0x60 is a complete code point and needs no + // UTF-8 decoding. The scanner stops only at a control + // character, a quote, or a backslash in that range, so look up + // its escape sequence and write all 8 bytes of the table entry + // straight into the write buffer; only the length stored in the + // last byte of the entry is kept. + if (data[i] < 0x60) + { + // emit any bytes still pending in string_buffer first to + // preserve output order + emit_pending(bytes); + // deliberately not put_buffer(): a fixed 8-byte memcpy + // has no asserts and no write-through branch, which + // matters on the hot path + const auto& escape = ascii_escapes()[data[i]]; + if (JSON_HEDLEY_UNLIKELY(write_buffer.size() - write_buffer_pos < escape.size())) + { + flush(); + } + std::memcpy(write_buffer.data() + write_buffer_pos, escape.data(), escape.size()); + write_buffer_pos += static_cast(escape[7]); + bytes_after_last_accept = 0; + undumped_chars = 0; + continue; + } } const auto byte = static_cast(s[i]); + if (EnsureAscii && state == UTF8_ACCEPT && byte >= 0x80) + { + // a run of well-formed multi-byte characters is escaped + // straight into the write buffer; an ill-formed sequence + // ends the run and is left to the byte-at-a-time path + if (bytes != 0) + { + put_buffer(string_buffer, bytes); + bytes = 0; + bytes_after_last_accept = 0; + } + const std::size_t escaped = write_u_escapes(reinterpret_cast(s.data()) + i, s.size() - i); + if (escaped != 0) + { + bytes_after_last_accept = 0; + undumped_chars = 0; + i += escaped - 1; + continue; + } + } + switch (decode(state, codepoint, byte)) { case UTF8_ACCEPT: // decode found a new code point { - switch (codepoint) + // the code point is at least 0x60 (smaller ones are handled + // above) and is not printable ASCII (which the scanner + // copies): it is DEL (0x7F) under EnsureAscii or a non-ASCII + // character + if (EnsureAscii) { - case 0x08: // backspace + JSON_ASSERT(codepoint >= 0x7F); + if (error_handler == error_handler_t::keep) { - string_buffer[bytes++] = '\\'; - string_buffer[bytes++] = 'b'; - break; + // this character was buffered as raw bytes below + // in case it turned out to be part of an + // ill-formed sequence (which is kept as is); now + // that it decoded to a well-formed code point, + // undo that and \u-escape it like any other + // character instead + bytes = bytes_after_last_accept; } - - case 0x09: // horizontal tab + if (codepoint <= 0xFFFF) { - string_buffer[bytes++] = '\\'; - string_buffer[bytes++] = 't'; - break; + write_u_escape(bytes, static_cast(codepoint)); } - - case 0x0A: // newline + else { - string_buffer[bytes++] = '\\'; - string_buffer[bytes++] = 'n'; - break; - } - - case 0x0C: // formfeed - { - string_buffer[bytes++] = '\\'; - string_buffer[bytes++] = 'f'; - break; - } - - case 0x0D: // carriage return - { - string_buffer[bytes++] = '\\'; - string_buffer[bytes++] = 'r'; - break; - } - - case 0x22: // quotation mark - { - string_buffer[bytes++] = '\\'; - string_buffer[bytes++] = '"'; - break; - } - - case 0x5C: // reverse solidus - { - string_buffer[bytes++] = '\\'; - string_buffer[bytes++] = '\\'; - break; - } - - default: - { - // escape control characters (0x00..0x1F) or, if - // EnsureAscii parameter is used, non-ASCII characters - if ((codepoint <= 0x1F) || (EnsureAscii && (codepoint >= 0x7F))) - { - if (EnsureAscii && error_handler == error_handler_t::keep) - { - // this character was buffered as raw bytes - // below in case it turned out to be part of - // an ill-formed sequence (which is kept as - // is); now that it decoded to a well-formed - // code point, undo that and \u-escape it - // like any other character instead - bytes = bytes_after_last_accept; - } - if (codepoint <= 0xFFFF) - { - write_u_escape(bytes, static_cast(codepoint)); - } - else - { - write_u_escape(bytes, static_cast(0xD7C0u + (codepoint >> 10u))); - write_u_escape(bytes, static_cast(0xDC00u + (codepoint & 0x3FFu))); - } - } - else - { - // copy byte to buffer (all previous bytes - // been copied have in default case above) - string_buffer[bytes++] = s[i]; - } - break; + write_u_escape(bytes, static_cast(0xD7C0u + (codepoint >> 10u))); + write_u_escape(bytes, static_cast(0xDC00u + (codepoint & 0x3FFu))); } } - - // write buffer and reset index; there must be 13 bytes - // left, as this is the maximal number of bytes to be - // written ("\uxxxx\uxxxx\0") for one code point - if (string_buffer.size() - bytes < 13) + else { - put_buffer(string_buffer, bytes); - bytes = 0; + // copy byte to buffer (all previous bytes of the + // sequence have been copied in the default case below) + string_buffer[bytes++] = s[i]; } + reserve_string_buffer(bytes); + // remember the byte position of this accept bytes_after_last_accept = bytes; undumped_chars = 0; @@ -26628,7 +34404,7 @@ class serializer { case error_handler_t::strict: { - JSON_THROW(type_error::create(316, concat("invalid UTF-8 byte at index ", std::to_string(i), ": 0x", detail::hex_byte(byte)), nullptr)); + JSON_THROW(type_error::create(exception_id::invalid_utf8, concat("invalid UTF-8 byte at index ", std::to_string(i), ": 0x", detail::hex_byte(byte)), nullptr)); } case error_handler_t::ignore: @@ -26666,14 +34442,7 @@ class serializer string_buffer[bytes++] = '\xBD'; } - // write buffer and reset index; there must be 13 bytes - // left, as this is the maximal number of bytes to be - // written ("\uxxxx\uxxxx\0") for one code point - if (string_buffer.size() - bytes < 13) - { - put_buffer(string_buffer, bytes); - bytes = 0; - } + reserve_string_buffer(bytes); bytes_after_last_accept = bytes; } @@ -26706,14 +34475,7 @@ class serializer string_buffer[bytes++] = s[i]; } - // write buffer and reset index; there must be 13 bytes - // left, as this is the maximal number of bytes to be - // written ("\uxxxx\uxxxx\0") for one code point - if (string_buffer.size() - bytes < 13) - { - put_buffer(string_buffer, bytes); - bytes = 0; - } + reserve_string_buffer(bytes); bytes_after_last_accept = bytes; undumped_chars = 0; @@ -26761,7 +34523,7 @@ class serializer { case error_handler_t::strict: { - JSON_THROW(type_error::create(316, concat("incomplete UTF-8 string; last byte: 0x", detail::hex_byte(static_cast(s[s.size() - 1]))), nullptr)); + JSON_THROW(type_error::create(exception_id::invalid_utf8, concat("incomplete UTF-8 string; last byte: 0x", detail::hex_byte(static_cast(s[s.size() - 1]))), nullptr)); } case error_handler_t::ignore: @@ -26833,8 +34595,12 @@ class serializer the indentation character once and flushing that same content repeatedly: flushing does not disturb what the buffer holds, so re-filling it between flushes would be redundant work. + + @note inlined on purpose, as is @ref put_literal: GCC otherwise stops + inlining both into the pretty-printing loops once @ref dump_escaped + has grown, which made indented output up to 13% slower */ - void put_indent(std::size_t indent) + JSON_HEDLEY_ALWAYS_INLINE void put_indent(std::size_t indent) { // closing braces at the outermost level ask for no indentation at all if (indent == 0) @@ -26842,6 +34608,12 @@ class serializer return; } + // the usual indentation, without a call of memset + if (JSON_HEDLEY_LIKELY(put_short_literal_indent("", indent))) + { + return; + } + const std::size_t capacity = write_buffer.size(); // fill whatever room is left in the buffer; this is the whole job @@ -26875,6 +34647,51 @@ class serializer write_buffer_pos = indent; } + /*! + @brief append a string literal and then @a indent indentation characters + + The usual line break and indentation go into the buffer with one check of + its room and four stores of the indentation, see @ref put_short_literal_indent. + */ + template + JSON_HEDLEY_ALWAYS_INLINE void put_literal_indent(const char (&s)[N], const std::size_t indent) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + { + if (JSON_HEDLEY_LIKELY(put_short_literal_indent(s, indent))) + { + return; + } + put_literal(s); + put_indent(indent); + } + + /*! + @brief append a string literal and up to 32 indentation characters in one go + + Four words of the indentation character are stored unconditionally and only + the part wanted is claimed, which spares the call of memset. Shared by + @ref put_indent (with an empty literal, whose copy compiles to nothing) and + @ref put_literal_indent. Returns false, having written nothing, if + @a indent is wider or the write buffer lacks the room. + */ + template + JSON_HEDLEY_ALWAYS_INLINE bool put_short_literal_indent(const char (&s)[N], const std::size_t indent) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + { + constexpr std::size_t length = N - 1; + if (indent > 32 || write_buffer_pos + length + 32 > write_buffer.size()) + { + return false; + } + const std::uint64_t word = 0x0101010101010101ull * static_cast(indent_char); + char* const d = write_buffer.data() + write_buffer_pos; + std::memcpy(d, s, length); + std::memcpy(d + length, &word, sizeof(word)); + std::memcpy(d + length + 8, &word, sizeof(word)); + std::memcpy(d + length + 16, &word, sizeof(word)); + std::memcpy(d + length + 24, &word, sizeof(word)); + write_buffer_pos += length + indent; + return true; + } + /*! @brief append a string literal to the write buffer @@ -26884,7 +34701,7 @@ class serializer write-through path for oversized runs. */ template - void put_literal(const char (&s)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + JSON_HEDLEY_ALWAYS_INLINE void put_literal(const char (&s)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) { static_assert(N >= 2, "put_literal expects a non-empty string literal"); // the array bound counts the terminating NUL, which is not written @@ -26899,6 +34716,28 @@ class serializer write_buffer_pos += length; } + /*! + @brief append the characters of @a str in [@a start, @a start + @a length) + + A run of up to 16 characters with 16 or more in the string from @a start on + and in the write buffer from its end on is copied as a whole block of 16, + which spares the call of memcpy with a length known only at run time; the + surplus characters in the buffer are overwritten by what follows. + */ + template + void put_run(const StringType& str, const std::size_t start, const std::size_t length) + { + // a short run is the exception in text without escapes (CJK, accented + // Latin), for which this keeps the long path the fast one + if (JSON_HEDLEY_UNLIKELY(length <= 16 && start + 16 <= str.size() && write_buffer_pos + 16 <= write_buffer.size())) + { + std::memcpy(write_buffer.data() + write_buffer_pos, str.data() + start, 16); + write_buffer_pos += length; + return; + } + put_string(str, start, start + length); + } + /*! @brief append the characters of @a str in [@a start, @a end) @@ -26967,7 +34806,7 @@ class serializer @param[in] x unsigned integer number to count its digits @return number of decimal digits */ - unsigned int count_digits(number_unsigned_t x) noexcept + unsigned int count_digits(number_magnitude_t x) noexcept { unsigned int n_digits = 1; for (;;) @@ -26993,13 +34832,214 @@ class serializer } } + /*! + @brief whether each byte can be written verbatim under either value of ensure_ascii + + Entry c is 1 for printable ASCII other than '"' and '\\' (see + is_ascii_copyable()) and 0 otherwise. A table lookup per byte is the + cheapest test for the short strings handled first in dump_escaped(). + */ + static const std::array& ascii_copyable() noexcept + { + // constant-initialized from the predicate: no runtime loop, no guard + static constexpr std::array copyable = make_ascii_copyable(make_index_sequence<256> {}); + return copyable; + } + + template + static constexpr std::array make_ascii_copyable(index_sequence /*unused*/) noexcept + { + return {{ static_cast(is_ascii_copyable(static_cast(Is)) ? 1 : 0)... }}; + } + + /*! + @brief the JSON form of each character below 0x60 + + Entry c holds the escape sequence of character c - the short form for + \\b, \\t, \\n, \\f, \\r, \\" and \\\\, \\u00xx for the other control characters - + or c itself if it needs no escaping, padded with zeros to 7 bytes. The last + byte is the length of the sequence. The 8 bytes of an entry are copied at + once, so writing an escape needs neither a branch nor a byte-wise loop. + */ + static const std::array, 0x60>& ascii_escapes() noexcept + { + static const std::array, 0x60> escapes = + { + { + {{'\\', 'u', '0', '0', '0', '0', 0, 6}}, // 0x00 + {{'\\', 'u', '0', '0', '0', '1', 0, 6}}, // 0x01 + {{'\\', 'u', '0', '0', '0', '2', 0, 6}}, // 0x02 + {{'\\', 'u', '0', '0', '0', '3', 0, 6}}, // 0x03 + {{'\\', 'u', '0', '0', '0', '4', 0, 6}}, // 0x04 + {{'\\', 'u', '0', '0', '0', '5', 0, 6}}, // 0x05 + {{'\\', 'u', '0', '0', '0', '6', 0, 6}}, // 0x06 + {{'\\', 'u', '0', '0', '0', '7', 0, 6}}, // 0x07 + {{'\\', 'b', 0, 0, 0, 0, 0, 2}}, // 0x08 + {{'\\', 't', 0, 0, 0, 0, 0, 2}}, // 0x09 + {{'\\', 'n', 0, 0, 0, 0, 0, 2}}, // 0x0A + {{'\\', 'u', '0', '0', '0', 'b', 0, 6}}, // 0x0B + {{'\\', 'f', 0, 0, 0, 0, 0, 2}}, // 0x0C + {{'\\', 'r', 0, 0, 0, 0, 0, 2}}, // 0x0D + {{'\\', 'u', '0', '0', '0', 'e', 0, 6}}, // 0x0E + {{'\\', 'u', '0', '0', '0', 'f', 0, 6}}, // 0x0F + {{'\\', 'u', '0', '0', '1', '0', 0, 6}}, // 0x10 + {{'\\', 'u', '0', '0', '1', '1', 0, 6}}, // 0x11 + {{'\\', 'u', '0', '0', '1', '2', 0, 6}}, // 0x12 + {{'\\', 'u', '0', '0', '1', '3', 0, 6}}, // 0x13 + {{'\\', 'u', '0', '0', '1', '4', 0, 6}}, // 0x14 + {{'\\', 'u', '0', '0', '1', '5', 0, 6}}, // 0x15 + {{'\\', 'u', '0', '0', '1', '6', 0, 6}}, // 0x16 + {{'\\', 'u', '0', '0', '1', '7', 0, 6}}, // 0x17 + {{'\\', 'u', '0', '0', '1', '8', 0, 6}}, // 0x18 + {{'\\', 'u', '0', '0', '1', '9', 0, 6}}, // 0x19 + {{'\\', 'u', '0', '0', '1', 'a', 0, 6}}, // 0x1A + {{'\\', 'u', '0', '0', '1', 'b', 0, 6}}, // 0x1B + {{'\\', 'u', '0', '0', '1', 'c', 0, 6}}, // 0x1C + {{'\\', 'u', '0', '0', '1', 'd', 0, 6}}, // 0x1D + {{'\\', 'u', '0', '0', '1', 'e', 0, 6}}, // 0x1E + {{'\\', 'u', '0', '0', '1', 'f', 0, 6}}, // 0x1F + {{' ', 0, 0, 0, 0, 0, 0, 1}}, // 0x20 + {{'!', 0, 0, 0, 0, 0, 0, 1}}, // 0x21 + {{'\\', '"', 0, 0, 0, 0, 0, 2}}, // 0x22 + {{'#', 0, 0, 0, 0, 0, 0, 1}}, // 0x23 + {{'$', 0, 0, 0, 0, 0, 0, 1}}, // 0x24 + {{'%', 0, 0, 0, 0, 0, 0, 1}}, // 0x25 + {{'&', 0, 0, 0, 0, 0, 0, 1}}, // 0x26 + {{'\'', 0, 0, 0, 0, 0, 0, 1}}, // 0x27 + {{'(', 0, 0, 0, 0, 0, 0, 1}}, // 0x28 + {{')', 0, 0, 0, 0, 0, 0, 1}}, // 0x29 + {{'*', 0, 0, 0, 0, 0, 0, 1}}, // 0x2A + {{'+', 0, 0, 0, 0, 0, 0, 1}}, // 0x2B + {{',', 0, 0, 0, 0, 0, 0, 1}}, // 0x2C + {{'-', 0, 0, 0, 0, 0, 0, 1}}, // 0x2D + {{'.', 0, 0, 0, 0, 0, 0, 1}}, // 0x2E + {{'/', 0, 0, 0, 0, 0, 0, 1}}, // 0x2F + {{'0', 0, 0, 0, 0, 0, 0, 1}}, // 0x30 + {{'1', 0, 0, 0, 0, 0, 0, 1}}, // 0x31 + {{'2', 0, 0, 0, 0, 0, 0, 1}}, // 0x32 + {{'3', 0, 0, 0, 0, 0, 0, 1}}, // 0x33 + {{'4', 0, 0, 0, 0, 0, 0, 1}}, // 0x34 + {{'5', 0, 0, 0, 0, 0, 0, 1}}, // 0x35 + {{'6', 0, 0, 0, 0, 0, 0, 1}}, // 0x36 + {{'7', 0, 0, 0, 0, 0, 0, 1}}, // 0x37 + {{'8', 0, 0, 0, 0, 0, 0, 1}}, // 0x38 + {{'9', 0, 0, 0, 0, 0, 0, 1}}, // 0x39 + {{':', 0, 0, 0, 0, 0, 0, 1}}, // 0x3A + {{';', 0, 0, 0, 0, 0, 0, 1}}, // 0x3B + {{'<', 0, 0, 0, 0, 0, 0, 1}}, // 0x3C + {{'=', 0, 0, 0, 0, 0, 0, 1}}, // 0x3D + {{'>', 0, 0, 0, 0, 0, 0, 1}}, // 0x3E + {{'?', 0, 0, 0, 0, 0, 0, 1}}, // 0x3F + {{'@', 0, 0, 0, 0, 0, 0, 1}}, // 0x40 + {{'A', 0, 0, 0, 0, 0, 0, 1}}, // 0x41 + {{'B', 0, 0, 0, 0, 0, 0, 1}}, // 0x42 + {{'C', 0, 0, 0, 0, 0, 0, 1}}, // 0x43 + {{'D', 0, 0, 0, 0, 0, 0, 1}}, // 0x44 + {{'E', 0, 0, 0, 0, 0, 0, 1}}, // 0x45 + {{'F', 0, 0, 0, 0, 0, 0, 1}}, // 0x46 + {{'G', 0, 0, 0, 0, 0, 0, 1}}, // 0x47 + {{'H', 0, 0, 0, 0, 0, 0, 1}}, // 0x48 + {{'I', 0, 0, 0, 0, 0, 0, 1}}, // 0x49 + {{'J', 0, 0, 0, 0, 0, 0, 1}}, // 0x4A + {{'K', 0, 0, 0, 0, 0, 0, 1}}, // 0x4B + {{'L', 0, 0, 0, 0, 0, 0, 1}}, // 0x4C + {{'M', 0, 0, 0, 0, 0, 0, 1}}, // 0x4D + {{'N', 0, 0, 0, 0, 0, 0, 1}}, // 0x4E + {{'O', 0, 0, 0, 0, 0, 0, 1}}, // 0x4F + {{'P', 0, 0, 0, 0, 0, 0, 1}}, // 0x50 + {{'Q', 0, 0, 0, 0, 0, 0, 1}}, // 0x51 + {{'R', 0, 0, 0, 0, 0, 0, 1}}, // 0x52 + {{'S', 0, 0, 0, 0, 0, 0, 1}}, // 0x53 + {{'T', 0, 0, 0, 0, 0, 0, 1}}, // 0x54 + {{'U', 0, 0, 0, 0, 0, 0, 1}}, // 0x55 + {{'V', 0, 0, 0, 0, 0, 0, 1}}, // 0x56 + {{'W', 0, 0, 0, 0, 0, 0, 1}}, // 0x57 + {{'X', 0, 0, 0, 0, 0, 0, 1}}, // 0x58 + {{'Y', 0, 0, 0, 0, 0, 0, 1}}, // 0x59 + {{'Z', 0, 0, 0, 0, 0, 0, 1}}, // 0x5A + {{'[', 0, 0, 0, 0, 0, 0, 1}}, // 0x5B + {{'\\', '\\', 0, 0, 0, 0, 0, 2}}, // 0x5C + {{']', 0, 0, 0, 0, 0, 0, 1}}, // 0x5D + {{'^', 0, 0, 0, 0, 0, 0, 1}}, // 0x5E + {{'_', 0, 0, 0, 0, 0, 0, 1}}, // 0x5F + } + }; + return escapes; + } + + /*! + @brief escape the well-formed multi-byte characters at the start of @a data + + Writes the "\\uXXXX" escape of each character (two of them for a character + above U+FFFF) straight into the write buffer, which is why the caller must + have written out what is pending in @ref string_buffer. Stops at the first + byte that is below 0x80 or starts an ill-formed or truncated sequence, which + the caller handles. + + @return the number of bytes of @a data that have been escaped + */ + JSON_HEDLEY_NEVER_INLINE std::size_t write_u_escapes(const unsigned char* data, std::size_t n) + { + std::size_t i = 0; + while (i < n && data[i] >= 0x80) + { + const std::size_t length = validate_one_utf8(data + i, n - i); + if (length == 0) + { + break; + } + + // a lead byte of 0x80 or above starts a sequence of 2 to 4 bytes + JSON_ASSERT(length >= 2 && length <= 4); + + const auto b0 = static_cast(data[i]); + const auto b1 = static_cast(data[i + 1]); + std::uint32_t codepoint = 0; + switch (length) + { + case 2: + codepoint = ((b0 & 0x1Fu) << 6u) | (b1 & 0x3Fu); + break; + case 3: + codepoint = ((b0 & 0x0Fu) << 12u) | ((b1 & 0x3Fu) << 6u) | (static_cast(data[i + 2]) & 0x3Fu); + break; + default: // 4 + codepoint = ((b0 & 0x07u) << 18u) | ((b1 & 0x3Fu) << 12u) | ((static_cast(data[i + 2]) & 0x3Fu) << 6u) | (static_cast(data[i + 3]) & 0x3Fu); + break; + } + + // at most two escapes of six bytes each + if (JSON_HEDLEY_UNLIKELY(write_buffer.size() - write_buffer_pos < 12)) + { + flush(); + } + char* d = write_buffer.data() + write_buffer_pos; + if (codepoint > 0xFFFFu) + { + d = put_u_escape(d, static_cast(0xD7C0u + (codepoint >> 10u))); + codepoint = 0xDC00u + (codepoint & 0x3FFu); + } + d = put_u_escape(d, static_cast(codepoint)); + write_buffer_pos = static_cast(d - write_buffer.data()); + i += length; + } + return i; + } + + static char* put_u_escape(char* d, const std::uint16_t codeunit) noexcept + { + d[0] = '\\'; + d[1] = 'u'; + return hex4_lower(codeunit, d + 2); + } + /*! * @brief write a lowercase "\\uXXXX" escape sequence into @a string_buffer * * Branch-free replacement for `snprintf(buf, 7, "\\u%04x", codeunit)` in the * string escaping hot path. It writes exactly six characters ('\\', 'u' and * four hex digits) at position @a pos of @a string_buffer via a nibble - * lookup table, avoiding the format-string parsing and locale machinery of + * lookup table (@ref hex4_lower), avoiding the format-string parsing and locale machinery of * `snprintf`. Advances @a pos by the number of bytes written (6). * * @param[in] pos position in @a string_buffer to write at; there must @@ -27009,13 +35049,9 @@ class serializer void write_u_escape(std::size_t& pos, std::uint16_t codeunit) noexcept { JSON_ASSERT(string_buffer.size() - pos >= 6); - constexpr const char* nibble_to_hex = "0123456789abcdef"; string_buffer[pos + 0] = '\\'; string_buffer[pos + 1] = 'u'; - string_buffer[pos + 2] = nibble_to_hex[(codeunit >> 12u) & 0x0Fu]; - string_buffer[pos + 3] = nibble_to_hex[(codeunit >> 8u) & 0x0Fu]; - string_buffer[pos + 4] = nibble_to_hex[(codeunit >> 4u) & 0x0Fu]; - string_buffer[pos + 5] = nibble_to_hex[codeunit & 0x0Fu]; + hex4_lower(codeunit, &string_buffer[pos + 2]); pos += 6; } @@ -27106,8 +35142,9 @@ class serializer /*! @brief dump an integer - Dump a given integer, appending it to @ref write_buffer. Works internally with - @a number_buffer. + Dump a given integer, appending it to @ref write_buffer (directly: copying + the digits from another buffer right after writing them waits until the + stores are done). @param[in] x integer number (signed or unsigned) to dump @tparam NumberType either @a number_integer_t or @a number_unsigned_t @@ -27142,33 +35179,57 @@ class serializer return; } - // use a pointer to fill the buffer - auto buffer_ptr = number_buffer.begin(); // NOLINT(llvm-qualified-auto,readability-qualified-auto) + // use a pointer to fill the buffer (room for as much as number_buffer holds) + if (JSON_HEDLEY_UNLIKELY(write_buffer_pos + number_buffer.size() > write_buffer.size())) + { + flush(); + } + auto* buffer_ptr = write_buffer.data() + write_buffer_pos; - number_unsigned_t abs_value; + number_magnitude_t abs_value; - unsigned int n_chars{}; + // one byte for the minus sign + unsigned int n_chars = 0; if (is_negative_number(x)) { *buffer_ptr = '-'; abs_value = remove_sign(static_cast(x)); - - // account one more byte for the minus sign - n_chars = 1 + count_digits(abs_value); + n_chars = 1; } else { - abs_value = static_cast(x); - n_chars = count_digits(abs_value); + abs_value = static_cast(x); } + // up to 16 digits: eight at a time (as the digits of floats), written + // without leading zeros + if (abs_value < 10000000000000000u) + { + const auto value = static_cast(abs_value); // fits: abs_value < 10^16 + const std::uint64_t upper = value / 100000000u; + const std::uint64_t first = dtoa_impl::eight_digit_bytes(upper != 0 ? upper : value); + const auto leading = static_cast(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); + n_chars += 8 - leading; + if (upper != 0) + { + dtoa_impl::store_msb_first(p + 8 - leading, dtoa_impl::eight_digit_bytes(value - (upper * 100000000u)) + 0x3030303030303030u); + n_chars += 8; + } + write_buffer_pos += n_chars; + return; + } + + n_chars += count_digits(abs_value); + // spare 1 byte for '\0' JSON_ASSERT(n_chars < number_buffer.size() - 1); // jump to the end to generate the string from backward, // so we later avoid reversing the result - buffer_ptr += static_cast(n_chars); + buffer_ptr += n_chars; // Fast int2ascii implementation inspired by "Fastware" talk by Andrei Alexandrescu // See: https://www.youtube.com/watch?v=o4-CwDo2zpg @@ -27191,14 +35252,13 @@ class serializer *(--buffer_ptr) = static_cast('0' + abs_value); } - put_buffer(number_buffer, n_chars); + write_buffer_pos += n_chars; } /*! @brief dump a floating-point number - Dump a given floating-point number, appending it to @ref write_buffer. Works internally - with @a number_buffer. + Dump a given floating-point number, appending it to @ref write_buffer. @param[in] x floating-point number to dump */ @@ -27225,10 +35285,15 @@ class serializer void dump_float(number_float_t x, std::true_type /*is_ieee_single_or_double*/) { - auto* begin = number_buffer.data(); + // directly into the write buffer: copying the text from number_buffer + // right after to_chars() wrote it waits until its stores are done + if (JSON_HEDLEY_UNLIKELY(write_buffer_pos + number_buffer.size() > write_buffer.size())) + { + flush(); + } + auto* begin = write_buffer.data() + write_buffer_pos; auto* end = ::nlohmann::detail::to_chars(begin, begin + number_buffer.size(), x); - - put_buffer(number_buffer, static_cast(end - begin)); + write_buffer_pos += static_cast(end - begin); } JSON_HEDLEY_NON_NULL(1) @@ -27302,17 +35367,6 @@ class serializer } } - /* - * Overload to make the compiler happy while it is instantiating - * dump_integer for number_unsigned_t. - * Must never be called. - */ - number_unsigned_t remove_sign(number_unsigned_t x) - { - JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE - return x; // LCOV_EXCL_LINE - } - /* * Helper function for dump_integer * @@ -27322,21 +35376,32 @@ class serializer * absolute values of INT_MIN and INT_MAX are usually not the same. See * #1708 for details. */ - number_unsigned_t remove_sign(number_integer_t x) noexcept + number_magnitude_t remove_sign(number_integer_t x) noexcept { JSON_ASSERT(x < 0); - return static_cast(-(x + 1)) + 1; + return static_cast(-(x + 1)) + 1; } private: /// the output of the serializer (non-owning; the adapter lives at the call site) output_adapter_protocol* o = nullptr; + /// a character buffer left uninitialized: the buffers below are written + /// before they are read (only the part written is ever read), and zeroing + /// 1.6 KiB took more than half of what dumping a small value costs (the + /// user-provided constructor keeps the member initialization, which + /// -Weffc++ asks for, from zeroing them) + template + struct uninitialized_buffer : std::array + { + uninitialized_buffer() noexcept {} // NOLINT(modernize-use-equals-default,hicpp-use-equals-default,cppcoreguidelines-pro-type-member-init,hicpp-member-init): see above + }; + /// a (hopefully) large enough character buffer - std::array number_buffer{{}}; + uninitialized_buffer<64> number_buffer; /// string buffer - std::array string_buffer{{}}; + uninitialized_buffer<512> string_buffer; /// the indentation character const char indent_char; @@ -27356,7 +35421,7 @@ class serializer /// buffer collecting output before it is flushed to the output adapter, so /// that the many small structural writes become few bulk writes static constexpr std::size_t write_buffer_size = 1024; - std::array write_buffer{{}}; + uninitialized_buffer write_buffer; /// number of valid bytes currently held in @ref write_buffer std::size_t write_buffer_pos = 0; }; @@ -27369,7 +35434,7 @@ NLOHMANN_JSON_NAMESPACE_END // #include // #include - +// IWYU pragma: export // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ @@ -27399,6 +35464,8 @@ NLOHMANN_JSON_NAMESPACE_END // #include +// #include + NLOHMANN_JSON_NAMESPACE_BEGIN @@ -27466,6 +35533,28 @@ private: return self.end(); } + /// @brief throw the exception of a failed at() lookup + /// @note kept out of line so that at_impl() stays small enough to inline + /// @throw std::out_of_range always + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_key_not_found() + { + JSON_THROW(std::out_of_range("key not found")); + } + + /// @brief shared implementation of the const and non-const at() overloads + /// @throw std::out_of_range if @a key is not found + template + static auto at_impl(Self& self, const KeyType& key) -> decltype((self.begin()->second)) + { + const auto it = find_impl(self, key); + if (it == self.end()) + { + throw_key_not_found(); + } + return it->second; + } + /// @brief remove the entry @a it points to, preserving order /// @note keys are not movable, so the tail is destroyed and re-constructed in place void erase_at(iterator it) @@ -27478,6 +35567,19 @@ private: Container::pop_back(); } + /// @brief remove the entry with the given key; returns the number of removed entries + template + size_type erase_impl(const K& key) + { + const auto it = find_impl(*this, key); + if (it != this->end()) + { + erase_at(it); + return 1; + } + return 0; + } + public: template::value, int> = 0> @@ -27506,6 +35608,17 @@ public: return {std::prev(this->end()), true}; } + /// @brief add an element at the end without checking whether the key exists + /// @note The caller must guarantee that @a key is not yet contained, otherwise + /// the container holds duplicate keys. This avoids the linear search of + /// emplace() and operator[] when many keys known to be new are added. + template::value, int> = 0> + void emplace_new_key(const key_type& key, V && t) + { + append(key, std::forward(t)); + } + T& operator[](const key_type& key) { return emplace(key, T{}).first->second; @@ -27532,70 +35645,38 @@ public: T& at(const key_type& key) { - const auto it = find_impl(*this, key); - if (it == this->end()) - { - JSON_THROW(std::out_of_range("key not found")); - } - return it->second; + return at_impl(*this, key); } template::value, int> = 0> T & at(KeyType && key) // NOLINT(cppcoreguidelines-missing-std-forward) { - const auto it = find_impl(*this, key); - if (it == this->end()) - { - JSON_THROW(std::out_of_range("key not found")); - } - return it->second; + return at_impl(*this, key); } const T& at(const key_type& key) const { - const auto it = find_impl(*this, key); - if (it == this->end()) - { - JSON_THROW(std::out_of_range("key not found")); - } - return it->second; + return at_impl(*this, key); } template::value, int> = 0> const T & at(KeyType && key) const // NOLINT(cppcoreguidelines-missing-std-forward) { - const auto it = find_impl(*this, key); - if (it == this->end()) - { - JSON_THROW(std::out_of_range("key not found")); - } - return it->second; + return at_impl(*this, key); } size_type erase(const key_type& key) { - const auto it = find_impl(*this, key); - if (it != this->end()) - { - erase_at(it); - return 1; - } - return 0; + return erase_impl(key); } template::value, int> = 0> size_type erase(KeyType && key) // NOLINT(cppcoreguidelines-missing-std-forward) { - const auto it = find_impl(*this, key); - if (it != this->end()) - { - erase_at(it); - return 1; - } - return 0; + return erase_impl(key); } iterator erase(iterator pos) @@ -27623,6 +35704,14 @@ public: // ^ ^ // first last + // Note on conformance: before C++20, [basic.life]/8 did not allow an + // object of a type with a const member (like value_type's const Key) + // to transparently replace the destroyed one, so strictly, accessing + // it through the vector's existing pointers would have required + // std::launder (which does not exist before C++17). C++20 dropped that + // condition (P1971R0, NB comment US 041). Compilers have always treated + // this pattern as intended, so it is kept deliberately. + // Since we cannot move const Keys, we re-construct them in place. // We start at first and re-construct (viz. copy) the elements from // the back of the vector. Example for the first iteration: @@ -27864,9 +35953,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec friend class ::nlohmann::detail::binary_writer; template friend class ::nlohmann::detail::binary_reader; - template + template friend class ::nlohmann::detail::json_sax_dom_parser; template + friend class ::nlohmann::detail::json_sax_dom_binary_parser; + template friend class ::nlohmann::detail::json_sax_dom_callback_parser; #if JSON_DIAGNOSTIC_POSITIONS friend struct ::nlohmann::detail::diagnostic_positions; @@ -28079,9 +36170,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec result["name"] = "JSON for Modern C++"; result["url"] = "https://github.com/nlohmann/json"; result["version"]["string"] = - detail::concat(std::to_string(NLOHMANN_JSON_VERSION_MAJOR), '.', - std::to_string(NLOHMANN_JSON_VERSION_MINOR), '.', - std::to_string(NLOHMANN_JSON_VERSION_PATCH)); + detail::concat(detail::to_string(static_cast(NLOHMANN_JSON_VERSION_MAJOR)), '.', + detail::to_string(static_cast(NLOHMANN_JSON_VERSION_MINOR)), '.', + detail::to_string(static_cast(NLOHMANN_JSON_VERSION_PATCH))); result["version"]["major"] = NLOHMANN_JSON_VERSION_MAJOR; result["version"]["minor"] = NLOHMANN_JSON_VERSION_MINOR; result["version"]["patch"] = NLOHMANN_JSON_VERSION_PATCH; @@ -28103,10 +36194,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec #elif defined(__clang__) result["compiler"] = {{"family", "clang"}, {"version", __clang_version__}}; #elif defined(__GNUC__) || defined(__GNUG__) - result["compiler"] = {{"family", "gcc"}, {"version", detail::concat( - std::to_string(__GNUC__), '.', - std::to_string(__GNUC_MINOR__), '.', - std::to_string(__GNUC_PATCHLEVEL__)) + result["compiler"] = {{"family", "gcc"}, {"version", detail::concat( + detail::to_string(static_cast(__GNUC__)), '.', + detail::to_string(static_cast(__GNUC_MINOR__)), '.', + detail::to_string(static_cast(__GNUC_PATCHLEVEL__))) } }; #elif defined(__HP_aCC) @@ -28124,9 +36215,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec #endif #if defined(_MSVC_LANG) - result["compiler"]["c++"] = std::to_string(_MSVC_LANG); + result["compiler"]["c++"] = detail::to_string(static_cast(_MSVC_LANG)); #elif defined(__cplusplus) - result["compiler"]["c++"] = std::to_string(__cplusplus); + result["compiler"]["c++"] = detail::to_string(static_cast(__cplusplus)); #else result["compiler"]["c++"] = "unknown"; #endif @@ -28208,8 +36299,22 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec "NumberUnsignedType must be at least as wide as NumberIntegerType, " "because it has to hold the absolute value of every NumberIntegerType value"); + // NumberUnsignedType may be the same type as NumberIntegerType (#2573), but + // a repeated NumberFloatType would give overloads that collide, so it gets + // a readable diagnostic instead. + static_assert(!std::is_same::value && !std::is_same::value, + "NumberFloatType must be a different type than NumberIntegerType and NumberUnsignedType"); + private: + /// the kind a value of kind @a t is stored as: if number_unsigned_t is the + /// same type as number_integer_t, every integer is stored as number_integer + /// (#2573); every other kind is stored as itself + static constexpr value_t stored_kind(const value_t t) noexcept + { + return (std::is_same::value && t == value_t::number_unsigned) ? value_t::number_integer : t; + } + /// helper for exception-safe object creation template JSON_HEDLEY_RETURNS_NON_NULL @@ -28283,8 +36388,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec json_value(boolean_t v) noexcept : boolean(v) {} /// constructor for numbers (integer) json_value(number_integer_t v) noexcept : number_integer(v) {} - /// constructor for numbers (unsigned) - json_value(number_unsigned_t v) noexcept : number_unsigned(v) {} + /// constructor for numbers (unsigned); takes a placeholder if number_unsigned_t is number_integer_t + json_value(detail::number_unsigned_overload_type v) noexcept : number_unsigned(v) {} /// constructor for numbers (floating-point) json_value(number_float_t v) noexcept : number_float(v) {} /// constructor for empty values of a given type @@ -28293,26 +36398,12 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec switch (t) { case value_t::object: - { - object = create(); - break; - } - case value_t::array: - { - array = create(); - break; - } - case value_t::string: - { - string = create(""); - break; - } - case value_t::binary: { - binary = create(); + // allocating: kept out of line, so the scalar cases below inline + init_owned(t); break; } @@ -28352,13 +36443,41 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec object = nullptr; // silence warning, see #821 if (JSON_HEDLEY_UNLIKELY(t == value_t::null)) { - JSON_THROW(other_error::create(500, "961c151d2e87f2686a955a9be24d316f1362bf21 3.12.0", nullptr)); // LCOV_EXCL_LINE + detail::throw_literal(detail::exception_id::internal_error, "961c151d2e87f2686a955a9be24d316f1362bf21 3.12.0", nullptr); // LCOV_EXCL_LINE } break; } } } + JSON_HEDLEY_NEVER_INLINE + void init_owned(value_t t) + { + switch (t) + { + case value_t::object: + object = create(); + break; + case value_t::array: + array = create(); + break; + case value_t::string: + string = create(""); + break; + case value_t::binary: + binary = create(); + break; + case value_t::null: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::discarded: + default: + break; + } + } + /// constructor for strings json_value(const string_t& value) : string(create(value)) {} @@ -28409,23 +36528,37 @@ private: // true if v is not an array/object, or is an already-empty one static bool has_no_children(const basic_json& v) noexcept { - switch (v.m_data.m_type) + if (v.m_data.m_type == value_t::array) { - case value_t::array: - return v.m_data.m_value.array->empty(); - case value_t::object: - return v.m_data.m_value.object->empty(); - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - return true; + return v.m_data.m_value.array->empty(); } + if (v.m_data.m_type == value_t::object) + { + return v.m_data.m_value.object->empty(); + } + return true; + } + + // true if no child of the non-empty array/object v has children of + // its own, i.e. v can be freed in one go (its destructor then + // destroys scalars and empty containers only, so it recurses at most + // one level deep); stops at the first structured child it meets + static bool all_children_leaves(const basic_json& v) noexcept + { + // array_t only needs forward iterators (see custom_array_type) + if (v.m_data.m_type == value_t::array) + { + const auto& a = *v.m_data.m_value.array; + return std::all_of(a.begin(), a.end(), [](const basic_json & el) + { + return has_no_children(el); + }); + } + const auto& o = *v.m_data.m_value.object; + return std::all_of(o.begin(), o.end(), [](const typename object_t::value_type & el) + { + return has_no_children(el.second); + }); } // The walk in destroy_container() may take the children of a @@ -28483,15 +36616,16 @@ private: } } - // deallocates the (already empty) array/object held by v; this is - // the same allocator-based free the old recursive implementation - // used, just factored out so every level of the walk in destroy() - // can share it + // deallocates the array/object held by v, which is either empty or + // holds only scalars and empty containers (so its destructor + // recurses at most one level deep); this is the same allocator-based + // free the old recursive implementation used, just factored out so + // every level of the walk in destroy() can share it static void free_container(basic_json& v) noexcept { + JSON_ASSERT(has_no_children(v) || all_children_leaves(v)); if (v.m_data.m_type == value_t::array) { - JSON_ASSERT(v.m_data.m_value.array->empty()); AllocatorType alloc; std::allocator_traits::destroy(alloc, v.m_data.m_value.array); std::allocator_traits::deallocate(alloc, v.m_data.m_value.array, 1); @@ -28499,7 +36633,6 @@ private: else { JSON_ASSERT(v.m_data.m_type == value_t::object); - JSON_ASSERT(v.m_data.m_value.object->empty()); AllocatorType alloc; std::allocator_traits::destroy(alloc, v.m_data.m_value.object); std::allocator_traits::deallocate(alloc, v.m_data.m_value.object, 1); @@ -28508,28 +36641,17 @@ private: } public: - void destroy_string() noexcept + template + static void destroy_pointer(T*& ptr) noexcept { - if (string == nullptr) + if (ptr == nullptr) { // not initialized (e.g., due to exception in the ctor) return; } - AllocatorType alloc; - std::allocator_traits::destroy(alloc, string); - std::allocator_traits::deallocate(alloc, string, 1); - } - - void destroy_binary() noexcept - { - if (binary == nullptr) - { - // not initialized (e.g., due to exception in the ctor) - return; - } - AllocatorType alloc; - std::allocator_traits::destroy(alloc, binary); - std::allocator_traits::deallocate(alloc, binary, 1); + AllocatorType alloc; + std::allocator_traits::destroy(alloc, ptr); + std::allocator_traits::deallocate(alloc, ptr, 1); } // t must be value_t::array or value_t::object @@ -28544,40 +36666,99 @@ public: return; } - // Destroy the tree without recursing per nesting level and - // without any heap allocation: a heap-allocated flattening - // stack (the previous implementation) can itself throw - // bad_alloc, which would escape this noexcept destructor and - // terminate the program (#5135). - // - // Instead, walk down a chain of children (always the one - // walk_child() picks), reversing links as we go: cur is the - // container currently being emptied, and prev is its parent - // (value_t::null when there is none). Each parent's - // walk_child() slot doubles as storage for that parent's own - // parent link while we are below it, so no extra memory is - // needed; the parent is not modified meanwhile, so - // walk_child() finds that same slot again on the way up. We - // only ever remove a child once it is a scalar or an empty - // array/object, which neither allocates nor recurses more - // than one level deep. - // - // This json_value is not itself a basic_json, so the - // top-level container is first moved into a local stand-in - // ("cur"); a default-constructed basic_json has a null - // pointer in its m_value (see data::m_value's initializer), - // so swapping it with *this leaves this union's own pointer - // null, and it is never looked at or freed a second time. + // This json_value is not itself a basic_json, so the top-level + // container is first moved into a local stand-in ("cur"); a + // default-constructed basic_json has a null pointer in its + // m_value (see data::m_value's initializer), so swapping it + // with *this leaves this union's own pointer null, and it is + // never looked at or freed a second time. basic_json cur; cur.m_data.m_type = t; using std::swap; swap(cur.m_data.m_value, *this); + destroy_bounded(cur, 0); + } + + // Destroy the array/object v, whose children are destroyed first. + // The first recursion_depth_limit() levels are destroyed by plain + // recursion, which is the fastest way and cannot throw; anything + // nested deeper is handed to destroy_walk(), which uses no call stack. + static void destroy_bounded(basic_json& v, std::size_t depth) noexcept + { + if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) + { + destroy_walk(v); + return; + } + + if (v.m_data.m_type == value_t::array) + { + for (auto& child : *v.m_data.m_value.array) + { + if (child.m_data.m_type == value_t::array || child.m_data.m_type == value_t::object) + { + destroy_bounded(child, depth + 1); + } + } + } + else + { + for (auto& child : *v.m_data.m_value.object) + { + if (child.second.m_data.m_type == value_t::array || child.second.m_data.m_type == value_t::object) + { + destroy_bounded(child.second, depth + 1); + } + } + } + + // only leaves and already destroyed (null) children remain + free_container(v); + } + + // Destroy the tree below top without recursing per nesting level and + // without any heap allocation: a heap-allocated flattening + // stack (the previous implementation) can itself throw + // bad_alloc, which would escape this noexcept destructor and + // terminate the program (#5135). + // + // Instead, walk down a chain of children (always the one + // walk_child() picks), reversing links as we go: cur is the + // container currently being emptied, and prev is its parent + // (value_t::null when there is none). Each parent's + // walk_child() slot doubles as storage for that parent's own + // parent link while we are below it, so no extra memory is + // needed; the parent is not modified meanwhile, so + // walk_child() finds that same slot again on the way up. We + // only ever remove a child once it is a scalar or an empty + // array/object, which neither allocates nor recurses more + // than one level deep. + // + // This json_value is not itself a basic_json, so the + // top-level container is first moved into a local stand-in + // ("cur"); a default-constructed basic_json has a null + // pointer in its m_value (see data::m_value's initializer), + // so swapping it with *this leaves this union's own pointer + // null, and it is never looked at or freed a second time. + static void destroy_walk(basic_json& top) noexcept + { + basic_json cur; + take(cur, top); + basic_json prev; // value_t::null: no parent + // true right after cur was entered: its children have not been + // looked at yet. If none of them has children of its own, cur is + // freed in one go instead of child by child; this is tried once + // per container, so the walk stays linear. + bool fresh = true; + while (true) { - if (has_no_children(cur)) + const bool fresh_now = fresh; + fresh = false; + if (has_no_children(cur) || (fresh_now && all_children_leaves(cur))) { if (prev.m_data.m_type == value_t::null) { @@ -28616,19 +36797,31 @@ public: take(cur_child_ref, prev); take(prev, cur); take(cur, tmp); + fresh = true; } } + // keep the check whether t owns memory inline so destroying a scalar + // is a compare and a branch, and leave the actual work to an + // out-of-line function void destroy(value_t t) + { + if (JSON_HEDLEY_UNLIKELY(detail::owns_memory(t))) + { + destroy_owned(t); + } + } + + JSON_HEDLEY_NEVER_INLINE void destroy_owned(value_t t) { switch (t) { case value_t::string: - destroy_string(); + destroy_pointer(string); break; case value_t::binary: - destroy_binary(); + destroy_pointer(binary); break; case value_t::object: @@ -28690,36 +36883,19 @@ public: void set_parents() { #if JSON_DIAGNOSTICS - switch (m_data.m_type) + if (m_data.m_type == value_t::array) { - case value_t::array: + for (auto& element : *m_data.m_value.array) { - for (auto& element : *m_data.m_value.array) - { - element.m_parent = this; - } - break; + element.m_parent = this; } - - case value_t::object: + } + else if (m_data.m_type == value_t::object) + { + for (auto& element : *m_data.m_value.object) { - for (auto& element : *m_data.m_value.object) - { - element.second.m_parent = this; - } - break; + element.second.m_parent = this; } - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - break; } #endif } @@ -28787,11 +36963,16 @@ public: return next; } - /// @brief the capacity of the stored array, or unknown_size() - /// Only JSON_DIAGNOSTICS uses the value, to detect a reallocation that - /// would invalidate the parent pointers. Array types that do not have a - /// capacity() member function report unknown_size(), which is treated as - /// "the elements may have moved". + /// @brief the capacity of the stored array or object, or unknown_size() + /// Only JSON_DIAGNOSTICS uses the value, to detect an insertion that may + /// have moved the other elements and so invalidated their parent pointers. + /// unknown_size() means "the elements may have moved". Arrays only grow at + /// the end, so their capacity tells whether they reallocated; arrays + /// without a capacity() member report unknown_size(). For objects, only + /// types known to keep their elements in place (std::map and the other + /// node-based standard containers) report a fixed 0, and only + /// nlohmann::ordered_map, which appends members, reports its capacity; any + /// other object type (a sorted flat map, say) reports unknown_size(). #if JSON_DIAGNOSTICS template < typename A = array_t, detail::enable_if_t < detail::has_capacity::value, int > = 0 > std::size_t array_capacity() const noexcept @@ -28804,57 +36985,63 @@ public: { return detail::unknown_size(); } + + template < typename O = object_t, detail::enable_if_t < detail::has_stable_elements::value, int > = 0 > + std::size_t object_capacity() const noexcept + { + return 0; + } + + template < typename O = object_t, detail::enable_if_t < !detail::has_stable_elements::value && detail::is_nlohmann_ordered_map::value, int > = 0 > + std::size_t object_capacity() const noexcept + { + return m_data.m_value.object->capacity(); + } + + template < typename O = object_t, detail::enable_if_t < !detail::has_stable_elements::value && !detail::is_nlohmann_ordered_map::value, int > = 0 > + std::size_t object_capacity() const noexcept + { + return detail::unknown_size(); + } + + std::size_t container_capacity() const noexcept + { + return is_array() ? array_capacity() : object_capacity(); + } #else - static constexpr std::size_t array_capacity() noexcept + static constexpr std::size_t container_capacity() noexcept { return detail::unknown_size(); } #endif - /// @brief set the parent of a value that has just been added to an array - /// @param j the added value - /// @param old_capacity the value @ref array_capacity() returned before the + /// @brief set the parent of a value that has just been added to an array or object + /// @param j the added (or looked-up) value + /// @param old_capacity the value @ref container_capacity() returned before the /// insertion - reference set_parent_after_array_insert(reference j, std::size_t old_capacity) + reference set_parent_after_insert(reference j, std::size_t old_capacity) { #if JSON_DIAGNOSTICS // see https://github.com/nlohmann/json/issues/2838 - JSON_ASSERT(type() == value_t::array); if (JSON_HEDLEY_UNLIKELY(old_capacity == detail::unknown_size() - || array_capacity() != old_capacity)) + || container_capacity() != old_capacity)) { - // the capacity has changed, or the array type does not let us tell: - // the elements may have moved, so update all parents + // the capacity has changed, or the container type does not let us + // tell: the elements may have moved, so update all parents set_parents(); return j; } #else static_cast(old_capacity); #endif - return set_parent(j); + return set_parent_at(j); } - reference set_parent(reference j) + /// @brief set the parent of an element accessed via at(); never moves elements + reference set_parent_at(reference j) noexcept { #if JSON_DIAGNOSTICS - // ordered_json uses a vector internally, so pointers could have - // been invalidated; see https://github.com/nlohmann/json/issues/2962 -#ifdef JSON_HEDLEY_MSVC_VERSION -#pragma warning(push ) -#pragma warning(disable : 4127) // ignore warning to replace if with if constexpr -#endif - if (detail::is_ordered_map::value) - { - set_parents(); - return j; - } -#ifdef JSON_HEDLEY_MSVC_VERSION -#pragma warning( pop ) -#endif - j.m_parent = this; -#else - static_cast(j); #endif return j; } @@ -29134,6 +37321,24 @@ public: return create(first, last); } + /// @brief compare two object keys for equality, if the key type supports it + /// @note object_t only needs operator< for its keys (std::map), so operator== + /// may not exist; the keys are then reported as different, which makes + /// copy_object_level pair the values via object_t::find() + template::value, int> = 0> + static bool copy_keys_equal(const K& a, const K& b) + { + return a == b; + } + + template < typename K = typename object_t::key_type, + detail::enable_if_t < !detail::is_detected::value, int > = 0 > + static bool copy_keys_equal(const K& /*a*/, const K& /*b*/) + { + return false; + } + /// @brief create the copy of the object @a src in @a dst /// @note structured values are appended to @a worklist instead static void copy_object_level(const basic_json& src, basic_json& dst, @@ -29166,7 +37371,7 @@ public: auto src_it = src_object.cbegin(); for (auto& element : *dst.m_data.m_value.object) { - if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && src_it->first == element.first)) + if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && copy_keys_equal(src_it->first, element.first))) { copy_shallow(src_it->second, element.second, worklist); ++src_it; @@ -29826,9 +38031,11 @@ public: /// @brief restore the parent pointers after erasing from an object - /// ordered_json keeps its members in a vector, and erasing a member - /// re-constructs every member after it in place, which resets their - /// parent pointers + /// Unless the object type is known to keep its elements in place (see + /// detail::has_stable_elements), erasing a member may move the members + /// after it; ordered_json, for instance, keeps its members in a vector + /// and re-constructs every member after the erased one in place, which + /// resets their parent pointers void set_parents_after_object_erase() { #if JSON_DIAGNOSTICS @@ -29836,7 +38043,7 @@ public: #pragma warning(push ) #pragma warning(disable : 4127) // ignore warning to replace if with if constexpr #endif - if (detail::is_ordered_map::value) + if (!detail::has_stable_elements::value) { set_parents(); } @@ -29871,7 +38078,7 @@ public: /// @brief create an empty value with a given type /// @sa https://json.nlohmann.me/api/basic_json/basic_json/ basic_json(const value_t v) - : m_data(v) + : m_data(stored_kind(v)) { assert_invariant(); } @@ -29952,7 +38159,7 @@ public: { convert_leaf(val); } - JSON_ASSERT(m_data.m_type == val.type()); + JSON_ASSERT(m_data.m_type == stored_kind(val.type())); set_parents(); assert_invariant(); @@ -30000,7 +38207,7 @@ public: // if an object is wanted but impossible, throw an exception if (JSON_HEDLEY_UNLIKELY(manual_type == value_t::object && !is_an_object)) { - JSON_THROW(type_error::create(301, "cannot create object from initializer list", nullptr)); + detail::throw_literal(detail::exception_id::object_from_non_pairs, "cannot create object from initializer list", nullptr); } } @@ -30120,7 +38327,7 @@ public: // make sure the iterator fits the current value if (JSON_HEDLEY_UNLIKELY(first.m_object != last.m_object)) { - JSON_THROW(invalid_iterator::create(201, "iterators are not compatible", nullptr)); + detail::throw_literal(detail::exception_id::iterators_incompatible, "iterators are not compatible", nullptr); } // copy type from the first iterator @@ -30136,10 +38343,9 @@ public: case value_t::string: case value_t::binary: { - if (JSON_HEDLEY_UNLIKELY(!first.m_it.primitive_iterator.is_begin() - || !last.m_it.primitive_iterator.is_end())) + if (JSON_HEDLEY_UNLIKELY(!primitive_range_is_complete(first, last))) { - JSON_THROW(invalid_iterator::create(204, "iterators out of range", first.m_object)); + detail::throw_literal(detail::exception_id::iterator_range_out_of_range, "iterators out of range", first.m_object); } break; } @@ -30207,7 +38413,7 @@ public: case value_t::null: case value_t::discarded: default: - JSON_THROW(invalid_iterator::create(206, detail::concat("cannot construct with iterators from ", first.m_object->type_name()), first.m_object)); + throw_iterator_range_of_null(first.m_object); } set_parents(); @@ -30236,18 +38442,16 @@ public: // check of passed value is valid other.assert_invariant(); - if (m_data.m_type == value_t::object || m_data.m_type == value_t::array) + if (JSON_HEDLEY_LIKELY(!detail::owns_memory(m_data.m_type))) { - // copying the container directly would call this constructor again - // for every element, once per nesting level - copy_structured(other); + // null, boolean, numbers, discarded: plain copy of the union + m_data.m_value = other.m_data.m_value; } else { - copy_leaf_value(other, *this); + copy_owning(other); } - set_parents(); assert_invariant(); } @@ -30477,6 +38681,22 @@ public: // value access // ////////////////// + /// out-of-line part of the copy constructor for owning types + JSON_HEDLEY_NEVER_INLINE + void copy_owning(const basic_json& other) + { + if (m_data.m_type == value_t::object || m_data.m_type == value_t::array) + { + // copying the container directly would call this constructor again + // for every element, once per nesting level + copy_structured(other); + } + else + { + copy_leaf_value(other, *this); + } + } + /// get a pointer to the value (object) object_t* get_impl_ptr(object_t* /*unused*/) noexcept { @@ -30537,14 +38757,16 @@ public: return m_data.m_type == value_t::number_integer ? &m_data.m_value.number_integer : nullptr; } - /// get a pointer to the value (unsigned number) - number_unsigned_t* get_impl_ptr(number_unsigned_t* /*unused*/) noexcept + /// get a pointer to the value (unsigned number); takes a placeholder if + /// number_unsigned_t is number_integer_t (the integer overload handles it then) + number_unsigned_t* get_impl_ptr(detail::number_unsigned_overload_type* /*unused*/) noexcept { return is_number_unsigned() ? &m_data.m_value.number_unsigned : nullptr; } - /// get a pointer to the value (unsigned number) - constexpr const number_unsigned_t* get_impl_ptr(const number_unsigned_t* /*unused*/) const noexcept + /// get a pointer to the value (unsigned number); takes a placeholder if + /// number_unsigned_t is number_integer_t (the integer overload handles it then) + constexpr const number_unsigned_t* get_impl_ptr(const detail::number_unsigned_overload_type* /*unused*/) const noexcept { return is_number_unsigned() ? &m_data.m_value.number_unsigned : nullptr; } @@ -30573,6 +38795,175 @@ public: return is_binary() ? m_data.m_value.binary : nullptr; } + /// out-of-line throw helpers (message construction kept out of the hot paths) + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_iterator_range_of_null(const basic_json* obj) + { + static_cast(obj); // unused when JSON_NOEXCEPTION is defined + JSON_THROW(invalid_iterator::create(detail::exception_id::iterator_range_of_null, detail::concat("cannot construct with iterators from ", obj->type_name()), obj)); + } + + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_incompatible_reference_type(const basic_json& obj) + { + static_cast(obj); // unused when JSON_NOEXCEPTION is defined + JSON_THROW(type_error::create(detail::exception_id::incompatible_reference_type, detail::concat("incompatible ReferenceType for get_ref, actual type is ", obj.type_name()), &obj)); + } + + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_array_index_exceeds_size_type(size_type idx) + { + static_cast(idx); // unused when JSON_NOEXCEPTION is defined + JSON_THROW(std::length_error(detail::concat("array index ", std::to_string(idx), " exceeds size_type"))); + } + + /// @throw type_error.309 always, as insert() is not defined for the type of @a j + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_insert_wrong_type(const basic_json& j) + { + detail::throw_cannot_use_with(detail::exception_id::insert_wrong_type, "insert()", j); + } + + /// @throw invalid_iterator.202 always, as an iterator belongs to another value than @a obj + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_iterator_from_other_value(const basic_json* obj) + { + detail::throw_literal(detail::exception_id::iterator_from_other_value, "iterator does not fit current value", obj); + } + + /// @throw type_error.310 always, as swap() with the given operand is not defined for the type of @a j + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_swap_wrong_type(const char* operation, const basic_json& j) + { + detail::throw_cannot_use_with(detail::exception_id::swap_wrong_type, operation, j); + } + + /// exchange the stored @a Stored value (if the value has that type) with @a other + template + void swap_impl(Other& other, const char* operation) + { + Stored* const stored = get_impl_ptr(static_cast(nullptr)); + if (JSON_HEDLEY_UNLIKELY(stored == nullptr)) + { + throw_swap_wrong_type(operation, *this); + } + + using std::swap; + swap(*stored, other); + } + + /// whether [first, last) spans the whole of a primitive value + template + JSON_HEDLEY_ALWAYS_INLINE + static bool primitive_range_is_complete(const IteratorType& first, const IteratorType& last) noexcept + { + return first.m_it.primitive_iterator.is_begin() && last.m_it.primitive_iterator.is_end(); + } + + /// destroy the current (primitive) value and become null + void reset_to_null_impl() + { + m_data.m_value.destroy(m_data.m_type); + m_data.m_value = {}; + m_data.m_type = value_t::null; + assert_invariant(); + } + + /// append @a val (moved or copied) to the array; a null value becomes an array + template + JSON_HEDLEY_ALWAYS_INLINE + void push_back_array_impl(ValueType&& val) + { + // push_back only works for null objects or arrays + if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_array()))) + { + detail::throw_cannot_use_with(detail::exception_id::push_back_wrong_type, "push_back()", *this); + } + + // transform a null object into an array + if (is_null()) + { + convert_null_to(); + } + + // add the element to the array + const auto old_capacity = container_capacity(); + m_data.m_value.array->push_back(std::forward(val)); + set_parent_after_insert(m_data.m_value.array->back(), old_capacity); + } + + /// parse the input behind an already created adapter + template + static basic_json parse_impl(InputAdapterType&& adapter, + parser_callback_t cb, + const bool allow_exceptions, + const bool ignore_comments, + const bool ignore_trailing_commas) + { + basic_json result; + parser(std::forward(adapter), std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas).parse(true, result); + return result; + } + + /// check the input behind an already created adapter + template + static bool accept_impl(InputAdapterType&& adapter, + const bool ignore_comments, + const bool ignore_trailing_commas) + { + return parser(std::forward(adapter), nullptr, false, ignore_comments, ignore_trailing_commas, true).accept(true); + } + + /// generate SAX events from the input behind an already created adapter + template + static bool sax_parse_impl(InputAdapterType ia, SAX* sax, + const input_format_t format, + const bool strict, + const bool ignore_comments, + const bool ignore_trailing_commas, + const cbor_tag_handler_t tag_handler) + { + if (format == input_format_t::json) + { + return parser(std::move(ia), nullptr, true, ignore_comments, ignore_trailing_commas).sax_parse(sax, strict); + } + return detail::binary_reader(std::move(ia), format).sax_parse(sax, strict, tag_handler); + } + + /// serialize @a j into a new vector; @a write receives the vector-backed writer + template + static std::vector to_binary_vector(const basic_json& j, const error_handler_t error_handler, WriteFunction write) + { + std::vector result; + result.reserve(detail::binary_initial_capacity(j)); + { + // the sink sets the size of result when it is destroyed, which + // must happen before result is returned (without NRVO, it is moved) + auto writer = vector_writer(result, error_handler); + write(writer); + } + return result; + } + + /// @throw out_of_range.401 always, as @a idx is out of range of @a j + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_array_index_out_of_range(size_type idx, const basic_json& j) + { + static_cast(idx); // unused when JSON_NOEXCEPTION is defined + static_cast(j); + JSON_THROW(out_of_range::create(detail::exception_id::array_index_out_of_range, detail::concat("array index ", std::to_string(idx), " is out of range"), &j)); + } + + /// @throw out_of_range.403 always, as @a key is not found in @a j + template + JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE + static void throw_key_not_found(const KeyType& key, const basic_json& j) + { + static_cast(key); // unused when JSON_NOEXCEPTION is defined + static_cast(j); + JSON_THROW(out_of_range::create(detail::exception_id::key_not_found, detail::concat("key '", key, "' not found"), &j)); + } + /*! @brief helper function to implement get_ref() @@ -30595,7 +38986,7 @@ public: return *ptr; } - JSON_THROW(type_error::create(303, detail::concat("incompatible ReferenceType for get_ref, actual type is ", obj.type_name()), &obj)); + throw_incompatible_reference_type(obj); } public: @@ -30979,7 +39370,7 @@ public: { if (!is_binary()) { - JSON_THROW(type_error::create(302, detail::concat("type must be binary, but is ", type_name()), this)); + detail::throw_type_must_be("binary", *this); } return *get_ptr(); @@ -30991,7 +39382,7 @@ public: { if (!is_binary()) { - JSON_THROW(type_error::create(302, detail::concat("type must be binary, but is ", type_name()), this)); + detail::throw_type_must_be("binary", *this); } return *get_ptr(); @@ -31033,7 +39424,7 @@ public: // at only works for objects if (JSON_HEDLEY_UNLIKELY(!j.is_object())) { - JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", j.type_name()), &j)); + detail::throw_cannot_use_with(detail::exception_id::at_wrong_type, "at()", j); } auto it = object_lookup(j, std::forward(key)); @@ -31043,11 +39434,28 @@ public: // std::map or ordered_map) never moves from its argument, so key is still // valid here regardless of whether KeyType was deduced as an rvalue reference // NOLINTNEXTLINE(bugprone-use-after-move,hicpp-invalid-access-moved) - JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(key), "' not found"), &j)); + throw_key_not_found(key_for_message(key), j); } return it->second; } + /// @brief key as it is passed to detail::concat for an error message + /// @note keys with data() and size() (such as string_t itself or a string + /// view) are passed through unchanged, so a miss does not copy them; + /// other keys (such as string literals or key types that only convert + /// to string_t) are converted to string_t + template < typename KeyType, detail::enable_if_t < detail::detect_string_can_append_data::value, int > = 0 > + static const KeyType & key_for_message(const KeyType& key) + { + return key; // NOLINT(bugprone-return-const-ref-from-parameter): the result is only passed to concat() within the full-expression that holds key + } + + template < typename KeyType, detail::enable_if_t < !detail::detect_string_can_append_data::value, int > = 0 > + static string_t key_for_message(const KeyType& key) + { + return string_t(key); + } + /// @brief checked array element access used by the at() overloads taking an index /// @throw type_error.304 if @a j is not an array /// @throw out_of_range.401 if @a idx is out of range @@ -31058,12 +39466,12 @@ public: // at only works for arrays if (JSON_HEDLEY_UNLIKELY(!j.is_array())) { - JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", j.type_name()), &j)); + detail::throw_cannot_use_with(detail::exception_id::at_wrong_type, "at()", j); } if (JSON_HEDLEY_UNLIKELY(idx >= j.m_data.m_value.array->size())) { - JSON_THROW(out_of_range::create(401, detail::concat("array index ", std::to_string(idx), " is out of range"), &j)); + throw_array_index_out_of_range(idx, j); } return (*j.m_data.m_value.array)[idx]; @@ -31096,6 +39504,15 @@ public: m_data.m_type = value_t::object; } + /// @brief throws because operator[] is not supported for the type of this value + /// @param[in] argument the kind of the operator's argument, "numeric" or "string" + /// @throw type_error.305 always + JSON_HEDLEY_NO_RETURN void throw_subscript_wrong_type(const char* argument) const + { + detail::throw_cannot_use_with(detail::exception_id::subscript_wrong_type, + detail::concat("operator[] with a ", argument, " argument").c_str(), *this); + } + public: //////////////////// // element access // @@ -31109,7 +39526,7 @@ public: /// @sa https://json.nlohmann.me/api/basic_json/at/ reference at(size_type idx) { - return set_parent(array_at(*this, idx)); + return set_parent_at(array_at(*this, idx)); } /// @brief access specified array element with bounds checking @@ -31123,7 +39540,7 @@ public: /// @sa https://json.nlohmann.me/api/basic_json/at/ reference at(const typename object_t::key_type& key) { - return set_parent(object_at(*this, key)); + return set_parent_at(object_at(*this, key)); } /// @brief access specified object element with bounds checking @@ -31132,7 +39549,7 @@ public: detail::is_usable_as_basic_json_key_type::value, int> = 0> reference at(KeyType && key) { - return set_parent(object_at(*this, std::forward(key))); + return set_parent_at(object_at(*this, std::forward(key))); } /// @brief access specified object element with bounds checking @@ -31172,18 +39589,18 @@ public: // resize() rejects other indices that are too large to represent if (JSON_HEDLEY_UNLIKELY(idx == (std::numeric_limits::max)())) { - JSON_THROW(std::length_error(detail::concat("array index ", std::to_string(idx), " exceeds size_type"))); + throw_array_index_exceeds_size_type(idx); } #if JSON_DIAGNOSTICS // remember array size & capacity before resizing const auto old_size = m_data.m_value.array->size(); - const auto old_capacity = array_capacity(); + const auto old_capacity = container_capacity(); #endif m_data.m_value.array->resize(idx + 1); #if JSON_DIAGNOSTICS if (JSON_HEDLEY_UNLIKELY(old_capacity == detail::unknown_size() - || array_capacity() != old_capacity)) + || container_capacity() != old_capacity)) { // capacity has changed: update all parents set_parents(); @@ -31200,7 +39617,7 @@ public: return m_data.m_value.array->operator[](idx); } - JSON_THROW(type_error::create(305, detail::concat("cannot use operator[] with a numeric argument with ", type_name()), this)); + throw_subscript_wrong_type("numeric"); } /// @brief access specified array element @@ -31214,7 +39631,7 @@ public: return m_data.m_value.array->operator[](idx); } - JSON_THROW(type_error::create(305, detail::concat("cannot use operator[] with a numeric argument with ", type_name()), this)); + throw_subscript_wrong_type("numeric"); } /// @brief access specified object element @@ -31230,11 +39647,12 @@ public: // operator[] only works for objects if (JSON_HEDLEY_LIKELY(is_object())) { + const auto old_capacity = container_capacity(); auto result = m_data.m_value.object->emplace(std::move(key), nullptr); - return set_parent(result.first->second); + return set_parent_after_insert(result.first->second, old_capacity); } - JSON_THROW(type_error::create(305, detail::concat("cannot use operator[] with a string argument with ", type_name()), this)); + throw_subscript_wrong_type("string"); } /// @brief access specified object element @@ -31249,7 +39667,7 @@ public: return it->second; } - JSON_THROW(type_error::create(305, detail::concat("cannot use operator[] with a string argument with ", type_name()), this)); + throw_subscript_wrong_type("string"); } // these two functions resolve a (const) char * ambiguity affecting Clang and MSVC @@ -31281,11 +39699,12 @@ public: // operator[] only works for objects if (JSON_HEDLEY_LIKELY(is_object())) { + const auto old_capacity = container_capacity(); auto result = m_data.m_value.object->emplace(lookup_key(std::forward(key)), nullptr); - return set_parent(result.first->second); + return set_parent_after_insert(result.first->second, old_capacity); } - JSON_THROW(type_error::create(305, detail::concat("cannot use operator[] with a string argument with ", type_name()), this)); + throw_subscript_wrong_type("string"); } /// @brief access specified object element @@ -31302,7 +39721,7 @@ public: return it->second; } - JSON_THROW(type_error::create(305, detail::concat("cannot use operator[] with a string argument with ", type_name()), this)); + throw_subscript_wrong_type("string"); } private: @@ -31326,7 +39745,7 @@ public: // value only works for objects if (JSON_HEDLEY_UNLIKELY(!is_object())) { - JSON_THROW(type_error::create(306, detail::concat("cannot use value() with ", type_name()), this)); + detail::throw_cannot_use_with(detail::exception_id::value_wrong_type, "value()", *this); } const auto it = find(std::forward(key)); @@ -31341,7 +39760,7 @@ public: // value only works for arrays and objects if (JSON_HEDLEY_UNLIKELY(!is_structured())) { - JSON_THROW(type_error::create(306, detail::concat("cannot use value() with ", type_name()), this)); + detail::throw_cannot_use_with(detail::exception_id::value_wrong_type, "value()", *this); } return ptr.get_checked_or_null(this); @@ -31508,7 +39927,7 @@ public: // make sure the iterator fits the current value if (JSON_HEDLEY_UNLIKELY(this != pos.m_object)) { - JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value", this)); + throw_iterator_from_other_value(this); } IteratorType result = end(); @@ -31524,13 +39943,10 @@ public: { if (JSON_HEDLEY_UNLIKELY(!pos.m_it.primitive_iterator.is_begin())) { - JSON_THROW(invalid_iterator::create(205, "iterator out of range", this)); + detail::throw_literal(detail::exception_id::iterator_out_of_range, "iterator out of range", this); } - m_data.m_value.destroy(m_data.m_type); - m_data.m_value = {}; - m_data.m_type = value_t::null; - assert_invariant(); + reset_to_null_impl(); break; } @@ -31550,7 +39966,7 @@ public: case value_t::null: case value_t::discarded: default: - JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this)); + detail::throw_cannot_use_with(detail::exception_id::erase_wrong_type, "erase()", *this); } return result; @@ -31566,7 +39982,7 @@ public: // make sure the iterator fits the current value if (JSON_HEDLEY_UNLIKELY(this != first.m_object || this != last.m_object)) { - JSON_THROW(invalid_iterator::create(203, "iterators do not fit current value", this)); + detail::throw_literal(detail::exception_id::iterator_range_from_other_value, "iterators do not fit current value", this); } IteratorType result = end(); @@ -31580,16 +39996,12 @@ public: case value_t::string: case value_t::binary: { - if (JSON_HEDLEY_UNLIKELY(!first.m_it.primitive_iterator.is_begin() - || !last.m_it.primitive_iterator.is_end())) + if (JSON_HEDLEY_UNLIKELY(!primitive_range_is_complete(first, last))) { - JSON_THROW(invalid_iterator::create(204, "iterators out of range", this)); + detail::throw_literal(detail::exception_id::iterator_range_out_of_range, "iterators out of range", this); } - m_data.m_value.destroy(m_data.m_type); - m_data.m_value = {}; - m_data.m_type = value_t::null; - assert_invariant(); + reset_to_null_impl(); break; } @@ -31611,7 +40023,7 @@ public: case value_t::null: case value_t::discarded: default: - JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this)); + detail::throw_cannot_use_with(detail::exception_id::erase_wrong_type, "erase()", *this); } return result; @@ -31625,7 +40037,7 @@ public: // this erase only works for objects if (JSON_HEDLEY_UNLIKELY(!is_object())) { - JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this)); + detail::throw_cannot_use_with(detail::exception_id::erase_wrong_type, "erase()", *this); } const auto erased = m_data.m_value.object->erase(std::forward(key)); @@ -31640,7 +40052,7 @@ public: // this erase only works for objects if (JSON_HEDLEY_UNLIKELY(!is_object())) { - JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this)); + detail::throw_cannot_use_with(detail::exception_id::erase_wrong_type, "erase()", *this); } const auto it = object_lookup(*this, std::forward(key)); @@ -31682,14 +40094,14 @@ public: { if (JSON_HEDLEY_UNLIKELY(idx >= size())) { - JSON_THROW(out_of_range::create(401, detail::concat("array index ", std::to_string(idx), " is out of range"), this)); + throw_array_index_out_of_range(idx, *this); } m_data.m_value.array->erase(m_data.m_value.array->begin() + static_cast(idx)); } else { - JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this)); + detail::throw_cannot_use_with(detail::exception_id::erase_wrong_type, "erase()", *this); } } @@ -32070,34 +40482,20 @@ public: JSON_HEDLEY_WARN_UNUSED_RESULT size_type max_size() const noexcept { - switch (m_data.m_type) + if (m_data.m_type == value_t::array) { - case value_t::array: - { - // delegate call to array_t::max_size() - return m_data.m_value.array->max_size(); - } - - case value_t::object: - { - // delegate call to object_t::max_size() - return m_data.m_value.object->max_size(); - } - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - { - // all other types have max_size() == size() - return size(); - } + // delegate call to array_t::max_size() + return m_data.m_value.array->max_size(); } + + if (m_data.m_type == value_t::object) + { + // delegate call to object_t::max_size() + return m_data.m_value.object->max_size(); + } + + // all other types have max_size() == size() + return size(); } /// @} @@ -32175,22 +40573,7 @@ public: /// @sa https://json.nlohmann.me/api/basic_json/push_back/ void push_back(basic_json&& val) { - // push_back only works for null objects or arrays - if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_array()))) - { - JSON_THROW(type_error::create(308, detail::concat("cannot use push_back() with ", type_name()), this)); - } - - // transform a null object into an array - if (is_null()) - { - convert_null_to(); - } - - // add the element to the array (move semantics) - const auto old_capacity = array_capacity(); - m_data.m_value.array->push_back(std::move(val)); - set_parent_after_array_insert(m_data.m_value.array->back(), old_capacity); + push_back_array_impl(std::move(val)); // if val is moved from, basic_json move constructor marks it null, so we do not call the destructor } @@ -32206,22 +40589,7 @@ public: /// @sa https://json.nlohmann.me/api/basic_json/push_back/ void push_back(const basic_json& val) { - // push_back only works for null objects or arrays - if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_array()))) - { - JSON_THROW(type_error::create(308, detail::concat("cannot use push_back() with ", type_name()), this)); - } - - // transform a null object into an array - if (is_null()) - { - convert_null_to(); - } - - // add the element to the array - const auto old_capacity = array_capacity(); - m_data.m_value.array->push_back(val); - set_parent_after_array_insert(m_data.m_value.array->back(), old_capacity); + push_back_array_impl(val); } /// @brief add an object to an array @@ -32239,7 +40607,7 @@ public: // push_back only works for null objects or objects if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_object()))) { - JSON_THROW(type_error::create(308, detail::concat("cannot use push_back() with ", type_name()), this)); + detail::throw_cannot_use_with(detail::exception_id::push_back_wrong_type, "push_back()", *this); } // transform a null object into an object @@ -32249,8 +40617,9 @@ public: } // add the element to the object + const auto old_capacity = container_capacity(); auto res = m_data.m_value.object->insert(val); - set_parent(res.first->second); + set_parent_after_insert(res.first->second, old_capacity); } /// @brief add an object to an object @@ -32293,7 +40662,7 @@ public: // emplace_back only works for null objects or arrays if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_array()))) { - JSON_THROW(type_error::create(311, detail::concat("cannot use emplace_back() with ", type_name()), this)); + detail::throw_cannot_use_with(detail::exception_id::emplace_wrong_type, "emplace_back()", *this); } // transform a null object into an array @@ -32303,9 +40672,9 @@ public: } // add the element to the array (perfect forwarding) - const auto old_capacity = array_capacity(); + const auto old_capacity = container_capacity(); m_data.m_value.array->emplace_back(std::forward(args)...); - return set_parent_after_array_insert(m_data.m_value.array->back(), old_capacity); + return set_parent_after_insert(m_data.m_value.array->back(), old_capacity); } /// @brief add an object to an object if key does not exist @@ -32316,7 +40685,7 @@ public: // emplace only works for null objects or arrays if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_object()))) { - JSON_THROW(type_error::create(311, detail::concat("cannot use emplace() with ", type_name()), this)); + detail::throw_cannot_use_with(detail::exception_id::emplace_wrong_type, "emplace()", *this); } // transform a null object into an object @@ -32326,8 +40695,9 @@ public: } // add the element to the array (perfect forwarding) + const auto old_capacity = container_capacity(); auto res = m_data.m_value.object->emplace(std::forward(args)...); - set_parent(res.first->second); + set_parent_after_insert(res.first->second, old_capacity); // create a result iterator and set iterator to the result of emplace auto it = begin(); @@ -32363,19 +40733,19 @@ public: iterator insert(const_iterator pos, const basic_json& val) // NOLINT(performance-unnecessary-value-param) { // insert only works for arrays - if (JSON_HEDLEY_LIKELY(is_array())) + if (JSON_HEDLEY_UNLIKELY(!is_array())) { - // check if iterator pos fits to this JSON value - if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) - { - JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value", this)); - } - - // insert to array and return iterator - return insert_iterator(pos, val); + throw_insert_wrong_type(*this); } - JSON_THROW(type_error::create(309, detail::concat("cannot use insert() with ", type_name()), this)); + // check if iterator pos fits to this JSON value + if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) + { + throw_iterator_from_other_value(this); + } + + // insert to array and return iterator + return insert_iterator(pos, val); } /// @brief inserts element into array @@ -32383,21 +40753,21 @@ public: iterator insert(const_iterator pos, basic_json&& val) // NOLINT(performance-unnecessary-value-param) { // insert only works for arrays - if (JSON_HEDLEY_LIKELY(is_array())) + if (JSON_HEDLEY_UNLIKELY(!is_array())) { - // check if iterator pos fits to this JSON value - if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) - { - JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value", this)); - } - - // moving into a local first keeps this safe even if val aliases - // an element of this array - basic_json tmp(std::move(val)); - return insert_iterator(pos, std::move(tmp)); + throw_insert_wrong_type(*this); } - JSON_THROW(type_error::create(309, detail::concat("cannot use insert() with ", type_name()), this)); + // check if iterator pos fits to this JSON value + if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) + { + throw_iterator_from_other_value(this); + } + + // moving into a local first keeps this safe even if val aliases + // an element of this array + basic_json tmp(std::move(val)); + return insert_iterator(pos, std::move(tmp)); } /// @brief inserts copies of element into array @@ -32405,19 +40775,19 @@ public: iterator insert(const_iterator pos, size_type cnt, const basic_json& val) // NOLINT(performance-unnecessary-value-param) { // insert only works for arrays - if (JSON_HEDLEY_LIKELY(is_array())) + if (JSON_HEDLEY_UNLIKELY(!is_array())) { - // check if iterator pos fits to this JSON value - if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) - { - JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value", this)); - } - - // insert to array and return iterator - return insert_iterator(pos, cnt, val); + throw_insert_wrong_type(*this); } - JSON_THROW(type_error::create(309, detail::concat("cannot use insert() with ", type_name()), this)); + // check if iterator pos fits to this JSON value + if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) + { + throw_iterator_from_other_value(this); + } + + // insert to array and return iterator + return insert_iterator(pos, cnt, val); } /// @brief inserts range of elements into array @@ -32427,30 +40797,30 @@ public: // insert only works for arrays if (JSON_HEDLEY_UNLIKELY(!is_array())) { - JSON_THROW(type_error::create(309, detail::concat("cannot use insert() with ", type_name()), this)); + throw_insert_wrong_type(*this); } // check if iterator pos fits to this JSON value if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) { - JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value", this)); + throw_iterator_from_other_value(this); } // check if range iterators belong to the same JSON object if (JSON_HEDLEY_UNLIKELY(first.m_object != last.m_object)) { - JSON_THROW(invalid_iterator::create(210, "iterators do not fit", this)); + detail::throw_literal(detail::exception_id::insert_range_incompatible, "iterators do not fit", this); } if (JSON_HEDLEY_UNLIKELY(first.m_object == this)) { - JSON_THROW(invalid_iterator::create(211, "passed iterators may not belong to container", this)); + detail::throw_literal(detail::exception_id::insert_range_into_itself, "passed iterators may not belong to container", this); } // passed iterators must belong to arrays if (JSON_HEDLEY_UNLIKELY(!first.m_object->is_array())) { - JSON_THROW(invalid_iterator::create(202, "iterators first and last must point to arrays", this)); + detail::throw_literal(detail::exception_id::iterator_from_other_value, "iterators first and last must point to arrays", this); } // insert to array and return iterator @@ -32464,13 +40834,13 @@ public: // insert only works for arrays if (JSON_HEDLEY_UNLIKELY(!is_array())) { - JSON_THROW(type_error::create(309, detail::concat("cannot use insert() with ", type_name()), this)); + throw_insert_wrong_type(*this); } // check if iterator pos fits to this JSON value if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) { - JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value", this)); + throw_iterator_from_other_value(this); } // copy the values first: ilist may refer to elements of this array @@ -32492,19 +40862,19 @@ public: // insert only works for objects if (JSON_HEDLEY_UNLIKELY(!is_object())) { - JSON_THROW(type_error::create(309, detail::concat("cannot use insert() with ", type_name()), this)); + throw_insert_wrong_type(*this); } // check if range iterators belong to the same JSON object if (JSON_HEDLEY_UNLIKELY(first.m_object != last.m_object)) { - JSON_THROW(invalid_iterator::create(210, "iterators do not fit", this)); + detail::throw_literal(detail::exception_id::insert_range_incompatible, "iterators do not fit", this); } // passed iterators must belong to objects if (JSON_HEDLEY_UNLIKELY(!first.m_object->is_object())) { - JSON_THROW(invalid_iterator::create(202, "iterators first and last must point to objects", this)); + detail::throw_literal(detail::exception_id::iterator_from_other_value, "iterators first and last must point to objects", this); } m_data.m_value.object->insert(first.m_it.object_iterator, last.m_it.object_iterator); @@ -32521,7 +40891,7 @@ public: // j, not the copy made below) if (JSON_HEDLEY_UNLIKELY(!j.is_object())) { - JSON_THROW(type_error::create(312, detail::concat("cannot use update() with ", j.type_name()), &j)); + detail::throw_cannot_use_with(detail::exception_id::update_wrong_type, "update()", j); } // copy first: j may be *this or one of its descendants, and is @@ -32539,13 +40909,13 @@ public: // check if range iterators belong to the same JSON object if (JSON_HEDLEY_UNLIKELY(first.m_object != last.m_object)) { - JSON_THROW(invalid_iterator::create(210, "iterators do not fit", this)); + detail::throw_literal(detail::exception_id::insert_range_incompatible, "iterators do not fit", this); } // passed iterators must belong to objects if (JSON_HEDLEY_UNLIKELY(!first.m_object->is_object())) { - JSON_THROW(type_error::create(312, detail::concat("cannot use update() with ", first.m_object->type_name()), first.m_object)); + detail::throw_cannot_use_with(detail::exception_id::update_wrong_type, "update()", *first.m_object); } // copy first: the range may belong to *this or one of its @@ -32581,7 +40951,7 @@ public: if (JSON_HEDLEY_UNLIKELY(!is_object())) { - JSON_THROW(type_error::create(312, detail::concat("cannot use update() with ", type_name()), this)); + detail::throw_cannot_use_with(detail::exception_id::update_wrong_type, "update()", *this); } } @@ -32626,9 +40996,9 @@ public: continue; } } - // set_parent() also repairs the other members, which ordered_json - // relocates when adding a key makes its vector grow - set_parent(m_data.m_value.object->operator[](it.key()) = std::move(it.value())); + // operator[] repairs the other members if the object type moves + // them; assigning through it keeps this O(1) per key + operator[](it.key()) = std::move(it.value()); } } @@ -32678,9 +41048,8 @@ public: continue; } } - // set_parent() also repairs the other members, which ordered_json - // relocates when adding a key makes its vector grow - target->set_parent(target->m_data.m_value.object->operator[](first.key()) = std::move(first.value())); + // operator[] repairs the other members if the object type moves them + target->operator[](first.key()) = std::move(first.value()); ++first; } } @@ -32735,82 +41104,37 @@ public: /// @sa https://json.nlohmann.me/api/basic_json/swap/ void swap(array_t& other) // NOLINT(bugprone-exception-escape,cppcoreguidelines-noexcept-swap,performance-noexcept-swap) { - // swap only works for arrays - if (JSON_HEDLEY_LIKELY(is_array())) - { - using std::swap; - swap(*(m_data.m_value.array), other); - set_parents(); - } - else - { - JSON_THROW(type_error::create(310, detail::concat("cannot use swap(array_t&) with ", type_name()), this)); - } + swap_impl(other, "swap(array_t&)"); + set_parents(); } /// @brief exchanges the values /// @sa https://json.nlohmann.me/api/basic_json/swap/ void swap(object_t& other) // NOLINT(bugprone-exception-escape,cppcoreguidelines-noexcept-swap,performance-noexcept-swap) { - // swap only works for objects - if (JSON_HEDLEY_LIKELY(is_object())) - { - using std::swap; - swap(*(m_data.m_value.object), other); - set_parents(); - } - else - { - JSON_THROW(type_error::create(310, detail::concat("cannot use swap(object_t&) with ", type_name()), this)); - } + swap_impl(other, "swap(object_t&)"); + set_parents(); } /// @brief exchanges the values /// @sa https://json.nlohmann.me/api/basic_json/swap/ void swap(string_t& other) // NOLINT(bugprone-exception-escape,cppcoreguidelines-noexcept-swap,performance-noexcept-swap) { - // swap only works for strings - if (JSON_HEDLEY_LIKELY(is_string())) - { - using std::swap; - swap(*(m_data.m_value.string), other); - } - else - { - JSON_THROW(type_error::create(310, detail::concat("cannot use swap(string_t&) with ", type_name()), this)); - } + swap_impl(other, "swap(string_t&)"); } /// @brief exchanges the values /// @sa https://json.nlohmann.me/api/basic_json/swap/ void swap(binary_t& other) // NOLINT(bugprone-exception-escape,cppcoreguidelines-noexcept-swap,performance-noexcept-swap) { - // swap only works for binary values - if (JSON_HEDLEY_LIKELY(is_binary())) - { - using std::swap; - swap(*(m_data.m_value.binary), other); - } - else - { - JSON_THROW(type_error::create(310, detail::concat("cannot use swap(binary_t&) with ", type_name()), this)); - } + swap_impl(other, "swap(binary_t&)"); } /// @brief exchanges the values /// @sa https://json.nlohmann.me/api/basic_json/swap/ void swap(typename binary_t::container_type& other) // NOLINT(bugprone-exception-escape) { - // swap only works for binary values - if (JSON_HEDLEY_LIKELY(is_binary())) - { - using std::swap; - swap(*(m_data.m_value.binary), other); - } - else - { - JSON_THROW(type_error::create(310, detail::concat("cannot use swap(binary_t::container_type&) with ", type_name()), this)); - } + swap_impl(other, "swap(binary_t::container_type&)"); } /// @} @@ -32822,7 +41146,22 @@ public: /// @name lexicographical comparison operators /// @{ - // note parentheses around operands are necessary; see + // the same-type cases for non-owning scalars, shared by JSON_IMPLEMENT_OPERATOR + // and JSON_IMPLEMENT_SCALAR_FAST +#define JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \ + case value_t::null: \ + return (null_result); \ + case value_t::boolean: \ + return lhs.m_data.m_value.boolean op rhs.m_data.m_value.boolean; \ + case value_t::number_integer: \ + return lhs.m_data.m_value.number_integer op rhs.m_data.m_value.number_integer; \ + case value_t::number_unsigned: \ + return lhs.m_data.m_value.number_unsigned op rhs.m_data.m_value.number_unsigned; \ + case value_t::number_float: \ + return lhs.m_data.m_value.number_float op rhs.m_data.m_value.number_float; + + // note parentheses around the container operands are necessary, because + // GCC otherwise parses `array <` as std::array; see // https://github.com/nlohmann/json/issues/1530 // Mixed signed/unsigned integer comparisons check whether the signed value // is negative before casting. If it is, the comparison is performed with @@ -32858,24 +41197,11 @@ public: return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \ } \ \ - case value_t::null: \ - return (null_result); \ - \ + JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \ + \ case value_t::string: \ return (*lhs.m_data.m_value.string) op (*rhs.m_data.m_value.string); \ \ - case value_t::boolean: \ - return (lhs.m_data.m_value.boolean) op (rhs.m_data.m_value.boolean); \ - \ - case value_t::number_integer: \ - return (lhs.m_data.m_value.number_integer) op (rhs.m_data.m_value.number_integer); \ - \ - case value_t::number_unsigned: \ - return (lhs.m_data.m_value.number_unsigned) op (rhs.m_data.m_value.number_unsigned); \ - \ - case value_t::number_float: \ - return (lhs.m_data.m_value.number_float) op (rhs.m_data.m_value.number_float); \ - \ case value_t::binary: \ return (*lhs.m_data.m_value.binary) op (*rhs.m_data.m_value.binary); \ \ @@ -32919,6 +41245,24 @@ public: \ return (default_result); + // inline fast path for two values of the same non-owning scalar type; anything + // else falls through to the out-of-line general comparison +#define JSON_IMPLEMENT_SCALAR_FAST(op, null_result) \ + if (lhs.m_data.m_type == rhs.m_data.m_type) /* NOLINT(readability/braces) */ \ + { \ + switch (lhs.m_data.m_type) \ + { \ + JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \ + case value_t::object: \ + case value_t::array: \ + case value_t::string: \ + case value_t::binary: \ + case value_t::discarded: \ + default: \ + break; \ + } \ + } + JSON_PRIVATE_UNLESS_TESTED: // returns true if: // - any operand is NaN and the other operand is of number type @@ -32946,6 +41290,173 @@ public: return compares_unordered(*this, rhs, inverse); } + /// @brief whether objects and arrays are the standard containers, whose + /// elements equal_bounded() may therefore compare itself + using standard_object_t = std::map>>; + using standard_array_t = std::vector>; + using equal_bounded_containers = detail::conjunction, std::is_same>; + + // out-of-line general comparisons; the operators below handle same-type + // scalars inline and fall through to these + JSON_HEDLEY_NEVER_INLINE + static bool equal_general(const_reference lhs, const_reference rhs) noexcept + { +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_PUSH + JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") +#endif + JSON_IMPLEMENT_OPERATOR( ==, true, false, false, + compare_iteratively(lhs, rhs, false) == compare_result::equal, true) +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_POP +#endif + } + +#if JSON_HAS_THREE_WAY_COMPARISON + JSON_HEDLEY_NEVER_INLINE + static std::partial_ordering three_way_general(const_reference lhs, const_reference rhs) noexcept // *NOPAD* + { + // default_result is used if we cannot compare values. In that case, + // we compare types. + JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD* + std::partial_ordering::equivalent, + std::partial_ordering::unordered, + lhs_type <=> rhs_type, // *NOPAD* + to_partial_ordering(compare_iteratively(lhs, rhs, false)), true) + } +#else + JSON_HEDLEY_NEVER_INLINE + static bool less_general(const_reference lhs, const_reference rhs) noexcept + { + // default_result is used if we cannot compare values. In that case, + // we compare types. Note we have to call the operator explicitly, + // because MSVC has problems otherwise. + JSON_IMPLEMENT_OPERATOR( <, false, false, operator<(lhs_type, rhs_type), + compare_iteratively(lhs, rhs, true) == compare_result::less, false) + } +#endif + + /// @brief compare two leaves of the same type inside equal_structured() + /// @return 1 if they are equal, 0 if they are not, and -1 if the values are + /// of different types or are not handled here, which this leaves to + /// equal_structured() + /// @note Unlike JSON_IMPLEMENT_SCALAR_FAST, this also compares strings: + /// they are the most common leaves in a document, so comparing them + /// inline pays off in the container walk, while the operators keep + /// their inlined code at each call site small. Binary values are + /// rare and left to equal_general(). + JSON_HEDLEY_ALWAYS_INLINE + static int equal_leaf(const_reference lhs, const_reference rhs) noexcept + { +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_PUSH + JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") +#endif + if (lhs.m_data.m_type == rhs.m_data.m_type) + { + switch (lhs.m_data.m_type) + { + case value_t::null: + return 1; + case value_t::boolean: + return lhs.m_data.m_value.boolean == rhs.m_data.m_value.boolean; + case value_t::number_integer: + return lhs.m_data.m_value.number_integer == rhs.m_data.m_value.number_integer; + case value_t::number_unsigned: + return lhs.m_data.m_value.number_unsigned == rhs.m_data.m_value.number_unsigned; + case value_t::number_float: + return lhs.m_data.m_value.number_float == rhs.m_data.m_value.number_float; + case value_t::string: + return *lhs.m_data.m_value.string == *rhs.m_data.m_value.string; + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::discarded: + default: + break; + } + } +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_POP +#endif + return -1; + } + + /// @brief compare for equality, walking objects and arrays here instead of + /// in their containers so that the depth is a parameter and needs no + /// thread-local count; only used for the standard containers + static bool equal_bounded(const_reference lhs, const_reference rhs, std::size_t depth) noexcept + { + const int leaf = equal_leaf(lhs, rhs); + return leaf < 0 ? equal_structured(lhs, rhs, depth) : leaf != 0; + } + + static bool equal_structured(const_reference lhs, const_reference rhs, std::size_t depth) noexcept + { + const auto type = lhs.m_data.m_type; + if (type != rhs.m_data.m_type || (type != value_t::array && type != value_t::object)) + { + return equal_general(lhs, rhs); + } + + if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) + { + return compare_iteratively(lhs, rhs, false) == compare_result::equal; + } + + if (type == value_t::array) + { + const auto& a = *lhs.m_data.m_value.array; + const auto& b = *rhs.m_data.m_value.array; + if (a.size() != b.size()) + { + return false; + } + auto it_b = b.cbegin(); + for (auto it_a = a.cbegin(); it_a != a.cend(); ++it_a, ++it_b) + { + const int leaf = equal_leaf(*it_a, *it_b); + if (leaf < 0 ? !equal_structured(*it_a, *it_b, depth + 1) : leaf == 0) + { + return false; + } + } + return true; + } + + const auto& a = *lhs.m_data.m_value.object; + const auto& b = *rhs.m_data.m_value.object; + if (a.size() != b.size()) + { + return false; + } + auto it_b = b.cbegin(); + for (auto it_a = a.cbegin(); it_a != a.cend(); ++it_a, ++it_b) + { + if (!(it_a->first == it_b->first)) + { + return false; + } + const int leaf = equal_leaf(it_a->second, it_b->second); + if (leaf < 0 ? !equal_structured(it_a->second, it_b->second, depth + 1) : leaf == 0) + { + return false; + } + } + return true; + } + + static bool equal_dispatch(const_reference lhs, const_reference rhs, std::true_type /*standard containers*/) noexcept + { + return equal_bounded(lhs, rhs, 0); + } + + static bool equal_dispatch(const_reference lhs, const_reference rhs, std::false_type /*other containers*/) noexcept + { + return equal_general(lhs, rhs); + } + public: #if JSON_HAS_THREE_WAY_COMPARISON /// @brief comparison: equal @@ -32957,11 +41468,11 @@ public: JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") #endif const_reference lhs = *this; - JSON_IMPLEMENT_OPERATOR( ==, true, false, false, - compare_iteratively(lhs, rhs, false) == compare_result::equal, true) + JSON_IMPLEMENT_SCALAR_FAST( ==, true) #ifdef __GNUC__ JSON_HEDLEY_DIAGNOSTIC_POP #endif + return equal_dispatch(lhs, rhs, equal_bounded_containers {}); } /// @brief comparison: equal @@ -32978,13 +41489,8 @@ public: std::partial_ordering operator<=>(const_reference rhs) const noexcept // *NOPAD* { const_reference lhs = *this; - // default_result is used if we cannot compare values. In that case, - // we compare types. - JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD* - std::partial_ordering::equivalent, - std::partial_ordering::unordered, - lhs_type <=> rhs_type, // *NOPAD* - to_partial_ordering(compare_iteratively(lhs, rhs, false)), true) + JSON_IMPLEMENT_SCALAR_FAST(<=>, std::partial_ordering::equivalent) // *NOPAD* + return three_way_general(lhs, rhs); } /// @brief comparison: 3-way @@ -33072,11 +41578,11 @@ public: JSON_HEDLEY_DIAGNOSTIC_PUSH JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") #endif - JSON_IMPLEMENT_OPERATOR( ==, true, false, false, - compare_iteratively(lhs, rhs, false) == compare_result::equal, true) + JSON_IMPLEMENT_SCALAR_FAST( ==, true) #ifdef __GNUC__ JSON_HEDLEY_DIAGNOSTIC_POP #endif + return equal_dispatch(lhs, rhs, equal_bounded_containers {}); } // The friend comparisons with a scalar name the JSON type via decltype of @@ -33131,11 +41637,8 @@ public: /// @sa https://json.nlohmann.me/api/basic_json/operator_lt/ friend bool operator<(const_reference lhs, const_reference rhs) noexcept { - // default_result is used if we cannot compare values. In that case, - // we compare types. Note we have to call the operator explicitly, - // because MSVC has problems otherwise. - JSON_IMPLEMENT_OPERATOR( <, false, false, operator<(lhs_type, rhs_type), - compare_iteratively(lhs, rhs, true) == compare_result::less, false) + JSON_IMPLEMENT_SCALAR_FAST( <, false) + return less_general(lhs, rhs); } /// @brief comparison: less than @@ -33246,6 +41749,8 @@ public: #endif #undef JSON_IMPLEMENT_OPERATOR +#undef JSON_IMPLEMENT_SCALAR_FAST +#undef JSON_IMPLEMENT_SCALAR_CASES /// @} @@ -33310,10 +41815,7 @@ public: const bool ignore_comments = false, const bool ignore_trailing_commas = false) { - basic_json result; - auto p = parser(detail::input_adapter(std::forward(i)), std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas); - p.parse(true, result); - return result; + return parse_impl(detail::input_adapter(std::forward(i)), std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas); } /// @brief deserialize from a pair of character iterators (or an iterator+sentinel pair, C++20 ranges support) @@ -33328,10 +41830,7 @@ public: const bool ignore_comments = false, const bool ignore_trailing_commas = false) { - basic_json result; - auto p = parser(detail::input_adapter(std::move(first), std::move(last)), std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas); - p.parse(true, result); - return result; + return parse_impl(detail::input_adapter(std::move(first), std::move(last)), std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas); } JSON_HEDLEY_WARN_UNUSED_RESULT @@ -33345,10 +41844,7 @@ public: = delete; #else { - basic_json result; - auto p = parser(i.get(), std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas); - p.parse(true, result); - return result; + return parse_impl(i.get(), std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas); } #endif @@ -33360,7 +41856,7 @@ public: const bool ignore_comments = false, const bool ignore_trailing_commas = false) { - return parser(detail::input_adapter(std::forward(i)), nullptr, false, ignore_comments, ignore_trailing_commas, true).accept(true); + return accept_impl(detail::input_adapter(std::forward(i)), ignore_comments, ignore_trailing_commas); } /// @brief check if the input is valid JSON (iterator pair, or iterator+sentinel pair for C++20 ranges support) @@ -33372,7 +41868,7 @@ public: const bool ignore_comments = false, const bool ignore_trailing_commas = false) { - return parser(detail::input_adapter(std::move(first), std::move(last)), nullptr, false, ignore_comments, ignore_trailing_commas, true).accept(true); + return accept_impl(detail::input_adapter(std::move(first), std::move(last)), ignore_comments, ignore_trailing_commas); } JSON_HEDLEY_WARN_UNUSED_RESULT @@ -33384,7 +41880,7 @@ public: = delete; #else { - return parser(i.get(), nullptr, false, ignore_comments, ignore_trailing_commas, true).accept(true); + return accept_impl(i.get(), ignore_comments, ignore_trailing_commas); } #endif @@ -33399,10 +41895,7 @@ public: const bool ignore_trailing_commas = false, const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error) { - auto ia = detail::input_adapter(std::forward(i)); - return format == input_format_t::json - ? parser(std::move(ia), nullptr, true, ignore_comments, ignore_trailing_commas).sax_parse(sax, strict) - : detail::binary_reader(std::move(ia), format).sax_parse(sax, strict, tag_handler); + return sax_parse_impl(detail::input_adapter(std::forward(i)), sax, format, strict, ignore_comments, ignore_trailing_commas, tag_handler); } /// @brief generate SAX events (iterator pair, or iterator+sentinel pair for C++20 ranges support) @@ -33417,10 +41910,7 @@ public: const bool ignore_trailing_commas = false, const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error) { - auto ia = detail::input_adapter(std::move(first), std::move(last)); - return format == input_format_t::json - ? parser(std::move(ia), nullptr, true, ignore_comments, ignore_trailing_commas).sax_parse(sax, strict) - : detail::binary_reader(std::move(ia), format).sax_parse(sax, strict, tag_handler); + return sax_parse_impl(detail::input_adapter(std::move(first), std::move(last)), sax, format, strict, ignore_comments, ignore_trailing_commas, tag_handler); } /// @brief generate SAX events @@ -33454,12 +41944,7 @@ public: = delete; #else { - auto ia = i.get(); - return format == input_format_t::json - // NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg) - ? parser(std::move(ia), nullptr, true, ignore_comments, ignore_trailing_commas).sax_parse(sax, strict) - // NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg) - : detail::binary_reader(std::move(ia), format).sax_parse(sax, strict, tag_handler); + return sax_parse_impl(i.get(), sax, format, strict, ignore_comments, ignore_trailing_commas, tag_handler); } #endif #if defined(__clang__) @@ -33505,29 +41990,7 @@ public: JSON_HEDLEY_RETURNS_NON_NULL const char* type_name() const noexcept { - switch (m_data.m_type) - { - case value_t::null: - return "null"; - case value_t::object: - return "object"; - case value_t::array: - return "array"; - case value_t::string: - return "string"; - case value_t::boolean: - return "boolean"; - case value_t::binary: - return "binary"; - case value_t::discarded: - return "discarded"; - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - return "number"; - default: - return "invalid"; - } + return detail::value_type_name(m_data.m_type); } JSON_PRIVATE_UNLESS_TESTED: @@ -33596,7 +42059,7 @@ public: from_ubjson/from_bjdata/from_bon8/from_bson overloads Building the @ref binary_reader as a named local, rather than as a - temporary that @ref detail::json_sax_dom_parser::parse is called on in + temporary that @ref detail::json_sax_dom_binary_parser::parse is called on in the same expression, avoids a false-positive cppcheck accessMoved warning in each of the 16 callers. */ @@ -33607,7 +42070,7 @@ public: const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error) { basic_json result; - detail::json_sax_dom_parser sdp(result, allow_exceptions); + detail::json_sax_dom_binary_parser sdp(result, allow_exceptions); binary_reader reader(std::move(ia), format, error_handler); if (!reader.sax_parse(&sdp, strict, tag_handler)) { @@ -33629,10 +42092,10 @@ public: static std::vector to_cbor(const basic_json& j, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - std::vector result; - result.reserve(detail::binary_reserve_hint(j)); - vector_writer(result, error_handler).write_cbor(j); - return result; + return to_binary_vector(j, error_handler, [&](vector_binary_writer& w) + { + w.write_cbor(j); + }); } /// @brief create a CBOR serialization of a given JSON value @@ -33640,7 +42103,9 @@ public: static void to_cbor(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_cbor(j); + binary_writer writer(o, error_handler); + writer.write_cbor(j); + writer.flush(); } /// @brief create a CBOR serialization of a given JSON value @@ -33648,7 +42113,9 @@ public: static void to_cbor(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_cbor(j); + binary_writer writer(o, error_handler); + writer.write_cbor(j); + writer.flush(); } /// @brief create a MessagePack serialization of a given JSON value @@ -33656,10 +42123,10 @@ public: static std::vector to_msgpack(const basic_json& j, const error_handler_t error_handler = error_handler_t::keep) { - std::vector result; - result.reserve(detail::binary_reserve_hint(j)); - vector_writer(result, error_handler).write_msgpack(j); - return result; + return to_binary_vector(j, error_handler, [&](vector_binary_writer& w) + { + w.write_msgpack(j); + }); } /// @brief create a MessagePack serialization of a given JSON value @@ -33667,7 +42134,9 @@ public: static void to_msgpack(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = error_handler_t::keep) { - binary_writer(o, error_handler).write_msgpack(j); + binary_writer writer(o, error_handler); + writer.write_msgpack(j); + writer.flush(); } /// @brief create a MessagePack serialization of a given JSON value @@ -33675,7 +42144,9 @@ public: static void to_msgpack(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = error_handler_t::keep) { - binary_writer(o, error_handler).write_msgpack(j); + binary_writer writer(o, error_handler); + writer.write_msgpack(j); + writer.flush(); } /// @brief create a UBJSON serialization of a given JSON value @@ -33685,10 +42156,10 @@ public: const bool use_type = false, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - std::vector result; - result.reserve(detail::binary_reserve_hint(j)); - vector_writer(result, error_handler).write_ubjson(j, use_size, use_type); - return result; + return to_binary_vector(j, error_handler, [&](vector_binary_writer& w) + { + w.write_ubjson(j, use_size, use_type); + }); } /// @brief create a UBJSON serialization of a given JSON value @@ -33697,7 +42168,9 @@ public: const bool use_size = false, const bool use_type = false, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_ubjson(j, use_size, use_type); + binary_writer writer(o, error_handler); + writer.write_ubjson(j, use_size, use_type); + writer.flush(); } /// @brief create a UBJSON serialization of a given JSON value @@ -33706,7 +42179,9 @@ public: const bool use_size = false, const bool use_type = false, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_ubjson(j, use_size, use_type); + binary_writer writer(o, error_handler); + writer.write_ubjson(j, use_size, use_type); + writer.flush(); } /// @brief create a BJData serialization of a given JSON value @@ -33717,10 +42192,10 @@ public: const bjdata_version_t version = bjdata_version_t::draft2, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - std::vector result; - result.reserve(detail::binary_reserve_hint(j)); - vector_writer(result, error_handler).write_ubjson(j, use_size, use_type, true, true, version); - return result; + return to_binary_vector(j, error_handler, [&](vector_binary_writer& w) + { + w.write_ubjson(j, use_size, use_type, true, true, version); + }); } /// @brief create a BJData serialization of a given JSON value @@ -33730,7 +42205,9 @@ public: const bjdata_version_t version = bjdata_version_t::draft2, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_ubjson(j, use_size, use_type, true, true, version); + binary_writer writer(o, error_handler); + writer.write_ubjson(j, use_size, use_type, true, true, version); + writer.flush(); } /// @brief create a BJData serialization of a given JSON value @@ -33740,7 +42217,9 @@ public: const bjdata_version_t version = bjdata_version_t::draft2, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_ubjson(j, use_size, use_type, true, true, version); + binary_writer writer(o, error_handler); + writer.write_ubjson(j, use_size, use_type, true, true, version); + writer.flush(); } /// @brief create a BSON serialization of a given JSON value @@ -33748,10 +42227,10 @@ public: static std::vector to_bson(const basic_json& j, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - std::vector result; - result.reserve(detail::binary_reserve_hint(j)); - vector_writer(result, error_handler).write_bson(j); - return result; + return to_binary_vector(j, error_handler, [&](vector_binary_writer& w) + { + w.write_bson(j); + }); } /// @brief create a BSON serialization of a given JSON value @@ -33759,7 +42238,9 @@ public: static void to_bson(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_bson(j); + binary_writer writer(o, error_handler); + writer.write_bson(j); + writer.flush(); } /// @brief create a BSON serialization of a given JSON value @@ -33767,31 +42248,37 @@ public: static void to_bson(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_bson(j); + binary_writer writer(o, error_handler); + writer.write_bson(j); + writer.flush(); } /// @brief create a BON8 serialization of a given JSON value /// @sa https://json.nlohmann.me/api/basic_json/to_bon8/ static std::vector to_bon8(const basic_json& j) { - std::vector result; - result.reserve(detail::binary_reserve_hint(j)); - vector_writer(result).write_bon8(j); - return result; + return to_binary_vector(j, detail::binary_writer_default_error_handler(), [&](vector_binary_writer& w) + { + w.write_bon8(j); + }); } /// @brief create a BON8 serialization of a given JSON value /// @sa https://json.nlohmann.me/api/basic_json/to_bon8/ static void to_bon8(const basic_json& j, detail::output_adapter o) { - binary_writer(o).write_bon8(j); + binary_writer writer(o); + writer.write_bon8(j); + writer.flush(); } /// @brief create a BON8 serialization of a given JSON value /// @sa https://json.nlohmann.me/api/basic_json/to_bon8/ static void to_bon8(const basic_json& j, detail::output_adapter o) { - binary_writer(o).write_bon8(j); + binary_writer writer(o); + writer.write_bon8(j); + writer.flush(); } /// @brief create a JSON value from an input in CBOR format @@ -34187,6 +42674,72 @@ public: /// @name JSON Patch functions /// @{ + private: + /// the members of an operation of a JSON patch that are of interest + struct patch_members + { + basic_json* op = nullptr; + basic_json* path = nullptr; + basic_json* value = nullptr; + basic_json* from = nullptr; + }; + + /// @brief find the members "op", "path", "value", and "from" of the object @a val in one pass + static patch_members find_patch_members(const basic_json& val, std::true_type /*key_type is string_t*/) + { + patch_members members; + for (auto& member : *val.m_data.m_value.object) + { + const string_t& key = member.first; + switch (key.size()) + { + case 2: + if (key == "op") + { + members.op = &member.second; + } + break; + case 4: + if (key == "path") + { + members.path = &member.second; + } + else if (key == "from") + { + members.from = &member.second; + } + break; + case 5: + if (key == "value") + { + members.value = &member.second; + } + break; + default: + break; + } + } + return members; + } + + /// @brief find the members by lookup for key types other than string_t + static patch_members find_patch_members(const basic_json& val, std::false_type /*key_type is not string_t*/) + { + const auto find_member = [&val](const char* name) -> basic_json * + { + const auto it = val.m_data.m_value.object->find(string_t(name)); + return it != val.m_data.m_value.object->end() ? &it->second : nullptr; + }; + + patch_members members; + members.op = find_member("op"); + members.path = find_member("path"); + members.value = find_member("value"); + members.from = find_member("from"); + return members; + } + + public: /// @brief applies a JSON patch in-place without copying the object /// @sa https://json.nlohmann.me/api/basic_json/patch/ void patch_inplace(const basic_json& json_patch) @@ -34226,24 +42779,18 @@ public: }; // wrapper for "add" operation; add value at ptr - const auto operation_add = [&result](json_pointer & ptr, const basic_json & val) + const auto operation_add = [&result](json_pointer & ptr, basic_json val) { // adding to the root of the target document means replacing it if (ptr.empty()) { - result = val; + result = std::move(val); return; } - // make sure the top element of the pointer exists - json_pointer const top_pointer = ptr.top(); - if (top_pointer != ptr) - { - result.at(top_pointer); - } - - // get reference to the parent of the JSON pointer ptr - const auto last_path = ptr.back(); + // get reference to the parent of the JSON pointer ptr; a missing + // parent (or one of its ancestors) is reported by at() + auto last_path = std::move(ptr.reference_tokens.back()); ptr.pop_back(); // parent must exist when performing patch add per RFC6902 specs basic_json& parent = result.at(ptr); @@ -34254,7 +42801,7 @@ public: case value_t::object: { // use operator[] to add value - parent[last_path] = val; + parent[std::move(last_path)] = std::move(val); break; } @@ -34263,7 +42810,7 @@ public: if (last_path == "-") { // special case: append to back - parent.push_back(val); + parent.push_back(std::move(val)); } else { @@ -34271,11 +42818,11 @@ public: if (JSON_HEDLEY_UNLIKELY(idx > parent.size())) { // avoid undefined behavior - JSON_THROW(out_of_range::create(401, detail::concat("array index ", std::to_string(idx), " is out of range"), &parent)); + throw_array_index_out_of_range(idx, parent); } // default case: insert add offset - parent.insert(parent.begin() + static_cast(idx), val); + parent.insert(parent.begin() + static_cast(idx), std::move(val)); } break; } @@ -34290,7 +42837,7 @@ public: case value_t::binary: case value_t::discarded: default: - JSON_THROW(out_of_range::create(411, detail::concat("cannot add value: the JSON Patch 'add' target's parent is of type ", parent.type_name(), ", but must be an object or array"), &parent)); + detail::throw_patch_add_parent(parent); } }; @@ -34298,7 +42845,8 @@ public: const auto operation_remove = [this, & result](json_pointer & ptr) { // get reference to the parent of the JSON pointer ptr - const auto last_path = ptr.back(); + static_cast(ptr.back()); // throws out_of_range.405 if ptr is the root + const auto last_path = std::move(ptr.reference_tokens.back()); ptr.pop_back(); basic_json& parent = result.at(ptr); @@ -34313,7 +42861,7 @@ public: } else { - JSON_THROW(out_of_range::create(403, detail::concat("key '", last_path, "' not found"), this)); + throw_key_not_found(last_path, *this); } } else if (parent.is_array()) @@ -34325,7 +42873,7 @@ public: { // the parent of a "remove" target must be an object or array // (see #5396) - JSON_THROW(out_of_range::create(413, detail::concat("cannot remove value: the JSON Patch 'remove' target's parent is of type ", parent.type_name(), ", but must be an object or array"), &parent)); + detail::throw_patch_remove_parent(parent); } }; @@ -34356,57 +42904,190 @@ public: // type check: top level value must be an array if (JSON_HEDLEY_UNLIKELY(!json_patch.is_array())) { - JSON_THROW(parse_error::create(104, 0, "JSON patch must be an array of objects", &json_patch)); + detail::throw_patch_not_an_array(json_patch); } + // the usual patch operations "add", "remove", "replace" and "test" on + // a pointer without escapes that denotes existing array elements and + // object members (as "add" does for the parent of the new value), as + // a diff creates them: apply the operation and return true; return + // false, with nothing changed, for all others + string_t token; + typename json_pointer::template plain_steps steps; + const auto try_plain = [&result, &token, &steps](patch_operations operation, const string_t& path, const patch_members & members, const basic_json & val) + { + if (path.empty()) + { + return false; + } + + // a missing "value" is reported below + const basic_json* const value_ptr = members.value; + const basic_json& value = value_ptr != nullptr ? *value_ptr : val; + if (operation != patch_operations::remove && value_ptr == nullptr) + { + return false; + } + + switch (operation) + { + case patch_operations::replace: + { + basic_json* const target = json_pointer::resolve_plain(&result, path, path.size(), token, steps); + if (target == nullptr) + { + return false; + } + *target = value; + return true; + } + + case patch_operations::test: + { + const basic_json* const target = json_pointer::resolve_plain(&result, path, path.size(), token, steps); + // a failing test is reported below + return target != nullptr && *target == value; + } + + case patch_operations::add: + case patch_operations::remove: + { + // the parent of the pointer and its last reference token + std::size_t slash = path.size(); + while (slash > 0 && path[slash - 1] != '/') + { + --slash; + } + if (slash == 0) + { + return false; + } + --slash; + + basic_json* const parent = json_pointer::resolve_plain(&result, path, slash, token, steps); + if (parent == nullptr) + { + return false; + } + token.clear(); + token.append(path.data() + slash + 1, path.size() - slash - 1); + if (token.find_first_of('~') != string_t::npos) + { + return false; + } + + if (operation == patch_operations::add) + { + if (parent->is_object()) + { + (*parent)[token] = value; + return true; + } + if (parent->is_array()) + { + typename basic_json::size_type idx{}; + if (token == "-") + { + parent->push_back(value); + return true; + } + if (json_pointer::template parse_array_index(token, idx) == json_pointer::array_index_status::ok && idx <= parent->size()) + { + parent->insert(parent->begin() + static_cast(idx), value); + return true; + } + } + return false; + } + + if (parent->is_object()) + { + const auto it = parent->find(token); + if (it == parent->end()) + { + return false; + } + parent->erase(it); + return true; + } + if (parent->is_array()) + { + typename basic_json::size_type idx{}; + if (json_pointer::template parse_array_index(token, idx) == json_pointer::array_index_status::ok && idx < parent->size()) + { + parent->erase(idx); + return true; + } + } + return false; + } + + case patch_operations::move: + case patch_operations::copy: + case patch_operations::invalid: + default: + return false; + } + }; + + // the name of the first member to check + const string_t op_member = "op"; + // iterate and apply the operations for (const auto& val : json_patch) { // wrapper to get a value for an operation - const auto get_value = [&val](const string_t& op, - const string_t& member, + const auto get_value = [&val](basic_json * found, + const string_t& op, + const char* member, bool string_type) -> basic_json & { - // find value - auto it = val.m_data.m_value.object->find(member); - - // context-sensitive error message - const auto error_msg = (op == "op") ? "operation" : detail::concat("operation '", op, '\''); // NOLINT(bugprone-unused-local-non-trivial-variable) - // check if the desired value is present - if (JSON_HEDLEY_UNLIKELY(it == val.m_data.m_value.object->end())) + if (JSON_HEDLEY_UNLIKELY(found == nullptr)) { - // NOLINTNEXTLINE(performance-inefficient-string-concatenation) - JSON_THROW(parse_error::create(105, 0, detail::concat(error_msg, " must have member '", member, "'"), &val)); + detail::throw_patch_missing_member(op, string_t(member), false, val); } // check if the result is of type string - if (JSON_HEDLEY_UNLIKELY(string_type && !it->second.is_string())) + if (JSON_HEDLEY_UNLIKELY(string_type && !found->is_string())) { - // NOLINTNEXTLINE(performance-inefficient-string-concatenation) - JSON_THROW(parse_error::create(105, 0, detail::concat(error_msg, " must have string member '", member, "'"), &val)); + detail::throw_patch_missing_member(op, string_t(member), true, val); } // no error: return value - return it->second; + return *found; }; // type check: every element of the array must be an object if (JSON_HEDLEY_UNLIKELY(!val.is_object())) { - JSON_THROW(parse_error::create(104, 0, "JSON patch must be an array of objects", &val)); + detail::throw_patch_not_an_array(val); } + const patch_members members = find_patch_members(val, std::is_same {}); + // collect mandatory members - const auto op = get_value("op", "op", true).template get(); - const auto path = get_value(op, "path", true).template get(); + const string_t& op = *get_value(members.op, op_member, "op", true).m_data.m_value.string; + const string_t& path = *get_value(members.path, op, "path", true).m_data.m_value.string; + const patch_operations operation = get_op(op); + + // the usual operations on a plain pointer are applied without + // building the pointer's reference tokens; anything else (and + // every error) is handled below + if (try_plain(operation, path, members, val)) + { + continue; + } + + // the general operations may change any container + steps.reset(); json_pointer ptr(path); - switch (get_op(op)) + switch (operation) { case patch_operations::add: { - operation_add(ptr, get_value("add", "value", false)); + operation_add(ptr, get_value(members.value, "add", "value", false)); break; } @@ -34419,44 +43100,43 @@ public: case patch_operations::replace: { // the "path" location must exist - use at() - result.at(ptr) = get_value("replace", "value", false); + result.at(ptr) = get_value(members.value, "replace", "value", false); break; } case patch_operations::move: { - const auto from_path = get_value("move", "from", true).template get(); + const string_t& from_path = *get_value(members.from, "move", "from", true).m_data.m_value.string; json_pointer from_ptr(from_path); if (JSON_HEDLEY_UNLIKELY(is_proper_prefix(from_ptr, ptr))) { - JSON_THROW(out_of_range::create(414, detail::concat("cannot move value: 'from' path '", from_path, "' is a proper prefix of 'path' '", path, "'"), &result)); + detail::throw_patch_move_into_child(from_path, path, result); } // the "from" location must exist - use at() - basic_json const v = result.at(from_ptr); + // a root "from" is rejected by operation_remove below, which must leave result intact + basic_json v = from_ptr.empty() ? basic_json(result) : std::move(result.at(from_ptr)); // The move operation is functionally identical to a // "remove" operation on the "from" location, followed // immediately by an "add" operation at the target // location with the value that was just removed. operation_remove(from_ptr); - operation_add(ptr, v); + operation_add(ptr, std::move(v)); break; } case patch_operations::copy: { - const auto from_path = get_value("copy", "from", true).template get(); + const string_t& from_path = *get_value(members.from, "copy", "from", true).m_data.m_value.string; const json_pointer from_ptr(from_path); - // the "from" location must exist - use at() - basic_json const v = result.at(from_ptr); - // The copy is functionally identical to an "add" // operation at the target location using the value - // specified in the "from" member. - operation_add(ptr, v); + // specified in the "from" member; the "from" location + // must exist - use at() + operation_add(ptr, result.at(from_ptr)); break; } @@ -34467,7 +43147,7 @@ public: { // check if "value" matches the one at "path" // the "path" location must exist - use at() - success = (result.at(ptr) == get_value("test", "value", false)); + success = (result.at(ptr) == get_value(members.value, "test", "value", false)); } JSON_INTERNAL_CATCH (out_of_range&) { @@ -34477,7 +43157,7 @@ public: // throw an exception if the test fails if (JSON_HEDLEY_UNLIKELY(!success)) { - JSON_THROW(other_error::create(501, detail::concat("unsuccessful: ", val.dump()), &val)); + detail::throw_patch_test_failed(val); } break; @@ -34488,7 +43168,7 @@ public: { // op must be "add", "remove", "replace", "move", "copy", or // "test" - JSON_THROW(parse_error::create(105, 0, detail::concat("operation value '", op, "' is invalid"), &val)); + detail::throw_patch_invalid_operation(op, val); } } } @@ -34511,7 +43191,8 @@ public: { // the patch basic_json result(value_t::array); - diff_recursively(result, source, target, path, 0); + string_t buffer = path; + diff_recursively(result, source, target, buffer, 0); return result; } @@ -34542,12 +43223,11 @@ public: std::size_t index = 0; /// objects: the next member of source to look at const_iterator member{}; // NOLINT(readability-redundant-member-init) - /// objects: the keys common to both, in source's order - std::vector common_keys{}; // NOLINT(readability-redundant-member-init) - /// objects: the next entry of common_keys - std::size_t next_common = 0; - /// objects: the "add" operations for keys only target has - basic_json added_ops{}; // NOLINT(readability-redundant-member-init) + /// objects: for each member of source (in order), its value in + /// target, or nullptr if target has no such key; `index` is the next + std::vector target_values{}; // NOLINT(readability-redundant-member-init) + /// objects: the members only target has, in target's order + std::vector added{}; // NOLINT(readability-redundant-member-init) }; // The operations of a diff are built by the functions below rather than @@ -34556,31 +43236,130 @@ public: // function it appears in. In diff_recursively, which is on the call stack // once per nesting level, that made every level cost kilobytes of stack. + /// @brief append an operation @a op for @a path to @a result, with @a value if it is not null + static void diff_operation(basic_json& result, const char* op, const string_t& path, const basic_json* value) + { + // the keys are distinct and in the order of std::map; the members are + // added without looking them up + basic_json entry(value_t::object); + auto& members = *entry.m_data.m_value.object; + auto hint = members.end(); + json_pointer::add_flattened(members, hint, string_t("op"), string_t(op), detail::priority_tag<2> {}); + json_pointer::add_flattened(members, hint, string_t("path"), path, detail::priority_tag<2> {}); + if (value != nullptr) + { + json_pointer::add_flattened(members, hint, string_t("value"), *value, detail::priority_tag<2> {}); + } + entry.set_parents(); + result.push_back(std::move(entry)); + } + /// @brief append a "replace" operation for @a path with @a value to @a result static void diff_replace(basic_json& result, const string_t& path, const basic_json& value) { - result.push_back( - { - {"op", "replace"}, {"path", path}, {"value", value} - }); + diff_operation(result, "replace", path, &value); } /// @brief append a "remove" operation for @a path to @a result static void diff_remove(basic_json& result, const string_t& path) { - result.push_back(object( - { - {"op", "remove"}, {"path", path} - })); + diff_operation(result, "remove", path, nullptr); } /// @brief append an "add" operation for @a path with @a value to @a result static void diff_add(basic_json& result, const string_t& path, const basic_json& value) { - result.push_back( + diff_operation(result, "add", path, &value); + } + + /// @brief the values in the target of the members of a source object, for the + /// objects of up to 16 members without allocating + /// + /// reserve() must be called with the number of values that are added. + class diff_target_values + { + public: + diff_target_values() noexcept + : inline_items() // NOLINT(readability-redundant-member-init) + {} + diff_target_values(const diff_target_values&) = delete; + diff_target_values& operator=(const diff_target_values&) = delete; + diff_target_values(diff_target_values&&) = delete; + diff_target_values& operator=(diff_target_values&&) = delete; + ~diff_target_values() = default; + + void reserve(const std::size_t n) { - {"op", "add"}, {"path", path}, {"value", value} - }); + if (n > inline_size) + { + large.resize(n); + items = large.data(); + } + } + + void push_back(const basic_json* value) + { + items[count++] = value; + } + + std::size_t size() const noexcept + { + return count; + } + + const basic_json* operator[](const std::size_t i) const noexcept + { + return items[i]; + } + + private: + static constexpr std::size_t inline_size = 16; + /// the values of a small object, left uninitialized: they are written + /// before they are read (the user-provided constructor keeps the + /// member initialization, which -Weffc++ asks for, from zeroing them) + struct uninitialized_items : std::array + { + uninitialized_items() noexcept {} // NOLINT(modernize-use-equals-default,hicpp-use-equals-default,cppcoreguidelines-pro-type-member-init,hicpp-member-init): see above + }; + + std::size_t count = 0; + std::vector large{}; // NOLINT(readability-redundant-member-init) + uninitialized_items inline_items; + const basic_json** items = inline_items.data(); + }; + + /// @brief append "/" and the decimal representation of @a index to @a path + static void diff_append_index(string_t& path, std::size_t index) + { + path.push_back('/'); + json_pointer::append_index(path, index); + } + + /// @brief append "/" and the escaped @a key to @a path + static void diff_append_key(string_t& path, const string_t& key) + { + path.push_back('/'); + detail::escape_into(path, key); + } + + /// @brief the path of the member @a key of the value at @a path + static string_t diff_key_path(const string_t& path, const string_t& key) + { + string_t result; + result.reserve(path.size() + key.size() + 1); + result.append(path.data(), path.size()); + diff_append_key(result, key); + return result; + } + + /// @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) + { + string_t result; + result.reserve(path.size() + 8); + result.append(path.data(), path.size()); + diff_append_index(result, index); + return result; } /// @brief append the "remove" operations for the elements of array @@ -34593,13 +43372,27 @@ public: // in that order avoids the quadratic reinsertion done before for (std::size_t j = source.size(); j > index; --j) { - diff_remove(result, detail::concat(path, '/', detail::to_string(j - 1))); + diff_remove(result, diff_index_path(path, j - 1)); } // add other remaining elements - for (std::size_t i = source.size(); i < target.size(); ++i) + if (source.size() < target.size()) { - diff_add(result, detail::concat(path, "/-"), target[i]); + string_t tail_path = path; + tail_path.append("/-", 2); + for (std::size_t i = source.size(); i < target.size(); ++i) + { + diff_add(result, tail_path, target[i]); + } + } + } + + /// @brief appends the "add" operations for the members @a added to @a result + static void diff_added(basic_json& result, const std::vector& added, const string_t& path) + { + for (const auto& it : added) + { + diff_add(result, diff_key_path(path, it.key()), it.value()); } } @@ -34608,106 +43401,128 @@ public: If object_t does not keep its members in insertion order, or if the keys both objects have are in the same order in both, and the keys only - @a target has come after them, stores the keys common to both in - source's order in @a common_keys, stores the "add" operations for the keys - only @a target has in @a added_ops, and returns true: the caller then diffs - the objects member by member. Otherwise, appends operations that remove - every member of @a source and add every member of @a target to @a result, - and returns false. + @a target has come after them, stores in @a target_values, for each member + of source in order, its value in target (nullptr for a key target does not + have), stores the members only target has in @a added, and returns true: + the caller then diffs the objects member by member. Otherwise, appends + operations that remove every member of @a source and add every member of + @a target to @a result, and returns false. */ + template static bool diff_object_keys(basic_json& result, const basic_json& source, const basic_json& target, - const string_t& path, std::vector& common_keys, - basic_json& added_ops) + const string_t& path, TargetValues& target_values, + std::vector& added) { - // first pass: record, for every source key, whether it is - // common to both objects (in source's iteration order) or - // was deleted (i.e., in source but not in target) -- this is - // a by-product of the target.find() call already needed to - // tell the two cases apart, so it adds no extra lookups. The - // "remove" ops themselves are emitted later, interleaved - // with the per-key diffs in the caller's fast path, to match - // source's original iteration order (as the original, - // pre-reordering-aware implementation did) instead of - // grouping all removes before all per-key diffs. - std::vector common_keys_source_order; - for (auto it = source.cbegin(); it != source.cend(); ++it) + // first pass: one lookup per source key + // Objects of the same size usually have the same keys, in the same + // order: walk them together as long as that holds, which takes no lookups. + target_values.reserve(source.size()); + std::size_t common = 0; + bool together = source.size() == target.size(); + for (auto it = source.cbegin(), partner = target.cbegin(); it != source.cend(); ++it) { - if (target.find(it.key()) != target.end()) + if (together) { - common_keys_source_order.push_back(it.key()); + if (it.key() == partner.key()) + { + target_values.push_back(&partner.value()); + ++common; + ++partner; + continue; + } + together = false; } - } - // second pass: find keys that were added (i.e., in target but - // not in source), and record the keys common to both, in - // target's iteration order -- again a by-product of the - // source.find() call already needed to detect added keys. At - // the same time, determine whether every added key comes - // after every common key in target's order (a precondition - // for the fast path, which only ever appends new keys - // at the very end). Both are only needed for an object_t that - // keeps its members in insertion order, such as the one - // backing `ordered_json`; for any other object_t, the fast - // path is always taken and they are not computed. - // The patch ops for keys that were added (i.e., in target but not - // in source) are built here so the fast path can reuse - // them without a second source.find() per target key. Only - // used by the fast path -- the slow (reordering) path - // rebuilds "add" ops for every key itself. - std::vector common_keys_target_order; - bool new_keys_form_suffix = true; - bool seen_new_key = false; - for (auto it = target.cbegin(); it != target.cend(); ++it) - { - if (source.find(it.key()) == source.end()) + const auto found = target.find(it.key()); + if (found != target.cend()) { - seen_new_key = true; - diff_add(added_ops, detail::concat(path, '/', detail::escape(it.key())), it.value()); + target_values.push_back(&found.value()); + ++common; } else { -#ifdef JSON_HEDLEY_MSVC_VERSION -#pragma warning(push ) -#pragma warning(disable : 4127) // ignore warning to replace if with if constexpr -#endif - if (detail::is_ordered_map::value) - { - common_keys_target_order.push_back(it.key()); - if (seen_new_key) - { - new_keys_form_suffix = false; - } - } -#ifdef JSON_HEDLEY_MSVC_VERSION -#pragma warning( pop ) -#endif + target_values.push_back(nullptr); } } - // Only an object type that keeps its members in insertion - // order, such as nlohmann::ordered_map, can need reordering: - // patch() appends a new member at the end of such an object. - // Any other object type places its members itself - std::map - // in key order, a hash map in an order its operator== ignores - - // so a member-by-member diff always reproduces target there. + // the same keys in the same order + if (together) + { + return true; + } + + // second pass: find the keys only target has. An object type that + // keeps its members in insertion order, such as the one backing + // ordered_json, also needs the common keys to be in the same order + // in both objects, and the new keys to come after them, as patch() + // appends a new member at the end of such an object. Any other + // object type places its members itself, so a member-by-member diff + // always reproduces target there. #ifdef JSON_HEDLEY_MSVC_VERSION #pragma warning(push ) #pragma warning(disable : 4127) // ignore warning to replace if with if constexpr #endif - if (!detail::is_ordered_map::value - || (common_keys_source_order == common_keys_target_order && new_keys_form_suffix)) + bool fast_path = true; + if (detail::is_ordered_map::value) { - // fast path: order of common keys already matches (or the - // object_t's iteration order does not depend on - // insertion history), so a plain per-key diff is correct - // and minimal, as before - common_keys = std::move(common_keys_source_order); - return true; + // walk the common keys of source in lockstep with target: a key + // of target that is not the next one is either new or out of order + auto src = source.cbegin(); + std::size_t next = 0; + bool seen_new = false; + for (auto it = target.cbegin(); it != target.cend() && fast_path; ++it) + { + while (next < target_values.size() && target_values[next] == nullptr) + { + ++src; + ++next; + } + + if (next < target_values.size() && src.key() == it.key()) + { + // a common key after a new one: new keys are no suffix + fast_path = !seen_new; + ++src; + ++next; + } + else if (source.find(it.key()) == source.cend()) + { + seen_new = true; + added.push_back(it); + } + else + { + // a common key out of order + fast_path = false; + } + } + } + else if (target.size() != common) + { + for (auto it = target.cbegin(); it != target.cend(); ++it) + { + if (source.find(it.key()) == source.cend()) + { + added.push_back(it); + } + } } #ifdef JSON_HEDLEY_MSVC_VERSION #pragma warning( pop ) #endif + if (fast_path) + { + return true; + } + + // equal objects, whose members are in a different order, need no + // operations + if (source == target) + { + return false; + } + // slow path: the common keys are in a different relative // order in source and target (only possible for a // reorderable object_t like ordered_map). Building a @@ -34724,7 +43539,7 @@ public: // it moves it to the end, fixing its position. for (auto it = source.cbegin(); it != source.cend(); ++it) { - diff_remove(result, detail::concat(path, '/', detail::escape(it.key()))); + diff_remove(result, diff_key_path(path, it.key())); } // add every key that is either common (just removed @@ -34733,7 +43548,7 @@ public: // target exactly for (auto it = target.cbegin(); it != target.cend(); ++it) { - diff_add(result, detail::concat(path, '/', detail::escape(it.key())), it.value()); + diff_add(result, diff_key_path(path, it.key()), it.value()); } return false; } @@ -34742,6 +43557,9 @@ public: @brief @ref diff, for values at nesting level @a depth, appending the operations to @a result + @a path is the path of the values; it is a buffer that is extended and + shortened again below, and is the same when the function returns. + Diffing two arrays or objects calls this function again, once per nesting level, so values nested deeply enough used to exhaust the call stack and terminate the process. The descent is bounded here: once @ref @@ -34749,7 +43567,7 @@ public: diff_iteratively diffs what is left without the call stack. */ static void diff_recursively(basic_json& result, const basic_json& source, const basic_json& target, - const string_t& path, const std::size_t depth) + string_t& path, const std::size_t depth) { // if the values are the same, there is nothing to do if (source == target) @@ -34757,6 +43575,14 @@ public: return; } + diff_unequal(result, source, target, path, depth); + } + + /// @brief @ref diff_recursively for values known to be unequal; callers + /// compare first so they extend the path only for unequal ones + static void diff_unequal(basic_json& result, const basic_json& source, const basic_json& target, + string_t& path, const std::size_t depth) + { if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) { diff_iteratively(result, source, target, path); @@ -34770,75 +43596,68 @@ public: return; } - switch (source.type()) + if (detail::is_primitive(source.type())) { - case value_t::array: - { - // first pass: traverse common elements - std::size_t i = 0; - while (i < source.size() && i < target.size()) - { - // recursive call to compare array values at index i - diff_recursively(result, source[i], target[i], detail::concat(path, '/', detail::to_string(i)), depth + 1); - ++i; - } + // both primitive types: replace value + diff_replace(result, path, target); + return; + } - // We now reached the end of at least one array - // in a second pass, traverse the remaining elements - diff_array_tails(result, source, target, path, i); - break; + if (source.is_array()) + { + const std::size_t path_length = path.size(); + + // first pass: traverse common elements + std::size_t i = 0; + while (i < source.size() && i < target.size()) + { + // recursive call to compare array values at index i + if (!(source[i] == target[i])) + { + diff_append_index(path, i); + diff_unequal(result, source[i], target[i], path, depth + 1); + path.resize(path_length); + } + ++i; } - case value_t::object: + // We now reached the end of at least one array + // in a second pass, traverse the remaining elements + diff_array_tails(result, source, target, path, i); + } + else + { + diff_target_values target_values; + std::vector added; + if (diff_object_keys(result, source, target, path, target_values, added)) { - std::vector common_keys; - basic_json added_ops(value_t::array); - if (diff_object_keys(result, source, target, path, common_keys, added_ops)) + // fast path: target_values holds, for every member of + // source in order, its value in target -- so no further + // lookup is needed. Deleted keys are interleaved here + // too, in source's original order, to match the + // historical (pre-reordering-aware) output order. + const std::size_t path_length = path.size(); + std::size_t i = 0; + for (auto it = source.cbegin(); it != source.cend(); ++it, ++i) { - // fast path: common_keys is, by construction, the - // subsequence of source's keys that are common to both - // objects, in source's iteration order -- so it can be - // walked in lockstep with `source` using a cheap key - // comparison instead of another lookup. Deleted keys - // (those source keys not in common_keys) are interleaved - // here too, in source's original order, to match the - // historical (pre-reordering-aware) output order. - auto common_it = common_keys.cbegin(); - for (auto it = source.cbegin(); it != source.cend(); ++it) + if (target_values[i] != nullptr) { - if (common_it != common_keys.cend() && it.key() == *common_it) + if (!(it.value() == *target_values[i])) { - diff_recursively(result, it.value(), target[it.key()], detail::concat(path, '/', detail::escape(it.key())), depth + 1); - ++common_it; - } - else - { - // found a key that is not in target -> remove it - diff_remove(result, detail::concat(path, '/', detail::escape(it.key()))); + diff_append_key(path, it.key()); + diff_unequal(result, it.value(), *target_values[i], path, depth + 1); + path.resize(path_length); } } - - // append the "add" ops for brand-new keys collected by - // diff_object_keys -- no second source.find() per target - // key needed - result.insert(result.end(), added_ops.begin(), added_ops.end()); + else + { + // found a key that is not in target -> remove it + diff_remove(result, diff_key_path(path, it.key())); + } } - break; - } - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - { - // both primitive types: replace value - diff_replace(result, path, target); - break; + // append the "add" operations for brand-new keys + diff_added(result, added, path); } } } @@ -34884,46 +43703,31 @@ public: return; } - switch (s.type()) + if (detail::is_primitive(s.type())) { - case value_t::array: - { - stack.emplace_back(&s, &t, current_path.size()); - return; - } - - case value_t::object: - { - std::vector common_keys; - basic_json added_ops(value_t::array); - if (diff_object_keys(result, s, t, current_path, common_keys, added_ops)) - { - // fast path: the frame walks source in lockstep with - // common_keys, as diff_recursively does, and appends - // added_ops once all members are done - stack.emplace_back(&s, &t, current_path.size()); - stack.back().member = s.cbegin(); - stack.back().common_keys = std::move(common_keys); - stack.back().added_ops = std::move(added_ops); - } - return; - } - - case value_t::null: - case value_t::string: - case value_t::boolean: - case value_t::number_integer: - case value_t::number_unsigned: - case value_t::number_float: - case value_t::binary: - case value_t::discarded: - default: - { - // both primitive types: replace value - diff_replace(result, current_path, t); - return; - } + // both primitive types: replace value + diff_replace(result, current_path, t); + return; } + + if (s.is_array()) + { + stack.emplace_back(&s, &t, current_path.size()); + return; + } + std::vector target_values; + std::vector added; + if (diff_object_keys(result, s, t, current_path, target_values, added)) + { + // fast path: the frame walks source in lockstep with + // common_keys, as diff_recursively does, and appends + // added_ops once all members are done + stack.emplace_back(&s, &t, current_path.size()); + stack.back().member = s.cbegin(); + stack.back().target_values = std::move(target_values); + stack.back().added = std::move(added); + } + return; }; enter(source, target); @@ -34947,7 +43751,7 @@ public: if (i < source_array.size() && i < target_array.size()) { ++stack.back().index; - detail::concat_into(current_path, '/', detail::to_string(i)); + diff_append_index(current_path, i); enter(source_array[i], target_array[i]); if (stack.size() == depth) { @@ -34966,12 +43770,11 @@ public: if (it != s->cend()) { ++stack.back().member; - const std::size_t next_common = stack.back().next_common; - if (next_common < stack.back().common_keys.size() && it.key() == stack.back().common_keys[next_common]) + const basic_json* const target_value_ptr = stack.back().target_values[stack.back().index++]; + if (target_value_ptr != nullptr) { - ++stack.back().next_common; - const basic_json& target_value = (*t)[it.key()]; - detail::concat_into(current_path, '/', detail::escape(it.key())); + const basic_json& target_value = *target_value_ptr; + diff_append_key(current_path, it.key()); enter(it.value(), target_value); if (stack.size() == depth) { @@ -34981,14 +43784,14 @@ public: else { // found a key that is not in target -> remove it - diff_remove(result, detail::concat(current_path, '/', detail::escape(it.key()))); + diff_remove(result, diff_key_path(current_path, it.key())); } continue; } // append the "add" ops for brand-new keys collected when the // object was entered - result.insert(result.end(), stack.back().added_ops.begin(), stack.back().added_ops.end()); + diff_added(result, stack.back().added, current_path); } // this array or object is done: continue with the one it is in @@ -35118,6 +43921,176 @@ public: } public: + /// @brief creates a diff as a JSON Merge Patch + /// @sa https://json.nlohmann.me/api/basic_json/merge_diff/ + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json merge_diff(const basic_json& source, const basic_json& target) + { + // unless both are objects, the patch is the target itself + if (!source.is_object() || !target.is_object()) + { + return target; + } + + return merge_diff_recursively(source, target, 0); + } + + private: + /// @brief two objects @ref merge_diff_iteratively is diffing + struct merge_diff_frame + { + merge_diff_frame(const basic_json* source_, const basic_json* target_) + : source(source_), target(target_), member(source_->cbegin()), patch(value_t::object) + {} + + // declared for GCC's -Weffc++, as in diff_frame + merge_diff_frame(const merge_diff_frame&) = default; + merge_diff_frame(merge_diff_frame&&) = default; + merge_diff_frame& operator=(const merge_diff_frame&) = default; + merge_diff_frame& operator=(merge_diff_frame&&) = default; + ~merge_diff_frame() = default; + + /// the objects being diffed + const basic_json* source; + const basic_json* target; + /// the member of source being diffed; advanced once it is done + const_iterator member; + /// the patch for the members of source diffed so far + basic_json patch; + }; + + /// @brief add the members of object @a target that @a source does not + /// have to the merge patch @a result + static void merge_diff_added_members(basic_json& result, const basic_json& source, const basic_json& target) + { + for (auto it = target.begin(); it != target.end(); ++it) + { + auto itf = source.find(it.key()); + if (itf == source.end()) + { + result[it.key()] = it.value(); + } + } + } + + /*! + @brief @ref merge_diff of two objects at nesting level @a depth + + Diffing two nested objects calls this function again, once per nesting + level, so values nested deeply enough would exhaust the call stack and + terminate the process. The descent is bounded here: once @ref + detail::recursion_depth_limit levels have been entered, @ref + merge_diff_iteratively diffs what is left without the call stack. + */ + static basic_json merge_diff_recursively(const basic_json& source, const basic_json& target, const std::size_t depth) + { + JSON_ASSERT(source.is_object() && target.is_object()); + + if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) + { + return merge_diff_iteratively(source, target); + } + + basic_json result(value_t::object); + + for (auto it = source.begin(); it != source.end(); ++it) + { + auto itf = target.find(it.key()); + if (itf != target.end()) + { + if (it.value().is_object() && itf.value().is_object()) + { + auto diff = merge_diff_recursively(it.value(), itf.value(), depth + 1); + if (!diff.empty()) + { + result[it.key()] = std::move(diff); + } + } + else if (it.value() != itf.value()) + { + result[it.key()] = itf.value(); + } + } + else + { + result[it.key()] = value_t::null; + } + } + + merge_diff_added_members(result, source, target); + return result; + } + + /*! + @brief @ref merge_diff of two objects without the call stack + + Produces the same patch as @ref merge_diff_recursively, keeping the objects + being diffed on an explicit stack instead of descending into them. Two + nested objects are still diffed completely before the next member, so the + patch has its members in the same order as in the recursive version. Only + reached for objects nested more deeply than @ref + detail::recursion_depth_limit. + */ + static basic_json merge_diff_iteratively(const basic_json& source, const basic_json& target) + { + // the patch of source and target, set once their frame is popped + basic_json result; + std::vector stack; + stack.emplace_back(&source, &target); + while (!stack.empty()) + { + // the frame's members are copied out and it is changed through + // stack.back(): pushing a frame for nested objects or popping + // this one would invalidate a reference to it + const basic_json* const s = stack.back().source; + const basic_json* const t = stack.back().target; + const const_iterator it = stack.back().member; + + if (it != s->cend()) + { + const const_iterator itf = t->find(it.key()); + if (itf != t->cend()) + { + if (it.value().is_object() && itf.value().is_object()) + { + // member is advanced once their patch is under its key + stack.emplace_back(&it.value(), &itf.value()); + continue; + } + if (it.value() != itf.value()) + { + stack.back().patch[it.key()] = itf.value(); + } + } + else + { + stack.back().patch[it.key()] = value_t::null; + } + ++stack.back().member; + continue; + } + + merge_diff_added_members(stack.back().patch, *s, *t); + + // these objects are done: continue with the ones they are in + basic_json patch = std::move(stack.back().patch); + stack.pop_back(); + if (stack.empty()) + { + result = std::move(patch); + } + else + { + if (!patch.empty()) + { + stack.back().patch[stack.back().member.key()] = std::move(patch); + } + ++stack.back().member; + } + } + return result; + } + /// @} }; @@ -35310,6 +44283,18 @@ struct formatter // NOLINT(cert-dcl58-c #undef JSON_NO_UNIQUE_ADDRESS #undef JSON_DISABLE_ENUM_SERIALIZATION #undef JSON_DISABLE_TUPLE_REFERENCE_CONVERSION +#undef JSON_UNLOCKED_FILE_IO +#undef JSON_DTOA_SSE2 +#undef JSON_DTOA_NEON +#undef JSON_LITTLE_ENDIAN_TARGET +#undef JSON_BIG_ENDIAN_TARGET +#undef JSON_SCAN_NEON +#undef JSON_SCAN_SSE2 +#undef JSON_SCAN_SSSE3 +#undef JSON_SCAN_SSSE3_DISPATCH +#undef JSON_SCAN_SSSE3_TARGET +#undef JSON_SCAN_VECTOR +#undef JSON_SCAN_VECTOR_UTF8 #ifndef JSON_TEST_KEEP_MACROS #undef JSON_CATCH @@ -35334,6 +44319,8 @@ struct formatter // NOLINT(cert-dcl58-c #undef JSON_STRICT_BINARY_UTF8 #undef JSON_DELETE_DEPRECATED_FUNCTIONS #undef JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS + #undef JSON_DISABLE_STREAM_BULK_SCAN + #undef JSON_DISABLE_UNLOCKED_FILE_IO #endif // #include @@ -35587,7 +44574,7 @@ NLOHMANN_JSON_NAMESPACE_END #undef NLOHMANN_JSON_LITERAL_OPERATOR #endif // INCLUDE_NLOHMANN_JSON_LITERALS_HPP_ - +// IWYU pragma: export #endif #endif // INCLUDE_NLOHMANN_JSON_HPP_ diff --git a/single_include/nlohmann/json_fwd.hpp b/single_include/nlohmann/json_fwd.hpp index 7b8aa5bfd..cd64b76a6 100644 --- a/single_include/nlohmann/json_fwd.hpp +++ b/single_include/nlohmann/json_fwd.hpp @@ -71,6 +71,14 @@ #define JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS 0 #endif +#ifndef JSON_DISABLE_STREAM_BULK_SCAN + #define JSON_DISABLE_STREAM_BULK_SCAN 0 +#endif + +#ifndef JSON_DISABLE_UNLOCKED_FILE_IO + #define JSON_DISABLE_UNLOCKED_FILE_IO 0 +#endif + #if JSON_DIAGNOSTICS #define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS _diag #else @@ -119,14 +127,44 @@ #define NLOHMANN_JSON_ABI_TAG_OBJECTS_FOR_ENUM_KEYED_MAPS #endif +#if JSON_DISABLE_STREAM_BULK_SCAN + #define NLOHMANN_JSON_ABI_TAG_DISABLE_STREAM_BULK_SCAN _dsbs +#else + #define NLOHMANN_JSON_ABI_TAG_DISABLE_STREAM_BULK_SCAN +#endif + +#if JSON_DISABLE_UNLOCKED_FILE_IO + #define NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO _dufio +#else + #define NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO +#endif + +#ifdef JSON_NO_SIMD + #define NLOHMANN_JSON_ABI_TAG_NO_SIMD _ns +#else + #define NLOHMANN_JSON_ABI_TAG_NO_SIMD +#endif + +#ifdef JSON_VIEW_NO_SIMD + #define NLOHMANN_JSON_ABI_TAG_VIEW_NO_SIMD _vns +#else + #define NLOHMANN_JSON_ABI_TAG_VIEW_NO_SIMD +#endif + +#ifdef JSON_VIEW_USE_SSSE3 + #define NLOHMANN_JSON_ABI_TAG_VIEW_USE_SSSE3 _vssse3 +#else + #define NLOHMANN_JSON_ABI_TAG_VIEW_USE_SSSE3 +#endif + #ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION #define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0 #endif // Construct the namespace ABI tags component -#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h) json_abi ## a ## b ## c ## d ## e ## f ## g ## h -#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c, d, e, f, g, h) \ - NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h) +#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l, m) json_abi ## a ## b ## c ## d ## e ## f ## g ## h ## i ## j ## k ## l ## m +#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c, d, e, f, g, h, i, j, k, l, m) \ + NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l, m) #define NLOHMANN_JSON_ABI_TAGS \ NLOHMANN_JSON_ABI_TAGS_CONCAT( \ @@ -137,7 +175,12 @@ NLOHMANN_JSON_ABI_TAG_PRECISE_STREAM_POSITION, \ NLOHMANN_JSON_ABI_TAG_STRICT_NUL_HANDLING, \ NLOHMANN_JSON_ABI_TAG_STRICT_BINARY_UTF8, \ - NLOHMANN_JSON_ABI_TAG_OBJECTS_FOR_ENUM_KEYED_MAPS) + NLOHMANN_JSON_ABI_TAG_OBJECTS_FOR_ENUM_KEYED_MAPS, \ + NLOHMANN_JSON_ABI_TAG_DISABLE_STREAM_BULK_SCAN, \ + NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO, \ + NLOHMANN_JSON_ABI_TAG_NO_SIMD, \ + NLOHMANN_JSON_ABI_TAG_VIEW_NO_SIMD, \ + NLOHMANN_JSON_ABI_TAG_VIEW_USE_SSSE3) // Construct the namespace version component #define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \ diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp new file mode 100644 index 000000000..338b55492 --- /dev/null +++ b/single_include/nlohmann/json_view.hpp @@ -0,0 +1,9402 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +/****************************************************************************\ + * Zero-copy view of a parsed JSON text. * + * * + * json_document::parse() builds a flat index of the values of a JSON text * + * (16 bytes per value) instead of a tree of basic_json values. Strings and * + * numbers stay in the source text; only strings with escapes are decoded, * + * into one buffer. json_view is a handle to one value of the document, with * + * the read-only part of the basic_json interface; materialize() turns a * + * subtree into the basic_json value that parse() would produce. An editable * + * document (json_editable_document) also has set(), push_back(), insert(), * + * and erase(): edits never write to the source text, and views stay valid. * + * * + * The source text must outlive a document that borrows it (lvalue byte * + * containers, C strings); rvalue strings, streams, and other inputs are * + * owned by the document. * +\****************************************************************************/ + +#ifndef INCLUDE_NLOHMANN_JSON_VIEW_HPP_ +#define INCLUDE_NLOHMANN_JSON_VIEW_HPP_ + +#include // all_of, min +#include // array +#include // nullptr_t, size_t // IWYU pragma: keep +#include // uint8_t, uint32_t +#include // FILE +#include // memcpy, strlen +#include // istream // IWYU pragma: keep +#include // distance, input_iterator_tag, iterator_traits +#include // map +#include // unique_ptr +#ifndef JSON_NO_IO + #include // ostream // IWYU pragma: keep +#endif +#include // string +#include // tuple_element, tuple_size // IWYU pragma: keep +#include // decay, enable_if, integral_constant, is_arithmetic, is_base_of, is_integral, is_same, remove_cv, remove_extent +#include // unordered_map +#include // forward, move +#include // vector + +#include // IWYU pragma: export + +// json.hpp provides the library's types and macros used below (it includes +// the headers that define them); json_view.hpp must not include them again, +// because the amalgamated json_view.hpp only includes json.hpp +// IWYU pragma: no_include +// IWYU pragma: no_include "nlohmann/detail/abi_config.hpp" +// IWYU pragma: no_include "nlohmann/detail/abi_macros.hpp" +// IWYU pragma: no_include "nlohmann/detail/input/input_adapters.hpp" +// IWYU pragma: no_include "nlohmann/detail/json_pointer.hpp" +// IWYU pragma: no_include "nlohmann/detail/meta/cpp_future.hpp" +// IWYU pragma: no_include "nlohmann/detail/string_concat.hpp" +// IWYU pragma: no_include "nlohmann/detail/value_t.hpp" +// IWYU pragma: no_include "nlohmann/json.hpp" +// IWYU pragma: no_include "nlohmann/json_fwd.hpp" + +// the view builds on internals of the library: both must be the same version +#if NLOHMANN_JSON_VERSION_MAJOR != 3 || NLOHMANN_JSON_VERSION_MINOR != 12 || NLOHMANN_JSON_VERSION_PATCH != 0 + #error "json_view.hpp requires json.hpp of the same version (3.12.0)" +#endif + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-FileCopyrightText: 2020 YaoYuan +// SPDX-License-Identifier: MIT + + + +#include // find, find_if, max, min +#include // array +#include // size_t, ptrdiff_t +#include // int64_t, uint8_t, uint16_t, uint32_t, uint64_t +#include // memcmp, memcpy +#include // numeric_limits +#include // string +#include // vector + +// #include +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // array +#include // size_t +#include // uint8_t, uint32_t +#include // memcmp, memcpy +#include // numeric_limits +#include // map +#include // unique_ptr +#include // bad_alloc, operator new, placement new +#include // string +#include // vector + +// #include +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// Macros of json_view.hpp and its detail headers. json.hpp undefines its own +// macros at its end (macro_unscope.hpp), so the view defines the few it needs +// under its own prefix; json_view.hpp undefines them all at its end +// (detail/view/macro_unscope.hpp). Configuration that json.hpp undefines is +// read from detail::abi_config instead. + +#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) + #define NLOHMANN_VIEW_HAS_CPP_17 1 +#else + #define NLOHMANN_VIEW_HAS_CPP_17 0 +#endif + +#if defined(__GNUC__) || defined(__clang__) + #define NLOHMANN_VIEW_LIKELY(x) __builtin_expect(!!(x), 1) + #define NLOHMANN_VIEW_UNLIKELY(x) __builtin_expect(!!(x), 0) + #define NLOHMANN_VIEW_ALWAYS_INLINE inline __attribute__((always_inline)) + #define NLOHMANN_VIEW_NOINLINE __attribute__((noinline)) +#elif defined(_MSC_VER) + #define NLOHMANN_VIEW_LIKELY(x) (x) + #define NLOHMANN_VIEW_UNLIKELY(x) (x) + // plain inline: __forceinline makes MSVC report C4714 (not inlined) for + // function templates it cannot inline, which is an error under /WX; the + // forced inlining is only a performance hint + #define NLOHMANN_VIEW_ALWAYS_INLINE inline + #define NLOHMANN_VIEW_NOINLINE __declspec(noinline) +#else + #define NLOHMANN_VIEW_LIKELY(x) (x) + #define NLOHMANN_VIEW_UNLIKELY(x) (x) + #define NLOHMANN_VIEW_ALWAYS_INLINE inline + #define NLOHMANN_VIEW_NOINLINE +#endif + +#if defined(__GNUC__) || defined(__clang__) + #define NLOHMANN_VIEW_NODISCARD __attribute__((warn_unused_result)) +#elif defined(_MSC_VER) + #define NLOHMANN_VIEW_NODISCARD _Check_return_ +#else + #define NLOHMANN_VIEW_NODISCARD +#endif + +// exceptions as in json.hpp (JSON_NOEXCEPTION, JSON_THROW_USER) +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) + #define NLOHMANN_VIEW_THROW(exception) throw exception +#else + #include + // (the exception is built first, so that the arguments of the throwing + // helpers count as used; the program ends anyway) + #define NLOHMANN_VIEW_THROW(exception) (static_cast(exception), std::abort()) +#endif +#if defined(JSON_THROW_USER) + #undef NLOHMANN_VIEW_THROW + #define NLOHMANN_VIEW_THROW JSON_THROW_USER +#endif + +// the parser stores a node's first word at once where the layout of `node` is +// known to be little-endian (MSVC targets are); elsewhere field by field +#if (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) || defined(_MSC_VER) + #define NLOHMANN_VIEW_LITTLE_ENDIAN 1 +#else + #define NLOHMANN_VIEW_LITTLE_ENDIAN 0 +#endif + +/// eight checks at fixed offsets 0..7 +#define NLOHMANN_VIEW_REPEAT_FIRST_8(X) X(0) X(1) X(2) X(3) X(4) X(5) X(6) X(7) + +/// eight checks at fixed offsets 8..15 +#define NLOHMANN_VIEW_REPEAT_SECOND_8(X) X(8) X(9) X(10) X(11) X(12) X(13) X(14) X(15) + +/// sixteen checks at fixed offsets 0..15 +#define NLOHMANN_VIEW_REPEAT16(X) NLOHMANN_VIEW_REPEAT_FIRST_8(X) NLOHMANN_VIEW_REPEAT_SECOND_8(X) + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // size_t +#include // uint8_t, uint16_t, uint32_t, uint64_t +#include // memcpy + +// #include +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// The largest input a document accepts, in bytes. Offsets and node counts are +/// 32 bits wide; the limit keeps 16 bytes (the width of the scanner's steps) +/// below 2^32, so that a position one step past the end of the text fits. +constexpr std::size_t max_input_size() noexcept +{ + return 0xFFFFFFEFu; +} + +/// node flags +struct node_flags +{ + static constexpr std::uint8_t escaped = 1; ///< string payload lives in the decode arena, not the source + static constexpr std::uint8_t edited = 2; ///< string or number token lives in the edit arena (editable documents) + static constexpr std::uint8_t storage = escaped | edited; ///< mask: where a string or number token lives (index into document_data::base) + static constexpr std::uint8_t is_true = 4; ///< boolean value + static constexpr std::uint8_t moved = 8; ///< array/object: the elements live in a separate sequence (editable documents) + static constexpr std::uint8_t is_new = 16; ///< written by an edit: no source position + static constexpr std::uint8_t linked = 32; ///< an entry of a moved sequence links to this value (editable documents): its extent in the parsed layout no longer matters +}; + +/// kind of an entry of an edited sequence that stands for a value stored +/// elsewhere (the address of the value's node is kept in the len/next bytes) +constexpr std::uint8_t kind_link = 10; + +/// One entry of the flat index, in document order. An object's members are +/// stored as key node followed by the value's subtree. Integers keep their +/// converted 64-bit value in the len/next bytes (the node after a scalar is +/// always the next one, and the token length follows from `extra`). +struct node +{ + std::uint8_t kind; ///< value_t, or kind_link + std::uint8_t flags; ///< node_flags + std::uint16_t extra; ///< numbers: integer digits (low byte) and fraction digits (high byte), 255 = "many"; objects: number of the object's key hash index (1-based, 0 = none); otherwise 0 + std::uint32_t off; ///< source offset (string content, number token, literal, bracket); arena offset if escaped/edited; number of the element sequence if moved + std::uint32_t len; ///< string: decoded bytes; float: token bytes; array/object: element count + std::uint32_t next; ///< array/object: number of nodes of the subtree (its extent in the enclosing sequence) +}; +static_assert(sizeof(node) == 16, "node must stay 16 bytes"); + +// the node kinds are value_t values; the test below (object and array are 1 +// and 2) and those of the number kinds depend on this numbering +static_assert(static_cast(value_t::null) == 0 && static_cast(value_t::object) == 1 + && static_cast(value_t::array) == 2 && static_cast(value_t::string) == 3 + && static_cast(value_t::boolean) == 4 && static_cast(value_t::number_integer) == 5 + && static_cast(value_t::number_unsigned) == 6 && static_cast(value_t::number_float) == 7, + "the node format depends on the numbering of value_t"); + +NLOHMANN_VIEW_ALWAYS_INLINE bool is_container(const node& n) noexcept +{ + return static_cast(n.kind) - 1u <= 1u; +} + +/// the value a link node stands for +NLOHMANN_VIEW_ALWAYS_INLINE const node* link_target(const node& n) noexcept +{ + const node* t = nullptr; + std::memcpy(static_cast(&t), reinterpret_cast(&n) + 8, sizeof(const node*)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + return t; +} + +/// let the entry n stand for the value at target (and mark the value) +inline void make_link(node& n, node* target) noexcept +{ + target->flags = static_cast(target->flags | node_flags::linked); + n = node{}; + n.kind = kind_link; + std::memcpy(reinterpret_cast(&n) + 8, static_cast(&target), sizeof(const node*)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) +} + +/// the converted value of an integer node: len is its low half, next its high +/// half (on little-endian targets the two words are the value in memory) +NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t integer_bits(const node& n) noexcept +{ +#if NLOHMANN_VIEW_LITTLE_ENDIAN + std::uint64_t v = 0; + std::memcpy(&v, reinterpret_cast(&n) + 8, 8); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + return v; +#else + return static_cast(n.len) | (static_cast(n.next) << 32); +#endif +} + +NLOHMANN_VIEW_ALWAYS_INLINE void set_integer_bits(node& n, std::uint64_t v) noexcept +{ +#if NLOHMANN_VIEW_LITTLE_ENDIAN + std::memcpy(reinterpret_cast(&n) + 8, &v, 8); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) +#else + n.len = static_cast(v); + n.next = static_cast(v >> 32); +#endif +} + +/// token length of a number node +NLOHMANN_VIEW_ALWAYS_INLINE std::uint32_t number_length(const node& n) noexcept +{ + return n.kind == static_cast(value_t::number_float) ? n.len + : (n.extra & 0xFFu) + (n.kind == static_cast(value_t::number_integer) ? 1u : 0u); +} + +/// estimated number of nodes for an input of `size` bytes (one node per ~12 +/// bytes covers typical documents without regrowth) +inline std::size_t estimate_nodes(std::size_t size) noexcept +{ + return (size / 12) + 16; +} + +/// estimated number of nodes for the input [src, src + size): pretty-printed +/// input (whitespace after the first byte) needs about a node per 12 bytes, +/// minified input up to one per 4 (yyjson tells the two apart the same way) +inline std::size_t estimate_nodes(const char* src, std::size_t size) noexcept +{ + return size >= 2 && (src[1] == ' ' || src[1] == '\n' || src[1] == '\r' || src[1] == '\t') ? estimate_nodes(size) : (size / 4) + 16; +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// storage of a parsed document; heap-allocated (header and an initial node +/// array in one block) so that views survive moves of the owning document +struct document_data +{ + const char* src = nullptr; + std::size_t size = 0; + node* nodes = nullptr; ///< the node index (see node.hpp) + std::size_t node_count = 0; + std::size_t node_capacity = 0; + node* inline_nodes = nullptr; ///< node array allocated together with this header + std::size_t inline_capacity = 0; + std::size_t max_depth = 0; ///< deepest nesting of arrays and objects (the root container is depth 1) + std::vector arena; ///< decoded strings that contained escapes (no terminator: they are handed out with their length) + std::size_t arena_size = 0; ///< bytes of decoded strings at base[1] (the arena, or those of a loaded image) + std::string owned; ///< owned copy of the input, if any (a std::string, so that its NUL serves as the parser's sentinel) + std::vector owned_image; ///< a loaded image the document owns (the text and the decoded strings point into it) + + // hash indexes of the keys of large objects (see object_index.hpp) + static constexpr std::uint32_t index_min_members = 128; + struct object_index + { + std::size_t start; ///< first slot in index_slots + std::uint32_t mask; ///< slot count - 1 (a power of two minus one) + }; + std::vector indexes; + std::vector index_slots; + std::vector large_objects; ///< positions of the objects to index (noted while parsing) + std::array base = {{nullptr, nullptr, nullptr, nullptr}}; ///< string bases: source, arena, edit arena, unused (indexed by flags & node_flags::storage) + bool discarded = true; + + /// The storage of edits (editable documents only; see edit_storage.hpp). + /// No edit moves or resizes the parsed index, and the nodes of values + /// written by edits never move either, so a view stays valid whenever it + /// was taken, before the first edit or between later ones: an array/object + /// whose elements change gets node_flags::moved, and its elements then live + /// in a separate sequence (a header node, then the entries), whose entries + /// link to the values. + struct edit_state + { + std::vector moved{}; ///< element sequences of moved arrays/objects (header node first) // NOLINT(readability-redundant-member-init) + std::vector moved_cap{}; ///< capacity in nodes of a growable block; 0: a fixed sequence (a new value) // NOLINT(readability-redundant-member-init) + std::vector> chunks{}; ///< storage of new values and blocks; never moved // NOLINT(readability-redundant-member-init,cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + std::map regions{}; ///< new arrays/objects: root -> container that uses it as its element sequence (nullptr: linked from a block) // NOLINT(readability-redundant-member-init) + node* chunk_cur = nullptr; + node* chunk_end = nullptr; + std::size_t chunk_next = 64; + std::vector> texts{}; ///< edit arena, the current buffer last; earlier ones stay alive for string views // NOLINT(readability-redundant-member-init,cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + std::size_t text_used = 0; + std::size_t text_cap = 0; + std::size_t bytes = 0; ///< memory held by edits + }; + std::unique_ptr edits; ///< created by the first edit + + /// One allocation for the header and room for `count` nodes, so that a + /// small document needs one allocation instead of two. Large documents get + /// a separate node array instead (so it can be trimmed). When a document + /// outgrows the inline array, that array stays unused: at most 256 nodes + /// (4 KiB), and only for documents whose estimate was too small. + static document_data* create(std::size_t count) + { + count = count <= 256 ? count : 0; + void* mem = ::operator new (sizeof(document_data) + (count * sizeof(node))); + auto* d = new (mem) document_data(); // NOLINT(cppcoreguidelines-owning-memory): owned by the returned pointer, freed by deleter + // (aligned: sizeof is a multiple of the alignment; through void*, as GCC's -Wcast-align wants) + // (null without inline nodes, not a pointer past the end of the allocation) + d->inline_nodes = count == 0 ? nullptr : static_cast(static_cast(static_cast(mem) + sizeof(document_data))); // NOLINT(bugprone-casting-through-void) + d->inline_capacity = count; + d->nodes = d->inline_nodes; + d->node_capacity = count; + return d; + } + + struct deleter + { + void operator()(document_data* d) const noexcept + { + d->~document_data(); + ::operator delete (d); + } + }; + + /// user-provided so that the class-type members can be initialized in the + /// member initialization list (-Weffc++ asks for it, and old GCC rejects a + /// defaulted constructor whose exception specification differs from the + /// implicit one); they cannot take default member initializers, which old + /// Clang (3.4-3.6) rejects. The other members have default member + /// initializers. noexcept: create() constructs into raw memory and could not + /// release it if this threw (the std::vector and std::string default + /// constructors do not allocate). + document_data() noexcept + : arena() // NOLINT(readability-redundant-member-init) + , owned() // NOLINT(readability-redundant-member-init) + , owned_image() // NOLINT(readability-redundant-member-init) + , indexes() // NOLINT(readability-redundant-member-init) + , index_slots() // NOLINT(readability-redundant-member-init) + , large_objects() // NOLINT(readability-redundant-member-init) + , edits() // NOLINT(readability-redundant-member-init) + {} + document_data(const document_data&) = delete; + document_data(document_data&&) = delete; + document_data& operator=(const document_data&) = delete; + document_data& operator=(document_data&&) = delete; + ~document_data() + { + release(); + } + + void release() noexcept + { + if (nodes != inline_nodes) + { + ::operator delete (nodes); + } + nodes = inline_nodes; + node_capacity = inline_capacity; + } + + /// the largest node count whose size in bytes fits a std::size_t + static constexpr std::size_t max_nodes() noexcept + { + return (std::numeric_limits::max)() / sizeof(node); + } + + [[noreturn]] NLOHMANN_VIEW_NOINLINE static void throw_bad_alloc() + { + NLOHMANN_VIEW_THROW(std::bad_alloc()); + } + + /// make room for n nodes; keeps the first node_count nodes (throws + /// std::bad_alloc for a count that does not fit the address space, + /// instead of wrapping around in n * sizeof(node)) + void reserve(std::size_t n) + { + if (n <= node_capacity) + { + return; + } + if (NLOHMANN_VIEW_UNLIKELY(n > max_nodes())) + { + throw_bad_alloc(); + } + node* fresh = static_cast(::operator new (n * sizeof(node))); + if (node_count != 0) + { + std::memcpy(fresh, nodes, node_count * sizeof(node)); + } + release(); + nodes = fresh; + node_capacity = n; + } + + const char* str(const node& n) const noexcept + { + return base[n.flags & node_flags::storage] + n.off; + } + + /// whether the string of node k (a key) is the n bytes at key + bool key_equals(const node& k, const char* key, std::size_t n) const noexcept + { + return k.len == n && (n == 0 || std::memcmp(str(k), key, n) == 0); + } + + /// the node after n's subtree (containers span `next` nodes, scalars one) + static NLOHMANN_VIEW_ALWAYS_INLINE const node* after(const node* n) noexcept + { + return n + (is_container(*n) ? n->next : 1u); + } + + /// first element (array) or first key (object) of a container + static NLOHMANN_VIEW_ALWAYS_INLINE const node* first_child(const node* n) noexcept + { + return n + 1; + } + + /// end of the elements of a container + static NLOHMANN_VIEW_ALWAYS_INLINE const node* child_end(const node* n) noexcept + { + return n + n->next; + } + + /// (editable documents) first element or key, also of a moved container + NLOHMANN_VIEW_ALWAYS_INLINE const node* first_child_edited(const node* n) const noexcept + { + return NLOHMANN_VIEW_LIKELY((n->flags & node_flags::moved) == 0) ? n + 1 : edits->moved[n->off] + 1; + } + + /// (editable documents) end of the elements, also of a moved container + NLOHMANN_VIEW_ALWAYS_INLINE const node* child_end_edited(const node* n) const noexcept + { + if (NLOHMANN_VIEW_LIKELY((n->flags & node_flags::moved) == 0)) + { + return n + n->next; + } + const node* const h = edits->moved[n->off]; + return h + h->next; + } + + /// (editable documents) the value at an element position: entries of + /// moved sequences are links. The link case is out of line, so that this + /// compiles to a predicted branch rather than a select that delays the + /// following loads. + static NLOHMANN_VIEW_ALWAYS_INLINE const node* deref(const node* n) noexcept + { + return NLOHMANN_VIEW_LIKELY(n->kind != kind_link) ? n : follow_link(n); + } + + static NLOHMANN_VIEW_NOINLINE const node* follow_link(const node* n) noexcept + { + return link_target(*n); + } +}; + +/// How the index is walked: views of read-only documents follow the node +/// array alone and compile without any of the edit handling; views of +/// editable documents also follow moved element sequences and links. +template +struct navigation +{ + /// whether the walk follows edits (the template argument as a runtime + /// condition: `Editable && ...` is a constant condition for MSVC, C4127) + static NLOHMANN_VIEW_ALWAYS_INLINE bool editable() noexcept + { + return false; + } + + static NLOHMANN_VIEW_ALWAYS_INLINE const node* first(const document_data& /*d*/, const node* n) noexcept + { + return n + 1; + } + + static NLOHMANN_VIEW_ALWAYS_INLINE const node* end(const document_data& /*d*/, const node* n) noexcept + { + return n + n->next; + } + + static NLOHMANN_VIEW_ALWAYS_INLINE const node* value(const node* n) noexcept + { + return n; + } +}; + +template<> +struct navigation +{ + static NLOHMANN_VIEW_ALWAYS_INLINE bool editable() noexcept + { + return true; + } + + static NLOHMANN_VIEW_ALWAYS_INLINE const node* first(const document_data& d, const node* n) noexcept + { + return d.first_child_edited(n); + } + + static NLOHMANN_VIEW_ALWAYS_INLINE const node* end(const document_data& d, const node* n) noexcept + { + return d.child_end_edited(n); + } + + static NLOHMANN_VIEW_ALWAYS_INLINE const node* value(const node* n) noexcept + { + return document_data::deref(n); + } +}; + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-FileCopyrightText: 2020 YaoYuan +// SPDX-License-Identifier: MIT + + + +#include // array +#include // size_t +#include // uint8_t, uint16_t, uint64_t +#include // memcpy + +// #include +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-FileCopyrightText: 2018-2025 The simdjson authors +// SPDX-License-Identifier: MIT + + + +#include // array +#include // atomic +#include // size_t +#include // uint8_t, uint64_t + +// #include +// #include + + +// Vector code for long runs of string bytes. NEON (AArch64) and SSE2 (x86-64) +// belong to the baseline instruction sets and are used by default. The vector +// UTF-8 check needs NEON or SSSE3. SSSE3 is not part of x86-64, and the code +// must not depend on the flags of a translation unit (two translation units +// with different flags would have different definitions of the same inline +// functions): the check is compiled for SSSE3 with a function attribute and +// used where the CPU has SSSE3 (all x86-64 CPUs since about 2011), else the +// portable check. JSON_VIEW_USE_SSSE3 skips the CPU check (for code compiled +// for SSSE3 anyway); JSON_VIEW_NO_SIMD selects the portable code. +// NLOHMANN_VIEW_NO_TARGET_ATTRIBUTE (set by the GCC module interface, where GCC +// ignores the target attribute and then rejects the SSSE3 intrinsics) keeps the +// check portable without disabling the SSE2 code. +#if !defined(JSON_VIEW_NO_SIMD) && defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) && NLOHMANN_VIEW_LITTLE_ENDIAN + #include + #define NLOHMANN_VIEW_NEON 1 +#else + #define NLOHMANN_VIEW_NEON 0 +#endif +// (x86 only: other targets can define __SSE2__ as well, e.g., WebAssembly with -msse2, but have no ) +#if !defined(JSON_VIEW_NO_SIMD) && !NLOHMANN_VIEW_NEON && (defined(__x86_64__) || defined(__i386__) || defined(_M_X64) || defined(_M_IX86)) && (defined(__SSE2__) || defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2)) + #include + #define NLOHMANN_VIEW_SSE2 1 +#else + #define NLOHMANN_VIEW_SSE2 0 +#endif +#if NLOHMANN_VIEW_SSE2 && defined(JSON_VIEW_USE_SSSE3) + #include + #define NLOHMANN_VIEW_SSSE3 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) +#else + #define NLOHMANN_VIEW_SSSE3 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) +#endif +#if NLOHMANN_VIEW_SSE2 && !NLOHMANN_VIEW_SSSE3 && !defined(NLOHMANN_VIEW_NO_TARGET_ATTRIBUTE) && ((defined(__clang__) && __clang_major__ >= 4) || (defined(__GNUC__) && !defined(__clang__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 9)))) + // (GCC before 4.9 has no SSSE3 intrinsics without -mssse3) + // of GCC before 11 has no include guard, and json.hpp and + // json_view.hpp both include it + #ifndef NLOHMANN_JSON_CPUID_INCLUDED + #define NLOHMANN_JSON_CPUID_INCLUDED + #include + #endif + #include + #define NLOHMANN_VIEW_SSSE3_DISPATCH 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) + #define NLOHMANN_VIEW_SSSE3_TARGET __attribute__((target("ssse3"))) +#elif NLOHMANN_VIEW_SSE2 && !NLOHMANN_VIEW_SSSE3 && defined(_MSC_VER) + // (MSVC compiles intrinsics of any instruction set) + #include + #include + #define NLOHMANN_VIEW_SSSE3_DISPATCH 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) + #define NLOHMANN_VIEW_SSSE3_TARGET +#else + #define NLOHMANN_VIEW_SSSE3_DISPATCH 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum) + #define NLOHMANN_VIEW_SSSE3_TARGET +#endif +#define NLOHMANN_VIEW_VECTOR (NLOHMANN_VIEW_NEON || NLOHMANN_VIEW_SSE2) +#define NLOHMANN_VIEW_VECTOR_UTF8 (NLOHMANN_VIEW_NEON || NLOHMANN_VIEW_SSSE3 || NLOHMANN_VIEW_SSSE3_DISPATCH) + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +#if NLOHMANN_VIEW_VECTOR +/*! +@brief the first byte of a string run that is a quote, a backslash, a control +character, or not ASCII, 16 bytes per step + +Stops at such a byte, or where fewer than 16 bytes are left (the caller tells +the two apart). A signed compare with 0x20 finds control characters and +non-ASCII bytes at once. +*/ +NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* vector_plain_run(const unsigned char* p, const unsigned char* e) noexcept +{ + while (e - p >= 16) + { +#if NLOHMANN_VIEW_NEON + const uint8x16_t in = vld1q_u8(p); + const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))), + vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20))); + // one nibble per byte (the usual NEON replacement of x86's movemask, see + // D. Kutenin, "Porting x86 vector bitmask optimizations to Arm NEON", 2022) + const std::uint64_t bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0); + if (bits != 0) + { + return p + (count_trailing_zeros(bits) >> 2u); + } +#else + const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_cmplt_epi8(in, _mm_set1_epi8(0x20))); + const auto bits = static_cast(static_cast(_mm_movemask_epi8(special))); + if (bits != 0) + { + return p + count_trailing_zeros(bits); + } +#endif + p += 16; + } + return p; +} + +/*! +@brief copy 16 bytes from p to w, and find the first quote, backslash, +control character, or non-ASCII byte among them + +The bytes are copied whether or not they are all plain (the caller has room +for 16 bytes and keeps only the plain ones). + +@return the offset of that byte, or 16 if there is none +*/ +NLOHMANN_VIEW_ALWAYS_INLINE unsigned copy_plain_run16(const unsigned char* p, char* w) noexcept +{ +#if NLOHMANN_VIEW_NEON + const uint8x16_t in = vld1q_u8(p); + vst1q_u8(reinterpret_cast(w), in); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))), + vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20))); + const std::uint64_t bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0); + return bits != 0 ? static_cast(count_trailing_zeros(bits) >> 2u) : 16u; +#else + const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) + _mm_storeu_si128(static_cast<__m128i*>(static_cast(w)), in); // NOLINT(bugprone-casting-through-void) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_cmplt_epi8(in, _mm_set1_epi8(0x20))); + const auto bits = static_cast(static_cast(_mm_movemask_epi8(special))); + return bits != 0 ? static_cast(count_trailing_zeros(bits)) : 16u; +#endif +} +#endif + +#if NLOHMANN_VIEW_SSSE3_DISPATCH +/// whether the CPU has SSSE3 (CPUID leaf 1, ECX bit 9) +inline bool cpu_ssse3() noexcept +{ +#if defined(_MSC_VER) && !defined(__clang__) + std::array regs {{}}; + __cpuid(regs.data(), 1); + return (static_cast(regs[2]) & (1u << 9u)) != 0; +#else + unsigned eax = 0; + unsigned ebx = 0; + unsigned ecx = 0; + unsigned edx = 0; + return __get_cpuid(1, &eax, &ebx, &ecx, &edx) != 0 && (ecx & (1u << 9u)) != 0; +#endif +} + +/// whether the CPU has SSSE3, asked once: the answer is kept in an atomic +/// that is initialized at compile time, so that neither a guard of a local +/// static nor a global constructor is needed (threads that ask at the same +/// time all store the same answer) +NLOHMANN_VIEW_ALWAYS_INLINE bool cpu_has_ssse3() noexcept +{ + static std::atomic known{0}; // 0: not asked yet, 1: no, 2: yes + int state = known.load(std::memory_order_relaxed); + if (NLOHMANN_VIEW_UNLIKELY(state == 0)) + { + state = cpu_ssse3() ? 2 : 1; + known.store(state, std::memory_order_relaxed); + } + return state == 2; +} +#endif + +#if NLOHMANN_VIEW_VECTOR_UTF8 +/// Tables of the UTF-8 check of J. Keiser and D. Lemire, "Validating UTF-8 In +/// Less Than One Instruction Per Byte" (2021), as in simdjson ("lookup4"): each +/// maps a nibble (high and low nibble of the previous byte, high nibble of the +/// current byte) to the errors it allows; a byte pair is ill-formed if all +/// three have an error bit in common. +template +struct utf8_lookup4 +{ + static constexpr std::uint8_t too_short = 1u << 0u, too_long = 1u << 1u, overlong_3 = 1u << 2u, too_large = 1u << 3u; + static constexpr std::uint8_t surrogate = 1u << 4u, overlong_2 = 1u << 5u, too_large_1000 = 1u << 6u, overlong_4 = 1u << 6u; + static constexpr std::uint8_t two_conts = 1u << 7u, carry = too_short | too_long | two_conts; + static const std::array byte_1_high; + static const std::array byte_1_low; + static const std::array byte_2_high; +}; + +template +const std::array utf8_lookup4::byte_1_high = +{ + { + too_long, too_long, too_long, too_long, too_long, too_long, too_long, too_long, + two_conts, two_conts, two_conts, two_conts, + too_short | overlong_2, too_short, too_short | overlong_3 | surrogate, too_short | too_large | too_large_1000 | overlong_4 + } +}; + +template +const std::array utf8_lookup4::byte_1_low = +{ + { + carry | overlong_3 | overlong_2 | overlong_4, carry | overlong_2, carry, carry, + carry | too_large, carry | too_large | too_large_1000, carry | too_large | too_large_1000, carry | too_large | too_large_1000, + carry | too_large | too_large_1000, carry | too_large | too_large_1000, carry | too_large | too_large_1000, carry | too_large | too_large_1000, + carry | too_large | too_large_1000, carry | too_large | too_large_1000 | surrogate, carry | too_large | too_large_1000, carry | too_large | too_large_1000 + } +}; + +template +const std::array utf8_lookup4::byte_2_high = +{ + { + too_short, too_short, too_short, too_short, too_short, too_short, too_short, too_short, + static_cast(too_long | overlong_2 | two_conts | overlong_3 | too_large_1000 | overlong_4), + static_cast(too_long | overlong_2 | two_conts | overlong_3 | too_large), + static_cast(too_long | overlong_2 | two_conts | surrogate | too_large), + static_cast(too_long | overlong_2 | two_conts | surrogate | too_large), + too_short, too_short, too_short, too_short + } +}; + +/// the end of scan_string_vector from block, where the vector loop stopped +/// (ill-formed UTF-8, or fewer than 16 bytes left): one byte or sequence at a +/// time, from the start of a sequence that crosses into the block +inline const unsigned char* scan_string_finish(const unsigned char* p, const unsigned char* block, const unsigned char* e, const std::uint8_t* plain) noexcept +{ + for (int i = 1; i <= 3 && block - i >= p; ++i) + { + const unsigned char c = block[-i]; + if (c < 0x80) + { + break; + } + if (c >= 0xC0) + { + const int len = 2 + static_cast(c >= 0xE0) + static_cast(c >= 0xF0); + if (len > i) + { + block -= i; + } + break; + } + } + for (p = block; p != e;) + { + if (*p < 0x80) + { + if (plain[*p] == 0) + { + return p; + } + ++p; + continue; + } + const std::size_t n = validate_one_utf8(p, static_cast(e - p)); + if (n == 0) + { + return p; + } + p += n; + } + return p; +} + +/*! +@brief the rest of a string from p (a character boundary), 16 bytes per step + +The first quote, backslash, or control character is found with vector +compares, and the UTF-8 check covers the bytes up to it. Returns where 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 +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()). +*/ +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 +{ + using lookup = utf8_lookup4<>; + const unsigned char* block = p; +#if NLOHMANN_VIEW_NEON + const uint8x16_t t1h = vld1q_u8(lookup::byte_1_high.data()); + const uint8x16_t t1l = vld1q_u8(lookup::byte_1_low.data()); + const uint8x16_t t2h = vld1q_u8(lookup::byte_2_high.data()); + uint8x16_t prev = vdupq_n_u8(0); + while (e - block >= 16) + { + const uint8x16_t in = vld1q_u8(block); + const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))), vcltq_u8(in, vdupq_n_u8(0x20))); + const uint8x16_t prev1 = vextq_u8(prev, in, 15); + const uint8x16_t sc = vandq_u8(vandq_u8(vqtbl1q_u8(t1h, vshrq_n_u8(prev1, 4)), vqtbl1q_u8(t1l, vandq_u8(prev1, vdupq_n_u8(0x0F)))), vqtbl1q_u8(t2h, vshrq_n_u8(in, 4))); + const uint8x16_t must23 = vorrq_u8(vqsubq_u8(vextq_u8(prev, in, 14), vdupq_n_u8(0xE0 - 0x80)), vqsubq_u8(vextq_u8(prev, in, 13), vdupq_n_u8(0xF0 - 0x80))); + const uint8x16_t err = veorq_u8(vandq_u8(must23, vdupq_n_u8(0x80)), sc); + const std::uint64_t special_bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0); + const std::uint64_t err_bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(vtstq_u8(err, err)), 4)), 0); + if (special_bits != 0) + { + // errors up to the special byte count (an incomplete sequence + // before a quote shows at the quote); the bytes after it do not + const unsigned k = static_cast(count_trailing_zeros(special_bits)) >> 2u; + const std::uint64_t upto = k == 15 ? ~std::uint64_t{0} : + (std::uint64_t{1} << (4u * (k + 1u))) - 1u; + if ((err_bits & upto) == 0) + { + return block + k; + } + break; + } + if (err_bits != 0) + { + break; + } + prev = in; + block += 16; + } +#else + // the same with SSSE3 (pshufb for the table lookups; nibbles from 16-bit + // shifts, as there are no byte shifts) + const __m128i t1h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_high.data()))); // NOLINT(bugprone-casting-through-void) + const __m128i t1l = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_low.data()))); // NOLINT(bugprone-casting-through-void) + const __m128i t2h = _mm_loadu_si128(static_cast(static_cast(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(static_cast(block))); // NOLINT(bugprone-casting-through-void) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_cmpeq_epi8(_mm_subs_epu8(in, _mm_set1_epi8(0x1F)), zero)); // in < 0x20 + const __m128i prev1 = _mm_alignr_epi8(in, prev, 15); + const __m128i sc = _mm_and_si128(_mm_and_si128(_mm_shuffle_epi8(t1h, _mm_and_si128(_mm_srli_epi16(prev1, 4), nibble)), + _mm_shuffle_epi8(t1l, _mm_and_si128(prev1, nibble))), + _mm_shuffle_epi8(t2h, _mm_and_si128(_mm_srli_epi16(in, 4), nibble))); + const __m128i must23 = _mm_or_si128(_mm_subs_epu8(_mm_alignr_epi8(in, prev, 14), _mm_set1_epi8(0xE0 - 0x80)), + _mm_subs_epu8(_mm_alignr_epi8(in, prev, 13), _mm_set1_epi8(0xF0 - 0x80))); + const __m128i err = _mm_xor_si128(_mm_and_si128(must23, _mm_set1_epi8(static_cast(-128))), sc); + const auto special_bits = static_cast(_mm_movemask_epi8(special)); + const auto err_bits = ~static_cast(_mm_movemask_epi8(_mm_cmpeq_epi8(err, zero))) & 0xFFFFu; + if (special_bits != 0) + { + const auto k = static_cast(count_trailing_zeros(static_cast(special_bits))); + if ((err_bits & ((2u << k) - 1u)) == 0) + { + return block + k; + } + break; + } + if (err_bits != 0) + { + break; + } + prev = in; + block += 16; + } +#endif + return scan_string_finish(p, block, e, plain); +} +#endif + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + + +// Scanning primitives of the view's parser. The unrolled checks at fixed +// offsets follow yyjson (https://github.com/ibireme/yyjson, MIT license): the +// loads do not depend on each other, so the CPU can run ahead. Words are read +// with read_eight_bytes(), so nothing here depends on the byte order. + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// 1 for bytes that may appear verbatim in a string: 0x20..0x7F except '"' and '\\' +inline const std::uint8_t* string_plain() noexcept +{ + static const std::array table = + { + { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x00..0x1F + 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x20..0x3F ('"') + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 0x40..0x5F ('\\') + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60..0x7F + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x80..0x9F + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xA0..0xBF + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xC0..0xDF + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xE0..0xFF + } + }; + return table.data(); +} + +NLOHMANN_VIEW_ALWAYS_INLINE bool is_digit(unsigned char c) noexcept +{ + return static_cast(c - '0') <= 9; +} + +/// two bytes as they are in memory (only compared with byte-symmetric patterns) +NLOHMANN_VIEW_ALWAYS_INLINE std::uint16_t load16(const unsigned char* p) noexcept +{ + std::uint16_t w = 0; + std::memcpy(&w, p, 2); + return w; +} + +/// Advance over plain string bytes and well-formed UTF-8. Stops at a quote, +/// a backslash, a control character, ill-formed UTF-8, or the end. The first +/// bytes are checked one by one, so that the position advances by constants +/// in predicted branches: 16 for keys, whose lengths repeat from record to +/// record, and 8 for string values (Value) where a vector loop follows, as +/// their lengths vary more. Longer runs continue 16 bytes at a time with NEON +/// or SSE2, else eight bytes at a time. With SSE2, the run is checked 16 bytes +/// at a time from its first byte instead: on x86-64, one compare that finds +/// the end of most keys and short values is faster than a branch per byte (on +/// AArch64, where a NEON mask costs more and branches predict well, slower). +template +NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* scan_string_run(const unsigned char* p, const unsigned char* e) noexcept +{ + const std::uint8_t* plain = string_plain(); + for (;;) + { +#if NLOHMANN_VIEW_SSE2 + p = vector_plain_run(p, e); +#else + if (e - p >= 16) + { +#define NLOHMANN_VIEW_STEP(i) if (NLOHMANN_VIEW_LIKELY(plain[p[i]] != 0)) {} else { p += (i); goto stop; } + NLOHMANN_VIEW_REPEAT_FIRST_8(NLOHMANN_VIEW_STEP) +#if NLOHMANN_VIEW_VECTOR + if (!Value) // (a plain `!Value || !NLOHMANN_VIEW_VECTOR` is a constant condition for MSVC, C4127) +#endif + { + NLOHMANN_VIEW_REPEAT_SECOND_8(NLOHMANN_VIEW_STEP) + p += 8; + } +#undef NLOHMANN_VIEW_STEP + p += 8; +#if NLOHMANN_VIEW_VECTOR + p = vector_plain_run(p, e); + if (p != e && plain[*p] == 0) + { + goto stop; + } +#else + while (e - p >= 8) + { + const std::uint64_t special = swar_string_special(read_eight_bytes(p)); + if (special != 0) + { + p += count_trailing_zeros(special) / 8; + goto stop; + } + p += 8; + } +#endif + continue; + } +#endif + while (p != e && plain[*p] != 0) + { + ++p; + } + if (p == e) + { + return p; + } +#if !NLOHMANN_VIEW_SSE2 +stop: +#endif + if (*p < 0x80) + { + return p; // quote, backslash, or control character + } +#if NLOHMANN_VIEW_VECTOR_UTF8 +#if NLOHMANN_VIEW_SSSE3_DISPATCH + if (NLOHMANN_VIEW_LIKELY(cpu_has_ssse3())) +#endif + { + // non-ASCII: the vector check, out of line + return scan_string_vector(p, e, plain); + } +#endif +#if !NLOHMANN_VIEW_VECTOR_UTF8 || NLOHMANN_VIEW_SSSE3_DISPATCH + // non-ASCII: a run of well-formed sequences (the library's check, so + // that exactly what json::parse accepts is accepted) + do + { + const std::size_t n = validate_one_utf8(p, static_cast(e - p)); + if (n == 0) + { + return p; + } + p += n; + } + while (p != e && *p >= 0x80); +#endif + } +} + +/// advance over ASCII digits +NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* skip_digits(const unsigned char* p, const unsigned char* e) noexcept +{ + while (e - p >= 16) + { +#define NLOHMANN_VIEW_STEP(i) if (NLOHMANN_VIEW_LIKELY(is_digit(p[i]))) {} else { return p + (i); } + NLOHMANN_VIEW_REPEAT16(NLOHMANN_VIEW_STEP) +#undef NLOHMANN_VIEW_STEP + p += 16; + } + while (p != e && is_digit(*p)) + { + ++p; + } + return p; +} + +/// powers of ten up to 10^19 as integers +inline std::uint64_t int_pow10(unsigned k) noexcept +{ + return ::nlohmann::detail::dtoa_impl::powers_of_ten()[k]; +} + +/// value of 0 < k < 8 digits at p in one step if [p, p + 8) lies below +/// limit, else one digit at a time (whole blocks of eight digits are read by +/// parse_upto19() directly) +NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t parse_upto8(const unsigned char* p, unsigned k, const unsigned char* limit) noexcept +{ + if (NLOHMANN_VIEW_LIKELY(limit - p >= 8)) + { + // move the k digits to the top and pad the vacated low bytes with '0' + const unsigned shift = 8 * (8 - k); + return parse_eight_digits((read_eight_bytes(p) << shift) | (0x3030303030303030u >> (8 * k))); + } + std::uint64_t v = 0; + for (unsigned i = 0; i < k; ++i) + { + v = (v * 10) + static_cast(p[i] - '0'); + } + return v; +} + +/// value of k <= 19 digits at p +NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t parse_upto19(const unsigned char* p, unsigned k, const unsigned char* limit) noexcept +{ + std::uint64_t w = 0; + while (k >= 8) + { + // (eight digits of the token: they lie below limit) + w = (w * 100000000u) + parse_eight_digits(read_eight_bytes(p)); + p += 8; + k -= 8; + } + if (k != 0) + { + w = (w * int_pow10(k)) + parse_upto8(p, k, limit); + } + return w; +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + + +// The view's parser: one pass over the input that emits the node index (see +// node.hpp). The table-driven decoding of \u escapes and the fast paths for +// ": " and indentation follow yyjson (https://github.com/ibireme/yyjson, MIT +// license). + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +enum class error_code : std::uint8_t +{ + none, + empty_input, + unexpected_value, + invalid_literal, + expected_key, + expected_colon, + expected_array_end, + expected_object_end, + trailing_characters, + number_after_minus, + number_after_dot, + number_after_exponent, + number_overflow, + string_missing_quote, + string_control_character, + string_utf8, + string_escape, + string_unicode_hex, + string_surrogate_high, + string_surrogate_low, + comment_start, + comment_unterminated, + input_too_large, +}; + +struct parse_failure +{ + error_code code = error_code::none; + std::size_t offset = 0; ///< byte offset of the offending character +}; + +/// FloatType: the number_float_t of the document, whose overflow parse() rejects +template +class builder +{ + public: + builder(document_data& d, const char* src, std::size_t size) noexcept + : m_doc(d) + , m_begin(reinterpret_cast(src)) + , m_end(m_begin + size) + , m_shallow() + {} + + /// returns false and fills `failure` on error + bool run() + { + cursor c(*this); + return c.run(); + } + + builder(const builder&) = delete; + builder& operator=(const builder&) = delete; + builder(builder&&) = delete; + builder& operator=(builder&&) = delete; + ~builder() = default; + + /// where and why the parse failed (after run() returned false) + const parse_failure& failure() const noexcept + { + return m_failure; + } + + private: + struct frame + { + std::uint32_t idx; + std::uint32_t count; + bool is_object; + }; + + /// 64 frames, left uninitialized: they are filled as containers open (the + /// user-provided constructor keeps the member initialization, which + /// -Weffc++ asks for, from zeroing them; that costs up to 30 % on small + /// documents) + struct frame_stack : std::array + { + frame_stack() noexcept {} // NOLINT(modernize-use-equals-default,hicpp-use-equals-default,cppcoreguidelines-pro-type-member-init,hicpp-member-init): see above + }; + + document_data& m_doc; + const unsigned char* const m_begin; + const unsigned char* const m_end; + parse_failure m_failure{}; + + // the open array/object is in the cursor; enclosing ones on a stack that is + // inline for the first 64 levels + frame_stack m_shallow; + std::vector m_deep{}; + + /// remember an object to index after parsing (out of line, so that the + /// parse loop only has a call for it) + NLOHMANN_VIEW_NOINLINE void note_large_object(std::uint32_t idx) + { + m_doc.large_objects.push_back(idx); + } + + NLOHMANN_VIEW_NOINLINE bool fail(error_code c, const unsigned char* at) noexcept + { + m_failure.code = c; + m_failure.offset = static_cast(at - m_begin); + m_doc.node_count = 0; + return false; + } + + /// a failure (recorded by fail) as a comment() result + const unsigned char* fail_at(error_code c, const unsigned char* at) noexcept + { + fail(c, at); + return nullptr; + } + + /// a decoded string: p after its closing quote (nullptr: an error), and + /// its bytes in the arena + struct decoded + { + const unsigned char* p; + std::size_t start; + std::size_t len; + }; + + decoded failed(error_code c, const unsigned char* at) noexcept + { + fail(c, at); + return decoded{nullptr, 0, 0}; + } + + /// the comment at p (*p == '/'): the position after it, or nullptr on error + NLOHMANN_VIEW_NOINLINE const unsigned char* comment(const unsigned char* p) + { + if (m_end - p < 2) + { + ++p; + return fail_at(error_code::comment_start, p); + } + if (p[1] == '/') + { + p += 2; + // (as in parse(), a null byte is the end of the input, so it is + // left for the caller to see) + while (p != m_end && *p != '\n' && *p != '\r' && !(NulIsEnd && *p == 0)) + { + ++p; + } + return p; + } + if (p[1] == '*') + { + p += 2; + for (;;) + { + if (p == m_end || (NulIsEnd && *p == 0)) + { + return fail_at(error_code::comment_unterminated, p); + } + if (*p == '*' && p + 1 != m_end && p[1] == '/') + { + p += 2; + return p; + } + ++p; + } + } + ++p; + return fail_at(error_code::comment_start, p); + } + + /// the index is full (n nodes, parsed up to at): extrapolate the node + /// count from the nodes per input byte so far (with headroom, and at least + /// 1.5 times as many), so that dense inputs regrow once instead of + /// doubling repeatedly; returns the new node array + NLOHMANN_VIEW_NOINLINE node* grow(std::size_t n, const unsigned char* at) + { + // (n nodes are written; at the limit there is no room for another one) + const std::uint64_t limit = document_data::max_nodes(); + // LCOV_EXCL_START (a node array that fills the address space) + if (NLOHMANN_VIEW_UNLIKELY(n >= limit)) + { + document_data::throw_bad_alloc(); + } + // LCOV_EXCL_STOP + const std::uint64_t done = static_cast(at - m_begin) + 1; + const std::uint64_t guess = static_cast(n) * static_cast(m_end - m_begin + 1) / done; + const std::uint64_t grown = guess + (guess / 4) + 64; // a variable: GCC calls a cast of the sum useless where std::uint64_t is std::size_t + // (n is below 2^32: the input is smaller than 4 GiB; the sum cannot wrap) + const std::uint64_t wanted = (std::max)(grown, static_cast(n) + (n / 2) + 64); + m_doc.node_count = n; // the number of nodes reserve() copies into the new array + // (a count beyond the limit is cut: the index does not grow beyond what can be addressed) + m_doc.reserve(static_cast((std::min)(wanted, limit))); + return m_doc.nodes; + } + + /// the four hex digits of a unicode escape at p as a code unit, and p + /// moved past them; or -1, and p at the first bad digit (one table lookup + /// per digit and a single check: the library's table, after yyjson's + /// read_hex_u16) + NLOHMANN_VIEW_ALWAYS_INLINE int hex4(const unsigned char*& p) noexcept + { + if (NLOHMANN_VIEW_LIKELY(m_end - p >= 4)) + { + const int cp = hex_codepoint(p); + if (NLOHMANN_VIEW_LIKELY(cp >= 0)) + { + p += 4; + return cp; + } + } + p = hex4_error(p); + return -1; + } + + /// hex4() failed: the first bad digit (none: p) + NLOHMANN_VIEW_NOINLINE const unsigned char* hex4_error(const unsigned char* p) noexcept + { + if (m_end - p >= 4) + { + while (is_hex(*p)) + { + ++p; + } + } + return p; + } + + /// 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 + /// the arena + NLOHMANN_VIEW_NOINLINE decoded slow_string(const unsigned char* s, const unsigned char* p) + { + // single-character escapes; 0: invalid (and 'u', handled separately) + static const std::array simple_escape = + { + { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, '"', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '/', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '\\', 0, 0, 0, + 0, 0, '\b', 0, 0, 0, '\f', 0, 0, 0, 0, 0, 0, 0, '\n', 0, 0, 0, '\r', 0, '\t', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + } + }; + // The write position is kept in locals: the stores of characters into + // the arena may alias the members of the builder, which would then be + // reloaded after every one. It is stored back where the arena grows, + // and by `write_back` on every return. + const unsigned char* const end = m_end; + const std::size_t start = arena_used(); + char* w = m_arena_write; + const char* w_end = m_arena_end; + const arena_write_back write_back(*this, w); + { + const auto n = static_cast(p - s); + if (n <= 16 && end - s >= 16) + { + arena_ensure(w, w_end, 16); + std::memcpy(w, s, 16); + } + else if (n != 0) + { + // (an empty run leaves an empty arena unallocated, and memcpy() + // must not be passed its null pointer even for zero bytes) + arena_ensure(w, w_end, n); + std::memcpy(w, s, n); + } + w += n; + } + for (;;) + { + if (p == end) + { + return failed(error_code::string_missing_quote, p); + } + const unsigned char c = *p; + if (c == '"') + { + ++p; + // (from w: `write_back` stores it only after the return value + // is made; the arena is allocated, since an escape was written) + return decoded{p, start, static_cast(w - m_doc.arena.data()) - start}; + } + if (c != '\\') + { + // (without JSON_STRICT_NUL_HANDLING, a NUL between values ends + // the input, as in json::parse; inside a string, a NUL is still + // a control character and an error) + 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') + { + 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); + } + cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); + } + arena_ensure(w, w_end, 4); + w = put_utf8(w, cp); + } + else if (NLOHMANN_VIEW_LIKELY(d < 128 && simple_escape[d] != 0)) + { + arena_ensure(w, w_end, 1); + *w++ = simple_escape[d]; + } + else + { + --p; + return failed(error_code::string_escape, p); + } +#if NLOHMANN_VIEW_NEON + if (p != end && *p == '\\') + { + continue; // consecutive escapes ("\u00e4\u00f6"): no run in between + } + // The first 16 bytes of the run are checked one by one, in predicted branches, and copied + // as one block: where the runs between escapes are short, the vector step below would put + // its longer dependency chain (load, compare, mask, count, add) on p for each of them, + // and be slower than these steps (the scanner of the keys works the same way) + { + if (end - p >= 16) + { + const std::uint8_t* const plain = string_plain(); + std::size_t steps = 16; +#define NLOHMANN_VIEW_STEP(i) if (NLOHMANN_VIEW_LIKELY(plain[p[i]] != 0)) {} else { steps = (i); goto run_found; } + NLOHMANN_VIEW_REPEAT16(NLOHMANN_VIEW_STEP) +#undef NLOHMANN_VIEW_STEP +run_found: + arena_ensure(w, w_end, 16); + std::memcpy(w, p, 16); // (only the first `steps` bytes are kept) + w += steps; + p += steps; + if (steps < 16) + { + if (*p >= 0x80) + { + // non-ASCII: the UTF-8 check, and the rest of the run + const unsigned char* const r = p; + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; + } + continue; + } + } + } +#else + if (p != end && (*p == '\\' || *p == '"')) + { + continue; // consecutive escapes ("\u00e4\u00f6"), or the end of the string: no run in between + } +#endif +#if NLOHMANN_VIEW_VECTOR + // the plain bytes up to the next quote, backslash, or control + // character are copied as they are scanned, 16 bytes at a time + for (;;) + { + if (NLOHMANN_VIEW_UNLIKELY(end - p < 16)) + { + const unsigned char* const r = p; + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; + break; + } + arena_ensure(w, w_end, 16); + const unsigned k = copy_plain_run16(p, w); + w += k; + p += k; + if (k == 16) + { + continue; + } + if (NLOHMANN_VIEW_UNLIKELY(*p >= 0x80)) + { + // non-ASCII: the UTF-8 check, and the rest of the run + const unsigned char* const r = p; + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; + } + break; + } +#else + const unsigned char* const r = p; + p = scan_string_run(p, end); + const auto n = static_cast(p - r); + arena_ensure(w, w_end, n); + std::memcpy(w, r, n); + w += n; +#endif + } + } + + /// does the float token [s, p) overflow FloatType? (parse() rejects it) + NLOHMANN_VIEW_NOINLINE static bool float_overflows(const unsigned char* s, const unsigned char* p) + { + const auto* const first = reinterpret_cast(s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + const auto* const last = reinterpret_cast(p); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + const char* const dot = std::find(first, last, '.'); + const char* const exponent = std::find_if(first, last, [](char c) + { + return c == 'e' || c == 'E'; + }); + const auto v = convert_float(first, last, dot == last ? std::string::npos : static_cast(dot - first), + static_cast(exponent - first)); + return v > (std::numeric_limits::max)() || v < -(std::numeric_limits::max)(); + } + + /// do the `digits` digits of a magnitude at d exceed `limit`, the largest + /// magnitude that fits (a string literal: N counts its terminating NUL, so + /// it has N - 1 digits)? + template + static NLOHMANN_VIEW_ALWAYS_INLINE bool digits_exceed(const unsigned char* d, std::size_t digits, const char (&limit)[N]) noexcept // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays): a string literal + { + if (NLOHMANN_VIEW_LIKELY(digits < N - 1)) + { + return false; + } + return digits > N - 1 || std::memcmp(d, limit, N - 1) > 0; + } + + static bool is_hex(unsigned char c) noexcept + { + return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); + } + + // decode arena: the vector in the document, written through a raw + // pointer (resized ahead in large steps; trimmed when parsing succeeds) + char* m_arena_write = nullptr; + char* m_arena_end = nullptr; + + /// room for n more bytes at w, the write position kept in a local, with + /// w_end the end of the arena. (A lambda instead is not always inlined, + /// and then w would be kept in memory, as m_arena_write is.) + NLOHMANN_VIEW_ALWAYS_INLINE void arena_ensure(char*& w, const char*& w_end, std::size_t n) + { + if (NLOHMANN_VIEW_UNLIKELY(static_cast(w_end - w) < n)) + { + m_arena_write = w; + arena_grow(n); + w = m_arena_write; + w_end = m_arena_end; + } + } + + /// stores a write position kept in a local back into m_arena_write when + /// the function that keeps it returns, on every path + class arena_write_back + { + public: + arena_write_back(builder& b, char*& w) noexcept + : m_builder(b) + , m_w(w) + {} + ~arena_write_back() + { + m_builder.m_arena_write = m_w; + } + + arena_write_back(const arena_write_back&) = delete; + arena_write_back& operator=(const arena_write_back&) = delete; + arena_write_back(arena_write_back&&) = delete; + arena_write_back& operator=(arena_write_back&&) = delete; + + private: + builder& m_builder; + char*& m_w; + }; + + NLOHMANN_VIEW_ALWAYS_INLINE std::size_t arena_used() const noexcept + { + return m_arena_write != nullptr ? static_cast(m_arena_write - m_doc.arena.data()) : 0; + } + + NLOHMANN_VIEW_NOINLINE void arena_grow(std::size_t n) + { + const std::size_t used = arena_used(); + m_doc.arena.resize((std::max)(m_doc.arena.size() * 2, used + n + 256)); + m_arena_write = m_doc.arena.data() + used; + m_arena_end = m_doc.arena.data() + m_doc.arena.size(); + } + + /// UTF-8 encoding of cp at w (room for 4 bytes); returns the position after + /// it. (A returned pointer stays in a register; writing through a + /// reference to m_arena_write would store and reload it around every byte.) + static char* put_utf8(char* w, int cp) noexcept + { + if (cp < 0x80) + { + *w++ = static_cast(cp); + } + else if (cp < 0x800) + { + *w++ = static_cast(0xC0 | (cp >> 6)); + *w++ = static_cast(0x80 | (cp & 0x3F)); + } + else if (cp < 0x10000) + { + *w++ = static_cast(0xE0 | (cp >> 12)); + *w++ = static_cast(0x80 | ((cp >> 6) & 0x3F)); + *w++ = static_cast(0x80 | (cp & 0x3F)); + } + else + { + *w++ = static_cast(0xF0 | (cp >> 18)); + *w++ = static_cast(0x80 | ((cp >> 12) & 0x3F)); + *w++ = static_cast(0x80 | ((cp >> 6) & 0x3F)); + *w++ = static_cast(0x80 | (cp & 0x3F)); + } + return w; + } + + /// a compile-time option as a runtime condition: testing the template + /// argument directly makes a condition like `TrailingCommas && c == ']'` + /// constant when the option is off, which MSVC reports as C4127 + static NLOHMANN_VIEW_ALWAYS_INLINE bool enabled(bool option) noexcept + { + return option; + } + + /// The parse state and the parser proper. The cursor is a local object of + /// run() whose address never escapes (everything it calls out of line is a + /// member of the builder and gets the positions it needs), so that the + /// compiler keeps the state in registers instead of reloading it from + /// memory after every node store and call. + struct cursor + { + explicit cursor(builder& owner) noexcept + : cold(owner) + , b(owner.m_begin) + , p(owner.m_begin) + , e(owner.m_end) + {} + + builder& cold; ///< out-of-line helpers and state that needs no registers + const unsigned char* const b; + const unsigned char* p; + const unsigned char* const e; + node* base = nullptr; + node* out = nullptr; + node* cap = nullptr; + + // the open array/object + std::uint32_t cur_idx = 0; + std::uint32_t cur_count = 0; + bool cur_is_object = false; + std::size_t depth = 0; + std::size_t max_depth = 0; + + NLOHMANN_VIEW_ALWAYS_INLINE bool run() + { + cold.m_doc.reserve(estimate_nodes(reinterpret_cast(b), static_cast(e - b))); + 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) + { + p += 3; // byte order mark + } + if (!ws()) + { + return false; + } + if (p == e || (NulIsEnd && *p == 0)) + { + return fail(error_code::empty_input); + } + + // root value + switch (cur()) + { + case '{': + open(value_t::object); + ++p; + goto obj_first; + case '[': + open(value_t::array); + ++p; + goto arr_first; + default: + if (!scalar()) + { + return false; + } + goto root_done; + } + + // value dispatch, expanded once for array elements and once for member + // values: each jump has its own history (arrays tend to hold one kind of + // value), and the continuation needs no branch on the container kind; + // after a scalar, the switch is left to the label that follows it +#define NLOHMANN_VIEW_VALUE \ + switch (cur()) \ + { \ + case '"': \ + if (NLOHMANN_VIEW_UNLIKELY(!string())) { return false; } \ + break; \ + case '{': \ + open(value_t::object); \ + ++p; \ + goto obj_first; \ + case '[': \ + open(value_t::array); \ + ++p; \ + goto arr_first; \ + case '-': \ + if (NLOHMANN_VIEW_UNLIKELY(!number())) { return false; } \ + break; \ + case '0': case '1': case '2': case '3': case '4': \ + case '5': case '6': case '7': case '8': case '9': \ + if (NLOHMANN_VIEW_UNLIKELY(!number())) { return false; } \ + break; \ + case 't': \ + if (NLOHMANN_VIEW_UNLIKELY(!literal_true())) { return false; } \ + break; \ + case 'f': \ + if (NLOHMANN_VIEW_UNLIKELY(!literal_false())) { return false; } \ + break; \ + case 'n': \ + if (NLOHMANN_VIEW_UNLIKELY(!literal_null())) { return false; } \ + break; \ + default: \ + return fail(error_code::unexpected_value); \ + } + +arr_first: + if (!ws()) + { + return false; + } + if (cur() == ']') + { + ++p; + goto close_container; + } +value: + NLOHMANN_VIEW_VALUE +arr_next: + ++cur_count; + if (!ws()) + { + return false; + } + if (NLOHMANN_VIEW_LIKELY(cur() == ',')) + { + ++p; + if (!ws()) + { + return false; + } + if (enabled(TrailingCommas) && cur() == ']') + { + ++p; + goto close_container; + } + goto value; + } + if (cur() == ']') + { + ++p; + goto close_container; + } + return fail(error_code::expected_array_end); + +obj_first: + if (!ws()) + { + return false; + } + if (cur() == '}') + { + ++p; + goto close_container; + } +obj_key: + if (NLOHMANN_VIEW_UNLIKELY(cur() != '"')) + { + return fail(error_code::expected_key); + } + if (NLOHMANN_VIEW_UNLIKELY(!string())) + { + return false; + } + if (NLOHMANN_VIEW_LIKELY(cur() == ':' && (Sentinel || e - p >= 2) && p[1] == ' ')) + { + p += 2; // ": " (pretty-printed input; a fast path of yyjson) + } + else + { + if (!ws()) + { + return false; + } + if (NLOHMANN_VIEW_UNLIKELY(cur() != ':')) + { + return fail(error_code::expected_colon); + } + ++p; + } + if (!ws()) + { + return false; + } + NLOHMANN_VIEW_VALUE +#undef NLOHMANN_VIEW_VALUE +obj_next: + ++cur_count; + if (!ws()) + { + return false; + } + if (NLOHMANN_VIEW_LIKELY(cur() == ',')) + { + ++p; + if (!ws()) + { + return false; + } + if (enabled(TrailingCommas) && cur() == '}') + { + ++p; + goto close_object; + } + goto obj_key; + } + if (cur() == '}') + { + ++p; + goto close_object; + } + return fail(error_code::expected_object_end); + +close_object: + // a large object gets a hash index for its keys (objects only, so that closing + // an array pays nothing for this) + if (NLOHMANN_VIEW_UNLIKELY(cur_count >= document_data::index_min_members)) + { + cold.note_large_object(cur_idx); + } + +close_container: + close(); + if (NLOHMANN_VIEW_UNLIKELY(depth == 0)) + { + goto root_done; + } + if (cur_is_object) + { + goto obj_next; + } + goto arr_next; + +root_done: + if (!ws()) + { + return false; + } + if (p != e && !(NulIsEnd && *p == 0)) + { + return fail(error_code::trailing_characters); + } + cold.m_doc.node_count = static_cast(out - base); + cold.m_doc.max_depth = max_depth; + // (so that size() is the number of bytes used: the arena grows ahead + // in large steps, and shrink_to_fit() keeps size() bytes) + cold.m_doc.arena.resize(cold.arena_used()); + return true; + } + + NLOHMANN_VIEW_ALWAYS_INLINE bool fail(error_code c) noexcept + { + return cold.fail(c, p); + } + + /// the current byte, or 0 at the end. With a NUL-terminated input + /// (Sentinel) the terminator is read instead of checking the bounds; a 0 + /// never matches a JSON token, so the error paths tell the end apart. + NLOHMANN_VIEW_ALWAYS_INLINE unsigned char cur() const noexcept + { + if (Sentinel) + { + return *p; + } + return p != e ? *p : 0; + } + + /// root scalar + NLOHMANN_VIEW_ALWAYS_INLINE bool scalar() + { + switch (cur()) + { + case '"': + return string(); + case 't': + return literal_true(); + case 'f': + return literal_false(); + case 'n': + return literal_null(); + case '-': + return number(); + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + return number(); + default: + return fail(error_code::unexpected_value); + } + } + + NLOHMANN_VIEW_ALWAYS_INLINE bool literal_true() + { + return literal("true", value_t::boolean, node_flags::is_true); + } + + NLOHMANN_VIEW_ALWAYS_INLINE bool literal_false() + { + return literal("false", value_t::boolean, 0); + } + + NLOHMANN_VIEW_ALWAYS_INLINE bool literal_null() + { + return literal("null", value_t::null, 0); + } + + /// skip whitespace (and comments); false on a malformed comment + NLOHMANN_VIEW_ALWAYS_INLINE bool ws() + { + const unsigned char c = cur(); + if (NLOHMANN_VIEW_LIKELY(c > ' ' && (!Comments || c != '/'))) + { + return true; // no whitespace: the common case in minified input + } + return ws_slow(); + } + + NLOHMANN_VIEW_ALWAYS_INLINE bool ws_slow() + { + for (;;) + { + if (cur() == ' ' && (Sentinel || e - p >= 2) && p[1] > ' ' && (!Comments || p[1] != '/')) + { + ++p; // single space, e.g. after ':' or ',' + return true; + } + if (cur() == '\n' || cur() == '\r') + { + // (a branch, not an add of the comparison: p must not + // wait for the byte after the line break) + if (NLOHMANN_VIEW_UNLIKELY(cur() == '\r') && (Sentinel || e - p >= 2) && p[1] == '\n') + { + p += 2; + } + else + { + ++p; + } + // indentation: two spaces per step, fixed offsets (after yyjson) + while (e - p >= 32) + { +#define NLOHMANN_VIEW_STEP(i) if (NLOHMANN_VIEW_LIKELY(load16(p + (std::ptrdiff_t{2} * (i))) == 0x2020)) {} else { p += std::ptrdiff_t{2} * (i); goto indent_done; } + NLOHMANN_VIEW_REPEAT16(NLOHMANN_VIEW_STEP) +#undef NLOHMANN_VIEW_STEP + p += 32; + } +indent_done: + ; + } + for (unsigned char c = cur(); c == ' ' || c == '\n' || c == '\r' || c == '\t'; c = cur()) + { + ++p; + } + if (enabled(Comments) && cur() == '/') + { + const unsigned char* const q = cold.comment(p); + if (q == nullptr) + { + return false; + } + p = q; + continue; + } + return true; + } + } + + /// append a node: (kind, flags, extra, off) and the second word (len, or + /// an integer's value); two stores on little-endian targets + NLOHMANN_VIEW_ALWAYS_INLINE node* push_node(value_t k, std::uint8_t flags, std::uint16_t extra, std::size_t off, std::uint64_t second) + { + if (NLOHMANN_VIEW_UNLIKELY(out == cap)) + { + const auto n = static_cast(out - base); + base = cold.grow(n, p); + out = base + n; + cap = base + cold.m_doc.node_capacity; + } + node* n = out++; +#if NLOHMANN_VIEW_LITTLE_ENDIAN + const std::uint64_t first = static_cast(k) | (static_cast(flags) << 8) + | (static_cast(extra) << 16) | (static_cast(off) << 32); + std::memcpy(reinterpret_cast(n), &first, 8); + std::memcpy(reinterpret_cast(n) + 8, &second, 8); +#else + n->kind = static_cast(k); + n->flags = flags; + n->extra = extra; + n->off = static_cast(off); + // len is the low half of the second word, next the high half + // (not a native word over both, which swaps them on big-endian) + n->len = static_cast(second); + n->next = static_cast(second >> 32); +#endif + return n; + } + + NLOHMANN_VIEW_ALWAYS_INLINE void open(value_t k) + { + // (base is read after push_node(), which moves the node array when it + // grows; in one expression the order of the two is unspecified) + const node* const n = push_node(k, 0, 0, static_cast(p - b), 0); + const auto idx = static_cast(n - base); + if (depth != 0) + { + if (NLOHMANN_VIEW_LIKELY(depth <= 64)) + { + // field by field: a frame put together on the stack and + // copied would be read back wider than it was written, + // and that load waits until the stores are done + frame& f = cold.m_shallow[depth - 1]; + f.idx = cur_idx; + f.count = cur_count; + f.is_object = cur_is_object; + } + else + { + cold.m_deep.push_back(frame{cur_idx, cur_count, cur_is_object}); + } + } + ++depth; + max_depth = (std::max)(max_depth, depth); + cur_idx = idx; + cur_count = 0; + cur_is_object = k == value_t::object; + } + + NLOHMANN_VIEW_ALWAYS_INLINE void close() + { + node& n = base[cur_idx]; + n.len = cur_count; + n.next = static_cast(out - base) - cur_idx; + if (--depth != 0) + { + if (NLOHMANN_VIEW_LIKELY(depth <= 64)) + { + const frame& f = cold.m_shallow[depth - 1]; + cur_idx = f.idx; + cur_count = f.count; + cur_is_object = f.is_object; + } + else + { + const frame f = cold.m_deep.back(); + cold.m_deep.pop_back(); + cur_idx = f.idx; + cur_count = f.count; + cur_is_object = f.is_object; + } + } + } + + /// the literal `text` at p (N counts the terminating NUL) + template + NLOHMANN_VIEW_ALWAYS_INLINE bool literal(const char (&text)[N], value_t k, std::uint8_t flags) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays): a string literal + { + const std::size_t n = N - 1; + if (NLOHMANN_VIEW_UNLIKELY(e - p < static_cast(n) || std::memcmp(p, text, n) != 0)) + { + return fail(error_code::invalid_literal); + } + push_node(k, flags, 0, static_cast(p - b), n); + p += n; + return true; + } + + /// a number at p; the sign is known from the dispatch (so that p does + /// not have to wait for the first byte) + template + NLOHMANN_VIEW_ALWAYS_INLINE bool number() + { + const unsigned char* const s = p; + if (negative) + { + ++p; + } + const unsigned char* const int_start = p; + if (cur() == '0') + { + ++p; + } + else if (NLOHMANN_VIEW_LIKELY(cur() >= '1' && cur() <= '9')) + { + p = skip_digits(p + 1, e); + } + else + { + return fail(error_code::number_after_minus); + } + const auto int_digits = static_cast(p - int_start); + std::size_t frac_digits = 0; + bool is_float = false; + if (cur() == '.') + { + ++p; + const unsigned char* const f0 = p; + p = skip_digits(p, e); + if (NLOHMANN_VIEW_UNLIKELY(p == f0)) + { + return fail(error_code::number_after_dot); + } + frac_digits = static_cast(p - f0); + is_float = true; + } + std::int64_t exponent = 0; + if ((cur() | 0x20) == 'e') + { + ++p; + bool exp_negative = false; + if (cur() == '+' || cur() == '-') + { + exp_negative = *p == '-'; + ++p; + } + if (NLOHMANN_VIEW_UNLIKELY(!is_digit(cur()))) + { + return fail(error_code::number_after_exponent); + } + while (is_digit(cur())) + { + if (exponent < 100000) + { + exponent = (exponent * 10) + (*p - '0'); + } + ++p; + } + if (exp_negative) + { + exponent = -exponent; + } + is_float = true; + } + + value_t kind = value_t::number_float; + if (!is_float) + { + // integers that do not fit become floats, as in parse() + if (negative) + { + kind = digits_exceed(int_start, int_digits, "9223372036854775808") ? value_t::number_float : value_t::number_integer; + } + else + { + kind = digits_exceed(int_start, int_digits, "18446744073709551615") ? value_t::number_float : value_t::number_unsigned; + } + } + // parse() rejects floats that overflow; only numbers whose magnitude + // could reach the largest FloatType (1e308 for double, 1e38 for + // float) need the conversion + if (NLOHMANN_VIEW_UNLIKELY(static_cast(int_digits) + exponent > std::numeric_limits::max_exponent10 - 8 && kind == value_t::number_float)) + { + if (builder::float_overflows(s, p)) + { + p = s; + return fail(error_code::number_overflow); + } + } + const auto layout = static_cast((int_digits < 255 ? int_digits : 255) | ((frac_digits < 255 ? frac_digits : 255) << 8)); + auto second = static_cast(p - s); + if (kind != value_t::number_float) + { + // integers are converted now, while their digits are in cache + const std::uint64_t m = int_digits <= 19 ? parse_upto19(int_start, static_cast(int_digits), e) + : (parse_upto19(int_start, 19, e) * 10) + static_cast(int_start[19] - '0'); + // (0 - m rather than -m: unary minus on an unsigned type is + // MSVC's C4146, an error under /sdl; both give the bit pattern + // of the negative std::int64_t) + second = negative ? 0 - m : m; + } + push_node(kind, 0, layout, static_cast(s - b), second); + return true; + } + + /// a string at p: a value (Value) or a key + template + NLOHMANN_VIEW_ALWAYS_INLINE bool string() + { + ++p; // opening quote + const unsigned char* const s = p; + p = scan_string_run(p, e); + if (NLOHMANN_VIEW_LIKELY(cur() == '"')) + { + push_node(value_t::string, 0, 0, static_cast(s - b), static_cast(p - s)); + ++p; + return true; + } + const decoded r = cold.slow_string(s, p); + if (r.p == nullptr) + { + return false; + } + p = r.p; + push_node(value_t::string, node_flags::escaped, 0, r.start, r.len); + return true; + } + }; +}; + +/// run the builder with compile-time options +template +inline bool build_with(document_data& d, const char* src, std::size_t size, bool sentinel, parse_failure& failure) +{ + if (sentinel) + { + builder bld(d, src, size); + const bool ok = bld.run(); + failure = bld.failure(); + return ok; + } + builder bld(d, src, size); + const bool ok = bld.run(); + failure = bld.failure(); + return ok; +} + +/// sentinel: src[size] is readable and 0 (e.g. std::string); FloatType: the +/// number_float_t of the document +template +inline bool build(document_data& d, const char* src, std::size_t size, bool comments, bool trailing_commas, bool sentinel, parse_failure& failure) +{ + if (comments) + { + return trailing_commas ? build_with(d, src, size, sentinel, failure) + : build_with(d, src, size, sentinel, failure); + } + return trailing_commas ? build_with(d, src, size, sentinel, failure) + : build_with(d, src, size, sentinel, failure); +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // sort, stable_sort +#include // size_t +#include // string +#include // move, pair +#include // vector + +// #include +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +// Equality of views, and of views with basic_json values, with the semantics +// of basic_json's operator== applied to the values parse() would produce: +// numbers compare by value across their types, an object is compared by its +// members with duplicate keys resolved as parse() resolves them (the last +// value, at the position of the first occurrence), and in document order if +// the object type keeps an order (ordered_json), by key otherwise. + +/// one side of a comparison: a view +template +class view_side +{ + public: + using string_view_t = typename View::string_view_t; + + explicit view_side(const View& v) noexcept + : m_view(v) + {} + + value_t type() const noexcept + { + return m_view.type(); + } + + std::size_t size() const noexcept + { + return m_view.size(); + } + + string_view_t string() const + { + return m_view.get_string(); + } + + /// a number, boolean, or null as a basic_json value (no allocation) + BasicJsonType scalar() const + { + switch (m_view.type()) + { + case value_t::number_integer: + return BasicJsonType(m_view.template get()); + case value_t::number_unsigned: + return BasicJsonType(m_view.template get()); + case value_t::number_float: + return BasicJsonType(m_view.template get()); + case value_t::boolean: + return BasicJsonType(m_view.template get()); + 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); + } + } + + void elements(std::vector& out) const + { + out.reserve(m_view.size()); + for (const View e : m_view) + { + out.emplace_back(e); + } + } + + /// the members as parse() keeps them: one per key, the last value at the + /// position of the first occurrence; in that order, or sorted by key + void members(std::vector>& out, bool ordered) const + { + struct member + { + string_view_t key; + View value; + std::size_t position; + }; + std::vector all; + all.reserve(m_view.size()); + std::size_t position = 0; + for (auto it = m_view.begin(); it != m_view.end(); ++it) + { + all.push_back(member{it.key(), it.value(), position++}); + } + std::stable_sort(all.begin(), all.end(), [](const member & a, const member & b) + { + return a.key < b.key; + }); + std::vector unique; + unique.reserve(all.size()); + for (std::size_t i = 0; i < all.size();) + { + std::size_t last = i; + while (last + 1 < all.size() && all[last + 1].key == all[i].key) + { + ++last; + } + unique.push_back(member{all[i].key, all[last].value, all[i].position}); + i = last + 1; + } + if (ordered) + { + std::sort(unique.begin(), unique.end(), [](const member & a, const member & b) + { + return a.position < b.position; + }); + } + out.reserve(unique.size()); + for (const member& m : unique) + { + out.emplace_back(m.key, view_side(m.value)); + } + } + + private: + View m_view; +}; + +/// the other side of a comparison: a basic_json value +template +class json_side +{ + public: + using string_view_t = StringView; + + explicit json_side(const BasicJsonType& j) noexcept + : m_json(&j) + {} + + value_t type() const noexcept + { + return m_json->type(); + } + + std::size_t size() const noexcept + { + return m_json->size(); + } + + string_view_t string() const + { + const auto& s = m_json->template get_ref(); + return string_view_t(s.data(), s.size()); + } + + BasicJsonType scalar() const + { + return *m_json; + } + + void elements(std::vector& out) const + { + out.reserve(m_json->size()); + for (const auto& e : *m_json) + { + out.emplace_back(e); + } + } + + void members(std::vector>& out, bool ordered) const + { + out.reserve(m_json->size()); + for (auto it = m_json->cbegin(); it != m_json->cend(); ++it) + { + out.emplace_back(string_view_t(it.key().data(), it.key().size()), json_side(it.value())); + } + if (!ordered) + { + std::sort(out.begin(), out.end(), [](const std::pair& a, const std::pair& b) + { + return a.first < b.first; + }); + } + } + + private: + const BasicJsonType* m_json; +}; + +inline bool is_number(value_t t) noexcept +{ + return t == value_t::number_integer || t == value_t::number_unsigned || t == value_t::number_float; +} + +/// whether two sides are equal; iterative, so that the nesting depth is +/// limited by memory only +template +bool equal(const A& a0, const B& b0) +{ + using string_view_t = typename A::string_view_t; + const bool ordered = is_ordered_map::value; + + struct frame + { + std::vector elements_a{}; + std::vector elements_b{}; + std::vector> members_a{}; + std::vector> members_b{}; + bool object = false; + std::size_t next = 0; + }; + std::vector stack; + A a = a0; + B b = b0; + for (;;) + { + const value_t ta = a.type(); + const value_t tb = b.type(); + if (ta == value_t::discarded || tb == value_t::discarded) + { + // basic_json decides (JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON) + if (ta != tb || !(BasicJsonType(value_t::discarded) == BasicJsonType(value_t::discarded))) + { + return false; + } + } + else + { + if (ta != tb && !(is_number(ta) && is_number(tb))) // numbers of different types can be equal + { + return false; + } + if (ta == value_t::string) + { + if (a.string() != b.string()) + { + return false; + } + } + else if (ta == value_t::array || ta == value_t::object) + { + if (a.size() != b.size() && ta == value_t::array) + { + return false; + } + frame f; + f.object = ta == value_t::object; + if (f.object) + { + a.members(f.members_a, ordered); + b.members(f.members_b, ordered); + if (f.members_a.size() != f.members_b.size()) + { + return false; + } + } + else + { + a.elements(f.elements_a); + b.elements(f.elements_b); + } + stack.push_back(std::move(f)); + } + else if (a.scalar() != b.scalar()) // numbers (also of different types), null, boolean + { + return false; + } + } + + // the next pair of values + for (;;) + { + if (stack.empty()) + { + return true; + } + frame& f = stack.back(); + const std::size_t count = f.object ? f.members_a.size() : f.elements_a.size(); + if (f.next == count) + { + stack.pop_back(); // (f dangles now, and is bound again after continue) + continue; + } + if (f.object) + { + if (!(f.members_a[f.next].first == f.members_b[f.next].first)) + { + return false; + } + a = f.members_a[f.next].second; + b = f.members_b[f.next].second; + } + else + { + a = f.elements_a[f.next]; + b = f.elements_b[f.next]; + } + ++f.next; + break; + } + } +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // array +#include // isinf, isnan +#include // size_t +#include // int64_t, uint8_t, uint32_t, uint64_t +#include // memmove +#include // numeric_limits +#include // string, to_string +#include // decay, enable_if, integral_constant, is_arithmetic, is_convertible, is_floating_point, is_same, is_signed +#include // forward +#include // vector + +// #include +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // max, min +#include // size_t +#include // uint8_t, uint32_t +#include // memcpy +#include // less +#include // unique_ptr +#include // move + +// #include +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // min +#include // size_t +#include // string + +// #include +// #include + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +// Exceptions are thrown out of line, so that the accessors that may throw stay +// small enough to be inlined. + +[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_type_error(exception_id id, const char* prefix, const char* type) +{ + NLOHMANN_VIEW_THROW(type_error::create(id, concat(prefix, type), nullptr)); +} + +[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_type_error(exception_id id, const std::string& msg) +{ + NLOHMANN_VIEW_THROW(type_error::create(id, msg, nullptr)); +} + +[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_out_of_range(exception_id id, const std::string& msg) +{ + NLOHMANN_VIEW_THROW(out_of_range::create(id, msg, nullptr)); +} + +[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_invalid_iterator(exception_id id, const char* msg) +{ + NLOHMANN_VIEW_THROW(invalid_iterator::create(id, msg, nullptr)); +} + +/// 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) +{ + NLOHMANN_VIEW_THROW(parse_error::create(id, 0, msg, nullptr)); +} + +/// out_of_range.401: the array index @a idx is not below the size +template +[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_out_of_range(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")); +} + +/// 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) +{ + throw_out_of_range(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 +template +[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_key_not_found(const Key& key) +{ + throw_out_of_range(exception_id::key_not_found, concat("key '", key, "' not found")); +} + +/// SAX handler of the error replay: accepts every event without storing it, +/// and throws the exception the parser reports +template +struct throwing_sax +{ + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + using binary_t = typename BasicJsonType::binary_t; + + bool null() + { + return true; + } + + bool boolean(bool /*unused*/) + { + return true; + } + + bool number_integer(number_integer_t /*unused*/) + { + return true; + } + + bool number_unsigned(number_unsigned_t /*unused*/) + { + return true; + } + + bool number_float(number_float_t /*unused*/, const string_t& /*unused*/) + { + return true; + } + + bool string(string_t& /*unused*/) + { + return true; + } + + bool binary(binary_t& /*unused*/) + { + return true; + } + + bool start_object(std::size_t /*unused*/) + { + return true; + } + + bool key(string_t& /*unused*/) + { + return true; + } + + bool end_object() + { + return true; + } + + bool start_array(std::size_t /*unused*/) + { + return true; + } + + bool end_array() + { + return true; + } + + /// (a template, as in json_sax_dom_parser: the parser passes the concrete + /// exception type, which is thrown as is) + template + [[noreturn]] bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, const Exception& ex) + { + NLOHMANN_VIEW_THROW(ex); + } +}; + +/*! +@brief throw the exception BasicJsonType::parse would throw for this input + +The view accepts exactly the inputs parse() accepts, so on a failure the +library parser is run on the same bytes: it throws the exception parse() would +throw, with the same message, position, and "last read" token. The replay +uses a SAX handler that stores nothing, so it only lexes and parses up to the +error, without building values. The error path is cold, so this costs nothing +on valid input. Should parse() accept the input nevertheless (a bug), the +view's own failure is reported. +*/ +template +[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_parse_failure(const parse_failure& f, const char* src, std::size_t size, + bool ignore_comments, bool ignore_trailing_commas) +{ + if (f.code == error_code::input_too_large) + { + // (the limit is detail::view::max_input_size(): 4 GiB minus 16 bytes) + NLOHMANN_VIEW_THROW(out_of_range::create(exception_id::document_too_large, "input of 4294967280 bytes or more is not supported by json_document", nullptr)); + } + throwing_sax sax; + const bool accepted = BasicJsonType::sax_parse(src, src + size, &sax, input_format_t::json, true, ignore_comments, ignore_trailing_commas); + // LCOV_EXCL_START (only if parse() accepts what the view rejects: a bug) + static_cast(accepted); + + position_t pos; + const std::size_t off = (std::min)(f.offset, size); + pos.chars_read_total = off + 1; + std::size_t line_start = 0; + for (std::size_t i = 0; i < off; ++i) + { + if (src[i] == '\n') + { + ++pos.lines_read; + line_start = i + 1; + } + } + pos.chars_read_current_line = off + 1 - line_start; + NLOHMANN_VIEW_THROW(parse_error::create(exception_id::syntax_error, pos, "syntax error while parsing value", nullptr)); + // LCOV_EXCL_STOP +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include + + +// The storage of edits. Edits never move or resize the parsed index: every +// value keeps its node, so views stay valid. New values and element sequences +// live in chunks that never move; strings and number tokens written by edits +// live in the edit arena. An array/object whose elements change gets +// node_flags::moved: its elements then live in a separate sequence (a header +// node, then the entries), whose entries link to the values (kind_link). + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +inline document_data::edit_state& edit_state_of(document_data& d) +{ + if (!d.edits) + { + d.edits.reset(new document_data::edit_state()); // NOLINT(cppcoreguidelines-owning-memory,modernize-make-unique): owned by the unique_ptr; std::make_unique is C++14 + } + return *d.edits; +} + +/// k consecutive nodes that never move (new values and blocks) +inline node* alloc_nodes(document_data& d, std::size_t k) +{ + document_data::edit_state& e = edit_state_of(d); + if (NLOHMANN_VIEW_UNLIKELY(static_cast(e.chunk_end - e.chunk_cur) < k)) + { + if (k >= e.chunk_next / 2) + { + // a large request (half of the next chunk or more) gets a chunk of + // its own, so that the rest of the current chunk stays available + // for smaller ones + std::unique_ptr own(new node[k]()); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + e.chunks.push_back(std::move(own)); + e.bytes += k * sizeof(node); + return e.chunks.back().get(); + } + // a smaller request leaves the rest of the current chunk unused + // (fewer than k nodes, and k is less than half of the new chunk) + const std::size_t count = e.chunk_next; + std::unique_ptr fresh(new node[count]()); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + e.chunks.push_back(std::move(fresh)); + e.chunk_cur = e.chunks.back().get(); + e.chunk_end = e.chunk_cur + count; + e.chunk_next = (std::min)(e.chunk_next * 2, std::size_t{65536}); + e.bytes += count * sizeof(node); + } + node* const r = e.chunk_cur; + e.chunk_cur += k; + return r; +} + +/// The capacity of the edit arena after it grows by n bytes (`used` of `cap` +/// are taken): doubled, or what is needed plus some room, but never more than +/// the 4 GiB - 1 bytes that the 32-bit offsets of nodes can address. An error +/// if n more bytes do not fit even then. +inline std::size_t text_capacity(std::size_t cap, std::size_t used, std::size_t n) +{ + constexpr std::size_t limit = 0xFFFFFFFFu; + if (NLOHMANN_VIEW_UNLIKELY(used > limit || n > limit - used)) + { + throw_out_of_range(exception_id::document_too_large, "edits of 4 GiB or more are not supported by json_document"); + } + const std::size_t needed = used + n; + const std::size_t wanted = needed + (std::min)(limit - needed, std::size_t{256}); + const std::size_t doubled = cap > limit / 2 ? limit : cap * 2; + return (std::max)(doubled, wanted); +} + +/// copy n bytes into the edit arena and return their offset; a new buffer +/// leaves the old one alive, so that string views into it remain valid; n is +/// 0 for an empty string (written by an edit, or the key of a new member) +inline std::uint32_t append_text(document_data& d, const char* s, std::size_t n) +{ + document_data::edit_state& e = edit_state_of(d); + if (NLOHMANN_VIEW_UNLIKELY(e.text_cap - e.text_used < n)) + { + const std::size_t cap = text_capacity(e.text_cap, e.text_used, n); + std::unique_ptr fresh(new char[cap]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + if (NLOHMANN_VIEW_LIKELY(e.text_used != 0)) // (0 only for the first buffer, and then texts is empty) + { + std::memcpy(fresh.get(), e.texts.back().get(), e.text_used); + } + e.texts.push_back(std::move(fresh)); + e.text_cap = cap; + e.bytes += cap; + d.base[2] = e.texts.back().get(); + } + const auto off = static_cast(e.text_used); + if (n != 0) + { + std::memcpy(e.texts.back().get() + e.text_used, s, n); + } + e.text_used += n; + return off; +} + +/// the capacity in nodes of the block of a moved container (0: a fixed +/// sequence, the elements of a new value) +inline std::size_t moved_capacity(const document_data& d, const node* n) noexcept +{ + return d.edits->moved_cap[n->off]; +} + +/// Make room for one more moved container. This is the part of set_moved() +/// that can throw: a caller that changes a node before it calls set_moved() +/// calls this first, so that a failure leaves the node as it was. +inline void reserve_moved(document_data& d) +{ + document_data::edit_state& e = edit_state_of(d); + if (e.moved.size() >= 0xFFFFFFFFu) + { + throw_out_of_range(exception_id::document_too_large, "more than 4294967295 edited arrays and objects are not supported by json_document"); // LCOV_EXCL_LINE + } + if (e.moved.size() == e.moved.capacity() || e.moved_cap.size() == e.moved_cap.capacity()) + { + // both grow before either changes, so that the push_backs cannot throw + // (both conditions are needed: reserve() may allocate more than asked, + // so the two capacities can differ) + e.moved.reserve((e.moved.size() * 2) + 16); + e.moved_cap.reserve((e.moved.size() * 2) + 16); + } +} + +/// let container n take its elements from `seq` (header node first); cannot +/// throw if n is moved already or reserve_moved() was called +inline void set_moved(document_data& d, node* n, node* seq, std::size_t cap) +{ + document_data::edit_state& e = edit_state_of(d); + if ((n->flags & node_flags::moved) != 0) + { + e.moved[n->off] = seq; + e.moved_cap[n->off] = cap; + return; + } + reserve_moved(d); + e.moved.push_back(seq); + e.moved_cap.push_back(cap); + n->off = static_cast(e.moved.size() - 1); + n->flags = static_cast(n->flags | node_flags::moved | node_flags::is_new); +} + +/// Make the elements of container n a growable block with room for `extra` +/// more nodes, and return its header. The entries link to the existing +/// values, which stay where they are. A block that grows is copied (its old +/// space is not reused). +inline node* block_of(document_data& d, node* n, std::size_t extra) +{ + if ((n->flags & node_flags::moved) != 0 && moved_capacity(d, n) != 0) + { + node* const h = d.edits->moved[n->off]; + if (h->next + extra <= moved_capacity(d, n)) + { + return h; + } + const std::size_t cap = (std::max)(2 * moved_capacity(d, n), h->next + extra); + node* const nh = alloc_nodes(d, cap); + std::memcpy(nh, h, h->next * sizeof(node)); + set_moved(d, n, nh, cap); + return nh; + } + reserve_moved(d); // (so that set_moved() below cannot throw: the links are marked before) + const bool object = n->kind == static_cast(value_t::object); + const std::size_t used = 1 + (static_cast(n->len) * (object ? 2 : 1)); + const std::size_t cap = used + extra; + node* const h = alloc_nodes(d, cap); + *h = node{}; + h->kind = n->kind; + h->len = n->len; + h->next = static_cast(used); + node* o = h + 1; + for (const node* c = d.first_child_edited(n), *e = d.child_end_edited(n); c != e;) + { + if (object) + { + *o++ = *c++; // the key + } + make_link(*o, const_cast(document_data::deref(c))); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to the document + ++o; + c = document_data::after(c); + } + set_moved(d, n, h, cap); + return h; +} + +/// The container whose elements include `target`; nullptr for the root, for +/// a value that is no longer part of the document, and for a value that is +/// only reached through a link. Values never move between allocations, so +/// the path to `target` stays inside the allocation that holds it (the parsed +/// index, or one new value), where the extent of each container (`next`) +/// still covers its original subtree. +inline node* find_parent(const document_data& d, const node* target) +{ + const std::less lt{}; + const node* lo = d.nodes; + const node* hi = d.nodes + d.node_count; + const node* c = d.nodes; + if (lt(target, lo) || !lt(target, hi)) + { + if (!d.edits) + { + return nullptr; // LCOV_EXCL_LINE (nodes outside the index exist only after edits) + } + auto it = d.edits->regions.upper_bound(target); + if (it == d.edits->regions.begin()) + { + return nullptr; // LCOV_EXCL_LINE (an array/object with elements is in the index or a new value) + } + --it; + lo = it->first; + hi = lo + lo->next; + if (!lt(target, hi)) + { + return nullptr; // LCOV_EXCL_LINE (a single-node value, reached through a link) + } + // the root of a new value is the element sequence of its owner, or a linked value + c = it->second != nullptr ? it->second : lo; + } + if (target == lo) + { + return nullptr; + } + for (;;) + { + if (!is_container(*c)) + { + return nullptr; // LCOV_EXCL_LINE (the value is inside c) + } + const bool object = c->kind == static_cast(value_t::object); + const node* down = nullptr; + for (const node* p = d.first_child_edited(c), *e = d.child_end_edited(c); p != e;) + { + const node* const at = object ? p + 1 : p; + const node* const v = document_data::deref(at); + if (v == target) + { + return const_cast(c); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to the document + } + if (is_container(*v) && !lt(v, lo) && lt(v, target) && lt(target, v + v->next)) + { + down = v; + break; + } + p = document_data::after(at); + } + if (down == nullptr) + { + return nullptr; + } + c = down; + } +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // size_t +#include // uint16_t, uint32_t, uint64_t +#include // memcmp, memcpy +#include // numeric_limits +#include // integral_constant, is_integral, is_same + +// #include +// #include + +// #include + +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // size_t +#include // uint32_t, uint64_t + +// #include +// #include + +// #include + +// #include + + +// Hash indexes of the keys of large objects, so that a lookup does not compare thousands +// of keys (as Boost.JSON switches from a linear search to a hash table for +// large objects). An object with document_data::index_min_members members or +// more gets an open-addressing table after parsing; its node stores the +// number of the table (1-based) in `extra`. A slot holds the offset of a key +// node from its object node (0: empty). Of duplicate keys, the first is kept, +// as for the linear search. +// +// The hash is not seeded, so keys chosen to collide could make the build +// quadratic. A key therefore sits at most index_max_displacement slots away +// from its home slot; if a key would sit further away, the table is dropped +// and the object is searched linearly (like a small one). For the same +// reason, a lookup visits at most index_max_displacement + 1 slots. + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// the farthest a key may sit from its home slot (a table with at most half of +/// its slots in use gives random keys a distance of about 50 for millions of +/// members; and every member costs at most this many steps while building) +constexpr std::size_t index_max_displacement = 64; + +// a table has at least 2 * len slots (len >= index_min_members), so the probe +// window of index_max_displacement + 1 slots stays far smaller than the table +// and a probe never wraps around onto its own start +static_assert(document_data::index_min_members >= 2 * index_max_displacement, + "a table must be much larger than the probe window of a lookup"); + +/// hash of a key: its bytes, eight at a time (in a fixed byte order), where the +/// last word may overlap the one before (so that no loop over single bytes is +/// needed); a key of fewer than eight bytes is read as two overlapping words of +/// four or two bytes (in the native byte order: the hash is not stored, and the +/// same on both sides of a lookup) +inline std::uint64_t key_hash(const char* s, std::size_t n) noexcept +{ + std::uint64_t h = 0x9E3779B97F4A7C15u * (n + 1); + const auto* p = reinterpret_cast(s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + std::uint64_t w = 0; + if (n >= 8) + { + const unsigned char* const last = p + n - 8; + while (p < last) + { + h = (h ^ read_eight_bytes(p)) * 0xBF58476D1CE4E5B9u; + h ^= h >> 29u; + p += 8; + } + w = read_eight_bytes(last); + } + else if (n >= 4) + { + std::uint32_t head = 0; + std::uint32_t tail = 0; + std::memcpy(&head, p, 4); + std::memcpy(&tail, p + n - 4, 4); + w = static_cast(head) | (static_cast(tail) << 32u); + } + else if (n >= 2) + { + std::uint16_t head = 0; + std::uint16_t tail = 0; + std::memcpy(&head, p, 2); + std::memcpy(&tail, p + n - 2, 2); + w = static_cast(head) | (static_cast(tail) << 16u); + } + else if (n == 1) + { + w = p[0]; + } + // (a product only carries bits upwards, and the slot is taken from the low + // bits: the high bits of the last word are folded down before the multiplication) + h = (h ^ w ^ (w >> 29u)) * 0x94D049BB133111EBu; + return h ^ (h >> 31u); +} + +/// whether the n <= 16 bytes at a and b are equal, as two overlapping words +/// (the first and the last 8, 4, or 2 bytes) and without a call +NLOHMANN_VIEW_ALWAYS_INLINE bool same_short_bytes(const unsigned char* a, const unsigned char* b, std::size_t n) noexcept +{ + if (n >= 8) + { + std::uint64_t x0 = 0; + std::uint64_t x1 = 0; + std::uint64_t y0 = 0; + std::uint64_t y1 = 0; + std::memcpy(&x0, a, 8); + std::memcpy(&x1, a + n - 8, 8); + std::memcpy(&y0, b, 8); + std::memcpy(&y1, b + n - 8, 8); + return x0 == y0 && x1 == y1; + } + if (n >= 4) + { + std::uint32_t x0 = 0; + std::uint32_t x1 = 0; + std::uint32_t y0 = 0; + std::uint32_t y1 = 0; + std::memcpy(&x0, a, 4); + std::memcpy(&x1, a + n - 4, 4); + std::memcpy(&y0, b, 4); + std::memcpy(&y1, b + n - 4, 4); + return x0 == y0 && x1 == y1; + } + if (n >= 2) + { + std::uint16_t x0 = 0; + std::uint16_t x1 = 0; + std::uint16_t y0 = 0; + std::uint16_t y1 = 0; + std::memcpy(&x0, a, 2); + std::memcpy(&x1, a + n - 2, 2); + std::memcpy(&y0, b, 2); + std::memcpy(&y1, b + n - 2, 2); + return x0 == y0 && x1 == y1; + } + return n == 0 || *a == *b; +} + +/// build the table of a large object +inline void build_object_index(document_data& d, node* obj) +{ + if (d.indexes.size() >= 0xFFFFu) + { + return; // LCOV_EXCL_LINE (the number must fit `extra`; more large objects are searched linearly) + } + std::size_t cap = 16; + while (cap < 2 * static_cast(obj->len)) + { + cap *= 2; + } + const std::size_t start = d.index_slots.size(); + d.index_slots.resize(start + cap, 0); + std::uint32_t* const table = d.index_slots.data() + start; + const std::size_t mask = cap - 1; + for (const node* k = document_data::first_child(obj), *end = document_data::child_end(obj); k != end; k = document_data::after(k + 1)) + { + const char* const key = d.str(*k); + std::size_t slot = conditional_static_cast(key_hash(key, k->len)) & mask; + bool duplicate = false; + std::size_t distance = 0; + while (table[slot] != 0) + { + const node* const other = obj + table[slot]; + if (d.key_equals(*other, key, k->len)) + { + duplicate = true; // keep the first + break; + } + if (distance == index_max_displacement) + { + // too many keys share a home region + d.index_slots.resize(start); // no table: the object is searched linearly + return; + } + ++distance; + slot = (slot + 1) & mask; + } + if (!duplicate) + { + table[slot] = static_cast(k - obj); + } + } + d.indexes.push_back(document_data::object_index{start, static_cast(mask)}); + obj->extra = static_cast(d.indexes.size()); +} + +/// build the tables of the large objects the parser noted +inline void build_object_indexes(document_data& d) +{ + for (const std::uint32_t i : d.large_objects) + { + build_object_index(d, d.nodes + i); + } +} + +/// the key node of the first member with this key of an indexed object, or +/// nullptr +inline const node* find_indexed(const document_data& d, const node* obj, const char* key, std::size_t n) noexcept +{ + const document_data::object_index& ix = d.indexes[obj->extra - 1u]; + const std::uint32_t* const table = d.index_slots.data() + ix.start; + std::size_t slot = conditional_static_cast(key_hash(key, n)) & ix.mask; + const auto* const k_bytes = reinterpret_cast(key); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + // (the loop for longer keys comes first, as the code is laid out; it is not the more common case) + if (NLOHMANN_VIEW_UNLIKELY(n <= 16)) + { + // (up to 16 bytes, the bytes are compared as two words, without a call) + for (std::size_t distance = 0; distance <= index_max_displacement; ++distance) + { + const std::uint32_t s = table[slot]; + if (s == 0) + { + return nullptr; + } + const node* const k = obj + s; + if (k->len == n && same_short_bytes(reinterpret_cast(d.str(*k)), k_bytes, n)) // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + { + return k; + } + slot = (slot + 1) & ix.mask; + } + return nullptr; + } + for (std::size_t distance = 0; distance <= index_max_displacement; ++distance) + { + const std::uint32_t s = table[slot]; + if (s == 0) + { + return nullptr; + } + const node* const k = obj + s; + if (d.key_equals(*k, key, n)) + { + return k; + } + slot = (slot + 1) & ix.mask; + } + return nullptr; // (no key sits further from its home slot) +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// equality test for strings of one length n <= 16: two overlapping loads per +/// string (the first and the last 8, 4, or 2 bytes) replace a memcmp, and no +/// byte outside [s, s + n) is read +class short_key +{ + public: + short_key(const unsigned char* k, std::size_t n) noexcept + : m_n(n) + { + load(k, m_a, m_b); + } + + NLOHMANN_VIEW_ALWAYS_INLINE bool matches(const unsigned char* s) const noexcept + { + std::uint64_t a = 0; + std::uint64_t b = 0; + load(s, a, b); + return a == m_a && b == m_b; + } + + private: + template + static NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t load_word(const unsigned char* s) noexcept + { + T w = 0; + std::memcpy(&w, s, sizeof(T)); + return w; + } + + NLOHMANN_VIEW_ALWAYS_INLINE void load(const unsigned char* s, std::uint64_t& a, std::uint64_t& b) const noexcept + { + if (m_n >= 8) + { + a = load_word(s); + b = load_word(s + m_n - 8); + } + else if (m_n >= 4) + { + a = load_word(s); + b = load_word(s + m_n - 4); + } + else if (m_n >= 2) + { + a = load_word(s); + b = load_word(s + m_n - 2); + } + else + { + a = m_n == 1 ? s[0] : 0; + b = 0; + } + } + + std::size_t m_n; + std::uint64_t m_a = 0; + std::uint64_t m_b = 0; +}; + +/// the key node of the first member of an object with the given key, or +/// nullptr (the search stops at the first match; materialize() and parse() +/// keep the last value of a duplicate key instead); most keys are rejected by +/// their length, from the index alone +template +const node* find_member(const document_data& d, const node* object, const char* key, std::size_t n) noexcept +{ + using nav = navigation; + if (NLOHMANN_VIEW_UNLIKELY(object->extra != 0) && (!nav::editable() || (object->flags & node_flags::moved) == 0)) + { + return find_indexed(d, object, key, n); // a large object (whose members have not been edited) + } + const node* const end = nav::end(d, object); + const auto* const k = reinterpret_cast(key); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + if (NLOHMANN_VIEW_LIKELY(n <= 16)) + { + const short_key probe(k, n); + for (const node* m = nav::first(d, object); m != end; m = document_data::after(m + 1)) + { + if (m->len == n && probe.matches(reinterpret_cast(d.str(*m)))) // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + { + return m; + } + } + return nullptr; + } + for (const node* m = nav::first(d, object); m != end; m = document_data::after(m + 1)) + { + if (d.key_equals(*m, key, n)) + { + return m; + } + } + return nullptr; +} + +/// the number of members of an object with this key (duplicates included) +template +std::size_t count_members(const document_data& d, const node* object, const char* key, std::size_t n) noexcept +{ + using nav = navigation; + std::size_t result = 0; + const node* const end = nav::end(d, object); + for (const node* m = nav::first(d, object); m != end; m = document_data::after(m + 1)) + { + if (d.key_equals(*m, key, n)) + { + ++result; + } + } + return result; +} + +/// whether an integer type is accepted as an array index by the view's +/// operator[] and at(): every integer type but bool and size_t, which has its +/// own overload +template +struct is_index_type : std::integral_constant < bool, + std::is_integral::value && !std::is_same::value && !std::is_same::value > +{}; + +/// an integer as an index: negative values, and values that do not fit a +/// size_t, map to the largest size_t (out of range for every array) +template +SizeType to_index(IntegerType idx) noexcept +{ + const IntegerType zero = 0; + const auto result = static_cast(idx); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): negative values are mapped below + return (idx < zero || static_cast(result) != idx) ? (std::numeric_limits::max)() : result; +} + +/// the entry of the element of an array at an index below its size (a link +/// in the moved sequences of editable documents) +template +const node* element_at(const document_data& d, const node* array, std::size_t idx) noexcept +{ + using nav = navigation; + const node* e = nav::first(d, array); + if (nav::editable() && (array->flags & node_flags::moved) != 0 && d.edits->moved_cap[array->off] != 0) + { + return e + idx; // a growable block: one link per element + } + if (array->next == array->len + 1u && (!nav::editable() || (array->flags & node_flags::moved) == 0)) + { + return e + idx; // no element has a subtree: the elements are the nodes after the array + } + for (std::size_t i = 0; i < idx; ++i) + { + e = document_data::after(e); + } + return e; +} + +/// the entry of the last element of a non-empty array, or the key of the +/// last member of a non-empty object +template +const node* last_child(const document_data& d, const node* container) noexcept +{ + const std::size_t value_offset = container->kind == static_cast(value_t::object) ? 1 : 0; + const node* const end = navigation::end(d, container); + const node* last = navigation::first(d, container); + if (value_offset == 0 && container->next == container->len + 1u && (!navigation::editable() || (container->flags & node_flags::moved) == 0)) + { + return last + (container->len - 1u); // an array without subtrees: the elements are the nodes after it + } + for (const node* c = document_data::after(last + value_offset); c != end; c = document_data::after(c + value_offset)) + { + last = c; + } + return last; +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN + +template +class basic_json_view; + +namespace detail +{ +namespace view +{ + +/// the index of the first true condition (the number of conditions if none is) +template +struct first_true : std::integral_constant {}; + +template +struct first_true : std::integral_constant < int, 1 + first_true::value > {}; + +/// Checks a string the way basic_json's serializer does when it writes it +/// (type_error.316 with the same message), so that an editable document +/// only holds valid UTF-8: the error is at the first byte that no +/// well-formed sequence can continue with (Unicode, Table 3-7). +inline void check_utf8(const char* s, std::size_t n) +{ + const auto* const p = reinterpret_cast(s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + for (std::size_t i = 0; i < n;) + { + const unsigned char c = p[i]; + if (c < 0x80) + { + ++i; + continue; + } + const utf8_sequence_shape shape = utf8_shape(c); + const std::size_t len = shape.length; + if (len == 0) + { + throw_type_error(exception_id::invalid_utf8, concat("invalid UTF-8 byte at index ", std::to_string(i), ": 0x", hex_byte(c))); + } + for (std::size_t k = 1; k < len; ++k) + { + if (i + k == n) + { + throw_type_error(exception_id::invalid_utf8, concat("incomplete UTF-8 string; last byte: 0x", hex_byte(p[n - 1]))); + } + const unsigned char b = p[i + k]; + if (b < (k == 1 ? shape.second_lo : 0x80) || b > (k == 1 ? shape.second_hi : 0xBF)) + { + throw_type_error(exception_id::invalid_utf8, concat("invalid UTF-8 byte at index ", std::to_string(i + k), ": 0x", hex_byte(b))); + } + } + i += len; + } +} + +/*! +@brief the edits of an editable basic_json_document + +Values are accepted as views (of any document), BasicJsonType values, and +everything BasicJsonType can be constructed from. The source text is never +written: new values go to storage owned by the document (see +edit_storage.hpp). +*/ +template +class editor +{ + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + using string_view_t = typename View::string_view_t; + using nav = navigation; + + public: + explicit editor(document_data& d) noexcept + : m_doc(d) + {} + + /// replace a value; returns its view + template + View set(const View& target, V&& value) + { + node* const slot = own(target); + const encoded e = encode(std::forward(value)); + assign(slot, e, nullptr, false); + return View(&m_doc, slot); + } + + /// set a member (appended if missing; a null value becomes an object); + /// returns a view of the member value + template + View set(const View& object, string_view_t key, V&& value) + { + node* const o = own(object); + if (o->kind != static_cast(value_t::object) && o->kind != static_cast(value_t::null)) + { + throw_type_error(exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", object.type_name()); + } + check_utf8(key.data(), key.size()); + const encoded e = encode(std::forward(value)); + if (o->kind == static_cast(value_t::null)) + { + become_empty(o, value_t::object); + } + // an existing member: assign it (and drop later duplicates, so that + // lookups, iteration, and materialize() agree) + node* slot = nullptr; + bool duplicates = false; + for (const node* k = nav::first(m_doc, o), *end = nav::end(m_doc, o); k != end; k = document_data::after(k + 1)) + { + if (key_equals(*k, key)) + { + if (slot != nullptr) + { + duplicates = true; + break; + } + slot = const_cast(nav::value(k + 1)); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to this document + } + } + if (slot != nullptr) + { + if (duplicates) + { + erase_members(o, key, true); + } + assign(slot, e, o, true); + return View(&m_doc, slot); + } + const node k = string_node(key.data(), key.size()); + slot = new_slot(e); + node* const h = block_of(m_doc, o, 2); + h[h->next] = k; + make_link(h[h->next + 1], slot); + h->next += 2; + ++h->len; + ++o->len; + return View(&m_doc, slot); + } + + /// assign an existing array element; returns a view of it + template + View set(const View& array, std::size_t idx, V&& value) + { + node* const a = own(array); + if (a->kind != static_cast(value_t::array)) + { + throw_type_error(exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", array.type_name()); + } + check_index(idx, a->len); + const encoded e = encode(std::forward(value)); + node* const slot = const_cast(nav::value(element_at(m_doc, a, idx))); // NOLINT(cppcoreguidelines-pro-type-const-cast) + assign(slot, e, a, true); + return View(&m_doc, slot); + } + + /// append to an array (a null value becomes an array); returns a view of + /// the new element + template + View push_back(const View& array, V&& value) + { + node* const a = own(array); + if (a->kind != static_cast(value_t::array) && a->kind != static_cast(value_t::null)) + { + throw_type_error(exception_id::push_back_wrong_type, "cannot use push_back() with ", array.type_name()); + } + const encoded e = encode(std::forward(value)); + if (a->kind == static_cast(value_t::null)) + { + become_empty(a, value_t::array); + } + node* const slot = new_slot(e); + node* const h = block_of(m_doc, a, 1); + make_link(h[h->next], slot); + ++h->next; + ++h->len; + ++a->len; + return View(&m_doc, slot); + } + + /// insert into an array before position idx (idx <= size()); returns a + /// view of the new element + template + View insert(const View& array, std::size_t idx, V&& value) + { + node* const a = own(array); + if (a->kind != static_cast(value_t::array)) + { + throw_type_error(exception_id::insert_wrong_type, "cannot use insert() with ", array.type_name()); + } + check_index(idx, a->len + 1); + const encoded e = encode(std::forward(value)); + node* const slot = new_slot(e); + node* const h = block_of(m_doc, a, 1); + std::memmove(h + 2 + idx, h + 1 + idx, (h->next - 1 - idx) * sizeof(node)); + make_link(h[1 + idx], slot); + ++h->next; + ++h->len; + ++a->len; + return View(&m_doc, slot); + } + + /// remove all members with this key; returns their number + std::size_t erase(const View& object, string_view_t key) + { + node* const o = own(object); + if (o->kind != static_cast(value_t::object)) + { + throw_type_error(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)) + { + if (key_equals(*k, key)) + { + return erase_members(o, key, false); + } + } + return 0; + } + + /// remove an array element + void erase(const View& array, std::size_t idx) + { + node* const a = own(array); + if (a->kind != static_cast(value_t::array)) + { + throw_type_error(exception_id::erase_wrong_type, "cannot use erase() with ", array.type_name()); + } + check_index(idx, a->len); + node* const h = block_of(m_doc, a, 0); + std::memmove(h + 1 + idx, h + 2 + idx, (h->next - 2 - idx) * sizeof(node)); + --h->next; + --h->len; + --a->len; + } + + private: + /// an encoded value: a scalar node, or the root of a new array/object + struct encoded + { + node scalar{}; + node* region = nullptr; + }; + + /// the node of a view of this document + node* own(const View& v) + { + if (NLOHMANN_VIEW_UNLIKELY(v.m_doc != &m_doc || v.m_node == nullptr)) + { + throw_invalid_iterator(exception_id::iterator_from_other_value, "view does not belong to this document"); + } + edit_state_of(m_doc); + return const_cast(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) + { + if (idx >= limit) + { + throw_array_index_out_of_range(idx); + } + } + + bool key_equals(const node& k, string_view_t key) const noexcept + { + return m_doc.key_equals(k, key.data(), key.size()); + } + + /// remove the members with this key (all, or all but the first) from an object + std::size_t erase_members(node* o, string_view_t key, bool keep_first) + { + node* const h = block_of(m_doc, o, 0); + node* w = h + 1; + std::size_t erased = 0; + bool kept = false; + for (const node* r = h + 1, *end = h + h->next; r != end; r += 2) + { + const bool match = key_equals(*r, key); + if (match && (kept || !keep_first)) + { + ++erased; + continue; + } + kept = kept || match; + if (w != r) + { + w[0] = r[0]; + w[1] = r[1]; + } + w += 2; + } + h->next = static_cast(w - h); + h->len -= static_cast(erased); + o->len -= static_cast(erased); + return erased; + } + + /// turn a null into an empty array/object in place + static void become_empty(node* n, value_t k) noexcept + { + const std::uint8_t linked = n->flags & node_flags::linked; + *n = node{}; + n->kind = static_cast(k); + n->flags = static_cast(node_flags::is_new | linked); + n->next = 1; + } + + /// Replace the value at slot; `parent` is the container whose elements + /// include slot (if known). + void assign(node* slot, const encoded& e, node* parent, bool parent_known) + { + // an entry of a moved sequence links to the slot: it can take any extent + const std::uint8_t linked = slot->flags & node_flags::linked; + if (e.region == nullptr) + { + if (is_container(*slot) && slot->next > 1 && slot != m_doc.nodes && linked == 0) + { + // The slot spans its old elements in the enclosing sequence, but + // a scalar is one node: the enclosing container first switches to + // links (then the extent of the slot no longer matters). Looking + // for the container is linear in the size of the document, so + // links (which are marked in the slot) avoid it. + node* const p = parent_known ? parent : find_parent(m_doc, slot); + if (p != nullptr && ((p->flags & node_flags::moved) == 0 || moved_capacity(m_doc, p) == 0)) + { + block_of(m_doc, p, 0); + } + } + *slot = e.scalar; + slot->flags = static_cast(slot->flags | linked); + return; + } + // an array/object: the slot keeps its extent (so that the enclosing + // sequence still steps over it), and the elements come from the new + // sequence + const node* const r = e.region; + const std::uint32_t extent = is_container(*slot) ? slot->next : 1; + const bool was_moved = (slot->flags & node_flags::moved) != 0; + // Everything that can throw happens before the slot changes: a slot + // that is a container without the moved flag would show its old + // elements. reserve_moved() makes the set_moved() below, which sets + // the flag, safe; the entry of `regions` exists already (encode() + // added it), so that the assignment at the end does not allocate. + if (!was_moved) + { + reserve_moved(m_doc); + } + slot->kind = r->kind; + slot->extra = 0; + slot->len = r->len; + slot->next = extent; + // (set_moved() adds the moved flag to a slot that does not have it yet) + slot->flags = static_cast((was_moved ? node_flags::moved | node_flags::is_new : 0) | linked); + set_moved(m_doc, slot, e.region, 0); + edit_state_of(m_doc).regions[e.region] = slot; + } + + /// a node for a new element (links point to it; it never moves) + node* new_slot(const encoded& e) + { + if (e.region != nullptr) + { + return e.region; + } + node* const s = alloc_nodes(m_doc, 1); + *s = e.scalar; + return s; + } + + ////////////// + // encoding // + ////////////// + + template + using encode_tag = std::integral_constant; + + template + struct is_view : std::false_type {}; + + template + struct is_view> : std::true_type {}; + + template + encoded encode(V&& v) + { + using D = typename std::decay::type; + return encode_impl(std::forward(v), encode_tag::value, + std::is_same::value, + std::is_same::value, + std::is_same::value, + std::is_arithmetic::value, + std::is_convertible::value>::value> {}); + } + + /// a view of any document (copied; nothing is shared with it) + template + encoded encode_impl(const basic_json_view& v, encode_tag<0> /*view*/) + { + if (NLOHMANN_VIEW_UNLIKELY(v.m_node == nullptr)) + { + throw_type_error(exception_id::type_mismatch, "type must be a value, but is ", "discarded"); + } + encoded r; + if (!is_container(*v.m_node)) + { + r.scalar = copy_scalar(*v.m_doc, *v.m_node); + return r; + } + r.region = alloc_nodes(m_doc, count_nodes(*v.m_doc, v.m_node)); + fill_nodes(*v.m_doc, v.m_node, r.region); + edit_state_of(m_doc).regions.emplace(r.region, nullptr); + return r; + } + + encoded encode_impl(const BasicJsonType& j, encode_tag<1> /*json*/) + { + encoded r; + if (!j.is_structured()) + { + r.scalar = json_scalar(j); + return r; + } + r.region = alloc_nodes(m_doc, count_nodes(j)); + fill_nodes(j, r.region); + edit_state_of(m_doc).regions.emplace(r.region, nullptr); + return r; + } + + encoded encode_impl(std::nullptr_t /*unused*/, encode_tag<2> /*null*/) + { + encoded r; + r.scalar = plain_node(value_t::null); + return r; + } + + encoded encode_impl(bool b, encode_tag<3> /*boolean*/) + { + encoded r; + r.scalar = plain_node(value_t::boolean); + r.scalar.flags = static_cast(r.scalar.flags | (b ? node_flags::is_true : 0)); + return r; + } + + template + encoded encode_impl(T x, encode_tag<4> /*number*/) + { + encoded r; + r.scalar = number_node(x, std::integral_constant::value, std::is_signed::value>::value> {}); + return r; + } + + template + encoded encode_impl(const T& s, encode_tag<5> /*string*/) + { + const string_view_t sv(s); + check_utf8(sv.data(), sv.size()); + encoded r; + r.scalar = string_node(sv.data(), sv.size()); + return r; + } + + template + encoded encode_impl(T&& x, encode_tag<6> /*other*/) + { + return encode_impl(BasicJsonType(std::forward(x)), encode_tag<1> {}); + } + + static node plain_node(value_t k) noexcept + { + node n{}; + n.kind = static_cast(k); + n.flags = node_flags::is_new; + return n; + } + + template + node number_node(T x, std::integral_constant /*floating-point*/) + { + return float_node(static_cast(x)); + } + + template + node number_node(T x, std::integral_constant /*signed*/) + { + return integer_node(static_cast(static_cast(x)), value_t::number_integer); + } + + template + node number_node(T x, std::integral_constant /*unsigned*/) + { + return integer_node(static_cast(x), value_t::number_unsigned); + } + + /// an integer with its canonical token in the edit arena + node integer_node(std::uint64_t bits, value_t k) + { + const bool negative = k == value_t::number_integer && static_cast(bits) < 0; + std::uint64_t magnitude = negative ? 0 - bits : bits; + std::array buf{}; + char* p = buf.data() + buf.size(); // NOLINT(misc-const-correctness): digits are written through p + do + { + *--p = static_cast('0' + (magnitude % 10)); + magnitude /= 10; + } + while (magnitude != 0); + if (negative) + { + *--p = '-'; + } + const auto len = static_cast(buf.data() + buf.size() - p); + node n = plain_node(k); + n.flags = static_cast(n.flags | node_flags::edited); + n.off = append_text(m_doc, p, len); + // number_length() adds one for the sign of number_integer nodes + n.extra = static_cast(k == value_t::number_integer ? len - 1 : len); + set_integer_bits(n, bits); + return n; + } + + /// a float with its shortest round-trip token (as basic_json::dump() + /// writes it), or nan, inf, -inf, in the edit arena + node float_node(number_float_t x) + { + string_t text; + if (std::isnan(x)) + { + text = "nan"; + } + else if (std::isinf(x)) + { + text = x > 0 ? "inf" : "-inf"; + } + else + { + text = BasicJsonType(x).dump(); + } + node n = plain_node(value_t::number_float); + n.flags = static_cast(n.flags | node_flags::edited); + n.extra = 0xFFFFu; // (the digit layout is not recorded) + n.off = append_text(m_doc, text.data(), text.size()); + n.len = static_cast(text.size()); + return n; + } + + /// a string (or key) in the edit arena + node string_node(const char* s, std::size_t len) + { + if (NLOHMANN_VIEW_UNLIKELY(len >= 0xFFFFFFFFu)) + { + throw_out_of_range(exception_id::document_too_large, "strings of 4 GiB or more are not supported by json_document"); // LCOV_EXCL_LINE + } + node n = plain_node(value_t::string); + n.flags = static_cast(n.flags | node_flags::edited); + n.off = append_text(m_doc, s, len); + n.len = static_cast(len); + return n; + } + + /// a scalar of a view (of any document) as a node of this document + node copy_scalar(const document_data& from, const node& n) + { + if (&from == &m_doc) + { + return n; // the same storage + } + switch (static_cast(n.kind)) + { + case value_t::string: + // (an editable document only holds valid UTF-8, whatever the check of the other document was) + check_utf8(from.str(n), n.len); + return string_node(from.str(n), n.len); + case value_t::number_integer: + case value_t::number_unsigned: + return integer_node(integer_bits(n), static_cast(n.kind)); + case value_t::number_float: + { + node r = plain_node(value_t::number_float); + r.flags = static_cast(r.flags | node_flags::edited); + r.off = append_text(m_doc, from.str(n), n.len); + r.len = n.len; + r.extra = n.extra; + return r; + } + case value_t::boolean: + { + node r = plain_node(value_t::boolean); + r.flags = static_cast(r.flags | (n.flags & node_flags::is_true)); + return r; + } + case value_t::null: + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::discarded: + default: + return plain_node(value_t::null); + } + } + + node json_scalar(const BasicJsonType& j) + { + switch (j.type()) + { + case value_t::null: + return plain_node(value_t::null); + case value_t::boolean: + { + node r = plain_node(value_t::boolean); + r.flags = static_cast(r.flags | (j.template get() ? node_flags::is_true : 0)); + return r; + } + case value_t::number_integer: + return integer_node(static_cast(static_cast(j.template get())), value_t::number_integer); + case value_t::number_unsigned: + return integer_node(static_cast(j.template get()), value_t::number_unsigned); + case value_t::number_float: + return float_node(j.template get()); + case value_t::string: + { + const auto& s = j.template get_ref(); + check_utf8(s.data(), s.size()); + return string_node(s.data(), s.size()); + } + case value_t::binary: + throw_type_error(exception_id::document_binary_value, "cannot store a binary value in a json_document", ""); + case value_t::discarded: + case value_t::object: + case value_t::array: + default: + throw_type_error(exception_id::type_mismatch, "type must be a value, but is ", "discarded"); + } + } + + // The subtrees are walked with an explicit stack (as materialize() does): + // the nesting depth is limited by memory only, not by the call stack. + + /// number of nodes of a subtree (containers, keys, scalars) + template + static std::size_t count_nodes(const document_data& d, const node* n) + { + using walk = navigation; + struct frame + { + const node* pos; ///< next element, or key of the next member + const node* end; + bool object; + }; + std::vector open; + std::size_t r = 0; + for (;;) + { + ++r; + if (is_container(*n)) + { + open.push_back(frame{walk::first(d, n), walk::end(d, n), n->kind == static_cast(value_t::object)}); + } + // the next value: close finished containers, then step over the key + for (;;) + { + if (open.empty()) + { + return r; + } + frame& f = open.back(); + if (f.pos == f.end) + { + open.pop_back(); + continue; + } + const node* v = f.pos; + if (f.object) + { + ++r; // the key + ++v; + } + f.pos = document_data::after(v); + n = walk::value(v); + break; + } + } + } + + /// copy a subtree (of any document) as a contiguous sequence of + /// count_nodes() nodes + template + void fill_nodes(const document_data& d, const node* n, node* out) + { + using walk = navigation; + struct frame + { + const node* pos; ///< next element, or key of the next member + const node* end; + bool object; + node* self; ///< the container in the copy + }; + std::vector open; + for (;;) + { + if (is_container(*n)) + { + node* const self = out++; + *self = plain_node(static_cast(n->kind)); + self->len = n->len; + open.push_back(frame{walk::first(d, n), walk::end(d, n), n->kind == static_cast(value_t::object), self}); + } + else + { + *out++ = copy_scalar(d, *n); + } + // the next value: close finished containers, then copy the key + for (;;) + { + if (open.empty()) + { + return; + } + frame& f = open.back(); + if (f.pos == f.end) + { + f.self->next = static_cast(out - f.self); + open.pop_back(); + continue; + } + const node* v = f.pos; + if (f.object) + { + *out++ = copy_scalar(d, *v); + ++v; + } + f.pos = document_data::after(v); + n = walk::value(v); + break; + } + } + } + + static std::size_t count_nodes(const BasicJsonType& j) + { + using iterator = typename BasicJsonType::const_iterator; + struct frame + { + iterator pos; + iterator end; + bool object; + }; + std::vector open; + const BasicJsonType* n = &j; + std::size_t r = 0; + for (;;) + { + ++r; + if (n->is_structured()) + { + open.push_back(frame{n->cbegin(), n->cend(), n->is_object()}); + } + for (;;) + { + if (open.empty()) + { + return r; + } + frame& f = open.back(); + if (f.pos == f.end) + { + open.pop_back(); + continue; + } + r += f.object ? 1 : 0; // the key + n = &*f.pos; + ++f.pos; + break; + } + } + } + + void fill_nodes(const BasicJsonType& j, node* out) + { + using iterator = typename BasicJsonType::const_iterator; + struct frame + { + iterator pos; + iterator end; + bool object; + node* self; ///< the container in the copy + }; + std::vector open; + const BasicJsonType* n = &j; + for (;;) + { + if (n->is_structured()) + { + node* const self = out++; + *self = plain_node(n->type()); + self->len = static_cast(n->size()); + open.push_back(frame{n->cbegin(), n->cend(), n->is_object(), self}); + } + else + { + *out++ = json_scalar(*n); + } + for (;;) + { + if (open.empty()) + { + return; + } + frame& f = open.back(); + if (f.pos == f.end) + { + f.self->next = static_cast(out - f.self); + open.pop_back(); + continue; + } + if (f.object) + { + const auto& key = f.pos.key(); + check_utf8(key.data(), key.size()); + *out++ = string_node(key.data(), key.size()); + } + n = &*f.pos; + ++f.pos; + break; + } + } + } + + document_data& m_doc; +}; + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // min, sort +#include // array +#include // offsetof, size_t +#include // int64_t, uint8_t, uint16_t, uint32_t, uint64_t +#include // memcmp, memcpy, memset +#include // numeric_limits +#include // string +#include // vector + +// #include +// #include + +// #include + +// #include + +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // size_t +#include // int64_t, uint64_t +#include // numeric_limits +#include // string +#include // integral_constant + +// #include +// #include + +// #include + +// #include + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/*! +@brief locate the decimal point and the end of the mantissa of a float token + +Also checks that the token is a JSON number. Tokens of the parser and of edits +always are; an image loaded with image_check::bounds can hold any bytes, which +must not reach the conversion (it expects a well-formed token). +*/ +inline bool float_token_layout(const char* first, const char* last, std::size_t& dot, std::size_t& mantissa_end) noexcept +{ + const auto digit = [last](const char* q) + { + return q != last && is_digit(static_cast(*q)); + }; + const char* p = first; + p += (p != last && *p == '-') ? 1 : 0; + if (!digit(p) || (*p == '0' && digit(p + 1))) + { + return false; + } + do + { + ++p; + } + while (digit(p)); + dot = std::string::npos; + if (p != last && *p == '.') + { + dot = static_cast(p - first); + if (!digit(++p)) + { + return false; + } + do + { + ++p; + } + while (digit(p)); + } + mantissa_end = static_cast(p - first); + if (p != last && (*p == 'e' || *p == 'E')) + { + ++p; + p += (p != last && (*p == '+' || *p == '-')) ? 1 : 0; + if (!digit(p)) + { + return false; + } + do + { + ++p; + } + while (digit(p)); + } + return p == last; +} + +/*! +@brief the value of the float token of a node, as parse() converts it + +Uses the lexer's conversion (detail::convert_float), so that the values are +bit-identical to parse(): float and double are converted without allocation +and independent of the locale. A token that is not a JSON number (only in a +damaged image loaded with image_check::bounds) yields 0. +*/ +template +NLOHMANN_VIEW_NOINLINE FloatType float_value(const char* first, const node& n) +{ + const char* const last = first + n.len; + std::size_t dot = 0; + std::size_t mantissa_end = 0; + if (NLOHMANN_VIEW_UNLIKELY(!float_token_layout(first, last, dot, mantissa_end))) + { + return FloatType{}; + } + return convert_float(first, last, dot, mantissa_end); +} + +/*! +@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 +fraction digits, and the exponent are, so the digits are read eight at a +time without scanning. + +@param[in] p first character of the token +@param[in] e end of the token +@param[in] limit end of the readable memory (the source text) +*/ +NLOHMANN_VIEW_ALWAYS_INLINE float_significand layout_decimal(const unsigned char* p, const unsigned char* e, unsigned int_digits, unsigned frac_digits, const unsigned char* limit) noexcept +{ + const bool negative = *p == '-'; + p += negative ? 1 : 0; + std::uint64_t w = parse_upto19(p, int_digits, limit); + p += int_digits; + std::int64_t q = 0; + if (frac_digits != 0) + { + w = (w * int_pow10(frac_digits)) + parse_upto19(p + 1, frac_digits, limit); + p += 1 + frac_digits; + q = -static_cast(frac_digits); + } + if (p != e) + { + // [eE][+-]digits; huge exponents saturate (the parser rejected + // overflow). The token is not read beyond e, and the digits are taken + // as unsigned, so that a token that is not well-formed (a damaged + // image loaded with image_check::bounds) yields a wrong value, but no + // overflow. + ++p; + const bool exp_negative = p != e && *p == '-'; + p += (p != e && (*p == '-' || *p == '+')) ? 1 : 0; + std::int64_t exp_value = 0; + for (; p != e; ++p) + { + if (exp_value < 0x10000000) + { + exp_value = (exp_value * 10) + static_cast(*p - '0'); + } + } + q += exp_negative ? -exp_value : exp_value; + } + + float_significand d; + d.w = w; + d.exponent = q; + d.negative = negative; + return d; +} + +/*! +@brief the value of a float token with at most 19 digits, from its layout + +The result is correctly rounded by the lexer's conversion +(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 +digits), so it is the value parse() produces. +*/ +template +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(layout_decimal(p, e, int_digits, frac_digits, limit)); +} + +/// the value of a float set by an edit: its token (the shortest round-trip +/// text, or "nan", "inf", "-inf") in the edit arena +template +NLOHMANN_VIEW_NOINLINE FloatType edited_float(const char* token, const node& n) +{ + if (token[0] == 'n') + { + return std::numeric_limits::quiet_NaN(); + } + if (token[0] == 'i' || (token[0] == '-' && token[1] == 'i')) + { + return token[0] == 'i' ? std::numeric_limits::infinity() : -std::numeric_limits::infinity(); + } + return float_value(token, n); +} + +/// 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 +template +FloatType float_value(const document_data& d, const node& n) +{ + if (NLOHMANN_VIEW_UNLIKELY((n.flags & node_flags::storage) == node_flags::edited)) + { + return edited_float(d.str(n), n); + } + return float_value(d, n, std::integral_constant::value> {}); +} + +template +FloatType float_value(const document_data& d, const node& n, std::true_type /*binary32 or binary64*/) +{ + const unsigned int_digits = n.extra & 0xFFu; + const unsigned frac_digits = n.extra >> 8u; + if (NLOHMANN_VIEW_LIKELY(int_digits + frac_digits <= 19)) // (255 marks "many") + { + // (a float token not written by an edit is in the text) + const auto* const first = reinterpret_cast(d.src + n.off); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + return layout_float(first, first + n.len, int_digits, frac_digits, reinterpret_cast(d.src + d.size)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + } + return float_value(d.str(n), n); +} + +template +FloatType float_value(const document_data& d, const node& n, std::false_type /*other*/) +{ + return float_value(d.str(n), n); +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include + + +// Images: a document stored so that loading it needs no parsing. +// +// Layout (little-endian): a 64-byte header, the nodes, the text (the source, +// followed by the number tokens written by edits), a NUL, the decoded strings +// (followed by the strings written by edits), a NUL. The idea is that of +// zero-copy formats such as FlatBuffers (https://github.com/google/flatbuffers) +// and YaFF (https://github.com/yandex/yaff); no code is taken from them. +// check_image follows the idea of FlatBuffers' Verifier (bounds and +// structure) and also checks what the parser guarantees about strings and +// numbers, so that reading and serializing a checked image is safe and yields +// valid JSON. + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// how load() checks an image +enum class image_check +{ + /// everything the parser guarantees: structure and bounds, strings (valid + /// UTF-8; source strings without quotes, backslashes, and control + /// characters), and numbers (well-formed, matching the stored values) + full, + /// structure and bounds only: reading and serializing are safe, but a + /// crafted image can yield invalid UTF-8, strings that serialize to + /// invalid JSON, or numbers that differ from their text + bounds, + /// none: for images from a trusted source only (a damaged image is + /// undefined behavior) + none, +}; + +struct image_header +{ + std::array magic; ///< "NJVI" + std::uint32_t version; ///< 1 + std::uint64_t node_count; + std::uint64_t text_size; + std::uint64_t arena_size; + std::array reserved; ///< zero (for later versions) +}; +static_assert(sizeof(image_header) == 64, "the image header must be 64 bytes"); + +constexpr std::uint32_t image_version = 1; + +/// the largest node count and text or string size of an image (as for parsed +/// documents, offsets and counts must fit 32 bits) +constexpr std::uint64_t image_limit = 0xFFFFFFF0u; + +/// Copy the current structure of an edited document into nodes in document +/// order, as the parser would have written them. Text written by edits is +/// appended to text_tail (number tokens) and arena_tail (strings); floats that +/// are not finite become null, as dump() writes them. +inline void compact_nodes(const document_data& d, std::size_t arena_size, std::vector& out, std::string& text_tail, std::string& arena_tail) +{ + struct frame + { + const node* cur; + const node* end; + std::size_t index; ///< the container's node in out + std::uint32_t count; + bool object; + }; + std::vector stack; + const auto string_node = [&](const node & s) + { + node r = s; + r.extra = 0; + r.flags = static_cast(s.flags & node_flags::storage); + if (r.flags == node_flags::edited) + { + r.off = static_cast(arena_size + arena_tail.size()); + arena_tail.append(d.str(s), s.len); + r.flags = node_flags::escaped; + } + return r; + }; + const auto copy_node = [&](const node * v) + { + node r = *v; + switch (static_cast(v->kind)) + { + case value_t::object: + case value_t::array: + r.flags = 0; + r.extra = 0; + r.off = (v->flags & (node_flags::moved | node_flags::is_new)) != 0 ? 0 : v->off; + r.len = 0; // counted below + r.next = 0; // set when the container is complete + stack.push_back(frame{d.first_child_edited(v), d.child_end_edited(v), out.size(), 0, v->kind == static_cast(value_t::object)}); + break; + case value_t::string: + r = string_node(*v); + break; + case value_t::number_integer: + case value_t::number_unsigned: + if ((v->flags & node_flags::storage) == node_flags::edited) + { + r.off = static_cast(d.size + text_tail.size()); + text_tail.append(d.str(*v), number_length(*v)); + } + r.flags = 0; + break; + case value_t::number_float: + if ((v->flags & node_flags::storage) == node_flags::edited) + { + const char* const t = d.str(*v); + if (t[0] == 'n' || t[0] == 'i' || (v->len > 1 && t[1] == 'i')) + { + r = node{}; // nan and infinity: null, as dump() writes them + r.kind = static_cast(value_t::null); + break; + } + r.off = static_cast(d.size + text_tail.size()); + text_tail.append(t, v->len); + r.extra = 0xFFFFu; // the digit layout is not recorded + } + r.flags = 0; + break; + case value_t::boolean: + r.flags = static_cast(v->flags & node_flags::is_true); + break; + case value_t::null: + case value_t::binary: + case value_t::discarded: + default: + r.flags = 0; + break; + } + out.push_back(r); + }; + copy_node(d.nodes); + while (!stack.empty()) + { + const frame top = stack.back(); + if (top.cur == top.end) + { + node& c = out[top.index]; + c.len = top.count; + c.next = static_cast(out.size() - top.index); + stack.pop_back(); + continue; + } + if (top.object) + { + out.push_back(string_node(*top.cur)); + } + const node* const value = top.cur + (top.object ? 1 : 0); + stack.back().cur = document_data::after(value); + ++stack.back().count; + copy_node(document_data::deref(value)); // may grow the stack + } +} + +/// the document as an image +inline std::vector save_image(const document_data& d) +{ +#if !NLOHMANN_VIEW_LITTLE_ENDIAN + throw_type_error(exception_id::document_image_unsupported, "json_document images need a little-endian target"); // LCOV_EXCL_LINE +#endif + const std::size_t arena_size = d.arena_size; + std::vector compacted; + std::string text_tail; + std::string arena_tail; + if (d.edits) + { + compact_nodes(d, arena_size, compacted, text_tail, arena_tail); + } + const node* const nodes = d.edits ? compacted.data() : d.nodes; + const std::size_t count = d.edits ? compacted.size() : d.node_count; + const std::size_t text_size = d.size + text_tail.size(); + const std::size_t total_arena = arena_size + arena_tail.size(); + if (NLOHMANN_VIEW_UNLIKELY(text_size >= image_limit || total_arena >= image_limit || count >= image_limit)) + { + // LCOV_EXCL_START (4 GiB) + throw_out_of_range(exception_id::document_too_large, "images of 4 GiB or more are not supported by json_document"); + // LCOV_EXCL_STOP + } + image_header h{}; + h.magic = {{'N', 'J', 'V', 'I'}}; + h.version = image_version; + h.node_count = count; + h.text_size = text_size; + h.arena_size = total_arena; + std::vector image(sizeof(h) + (count * sizeof(node)) + text_size + 1 + total_arena + 1); + std::uint8_t* o = image.data(); + std::memcpy(o, &h, sizeof(h)); + o += sizeof(h); + std::memcpy(o, nodes, count * sizeof(node)); + // the hash indexes are rebuilt by load(), whose check rejects any other + // extra of an object, and the same document saves to the same bytes + for (std::size_t i = 0; i < count; ++i) + { + if (nodes[i].kind == static_cast(value_t::object) && nodes[i].extra != 0) + { + std::memset(o + (i * sizeof(node)) + offsetof(node, extra), 0, sizeof(nodes[i].extra)); + } + } + o += count * sizeof(node); + const auto append = [&o](const char* s, std::size_t n) + { + if (n != 0) + { + std::memcpy(o, s, n); + o += n; + } + }; + append(d.src, d.size); + append(text_tail.data(), text_tail.size()); + *o++ = 0; + append(d.base[1], arena_size); + append(arena_tail.data(), arena_tail.size()); + *o = 0; + return image; +} + +/// the parts of a number token that the checks need +struct number_token +{ + const unsigned char* int_start; + std::size_t int_digits; + std::size_t frac_digits; + std::int64_t exponent; + bool negative; + bool is_float; ///< a fraction or an exponent +}; + +/// whether [s, s + len) is a JSON number (the grammar the parser accepts) +inline bool scan_number_token(const unsigned char* s, std::size_t len, number_token& t) +{ + const unsigned char* const e = s + len; + const unsigned char* p = s; + t.negative = p != e && *p == '-'; + p += t.negative ? 1 : 0; + if (p == e) + { + return false; + } + t.int_start = p; + if (*p == '0') + { + ++p; + } + else if (*p >= '1' && *p <= '9') + { + do + { + ++p; + } + while (p != e && is_digit(*p)); + } + else + { + return false; + } + t.int_digits = static_cast(p - t.int_start); + t.frac_digits = 0; + t.is_float = false; + if (p != e && *p == '.') + { + const unsigned char* const f0 = ++p; + while (p != e && is_digit(*p)) + { + ++p; + } + if (p == f0) + { + return false; + } + t.frac_digits = static_cast(p - f0); + t.is_float = true; + } + t.exponent = 0; + if (p != e && (*p | 0x20u) == 'e') + { + ++p; + const bool exp_negative = p != e && *p == '-'; + p += (p != e && (*p == '+' || *p == '-')) ? 1 : 0; + if (p == e || !is_digit(*p)) + { + return false; + } + do + { + t.exponent = t.exponent < 100000 ? (t.exponent * 10) + (*p - '0') : t.exponent; + ++p; + } + while (p != e && is_digit(*p)); + t.exponent = exp_negative ? -t.exponent : t.exponent; + t.is_float = true; + } + return p == e; +} + +/// the digit layout the parser records for a float token (compaction +/// writes "many" instead; the caller accepts both) +inline std::uint16_t float_layout(const number_token& t) +{ + return static_cast((std::min)(t.int_digits, std::size_t{255}) | ((std::min)(t.frac_digits, std::size_t{255}) << 8u)); +} + +/// whether a float token (well-formed, per scan_number_token) is finite as +/// double; parse() rejects floats that overflow, and as there, only a number +/// whose magnitude could reach 1e308 needs the conversion +inline bool float_token_finite(const unsigned char* s, std::size_t len, const number_token& t) +{ + if (static_cast(t.int_digits) + t.exponent > 300) + { + node n{}; + n.kind = static_cast(value_t::number_float); + n.len = static_cast(len); + const auto v = float_value(reinterpret_cast(s), n); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + return v <= (std::numeric_limits::max)() && v >= -(std::numeric_limits::max)(); + } + return true; +} + +/// whether an integer node matches its token the way the parser records it +/// (after the bounds check; the token has at most 256 characters) +inline bool check_integer(const node& n, const unsigned char* text) +{ + const std::size_t len = number_length(n); + const unsigned char* const s = text + n.off; + number_token t{}; + if (!scan_number_token(s, len, t)) + { + return false; + } + // the token's value is the stored one; number_integer nodes of edits can + // be non-negative (as basic_json keeps the type of a value) + const bool integer = n.kind == static_cast(value_t::number_integer); + if (t.is_float || t.int_digits > 20 || (t.negative && !integer)) + { + return false; + } + // (at most 19 digits cannot overflow; 20 digits are compared with 2^64 - 1) + if (t.int_digits == 20 && std::memcmp(t.int_start, "18446744073709551615", 20) > 0) + { + return false; + } + std::uint64_t m = 0; + for (const unsigned char* d = t.int_start; d != t.int_start + t.int_digits; ++d) + { + m = (m * 10) + static_cast(*d - '0'); + } + if (integer && m > (t.negative ? std::uint64_t{1} << 63u : (std::uint64_t{1} << 63u) - 1)) + { + return false; + } + return integer_bits(n) == (t.negative ? 0 - m : m); +} + +/// a byte range of the text or of the decoded strings +struct byte_range +{ + std::uint32_t off; + std::uint32_t len; +}; + +/// a float token and the digit layout its node records +struct float_range +{ + std::uint32_t off; + std::uint32_t len; + std::uint16_t extra; +}; + +/// Run check once for each distinct range of ranges (which are within bounds +/// and not empty). Ranges that are not identical must not overlap: save() +/// writes every string and token to a place of its own, and nodes that share +/// a value (copies within a document) share the whole range. This bounds the +/// work by the size of the text, however many nodes point to the same bytes. +template +bool check_distinct_ranges(std::vector& ranges, Check check) +{ + std::sort(ranges.begin(), ranges.end(), [](const byte_range & a, const byte_range & b) + { + return a.off != b.off ? a.off < b.off : a.len < b.len; + }); + std::size_t end = 0; + for (std::size_t i = 0; i < ranges.size(); ++i) + { + const byte_range r = ranges[i]; + if (i != 0 && r.off == ranges[i - 1].off && r.len == ranges[i - 1].len) + { + continue; + } + if (r.off < end || !check(r)) + { + return false; + } + end = static_cast(r.off) + r.len; + } + return true; +} + +/// Check the float nodes: each token once (nodes of the same token must +/// record layouts that match it), tokens must not overlap. +inline bool check_float_ranges(std::vector& ranges, const unsigned char* text) +{ + std::sort(ranges.begin(), ranges.end(), [](const float_range & a, const float_range & b) + { + if (a.off != b.off) + { + return a.off < b.off; + } + if (a.len != b.len) + { + return a.len < b.len; + } + return a.extra < b.extra; + }); + std::size_t end = 0; + std::size_t i = 0; + while (i < ranges.size()) + { + const float_range r = ranges[i]; + if (r.off < end) + { + return false; + } + number_token t{}; + const unsigned char* const s = text + r.off; + if (!scan_number_token(s, r.len, t) || !float_token_finite(s, r.len, t)) + { + return false; + } + // the digit layout the parser records (or "many", as compaction writes it) + const std::uint16_t layout = float_layout(t); + for (; i < ranges.size() && ranges[i].off == r.off && ranges[i].len == r.len; ++i) + { + if (ranges[i].extra != layout && ranges[i].extra != 0xFFFFu) + { + return false; + } + } + end = static_cast(r.off) + r.len; + } + return true; +} + +/// the content checks of check_image: source strings as the parser leaves +/// them (no quotes, backslashes, or control characters), decoded strings +/// (valid UTF-8), and float tokens +inline bool check_contents(const unsigned char* text, const unsigned char* arena, + std::vector& source_strings, std::vector& decoded_strings, + std::vector& floats) +{ + return check_distinct_ranges(source_strings, [text](const byte_range & r) + { + const unsigned char* const b = text + r.off; + return scan_string_run(b, b + r.len) == b + r.len; + }) + && check_distinct_ranges(decoded_strings, [arena](const byte_range & r) + { + return valid_utf8_prefix(arena + r.off, r.len) == r.len; + }) + && check_float_ranges(floats, text); +} + +/// Check the nodes of a loaded image against its text and decoded strings: +/// kinds, flags, and `extra`; extents and element counts of arrays and +/// objects; keys; bounds; string contents (source strings as the parser +/// leaves them: no quotes, backslashes, or control characters; all strings +/// valid UTF-8); and number tokens. +/// +/// The structure and the bounds are checked node by node. The contents of +/// strings and of float tokens are checked afterwards, once for each distinct +/// range (see check_distinct_ranges), so that the full check is linear in the +/// size of the image plus the sorting of the ranges, and not in the number of +/// nodes times the size of the text. +inline bool check_image(const node* nodes, std::size_t count, const unsigned char* text, std::size_t text_size, + const unsigned char* arena, std::size_t arena_size, bool full) +{ + struct frame + { + std::size_t end; + std::uint32_t len; + std::uint32_t seen; + bool object; + bool expect_key; + }; + std::vector stack; + std::vector source_strings; + std::vector decoded_strings; + std::vector floats; + const auto check_string = [&](const node & n) -> bool + { + if ((n.flags & ~node_flags::escaped) != 0 || n.extra != 0) + { + return false; + } + const bool decoded = (n.flags & node_flags::escaped) != 0; + const std::size_t limit = decoded ? arena_size : text_size; + if (n.off > limit || n.len > limit - n.off) + { + return false; + } + if (full && n.len != 0) + { + (decoded ? decoded_strings : source_strings).push_back(byte_range{n.off, n.len}); + } + return true; + }; + // bounds of a number token; the recorded digit layout must lie within it + const auto number_in_bounds = [&](const node & n) -> bool + { + const std::size_t len = number_length(n); + if (len == 0 || n.off > text_size || len > text_size - n.off) + { + return false; + } + if (n.kind != static_cast(value_t::number_float)) + { + return (n.extra >> 8u) == 0; + } + // float_value() reads the sign, the integer digits, and the point and + // fraction digits the layout records (a layout of more than 19 digits + // means the general conversion, which stays within the token) + const std::size_t int_digits = n.extra & 0xFFu; + const std::size_t frac_digits = n.extra >> 8u; + const std::size_t need = (text[n.off] == '-' ? 1u : 0u) + int_digits + (frac_digits != 0 ? frac_digits + 1 : 0); + return int_digits + frac_digits > 19 || need <= len; + }; + std::size_t i = 0; + for (;;) + { + // close finished arrays and objects + while (!stack.empty() && i == stack.back().end) + { + const frame f = stack.back(); + if (f.seen != f.len || (f.object && !f.expect_key)) + { + return false; + } + stack.pop_back(); + if (!stack.empty()) + { + ++stack.back().seen; + stack.back().expect_key = true; + } + } + if (i == count) + { + return stack.empty() && (!full || check_contents(text, arena, source_strings, decoded_strings, floats)); + } + if (i != 0 && stack.empty()) + { + return false; // nodes after the root + } + const node& n = nodes[i]; + if (!stack.empty() && stack.back().object && stack.back().expect_key) + { + if (n.kind != static_cast(value_t::string) || !check_string(n)) + { + return false; + } + stack.back().expect_key = false; + ++i; + continue; + } + bool complete = true; + switch (static_cast(n.kind)) + { + case value_t::null: + // (the offset of a literal is read to size the output of dump()) + if (n.flags != 0 || n.extra != 0 || n.off > text_size) + { + return false; + } + break; + case value_t::boolean: + if ((n.flags & ~node_flags::is_true) != 0 || n.extra != 0 || n.off > text_size) + { + return false; + } + break; + case value_t::string: + if (!check_string(n)) + { + return false; + } + break; + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + if (n.flags != 0 || !number_in_bounds(n)) + { + return false; + } + if (full) + { + // (the contents of float tokens are checked later; the token of an + // integer has at most 256 characters) + if (n.kind == static_cast(value_t::number_float)) + { + floats.push_back(float_range{n.off, n.len, n.extra}); + } + else if (!check_integer(n, text)) + { + return false; + } + } + break; + case value_t::array: + case value_t::object: + { + const std::size_t limit = stack.empty() ? count : stack.back().end; + if (n.flags != 0 || n.extra != 0 || n.next == 0 || n.next > limit - i || n.off > text_size) + { + return false; + } + stack.push_back(frame{i + n.next, n.len, 0, n.kind == static_cast(value_t::object), true}); + complete = false; + break; + } + case value_t::binary: + case value_t::discarded: + default: + return false; + } + ++i; + if (complete && !stack.empty()) + { + ++stack.back().seen; + stack.back().expect_key = true; + } + } +} + +[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_invalid_image(const char* what) +{ + throw_parse_error(exception_id::invalid_document_image, concat("invalid json_document image: ", what)); +} + +/// Read an image into d. The text and the decoded strings stay in the image; +/// the nodes are copied (so that they are aligned, and edits can change them). +inline void load_image(document_data& d, const std::uint8_t* image, std::size_t size, image_check check) +{ +#if !NLOHMANN_VIEW_LITTLE_ENDIAN + throw_type_error(exception_id::document_image_unsupported, "json_document images need a little-endian target"); // LCOV_EXCL_LINE +#endif + if (image == nullptr || size < sizeof(image_header)) + { + throw_invalid_image("too short"); + } + image_header h{}; + std::memcpy(&h, image, sizeof(h)); + // (the reserved fields are for later versions) + if (std::memcmp(h.magic.data(), "NJVI", 4) != 0 || h.version != image_version + || (h.reserved[0] | h.reserved[1] | h.reserved[2] | h.reserved[3]) != 0) + { + throw_invalid_image("unknown format"); + } + const std::size_t room = size - sizeof(h); + if (h.node_count == 0 || h.node_count > room / sizeof(node) || h.node_count >= image_limit || h.text_size >= image_limit || h.arena_size >= image_limit) + { + throw_invalid_image("sizes out of range"); + } + const auto count = static_cast(h.node_count); + const auto text_size = static_cast(h.text_size); + const auto arena_size = static_cast(h.arena_size); + const std::size_t text_at = sizeof(h) + (count * sizeof(node)); + // the text, a NUL, the decoded strings, a NUL, and nothing after them + if (size - text_at < 2 || text_size > size - text_at - 2 || arena_size != size - text_at - text_size - 2 + || image[text_at + text_size] != 0 || image[size - 1] != 0) + { + throw_invalid_image("sizes out of range"); + } + + d.discarded = true; + d.edits.reset(); + d.base[2] = nullptr; + d.owned.clear(); + if (d.owned_image.empty() || image != d.owned_image.data()) + { + d.owned_image.clear(); + } + d.arena.clear(); + d.indexes.clear(); + d.index_slots.clear(); + d.large_objects.clear(); + d.node_count = 0; + d.reserve(count); + std::memcpy(d.nodes, image + sizeof(h), count * sizeof(node)); + d.node_count = count; + const std::uint8_t* const text = image + text_at; + const std::uint8_t* const arena = text + text_size + 1; + d.src = reinterpret_cast(text); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + d.size = text_size; + d.base[0] = d.src; + d.base[1] = reinterpret_cast(arena); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + d.arena_size = arena_size; + if (check != image_check::none && !check_image(d.nodes, count, text, text_size, arena, arena_size, check == image_check::full)) + { + throw_invalid_image("the check failed"); + } + // the hash indexes of large objects, as after parsing + for (std::size_t i = 0; i < count; ++i) + { + const node& n = d.nodes[i]; + if (n.kind == static_cast(value_t::object) && n.len >= document_data::index_min_members) + { + d.large_objects.push_back(static_cast(i)); + } + } + build_object_indexes(d); + std::vector().swap(d.large_objects); // (only needed while building) + d.discarded = false; +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // max +#include // size_t +#ifndef JSON_NO_IO + #include // FILE, fread +#endif +#include // istream, streambuf +#include // basic_string, char_traits, string +#include // decay, integral_constant, is_array, is_const, is_integral, is_lvalue_reference, is_pointer, is_same, remove_cv, remove_extent, remove_pointer, remove_reference +#include // forward + +// #include +// #include + + +#if NLOHMANN_VIEW_HAS_CPP_17 + #include // string_view +#endif + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// how a document takes its input +enum class input_kind +{ + move_string, ///< non-const rvalue std::string: owned without a copy + c_string, ///< pointer to single-byte characters (NUL-terminated): borrowed + char_array, ///< array of single-byte characters (e.g. a string literal): borrowed + borrow_range, ///< lvalue contiguous byte container, or std::string_view: borrowed + copy_range, ///< rvalue contiguous byte container (a const rvalue std::string too): copied + wide_c_string, ///< pointer to wide characters (NUL-terminated): converted to UTF-8 into a buffer + adapter, ///< streams, FILE*, wide strings and arrays, and the rest of what the library's input adapter reads: read into a buffer +}; + +/// a single-byte character type (char, signed/unsigned char, char8_t, std::uint8_t, ...) +template +struct is_byte_char : conjunction, std::integral_constant> {}; // NOLINT(bugprone-sizeof-expression): T is any pointee or element type + +/// a character type the library's input adapter converts to UTF-8 +template +struct is_wide_char : disjunction, std::is_same, std::is_same> {}; + +template +struct classify_input +{ + using R = typename std::remove_reference::type; + using D = typename std::decay::type; + using element = typename std::remove_cv::type>::type; + using pointee = typename std::remove_cv::type>::type; + static constexpr bool is_rvalue = !std::is_lvalue_reference::value; + static constexpr bool is_bytes = is_contiguous_byte_container::value; +#if NLOHMANN_VIEW_HAS_CPP_17 + static constexpr bool is_string_view = std::is_same::value; +#else + static constexpr bool is_string_view = false; +#endif + // NOLINTBEGIN(readability-avoid-nested-conditional-operator): a constant expression of C++11 + // (arrays and pointers of other types, such as wide-character arrays and + // FILE*, go to the library's input adapter) + static constexpr input_kind value = + (std::is_array::value && is_byte_char::value) ? input_kind::char_array + : std::is_array::value ? input_kind::adapter + : (std::is_pointer::value && is_byte_char::value) ? input_kind::c_string + : (std::is_pointer::value && is_wide_char::value) ? input_kind::wide_c_string + : std::is_pointer::value ? input_kind::adapter + : (is_rvalue && !std::is_const::value && std::is_same::value) ? input_kind::move_string + : (is_bytes && (!is_rvalue || is_string_view)) ? input_kind::borrow_range + : is_bytes ? input_kind::copy_range + : input_kind::adapter; + // NOLINTEND(readability-avoid-nested-conditional-operator) +}; + +/// an integer type other than bool: a length passed where a flag is expected +template +struct is_integer_not_bool : std::is_integral {}; + +template<> +struct is_integer_not_bool : std::false_type {}; + +/// std::basic_string guarantees a NUL at data()[size()] (the parser's sentinel) +template +struct is_std_string : std::false_type {}; + +template +struct is_std_string> : std::true_type {}; + +/// append what a json input adapter yields (UTF-16/32 inputs arrive as UTF-8) +template +void append_adapter(std::string& buf, Adapter ia) +{ + for (;;) + { + const auto ch = ia.get_character(); + if (ch == std::char_traits::eof()) + { + break; + } + buf.push_back(static_cast(ch)); + } +} + +/// an input adapter made by the document for an input that the library's +/// input_adapter() does not take as one argument (an iterator range, a wide C +/// string), to be appended with append_adapter() +template +struct adapter_input +{ + Adapter ia; +}; + +template +struct is_adapter_input : std::false_type {}; + +template +struct is_adapter_input> : std::true_type {}; + +template +adapter_input::type> make_adapter_input(Adapter&& ia) +{ + return {std::forward(ia)}; +} + +/// append the rest of a stream: its characters need no conversion, so they are +/// read in blocks from the stream buffer, straight into buf (reusing its +/// capacity). As the library's stream adapter, this reads the stream buffer +/// directly and sets eofbit at the end. +inline void append_stream(std::string& buf, std::istream& is) +{ + std::streambuf* const sb = is.rdbuf(); + if (sb == nullptr) + { + return; + } + for (;;) + { + const std::size_t used = buf.size(); + const std::size_t room = (std::max)(buf.capacity() - used, std::size_t{4096}); + buf.resize(used + room); + const std::streamsize got = sb->sgetn(&buf[used], static_cast(room)); + buf.resize(used + static_cast((std::max)(got, std::streamsize{0}))); + if (got <= 0) + { + break; + } + } + is.clear(is.rdstate() | std::ios::eofbit); +} + +/// a pointer to a C stream (never with JSON_NO_IO, which leaves out ) +template +struct is_c_stream +#ifndef JSON_NO_IO + : std::is_same +#else + : std::false_type +#endif +{}; + +#ifndef JSON_NO_IO +/// append the rest of a C stream: in blocks with fread, not character by +/// character (the library's file adapter reads with fgetc); +/// f is not null +inline void append_file(std::string& buf, std::FILE* f) +{ + for (;;) + { + const std::size_t used = buf.size(); + const std::size_t room = (std::max)(buf.capacity() - used, std::size_t{4096}); + buf.resize(used + room); + const std::size_t got = std::fread(&buf[used], 1, room, f); + buf.resize(used + got); + if (got < room) + { + break; // end of file, or an error (as fgetc, which returns EOF for both) + } + } +} +#endif + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // ptrdiff_t, size_t +#include // forward_iterator_tag +#include // string, to_string +#include // enable_if + +// #include +// #include + +// #include + +// #include + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// the result of view_iterator::operator->: keeps the view alive for the +/// duration of the member access +template +class arrow_proxy +{ + public: + explicit arrow_proxy(const View& v) noexcept + : m_view(v) + {} + + const View* operator->() const noexcept + { + return &m_view; + } + + private: + View m_view; +}; + +/*! +@brief forward iterator over the elements of a basic_json_view + +Iterates over the elements of an array or the member values of an object, in +document order; key() gives the key of an object member. As for basic_json, a +primitive value iterates as a range of one element (itself), and null as an +empty range. +*/ +template +class view_iterator +{ + public: + using iterator_category = std::forward_iterator_tag; + using value_type = View; + using difference_type = std::ptrdiff_t; + using pointer = arrow_proxy; + using reference = View; + using string_view_t = typename View::string_view_t; + + view_iterator() noexcept = default; + + /// @param[in] pos the element, or the key of the member + /// @param[in] object whether pos is a key (its value is the next node) + view_iterator(const document_data* d, const node* pos, bool object) noexcept + : m_doc(d), m_pos(pos), m_value_offset(object ? 1 : 0) + {} + + NLOHMANN_VIEW_ALWAYS_INLINE View operator*() const noexcept + { + return View(m_doc, View::navigation::value(m_pos + m_value_offset)); + } + + pointer operator->() const noexcept + { + return pointer(**this); + } + + NLOHMANN_VIEW_ALWAYS_INLINE view_iterator& operator++() noexcept + { + m_pos = document_data::after(m_pos + m_value_offset); + return *this; + } + + view_iterator operator++(int) noexcept + { + const view_iterator r = *this; + ++*this; + return r; + } + + friend bool operator==(const view_iterator& a, const view_iterator& b) noexcept + { + return a.m_pos == b.m_pos; + } + + friend bool operator!=(const view_iterator& a, const view_iterator& b) noexcept + { + return a.m_pos != b.m_pos; + } + + /// the key of the current object member; throws invalid_iterator.207 for + /// other iterators, like basic_json's iterators + string_view_t key() const + { + if (NLOHMANN_VIEW_UNLIKELY(m_value_offset == 0)) + { + throw_invalid_iterator(exception_id::iterator_key_not_object, "cannot use key() for non-object iterators"); + } + return string_view_t(m_doc->str(*m_pos), m_pos->len); + } + + View value() const noexcept + { + return **this; + } + + /// whether the iterator runs over the members of an object + bool is_object_iterator() const noexcept + { + return m_value_offset != 0; + } + + private: + const document_data* m_doc = nullptr; + const node* m_pos = nullptr; + std::size_t m_value_offset = 0; ///< 1 for objects: the value follows its key +}; + +/*! +@brief a (key, value) item of basic_json_view::items() + +The key of an array element is its index, as for basic_json::items(). +Supports structured bindings: for (const auto [key, value] : view.items()) +*/ +template +class view_item +{ + public: + using string_view_t = typename View::string_view_t; + using iterator = view_iterator; + + view_item(const iterator& it, std::size_t index) + : m_it(it) + , m_position(index) + {} + + /// the member key, or the element index for arrays (as text made on the + /// first call, so that iterating over the values pays nothing for it) + string_view_t key() const + { + if (m_it.is_object_iterator()) + { + return m_it.key(); + } + if (m_index.empty()) + { + m_index = std::to_string(m_position); + } + return string_view_t(m_index.data(), m_index.size()); + } + + View value() const noexcept + { + return *m_it; + } + + template::type = 0> + string_view_t get() const + { + return key(); + } + + template::type = 0> + View get() const noexcept + { + return value(); + } + + private: + iterator m_it; + std::size_t m_position; ///< the index of an array element + mutable std::string m_index{}; ///< the text of m_position, once key() was called // NOLINT(readability-redundant-member-init) +}; + +/// the range returned by basic_json_view::items() +template +class view_items +{ + public: + using item = view_item; + + class iterator + { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = item; + using difference_type = std::ptrdiff_t; + using pointer = void; + using reference = item; + + explicit iterator(const view_iterator& it) noexcept + : m_it(it) + {} + + item operator*() const + { + return item(m_it, m_index); + } + + iterator& operator++() noexcept + { + ++m_it; + ++m_index; + return *this; + } + + iterator operator++(int) noexcept + { + const iterator r = *this; + ++*this; + return r; + } + + friend bool operator==(const iterator& a, const iterator& b) noexcept + { + return a.m_it == b.m_it; + } + + friend bool operator!=(const iterator& a, const iterator& b) noexcept + { + return a.m_it != b.m_it; + } + + private: + view_iterator m_it; + std::size_t m_index = 0; + }; + + explicit view_items(const View& v) noexcept + : m_view(v) + {} + + iterator begin() const noexcept + { + return iterator(m_view.begin()); + } + + iterator end() const noexcept + { + return iterator(m_view.end()); + } + + private: + View m_view; +}; + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // array +#include // size_t +#include // int64_t, uint8_t +#include // string +#include // vector + +// #include +// #include + +// #include + +// #include + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/*! +@brief the basic_json value of the subtree at n + +The subtree is replayed into the SAX handler that parse() uses to build its +values, so the result is the value parse() would produce: duplicate keys keep +the last value, and with JSON_DIAGNOSTICS the parent pointers are set. It is +iterative, so the nesting depth is limited by memory only, as for parse(). +Without a lexer the handler records no source positions +(JSON_DIAGNOSTIC_POSITIONS). +*/ +template +BasicJsonType materialize(const document_data& d, const node* n) +{ + using string_t = typename BasicJsonType::string_t; + using sax_t = json_sax_dom_parser, true>; + using nav = navigation; + + // the open containers: their next element (or key of the next member), + // their end, and whether they are objects; for the first 16 levels on the + // stack, else sized once from the document's deepest nesting (for a + // subtree an upper bound) + struct frame + { + const node* pos; + const node* end; + bool object; + }; + + // a scalar needs neither the handler nor the stack of open containers + switch (static_cast(n->kind)) + { + case value_t::string: + return BasicJsonType(string_t(d.str(*n), n->len)); + case value_t::number_integer: + return BasicJsonType(static_cast(static_cast(integer_bits(*n)))); + case value_t::number_unsigned: + return BasicJsonType(static_cast(integer_bits(*n))); + case value_t::number_float: + return BasicJsonType(float_value(d, *n)); + case value_t::boolean: + return BasicJsonType((n->flags & node_flags::is_true) != 0); + case value_t::null: + return BasicJsonType(nullptr); + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::discarded: + default: + break; + } + + BasicJsonType result; + sax_t sax(result, true); + const string_t no_token{}; + // (shallow subtrees on the stack: a call for a small value then allocates only for the value) + std::array shallow; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): filled as containers open + std::vector deep{}; + frame* open = shallow.data(); + std::size_t capacity = shallow.size(); + if (d.max_depth > capacity) + { + deep.resize(d.max_depth); + open = deep.data(); + capacity = deep.size(); + } + std::size_t depth = 0; + for (;;) + { + // false positive: n comes from nav::value(), which never returns null for a valid index + // @infer-ignore NULLPTR_DEREFERENCE + switch (static_cast(n->kind)) + { + case value_t::object: + case value_t::array: + { + const bool object = n->kind == static_cast(value_t::object); + if (object) + { + sax.start_object(n->len); + } + else + { + sax.start_array(n->len); + } + if (NLOHMANN_VIEW_UNLIKELY(depth == capacity)) + { + // (deeper than the recorded nesting, e.g. after edits) + deep.assign(open, open + depth); + deep.resize(depth * 2); + open = deep.data(); + capacity = deep.size(); + } + open[depth++] = frame{nav::first(d, n), nav::end(d, n), object}; + break; + } + case value_t::string: + { + string_t s(d.str(*n), n->len); + sax.string(s); + break; + } + case value_t::number_integer: + sax.number_integer(static_cast(static_cast(integer_bits(*n)))); + break; + case value_t::number_unsigned: + sax.number_unsigned(static_cast(integer_bits(*n))); + break; + case value_t::number_float: + sax.number_float(float_value(d, *n), no_token); + break; + case value_t::boolean: + sax.boolean((n->flags & node_flags::is_true) != 0); + break; + case value_t::null: + case value_t::binary: + case value_t::discarded: + default: + // false positive: the parser's frame stack points into its own storage (never null) + // @infer-ignore NULLPTR_DEREFERENCE + sax.null(); + break; + } + + // the next value: close finished containers, then read the key + for (;;) + { + if (depth == 0) + { + return result; + } + frame& f = open[depth - 1]; + if (f.pos == f.end) + { + if (f.object) + { + sax.end_object(); + } + else + { + sax.end_array(); + } + --depth; + continue; + } + const node* entry = f.pos; + if (f.object) + { + string_t key(d.str(*entry), entry->len); + sax.key(key); + ++entry; + } + n = nav::value(entry); + f.pos = document_data::after(entry); + break; + } + } +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // size_t +#include // uint64_t +#include // numeric_limits +#include // string, to_string + +// #include +// #include + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// what resolving a JSON pointer does where it cannot continue +enum class pointer_mode +{ + unchecked, ///< as const basic_json::operator[]: a discarded view where basic_json's behavior is undefined + checked, ///< as basic_json::at(): out_of_range.401/403 + value, ///< as basic_json::value(): no out_of_range exceptions (the default value is used) + contains, ///< as basic_json::contains(): no exceptions at all +}; + +/// the outcome of reading an array index: detail::parse_array_index(), shared with json_pointer +using index_status = detail::array_index_status; + +/// reads an array index like json_pointer::array_index (RFC 6901, Sect. 4), +/// but reports errors instead of throwing them +template +index_status array_index(const StringType& s, std::size_t& idx) noexcept +{ + return detail::parse_array_index(s, idx); +} + +/// throws the exception json_pointer::array_index throws for this status +template +[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_error(index_status status, const StringType& s) +{ + switch (status) + { + case index_status::leading_zero: + throw_parse_error(exception_id::pointer_index_leading_zero, concat("array index '", s, "' must not begin with '0'")); + case index_status::not_a_number: + throw_parse_error(exception_id::pointer_index_not_a_number, concat("array index '", s, "' is not a number")); + case index_status::exceeds_size_type: + throw_out_of_range(exception_id::value_out_of_range, concat("array index ", s, " exceeds size_type")); // LCOV_EXCL_LINE + case index_status::unresolved: + case index_status::ok: + default: + throw_out_of_range(exception_id::pointer_unresolved, concat("unresolved reference token '", s, "'")); + } +} + +/*! +@brief resolve the reference tokens of a JSON pointer, starting at a view + +The exceptions are those basic_json throws for the same pointer; where +basic_json's behavior is undefined (a missing key or an index out of range +with const operator[]), the result is a discarded view. +*/ +template +View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode) +{ + using string_view_t = typename View::string_view_t; + const bool throwing = mode == pointer_mode::unchecked || mode == pointer_mode::checked; + for (const auto& token : tokens) + { + if (cur.is_object()) + { + const auto it = cur.find(string_view_t(token.data(), token.size())); + if (it == cur.end()) + { + if (mode == pointer_mode::checked) + { + throw_key_not_found(token); + } + return View(); + } + cur = *it; + } + else if (cur.is_array()) + { + if (token.size() == 1 && token[0] == '-') + { + if (throwing) + { + throw_array_index_past_end(cur.size()); + } + return View(); + } + std::size_t idx = 0; + const index_status status = array_index(token, idx); + if (status != index_status::ok) + { + const bool parse_error = status == index_status::leading_zero || status == index_status::not_a_number; + if (throwing || (mode == pointer_mode::value && parse_error)) + { + throw_array_index_error(status, token); + } + return View(); + } + if (idx >= cur.size()) + { + if (mode == pointer_mode::checked) + { + throw_array_index_out_of_range(idx); + } + return View(); + } + cur = cur[idx]; + } + else + { + if (throwing) + { + throw_out_of_range(exception_id::pointer_unresolved, concat("unresolved reference token '", token, "'")); + } + return View(); + } + } + return cur; +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // max +#include // array +#include // isfinite +#include // size_t +#include // uint8_t, uint32_t +#include // memcpy, memset +#include // numeric_limits +#include // integral_constant +#include // vector + +// #include +// #include + +// #include + +// #include + +// #include + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// append-only output buffer: writes through a raw pointer into a string that +/// is resized ahead, and trimmed by finish(). The estimate is reserved, and the +/// string grows in steps of 64 KiB within it: resize() fills the new bytes +/// with zeros (before C++23, a string cannot grow without), and a small step +/// is filled while the writer is about to use it, in the cache, instead of +/// filling the whole estimate in memory first. +template +class output_buffer +{ + public: + output_buffer(StringType& out, std::size_t estimate) + : m_out(sized(out, estimate)) + , m_pos(&m_out[0]) + , m_end(m_pos + m_out.size()) + {} + + void finish() + { + const auto size = static_cast(m_pos - m_out.data()); + m_out.resize(size); + } + + NLOHMANN_VIEW_ALWAYS_INLINE void reserve(std::size_t n) + { + if (NLOHMANN_VIEW_UNLIKELY(static_cast(m_end - m_pos) < n)) + { + grow(n); + } + } + + NLOHMANN_VIEW_ALWAYS_INLINE void put(char c) + { + reserve(1); + *m_pos++ = c; + } + + NLOHMANN_VIEW_ALWAYS_INLINE void put(const char* s, std::size_t n) + { + reserve(n); + std::memcpy(m_pos, s, n); + m_pos += n; + } + + /// a string literal, without its terminating NUL + template + NLOHMANN_VIEW_ALWAYS_INLINE void put(const char (&s)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + { + put(s, N - 1); + } + + /// the write position and the end of the writable space, for a writer + /// that keeps the position in a local variable (set_cursor() hands it back) + char* cursor() const noexcept + { + return m_pos; + } + + char* limit() const noexcept + { + return m_end; + } + + void set_cursor(char* p) noexcept + { + m_pos = p; + } + + private: + /// the size of a growth step + static constexpr std::size_t step = 0x10000; + + static StringType& sized(StringType& out, std::size_t estimate) + { + out.reserve(estimate); + out.resize(detail::clamp(estimate, std::size_t{64}, step)); + return out; + } + + NLOHMANN_VIEW_NOINLINE void grow(std::size_t n) + { + const auto used = static_cast(m_pos - m_out.data()); + // (a step does not go beyond the reserved estimate, so that a good + // estimate is never copied to a larger allocation; not a clamp: the + // lower bound used + n + 256 can exceed capacity(), and then it must win) + const std::size_t size = (std::max)((std::min)(m_out.size() + step, m_out.capacity()), used + n + 256); + if (size > m_out.capacity()) + { + m_out.reserve((std::max)(m_out.capacity() * 2, size)); + } + m_out.resize(size); + m_pos = &m_out[0] + used; + m_end = &m_out[0] + m_out.size(); + } + + StringType& m_out; + char* m_pos; + char* m_end; +}; + +/// The length of the run at s that dump() writes unchanged without +/// ensure_ascii: all bytes but quotes, backslashes, and control characters. +/// Unlike detail::string_bulk_run(), non-ASCII bytes are not validated: the +/// strings of a document are valid UTF-8 (a damaged image loaded with +/// image_check::bounds can have others, which are then written unchanged). +inline std::size_t plain_output_run(const unsigned char* s, std::size_t n) noexcept +{ + std::size_t i = 0; +#if NLOHMANN_VIEW_SSE2 + // (16 bytes per step; with NEON, the mask of a step costs more than it saves on the + // short runs that are typical here) + // NOLINTBEGIN(portability-simd-intrinsics) + for (; i + 16 <= n; i += 16) + { + const __m128i in = _mm_loadu_si128(static_cast(static_cast(s + i))); // NOLINT(bugprone-casting-through-void) + // (a byte is below 0x20 if it is its own minimum with 0x1F) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_cmpeq_epi8(_mm_min_epu8(in, _mm_set1_epi8(0x1F)), in)); + const auto mask = static_cast(static_cast(_mm_movemask_epi8(special))); + if (mask != 0) + { + return i + static_cast(count_trailing_zeros(mask)); + } + } + // NOLINTEND(portability-simd-intrinsics) +#endif + constexpr std::uint64_t ones = 0x0101010101010101ull; + constexpr std::uint64_t high = 0x8080808080808080ull; + for (; i + 8 <= n; i += 8) + { + const std::uint64_t v = read_eight_bytes(s + i); + const std::uint64_t q = v ^ 0x2222222222222222ull; // '"' + const std::uint64_t b = v ^ 0x5C5C5C5C5C5C5C5Cull; // '\\' + const std::uint64_t stop = (((q - ones) & ~q) | ((b - ones) & ~b) | ((v - 0x2020202020202020ull) & ~v)) & high; + if (stop != 0) + { + // the lowest flagged byte is the first stop: borrows only flag bytes above a true one + return i + (static_cast(count_trailing_zeros(stop)) / 8); + } + } + for (; i < n; ++i) + { + if (s[i] == '"' || s[i] == '\\' || s[i] < 0x20) + { + return i; + } + } + return n; +} + +/// Whether dump() with ensure_ascii writes the string at s unchanged, for a +/// string of the source without escape sequences (which has no quote, +/// backslash, or control character): no byte is DEL (0x7F) or above. Adding 1 +/// to each byte sets its high bit exactly for 0x7F, and a carry into the next +/// byte only comes from 0xFF, whose own high bit is set anyway. +inline bool is_ascii_without_delete(const char* s, 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(s + i); + if (((v | (v + ones)) & high) != 0) + { + return false; + } + } + for (; i < n; ++i) + { + if (static_cast(s[i]) >= 0x7F) + { + return false; + } + } + return true; +} + +#if NLOHMANN_VIEW_VECTOR +/// the bytes of the 16 at p that ensure_ascii cannot copy (a quote, a +/// backslash, a control character, DEL, or not ASCII; a signed compare with +/// 0x20 finds control characters and non-ASCII bytes at once), one bit per +/// byte (x86) or one nibble per byte (NEON) +NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t ascii_stop_mask(const unsigned char* p) noexcept +{ +#if NLOHMANN_VIEW_NEON + const uint8x16_t in = vld1q_u8(p); + const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))), + vorrq_u8(vceqq_u8(in, vdupq_n_u8(0x7F)), vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20)))); + return vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0); +#else + const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) + const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), + _mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8(0x7F)), _mm_cmplt_epi8(in, _mm_set1_epi8(0x20)))); + return static_cast(static_cast(_mm_movemask_epi8(special))); +#endif +} + +/// the width in bits of a byte in the mask of ascii_stop_mask() +constexpr unsigned ascii_stop_bits() noexcept +{ + return NLOHMANN_VIEW_NEON ? 4u : 1u; +} +#endif + +/// As find_ascii_copyable_run(), 16 bytes per step. end is the end of the +/// buffer that holds the n bytes at p (a short run is read as 16 bytes if +/// they are in the buffer). +inline std::size_t ascii_copyable_run(const unsigned char* p, std::size_t n, const unsigned char* end) noexcept +{ +#if NLOHMANN_VIEW_VECTOR + if (n >= 16) + { + std::size_t i = 0; + for (; i + 16 <= n; i += 16) + { + const std::uint64_t mask = ascii_stop_mask(p + i); + if (mask != 0) + { + return i + (static_cast(count_trailing_zeros(mask)) / ascii_stop_bits()); + } + } + if (i == n) + { + return n; + } + // the last 16 bytes: those before i are known to be copyable + const std::uint64_t mask = ascii_stop_mask(p + n - 16); + return mask != 0 ? n - 16 + (static_cast(count_trailing_zeros(mask)) / ascii_stop_bits()) : n; + } + if (end - p >= 16) + { + // (the bytes after the run are masked out) + const std::uint64_t mask = ascii_stop_mask(p) & ((std::uint64_t{1} << (n * ascii_stop_bits())) - 1u); + return mask != 0 ? static_cast(count_trailing_zeros(mask)) / ascii_stop_bits() : n; + } +#else + static_cast(end); +#endif + return find_ascii_copyable_run(p, n); +} + +/// A stack that starts in a buffer of the caller (a local array) and moves to +/// the heap (a vector of the caller) only when that is full, so that dumps of +/// shallow documents need no allocation. The top is a pointer, as in +/// std::vector. grow() is static and out of line: it gets the vector and the +/// used range and returns the new storage, so the address of the stack never +/// reaches a call. Otherwise the compiler would have to keep the three +/// pointers in memory and load and store them on every push_back() and +/// pop_back(), not just when growing. +template +class small_stack +{ + public: + small_stack(T* buffer, std::size_t capacity, std::vector& heap) noexcept + : m_begin(buffer), m_top(buffer), m_end(buffer + capacity), m_heap(heap) + {} + small_stack(const small_stack&) = delete; + small_stack(small_stack&&) = delete; + small_stack& operator=(const small_stack&) = delete; + small_stack& operator=(small_stack&&) = delete; + ~small_stack() = default; + + NLOHMANN_VIEW_ALWAYS_INLINE void push_back(const T& x) + { + if (NLOHMANN_VIEW_UNLIKELY(m_top == m_end)) + { + const std::size_t used = size(); + const std::size_t capacity = 2 * static_cast(m_end - m_begin); + m_begin = grow(m_heap, m_begin, used, capacity); + m_top = m_begin + used; + m_end = m_begin + capacity; + } + *m_top++ = x; + } + + NLOHMANN_VIEW_ALWAYS_INLINE T& back() noexcept + { + return m_top[-1]; + } + + NLOHMANN_VIEW_ALWAYS_INLINE void pop_back() noexcept + { + --m_top; + } + + NLOHMANN_VIEW_ALWAYS_INLINE bool empty() const noexcept + { + return m_top == m_begin; + } + + NLOHMANN_VIEW_ALWAYS_INLINE std::size_t size() const noexcept + { + return static_cast(m_top - m_begin); + } + + private: + /// the used entries moved to heap storage of the given capacity + NLOHMANN_VIEW_NOINLINE static T* grow(std::vector& heap, const T* begin, std::size_t used, std::size_t capacity) + { + std::vector bigger(capacity); + std::copy(begin, begin + used, bigger.begin()); + heap.swap(bigger); + return heap.data(); + } + + T* m_begin; + T* m_top; + T* m_end; + std::vector& m_heap; +}; + +/// a string literal at w, without its terminating NUL (after room()) +/// @return a pointer past it +template +NLOHMANN_VIEW_ALWAYS_INLINE char* put_literal(char* w, const char (&s)[N]) noexcept // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) +{ + std::memcpy(w, s, N - 1); + return w + N - 1; +} + +/// the depth of the innermost open container, counted only for indented output +template +struct depth_counter +{ + std::size_t value = 0; + void enter() noexcept + { + ++value; + } + std::size_t leave() noexcept + { + return --value; + } + std::size_t get() const noexcept + { + return value; + } +}; + +template<> +struct depth_counter +{ + void enter() noexcept {} + std::size_t leave() noexcept // NOLINT(readability-convert-member-functions-to-static): same interface as depth_counter + { + return 0; + } + std::size_t get() const noexcept // NOLINT(readability-convert-member-functions-to-static): same interface as depth_counter + { + return 0; + } +}; + +/// how the view's dump() writes a value +struct dump_style +{ + bool pretty = false; ///< indent >= 0 + std::size_t indent = 0; ///< characters per level + char indent_char = ' '; + bool ensure_ascii = false; + bool source_numbers = false; ///< copy number tokens from the source +}; + +/*! +@brief write a view's subtree as basic_json::dump() writes the value + +The output of a subtree equals ordered_json::parse(text).dump() of it for +the same arguments (members in document order): strings are escaped by the +same rules, with the library's scanning kernels; floats are written with +the library's conversion; integers are copied from the source, where they +are canonical (except "-0", which parse() reads as 0). The walk is +iterative, so the nesting depth is limited by memory only. +*/ +template +class view_serializer +{ + using nav = navigation; + using string_t = typename BasicJsonType::string_t; + using number_float_t = typename BasicJsonType::number_float_t; + /// doubles that are IEEE 754 binary64 are written from their bits (Zmij) + using fast_double = std::integral_constant < bool, std::is_same::value && ::nlohmann::detail::dtoa_impl::is_binary64::value >; + + public: + view_serializer(const document_data& d, string_t& out, std::size_t estimate, const dump_style& style) + : m_doc(d), m_out(out, estimate), m_style(style) + {} + + void dump(const node* root) + { + if (m_style.ensure_ascii) + { + dump_ascii(root); + } + else if (m_style.pretty) + { + dump_pretty(root); + } + else if (m_style.source_numbers) + { + dump_text(root); + } + else + { + dump_text(root); + } + } + + private: + /// dump_text() with ensure_ascii, out of line: the walk without it is + /// faster when it is not merged with this one into dump() + NLOHMANN_VIEW_NOINLINE void dump_ascii(const node* root) + { + if (m_style.pretty) + { + if (m_style.source_numbers) + { + dump_text(root); + } + else + { + dump_text(root); + } + } + else if (m_style.source_numbers) + { + dump_text(root); + } + else + { + dump_text(root); + } + } + + /// dump_text() with indentation, out of line: the compact walk is faster + /// when it is not merged with the indented ones into dump() + NLOHMANN_VIEW_NOINLINE void dump_pretty(const node* root) + { + if (m_style.source_numbers) + { + dump_text(root); + } + else + { + dump_text(root); + } + } + + /// a string that is not a plain string of the source (decoded, or written + /// by an edit), without ensure_ascii: runs without characters to escape + /// are copied. If validate is set, the string must be valid UTF-8: the + /// runs are found by the lexer's scan, which validates the sequences, and + /// invalid UTF-8 throws what basic_json::dump() throws. (The compact output + /// writes the bytes of a damaged image unchanged, and scans without + /// validating them.) + NLOHMANN_VIEW_NOINLINE void write_decoded(const node& n, bool validate) + { + const auto* const s = reinterpret_cast(m_doc.str(n)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + m_out.put('"'); + for (std::size_t i = 0; i < n.len;) + { + const std::size_t run = validate ? string_bulk_run(s + i, n.len - i) : plain_output_run(s + i, n.len - i); + if (run != 0) + { + m_out.put(reinterpret_cast(s + i), run); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + i += run; + continue; + } + if (validate && s[i] >= 0x80) + { + invalid_utf8(s, n.len); // (the scan stopped at ill-formed UTF-8) + } + write_codepoint(s[i], s + i, 1); // a quote, a backslash, or a control character + ++i; + } + m_out.put('"'); + } + + /// whether dump_text() copies a string or key from the source: one of + /// the source, and with ensure_ascii one without bytes >= 0x7F (an overload + /// each, so that the walk without ensure_ascii is compiled exactly as + /// before: with GCC, a condition that only folds away later changed its + /// block layout and made it 4-6% slower) + static bool copied(const node& n, const char* /*src*/, std::false_type /*ensure_ascii*/) noexcept + { + return (n.flags & node_flags::storage) == 0; + } + + static bool copied(const node& n, const char* src, std::true_type /*ensure_ascii*/) noexcept + { + return (n.flags & node_flags::storage) == 0 && is_ascii_without_delete(src + n.off, n.len); + } + + /*! + @brief a string with ensure_ascii: the characters that are not printable ASCII as \\uXXXX + (a character beyond U+FFFF as a surrogate pair); strings of a document + are valid UTF-8, except in a damaged image loaded with image_check::bounds, + for which this throws what basic_json::dump() throws for the string + */ + NLOHMANN_VIEW_NOINLINE void write_ascii(const node& n) + { + const auto* const s = reinterpret_cast(m_doc.str(n)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + const std::size_t size = n.len; + char* w = m_out.cursor(); + char* lim = m_out.limit(); + // room for k bytes + const auto room = [&](std::size_t k) + { + const auto left = static_cast(lim - w); + if (NLOHMANN_VIEW_UNLIKELY(left < k)) + { + m_out.set_cursor(w); + m_out.reserve(k); + w = m_out.cursor(); + lim = m_out.limit(); + } + }; + static constexpr const char* hex = "0123456789abcdef"; + // \\uXXXX + const auto u_escape = [&](std::uint32_t u) + { + w[0] = '\\'; + w[1] = 'u'; + w[2] = hex[(u >> 12u) & 0xFu]; + w[3] = hex[(u >> 8u) & 0xFu]; + w[4] = hex[(u >> 4u) & 0xFu]; + w[5] = hex[u & 0xFu]; + w += 6; + }; + room(1); + *w++ = '"'; + std::size_t i = 0; + // (a string of the source starts with a run that has nothing to escape, 16 bytes per step) + const std::size_t prefix = (n.flags & node_flags::storage) == 0 ? ascii_copyable_run(s, size, reinterpret_cast(m_doc.src + m_doc.size)) : 0; // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + if (prefix != 0) + { + room(prefix); + std::memcpy(w, s, prefix); + w += prefix; + i = prefix; + } + while (i < size) + { + // (a character that needs an escape sequence is not worth a call) + const std::size_t run = is_ascii_copyable(s[i]) ? find_ascii_copyable_run(s + i, size - i) : 0; + if (run != 0) + { + room(run); + std::memcpy(w, s + i, run); + w += run; + i += run; + continue; + } + room(12); + if (s[i] >= 0x80) + { + const unsigned char lead = s[i]; + // (the two-byte sequences of Latin text and the three-byte sequences of most other + // text, with continuation bytes of any value: the ranges that are restricted (E0, + // ED, and the sequences of four bytes) go through validate_one_utf8()) + if (lead >= 0xE1 && lead <= 0xEF && lead != 0xED && size - i >= 3 && (s[i + 1] & 0xC0u) == 0x80 && (s[i + 2] & 0xC0u) == 0x80) + { + u_escape(((lead & 0x0Fu) << 12u) | ((s[i + 1] & 0x3Fu) << 6u) | (s[i + 2] & 0x3Fu)); + i += 3; + continue; + } + if (lead >= 0xC2 && lead <= 0xDF && size - i >= 2 && (s[i + 1] & 0xC0u) == 0x80) + { + u_escape(((lead & 0x1Fu) << 6u) | (s[i + 1] & 0x3Fu)); + i += 2; + continue; + } + const std::size_t len = validate_one_utf8(s + i, size - i); + if (NLOHMANN_VIEW_UNLIKELY(len == 0)) + { + m_out.set_cursor(w); + invalid_utf8(s, size); + return; // LCOV_EXCL_LINE (invalid_utf8 throws) + } + std::uint32_t codepoint = s[i] & (0xFFu >> (len + 1)); + for (std::size_t k = 1; k < len; ++k) + { + codepoint = (codepoint << 6u) | (s[i + k] & 0x3Fu); + } + i += len; + if (codepoint <= 0xFFFF) + { + u_escape(codepoint); + } + else + { + u_escape(0xD7C0u + (codepoint >> 10u)); + u_escape(0xDC00u + (codepoint & 0x3FFu)); + } + continue; + } + // a quote, a backslash, or a control character (or DEL) + const unsigned char c = s[i++]; + char escape = 0; + switch (c) + { + case 0x08: + escape = 'b'; + break; + case 0x09: + escape = 't'; + break; + case 0x0A: + escape = 'n'; + break; + case 0x0C: + escape = 'f'; + break; + case 0x0D: + escape = 'r'; + break; + case 0x22: + escape = '"'; + break; + case 0x5C: + escape = '\\'; + break; + default: + break; + } + if (escape != 0) + { + w[0] = '\\'; + w[1] = escape; + w += 2; + } + else + { + u_escape(c); + } + } + room(1); + *w++ = '"'; + m_out.set_cursor(w); + } + + /// the copies of dump_text() that are not fixed-size moves (long + /// strings, or near the end of the source); out of line, so that the + /// compiler does not merge the fixed-size moves into this call + NLOHMANN_VIEW_NOINLINE static void copy_long(char* to, const char* from, std::size_t n) noexcept + { + std::memcpy(to, from, n); + } + + /// a compile-time option as a runtime condition: testing the template + /// argument directly makes a condition like `Editable && c` constant when + /// the option is off, which MSVC reports as C4127 + static NLOHMANN_VIEW_ALWAYS_INLINE bool enabled(bool option) noexcept + { + return option; + } + + /*! + @brief the output, compact (the default dump()) or indented, with or without ensure_ascii + + The same walk as dump(), with the write position in a local variable + (stores through char pointers would otherwise force a reload of the + buffer's members after each one), and with strings and number tokens of + the source copied by fixed-size moves of 32 bytes where the source has + that many bytes left, instead of a library call per token. The buffer + keeps 64 bytes of slack for the overshoot. With ensure_ascii (Ascii), a + string of the source is copied only if it has nothing to escape (see + copied()); any other string is written by write_ascii(). + */ + template + void dump_text(const node* root) + { + struct frame + { + const node* pos; ///< (editable documents) next element, or key of the next member + const node* end; + bool object; + }; + std::array buffer; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read + std::vector heap; + small_stack stack(buffer.data(), buffer.size(), heap); + const char* const src = m_doc.src; + const char* const src_end = src + m_doc.size; + char* w = m_out.cursor(); + char* lim = m_out.limit(); + // (indented output) a line break and 63 indentation characters, from + // which the line break of a level of up to 63 characters is a copy + std::array ruler; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): filled below if used + if (Pretty) + { + ruler.fill(m_style.indent_char); + ruler[0] = '\n'; + } + const std::size_t indent = m_style.indent; + depth_counter level; // depth of the innermost open container (indented output) + // room for n bytes and the slack + const auto room = [&](std::size_t n) + { + if (NLOHMANN_VIEW_UNLIKELY(static_cast(lim - w) < n + 64)) + { + m_out.set_cursor(w); + m_out.reserve(n + 64); + w = m_out.cursor(); + lim = m_out.limit(); + } + }; + // copy n bytes of the source (after room(n)) + const auto copy = [&](const char* from, std::size_t n) + { + if (n <= 32 && static_cast(src_end - from) >= 32) + { + std::memcpy(w, from, 32); + } + else if (n <= 256 && static_cast(src_end - from) >= n + 32) + { + for (std::size_t i = 0; i < n; i += 32) + { + std::memcpy(w + i, from + i, 32); + } + } + else + { + copy_long(w, from, n); + } + w += n; + }; + // (indented output) the line break and the indentation of a level + const auto newline = [&](std::size_t depth) + { + const std::size_t count = 1 + (depth * indent); + room(count); + if (count <= ruler.size()) + { + std::memcpy(w, ruler.data(), ruler.size()); + } + else + { + *w = '\n'; + std::memset(w + 1, m_style.indent_char, count - 1); + } + w += count; + }; + // a string that is not a plain string of the source (out of line, so + // that the cursor stays in a register here) + const auto escaped = [&](const node & n) + { + m_out.set_cursor(w); + if (enabled(Ascii)) + { + write_ascii(n); + } + else + { + write_decoded(n, Pretty); + } + w = m_out.cursor(); + lim = m_out.limit(); + }; + + // Read-only documents: the elements of a container follow it in the + // node array, so the walk goes through the array in order, and a + // frame only needs the end of its container. Editable documents: the + // elements of a moved container live elsewhere, so a frame keeps the + // position of the next element (see navigation). + // The innermost open container is kept in registers (cur; end == + // nullptr: none), the stack holds the ones around it. + frame cur{nullptr, nullptr, false}; + const node* n = root; + for (;;) + { + // write the value at n (read-only documents: and advance n) + bool opened = false; + // false positive: n comes from nav::value(), which never returns null for a valid index + // @infer-ignore NULLPTR_DEREFERENCE + switch (static_cast(n->kind)) + { + case value_t::string: + if (copied(*n, src, std::integral_constant())) + { + room(n->len + 2); + *w++ = '"'; + copy(src + n->off, n->len); + *w++ = '"'; + } + else + { + escaped(*n); + } + break; + case value_t::number_integer: + case value_t::number_unsigned: + { + const std::uint32_t len = number_length(*n); + room(len); + if (enabled(Editable) && (n->flags & node_flags::storage) != 0) + { + copy_long(w, m_doc.str(*n), len); // a canonical token written by an edit + w += len; + break; + } + const char* const token = src + n->off; + if (!enabled(SourceNumbers) && NLOHMANN_VIEW_UNLIKELY(len == 2 && token[0] == '-' && token[1] == '0')) + { + *w++ = '0'; // parse() reads -0 as the integer 0 + } + else + { + copy(token, len); + } + break; + } + case value_t::number_float: + if (enabled(SourceNumbers) && (n->flags & node_flags::storage) != node_flags::edited) + { + room(n->len); + copy(src + n->off, n->len); + } + else if (enabled(fast_double::value)) + { + room(64); + w = write_double_at(w, *n); + } + else + { + m_out.set_cursor(w); + write_float_node(*n); + w = m_out.cursor(); + lim = m_out.limit(); + } + break; + case value_t::boolean: + room(8); + if ((n->flags & node_flags::is_true) != 0) + { + w = put_literal(w, "true"); + } + else + { + w = put_literal(w, "false"); + } + break; + case value_t::object: + case value_t::array: + { + const bool object = n->kind == static_cast(value_t::object); + room(8); + if (n->len == 0) + { + w = object ? put_literal(w, "{}") : put_literal(w, "[]"); + } + else + { + *w++ = object ? '{' : '['; + stack.push_back(cur); + level.enter(); + if (Editable) + { + cur = frame{nav::first(m_doc, n), nav::end(m_doc, n), object}; + } + else + { + cur = frame{nullptr, n + n->next, object}; + } + opened = true; + } + break; + } + case value_t::null: + room(8); + w = put_literal(w, "null"); + break; + case value_t::binary: // LCOV_EXCL_LINE (not in a document) + case value_t::discarded: // LCOV_EXCL_LINE + default: // LCOV_EXCL_LINE + break; // LCOV_EXCL_LINE + } + if (!Editable) + { + ++n; // the next node: the first element of an opened container, or the node after a scalar + } + + // go to the next value: close finished containers, then separate + // (a container just opened has an element) + if (!opened) + { + for (;;) + { + if (cur.end == nullptr) + { + m_out.set_cursor(w); + m_out.finish(); + return; + } + if ((Editable ? cur.pos : n) != cur.end) + { + break; + } + if (Pretty) + { + newline(level.leave()); + } + room(1); + *w++ = cur.object ? '}' : ']'; + cur = stack.back(); + stack.pop_back(); + } + room(1); + *w++ = ','; + } + if (Pretty) + { + newline(level.get()); + } + const node* const at = Editable ? cur.pos : n; + if (cur.object) + { + const node& key = *at; + if (copied(key, src, std::integral_constant())) + { + room(key.len + (Pretty ? 4 : 3)); + *w++ = '"'; + copy(src + key.off, key.len); + w[0] = '"'; + w[1] = ':'; + w += 2; + } + else + { + escaped(key); + room(Pretty ? 2 : 1); + *w++ = ':'; + } + if (Pretty) + { + *w++ = ' '; + } + if (Editable) + { + n = nav::value(at + 1); + cur.pos = document_data::after(at + 1); + } + else + { + ++n; + } + } + else if (Editable) + { + n = nav::value(at); + cur.pos = document_data::after(at); + } + } + } + + /// a float node as dump() writes it + void write_float_node(const node& n) + { + write_float_node(n, fast_double {}); + } + + void write_float_node(const node& n, std::false_type /*other*/) + { + write_float(float_value(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) + + A token of at most 15 significant digits is written from its digits, + without a conversion: two decimals of at most 15 digits are farther + apart than the rounding interval of a (normal) double (the argument + behind DBL_DIG), so the token's digits are the shortest ones of its + double, which the library's conversion writes (Zmij). Other tokens are + converted from the digits already read. + */ + char* write_double_at(char* w, const node& n) + { + const unsigned int_digits = n.extra & 0xFFu; + const unsigned frac_digits = n.extra >> 8u; + if ((n.flags & node_flags::storage) != node_flags::edited && int_digits + frac_digits <= 19) + { + const auto* const first = reinterpret_cast(m_doc.src + n.off); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + const float_significand d = layout_decimal(first, first + n.len, int_digits, frac_digits, reinterpret_cast(m_doc.src + m_doc.size)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + // (the exponent keeps the value far from subnormals and overflow) + if (d.w != 0 && d.w < 1000000000000000u && d.exponent >= -290 && d.exponent <= 290) + { + *w = '-'; + w += d.negative ? 1 : 0; + // (without leading zeros, all digits of the token count; the + // check also keeps an image that was only checked for bounds, + // whose token may not be made of digits, from the counted + // overload) + const auto& powers = ::nlohmann::detail::dtoa_impl::powers_of_ten(); + const unsigned count = int_digits + frac_digits; + return count - 1u < 15u && d.w >= powers[count - 1u] && d.w < powers[count] + ? ::nlohmann::detail::dtoa_impl::write_short_decimal(w, d.w, static_cast(count), static_cast(d.exponent)) + : ::nlohmann::detail::dtoa_impl::write_short_decimal(w, d.w, static_cast(d.exponent)); + } + return write_double_value_at(w, decimal_to_float(d)); // (without reading the token again) + } + return write_double_value_at(w, static_cast(float_value(m_doc, n))); + } + + /// n bytes of text at w + static char* write_text_at(char* w, const char* text, std::size_t n) noexcept + { + std::memcpy(w, text, n); + return w + n; + } + + /// a double as dump() writes it, at w (64 bytes of room) + static char* write_double_value_at(char* w, double x) + { + // (from the bits: without the checks of to_chars()) + auto bits = bit_cast(x); + if (NLOHMANN_VIEW_UNLIKELY((bits & 0x7FF0000000000000u) == 0x7FF0000000000000u)) + { + return write_text_at(w, "null", 4); + } + *w = '-'; + w += bits >> 63u; + bits &= ~(std::uint64_t{1} << 63u); + if (bits == 0) + { + return write_text_at(w, "0.0", 3); + } + return ::nlohmann::detail::dtoa_impl::write_shortest(w, ::nlohmann::detail::zmij::to_shortest(bits)); + } + + /// as serializer::dump_float() + void write_float(number_float_t x) + { + if (!std::isfinite(x)) + { + m_out.put("null"); + return; + } + write_float(x, std::integral_constant::value>()); + } + + void write_float(number_float_t x, std::true_type /*has_native_float_format*/) + { + std::array buf{}; + const char* const end = ::nlohmann::detail::to_chars(buf.data(), buf.data() + buf.size(), x); + m_out.put(buf.data(), static_cast(end - buf.data())); + } + + void write_float(number_float_t x, std::false_type /*has_native_float_format*/) + { + // other types (e.g. long double) are rare: the library writes them + const string_t s = BasicJsonType(x).dump(); + m_out.put(s.data(), s.size()); + } + + /// throw what basic_json::dump() throws for a string that is not valid UTF-8 + NLOHMANN_VIEW_NOINLINE static void invalid_utf8(const unsigned char* s, std::size_t n) + { + const string_t dumped = BasicJsonType(string_t(reinterpret_cast(s), n)).dump(); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + static_cast(dumped); + } + + template + void write_codepoint(std::uint32_t codepoint, const unsigned char* bytes, std::size_t len) + { + switch (codepoint) + { + case 0x08: + m_out.put("\\b"); + return; + case 0x09: + m_out.put("\\t"); + return; + case 0x0A: + m_out.put("\\n"); + return; + case 0x0C: + m_out.put("\\f"); + return; + case 0x0D: + m_out.put("\\r"); + return; + case 0x22: + m_out.put("\\\""); + return; + case 0x5C: + m_out.put("\\\\"); + return; + default: + break; + } + if (codepoint <= 0x1F || (EnsureAscii && codepoint >= 0x7F)) + { + 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(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) + { + std::array e = {{'\\', 'u', 0, 0, 0, 0}}; + hex4_lower(static_cast(u), &e[2]); + m_out.put(e.data(), e.size()); + } + + const document_data& m_doc; + output_buffer m_out; + const dump_style m_style; +}; + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // min +#include // size_t +#include // memcmp, strlen +#include // basic_string + +// #include +// #include + + +#if NLOHMANN_VIEW_HAS_CPP_17 + #include // string_view +#endif +#ifndef JSON_NO_IO + #include // ostream +#endif + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +#if NLOHMANN_VIEW_HAS_CPP_17 +using string_ref = std::string_view; +#else +/// minimal C++11 stand-in for std::string_view +class string_ref +{ + public: + using size_type = std::size_t; + using const_iterator = const char*; + + string_ref() noexcept = default; + // s must be null-terminated, as for std::string_view(const char*) + // flawfinder: ignore + string_ref(const char* s) : m_data(s), m_size(std::strlen(s)) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) + string_ref(const char* s, std::size_t n) noexcept : m_data(s), m_size(n) {} + template + string_ref(const std::basic_string& s) noexcept : m_data(s.data()), m_size(s.size()) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) + + const char* data() const noexcept + { + return m_data; + } + std::size_t size() const noexcept + { + return m_size; + } + std::size_t length() const noexcept + { + return m_size; + } + bool empty() const noexcept + { + return m_size == 0; + } + const char* begin() const noexcept + { + return m_data; + } + const char* end() const noexcept + { + return m_data + m_size; + } + char operator[](std::size_t i) const noexcept + { + return m_data[i]; + } + + template + explicit operator std::basic_string() const + { + return std::basic_string(m_data, m_size); + } + + friend bool operator==(string_ref a, string_ref b) noexcept + { + return a.m_size == b.m_size && (a.m_size == 0 || std::memcmp(a.m_data, b.m_data, a.m_size) == 0); + } + friend bool operator!=(string_ref a, string_ref b) noexcept + { + return !(a == b); + } + friend bool operator<(string_ref a, string_ref b) noexcept + { + const int c = std::memcmp(a.m_data, b.m_data, (std::min)(a.m_size, b.m_size)); + return c != 0 ? c < 0 : a.m_size < b.m_size; + } +#ifndef JSON_NO_IO + friend std::ostream& operator<<(std::ostream& o, string_ref s) + { + return o.write(s.m_data, static_cast(s.m_size)); + } +#endif + + private: + const char* m_data = ""; + std::size_t m_size = 0; +}; +#endif + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // nullptr_t, size_t +#include // int64_t +#include // map +#include // basic_string +#include // decay, enable_if, is_arithmetic, is_constructible, is_integral, is_same +#include // unordered_map +#include // vector + +// #include +// #include + +// #include + +// #include + +// #include + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// selects a conversion by its target type +template +struct value_tag {}; + +/*! +@brief the number or boolean of a node converted to an arithmetic type + +As basic_json's get() for arithmetic types: integers and floats are +converted with static_cast, booleans give 0 or 1, and other types throw +type_error.302. +*/ +template +NLOHMANN_VIEW_ALWAYS_INLINE T arithmetic_value(const document_data& d, const node& n) +{ + switch (static_cast(n.kind)) + { + case value_t::number_unsigned: + return static_cast(static_cast(integer_bits(n))); + case value_t::number_integer: + return static_cast(static_cast(static_cast(integer_bits(n)))); + case value_t::number_float: + return static_cast(float_value(d, n)); + case value_t::boolean: + return static_cast((n.flags & node_flags::is_true) != 0); + 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: + throw_type_error(exception_id::type_mismatch, "type must be number, but is ", value_type_name(static_cast(n.kind))); + } +} + +/// std::vector from an array, element by element (type_error.302 otherwise) +template +std::vector vector_value(const View& v) +{ + if (NLOHMANN_VIEW_UNLIKELY(!v.is_array())) + { + throw_type_error(exception_id::type_mismatch, "type must be array, but is ", v.type_name()); + } + std::vector r; + r.reserve(v.size()); + for (const View e : v) + { + r.push_back(e.template get()); + } + return r; +} + +/// a map with string keys from an object; with duplicate keys, the last +/// value is kept, as parse() does (type_error.302 for other types) +template +Map map_value(const View& v) +{ + if (NLOHMANN_VIEW_UNLIKELY(!v.is_object())) + { + throw_type_error(exception_id::type_mismatch, "type must be object, but is ", v.type_name()); + } + Map r; + for (auto it = v.begin(); it != v.end(); ++it) + { + const auto key = it.key(); + r[typename Map::key_type(key.data(), key.size())] = it.value().template get(); + } + return r; +} + +/// whether a map type is read member by member (its keys are made from +/// characters and a length); other maps go through basic_json +template +struct is_string_key : std::is_constructible {}; + +/// the arithmetic types arithmetic_value() converts to (bool has its own +/// conversion) +template +struct is_non_bool_arithmetic : conjunction, negation>> {}; + +/// the default values of value() other than C strings, which have their own +/// overload (returning string_t) +template +struct is_non_c_string_default : negation::type, const char*>> {}; + +/// the integral types accepted as an array index by the edits of a document +template +struct is_non_bool_integral : conjunction, negation>> {}; + +/// whether View::get() converts to T without materialize(); get_to() +/// assigns these, and passes the others to from_json +template +struct is_direct_value : std::integral_constant < bool, + std::is_arithmetic::value || std::is_same::value + || std::is_same::value + || std::is_same::value || std::is_same::value > +{}; + +template +struct is_direct_value, BasicJsonType, View> : std::true_type {}; + +template +struct is_direct_value, BasicJsonType, View> : std::true_type {}; + +template +struct is_direct_value, BasicJsonType, View> : is_string_key {}; + +template +struct is_direct_value, BasicJsonType, View> : is_string_key {}; + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + + +NLOHMANN_JSON_NAMESPACE_BEGIN + +/*! +@brief read-only handle to one value of a basic_json_document + +Trivially copyable (two pointers). Valid as long as the document is alive and +has not been re-parsed, and as long as a borrowed source text is alive. +*/ +template +class basic_json_view +{ + using node = detail::view::node; + using document_data = detail::view::document_data; + /// how the index is walked (with edits only for editable documents) + using navigation = detail::view::navigation; + + public: + using value_t = detail::value_t; + using string_t = typename BasicJsonType::string_t; + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using json_pointer = typename BasicJsonType::json_pointer; + using size_type = std::size_t; + /// std::string_view from C++17 on + using string_view_t = detail::view::string_ref; + /// forward iterator over elements (arrays) or member values (objects) + using iterator = detail::view::view_iterator; + using const_iterator = iterator; + /// a (key, value) item of items() + using item = detail::view::view_item; + + /// an invalid view (type() == value_t::discarded) + basic_json_view() noexcept = default; + + ////////// + // type // + ////////// + + NLOHMANN_VIEW_ALWAYS_INLINE value_t type() const noexcept + { + return m_node != nullptr ? static_cast(m_node->kind) : value_t::discarded; + } + + bool is_null() const noexcept + { + return type() == value_t::null; + } + + bool is_boolean() const noexcept + { + return type() == value_t::boolean; + } + + bool is_number() const noexcept + { + return is_number_integer() || is_number_float(); + } + + bool is_number_integer() const noexcept + { + return type() == value_t::number_integer || type() == value_t::number_unsigned; + } + + bool is_number_unsigned() const noexcept + { + return type() == value_t::number_unsigned; + } + + bool is_number_float() const noexcept + { + return type() == value_t::number_float; + } + + bool is_string() const noexcept + { + return type() == value_t::string; + } + + bool is_array() const noexcept + { + return type() == value_t::array; + } + + bool is_object() const noexcept + { + return type() == value_t::object; + } + + /// always false: JSON text has no binary values + bool is_binary() const noexcept + { + return false; + } + + bool is_primitive() const noexcept + { + return is_null() || is_string() || is_boolean() || is_number(); + } + + bool is_structured() const noexcept + { + return is_array() || is_object(); + } + + /// the root of a failed parse with allow_exceptions == false, or a + /// default-constructed view + bool is_discarded() const noexcept + { + return type() == value_t::discarded; + } + + /// the name of the type, as basic_json::type_name() + const char* type_name() const noexcept + { + return detail::value_type_name(type()); + } + + ////////////// + // capacity // + ////////////// + + /// the number of elements (arrays, objects), 0 for null and discarded, + /// 1 otherwise, as basic_json::size() + size_type size() const noexcept + { + switch (type()) + { + case value_t::null: + case value_t::discarded: + return 0; + case value_t::array: + case value_t::object: + return m_node->len; + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + default: + return 1; + } + } + + /// as basic_json::empty() + bool empty() const noexcept + { + switch (type()) + { + case value_t::null: + case value_t::discarded: + return true; + case value_t::array: + case value_t::object: + return m_node->len == 0; + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + default: + return false; + } + } + + //////////////////// + // element access // + //////////////////// + + /// the value of the member with this key (the first one, should the key + /// occur more than once); a discarded view if there is none, or if this + /// is a discarded view (so that v["a"]["b"] is safe). Throws type_error.305 + /// if this is any other value but an object. + NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view operator[](string_view_t key) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_object())) + { + if (is_discarded()) + { + return {}; + } + detail::view::throw_type_error(detail::exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", type_name()); + } + return lookup(key); + } + + basic_json_view operator[](const char* key) const + { + return operator[](string_view_t(key)); + } + + basic_json_view operator[](const string_t& key) const + { + return operator[](string_view_t(key.data(), key.size())); + } + + /// the element at this index; a discarded view if the index is out of + /// range, or if this is a discarded view. Throws type_error.305 if this is + /// any other value but an array. + basic_json_view operator[](size_type idx) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_array())) + { + if (is_discarded()) + { + return {}; + } + detail::view::throw_type_error(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(); + } + + /// any other integer type (int, unsigned, long, std::int64_t, ...; a + /// single overload for size_type alone would be ambiguous for all of them + /// and for const char*); negative values are out of range + template < typename IntegerType, typename std::enable_if < detail::view::is_index_type::value, int >::type = 0 > + basic_json_view operator[](IntegerType idx) const + { + return operator[](detail::view::to_index(idx)); + } + + /// the value a JSON pointer refers to; a discarded view if a key is + /// missing or an index is out of range, or if this is a discarded view. + /// Other errors throw what const basic_json::operator[] throws. + basic_json_view operator[](const json_pointer& ptr) const + { + if (NLOHMANN_VIEW_UNLIKELY(is_discarded())) + { + return {}; + } + return detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::unchecked); + } + + /// the value of the member with this key (the first one, should the key + /// occur more than once). Throws type_error.304 if this is not an object, + /// and out_of_range.403 if there is no such member. + basic_json_view at(string_view_t key) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_object())) + { + detail::view::throw_type_error(detail::exception_id::at_wrong_type, "cannot use at() with ", type_name()); + } + const basic_json_view r = lookup(key); + if (NLOHMANN_VIEW_UNLIKELY(r.is_discarded())) + { + detail::view::throw_key_not_found(std::string(key.data(), key.size())); + } + return r; + } + + basic_json_view at(const char* key) const + { + return at(string_view_t(key)); + } + + basic_json_view at(const string_t& key) const + { + return at(string_view_t(key.data(), key.size())); + } + + /// the element at this index. Throws type_error.304 if this is not an + /// array, and out_of_range.401 if the index is out of range. + basic_json_view at(size_type idx) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_array())) + { + detail::view::throw_type_error(detail::exception_id::at_wrong_type, "cannot use at() with ", type_name()); + } + if (NLOHMANN_VIEW_UNLIKELY(idx >= m_node->len)) + { + detail::view::throw_array_index_out_of_range(idx); + } + return element(idx); + } + + /// any other integer type, see operator[]; negative values are out of + /// range + template < typename IntegerType, typename std::enable_if < detail::view::is_index_type::value, int >::type = 0 > + basic_json_view at(IntegerType idx) const + { + return at(detail::view::to_index(idx)); + } + + /// the value a JSON pointer refers to; throws what basic_json::at() + /// throws if it cannot be resolved + basic_json_view at(const json_pointer& ptr) const + { + return detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::checked); + } + + /// the member with this key converted to T, or the default value if there + /// is no such member (the first one, should the key occur more than + /// once). Throws type_error.306 if this is not an object. + template::value, int>::type = 0> + T value(string_view_t key, const T& default_value) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_object())) + { + detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name()); + } + const basic_json_view r = lookup(key); + return r.is_discarded() ? default_value : r.template get(); + } + + string_t value(string_view_t key, const char* default_value) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_object())) + { + detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name()); + } + // (the default is made only if there is no such member) + const basic_json_view r = lookup(key); + return r.is_discarded() ? string_t(default_value) : r.template get(); + } + + /// the value a JSON pointer refers to converted to T, or the default + /// value if the pointer cannot be resolved. Throws type_error.306 if this + /// is neither an object nor an array. + template::value, int>::type = 0> + T value(const json_pointer& ptr, const T& default_value) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_structured())) + { + detail::view::throw_type_error(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); + return r.is_discarded() ? default_value : r.template get(); + } + + string_t value(const json_pointer& ptr, const char* default_value) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_structured())) + { + detail::view::throw_type_error(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); + return r.is_discarded() ? string_t(default_value) : r.template get(); + } + + /// the first element or member value; a primitive value itself. Throws + /// invalid_iterator.214 for null, discarded views, and empty containers. + basic_json_view front() const + { + const iterator it = begin(); + if (NLOHMANN_VIEW_UNLIKELY(it == end())) + { + detail::view::throw_invalid_iterator(detail::exception_id::iterator_value_unavailable, "cannot get value"); + } + return *it; + } + + /// the last element or member value (linear in the size); a primitive + /// value itself. Throws invalid_iterator.214 for null, discarded views, + /// and empty containers. + basic_json_view back() const + { + if (is_structured() && m_node->len != 0) + { + return unedited() ? basic_json_view(m_doc, detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0)) + : basic_json_view(m_doc, navigation::value(detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0))); + } + return front(); + } + + //////////// + // lookup // + //////////// + + /// an iterator to the member with this key (the first one, should the + /// key occur more than once), or end(); end() also for non-objects + iterator find(string_view_t key) const + { + if (!is_object()) + { + return end(); + } + const node* const k = find_key(key); + return k != nullptr ? iterator(m_doc, k, true) : end(); + } + + iterator find(const char* key) const + { + return find(string_view_t(key)); + } + + iterator find(const string_t& key) const + { + return find(string_view_t(key.data(), key.size())); + } + + /// whether this is an object with a member with this key + bool contains(string_view_t key) const + { + return is_object() && find_key(key) != nullptr; + } + + bool contains(const char* key) const + { + return contains(string_view_t(key)); + } + + bool contains(const string_t& key) const + { + return contains(string_view_t(key.data(), key.size())); + } + + /// whether a JSON pointer can be resolved (never throws, as + /// basic_json::contains()) + bool contains(const json_pointer& ptr) const + { + return !detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::contains).is_discarded(); + } + + /// the number of members with this key (more than 1 if the key occurs + /// more than once); 0 if this is not an object + size_type count(string_view_t key) const + { + return is_object() ? detail::view::count_members(*m_doc, m_node, key.data(), key.size()) : 0; + } + + size_type count(const char* key) const + { + return count(string_view_t(key)); + } + + size_type count(const string_t& key) const + { + return count(string_view_t(key.data(), key.size())); + } + + /////////////// + // iteration // + /////////////// + + /// the first element or member value, in document order; a primitive + /// value is a range of one element (itself), null an empty range + NLOHMANN_VIEW_ALWAYS_INLINE iterator begin() const noexcept + { + if (NLOHMANN_VIEW_LIKELY(is_structured())) + { + return iterator(m_doc, navigation::first(*m_doc, m_node), is_object()); + } + return iterator(m_doc, m_node, false); + } + + NLOHMANN_VIEW_ALWAYS_INLINE iterator end() const noexcept + { + if (NLOHMANN_VIEW_LIKELY(is_structured())) + { + return iterator(m_doc, navigation::end(*m_doc, m_node), is_object()); + } + return iterator(m_doc, (is_null() || is_discarded()) ? m_node : m_node + 1, false); + } + + iterator cbegin() const noexcept + { + return begin(); + } + + iterator cend() const noexcept + { + return end(); + } + + /// (key, value) items; the key of an array element is its index + detail::view::view_items items() const noexcept + { + return detail::view::view_items(*this); + } + + //////////////// + // conversion // + //////////////// + + /// the value converted to T, as BasicJsonType::get(): arithmetic types, + /// strings (string_view_t without a copy), std::nullptr_t, std::vector, + /// maps with string keys, and views are converted directly; other types + /// through materialize().get() + template + NLOHMANN_VIEW_ALWAYS_INLINE T get() const + { + return get_impl(detail::view::value_tag {}, detail::priority_tag<2> {}); + } + + /// as BasicJsonType::get_to(): the types get() converts directly are + /// assigned, all others are passed to from_json with v as it is + template + T& get_to(T& v) const + { + get_to_impl(v, detail::view::is_direct_value {}); + return v; + } + + /// the string, without a copy; valid as long as the view is. Throws + /// type_error.302 for other types. + string_view_t get_string() const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_string())) + { + detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be string, but is ", type_name()); + } + return {m_doc->str(*m_node), m_node->len}; + } + + /// the text of a number as it appears in the source (e.g. "1.50", "1E2", + /// or an integer with more digits than any number type holds). Throws + /// type_error.302 for other types. + string_view_t number_token() const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_number())) + { + detail::view::throw_type_error(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)}; + } + + /////////////////// + // serialization // + /////////////////// + + /// how dump() writes numbers + enum class number_format + { + /// as basic_json::dump(): integers canonically, floats with the + /// library's shortest round-trip digits ("1.5", "100.0", "1e+100") + shortest, + /// the number text of the source as it is ("1.50", "1E2", "-0", all + /// digits of a long integer) + source, + }; + + /// the text of this value; with number_format::shortest, the output of + /// ordered_json::parse(text).dump() with the same arguments (members in + /// document order, all of them should a key occur more than once) + string_t dump(const int indent = -1, const char indent_char = ' ', const bool ensure_ascii = false, + const number_format numbers = number_format::shortest) const + { + string_t out; + if (m_node == nullptr) + { + out = ""; // as basic_json::dump() of a discarded value + return out; + } + detail::view::dump_style style; + style.pretty = indent >= 0; + style.indent = indent >= 0 ? static_cast(indent) : 0; + style.indent_char = indent_char; + style.ensure_ascii = ensure_ascii; + style.source_numbers = numbers == number_format::source; + // the compact text is about as long as the source text of the value; + // the compact writer keeps 64 bytes of slack, so that it does not grow + // the buffer just before the end + const std::size_t estimate = source_extent() + (style.pretty ? source_extent() / 2 : 0) + 160; + if (unedited()) + { + detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + } + else + { + detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + } + return out; + } + +#ifndef JSON_NO_IO + /// as operator<< of basic_json: a stream width > 0 is the indentation, + /// the fill character the indentation character + friend std::ostream& operator<<(std::ostream& o, const basic_json_view& v) + { + const bool pretty = o.width() > 0; + const auto indentation = pretty ? o.width() : 0; + o.width(0); + const string_t s = v.dump(pretty ? static_cast(indentation) : -1, o.fill()); + return o.write(s.data(), static_cast(s.size())); + } +#endif + + //////////////// + // comparison // + //////////////// + + /// whether the values parse() would produce for two views are equal, as + /// by BasicJsonType's operator== (numbers by value, objects by their + /// members with duplicate keys resolved as parse() resolves them) + friend bool operator==(const basic_json_view& a, const basic_json_view& b) + { + return detail::view::equal(side(a), side(b)); + } + + friend bool operator!=(const basic_json_view& a, const basic_json_view& b) + { + return !(a == b); + } + + /// a view of an editable document compares with one of a read-only document + template < bool E, typename std::enable_if < E != Editable, int >::type = 0 > + friend bool operator==(const basic_json_view& a, const basic_json_view& b) + { + return detail::view::equal(side(a), detail::view::view_side>(b)); + } + + template < bool E, typename std::enable_if < E != Editable, int >::type = 0 > + friend bool operator!=(const basic_json_view& a, const basic_json_view& b) + { + return !(a == b); + } + + /// whether the value parse() would produce for a view equals a value + friend bool operator==(const basic_json_view& a, const BasicJsonType& j) + { + return detail::view::equal(side(a), json_side_t(j)); + } + + friend bool operator==(const BasicJsonType& j, const basic_json_view& a) + { + return a == j; + } + + friend bool operator!=(const basic_json_view& a, const BasicJsonType& j) + { + return !(a == j); + } + + friend bool operator!=(const BasicJsonType& j, const basic_json_view& a) + { + return !(a == j); + } + + ///////////////// + // materialize // + ///////////////// + + /// the basic_json value of this subtree, as parse() would produce it + /// (a discarded value for a discarded view) + BasicJsonType materialize() const + { + if (m_node == nullptr) + { + return BasicJsonType(value_t::discarded); + } + return unedited() ? detail::view::materialize(*m_doc, m_node) : detail::view::materialize(*m_doc, m_node); + } + + /// byte offset of this value in the source text (for strings: of the + /// first byte after the opening quote); static_cast(-1) for + /// a discarded view and for strings with escapes, which are decoded + std::size_t source_offset() const noexcept + { + return m_node != nullptr && (m_node->flags & (detail::view::node_flags::storage | detail::view::node_flags::moved | detail::view::node_flags::is_new)) == 0 + ? m_node->off : static_cast(-1); + } + + private: + template friend class basic_json_document; + template friend class basic_json_view; + template friend class detail::view::editor; + friend iterator; + + basic_json_view(const document_data* d, const node* n) noexcept + : m_doc(d), m_node(n) + {} + + using json_side_t = detail::view::json_side; + + static detail::view::view_side side(const basic_json_view& v) noexcept + { + return detail::view::view_side(v); + } + + /// the template argument as a runtime condition: testing it directly in + /// `Editable && ...` makes the condition constant, which MSVC reports as + /// C4127 + static bool editable() noexcept + { + return Editable; + } + + /// the number of source bytes of this value (estimated for values with + /// decoded strings); the estimate sizes the output buffer of dump() + std::size_t source_extent() const noexcept + { + if (editable() && m_doc->edits != nullptr) + { + // positions of moved and new values are not source offsets; the root + // (the first node) spans the source text and every edit + const bool is_root = m_node == m_doc->nodes; + return is_root ? m_doc->size + m_doc->edits->text_used : 64; + } + const node* const end = m_doc->nodes + m_doc->node_count; + if ((m_node->flags & detail::view::node_flags::storage) != 0) + { + return m_node->len; + } + // the value ends where the next node in the source begins; nodes + // with decoded strings (their offset is in the arena) are skipped, + // but only a few of them, to keep the walk short + const node* next = document_data::after(m_node); + for (int skipped = 0; next != end && skipped < 16; ++skipped, ++next) + { + if ((next->flags & detail::view::node_flags::storage) == 0) + { + return next->off >= m_node->off ? next->off - m_node->off : 0; + } + } + if (next == end) + { + return m_doc->size - m_node->off; + } + // the end is unknown: assume a few bytes per node, the output buffer + // grows should the value be larger + const auto nodes = static_cast(document_data::after(m_node) - m_node); + return (std::min)(m_doc->size - m_node->off, static_cast(1024) + (nodes * 16)); + } + + /// the value of the first member with this key, or a discarded view + /// (object required) + NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view lookup(string_view_t key) const noexcept + { + if (unedited()) + { + const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + return k != nullptr ? basic_json_view(m_doc, k + 1) : basic_json_view(); + } + const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + return k != nullptr ? basic_json_view(m_doc, navigation::value(k + 1)) : basic_json_view(); + } + + /// whether the document has not been edited yet: until the first edit, its index is walked like that of a + /// read-only document (nothing is moved or linked), which is cheaper + NLOHMANN_VIEW_ALWAYS_INLINE bool unedited() const noexcept + { + return !navigation::editable() || m_doc->edits == nullptr; + } + + /// the member of this object with @a key (the last one if the key occurs more than once), or nullptr + NLOHMANN_VIEW_ALWAYS_INLINE const node* find_key(string_view_t key) const + { + return find_key(key, std::integral_constant {}); + } + + NLOHMANN_VIEW_ALWAYS_INLINE const node* find_key(string_view_t key, std::true_type /*editable*/) const + { + // (an editable document without edits is looked up as one that cannot be edited) + return unedited() ? detail::view::find_member(*m_doc, m_node, key.data(), key.size()) + : detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + } + + NLOHMANN_VIEW_ALWAYS_INLINE const node* find_key(string_view_t key, std::false_type /*editable*/) const + { + return detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + } + + /// the element of an array at an index below its size + NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view element(size_type idx) const + { + if (unedited()) + { + return basic_json_view(m_doc, detail::view::element_at(*m_doc, m_node, idx)); + } + return basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))); + } + + // --- get() dispatch --- + + bool get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_boolean())) + { + detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be boolean, but is ", type_name()); + } + return (m_node->flags & detail::view::node_flags::is_true) != 0; + } + + template::value, int>::type = 0> + NLOHMANN_VIEW_ALWAYS_INLINE T get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const + { + 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()); + } + return detail::view::arithmetic_value(*m_doc, *m_node); + } + + std::nullptr_t get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_null())) + { + detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be null, but is ", type_name()); + } + return nullptr; + } + + string_view_t get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const + { + return get_string(); + } + + template + std::basic_string get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const + { + const string_view_t s = get_string(); + return std::basic_string(s.data(), s.size()); + } + + BasicJsonType get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const + { + return materialize(); + } + + basic_json_view get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const noexcept + { + return *this; + } + + template + std::vector get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const + { + return detail::view::vector_value(*this); + } + + template::value, int>::type = 0> + std::map get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const + { + return detail::view::map_value>(*this); + } + + template::value, int>::type = 0> + std::unordered_map get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const + { + return detail::view::map_value>(*this); + } + + /// everything else through the BasicJsonType value (from_json included) + template + T get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<0> /*unused*/) const + { + return materialize().template get(); + } + + // --- get_to() dispatch --- + + template + void get_to_impl(T& v, std::true_type /*direct*/) const + { + v = get(); + } + + /// from_json may keep parts of v (members a JSON object lacks, say) + template + void get_to_impl(T& v, std::false_type /*direct*/) const + { + materialize().get_to(v); + } + + const document_data* m_doc = nullptr; + const node* m_node = nullptr; +}; + +/*! +@brief a parsed JSON text: owns the node index (and, optionally, the text) + +Borrowed parses keep a pointer to the caller's text, which must outlive the +document. Owned parses (parse_copy, rvalue std::string, streams, and inputs +that are not contiguous byte ranges) keep their own copy. +*/ +template +class basic_json_document +{ + using document_data = detail::view::document_data; + + static_assert(sizeof(typename BasicJsonType::number_integer_t) == 8 && sizeof(typename BasicJsonType::number_unsigned_t) == 8, + "json_view supports 64-bit integer types only"); + + public: + using view_type = basic_json_view; + using value_t = detail::value_t; + + /// an empty (discarded) document + basic_json_document() = default; + basic_json_document(basic_json_document&&) noexcept = default; + basic_json_document& operator=(basic_json_document&&) noexcept = default; + basic_json_document(const basic_json_document&) = delete; + basic_json_document& operator=(const basic_json_document&) = delete; + ~basic_json_document() = default; + + ///////////// + // parsing // + ///////////// + + /// parse a JSON text; contiguous byte inputs are borrowed, everything else + /// (and rvalue std::string) is owned + template + NLOHMANN_VIEW_NODISCARD + static basic_json_document parse(InputType&& input, + const bool allow_exceptions = true, + const bool ignore_comments = false, + const bool ignore_trailing_commas = false) + { + basic_json_document d; + d.read(std::forward(input), allow_exceptions, ignore_comments, ignore_trailing_commas); + return d; + } + + /// parse(ptr, len) does not compile: len would convert to allow_exceptions + /// and ptr be read as a C string (as for the overloads of parse_copy, + /// accept, and read below) + template + static typename std::enable_if::value, basic_json_document>::type + parse(InputType&& input, IntegerType value, Flags&&... flags) = delete; + + /// parse [first, last) + template::iterator_category>::value, int>::type = 0> + NLOHMANN_VIEW_NODISCARD + static basic_json_document parse(IteratorType first, IteratorType last, + const bool allow_exceptions = true, + const bool ignore_comments = false, + const bool ignore_trailing_commas = false) + { + basic_json_document d; + d.read_range(first, last, allow_exceptions, ignore_comments, ignore_trailing_commas); + return d; + } + + /// parse a copy of the input; the document does not depend on it afterwards + template + NLOHMANN_VIEW_NODISCARD + static basic_json_document parse_copy(InputType&& input, + const bool allow_exceptions = true, + const bool ignore_comments = false, + const bool ignore_trailing_commas = false) + { + basic_json_document d; + std::string buf; + append_input(buf, std::forward(input)); + d.build_owned(std::move(buf), allow_exceptions, ignore_comments, ignore_trailing_commas); + return d; + } + + template + static typename std::enable_if::value, basic_json_document>::type + parse_copy(InputType&& input, IntegerType value, Flags&&... flags) = delete; + + /// check whether the input is valid JSON (the result of basic_json::accept) + template + static bool accept(InputType&& input, const bool ignore_comments = false, const bool ignore_trailing_commas = false) + { + basic_json_document d; + d.read(std::forward(input), false, ignore_comments, ignore_trailing_commas); + return !d.is_discarded(); + } + + template + static typename std::enable_if::value, bool>::type + accept(InputType&& input, IntegerType value, Flags&&... flags) = delete; + + /// parse into this document, reusing its memory + template + // flawfinder: ignore (a member function, not POSIX read()) + void read(InputType&& input, + const bool allow_exceptions = true, + const bool ignore_comments = false, + const bool ignore_trailing_commas = false) + { + read_kind(std::forward(input), allow_exceptions, ignore_comments, ignore_trailing_commas, + std::integral_constant::value> {}); + } + + template + typename std::enable_if::value, void>::type + // flawfinder: ignore (a member function, not POSIX read()) + read(InputType&& input, IntegerType value, Flags&&... flags) = delete; + + //////////// + // access // + //////////// + + /// the root value (discarded if parsing failed without exceptions) + view_type root() const& noexcept + { + if (!m_data || m_data->discarded) + { + return view_type(); + } + return view_type(m_data.get(), m_data->nodes); + } + + /// deleted: the view of a temporary document would dangle + view_type root() const&& = delete; + + bool is_discarded() const noexcept + { + return !m_data || m_data->discarded; + } + + /// the parsed text + typename view_type::string_view_t source() const noexcept + { + return m_data ? typename view_type::string_view_t(m_data->src, m_data->size) : typename view_type::string_view_t(); + } + + /// whether the document holds its own copy of the text + bool owns_source() const noexcept + { + return m_data && ((!m_data->owned.empty() && m_data->src == m_data->owned.data()) || !m_data->owned_image.empty()); + } + + /// number of index nodes (values plus object keys) + std::size_t node_count() const noexcept + { + return m_data ? m_data->node_count : 0; + } + + /// bytes held by the document (index, decoded strings, and owned text or image) + std::size_t memory_usage() const noexcept + { + if (!m_data) + { + return 0; + } + return sizeof(document_data) + (m_data->inline_capacity * sizeof(detail::view::node)) + + (m_data->nodes != m_data->inline_nodes ? m_data->node_capacity * sizeof(detail::view::node) : 0) + + m_data->arena.capacity() + m_data->owned.capacity() + m_data->owned_image.capacity() + + (m_data->indexes.capacity() * sizeof(document_data::object_index)) + (m_data->index_slots.capacity() * sizeof(std::uint32_t)) + + (m_data->large_objects.capacity() * sizeof(std::uint32_t)) + + (m_data->edits != nullptr ? m_data->edits->bytes : 0); + } + + /// release unused capacity of the index and the decoded strings; like + /// std::vector::shrink_to_fit, this invalidates the views of the document + /// (take new ones from root()) + void shrink_to_fit() + { + if (!m_data) + { + return; + } + using detail::view::node; + document_data& d = *m_data; + + // allocate everything first, so that an exception leaves the document + // unchanged + // (the decoded strings of a loaded image stay in the image) + const bool arena_in_use = d.base[1] == d.arena.data(); + const bool shrink_arena = d.arena.capacity() > d.arena.size(); + std::vector arena(shrink_arena && arena_in_use ? d.arena : std::vector()); + // (edits link to the nodes of the index, which then stays in place) + const bool shrink_nodes = d.nodes != d.inline_nodes && d.node_count != d.node_capacity && d.edits == nullptr; + const bool into_header = d.node_count <= d.inline_capacity; + std::vector indexes(d.indexes.capacity() > d.indexes.size() ? d.indexes : std::vector()); + std::vector index_slots(d.index_slots.capacity() > d.index_slots.size() ? d.index_slots : std::vector()); + node* fresh = (shrink_nodes && !into_header) ? static_cast(::operator new (d.node_count * sizeof(node))) : d.inline_nodes; + + if (shrink_nodes) + { + if (d.node_count != 0) + { + std::memcpy(fresh, d.nodes, d.node_count * sizeof(node)); + } + ::operator delete (d.nodes); + d.nodes = fresh; + d.node_capacity = into_header ? d.inline_capacity : d.node_count; + } + if (shrink_arena) + { + d.arena.swap(arena); + if (arena_in_use) + { + d.base[1] = d.arena.data(); + } + } + if (d.indexes.capacity() > d.indexes.size()) + { + d.indexes.swap(indexes); + } + if (d.index_slots.capacity() > d.index_slots.size()) + { + d.index_slots.swap(index_slots); + } + } + + //////////// + // images // + //////////// + + /// how load() checks an image (full, bounds, or none) + using image_check = detail::view::image_check; + + /// The document as an image that load() reads without parsing: the node + /// index, the text, and the decoded strings. An edited document is + /// written in its current state (floats that are not finite become null, + /// as in dump()). + std::vector save() const + { + if (NLOHMANN_VIEW_UNLIKELY(!m_data || m_data->discarded)) + { + detail::view::throw_type_error(detail::exception_id::document_image_unsupported, "cannot save a discarded json_document"); + } + return detail::view::save_image(*m_data); + } + + /// Read an image written by save(). The image is borrowed: it must stay + /// alive and unchanged while the document is used. + NLOHMANN_VIEW_NODISCARD + static basic_json_document load(const std::uint8_t* image, std::size_t size, const image_check check = image_check::full) + { + basic_json_document d; + d.ensure_data(nullptr, 0); + detail::view::load_image(*d.m_data, image, size, check); + return d; + } + + /// read an image (borrowed) + NLOHMANN_VIEW_NODISCARD + static basic_json_document load(const std::vector& image, const image_check check = image_check::full) + { + return load(image.data(), image.size(), check); + } + + /// read an image and keep it (no copy) + NLOHMANN_VIEW_NODISCARD + static basic_json_document load(std::vector&& image, const image_check check = image_check::full) + { + basic_json_document d; + d.ensure_data(nullptr, 0); + d.m_data->owned_image = std::move(image); + detail::view::load_image(*d.m_data, d.m_data->owned_image.data(), d.m_data->owned_image.size(), check); + return d; + } + + /////////// + // edits // + /////////// + + // The source text is never written; new values go to storage owned by + // the document. A view keeps referring to the same value: after an + // assignment it sees the new value, and edits elsewhere do not affect it. + // A view of an erased value keeps its last value. An edit of an + // array/object invalidates the iterators over it. Values are accepted as + // views (of any document), BasicJsonType values, and everything + // BasicJsonType can be constructed from. + + using string_view_t = typename view_type::string_view_t; + using json_pointer = typename BasicJsonType::json_pointer; + + /// replace a value (a view of this document); returns a view of it + template + view_type set(view_type target, V&& value) + { + return editor().set(target, std::forward(value)); + } + + /// set a member (added if missing; a null value becomes an object); + /// returns a view of the member value + template + view_type set(view_type object, string_view_t key, V&& value) + { + return editor().set(object, key, std::forward(value)); + } + + /// assign an existing array element; returns a view of it + template::value, int>::type = 0> + view_type set(view_type array, I idx, V && value) + { + return editor().set(array, index(idx), std::forward(value)); + } + + /// set the value at a JSON pointer: its parent must exist; an object + /// member is set (added if missing), an array element assigned, and "-" + /// or the size of the array appends. A null parent becomes what + /// basic_json's operator[](json_pointer) makes of it: an array for "-" + /// and for digits (padded with nulls up to the index), an object otherwise. + template + view_type set(const json_pointer& ptr, V&& value) + { + if (ptr.empty()) + { + return set(root(), std::forward(value)); + } + const view_type parent = root().at(ptr.parent_pointer()); + const auto& token = ptr.back(); + if (parent.is_null()) + { + if (token == "-") + { + return push_back(parent, std::forward(value)); + } + const bool digits = std::all_of(token.begin(), token.end(), [](const char c) + { + return c >= '0' && c <= '9'; + }); + if (digits) + { + // (an invalid index is an error before the parent changes) + const std::size_t idx = pointer_index(token); + if (idx >= 0xFFFFFFFFu) + { + detail::view::throw_array_index_out_of_range(idx); + } + for (std::size_t i = 0; i < idx; ++i) + { + push_back(parent, nullptr); + } + return push_back(parent, std::forward(value)); + } + } + if (parent.is_array()) + { + const std::size_t idx = token == "-" ? parent.size() : pointer_index(token); + if (idx == parent.size()) + { + return push_back(parent, std::forward(value)); + } + return set(parent, idx, std::forward(value)); + } + return set(parent, string_view_t(token.data(), token.size()), std::forward(value)); + } + + /// append to an array (a null value becomes an array); returns a view of + /// the new element + template + view_type push_back(view_type array, V&& value) + { + return editor().push_back(array, std::forward(value)); + } + + /// insert into an array before position idx (idx <= size()); returns a + /// view of the new element + template::value, int>::type = 0> + view_type insert(view_type array, I idx, V && value) + { + return editor().insert(array, index(idx), std::forward(value)); + } + + /// remove all members with this key; returns their number + std::size_t erase(view_type object, string_view_t key) + { + return editor().erase(object, key); + } + + /// remove an array element + template::value, int>::type = 0> + void erase(view_type array, I idx) + { + editor().erase(array, index(idx)); + } + + /// remove the value at a JSON pointer; returns the number of removed + /// values + std::size_t erase(const json_pointer& ptr) + { + if (ptr.empty()) + { + detail::view::throw_out_of_range(detail::exception_id::patch_on_root, "JSON pointer has no parent"); + } + const view_type parent = root().at(ptr.parent_pointer()); + const auto& token = ptr.back(); + if (parent.is_array()) + { + erase(parent, pointer_index(token)); + return 1; + } + return erase(parent, string_view_t(token.data(), token.size())); + } + + private: + using input_kind = detail::view::input_kind; + + detail::view::editor editor() + { + static_assert(Editable, "only an editable document can be edited: use basic_json_document (json_editable_document)"); + if (NLOHMANN_VIEW_UNLIKELY(!m_data || m_data->discarded)) + { + detail::view::throw_invalid_iterator(detail::exception_id::iterator_from_other_value, "view does not belong to this document"); + } + return detail::view::editor(*m_data); + } + + /// an index (out_of_range.401 if negative) + template + static std::size_t index(I idx) + { + return index(idx, std::is_signed {}); + } + + template + static std::size_t index(I idx, std::true_type /*signed*/) + { + if (idx < 0) + { + detail::view::throw_array_index_out_of_range(idx); + } + return static_cast(idx); + } + + template + static std::size_t index(I idx, std::false_type /*unsigned*/) + { + return static_cast(idx); + } + + /// the array index of a JSON pointer token (json_pointer's rules) + template + static std::size_t pointer_index(const StringType& token) + { + std::size_t idx = 0; + const detail::view::index_status status = detail::view::array_index(token, idx); + if (status != detail::view::index_status::ok) + { + detail::view::throw_array_index_error(status, token); + } + return idx; + } + + /// create the storage (sized for the input) on first use + void ensure_data(const char* src, std::size_t size) + { + if (!m_data) + { + m_data.reset(document_data::create(detail::view::estimate_nodes(src, size))); + } + } + + /// parse a buffer the document takes ownership of (it replaces the + /// previous owned copy) + void build_owned(std::string&& buf, bool allow_exceptions, bool comments, bool trailing_commas) + { + ensure_data(buf.data(), buf.size()); + m_data->owned = std::move(buf); + build(m_data->owned.data(), m_data->owned.size(), allow_exceptions, comments, trailing_commas, true, true); + } + + /// parse a copy of the input; the copy is appended to the cleared owned + /// buffer, so that its capacity is reused + template + void build_copy(T&& input, bool allow_exceptions, bool comments, bool trailing_commas) + { + if (!m_data) + { + std::string buf; + append_input(buf, std::forward(input)); + build_owned(std::move(buf), allow_exceptions, comments, trailing_commas); + return; + } + document_data& d = *m_data; + // (the previous text is gone from here on: should reading the input + // throw, the document is discarded) + d.discarded = true; + d.src = ""; + d.size = 0; + d.owned.clear(); + append_input(d.owned, std::forward(input)); + build(d.owned.data(), d.owned.size(), allow_exceptions, comments, trailing_commas, true, true); + } + + /// 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) + { + ensure_data(src, size); + document_data& d = *m_data; + if (!owned) + { + d.owned.clear(); + } + d.owned_image.clear(); + d.src = src; + d.size = size; + d.node_count = 0; + d.edits.reset(); // (views of the previous text end here anyway) + d.base[2] = nullptr; + d.arena.clear(); + d.indexes.clear(); + d.index_slots.clear(); + d.large_objects.clear(); + d.discarded = true; + detail::view::parse_failure failure; + bool ok = false; + if (NLOHMANN_VIEW_UNLIKELY(size > detail::view::max_input_size())) + { + failure.code = detail::view::error_code::input_too_large; + } + else + { + ok = detail::view::build < typename BasicJsonType::number_float_t, !detail::abi_config::strict_nul_handling > (d, src, size, comments, trailing_commas, sentinel, failure); + } + if (NLOHMANN_VIEW_LIKELY(ok)) + { + d.base[0] = d.src; + d.base[1] = d.arena.data(); + d.arena_size = d.arena.size(); + detail::view::build_object_indexes(d); + std::vector().swap(d.large_objects); // (only needed while parsing) + d.discarded = false; + return; + } + std::vector().swap(d.large_objects); + if (allow_exceptions) + { + detail::view::throw_parse_failure(failure, src, size, comments, trailing_commas); + } + } + + // --- input dispatch (see detail::view::input_kind) --- + + void read_kind(std::string&& s, bool ae, bool c, bool tc, std::integral_constant /*unused*/) + { + build_owned(std::move(s), ae, c, tc); + } + + template + void read_kind(CharT* s, bool ae, bool c, bool tc, std::integral_constant /*unused*/) + { + if (s == nullptr) + { + build("", 0, ae, c, tc, false, true); + return; + } + const char* cs = reinterpret_cast(s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + // C strings are null-terminated, as for json::parse(const char*) + // flawfinder: ignore + build(cs, std::strlen(cs), ae, c, tc, false, true); + } + + template + void read_kind(Array& a, bool ae, bool c, bool tc, std::integral_constant /*unused*/) + { + using CharT = typename std::remove_cv::type>::type; + const std::size_t n = std::extent::value; + // a trailing NUL (string literals) is not part of the text, as for + // parse(), and serves as sentinel + const bool terminated = n > 0 && a[n - 1] == 0 && (!detail::abi_config::strict_nul_handling || std::is_same::value); + build(reinterpret_cast(&a[0]), terminated ? n - 1 : n, ae, c, tc, false, terminated); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + } + + template + void read_kind(const T& s, bool ae, bool c, bool tc, std::integral_constant /*unused*/) + { + // std::basic_string guarantees data()[size()] == 0: use it as sentinel + const auto size = static_cast(s.size()); + build(size == 0 ? "" : reinterpret_cast(s.data()), size, ae, c, tc, false, // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + detail::view::is_std_string::value || size == 0); + } + + template + void read_kind(const T& s, bool ae, bool c, bool tc, std::integral_constant /*unused*/) + { + build_copy(s, ae, c, tc); + } + + template + void read_kind(CharT* s, bool ae, bool c, bool tc, std::integral_constant /*unused*/) + { + if (s == nullptr) + { + build("", 0, ae, c, tc, false, true); + return; + } + // NUL-terminated, as for the single-byte C strings + build_copy(detail::view::make_adapter_input(detail::input_adapter(s, s + std::char_traits::type>::length(s))), ae, c, tc); + } + + template + void read_kind(T&& input, bool ae, bool c, bool tc, std::integral_constant /*unused*/) + { + build_copy(std::forward(input), ae, c, tc); + } + + template + void read_range(IteratorType first, IteratorType last, bool ae, bool c, bool tc) + { + using value_type = typename std::remove_cv::value_type>::type; + // borrow the range where the library's input adapter scans it as one + // block: pointers and, in C++20, contiguous iterators (std::vector, + // std::string, ...) over single bytes + read_range_impl(first, last, ae, c, tc, std::integral_constant < bool, std::is_integral::value + && detail::iterator_input_adapter::supports_bulk_scan > {}); + } + + template + void read_range_impl(IteratorType first, IteratorType last, bool ae, bool c, bool tc, std::true_type /*contiguous bytes*/) + { + const auto size = static_cast(std::distance(first, last)); + build(size == 0 ? "" : reinterpret_cast(&*first), size, ae, c, tc, false, size == 0); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + } + + template + void read_range_impl(IteratorType first, IteratorType last, bool ae, bool c, bool tc, std::false_type /*other*/) + { + build_copy(detail::view::make_adapter_input(detail::input_adapter(first, last)), ae, c, tc); + } + + /// append the text of an input to buf: a contiguous byte input as one + /// block, a stream in blocks, an input adapter (made here for any other + /// input) character by character + template + static void append_input(std::string& buf, T&& input) + { + using D = typename std::decay::type; + // NOLINTBEGIN(readability-avoid-nested-conditional-operator): a constant expression of C++11 + append_input_impl(buf, std::forward(input), std::integral_constant < int, detail::is_contiguous_byte_container::value ? 0 + : std::is_base_of::value ? 1 + : detail::view::is_adapter_input::value ? 2 + : detail::view::is_c_stream::value ? 4 : 3 > {}); + // NOLINTEND(readability-avoid-nested-conditional-operator) + } + + template + static void append_input_impl(std::string& buf, const T& s, std::integral_constant /*contiguous bytes*/) + { + buf.append(reinterpret_cast(s.data()), static_cast(s.size())); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + } + + template + static void append_input_impl(std::string& buf, Stream&& is, std::integral_constant /*stream*/) // NOLINT(cppcoreguidelines-missing-std-forward): read in place, as by the library's stream adapter + { + detail::view::append_stream(buf, is); + } + + template + static void append_input_impl(std::string& buf, detail::view::adapter_input&& input, std::integral_constant /*input adapter*/) + { + detail::view::append_adapter(buf, std::move(input.ia)); + } + +#ifndef JSON_NO_IO + static void append_input_impl(std::string& buf, std::FILE* file, std::integral_constant /*C stream*/) + { + if (file == nullptr) + { + // (as before: the library's adapter decides what a null stream yields) + detail::view::append_adapter(buf, detail::input_adapter(file)); + return; + } + detail::view::append_file(buf, file); + } +#endif + + template + static void append_input_impl(std::string& buf, T&& input, std::integral_constant /*other*/) + { + detail::view::append_adapter(buf, detail::input_adapter(std::forward(input))); + } + + std::unique_ptr m_data{}; // NOLINT(readability-redundant-member-init) +}; + +/// a parsed JSON text for json +using json_document = basic_json_document; +/// a value of a json_document +using json_view = basic_json_view; +/// a parsed JSON text for ordered_json +using ordered_json_document = basic_json_document; +/// a value of an ordered_json_document +using ordered_json_view = basic_json_view; +/// an editable parsed JSON text for json +using json_editable_document = basic_json_document; +/// a value of a json_editable_document +using json_editable_view = basic_json_view; +/// an editable parsed JSON text for ordered_json +using ordered_json_editable_document = basic_json_document; +/// a value of an ordered_json_editable_document +using ordered_json_editable_view = basic_json_view; + +NLOHMANN_JSON_NAMESPACE_END + +// tuple protocol for the items of basic_json_view::items() (structured bindings) +namespace std // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) +{ + +#if defined(__clang__) + // Fix: https://github.com/nlohmann/json/issues/1401 + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wmismatched-tags" +#endif +template +class tuple_size<::nlohmann::detail::view::view_item> // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) + : public std::integral_constant {}; + +template +class tuple_element> // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) +{ + public: + using type = decltype(std::declval<::nlohmann::detail::view::view_item>().template get()); +}; +#if defined(__clang__) + #pragma clang diagnostic pop +#endif + +} // namespace std + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// undefine the macros of detail/view/macro_scope.hpp (at the end of json_view.hpp) + +#undef NLOHMANN_VIEW_HAS_CPP_17 +#undef NLOHMANN_VIEW_LIKELY +#undef NLOHMANN_VIEW_UNLIKELY +#undef NLOHMANN_VIEW_ALWAYS_INLINE +#undef NLOHMANN_VIEW_NOINLINE +#undef NLOHMANN_VIEW_NODISCARD +#undef NLOHMANN_VIEW_THROW +#undef NLOHMANN_VIEW_LITTLE_ENDIAN +#undef NLOHMANN_VIEW_REPEAT16 +#undef NLOHMANN_VIEW_REPEAT_SECOND_8 +#undef NLOHMANN_VIEW_REPEAT_FIRST_8 +#undef NLOHMANN_VIEW_NEON +#undef NLOHMANN_VIEW_SSE2 +#undef NLOHMANN_VIEW_SSSE3 +#undef NLOHMANN_VIEW_SSSE3_DISPATCH +#undef NLOHMANN_VIEW_SSSE3_TARGET +#undef NLOHMANN_VIEW_VECTOR +#undef NLOHMANN_VIEW_VECTOR_UTF8 +// IWYU pragma: keep + +#endif // INCLUDE_NLOHMANN_JSON_VIEW_HPP_ diff --git a/src/modules/json.cppm b/src/modules/json.cppm index a32b97603..3917e0722 100644 --- a/src/modules/json.cppm +++ b/src/modules/json.cppm @@ -17,8 +17,15 @@ module; // 3. Or upgrade to a newer GCC version with better modules support. // See: https://github.com/nlohmann/json/issues/5103 +// GCC ignores the target attribute in modules ("'target' attribute currently +// unsupported in modules") and then rejects the SSSE3 intrinsics of json_view. +#if defined(__GNUC__) && !defined(__clang__) + #define NLOHMANN_VIEW_NO_TARGET_ATTRIBUTE +#endif + #include #include +#include export module nlohmann.json; @@ -27,9 +34,15 @@ NLOHMANN_JSON_NAMESPACE_BEGIN using NLOHMANN_JSON_NAMESPACE::adl_serializer; using NLOHMANN_JSON_NAMESPACE::basic_json; +using NLOHMANN_JSON_NAMESPACE::basic_json_document; +using NLOHMANN_JSON_NAMESPACE::basic_json_view; using NLOHMANN_JSON_NAMESPACE::json; +using NLOHMANN_JSON_NAMESPACE::json_document; using NLOHMANN_JSON_NAMESPACE::json_pointer; +using NLOHMANN_JSON_NAMESPACE::json_view; using NLOHMANN_JSON_NAMESPACE::ordered_json; +using NLOHMANN_JSON_NAMESPACE::ordered_json_document; +using NLOHMANN_JSON_NAMESPACE::ordered_json_view; using NLOHMANN_JSON_NAMESPACE::ordered_map; using NLOHMANN_JSON_NAMESPACE::to_string; @@ -47,6 +60,7 @@ inline namespace json_literals namespace detail { using NLOHMANN_JSON_NAMESPACE::detail::json_sax_dom_callback_parser; + using NLOHMANN_JSON_NAMESPACE::detail::json_sax_dom_parser; using NLOHMANN_JSON_NAMESPACE::detail::unknown_size; } // namespace detail diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e092616b7..ee7e07f08 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -118,7 +118,7 @@ endif() if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") # avoid stack overflow, see https://github.com/nlohmann/json/issues/2955 - json_test_set_test_options("test-bon8;test-cbor;test-msgpack;test-ubjson;test-bjdata;test-binary_formats" LINK_OPTIONS /STACK:4000000) + json_test_set_test_options("test-bon8;test-cbor;test-msgpack;test-ubjson;test-bjdata;test-bjdata_round_trip_draft2;test-bjdata_round_trip_draft3;test-binary_formats" LINK_OPTIONS /STACK:4000000) endif() # disable exceptions for test-disabled_exceptions @@ -138,7 +138,8 @@ json_test_set_test_options(test-disabled_exceptions # only the #972 regression test needs thirdparty/fifo_map on its include path json_test_set_test_options(test-regression1 LINK_LIBRARIES fifo_map_include) -# GCC's false -Warray-bounds error with JSON_DIAGNOSTICS only shows up when optimizing (#5742). +# Regression test for GCC's false -Warray-bounds error with JSON_DIAGNOSTICS (#5742, fixed in #5585). It only +# showed up when optimizing, so build this test with -O3 and the warning as an error. # -O3 makes the optimizer-driven warnings of the ci_test_gcc flag set (-Winline, # -Wsuggest-attribute=...) fire on the library's inline functions; they are not # what this test checks, so turn them off for it. @@ -284,6 +285,10 @@ if (JSON_TestSimdutf) endif() endif() +# test-file_input checks from a second thread that parsing from a FILE* releases the file's lock +find_package(Threads REQUIRED) +json_test_set_test_options("test-file_input;test-file_input_unlocked_io_disabled" LINK_LIBRARIES Threads::Threads) + # *DO* use json_test_set_test_options() above this line json_test_should_build_32bit_test(json_32bit_test json_32bit_test_only "${JSON_32bitTest}") @@ -368,6 +373,71 @@ json_test_add_test_for(src/unit-diagnostic-positions.cpp MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force} ) +# the lexer again with the portable string scanning instead of SSE2/NEON, and on +# x86-64 with SSSE3 compiled in (no check of the CPU) +json_test_set_test_options(test-class_lexer_no_simd + COMPILE_DEFINITIONS JSON_NO_SIMD +) +json_test_add_test_for(src/unit-class_lexer.cpp + NAME test-class_lexer_no_simd + MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force} +) +if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$" AND NOT MSVC) + json_test_set_test_options(test-class_lexer_ssse3 + COMPILE_OPTIONS -mssse3 + ) + json_test_add_test_for(src/unit-class_lexer.cpp + NAME test-class_lexer_ssse3 + MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force} + ) +endif() + +# test reading from a std::istream again without scanning the get area in bulk +json_test_set_test_options(test-stream-bulk-scan_disabled + COMPILE_DEFINITIONS JSON_DISABLE_STREAM_BULK_SCAN=1 +) +json_test_add_test_for(src/unit-stream-bulk-scan.cpp + NAME test-stream-bulk-scan_disabled + MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force} +) + +# test reading from a std::istream again with JSON_PRECISE_STREAM_POSITION +json_test_set_test_options(test-stream-bulk-scan_precise + COMPILE_DEFINITIONS JSON_PRECISE_STREAM_POSITION=1 +) +json_test_add_test_for(src/unit-stream-bulk-scan.cpp + NAME test-stream-bulk-scan_precise + MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force} +) + +# test reading from a FILE* again with the locking stdio functions +json_test_set_test_options(test-file_input_unlocked_io_disabled + COMPILE_DEFINITIONS JSON_DISABLE_UNLOCKED_FILE_IO=1 +) +json_test_add_test_for(src/unit-file_input.cpp + NAME test-file_input_unlocked_io_disabled + MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force} +) + +# the json_view parser again with the portable string scanning instead of +# NEON/SSE2, and on x86-64 with the SSSE3 UTF-8 check (JSON_VIEW_USE_SSSE3) +json_test_set_test_options(test-json_view_builder_portable + COMPILE_DEFINITIONS JSON_VIEW_NO_SIMD +) +json_test_add_test_for(src/unit-json_view_builder.cpp + NAME test-json_view_builder_portable + MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force} +) +if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$" AND NOT MSVC) + json_test_set_test_options(test-json_view_builder_ssse3 + COMPILE_DEFINITIONS JSON_VIEW_USE_SSSE3 + COMPILE_OPTIONS -mssse3 + ) + json_test_add_test_for(src/unit-json_view_builder.cpp + NAME test-json_view_builder_ssse3 + MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force} + ) +endif() endif() # *DO NOT* use json_test_set_test_options() below this line diff --git a/tests/Makefile b/tests/Makefile index 6bfa14397..e03b9dc7d 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -10,12 +10,18 @@ CXXFLAGS += -std=c++11 CPPFLAGS += -I ../single_include FUZZER_ENGINE = src/fuzzer-driver_afl.cpp -FUZZERS = parse_afl_fuzzer parse_bson_fuzzer parse_cbor_fuzzer parse_msgpack_fuzzer parse_ubjson_fuzzer parse_bjdata_fuzzer parse_bon8_fuzzer +FUZZERS = parse_afl_fuzzer parse_bson_fuzzer parse_cbor_fuzzer parse_msgpack_fuzzer parse_ubjson_fuzzer parse_bjdata_fuzzer parse_bon8_fuzzer parse_json_view_fuzzer json_view_image_fuzzer fuzzers: $(FUZZERS) parse_afl_fuzzer: $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(FUZZER_ENGINE) src/fuzzer-parse_json.cpp -o $@ +parse_json_view_fuzzer: + $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(FUZZER_ENGINE) src/fuzzer-parse_json_view.cpp -o $@ + +json_view_image_fuzzer: + $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(FUZZER_ENGINE) src/fuzzer-json_view_image.cpp -o $@ + parse_bson_fuzzer: $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(FUZZER_ENGINE) src/fuzzer-parse_bson.cpp -o $@ diff --git a/tests/abi/config/CMakeLists.txt b/tests/abi/config/CMakeLists.txt index 52941dc33..930afeecc 100644 --- a/tests/abi/config/CMakeLists.txt +++ b/tests/abi/config/CMakeLists.txt @@ -21,7 +21,12 @@ foreach(test default noversion) target_compile_definitions(abi_config_${test}_all_tags PRIVATE JSON_DIAGNOSTICS=1 JSON_DIAGNOSTIC_POSITIONS=1 - JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON=1) + JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON=1 + JSON_DISABLE_STREAM_BULK_SCAN=1 + JSON_DISABLE_UNLOCKED_FILE_IO=1 + JSON_NO_SIMD + JSON_VIEW_NO_SIMD + JSON_VIEW_USE_SSSE3) target_link_libraries(abi_config_${test}_all_tags PRIVATE abi_compat_main) add_test( NAME test-abi_config_${test}_all_tags diff --git a/tests/abi/config/default.cpp b/tests/abi/config/default.cpp index d42e883f0..a9fa472d3 100644 --- a/tests/abi/config/default.cpp +++ b/tests/abi/config/default.cpp @@ -52,6 +52,26 @@ TEST_CASE("default namespace") expected += "_ekmo"; #endif +#if JSON_DISABLE_STREAM_BULK_SCAN + expected += "_dsbs"; +#endif + +#if JSON_DISABLE_UNLOCKED_FILE_IO + expected += "_dufio"; +#endif + +#ifdef JSON_NO_SIMD + expected += "_ns"; +#endif + +#ifdef JSON_VIEW_NO_SIMD + expected += "_vns"; +#endif + +#ifdef JSON_VIEW_USE_SSSE3 + expected += "_vssse3"; +#endif + expected += "_v" STRINGIZE(NLOHMANN_JSON_VERSION_MAJOR); expected += "_" STRINGIZE(NLOHMANN_JSON_VERSION_MINOR); expected += "_" STRINGIZE(NLOHMANN_JSON_VERSION_PATCH) "::basic_json"; diff --git a/tests/abi/config/noversion.cpp b/tests/abi/config/noversion.cpp index ec406c716..a090d1d02 100644 --- a/tests/abi/config/noversion.cpp +++ b/tests/abi/config/noversion.cpp @@ -53,6 +53,26 @@ TEST_CASE("default namespace without version component") expected += "_ekmo"; #endif +#if JSON_DISABLE_STREAM_BULK_SCAN + expected += "_dsbs"; +#endif + +#if JSON_DISABLE_UNLOCKED_FILE_IO + expected += "_dufio"; +#endif + +#ifdef JSON_NO_SIMD + expected += "_ns"; +#endif + +#ifdef JSON_VIEW_NO_SIMD + expected += "_vns"; +#endif + +#ifdef JSON_VIEW_USE_SSSE3 + expected += "_vssse3"; +#endif + expected += "::basic_json"; // fallback for Clang diff --git a/tests/benchmarks/CMakeLists.txt b/tests/benchmarks/CMakeLists.txt index 4d7265145..c016add48 100644 --- a/tests/benchmarks/CMakeLists.txt +++ b/tests/benchmarks/CMakeLists.txt @@ -6,8 +6,9 @@ if((CMAKE_CXX_COMPILER_ID MATCHES GNU) OR (CMAKE_CXX_COMPILER_ID MATCHES Clang)) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -flto -DNDEBUG -O3") endif() -# configure Google Benchmark; a fixed release, so that results stay comparable -set(JSON_GOOGLE_BENCHMARK_VERSION 1.9.5) +# configure Google Benchmark; a fixed release with a checksum, so that builds are reproducible. Update it like any +# other dependency; comparisons stay fair because both sides are built with the same version (see README.md) +set(JSON_GOOGLE_BENCHMARK_VERSION 1.10.0) include(FetchContent) # only the library is needed; -Werror would break the pinned release as soon as @@ -18,7 +19,7 @@ set(BENCHMARK_ENABLE_WERROR OFF CACHE BOOL "" FORCE) FetchContent_Declare(benchmark URL https://github.com/google/benchmark/archive/refs/tags/v${JSON_GOOGLE_BENCHMARK_VERSION}.tar.gz - URL_HASH SHA256=9631341c82bac4a288bef951f8b26b41f69021794184ece969f8473977eaa340 + URL_HASH SHA256=acc5c7c0fbed855c4aaa2d7c3c36fb82b0c15c28f4e151e3f679c2fc0aa404af DOWNLOAD_EXTRACT_TIMESTAMP TRUE ) FetchContent_MakeAvailable(benchmark) @@ -38,3 +39,10 @@ target_compile_features(json_benchmarks PRIVATE cxx_std_11) target_link_libraries(json_benchmarks benchmark ${CMAKE_THREAD_LIBS_INIT}) add_dependencies(json_benchmarks download_test_data) target_include_directories(json_benchmarks PRIVATE ${JSON_BENCHMARK_INCLUDE_DIR} ${CMAKE_BINARY_DIR}/include) + +# the json_document benchmarks, unless the header to benchmark predates json_view.hpp +if(EXISTS "${JSON_BENCHMARK_INCLUDE_DIR}/nlohmann/json_view.hpp") + target_sources(json_benchmarks PRIVATE src/benchmarks_view.cpp) +else() + message(STATUS "${JSON_BENCHMARK_INCLUDE_DIR} has no nlohmann/json_view.hpp: skipping the json_document benchmarks") +endif() diff --git a/tests/benchmarks/README.md b/tests/benchmarks/README.md index aaf5abe46..132bbb74a 100644 --- a/tests/benchmarks/README.md +++ b/tests/benchmarks/README.md @@ -9,6 +9,7 @@ Micro-benchmarks for parsing, serialization and the binary formats, written with | benchmark | what it does | |---|---| | `ParseFile`, `ParseString` | parse JSON from a file stream or a string | +| `Accept` | validate JSON from a string (`json::accept`) | | `ParseIndented` | parse the large files re-indented by 4 spaces, for the lexer's whitespace handling | | `Dump` | serialize, compact (`-`) and indented (`4`) | | `ToCbor`, `BinaryToCbor` | write CBOR; `BinaryToCbor` writes binary values of growing size | @@ -16,6 +17,20 @@ Micro-benchmarks for parsing, serialization and the binary formats, written with | `FromBinaryBuffer`, `FromBinaryFile` | read CBOR, MessagePack, UBJSON, BJData and BSON from a buffer or a `FILE*` | | `FromBinaryShape` | read deeply nested, container-heavy and scalar-heavy documents in every binary format | | `FromCborChunkedString` | read CBOR strings split into indefinite-length chunks | +| `ViewParse`, `ViewRead` | parse with [`json_document`](https://json.nlohmann.me/features/json_view/) into a new document, or into one that is reused; compare with `ParseString` | +| `ViewParseIndented` | as `ParseIndented`, with a reused `json_document` | +| `ViewAccept` | validate with `json_document::accept`; compare with `Accept` | +| `ViewMaterialize` | convert a parsed `json_document` into a `json` value | +| `ViewDump` | serialize a parsed `json_document`; compare with `Dump` | +| `SaxParse` | parse with a SAX handler that only counts events, so no values are built | +| `Copy`, `CopyAssign`, `CopyRing` | copy a parsed document and destroy a copy, both timed: a temporary copy, an assignment over an earlier copy, and replacing the oldest of eight live copies | +| `CopyOnly`, `DestroyOnly` | only the copy or only the destruction, the other half untimed; a fast destructor can leave work to the next allocations, so these tell the halves apart but do not compare versions | +| `Equal`, `Iterate` | compare a parsed document with a copy, or visit every value with range-based `for` loops | +| `Flatten`, `FlattenOrdered`, `Unflatten` | `flatten()` for `json` and `ordered_json`, and `unflatten()` | +| `Diff`, `Patch` | `diff()` against the document with every eighth scalar changed, and applying that patch | +| `JsonPointerParse`, `JsonPointerToString`, `JsonPointerAt`, `JsonPointerContains` | the JSON pointers of every scalar in a document: parse them from strings, turn them back into strings, look them up with `at()` and `contains()` | +| `GetVectorInt`, `GetVectorDouble`, `GetMap` | `get>()`, `get>()` and `get>()` on large arrays and a large flat object | +| `ToJsonStrings` | create a `json` array from a `std::vector` of 100,000 strings, copied or moved, and destroy it again | The input files are those of [nativejson-benchmark](https://github.com/miloyip/nativejson-benchmark) (`canada`, `citm_catalog`, `twitter`), a large `jeopardy` file, and number-heavy files (`floats`, `signed_ints`, ...). @@ -27,8 +42,11 @@ The input files are those of [nativejson-benchmark](https://github.com/miloyip/n - Network access on the first configure: CMake downloads Google Benchmark and the [test data](https://github.com/nlohmann/json_test_data) into the build directory. To reuse a download of the test data, pass `-DJSON_TestDataDirectory=/test_files`. -- Google Benchmark is pinned to a release (1.9.5), so that results from different days stay comparable. To update it, - change `JSON_GOOGLE_BENCHMARK_VERSION` and the archive's `URL_HASH` in `CMakeLists.txt` together. +- Google Benchmark is fetched as a fixed release with a checksum, so that every build uses the same + code and a download cannot change under you. It is updated like any other dependency when a new release comes out: + change `JSON_GOOGLE_BENCHMARK_VERSION` and the archive's `URL_HASH` in `CMakeLists.txt` together. Results are kept + comparable by building both sides of a comparison with the same version, not by keeping that version forever (see + [Comparing two versions](#comparing-two-versions)). - The benchmarks include `single_include/nlohmann/json.hpp`, so run `make amalgamate` after changing anything in `include/`. @@ -110,13 +128,20 @@ Python, unpinned versions work as well. In its output: - `OVERALL_GEOMEAN` summarizes all benchmarks; - `-a` shows only the aggregates, not every repetition. -The header you compare with must support everything the benchmarks use. The current benchmarks build against 3.12.0. +Both builds use the same Google Benchmark, the one `CMakeLists.txt` names, so only the header differs. For the same +reason, do not compare against result files from an earlier checkout: a different Google Benchmark, compiler or +machine state can shift the numbers as much as the change under test. Rerun the baseline instead. + +The header you compare with must support everything the benchmarks use. The current benchmarks build against 3.12.0; +the `View*` benchmarks (in `src/benchmarks_view.cpp`) are only built if the directory also holds +`nlohmann/json_view.hpp`. Only benchmarks present in both result files are compared, so for older releases, either filter the benchmarks or build that release's own `tests/benchmarks` against its own header. ## Getting stable numbers -- Build and run both versions on the same machine, one right after the other. +- Build and run both versions on the same machine with the same compiler and Google Benchmark, one right after the + other. - Keep the machine otherwise idle: no builds, no browser, and a laptop plugged in. - On Linux, set the CPU frequency governor to `performance`, e.g. `sudo cpupower frequency-set --governor performance`. Google Benchmark prints a warning when frequency scaling is enabled. Pinning the process to a core diff --git a/tests/benchmarks/json_view/.gitignore b/tests/benchmarks/json_view/.gitignore new file mode 100644 index 000000000..567609b12 --- /dev/null +++ b/tests/benchmarks/json_view/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/tests/benchmarks/json_view/README.md b/tests/benchmarks/json_view/README.md new file mode 100644 index 000000000..c4a05d35a --- /dev/null +++ b/tests/benchmarks/json_view/README.md @@ -0,0 +1,90 @@ +# json_view compared with other libraries + +The in-tree benchmarks in [`tests/benchmarks`](../README.md) measure `json_document` against `json::parse` only. The +programs here compare it with [yyjson](https://github.com/ibireme/yyjson), +[simdjson](https://github.com/simdjson/simdjson), and [Boost.JSON](https://github.com/boostorg/json): the question +users ask when they pick a library. They are not built by CMake or run by CI. + +## Reproducing the numbers + +`compare.py` builds the programs against `include/` of this checkout, runs them, and writes the results together with +everything needed to reproduce them to `results/-.md` (and `.csv`): the date, the commit, the CPU, the +OS, the compiler, the flags, and the versions of all libraries. + +```sh +python3 tests/benchmarks/json_view/compare.py --data [--native] [--rounds 30] +``` + +- `--data` is the downloaded [test data](https://github.com/nlohmann/json_test_data), e.g. the `test_files` directory + of a CMake build directory. It needs `nativejson-benchmark/{twitter,citm_catalog,canada}.json` and + `jeopardy/jeopardy.json`. +- The other libraries come from the system: pkg-config, or Homebrew (`brew install yyjson simdjson boost`). With + `--download`, pinned releases are downloaded instead and checked against their SHA-256. Without Boost headers (or + with `--no-boost`), the Boost.JSON columns are skipped, and the results say so. +- `--corpus file...` adds files to the corpus benchmark, e.g. those of + [simdjson-data](https://github.com/simdjson/simdjson-data) or the + [yyjson benchmark](https://github.com/ibireme/yyjson_benchmark). +- Only the Python 3 standard library is used; a C++17 compiler is needed (`CXX` and `CC` are honored). + +For numbers worth publishing, use a quiet machine (see [Getting stable numbers](../README.md#getting-stable-numbers)), +the default 30 rounds or more, and `--native` only if the other libraries were built for the same CPU. + +### On GitHub-hosted runners + +The workflow [json_view benchmarks](../../../.github/workflows/json_view_benchmarks.yml) runs `compare.py --download` +on demand: by hand (Actions → "json_view benchmarks" → "Run workflow"), on an x86-64 or AArch64 Ubuntu runner with GCC +or Clang, or when a pull request gets the label `benchmark`, on both architectures with GCC. The results appear as the +job summary and as an artifact. Shared runners are noisy, so these numbers show +where `json_view` stands on another architecture; they are not meant for publication. + +## What is measured + +`bench_view.cpp` runs four workloads on twitter, citm_catalog, canada, jeopardy, a single tweet (`status`), and a +JSON-RPC request (`rpc`): + +| workload | what it does | +|---|---| +| parse | build and free a document | +| traverse | parse, then visit every value, convert every number, touch every string and key | +| select | parse, then read a few fields per record (e.g. id, user name, and retweet count of each tweet) | +| dump | serialize a parsed document (compact) | + +`bench_corpus.cpp` runs parse, traverse, and dump on any list of files, so that no library is tuned to a handful of +documents. Its dump also writes the numbers as they are in the input: `json_view` with `number_format::source`, and +yyjson with numbers read as raw text (`YYJSON_READ_NUMBER_AS_RAW`), without converting them. + +`bench_edit.cpp` measures read-modify-write: parse, apply the same logical edits with each library's own API, and +serialize (compact). Workloads: `patch` (a handful of edits at fixed places) and `update` (edits in every record). +An editable `json_document` edits in place; yyjson copies its immutable document into a mutable one first +(`yyjson_doc_mut_copy`); Boost.JSON and `json::parse` build mutable DOMs; simdjson cannot edit a document. All +outputs are checked to describe the same value. + +Before anything is timed, all engines must accept each document and agree on the traversal: the number of values, the +bytes of all strings and keys, and the sum of all numbers. All engines run interleaved in every round, and the best +round is reported, as time and as a factor of the `json_view` time (below 1 means faster than `json_view`). Each timed +call follows an untimed call of the same engine: otherwise the engine after `json::parse` pays for the allocator +cleaning up the tens of thousands of nodes `json::parse` just freed (with glibc, this made `json_view` look 1.7 times +slower on citm_catalog traverse). + +The engines do not all offer the same features, which the numbers should be read with: + +| engine | document | random access | editable | notes | +|---|---|---|---|---| +| `json_view` | immutable index into the text | yes | no | a fresh document per parse; "reused" parses into the same document | +| yyjson | immutable (`yyjson_read`) | yes | via a mutable copy | | +| simdjson DOM | immutable, parser reused | yes | no | "fresh" uses a new parser per parse | +| simdjson On-Demand | none: forward-only, lazy | no | no | only traverse and select | +| Boost.JSON | owning, mutable DOM | yes | yes | monotonic resource | +| `json::parse` | owning, mutable DOM | yes | yes | | + +Reusing memory matters as much as the parser. simdjson DOM reuses its parser, so it writes into memory it already +touched; a fresh `json_view` document or yyjson document gets new memory for every parse. On Linux, glibc returns large +blocks to the system when they are freed, so every fresh parse of a large document pays a page fault per 4 KiB page: +on x86-64 Linux, a fresh `json_view` parse of jeopardy took about twice as long as a reused one. On macOS on Apple +silicon, with 16 KiB pages, the difference is much smaller. Compare "json_view (reused)" with "simdjson DOM", and the +fresh `json_view` with "simdjson DOM (fresh)" and yyjson. + +## Published results + +Results are only published with the file `compare.py` wrote, which names the machine and the versions; see +`results/`. Numbers from one machine and compiler do not carry over to another: rerun the script. diff --git a/tests/benchmarks/json_view/bench_corpus.cpp b/tests/benchmarks/json_view/bench_corpus.cpp new file mode 100644 index 000000000..34b40ebb5 --- /dev/null +++ b/tests/benchmarks/json_view/bench_corpus.cpp @@ -0,0 +1,348 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// Corpus benchmark: the read-only workloads of bench_view.cpp on any list of +// JSON files (for example the benchmark sets of simdjson and yyjson). +// +// ./bench_corpus [--rounds N] file... +// +// For every file, all engines must accept it and agree on a traversal (value +// count, string bytes, sum of numbers) before anything is timed. Workloads: +// parse (build and free a document), traverse (visit every value, convert +// every number), dump (compact), and for json_view also dump with the source +// number text, compared with yyjson writing numbers read as raw text +// (YYJSON_READ_NUMBER_AS_RAW). Results go to bench_corpus.csv. +#include + +#if JSON_VIEW_BENCH_BOOST + #include + #include +#endif +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using nlohmann::json; +using nlohmann::json_document; +using nlohmann::json_view; + +static volatile double g_sink; + +struct stats +{ + double num = 0; + std::size_t str = 0, nodes = 0; +}; + +static void walk(json_view v, stats& st) +{ + ++st.nodes; + switch (v.type()) + { + case json::value_t::object: + for (auto it = v.begin(); it != v.end(); ++it) + { + st.str += it.key().size(); + walk(*it, st); + } + break; + case json::value_t::array: + for (const json_view e : v) + { + walk(e, st); + } + break; + case json::value_t::string: + st.str += v.get_string().size(); + break; + case json::value_t::number_integer: + st.num += static_cast(v.get()); + break; + case json::value_t::number_unsigned: + st.num += static_cast(v.get()); + break; + case json::value_t::number_float: + st.num += v.get(); + break; + default: + break; + } +} + +static void walk(yyjson_val* v, stats& st) +{ + ++st.nodes; + switch (yyjson_get_type(v)) + { + case YYJSON_TYPE_OBJ: + { + std::size_t idx, max; + yyjson_val* k, * val; + yyjson_obj_foreach(v, idx, max, k, val) + { + st.str += yyjson_get_len(k); + walk(val, st); + } + break; + } + case YYJSON_TYPE_ARR: + { + std::size_t idx, max; + yyjson_val* val; + yyjson_arr_foreach(v, idx, max, val) + { + walk(val, st); + } + break; + } + case YYJSON_TYPE_STR: + st.str += yyjson_get_len(v); + break; + case YYJSON_TYPE_NUM: + st.num += yyjson_is_sint(v) ? static_cast(yyjson_get_sint(v)) : yyjson_is_uint(v) ? static_cast(yyjson_get_uint(v)) : yyjson_get_real(v); + break; + default: + break; + } +} + +static void walk(simdjson::dom::element e, stats& st) +{ + ++st.nodes; + switch (e.type()) + { + case simdjson::dom::element_type::OBJECT: + for (auto f : simdjson::dom::object(e)) + { + st.str += f.key.size(); + walk(f.value, st); + } + break; + case simdjson::dom::element_type::ARRAY: + for (auto c : simdjson::dom::array(e)) + { + walk(c, st); + } + break; + case simdjson::dom::element_type::STRING: + st.str += std::string_view(e).size(); + break; + case simdjson::dom::element_type::INT64: + st.num += static_cast(int64_t(e)); + break; + case simdjson::dom::element_type::UINT64: + st.num += static_cast(uint64_t(e)); + break; + case simdjson::dom::element_type::DOUBLE: + st.num += double(e); + break; + default: + break; + } +} + +#if JSON_VIEW_BENCH_BOOST +static void walk(const boost::json::value& v, stats& st) +{ + ++st.nodes; + switch (v.kind()) + { + case boost::json::kind::object: + for (const auto& kv : v.get_object()) + { + st.str += kv.key().size(); + walk(kv.value(), st); + } + break; + case boost::json::kind::array: + for (const auto& c : v.get_array()) + { + walk(c, st); + } + break; + case boost::json::kind::string: + st.str += v.get_string().size(); + break; + case boost::json::kind::int64: + st.num += static_cast(v.get_int64()); + break; + case boost::json::kind::uint64: + st.num += static_cast(v.get_uint64()); + break; + case boost::json::kind::double_: + st.num += v.get_double(); + break; + default: + break; + } +} +#endif + +static std::string slurp(const std::string& p) +{ + std::ifstream f(p, std::ios::binary); + if (!f) + { + std::fprintf(stderr, "cannot open %s\n", p.c_str()); + std::exit(1); + } + std::stringstream ss; + ss << f.rdbuf(); + return ss.str(); +} + +static bool same(const stats& a, const stats& b) +{ + return a.nodes == b.nodes && a.str == b.str && (a.num == b.num || std::fabs(a.num - b.num) <= 1e-9 * std::fabs(a.num)); +} + +int main(int argc, char** argv) +{ + int rounds = 0; // 0: by size + std::vector files; + for (int i = 1; i < argc; ++i) + { + if (std::strcmp(argv[i], "--rounds") == 0 && i + 1 < argc) + { + // (clamped: 0 rounds by size, at most a million) + rounds = static_cast((std::min)((std::max)(std::strtol(argv[++i], nullptr, 10), 0L), 1000000L)); + } + else + { + files.push_back(argv[i]); + } + } + std::FILE* csv = std::fopen("bench_corpus.csv", "w"); + if (csv == nullptr) + { + std::perror("bench_corpus.csv"); + return 1; + } + std::fprintf(csv, "file,bytes,workload,engine,ns\n"); + json_document reused; + simdjson::dom::parser sj; + for (const auto& path : files) + { + const std::string s = slurp(path); + const std::string name = path.substr(path.rfind('/') + 1); + const simdjson::padded_string ps(s); + + // all engines must agree before timing + stats a, b, c, d; + const json_document doc = json_document::parse(s); + walk(doc.root(), a); + yyjson_doc* y = yyjson_read(s.data(), s.size(), 0); + auto sjr = sj.parse(ps); +#if JSON_VIEW_BENCH_BOOST + boost::json::parse_options opt; + opt.numbers = boost::json::number_precision::precise; + boost::json::monotonic_resource mr0; + const boost::json::value bv = boost::json::parse(s, &mr0, opt); +#endif + if (y == nullptr || sjr.error()) + { + std::printf("%-34s skipped (an engine rejects it)\n", name.c_str()); + yyjson_doc_free(y); + continue; + } + walk(yyjson_doc_get_root(y), b); + walk(sjr.value_unsafe(), c); +#if JSON_VIEW_BENCH_BOOST + walk(bv, d); +#else + d = a; +#endif + yyjson_doc_free(y); + const bool ok = same(a, b) && same(a, c) && same(a, d); + + const int r = rounds > 0 ? rounds : static_cast(std::max(3, std::min(60, 400000000 / (s.size() + 1)))); + struct engine + { + std::string name; + std::function fn; + }; + json_document vd = json_document::parse(s); + yyjson_doc* yd = yyjson_read(s.data(), s.size(), 0); + yyjson_doc* yd_raw = yyjson_read(s.data(), s.size(), YYJSON_READ_NUMBER_AS_RAW); + simdjson::dom::parser sjd; + const simdjson::dom::element se = sjd.parse(ps).value_unsafe(); + const std::vector>> workloads = + { + { + "parse", { + {"json_view", [&] { auto x = json_document::parse(s); g_sink = static_cast(x.node_count()); }}, + {"json_view (reused)", [&] { reused.read(s); g_sink = static_cast(reused.node_count()); }}, + {"yyjson", [&] { yyjson_doc* x = yyjson_read(s.data(), s.size(), 0); g_sink = static_cast(yyjson_doc_get_val_count(x)); yyjson_doc_free(x); }}, + {"simdjson DOM", [&] { auto e = sj.parse(ps).value_unsafe(); g_sink = e.is_object(); }}, + {"simdjson DOM (fresh)", [&] { simdjson::dom::parser p; auto e = p.parse(ps).value_unsafe(); g_sink = e.is_object(); }}, +#if JSON_VIEW_BENCH_BOOST + {"Boost.JSON", [&] { boost::json::monotonic_resource mr; auto v = boost::json::parse(s, &mr); g_sink = v.is_object(); }}, +#endif + } + }, + { + "traverse", { + {"json_view", [&] { auto x = json_document::parse(s); stats st; walk(x.root(), st); g_sink = st.num; }}, + {"yyjson", [&] { yyjson_doc* x = yyjson_read(s.data(), s.size(), 0); stats st; walk(yyjson_doc_get_root(x), st); g_sink = st.num; yyjson_doc_free(x); }}, + {"simdjson DOM", [&] { stats st; walk(sj.parse(ps).value_unsafe(), st); g_sink = st.num; }}, +#if JSON_VIEW_BENCH_BOOST + {"Boost.JSON", [&] { boost::json::monotonic_resource mr; auto v = boost::json::parse(s, &mr); stats st; walk(v, st); g_sink = st.num; }}, +#endif + } + }, + { + "dump", { + {"json_view", [&] { std::string o = vd.root().dump(); g_sink = static_cast(o.size()); }}, + {"yyjson", [&] { std::size_t n = 0; char* o = yyjson_write(yd, 0, &n); g_sink = static_cast(n); std::free(o); }}, + {"simdjson DOM", [&] { std::string o = simdjson::to_string(se); g_sink = static_cast(o.size()); }}, + {"json_view (source numbers)", [&] { std::string o = vd.root().dump(-1, ' ', false, json_view::number_format::source); g_sink = static_cast(o.size()); }}, + {"yyjson (raw numbers)", [&] { std::size_t n = 0; char* o = yyjson_write(yd_raw, 0, &n); g_sink = static_cast(n); std::free(o); }}, + } + }, + }; + std::printf("%-34s %9zu B%s\n", name.c_str(), s.size(), ok ? "" : " [ENGINES DISAGREE]"); + for (const auto& wl : workloads) + { + std::vector best(wl.second.size(), 1e300); + for (int i = 0; i < r; ++i) + { + for (std::size_t k = 0; k < wl.second.size(); ++k) + { + // an untimed call first: whatever the previous engine left to the allocator + // (e.g. thousands of freed json nodes) is cleaned up here, not in the timing + wl.second[k].fn(); + const auto t0 = std::chrono::steady_clock::now(); + wl.second[k].fn(); + best[k] = std::min(best[k], std::chrono::duration(std::chrono::steady_clock::now() - t0).count()); + } + } + std::printf(" %-9s", wl.first.c_str()); + for (std::size_t k = 0; k < wl.second.size(); ++k) + { + std::printf(" %s %.2f GB/s (%.2fx)", wl.second[k].name.c_str(), static_cast(s.size()) / best[k], best[k] / best[0]); + std::fprintf(csv, "%s,%zu,%s,%s,%.1f\n", name.c_str(), s.size(), wl.first.c_str(), wl.second[k].name.c_str(), best[k]); + } + std::printf("\n"); + std::fflush(stdout); + } + yyjson_doc_free(yd); + yyjson_doc_free(yd_raw); + } + std::fclose(csv); +} diff --git a/tests/benchmarks/json_view/bench_edit.cpp b/tests/benchmarks/json_view/bench_edit.cpp new file mode 100644 index 000000000..c43da11af --- /dev/null +++ b/tests/benchmarks/json_view/bench_edit.cpp @@ -0,0 +1,589 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// Read-modify-write benchmark: parse a document, apply the same logical edits +// with each library's own API, and serialize it (compact). +// +// json_view json_editable_document: edits in place, unchanged values stay in the index +// yyjson yyjson_read + yyjson_doc_mut_copy (the way to edit a parsed document) +// Boost.JSON parse into a mutable DOM (monotonic resource, precise numbers), serialize +// json::parse nlohmann::json today +// simdjson has no mutable document and is not part of this comparison. +// +// Workloads: +// patch a handful of edits at fixed places (scalars, a new member, a new array element) +// update edits in every record (twitter: 100 statuses, citm: 243 performances, +// canada: 480 rings, jeopardy: 216,930 questions): set scalars, erase a +// member, add a member (canada: replace the first point of every ring) +// +// Build: see README.md (same flags as bench_view.cpp). +#include + +#if JSON_VIEW_BENCH_BOOST + #include + #include +#endif +#include + +#include +#include +#include +#include +#include +#include +#include + +using nlohmann::json; +using nlohmann::json_editable_document; +using nlohmann::json_editable_view; +#if JSON_VIEW_BENCH_BOOST + namespace bj = boost::json; +#endif + +static volatile std::size_t g_sink; + +// ---------------- json_view ---------------- + +static std::string edit_view(const std::string& name, const std::string& s, bool update) +{ + json_editable_document d = json_editable_document::parse(s); + const json_editable_view r = d.root(); + if (name == "twitter") + { + if (update) + { + std::int64_t i = 0; + for (const json_editable_view st : r["statuses"]) + { + d.set(st, "retweet_count", i++); + d.set(st, "favorited", true); + d.set(st, "text", "redacted"); + d.erase(st, "entities"); + d.set(st, "edited", true); + } + } + else + { + d.set(r["search_metadata"], "count", 200); + d.set(r["statuses"][0], "text", "patched"); + d.set(r["statuses"][0]["user"], "followers_count", 1); + d.set(r["statuses"][99], "favorited", true); + d.set(r, "patched", true); + } + } + else if (name == "citm_catalog") + { + if (update) + { + for (const json_editable_view p : r["performances"]) + { + d.set(p, "name", "performance"); + d.set(p, "start", p["start"].get() + 1); + d.erase(p, "seatMapImage"); + d.set(p, "edited", true); + } + } + else + { + d.set(r["events"]["138586341"], "name", "patched"); + d.set(r["performances"][0], "start", 0); + d.set(r["venueNames"], "PLEYEL_PLEYEL", "Salle"); + d.set(r, "patched", true); + } + } + else if (name == "canada") + { + const json_editable_view coords = r["features"][0]["geometry"]["coordinates"]; + if (update) + { + for (const json_editable_view ring : coords) + { + d.set(ring, 0, json::array({0.5, 0.5})); + } + } + else + { + d.set(r["features"][0]["properties"], "name", "patched"); + d.set(r, "type", "FeatureCollection2"); + d.set(coords[0], 0, json::array({0.0, 0.0})); + } + } + else if (name == "jeopardy") + { + if (update) + { + for (const json_editable_view q : r) + { + d.set(q, "value", "$1"); + d.erase(q, "air_date"); + } + } + else + { + d.set(r[0], "value", "$0"); + d.set(r[100000], "answer", "patched"); + d.set(r[216929], "round", "x"); + d.push_back(r, json::object({{"category", "NEW"}, {"value", "$5"}})); + } + } + else if (name == "status") + { + d.set(r, "retweet_count", 1); + d.set(r["user"], "name", "x"); + } + else if (name == "rpc") + { + d.set(r, "id", 4); + d.set(r["params"], "subtrahend", 24); + } + return r.dump(); +} + +// ---------------- nlohmann::json ---------------- + +static std::string edit_json(const std::string& name, const std::string& s, bool update) +{ + json r = json::parse(s); + if (name == "twitter") + { + if (update) + { + std::int64_t i = 0; + for (auto& st : r["statuses"]) + { + st["retweet_count"] = i++; + st["favorited"] = true; + st["text"] = "redacted"; + st.erase("entities"); + st["edited"] = true; + } + } + else + { + r["search_metadata"]["count"] = 200; + r["statuses"][0]["text"] = "patched"; + r["statuses"][0]["user"]["followers_count"] = 1; + r["statuses"][99]["favorited"] = true; + r["patched"] = true; + } + } + else if (name == "citm_catalog") + { + if (update) + { + for (auto& p : r["performances"]) + { + p["name"] = "performance"; + p["start"] = p["start"].get() + 1; + p.erase("seatMapImage"); + p["edited"] = true; + } + } + else + { + r["events"]["138586341"]["name"] = "patched"; + r["performances"][0]["start"] = 0; + r["venueNames"]["PLEYEL_PLEYEL"] = "Salle"; + r["patched"] = true; + } + } + else if (name == "canada") + { + json& coords = r["features"][0]["geometry"]["coordinates"]; + if (update) + { + for (auto& ring : coords) + { + ring[0] = json::array({0.5, 0.5}); + } + } + else + { + r["features"][0]["properties"]["name"] = "patched"; + r["type"] = "FeatureCollection2"; + coords[0][0] = json::array({0.0, 0.0}); + } + } + else if (name == "jeopardy") + { + if (update) + { + for (auto& q : r) + { + q["value"] = "$1"; + q.erase("air_date"); + } + } + else + { + r[0]["value"] = "$0"; + r[100000]["answer"] = "patched"; + r[216929]["round"] = "x"; + r.push_back(json::object({{"category", "NEW"}, {"value", "$5"}})); + } + } + else if (name == "status") + { + r["retweet_count"] = 1; + r["user"]["name"] = "x"; + } + else if (name == "rpc") + { + r["id"] = 4; + r["params"]["subtrahend"] = 24; + } + return r.dump(); +} + +// ---------------- yyjson ---------------- + +static std::string edit_yyjson(const std::string& name, const std::string& s, bool update) +{ + yyjson_doc* idoc = yyjson_read(s.data(), s.size(), 0); + yyjson_mut_doc* d = yyjson_doc_mut_copy(idoc, nullptr); + yyjson_doc_free(idoc); + yyjson_mut_val* r = yyjson_mut_doc_get_root(d); + auto get = [](yyjson_mut_val * o, const char* k) + { + return yyjson_mut_obj_get(o, k); + }; + if (name == "twitter") + { + yyjson_mut_val* sts = get(r, "statuses"); + if (update) + { + std::size_t idx, max; + yyjson_mut_val* st; + std::int64_t i = 0; + yyjson_mut_arr_foreach(sts, idx, max, st) + { + yyjson_mut_set_sint(get(st, "retweet_count"), i++); + yyjson_mut_set_bool(get(st, "favorited"), true); + yyjson_mut_set_str(get(st, "text"), "redacted"); + yyjson_mut_obj_remove_key(st, "entities"); + yyjson_mut_obj_add_bool(d, st, "edited", true); + } + } + else + { + yyjson_mut_set_sint(get(get(r, "search_metadata"), "count"), 200); + yyjson_mut_val* s0 = yyjson_mut_arr_get(sts, 0); + yyjson_mut_set_str(get(s0, "text"), "patched"); + yyjson_mut_set_sint(get(get(s0, "user"), "followers_count"), 1); + yyjson_mut_set_bool(get(yyjson_mut_arr_get(sts, 99), "favorited"), true); + yyjson_mut_obj_add_bool(d, r, "patched", true); + } + } + else if (name == "citm_catalog") + { + if (update) + { + std::size_t idx, max; + yyjson_mut_val* p; + yyjson_mut_arr_foreach(get(r, "performances"), idx, max, p) + { + yyjson_mut_set_str(get(p, "name"), "performance"); + yyjson_mut_val* start = get(p, "start"); + yyjson_mut_set_sint(start, yyjson_mut_get_sint(start) + 1); + yyjson_mut_obj_remove_key(p, "seatMapImage"); + yyjson_mut_obj_add_bool(d, p, "edited", true); + } + } + else + { + yyjson_mut_set_str(get(get(get(r, "events"), "138586341"), "name"), "patched"); + yyjson_mut_set_sint(get(yyjson_mut_arr_get(get(r, "performances"), 0), "start"), 0); + yyjson_mut_set_str(get(get(r, "venueNames"), "PLEYEL_PLEYEL"), "Salle"); + yyjson_mut_obj_add_bool(d, r, "patched", true); + } + } + else if (name == "canada") + { + yyjson_mut_val* f0 = yyjson_mut_arr_get(get(r, "features"), 0); + yyjson_mut_val* coords = get(get(f0, "geometry"), "coordinates"); + if (update) + { + static const double half[2] = {0.5, 0.5}; + std::size_t idx, max; + yyjson_mut_val* ring; + yyjson_mut_arr_foreach(coords, idx, max, ring) + { + yyjson_mut_arr_replace(ring, 0, yyjson_mut_arr_with_real(d, half, 2)); + } + } + else + { + static const double zero[2] = {0.0, 0.0}; + yyjson_mut_set_str(get(get(f0, "properties"), "name"), "patched"); + yyjson_mut_set_str(get(r, "type"), "FeatureCollection2"); + yyjson_mut_arr_replace(yyjson_mut_arr_get(coords, 0), 0, yyjson_mut_arr_with_real(d, zero, 2)); + } + } + else if (name == "jeopardy") + { + if (update) + { + std::size_t idx, max; + yyjson_mut_val* q; + yyjson_mut_arr_foreach(r, idx, max, q) + { + yyjson_mut_set_str(get(q, "value"), "$1"); + yyjson_mut_obj_remove_key(q, "air_date"); + } + } + else + { + yyjson_mut_set_str(get(yyjson_mut_arr_get(r, 0), "value"), "$0"); + yyjson_mut_set_str(get(yyjson_mut_arr_get(r, 100000), "answer"), "patched"); + yyjson_mut_set_str(get(yyjson_mut_arr_get(r, 216929), "round"), "x"); + yyjson_mut_val* o = yyjson_mut_obj(d); + yyjson_mut_obj_add_str(d, o, "category", "NEW"); + yyjson_mut_obj_add_str(d, o, "value", "$5"); + yyjson_mut_arr_append(r, o); + } + } + else if (name == "status") + { + yyjson_mut_set_sint(get(r, "retweet_count"), 1); + yyjson_mut_set_str(get(get(r, "user"), "name"), "x"); + } + else if (name == "rpc") + { + yyjson_mut_set_sint(get(r, "id"), 4); + yyjson_mut_set_sint(get(get(r, "params"), "subtrahend"), 24); + } + std::size_t n = 0; + char* out = yyjson_mut_write(d, 0, &n); + std::string result(out, n); + std::free(out); + yyjson_mut_doc_free(d); + return result; +} + +#if JSON_VIEW_BENCH_BOOST +// ---------------- Boost.JSON ---------------- + +static std::string edit_boost(const std::string& name, const std::string& s, bool update) +{ + bj::monotonic_resource mr; + bj::parse_options opt; + opt.numbers = bj::number_precision::precise; // correctly rounded, like the others + bj::value v = bj::parse(s, &mr, opt); + bj::object* const obj = v.if_object(); // nullptr for jeopardy (an array) + if (name == "twitter") + { + bj::array& sts = (*obj)["statuses"].as_array(); + if (update) + { + std::int64_t i = 0; + for (auto& e : sts) + { + bj::object& st = e.as_object(); + st["retweet_count"] = i++; + st["favorited"] = true; + st["text"] = "redacted"; + st.erase("entities"); + st["edited"] = true; + } + } + else + { + (*obj)["search_metadata"].as_object()["count"] = 200; + bj::object& s0 = sts[0].as_object(); + s0["text"] = "patched"; + s0["user"].as_object()["followers_count"] = 1; + sts[99].as_object()["favorited"] = true; + (*obj)["patched"] = true; + } + } + else if (name == "citm_catalog") + { + if (update) + { + for (auto& e : (*obj)["performances"].as_array()) + { + bj::object& p = e.as_object(); + p["name"] = "performance"; + p["start"] = p["start"].as_int64() + 1; + p.erase("seatMapImage"); + p["edited"] = true; + } + } + else + { + (*obj)["events"].as_object()["138586341"].as_object()["name"] = "patched"; + (*obj)["performances"].as_array()[0].as_object()["start"] = 0; + (*obj)["venueNames"].as_object()["PLEYEL_PLEYEL"] = "Salle"; + (*obj)["patched"] = true; + } + } + else if (name == "canada") + { + bj::object& f0 = (*obj)["features"].as_array()[0].as_object(); + bj::array& coords = f0["geometry"].as_object()["coordinates"].as_array(); + if (update) + { + for (auto& ring : coords) + { + ring.as_array()[0] = bj::array({0.5, 0.5}); + } + } + else + { + f0["properties"].as_object()["name"] = "patched"; + (*obj)["type"] = "FeatureCollection2"; + coords[0].as_array()[0] = bj::array({0.0, 0.0}); + } + } + else if (name == "jeopardy") + { + bj::array& a = v.as_array(); + if (update) + { + for (auto& e : a) + { + bj::object& q = e.as_object(); + q["value"] = "$1"; + q.erase("air_date"); + } + } + else + { + a[0].as_object()["value"] = "$0"; + a[100000].as_object()["answer"] = "patched"; + a[216929].as_object()["round"] = "x"; + a.push_back(bj::object({{"category", "NEW"}, {"value", "$5"}})); + } + } + else if (name == "status") + { + (*obj)["retweet_count"] = 1; + (*obj)["user"].as_object()["name"] = "x"; + } + else if (name == "rpc") + { + (*obj)["id"] = 4; + (*obj)["params"].as_object()["subtrahend"] = 24; + } + return bj::serialize(v); +} +#endif + +// ---------------- harness ---------------- + +static std::string slurp(const std::string& p) +{ + std::ifstream f(p, std::ios::binary); + if (!f) + { + std::fprintf(stderr, "cannot open %s\n", p.c_str()); + std::exit(1); + } + std::stringstream ss; + ss << f.rdbuf(); + return ss.str(); +} + +int main(int argc, char** argv) +{ + if (argc < 2) + { + std::fprintf(stderr, "usage: %s [rounds] [document]\n", argv[0]); + return 1; + } + const std::string T = std::string(argv[1]) + "/"; + // (clamped to 1 to a million) + const int rounds = argc > 2 ? static_cast((std::min)((std::max)(std::strtol(argv[2], nullptr, 10), 1L), 1000000L)) : 20; + const std::string only = argc > 3 ? argv[3] : ""; + struct doc + { + std::string name, text; + int batch; + }; + std::vector docs; + for (const char* f : + {"nativejson-benchmark/twitter.json", "nativejson-benchmark/citm_catalog.json", "nativejson-benchmark/canada.json", "jeopardy/jeopardy.json" + }) + { + std::string n = std::string(f).substr(std::string(f).find('/') + 1); + docs.push_back({n.substr(0, n.size() - 5), slurp(T + f), 1}); + } + docs.push_back({"status", json::parse(docs[0].text)["statuses"][0].dump(), 200}); + docs.push_back({"rpc", R"({"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 3})", 5000}); + + using fn = std::string (*)(const std::string&, const std::string&, bool); + const std::vector> engines = + { + {"json_view", edit_view}, {"yyjson", edit_yyjson}, +#if JSON_VIEW_BENCH_BOOST + {"Boost.JSON", edit_boost}, +#endif + {"json::parse", edit_json} + }; + + std::FILE* csv = std::fopen("bench_edit.csv", "w"); + if (csv == nullptr) + { + std::perror("bench_edit.csv"); + return 1; + } + std::fprintf(csv, "doc,bytes,workload,engine,ns\n"); + for (const auto& dc : docs) + { + if (!only.empty() && dc.name != only) + { + continue; + } + for (const bool update : + { + false, true + }) + { + if (update && (dc.name == "status" || dc.name == "rpc")) + { + continue; + } + // all engines must produce the same value + const json expected = json::parse(edit_json(dc.name, dc.text, update)); + bool ok = true; + for (const auto& e : engines) + { + ok = ok && json::parse(e.second(dc.name, dc.text, update)) == expected; + } + std::vector best(engines.size(), 1e300); + const int r = dc.text.size() > 10000000 ? std::max(3, rounds / 4) : rounds; + for (int i = 0; i < r; ++i) + { + for (std::size_t k = 0; k < engines.size(); ++k) + { + // an untimed call first: whatever the previous engine left to the allocator + // (e.g. thousands of freed json nodes) is cleaned up here, not in the timing + g_sink = engines[k].second(dc.name, dc.text, update).size(); + const auto t0 = std::chrono::steady_clock::now(); + for (int b = 0; b < dc.batch; ++b) + { + g_sink = engines[k].second(dc.name, dc.text, update).size(); + } + const double ns = std::chrono::duration(std::chrono::steady_clock::now() - t0).count() / dc.batch; + best[k] = std::min(best[k], ns); + } + } + const char* wl = update ? "update" : "patch"; + std::printf("%-13s %-7s %s", dc.name.c_str(), wl, ok ? "" : "[OUTPUT MISMATCH] "); + for (std::size_t k = 0; k < engines.size(); ++k) + { + const double us = best[k] / 1e3; + std::printf(" %s %.*fus (%.2fx)", engines[k].first.c_str(), us < 10 ? 3 : (us < 1000 ? 1 : 0), us, best[k] / best[0]); + std::fprintf(csv, "%s,%zu,%s,%s,%.1f\n", dc.name.c_str(), dc.text.size(), wl, engines[k].first.c_str(), best[k]); + } + std::printf("\n"); + std::fflush(stdout); + } + } + std::fclose(csv); +} diff --git a/tests/benchmarks/json_view/bench_view.cpp b/tests/benchmarks/json_view/bench_view.cpp new file mode 100644 index 000000000..32d038649 --- /dev/null +++ b/tests/benchmarks/json_view/bench_view.cpp @@ -0,0 +1,755 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// Same-feature-set benchmark: read-only JSON documents with random access. +// +// json_view nlohmann/json_view.hpp (fresh document per parse / reused) +// yyjson yyjson_read(): immutable document, random access +// simdjson DOM dom::parser (reused, as recommended; "fresh": a new parser +// per parse): immutable, random access +// references (different feature sets): +// simdjson OD On-Demand: forward-only, lazy +// Boost.JSON owning, mutable DOM (monotonic resource) +// json::parse owning, mutable DOM (nlohmann today) +// +// Workloads: parse (build + free), traverse (visit everything, convert every +// number, touch every string and key), select (a few fields per document), +// dump (compact serialization of the parsed document). +// All engines run interleaved in every round, each timed call after an untimed +// one of the same engine; the best round is reported. +#include + +#if JSON_VIEW_BENCH_BOOST + #include + #include +#endif +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using nlohmann::json; +using nlohmann::json_document; +using nlohmann::json_view; + +static volatile double g_sink; + +struct stats +{ + double num = 0; + std::size_t str = 0, nodes = 0; +}; + +// ---------------- traversal ---------------- + +static void walk(json_view v, stats& st) +{ + ++st.nodes; + switch (v.type()) + { + case json::value_t::object: + for (auto it = v.begin(); it != v.end(); ++it) + { + st.str += it.key().size(); + walk(*it, st); + } + break; + case json::value_t::array: + for (const json_view e : v) + { + walk(e, st); + } + break; + case json::value_t::string: + st.str += v.get_string().size(); + break; + case json::value_t::number_integer: + st.num += static_cast(v.get()); + break; + case json::value_t::number_unsigned: + st.num += static_cast(v.get()); + break; + case json::value_t::number_float: + st.num += v.get(); + break; + default: + break; + } +} + +static void walk(const json& j, stats& st) +{ + ++st.nodes; + switch (j.type()) + { + case json::value_t::object: + for (const auto& kv : j.get_ref()) + { + st.str += kv.first.size(); + walk(kv.second, st); + } + break; + case json::value_t::array: + for (const auto& e : j.get_ref()) + { + walk(e, st); + } + break; + case json::value_t::string: + st.str += j.get_ref().size(); + break; + case json::value_t::number_integer: + st.num += static_cast(*j.get_ptr()); + break; + case json::value_t::number_unsigned: + st.num += static_cast(*j.get_ptr()); + break; + case json::value_t::number_float: + st.num += *j.get_ptr(); + break; + default: + break; + } +} + +static void walk(yyjson_val* v, stats& st) +{ + ++st.nodes; + switch (yyjson_get_type(v)) + { + case YYJSON_TYPE_OBJ: + { + std::size_t idx, max; + yyjson_val* k, * val; + yyjson_obj_foreach(v, idx, max, k, val) + { + st.str += yyjson_get_len(k); + walk(val, st); + } + break; + } + case YYJSON_TYPE_ARR: + { + std::size_t idx, max; + yyjson_val* val; + yyjson_arr_foreach(v, idx, max, val) + { + walk(val, st); + } + break; + } + case YYJSON_TYPE_STR: + st.str += yyjson_get_len(v); + break; + case YYJSON_TYPE_NUM: + if (yyjson_is_sint(v)) + { + st.num += static_cast(yyjson_get_sint(v)); + } + else if (yyjson_is_uint(v)) + { + st.num += static_cast(yyjson_get_uint(v)); + } + else + { + st.num += yyjson_get_real(v); + } + break; + default: + break; + } +} + +static void walk(simdjson::dom::element e, stats& st) +{ + ++st.nodes; + switch (e.type()) + { + case simdjson::dom::element_type::OBJECT: + for (auto f : simdjson::dom::object(e)) + { + st.str += f.key.size(); + walk(f.value, st); + } + break; + case simdjson::dom::element_type::ARRAY: + for (auto c : simdjson::dom::array(e)) + { + walk(c, st); + } + break; + case simdjson::dom::element_type::STRING: + st.str += std::string_view(e).size(); + break; + case simdjson::dom::element_type::INT64: + st.num += static_cast(int64_t(e)); + break; + case simdjson::dom::element_type::UINT64: + st.num += static_cast(uint64_t(e)); + break; + case simdjson::dom::element_type::DOUBLE: + st.num += double(e); + break; + default: + break; + } +} + +static void walk_od(simdjson::ondemand::value v, stats& st) +{ + ++st.nodes; + switch (v.type()) + { + case simdjson::ondemand::json_type::object: + for (auto f : v.get_object()) + { + st.str += std::string_view(f.unescaped_key()).size(); + walk_od(f.value(), st); + } + break; + case simdjson::ondemand::json_type::array: + for (auto c : v.get_array()) + { + walk_od(c.value(), st); + } + break; + case simdjson::ondemand::json_type::string: + st.str += std::string_view(v.get_string()).size(); + break; + case simdjson::ondemand::json_type::number: + { + simdjson::ondemand::number n = v.get_number(); + switch (n.get_number_type()) + { + case simdjson::ondemand::number_type::signed_integer: + st.num += static_cast(n.get_int64()); + break; + case simdjson::ondemand::number_type::unsigned_integer: + st.num += static_cast(n.get_uint64()); + break; + default: + st.num += n.get_double(); + break; + } + break; + } + case simdjson::ondemand::json_type::boolean: + (void)bool(v.get_bool()); + break; + default: + (void)v.is_null(); + break; + } +} + +#if JSON_VIEW_BENCH_BOOST +static void walk(const boost::json::value& v, stats& st) +{ + ++st.nodes; + switch (v.kind()) + { + case boost::json::kind::object: + for (const auto& kv : v.get_object()) + { + st.str += kv.key().size(); + walk(kv.value(), st); + } + break; + case boost::json::kind::array: + for (const auto& c : v.get_array()) + { + walk(c, st); + } + break; + case boost::json::kind::string: + st.str += v.get_string().size(); + break; + case boost::json::kind::int64: + st.num += static_cast(v.get_int64()); + break; + case boost::json::kind::uint64: + st.num += static_cast(v.get_uint64()); + break; + case boost::json::kind::double_: + st.num += v.get_double(); + break; + default: + break; + } +} +#endif + +// ---------------- selective access ---------------- +// twitter: per status id, user.screen_name, retweet_count +// citm: per performance id, eventId, #seatCategories; #events +// canada: type, features[0].geometry.type, #coordinates +// jeopardy: per question: round == "Final Jeopardy!", len(category) +// status (one tweet): id, user.screen_name, retweet_count +// rpc: method, params.minuend, id + +static double pick(const std::string& name, json_view r) +{ + double acc = 0; + if (name == "twitter" || name == "status") + { + auto one = [&](json_view s) + { + acc += static_cast(s["id"].get()); + acc += static_cast(s["user"]["screen_name"].get_string().size()); + acc += static_cast(s["retweet_count"].get()); + }; + if (name == "twitter") + { + for (const json_view s : r["statuses"]) + { + one(s); + } + } + else + { + one(r); + } + } + else if (name == "citm_catalog") + { + for (const json_view p : r["performances"]) + { + acc += static_cast(p["id"].get() + p["eventId"].get() + p["seatCategories"].size()); + } + acc += static_cast(r["events"].size()); + } + else if (name == "canada") + { + const json_view g = r["features"][0]["geometry"]; + acc += static_cast(r["type"].get_string().size() + g["type"].get_string().size() + g["coordinates"].size()); + } + else if (name == "jeopardy") + { + for (const json_view q : r) + { + acc += q["round"].get_string() == "Final Jeopardy!" ? 1 : 0; + acc += static_cast(q["category"].get_string().size()); + } + } + else if (name == "rpc") + { + acc += static_cast(r["method"].get_string().size()); + acc += static_cast(r["params"]["minuend"].get() + r["id"].get()); + } + return acc; +} + +static double pick(const std::string& name, const json& r) +{ + double acc = 0; + if (name == "twitter" || name == "status") + { + auto one = [&](const json & s) + { + acc += static_cast(s["id"].get()); + acc += static_cast(s["user"]["screen_name"].get_ref().size()); + acc += static_cast(s["retweet_count"].get()); + }; + if (name == "twitter") + { + for (const auto& s : r["statuses"]) + { + one(s); + } + } + else + { + one(r); + } + } + else if (name == "citm_catalog") + { + for (const auto& p : r["performances"]) + { + acc += static_cast(p["id"].get() + p["eventId"].get() + p["seatCategories"].size()); + } + acc += static_cast(r["events"].size()); + } + else if (name == "canada") + { + const json& g = r["features"][0]["geometry"]; + acc += static_cast(r["type"].get_ref().size() + g["type"].get_ref().size() + g["coordinates"].size()); + } + else if (name == "jeopardy") + { + for (const auto& q : r) + { + acc += q["round"].get_ref() == "Final Jeopardy!" ? 1 : 0; + acc += static_cast(q["category"].get_ref().size()); + } + } + else if (name == "rpc") + { + acc += static_cast(r["method"].get_ref().size()); + acc += static_cast(r["params"]["minuend"].get() + r["id"].get()); + } + return acc; +} + +static double pick(const std::string& name, yyjson_val* r) +{ + double acc = 0; + auto get = [](yyjson_val * o, const char* k) + { + return yyjson_obj_get(o, k); + }; + if (name == "twitter" || name == "status") + { + auto one = [&](yyjson_val * s) + { + acc += static_cast(yyjson_get_uint(get(s, "id"))); + acc += static_cast(yyjson_get_len(get(get(s, "user"), "screen_name"))); + acc += static_cast(yyjson_get_sint(get(s, "retweet_count"))); + }; + if (name == "twitter") + { + std::size_t idx, max; + yyjson_val* s; + yyjson_arr_foreach(get(r, "statuses"), idx, max, s) + { + one(s); + } + } + else + { + one(r); + } + } + else if (name == "citm_catalog") + { + std::size_t idx, max; + yyjson_val* p; + yyjson_arr_foreach(get(r, "performances"), idx, max, p) + { + acc += static_cast(yyjson_get_uint(get(p, "id")) + yyjson_get_uint(get(p, "eventId")) + yyjson_arr_size(get(p, "seatCategories"))); + } + acc += static_cast(yyjson_obj_size(get(r, "events"))); + } + else if (name == "canada") + { + yyjson_val* g = get(yyjson_arr_get(get(r, "features"), 0), "geometry"); + acc += static_cast(yyjson_get_len(get(r, "type")) + yyjson_get_len(get(g, "type")) + yyjson_arr_size(get(g, "coordinates"))); + } + else if (name == "jeopardy") + { + std::size_t idx, max; + yyjson_val* q; + yyjson_arr_foreach(r, idx, max, q) + { + acc += yyjson_equals_str(get(q, "round"), "Final Jeopardy!") ? 1 : 0; + acc += static_cast(yyjson_get_len(get(q, "category"))); + } + } + else if (name == "rpc") + { + acc += static_cast(yyjson_get_len(get(r, "method"))); + acc += static_cast(yyjson_get_sint(get(get(r, "params"), "minuend")) + yyjson_get_sint(get(r, "id"))); + } + return acc; +} + +static double pick(const std::string& name, simdjson::dom::element r) +{ + double acc = 0; + if (name == "twitter" || name == "status") + { + auto one = [&](simdjson::dom::element s) + { + acc += static_cast(uint64_t(s["id"])); + acc += static_cast(std::string_view(s["user"]["screen_name"]).size()); + acc += static_cast(int64_t(s["retweet_count"])); + }; + if (name == "twitter") + { + for (auto s : simdjson::dom::array(r["statuses"])) + { + one(s); + } + } + else + { + one(r); + } + } + else if (name == "citm_catalog") + { + for (auto p : simdjson::dom::array(r["performances"])) + { + acc += static_cast(uint64_t(p["id"]) + uint64_t(p["eventId"]) + simdjson::dom::array(p["seatCategories"]).size()); + } + acc += static_cast(simdjson::dom::object(r["events"]).size()); + } + else if (name == "canada") + { + auto g = r["features"].at(0)["geometry"]; + acc += static_cast(std::string_view(r["type"]).size() + std::string_view(g["type"]).size() + simdjson::dom::array(g["coordinates"]).size()); + } + else if (name == "jeopardy") + { + for (auto q : simdjson::dom::array(r)) + { + acc += std::string_view(q["round"]) == "Final Jeopardy!" ? 1 : 0; + acc += static_cast(std::string_view(q["category"]).size()); + } + } + else if (name == "rpc") + { + acc += static_cast(std::string_view(r["method"]).size()); + acc += static_cast(int64_t(r["params"]["minuend"]) + int64_t(r["id"])); + } + return acc; +} + +static double pick_od(const std::string& name, simdjson::ondemand::document& d) +{ + double acc = 0; + if (name == "twitter" || name == "status") + { + auto one = [&](simdjson::ondemand::object s) + { + acc += static_cast(uint64_t(s["id"])); + acc += static_cast(std::string_view(s["user"]["screen_name"]).size()); + acc += static_cast(int64_t(s["retweet_count"])); + }; + if (name == "twitter") + { + for (auto s : d["statuses"]) + { + one(s.get_object()); + } + } + else + { + one(d.get_object()); + } + } + else if (name == "citm_catalog") + { + simdjson::ondemand::object ev = d["events"].get_object(); + acc += static_cast(ev.count_fields()); + for (auto p : d["performances"]) + { + simdjson::ondemand::object o = p.get_object(); + const auto a = uint64_t(o["eventId"]) + uint64_t(o["id"]); + simdjson::ondemand::array sc = o["seatCategories"].get_array(); + acc += static_cast(a + sc.count_elements()); + } + } + else if (name == "canada") + { + acc += static_cast(std::string_view(d["type"]).size()); + auto g = d["features"].at(0)["geometry"]; + acc += static_cast(std::string_view(g["type"]).size()); + simdjson::ondemand::array co = g["coordinates"].get_array(); + acc += static_cast(co.count_elements()); + } + else if (name == "jeopardy") + { + for (auto q : d) + { + simdjson::ondemand::object o = q.get_object(); + acc += static_cast(std::string_view(o["category"]).size()); + acc += std::string_view(o["round"]) == "Final Jeopardy!" ? 1 : 0; + } + } + else if (name == "rpc") + { + acc += static_cast(std::string_view(d["method"]).size()); + acc += static_cast(int64_t(d["params"]["minuend"])); + acc += static_cast(int64_t(d["id"])); + } + return acc; +} + +// ---------------- harness ---------------- + +static std::string slurp(const std::string& p) +{ + std::ifstream f(p, std::ios::binary); + if (!f) + { + std::fprintf(stderr, "cannot open %s\n", p.c_str()); + std::exit(1); + } + std::stringstream ss; + ss << f.rdbuf(); + return ss.str(); +} + +struct engine +{ + std::string name; + std::function fn; +}; + +int main(int argc, char** argv) +{ + if (argc < 2) + { + std::fprintf(stderr, "usage: %s [rounds] [document]\n", argv[0]); + return 1; + } + const std::string T = std::string(argv[1]) + "/"; + // (clamped to 1 to a million) + const int rounds = argc > 2 ? static_cast((std::min)((std::max)(std::strtol(argv[2], nullptr, 10), 1L), 1000000L)) : 30; + const std::string only = argc > 3 ? argv[3] : ""; + struct doc + { + std::string name, text; + int batch; + }; + std::vector docs; + for (const char* f : + {"nativejson-benchmark/twitter.json", "nativejson-benchmark/citm_catalog.json", "nativejson-benchmark/canada.json", "jeopardy/jeopardy.json" + }) + { + std::string n = std::string(f).substr(std::string(f).find('/') + 1); + docs.push_back({n.substr(0, n.size() - 5), slurp(T + f), 1}); + } + docs.push_back({"status", json::parse(docs[0].text)["statuses"][0].dump(), 200}); + docs.push_back({"rpc", R"({"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 3})", 5000}); + + // correctness cross-check of the workloads + for (const auto& dc : docs) + { + stats a, b, c, dd; + walk(json::parse(dc.text), a); + auto d = json_document::parse(dc.text); + walk(d.root(), b); + yyjson_doc* y = yyjson_read(dc.text.data(), dc.text.size(), 0); + walk(yyjson_doc_get_root(y), c); + simdjson::dom::parser p; + walk(p.parse(dc.text).value(), dd); + const bool ok = a.nodes == b.nodes && a.nodes == c.nodes && a.nodes == dd.nodes && a.str == b.str && a.str == c.str && a.str == dd.str + && std::fabs(a.num - b.num) <= 1e-9 * std::fabs(a.num) && std::fabs(a.num - c.num) <= 1e-9 * std::fabs(a.num); + const double pa = pick(dc.name, json::parse(dc.text)), pb = pick(dc.name, d.root()), pc = pick(dc.name, yyjson_doc_get_root(y)), pd = pick(dc.name, p.parse(dc.text).value()); + std::printf("check %-13s traverse %s select %s\n", dc.name.c_str(), ok ? "OK" : "MISMATCH", (pa == pb && pa == pc && pa == pd) ? "OK" : "MISMATCH"); + yyjson_doc_free(y); + } + + std::FILE* csv = std::fopen("bench_view.csv", "w"); + if (csv == nullptr) + { + std::perror("bench_view.csv"); + return 1; + } + std::fprintf(csv, "doc,bytes,workload,engine,ns\n"); + json_document reused; + simdjson::dom::parser sj; + simdjson::ondemand::parser od; + for (const auto& dc : docs) + { + if (!only.empty() && dc.name != only) + { + continue; + } + const std::string& s = dc.text; + const simdjson::padded_string ps(s); + const std::string name = dc.name; + std::vector>> workloads; + + workloads.push_back({"parse", { + {"json_view", [&] { auto d = json_document::parse(s); g_sink = static_cast(d.node_count()); }}, + {"json_view (reused)", [&] { reused.read(s); g_sink = static_cast(reused.node_count()); }}, + {"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = static_cast(yyjson_doc_get_val_count(d)); yyjson_doc_free(d); }}, + {"simdjson DOM", [&] { auto e = sj.parse(ps).value_unsafe(); g_sink = e.is_object(); }}, + {"simdjson DOM (fresh)", [&] { simdjson::dom::parser p; auto e = p.parse(ps).value_unsafe(); g_sink = e.is_object(); }}, +#if JSON_VIEW_BENCH_BOOST + {"Boost.JSON", [&] { boost::json::monotonic_resource mr; auto v = boost::json::parse(s, &mr); g_sink = v.is_object(); }}, +#endif + {"json::parse", [&] { json j = json::parse(s); g_sink = static_cast(j.size()); }}, + }}); + workloads.push_back({"traverse", { + {"json_view", [&] { auto d = json_document::parse(s); stats st; walk(d.root(), st); g_sink = st.num; }}, + {"json_view (reused)", [&] { reused.read(s); stats st; walk(reused.root(), st); g_sink = st.num; }}, + {"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); stats st; walk(yyjson_doc_get_root(d), st); g_sink = st.num; yyjson_doc_free(d); }}, + {"simdjson DOM", [&] { stats st; walk(sj.parse(ps).value_unsafe(), st); g_sink = st.num; }}, + {"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); stats st; walk_od(d.get_value().value_unsafe(), st); g_sink = st.num; }}, +#if JSON_VIEW_BENCH_BOOST + {"Boost.JSON", [&] { boost::json::monotonic_resource mr; auto v = boost::json::parse(s, &mr); stats st; walk(v, st); g_sink = st.num; }}, +#endif + {"json::parse", [&] { json j = json::parse(s); stats st; walk(j, st); g_sink = st.num; }}, + }}); + workloads.push_back({"select", { + {"json_view", [&] { auto d = json_document::parse(s); g_sink = pick(name, d.root()); }}, + {"json_view (reused)", [&] { reused.read(s); g_sink = pick(name, reused.root()); }}, + {"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = pick(name, yyjson_doc_get_root(d)); yyjson_doc_free(d); }}, + {"simdjson DOM", [&] { g_sink = pick(name, sj.parse(ps).value_unsafe()); }}, + {"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); g_sink = pick_od(name, d); }}, + {"json::parse", [&] { json j = json::parse(s); g_sink = pick(name, j); }}, + }}); + { + // serialization of an already parsed document + static json_document vd; + vd.read(s); + static yyjson_doc* yd = nullptr; + if (yd) + { + yyjson_doc_free(yd); + } + yd = yyjson_read(s.data(), s.size(), 0); + static simdjson::dom::parser sjd; + static simdjson::dom::element se; + se = sjd.parse(ps).value_unsafe(); + static json jd; + jd = json::parse(s); + workloads.push_back({"dump", { + {"json_view", [&] { std::string o = vd.root().dump(); g_sink = static_cast(o.size()); }}, + {"yyjson", [&] { std::size_t n = 0; char* o = yyjson_write(yd, 0, &n); g_sink = static_cast(n); std::free(o); }}, + {"simdjson DOM", [&] { std::string o = simdjson::to_string(se); g_sink = static_cast(o.size()); }}, + {"json::parse", [&] { std::string o = jd.dump(); g_sink = static_cast(o.size()); }}, + }}); + } + + for (auto& wl : workloads) + { + std::vector best(wl.second.size(), 1e300); + const int r = s.size() > 10000000 ? std::max(3, rounds / 5) : rounds; + for (int i = 0; i < r; ++i) + { + for (std::size_t k = 0; k < wl.second.size(); ++k) + { + // an untimed call first: whatever the previous engine left to the allocator + // (e.g. thousands of freed json nodes) is cleaned up here, not in the timing + wl.second[k].fn(); + const auto t0 = std::chrono::steady_clock::now(); + for (int b = 0; b < dc.batch; ++b) + { + wl.second[k].fn(); + } + const double ns = std::chrono::duration(std::chrono::steady_clock::now() - t0).count() / dc.batch; + best[k] = std::min(best[k], ns); + } + } + const double ref = best[0]; + std::printf("%-13s %-9s", dc.name.c_str(), wl.first.c_str()); + for (std::size_t k = 0; k < wl.second.size(); ++k) + { + const double us = best[k] / 1e3; + std::printf(" %s %s%s (%.2fx)", wl.second[k].name.c_str(), us >= 100 ? "" : "", (us >= 1000 ? std::to_string(static_cast(us)) + "us" : (std::to_string(us).substr(0, 5) + "us")).c_str(), best[k] / ref); + std::fprintf(csv, "%s,%zu,%s,%s,%.1f\n", dc.name.c_str(), s.size(), wl.first.c_str(), wl.second[k].name.c_str(), best[k]); + } + std::printf("\n"); + std::fflush(stdout); + } + } + std::fclose(csv); +} diff --git a/tests/benchmarks/json_view/compare.py b/tests/benchmarks/json_view/compare.py new file mode 100755 index 000000000..39d5048f0 --- /dev/null +++ b/tests/benchmarks/json_view/compare.py @@ -0,0 +1,342 @@ +#!/usr/bin/env python3 +# __ _____ _____ _____ +# __| | __| | | | JSON for Modern C++ (supporting code) +# | | |__ | | | | | | version 3.12.0 +# |_____|_____|_____|_|___| https://github.com/nlohmann/json +# +# SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +# SPDX-License-Identifier: MIT + +"""Compare json_view with yyjson, simdjson, Boost.JSON, and json::parse. + +Builds bench_view.cpp, bench_corpus.cpp, and bench_edit.cpp against the include/ directory of +this checkout, runs them, and writes the results with everything needed to +reproduce them (date, commit, CPU, OS, compiler, library versions, flags) to +results/-.md and .csv next to this script. + +The other libraries come from the system (--system, the default: pkg-config +or Homebrew) or are downloaded as pinned releases and checked against their +SHA-256 (--download). Boost.JSON is optional: without Boost headers, its +columns are skipped, and the results say so. + +Only the Python 3 standard library is used; a C++17 compiler is needed. +""" + +import argparse +import datetime +import hashlib +import os +import platform +import re +import shlex +import shutil +# runs only the compilers and benchmark binaries this script builds +import subprocess # nosec B404 +import sys +import tarfile +import tempfile +import urllib.request + +HERE = os.path.dirname(os.path.abspath(__file__)) +REPO = os.path.abspath(os.path.join(HERE, '..', '..', '..')) + +# seconds without data after which a download fails +DOWNLOAD_TIMEOUT = 60 + +# pinned releases for --download; the hashes are those of the archives +# +# The yyjson and simdjson archives are the tag archives GitHub generates +# (archive/refs/tags/...), whose bytes GitHub does not guarantee to be stable. A SHA-256 +# mismatch for one of them means that GitHub regenerated the archive: check the upstream +# tag, then pin the new hash. +PINNED = { + 'yyjson': { + 'version': '0.13.0', + 'url': 'https://github.com/ibireme/yyjson/archive/refs/tags/0.13.0.tar.gz', + 'sha256': '34e0f62a2bc11ab20d601e8ca1cc2b2079503aa45119a19133d89d19b94a0fae', + 'dir': 'yyjson-0.13.0', + }, + 'simdjson': { + 'version': '4.6.11', + 'url': 'https://github.com/simdjson/simdjson/archive/refs/tags/v4.6.11.tar.gz', + 'sha256': '61d948fc24f0d793829ad658058e7597d064988a89b4607ea02e401a82df98ff', + 'dir': 'simdjson-4.6.11', + }, + 'boost': { + 'version': '1.92.0', + 'url': 'https://archives.boost.io/release/1.92.0/source/boost_1_92_0.tar.gz', + 'sha256': 'c4a3b310ddd2472416e091067166b0713be97c63f38c212c484ada022fd296ce', + 'dir': 'boost_1_92_0', + }, +} + +# the documents of bench_view.cpp, relative to the json_test_data directory +DEFAULT_CORPUS = [ + 'nativejson-benchmark/twitter.json', + 'nativejson-benchmark/citm_catalog.json', + 'nativejson-benchmark/canada.json', + 'jeopardy/jeopardy.json', +] + + +def run(cmd, **kwargs): + print('+ ' + ' '.join(shlex.quote(c) for c in cmd), flush=True) + # cmd is an argument list built by this script, never a shell string + return subprocess.run(cmd, check=True, **kwargs) # nosec B603 + + +def output(cmd): + try: + # cmd is an argument list built by this script, never a shell string + return subprocess.run(cmd, check=True, capture_output=True, text=True).stdout.strip() # nosec B603 + except (OSError, subprocess.CalledProcessError): + return '' + + +# --------------------------------------------------------------------------- +# libraries +# --------------------------------------------------------------------------- + +class Library: + """include directories, sources to compile, and linker flags of a library""" + + def __init__(self, name, include=None, sources=None, link=None, version=''): + self.name = name + self.include = include or [] + self.sources = sources or [] + self.link = link or [] + self.version = version + + +def header_version(path, pattern): + try: + with open(path, encoding='utf-8', errors='replace') as f: + m = re.search(pattern, f.read()) + return m.group(1) if m else '' + except OSError: + return '' + + +def library_version(name, include_dirs): + patterns = { + 'yyjson': ('yyjson.h', r'#define\s+YYJSON_VERSION_STRING\s+"([^"]+)"'), + 'simdjson': ('simdjson.h', r'#define\s+SIMDJSON_VERSION\s+"?([0-9.]+)"?'), + 'boost': (os.path.join('boost', 'version.hpp'), r'#define\s+BOOST_LIB_VERSION\s+"([^"]+)"'), + } + header, pattern = patterns[name] + for d in include_dirs: + v = header_version(os.path.join(d, header), pattern) + if v: + return v.replace('_', '.') + return '' + + +def system_library(name): + """a library found with pkg-config or Homebrew, or None""" + flags = output(['pkg-config', '--cflags', '--libs', name]).split() + if flags: + include = [f[2:] for f in flags if f.startswith('-I')] + link = [f for f in flags if f.startswith('-L') or f.startswith('-l')] + libdirs = [f[2:] for f in link if f.startswith('-L')] + link += ['-Wl,-rpath,' + d for d in libdirs] + return Library(name, include, [], link, library_version(name, include)) + prefix = output(['brew', '--prefix', name]) if shutil.which('brew') else '' + if prefix and os.path.isdir(os.path.join(prefix, 'include')): + include = [os.path.join(prefix, 'include')] + link = [] + if name != 'boost': + lib = os.path.join(prefix, 'lib') + link = ['-L' + lib, '-l' + name, '-Wl,-rpath,' + lib] + return Library(name, include, [], link, library_version(name, include)) + if name == 'boost': + for d in ['/usr/include', '/usr/local/include']: + if os.path.isfile(os.path.join(d, 'boost', 'json.hpp')): + return Library(name, [d], [], [], library_version(name, [d])) + return None + + +def sha256_file(path): + """SHA-256 of a file, read in chunks (the Boost archive is more than 100 MB)""" + h = hashlib.sha256() + with open(path, 'rb') as f: + for chunk in iter(lambda: f.read(1 << 20), b''): + h.update(chunk) + return h.hexdigest() + + +def download_library(name, work): + """a pinned release, downloaded and checked, or an error""" + pin = PINNED[name] + archive = os.path.join(work, 'download', os.path.basename(pin['url'])) + os.makedirs(os.path.dirname(archive), exist_ok=True) + if not os.path.isfile(archive): + print(f'downloading {pin["url"]}', flush=True) + # the URLs are the https constants in PINNED, and the SHA-256 is checked below + # (into a .part file first, so that an interrupted download is not kept) + with urllib.request.urlopen(pin['url'], timeout=DOWNLOAD_TIMEOUT) as response: # nosec B310 + with open(archive + '.part', 'wb') as f: + shutil.copyfileobj(response, f) + os.replace(archive + '.part', archive) + digest = sha256_file(archive) + if digest != pin['sha256']: + os.remove(archive) # downloaded again by the next run + sys.exit(f'error: SHA-256 of {archive} is {digest}, expected {pin["sha256"]} (removed)') + src = os.path.join(work, 'download', pin['dir']) + if not os.path.isdir(src): + # extracted into a temporary directory first, so that an interrupted extraction + # is not mistaken for a complete one + tmp = tempfile.mkdtemp(prefix=pin['dir'] + '.part-', dir=os.path.join(work, 'download')) + try: + with tarfile.open(archive) as t: + # (the 'data' filter rejects links and paths outside the target where Python has it) + kwargs = {'filter': 'data'} if hasattr(tarfile, 'data_filter') else {} + t.extractall(tmp, **kwargs) # noqa: S202 (checked archive) # nosec B202 + os.rename(os.path.join(tmp, pin['dir']), src) + finally: + shutil.rmtree(tmp, ignore_errors=True) + if name == 'yyjson': + return Library(name, [os.path.join(src, 'src')], [os.path.join(src, 'src', 'yyjson.c')], [], pin['version']) + if name == 'simdjson': + single = os.path.join(src, 'singleheader') + return Library(name, [single], [os.path.join(single, 'simdjson.cpp')], [], pin['version']) + return Library(name, [src], [], [], pin['version']) + + +# --------------------------------------------------------------------------- +# machine description +# --------------------------------------------------------------------------- + +def cpu_model(): + if sys.platform == 'darwin': + return output(['sysctl', '-n', 'machdep.cpu.brand_string']) + try: + with open('/proc/cpuinfo', encoding='utf-8') as f: + for line in f: + if line.startswith('model name') or line.startswith('Model'): + return line.split(':', 1)[1].strip() + except OSError: + pass + # (AArch64 Linux: /proc/cpuinfo has no model name, lscpu knows it) + for line in output(['lscpu']).splitlines(): + if line.startswith('Model name:'): + return line.split(':', 1)[1].strip() + return platform.processor() or platform.machine() + + +def git_commit(): + commit = output(['git', '-C', REPO, 'rev-parse', '--short=12', 'HEAD']) + dirty = output(['git', '-C', REPO, 'status', '--porcelain', '--untracked-files=no']) + return commit + (' (with local changes)' if dirty else '') + + +# --------------------------------------------------------------------------- +# main +# --------------------------------------------------------------------------- + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument('--data', required=True, help='json_test_data directory (with nativejson-benchmark/ and jeopardy/)') + ap.add_argument('--download', action='store_true', help='use pinned downloads instead of system libraries') + ap.add_argument('--no-boost', action='store_true', help='skip Boost.JSON') + ap.add_argument('--native', action='store_true', help='compile for this CPU (-march=native / -mcpu=native)') + ap.add_argument('--rounds', type=int, default=30, help='rounds of bench_view (default: 30)') + ap.add_argument('--corpus', nargs='*', default=[], help='more files for bench_corpus') + ap.add_argument('--build-dir', default=os.path.join(HERE, 'build'), help='where to build (default: build/ next to this script)') + args = ap.parse_args() + # the benchmarks run in the build directory: make the paths absolute + args.data = os.path.abspath(args.data) + args.corpus = [os.path.abspath(f) for f in args.corpus] + args.build_dir = os.path.abspath(args.build_dir) + + # (CXX and CC may contain arguments, e.g. 'ccache g++') + cxx = shlex.split(os.environ.get('CXX', 'c++')) + cc = shlex.split(os.environ.get('CC', 'cc')) + os.makedirs(args.build_dir, exist_ok=True) + + libs = {} + for name in ['yyjson', 'simdjson', 'boost']: + if name == 'boost' and args.no_boost: + continue + lib = download_library(name, args.build_dir) if args.download else system_library(name) + if lib is None and name != 'boost': + sys.exit(f'error: {name} not found; install it, or use --download') + if lib is not None: + libs[name] = lib + with_boost = 'boost' in libs + if not with_boost: + print('Boost.JSON not found: its columns are skipped', flush=True) + + flags = ['-std=c++17', '-O3', '-DNDEBUG', f'-DJSON_VIEW_BENCH_BOOST={1 if with_boost else 0}'] + if args.native: + flags.append('-mcpu=native' if platform.machine().lower() in ('arm64', 'aarch64') else '-march=native') + include = ['-I' + os.path.join(REPO, 'include')] + ['-I' + d for lib in libs.values() for d in lib.include] + link = [f for lib in libs.values() for f in lib.link] + + # C sources of downloaded libraries are compiled once + objects = [] + for lib in libs.values(): + for src in lib.sources: + obj = os.path.join(args.build_dir, os.path.basename(src) + '.o') + is_c = src.endswith('.c') + run((cc if is_c else cxx) + ([] if is_c else ['-std=c++17']) + ['-O3', '-DNDEBUG', '-c', src, '-o', obj] + + ['-I' + d for d in lib.include]) + objects.append(obj) + + binaries = {} + for bench in ['bench_view', 'bench_corpus', 'bench_edit']: + exe = os.path.join(args.build_dir, bench) + run(cxx + flags + include + [os.path.join(HERE, bench + '.cpp')] + objects + link + ['-o', exe]) + binaries[bench] = exe + + # run: bench_view on its documents, bench_corpus on those and the given files + corpus = [os.path.join(args.data, f) for f in DEFAULT_CORPUS] + args.corpus + outputs = {} + outputs['bench_view'] = run([binaries['bench_view'], args.data, str(args.rounds)], cwd=args.build_dir, + capture_output=True, text=True).stdout + outputs['bench_corpus'] = run([binaries['bench_corpus']] + corpus, cwd=args.build_dir, + capture_output=True, text=True).stdout + outputs['bench_edit'] = run([binaries['bench_edit'], args.data, str(max(1, args.rounds // 2))], cwd=args.build_dir, + capture_output=True, text=True).stdout + for name, text in outputs.items(): + print(text) + + # results with their metadata + now = datetime.datetime.now() + host = re.sub(r'[^A-Za-z0-9-]+', '-', platform.node().split('.')[0]) or 'host' + stem = os.path.join(HERE, 'results', f'{now:%Y-%m-%d}-{host}') + os.makedirs(os.path.dirname(stem), exist_ok=True) + cxx_version = output(cxx + ['--version']) + compiler_version = cxx_version.splitlines()[0] if cxx_version else ' '.join(cxx) + meta = [ + ('date', f'{now:%Y-%m-%d %H:%M}'), + ('commit', git_commit()), + ('CPU', cpu_model()), + ('OS', f'{platform.system()} {platform.release()} ({platform.machine()})'), + ('compiler', compiler_version), + ('flags', ' '.join(flags)), + ('yyjson', libs['yyjson'].version), + ('simdjson', libs['simdjson'].version), + ('Boost.JSON', libs['boost'].version if with_boost else 'skipped (not found)'), + ('libraries from', 'pinned downloads' if args.download else 'the system'), + ('rounds', str(args.rounds)), + ] + with open(stem + '.md', 'w', encoding='utf-8') as f: + f.write(f'# json_view comparison, {now:%Y-%m-%d}\n\n') + f.write('Generated by `tests/benchmarks/json_view/compare.py`; best of the interleaved rounds.\n\n') + f.write('| | |\n|---|---|\n') + for key, value in meta: + f.write(f'| {key} | {value} |\n') + for name, text in outputs.items(): + f.write(f'\n## {name}\n\n```\n{text.rstrip()}\n```\n') + with open(stem + '.csv', 'w', encoding='utf-8') as out: + out.write(''.join(f'# {key}: {value}\n' for key, value in meta)) + for name in ['bench_view', 'bench_corpus', 'bench_edit']: + path = os.path.join(args.build_dir, name + '.csv') + if os.path.isfile(path): + with open(path, encoding='utf-8') as f: + out.write(f'# {name}\n' + f.read()) + print(f'results: {stem}.md, {stem}.csv') + + +if __name__ == '__main__': + main() diff --git a/tests/benchmarks/src/benchmarks.cpp b/tests/benchmarks/src/benchmarks.cpp index 5df7dd473..0d9e06d21 100644 --- a/tests/benchmarks/src/benchmarks.cpp +++ b/tests/benchmarks/src/benchmarks.cpp @@ -8,12 +8,20 @@ #include #include +#include +#include +#include +#include #include +#include #include +#include +#include +#include #include #include -using json = nlohmann::json; +using nlohmann::json; ////////////////////////////////////////////////////////////////////////////// // parse JSON from file @@ -48,6 +56,65 @@ BENCHMARK_CAPTURE(ParseFile, signed_ints, TEST_DATA_DIRECTORY "/regression BENCHMARK_CAPTURE(ParseFile, unsigned_ints, TEST_DATA_DIRECTORY "/regression/unsigned_ints.json"); BENCHMARK_CAPTURE(ParseFile, small_signed_ints, TEST_DATA_DIRECTORY "/regression/small_signed_ints.json"); +////////////////////////////////////////////////////////////////////////////// +// parse JSON from a FILE* +////////////////////////////////////////////////////////////////////////////// + +static void ParseFILE(benchmark::State& state, const char* filename) +{ + while (state.KeepRunning()) + { + state.PauseTiming(); + std::FILE* f = std::fopen(filename, "rb"); + auto* j = new json(); + state.ResumeTiming(); + + *j = json::parse(f); + + state.PauseTiming(); + std::fclose(f); + delete j; + state.ResumeTiming(); + } + + std::ifstream file(filename, std::ios::binary | std::ios::ate); + state.SetBytesProcessed(state.iterations() * file.tellg()); +} +BENCHMARK_CAPTURE(ParseFILE, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json"); +BENCHMARK_CAPTURE(ParseFILE, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json"); +BENCHMARK_CAPTURE(ParseFILE, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json"); + +////////////////////////////////////////////////////////////////////////////// +// read many small values from one stream (NDJSON) +////////////////////////////////////////////////////////////////////////////// + +static void ParseStreamValues(benchmark::State& state, bool numbers) +{ + std::string input; + for (int i = 0; i < 10000; ++i) + { + input += numbers ? std::to_string(i * 7919) + " " + : R"({"id":)" + std::to_string(i) + R"(,"name":"user)" + std::to_string(i) + R"(","active":true,"score":)" + std::to_string(i * 0.25) + "}\n"; + } + + for (auto _ : state) + { + std::istringstream is(input); + json j; + std::size_t count = 0; + while (!(is >> std::ws).eof()) + { + is >> j; + ++count; + } + benchmark::DoNotOptimize(count); + } + + state.SetBytesProcessed(static_cast(state.iterations() * input.size())); +} +BENCHMARK_CAPTURE(ParseStreamValues, ndjson, false); +BENCHMARK_CAPTURE(ParseStreamValues, numbers, true); + ////////////////////////////////////////////////////////////////////////////// // parse JSON from string ////////////////////////////////////////////////////////////////////////////// @@ -81,6 +148,31 @@ BENCHMARK_CAPTURE(ParseString, signed_ints, TEST_DATA_DIRECTORY "/regressi BENCHMARK_CAPTURE(ParseString, unsigned_ints, TEST_DATA_DIRECTORY "/regression/unsigned_ints.json"); BENCHMARK_CAPTURE(ParseString, small_signed_ints, TEST_DATA_DIRECTORY "/regression/small_signed_ints.json"); +////////////////////////////////////////////////////////////////////////////// +// validate JSON from string +////////////////////////////////////////////////////////////////////////////// + +static void Accept(benchmark::State& state, const char* filename) +{ + std::ifstream f(filename); + std::string str((std::istreambuf_iterator(f)), std::istreambuf_iterator()); + + while (state.KeepRunning()) + { + benchmark::DoNotOptimize(json::accept(str)); + } + + state.SetBytesProcessed(state.iterations() * str.size()); +} +BENCHMARK_CAPTURE(Accept, jeopardy, TEST_DATA_DIRECTORY "/jeopardy/jeopardy.json"); +BENCHMARK_CAPTURE(Accept, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json"); +BENCHMARK_CAPTURE(Accept, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json"); +BENCHMARK_CAPTURE(Accept, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json"); +BENCHMARK_CAPTURE(Accept, floats, TEST_DATA_DIRECTORY "/regression/floats.json"); +BENCHMARK_CAPTURE(Accept, signed_ints, TEST_DATA_DIRECTORY "/regression/signed_ints.json"); +BENCHMARK_CAPTURE(Accept, unsigned_ints, TEST_DATA_DIRECTORY "/regression/unsigned_ints.json"); +BENCHMARK_CAPTURE(Accept, small_signed_ints, TEST_DATA_DIRECTORY "/regression/small_signed_ints.json"); + ////////////////////////////////////////////////////////////////////////////// // parse pretty-printed JSON from string // @@ -621,4 +713,605 @@ static void FromCborChunkedString(benchmark::State& state, const std::size_t chu BENCHMARK_CAPTURE(FromCborChunkedString, 10000 chunks, 10000); +////////////////////////////////////////////////////////////////////////////// +// SAX-parse JSON from string +// +// sax_parse() with a handler that does next to nothing runs the lexer and +// parser without building values, so the gap to ParseString above is the cost +// of building the DOM. +////////////////////////////////////////////////////////////////////////////// + +static std::string read_file(const char* filename) +{ + std::ifstream f(filename); + return std::string((std::istreambuf_iterator(f)), std::istreambuf_iterator()); +} + +static json load_json(const char* filename) +{ + return json::parse(read_file(filename)); +} + +/// SAX handler that only counts the events, so that the handler costs next to nothing +class counting_sax : public nlohmann::json_sax +{ + public: + std::size_t events = 0; + + bool null() override + { + return count(); + } + bool boolean(bool /*val*/) override + { + return count(); + } + bool number_integer(number_integer_t /*val*/) override + { + return count(); + } + bool number_unsigned(number_unsigned_t /*val*/) override + { + return count(); + } + bool number_float(number_float_t /*val*/, const string_t& /*s*/) override + { + return count(); + } + bool string(string_t& /*val*/) override + { + return count(); + } + bool binary(binary_t& /*val*/) override + { + return count(); + } + bool start_object(std::size_t /*elements*/) override + { + return count(); + } + bool key(string_t& /*val*/) override + { + return count(); + } + bool end_object() override + { + return count(); + } + bool start_array(std::size_t /*elements*/) override + { + return count(); + } + bool end_array() override + { + return count(); + } + bool parse_error(std::size_t /*position*/, const std::string& /*last_token*/, const json::exception& /*ex*/) override + { + return false; + } + + private: + bool count() + { + ++events; + return true; + } +}; + +static void SaxParse(benchmark::State& state, const char* filename) +{ + const std::string str = read_file(filename); + + for (auto _ : state) + { + counting_sax sax; + bool result = json::sax_parse(str, &sax); + benchmark::DoNotOptimize(result); + benchmark::DoNotOptimize(sax.events); + } + + state.SetBytesProcessed(state.iterations() * str.size()); +} +BENCHMARK_CAPTURE(SaxParse, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json"); +BENCHMARK_CAPTURE(SaxParse, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json"); +BENCHMARK_CAPTURE(SaxParse, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json"); + +////////////////////////////////////////////////////////////////////////////// +// work with values: copy, destroy, compare, iterate +// +// The parsed test files stand in for a document an application keeps around. +// Each benchmark times one whole-document operation; setup and cleanup run +// outside the timed section. +// +// A copy is always destroyed again, and how a document is freed shapes the +// heap the next copy allocates from: a destructor that frees quickly can leave +// work to the allocator's next calls. Copy, CopyAssign and CopyRing therefore +// time the copy together with the destruction of a copy; CopyOnly and +// DestroyOnly time one half each and are meant for telling the halves apart, +// not for comparing versions. +////////////////////////////////////////////////////////////////////////////// + +/// copy a document and destroy the copy again, as a temporary copy is used +static void Copy(benchmark::State& state, const char* filename) +{ + const json j = load_json(filename); + + for (auto _ : state) + { + json copy = j; + benchmark::DoNotOptimize(copy); + } +} +BENCHMARK_CAPTURE(Copy, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json"); +BENCHMARK_CAPTURE(Copy, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json"); +BENCHMARK_CAPTURE(Copy, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json"); + +/// copy-assign a document over an earlier copy of it, which is freed in the process +static void CopyAssign(benchmark::State& state, const char* filename) +{ + const json j = load_json(filename); + json target = j; + + for (auto _ : state) + { + target = j; + benchmark::DoNotOptimize(target); + } +} +BENCHMARK_CAPTURE(CopyAssign, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json"); +BENCHMARK_CAPTURE(CopyAssign, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json"); +BENCHMARK_CAPTURE(CopyAssign, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json"); + +/// keep eight copies alive and replace the oldest one, as a cache or an undo +/// history does, so that the copy is not made from memory just freed by an +/// identical document +static void CopyRing(benchmark::State& state, const char* filename) +{ + const json j = load_json(filename); + std::vector ring(8, j); + std::size_t oldest = 0; + + for (auto _ : state) + { + ring[oldest] = j; + benchmark::DoNotOptimize(ring[oldest]); + oldest = (oldest + 1) % ring.size(); + } +} +BENCHMARK_CAPTURE(CopyRing, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json"); +BENCHMARK_CAPTURE(CopyRing, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json"); +BENCHMARK_CAPTURE(CopyRing, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json"); + +/// only the copy; the copy is destroyed with the timer paused (diagnostic) +static void CopyOnly(benchmark::State& state, const char* filename) +{ + const json j = load_json(filename); + + for (auto _ : state) + { + auto* copy = new json(j); + + state.PauseTiming(); + delete copy; + state.ResumeTiming(); + } +} +BENCHMARK_CAPTURE(CopyOnly, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json"); +BENCHMARK_CAPTURE(CopyOnly, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json"); +BENCHMARK_CAPTURE(CopyOnly, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json"); + +/// only the destruction; the copy is made with the timer paused (diagnostic) +static void DestroyOnly(benchmark::State& state, const char* filename) +{ + const json j = load_json(filename); + + for (auto _ : state) + { + state.PauseTiming(); + auto* copy = new json(j); + state.ResumeTiming(); + + delete copy; + } +} +BENCHMARK_CAPTURE(DestroyOnly, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json"); +BENCHMARK_CAPTURE(DestroyOnly, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json"); +BENCHMARK_CAPTURE(DestroyOnly, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json"); + +static void Equal(benchmark::State& state, const char* filename) +{ + // two separate values, so that the comparison cannot stop at identical addresses + const json j = load_json(filename); + const json copy = j; + + for (auto _ : state) + { + bool result = (j == copy); + benchmark::DoNotOptimize(result); + } +} +BENCHMARK_CAPTURE(Equal, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json"); +BENCHMARK_CAPTURE(Equal, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json"); +BENCHMARK_CAPTURE(Equal, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json"); + +/// visit every value with range-based for loops, as client code walks a document +static std::size_t count_values(const json& j) +{ + std::size_t result = 1; + if (j.is_structured()) + { + for (const auto& element : j) + { + result += count_values(element); + } + } + return result; +} + +static void Iterate(benchmark::State& state, const char* filename) +{ + const json j = load_json(filename); + + for (auto _ : state) + { + std::size_t result = count_values(j); + benchmark::DoNotOptimize(result); + } + + state.SetItemsProcessed(state.iterations() * static_cast(count_values(j))); +} +BENCHMARK_CAPTURE(Iterate, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json"); +BENCHMARK_CAPTURE(Iterate, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json"); +BENCHMARK_CAPTURE(Iterate, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json"); + +////////////////////////////////////////////////////////////////////////////// +// flatten and unflatten +////////////////////////////////////////////////////////////////////////////// + +static void Flatten(benchmark::State& state, const char* filename) +{ + const json j = load_json(filename); + + for (auto _ : state) + { + json flat = j.flatten(); + benchmark::DoNotOptimize(flat); + } +} +BENCHMARK_CAPTURE(Flatten, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json"); +BENCHMARK_CAPTURE(Flatten, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json"); +BENCHMARK_CAPTURE(Flatten, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json"); + +// ordered_json objects look keys up linearly, which flatten() used to do for +// every value; canada (330,000 values in one flat object) is left out, as it +// would take minutes with such versions +static void FlattenOrdered(benchmark::State& state, const char* filename) +{ + const nlohmann::ordered_json j = nlohmann::ordered_json::parse(read_file(filename)); + + for (auto _ : state) + { + nlohmann::ordered_json flat = j.flatten(); + benchmark::DoNotOptimize(flat); + } +} +BENCHMARK_CAPTURE(FlattenOrdered, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json"); +BENCHMARK_CAPTURE(FlattenOrdered, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json"); + +static void Unflatten(benchmark::State& state, const char* filename) +{ + const json flat = load_json(filename).flatten(); + + for (auto _ : state) + { + json j = flat.unflatten(); + benchmark::DoNotOptimize(j); + } +} +BENCHMARK_CAPTURE(Unflatten, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json"); +BENCHMARK_CAPTURE(Unflatten, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json"); +BENCHMARK_CAPTURE(Unflatten, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json"); + +////////////////////////////////////////////////////////////////////////////// +// diff and patch +// +// The target is the document with every eighth scalar changed, so the patch +// has one "replace" operation per changed value and the structure stays the +// same. +////////////////////////////////////////////////////////////////////////////// + +static void change_every_eighth_scalar(json& j, std::size_t& counter) +{ + if (j.is_structured()) + { + for (auto& element : j) + { + change_every_eighth_scalar(element, counter); + } + return; + } + + if (++counter % 8 != 0) + { + return; + } + + switch (j.type()) + { + case json::value_t::number_integer: + j = j.get() + 1; + break; + case json::value_t::number_unsigned: + j = j.get() + 1; + break; + case json::value_t::number_float: + j = j.get() + 0.5; + break; + case json::value_t::string: + j = j.get() + "!"; + break; + case json::value_t::boolean: + j = !j.get(); + break; + case json::value_t::null: + case json::value_t::object: + case json::value_t::array: + case json::value_t::binary: + case json::value_t::discarded: + default: + j = 0; + break; + } +} + +static json changed_copy(const json& j) +{ + json result = j; + std::size_t counter = 0; + change_every_eighth_scalar(result, counter); + return result; +} + +static void Diff(benchmark::State& state, const char* filename) +{ + const json source = load_json(filename); + const json target = changed_copy(source); + + for (auto _ : state) + { + json patch = json::diff(source, target); + benchmark::DoNotOptimize(patch); + } + + state.SetItemsProcessed(state.iterations() * static_cast(json::diff(source, target).size())); +} +BENCHMARK_CAPTURE(Diff, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json"); +BENCHMARK_CAPTURE(Diff, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json"); +BENCHMARK_CAPTURE(Diff, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json"); + +static void Patch(benchmark::State& state, const char* filename) +{ + const json source = load_json(filename); + const json patch = json::diff(source, changed_copy(source)); + + for (auto _ : state) + { + json result = source.patch(patch); + benchmark::DoNotOptimize(result); + } + + state.SetItemsProcessed(state.iterations() * static_cast(patch.size())); +} +BENCHMARK_CAPTURE(Patch, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json"); +BENCHMARK_CAPTURE(Patch, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json"); +BENCHMARK_CAPTURE(Patch, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json"); + +////////////////////////////////////////////////////////////////////////////// +// JSON Pointer +// +// The pointers are those of every scalar in the document, i.e., the keys of +// its flattened form. +////////////////////////////////////////////////////////////////////////////// + +static std::vector pointer_strings(const json& j) +{ + std::vector result; + const json flat = j.flatten(); + result.reserve(flat.size()); + for (auto it = flat.begin(); it != flat.end(); ++it) + { + result.push_back(it.key()); + } + return result; +} + +static std::vector pointers(const json& j) +{ + const std::vector strings = pointer_strings(j); + std::vector result; + result.reserve(strings.size()); + for (const auto& s : strings) + { + result.emplace_back(s); + } + return result; +} + +static void JsonPointerParse(benchmark::State& state, const char* filename) +{ + const std::vector strings = pointer_strings(load_json(filename)); + + for (auto _ : state) + { + for (const auto& s : strings) + { + json::json_pointer ptr(s); + benchmark::DoNotOptimize(ptr); + } + } + + state.SetItemsProcessed(state.iterations() * static_cast(strings.size())); +} +BENCHMARK_CAPTURE(JsonPointerParse, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json"); +BENCHMARK_CAPTURE(JsonPointerParse, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json"); + +static void JsonPointerToString(benchmark::State& state, const char* filename) +{ + const std::vector ptrs = pointers(load_json(filename)); + + for (auto _ : state) + { + for (const auto& ptr : ptrs) + { + std::string s = ptr.to_string(); + benchmark::DoNotOptimize(s); + } + } + + state.SetItemsProcessed(state.iterations() * static_cast(ptrs.size())); +} +BENCHMARK_CAPTURE(JsonPointerToString, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json"); +BENCHMARK_CAPTURE(JsonPointerToString, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json"); + +static void JsonPointerAt(benchmark::State& state, const char* filename) +{ + const json j = load_json(filename); + const std::vector ptrs = pointers(j); + + for (auto _ : state) + { + for (const auto& ptr : ptrs) + { + const json& value = j.at(ptr); + benchmark::DoNotOptimize(&value); + } + } + + state.SetItemsProcessed(state.iterations() * static_cast(ptrs.size())); +} +BENCHMARK_CAPTURE(JsonPointerAt, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json"); +BENCHMARK_CAPTURE(JsonPointerAt, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json"); + +static void JsonPointerContains(benchmark::State& state, const char* filename) +{ + const json j = load_json(filename); + const std::vector ptrs = pointers(j); + + for (auto _ : state) + { + for (const auto& ptr : ptrs) + { + bool result = j.contains(ptr); + benchmark::DoNotOptimize(result); + } + } + + state.SetItemsProcessed(state.iterations() * static_cast(ptrs.size())); +} +BENCHMARK_CAPTURE(JsonPointerContains, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json"); +BENCHMARK_CAPTURE(JsonPointerContains, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json"); + +////////////////////////////////////////////////////////////////////////////// +// conversions from and to C++ containers +////////////////////////////////////////////////////////////////////////////// + +static void GetVectorInt(benchmark::State& state, const char* filename) +{ + const json j = load_json(filename); + + for (auto _ : state) + { + auto v = j.get>(); + benchmark::DoNotOptimize(v); + } + + state.SetItemsProcessed(state.iterations() * static_cast(j.size())); +} +BENCHMARK_CAPTURE(GetVectorInt, signed_ints, TEST_DATA_DIRECTORY "/regression/signed_ints.json"); + +static void GetVectorDouble(benchmark::State& state, const char* filename) +{ + const json j = load_json(filename); + + for (auto _ : state) + { + auto v = j.get>(); + benchmark::DoNotOptimize(v); + } + + state.SetItemsProcessed(state.iterations() * static_cast(j.size())); +} +BENCHMARK_CAPTURE(GetVectorDouble, floats, TEST_DATA_DIRECTORY "/regression/floats.json"); + +// a large flat object: the flattened document, one member per scalar +static void GetMap(benchmark::State& state, const char* filename) +{ + const json j = load_json(filename).flatten(); + + for (auto _ : state) + { + auto m = j.get>(); + benchmark::DoNotOptimize(m); + } + + state.SetItemsProcessed(state.iterations() * static_cast(j.size())); +} +BENCHMARK_CAPTURE(GetMap, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json"); + +/// 100,000 strings, each too long for the small string optimization +static std::vector make_strings() +{ + std::vector result; + result.reserve(100000); + for (std::size_t i = 0; i < 100000; ++i) + { + result.push_back("a string that does not fit in place: " + std::to_string(i)); + } + return result; +} + +static void ToJsonStrings(benchmark::State& state, const bool move) +{ + const std::vector strings = make_strings(); + + for (auto _ : state) + { + state.PauseTiming(); + std::vector input = strings; + state.ResumeTiming(); + + // destroyed inside the timed section, like the copies above + auto* j = move ? new json(std::move(input)) : new json(input); + delete j; + } + + state.SetItemsProcessed(state.iterations() * static_cast(strings.size())); +} +BENCHMARK_CAPTURE(ToJsonStrings, copy, false); +BENCHMARK_CAPTURE(ToJsonStrings, move, true); + +// conversion of a JSON array into standard containers +template +static void GetArray(benchmark::State& state) +{ + json j = json::array(); + for (int i = 0; i < 1000; ++i) + { + j.push_back(i); + } + + for (auto _ : state) + { + auto c = j.get(); + benchmark::DoNotOptimize(c); + } + state.SetItemsProcessed(state.iterations() * 1000); +} + +BENCHMARK_TEMPLATE(GetArray, std::vector); +BENCHMARK_TEMPLATE(GetArray, std::deque); +BENCHMARK_TEMPLATE(GetArray, std::set); +BENCHMARK_TEMPLATE(GetArray, std::array); + BENCHMARK_MAIN(); diff --git a/tests/benchmarks/src/benchmarks_view.cpp b/tests/benchmarks/src/benchmarks_view.cpp new file mode 100644 index 000000000..02920ffab --- /dev/null +++ b/tests/benchmarks/src/benchmarks_view.cpp @@ -0,0 +1,180 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// Benchmarks of json_document (nlohmann/json_view.hpp). They use the inputs of +// ParseString and ParseIndented in benchmarks.cpp, so that each ViewParse row +// can be read against the json::parse row of the same file. + +#include +#include +#include +#include +#include +#include + +using nlohmann::json; +using nlohmann::json_document; + +static std::string read_file(const char* filename) +{ + std::ifstream f(filename, std::ios::binary); + return std::string((std::istreambuf_iterator(f)), std::istreambuf_iterator()); +} + +#define JSON_VIEW_BENCHMARK_FILES(fn) \ + BENCHMARK_CAPTURE(fn, jeopardy, TEST_DATA_DIRECTORY "/jeopardy/jeopardy.json"); \ + BENCHMARK_CAPTURE(fn, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json"); \ + BENCHMARK_CAPTURE(fn, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json"); \ + BENCHMARK_CAPTURE(fn, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json"); \ + BENCHMARK_CAPTURE(fn, floats, TEST_DATA_DIRECTORY "/regression/floats.json"); \ + BENCHMARK_CAPTURE(fn, signed_ints, TEST_DATA_DIRECTORY "/regression/signed_ints.json"); \ + BENCHMARK_CAPTURE(fn, unsigned_ints, TEST_DATA_DIRECTORY "/regression/unsigned_ints.json"); \ + BENCHMARK_CAPTURE(fn, small_signed_ints, TEST_DATA_DIRECTORY "/regression/small_signed_ints.json") + +////////////////////////////////////////////////////////////////////////////// +// parse into a new document (compare with ParseString) +////////////////////////////////////////////////////////////////////////////// + +static void ViewParse(benchmark::State& state, const char* filename) +{ + const std::string str = read_file(filename); + + while (state.KeepRunning()) + { + state.PauseTiming(); + auto* d = new json_document(); + state.ResumeTiming(); + + *d = json_document::parse(str); + + state.PauseTiming(); + delete d; + state.ResumeTiming(); + } + + state.SetBytesProcessed(state.iterations() * str.size()); +} +JSON_VIEW_BENCHMARK_FILES(ViewParse); + +////////////////////////////////////////////////////////////////////////////// +// parse into a document that is reused (its memory stays allocated) +////////////////////////////////////////////////////////////////////////////// + +static void ViewRead(benchmark::State& state, const char* filename) +{ + const std::string str = read_file(filename); + json_document d; + + while (state.KeepRunning()) + { + d.read(str); + benchmark::DoNotOptimize(d); + } + + state.SetBytesProcessed(state.iterations() * str.size()); +} +JSON_VIEW_BENCHMARK_FILES(ViewRead); + +////////////////////////////////////////////////////////////////////////////// +// parse pretty-printed JSON (compare with ParseIndented) +////////////////////////////////////////////////////////////////////////////// + +static void ViewParseIndented(benchmark::State& state, const char* filename, int indent) +{ + const std::string indented = json::parse(read_file(filename)).dump(indent); + json_document d; + + while (state.KeepRunning()) + { + d.read(indented); + benchmark::DoNotOptimize(d); + } + + state.SetBytesProcessed(state.iterations() * indented.size()); +} +BENCHMARK_CAPTURE(ViewParseIndented, jeopardy / 4, TEST_DATA_DIRECTORY "/jeopardy/jeopardy.json", 4); +BENCHMARK_CAPTURE(ViewParseIndented, canada / 4, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json", 4); +BENCHMARK_CAPTURE(ViewParseIndented, citm_catalog / 4, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json", 4); +BENCHMARK_CAPTURE(ViewParseIndented, twitter / 4, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json", 4); + +////////////////////////////////////////////////////////////////////////////// +// validate only (compare with Accept) +////////////////////////////////////////////////////////////////////////////// + +static void ViewAccept(benchmark::State& state, const char* filename) +{ + const std::string str = read_file(filename); + + while (state.KeepRunning()) + { + benchmark::DoNotOptimize(json_document::accept(str)); + } + + state.SetBytesProcessed(state.iterations() * str.size()); +} +JSON_VIEW_BENCHMARK_FILES(ViewAccept); + +////////////////////////////////////////////////////////////////////////////// +// convert a parsed document into a json value +////////////////////////////////////////////////////////////////////////////// + +static void ViewMaterialize(benchmark::State& state, const char* filename) +{ + const std::string str = read_file(filename); + const json_document d = json_document::parse(str); + + while (state.KeepRunning()) + { + state.PauseTiming(); + auto* j = new json(); + state.ResumeTiming(); + + *j = d.root().materialize(); + + state.PauseTiming(); + delete j; + state.ResumeTiming(); + } + + state.SetBytesProcessed(state.iterations() * str.size()); +} +JSON_VIEW_BENCHMARK_FILES(ViewMaterialize); + +////////////////////////////////////////////////////////////////////////////// +// serialize a parsed document (compare with Dump) +////////////////////////////////////////////////////////////////////////////// + +static void ViewDump(benchmark::State& state, const char* filename, int indent) +{ + const std::string str = read_file(filename); + const json_document d = json_document::parse(str); + + while (state.KeepRunning()) + { + std::string output = d.root().dump(indent); + benchmark::DoNotOptimize(output); + } + + state.SetBytesProcessed(state.iterations() * d.root().dump(indent).size()); +} +BENCHMARK_CAPTURE(ViewDump, jeopardy / -, TEST_DATA_DIRECTORY "/jeopardy/jeopardy.json", -1); +BENCHMARK_CAPTURE(ViewDump, jeopardy / 4, TEST_DATA_DIRECTORY "/jeopardy/jeopardy.json", 4); +BENCHMARK_CAPTURE(ViewDump, canada / -, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json", -1); +BENCHMARK_CAPTURE(ViewDump, canada / 4, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json", 4); +BENCHMARK_CAPTURE(ViewDump, citm_catalog / -, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json", -1); +BENCHMARK_CAPTURE(ViewDump, citm_catalog / 4, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json", 4); +BENCHMARK_CAPTURE(ViewDump, twitter / -, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json", -1); +BENCHMARK_CAPTURE(ViewDump, twitter / 4, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json", 4); +BENCHMARK_CAPTURE(ViewDump, floats / -, TEST_DATA_DIRECTORY "/regression/floats.json", -1); +BENCHMARK_CAPTURE(ViewDump, floats / 4, TEST_DATA_DIRECTORY "/regression/floats.json", 4); +BENCHMARK_CAPTURE(ViewDump, signed_ints / -, TEST_DATA_DIRECTORY "/regression/signed_ints.json", -1); +BENCHMARK_CAPTURE(ViewDump, signed_ints / 4, TEST_DATA_DIRECTORY "/regression/signed_ints.json", 4); +BENCHMARK_CAPTURE(ViewDump, unsigned_ints / -, TEST_DATA_DIRECTORY "/regression/unsigned_ints.json", -1); +BENCHMARK_CAPTURE(ViewDump, unsigned_ints / 4, TEST_DATA_DIRECTORY "/regression/unsigned_ints.json", 4); +BENCHMARK_CAPTURE(ViewDump, small_signed_ints / -, TEST_DATA_DIRECTORY "/regression/small_signed_ints.json", -1); +BENCHMARK_CAPTURE(ViewDump, small_signed_ints / 4, TEST_DATA_DIRECTORY "/regression/small_signed_ints.json", 4); diff --git a/tests/fuzzing.md b/tests/fuzzing.md index 329cdbf58..2399bdfd2 100644 --- a/tests/fuzzing.md +++ b/tests/fuzzing.md @@ -3,13 +3,37 @@ Each parser of the library (JSON, BJData, BON8, BSON, CBOR, MessagePack, and UBJSON) can be fuzz tested. Currently, [libFuzzer](https://llvm.org/docs/LibFuzzer.html) and [afl++](https://github.com/AFLplusplus/AFLplusplus) are supported. +Additionally, `parse_json_view_fuzzer` (`tests/src/fuzzer-parse_json_view.cpp`) cross-checks `json_document`/`json_view` +(the zero-copy, read-only view declared in `json_view.hpp`) against `basic_json` on the same JSON text: it asserts that +`json_document::accept` agrees with `json::accept`, that an accepted input materializes to the same value `json::parse` +produces, and that a rejected input makes both parsers throw with an identical `what()`. It checks this for a +`std::string` input (borrowed, with a NUL after the last byte) and for an exact-size `std::vector` +(borrowed, with nothing after the last byte), and for the `ignore_comments` and `ignore_trailing_commas` options, which +are taken from the low bits of the first input byte (the byte stays part of the text). It takes plain JSON text, so it +reuses the `corpus_json` corpus rather than a format of its own. + +`json_view_image_fuzzer` (`tests/src/fuzzer-json_view_image.cpp`) tests the images of `json_document` (`save()` and +`load()`). It uses each input twice: as an image, which `load()` must either reject with `parse_error.116` or read +safely (with `image_check::full`, the document must also serialize to the JSON it reads as), and as a JSON text, whose +image must load and serialize to the same text. A corpus of images can be made from JSON files with a small program +that calls `json_document::parse(text).save()`; plain JSON files work as well. + ## What the fuzzers check Each fuzzer driver (`tests/src/fuzzer-parse_*.cpp`) parses its input twice: once with `allow_exceptions = false` and once with exceptions. Both calls must agree. Where parsing with exceptions fails, the call without exceptions must return a discarded value (or throw the same kind of non-parse error), and it must never throw a `parse_error`. Where parsing succeeds, both calls must return the same value. The drivers then serialize the value, parse the result back, -and check that nothing was lost. The drivers check all of this with `assert`, so they refuse to build with `NDEBUG`. +and check that nothing was lost. + +Finally, each driver reads the same input through the other input adapters: a `std::istringstream`, a non-contiguous +iterator range (`std::list`), and, on POSIX systems, a `FILE*` from `fmemopen`. The pointer range takes the +fast paths for contiguous input (bulk string scanning, multi-byte loads, lazy position tracking), while the other +adapters read byte by byte. Every adapter must give the same value, or the same exception with the same id, message, +and byte position. The JSON driver checks this for `parse` and `accept`, each with and without `ignore_comments`. The +shared code is `check_input_adapters()` in `tests/src/fuzzer_common.hpp`. + +The drivers check all of this with `assert`, so they refuse to build with `NDEBUG`. ## Corpus creation diff --git a/tests/iwyu_consumer/conversions.cpp b/tests/iwyu_consumer/conversions.cpp new file mode 100644 index 000000000..599da67b7 --- /dev/null +++ b/tests/iwyu_consumer/conversions.cpp @@ -0,0 +1,62 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// See values.cpp: the conversion macros and nlohmann::adl_serializer must be available through +// alone. NLOHMANN_JSON_SERIALIZE_ENUM (from the same header as +// NLOHMANN_DEFINE_TYPE_INTRUSIVE) is left out: IWYU attributes the basic_json comparison in its +// expansion to an unrelated standard header, the ambiguity described for json.hpp in cmake/ci.cmake. + +#include + +#include + +namespace ns +{ +struct person +{ + std::string name; + int age; + NLOHMANN_DEFINE_TYPE_INTRUSIVE(person, name, age) +}; + +struct legacy +{ + int value; +}; +} // namespace ns + +namespace nlohmann +{ +template<> +struct adl_serializer +{ + static void to_json(json& j, const ns::legacy& l) + { + j = l.value; + } + + static void from_json(const json& j, ns::legacy& l) + { + l.value = j.get(); + } +}; +} // namespace nlohmann + +int main() +{ + ns::person ada; + ada.name = "Ada"; + ada.age = 36; + ns::legacy one; + one.value = 1; + + const nlohmann::json j = ada; + const auto p = j.get(); + const auto l = nlohmann::json(one).get(); + return p.age - 36 + l.value - 1; +} diff --git a/tests/iwyu_consumer/literals.cpp b/tests/iwyu_consumer/literals.cpp new file mode 100644 index 000000000..7aaf58d79 --- /dev/null +++ b/tests/iwyu_consumer/literals.cpp @@ -0,0 +1,19 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// See values.cpp: the user-defined literals must be available through alone. + +#include + +using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) + +int main() +{ + const auto j = R"({"a": [1, 2]})"_json; + return j.at("/a/1"_json_pointer).get() - 2; +} diff --git a/tests/iwyu_consumer/values.cpp b/tests/iwyu_consumer/values.cpp new file mode 100644 index 000000000..1f6d00b76 --- /dev/null +++ b/tests/iwyu_consumer/values.cpp @@ -0,0 +1,30 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// Uses the library the way a client does, including only . ci_single_binaries +// runs include-what-you-use and clang-tidy's misc-include-cleaner on this file and fails if either +// asks for another nlohmann header, which would mean json.hpp does not export it (issue #5812). + +#include +#include +#include + +#include + +int main() +{ + // brace initialization (nlohmann::json, detail::json_ref) + nlohmann::json j = {{"pi", 3.141}, {"list", {1, 0, 2}}}; + const nlohmann::ordered_json o = {{"b", 1}, {"a", 2}}; + + j["parsed"] = nlohmann::json::parse(R"({"happy": true})"); + j["binary"] = nlohmann::json::binary(std::vector {0xCA, 0xFE}, 42); + + const std::string s = j.dump() + o.dump(); + return s.empty() ? 1 : 0; +} diff --git a/tests/module_cpp20/main.cpp b/tests/module_cpp20/main.cpp index a844e657d..4e492cef9 100644 --- a/tests/module_cpp20/main.cpp +++ b/tests/module_cpp20/main.cpp @@ -52,8 +52,13 @@ int main() std::ostringstream os; os << j << oj << lit; + // json_document / json_view (json_view.hpp): zero-copy parse of a literal + const nlohmann::json_document doc = nlohmann::json_document::parse(R"({"a": 1, "list": [1, 2, 3]})"); + const std::size_t doc_size = doc.root().size(); + // use every result so the references cannot be optimized away return (a == 1 && last == 3 && b == 2 && lit.size() == 3 - && m.size() == 1 && !dumped.empty() && !os.str().empty()) + && m.size() == 1 && !dumped.empty() && !os.str().empty() + && doc_size == 2) ? 0 : 1; } diff --git a/tests/src/bjdata_round_trip.hpp b/tests/src/bjdata_round_trip.hpp new file mode 100644 index 000000000..4fdde833b --- /dev/null +++ b/tests/src/bjdata_round_trip.hpp @@ -0,0 +1,103 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // uint8_t +#include // vector + +#include "doctest_compatibility.h" + +#include + +#include "round_trip_corpus.hpp" + +// The BJData round-trip invariants, split by the BJData version that turns a +// corpus value into j1 (unit-bjdata_round_trip_draft2.cpp and +// unit-bjdata_round_trip_draft3.cpp). They were part of unit-bjdata.cpp, but +// took 80 % of its time; as separate tests, both halves run in parallel and +// stay below the ctest timeout under Valgrind. +namespace utils +{ + +// This checks what the parse_bjdata_fuzzer driver checks (see +// tests/src/fuzzer-parse_bjdata.cpp), so that a regression shows up in CI +// rather than as an OSS-Fuzz report: every value from_bjdata() returns +// (j1) can be serialized with any combination of options, the result can +// be parsed back (j2), and serializing j2 again with the same options +// yields a value-equal result. +// +// Beyond the driver, this also checks that j2 equals j1 and that +// serializing j2 reproduces the exact bytes, both except for values that +// contain a binary value: a binary value is only written as a binary +// value with Draft 3's optimized binary array, and otherwise read back as +// an array of integers, for which the writer may choose different (but +// equally valid) type markers when it is serialized again (see #5494). +// +// Values are compared with dump() rather than operator==, because a NaN +// never compares equal to itself. +// +// Only the corpus values written with @a initial_version become j1; each j1 +// is then serialized with every combination of options, both versions +// included. +inline void check_bjdata_round_trip(const nlohmann::json::bjdata_version_t initial_version) +{ + using nlohmann::json; + + struct options + { + bool use_size; + bool use_type; + json::bjdata_version_t version; + }; + const std::vector all_options = + { + {false, false, json::bjdata_version_t::draft2}, + {true, false, json::bjdata_version_t::draft2}, + {true, true, json::bjdata_version_t::draft2}, + {false, false, json::bjdata_version_t::draft3}, + {true, false, json::bjdata_version_t::draft3}, + {true, true, json::bjdata_version_t::draft3}, + }; + + for (const auto& j0 : utils::round_trip_corpus::values()) + { + // turn the corpus value into a value as from_bjdata() returns it + for (const auto& initial : all_options) + { + if (initial.version != initial_version) + { + continue; + } + + const json j1 = json::from_bjdata(json::to_bjdata(j0, initial.use_size, initial.use_type, initial.version)); + const bool has_binary = utils::round_trip_corpus::contains_binary(j1); + + for (const auto& o : all_options) + { + INFO("j1 = " << j1.dump() << ", use_size = " << o.use_size << ", use_type = " << o.use_type + << ", draft3 = " << (o.version == json::bjdata_version_t::draft3)); + + const std::vector vec = json::to_bjdata(j1, o.use_size, o.use_type, o.version); + json j2; + // anything the library writes must be parsable by the library + REQUIRE_NOTHROW(j2 = json::from_bjdata(vec)); + const std::vector vec2 = json::to_bjdata(j2, o.use_size, o.use_type, o.version); + CHECK(json::from_bjdata(vec2).dump() == j2.dump()); + + if (!has_binary) + { + CHECK(j2.dump() == j1.dump()); + CHECK(vec2 == vec); + } + } + } + } +} + +} // namespace utils diff --git a/tests/src/custom_key_test.hpp b/tests/src/custom_key_test.hpp new file mode 100644 index 000000000..224517b0f --- /dev/null +++ b/tests/src/custom_key_test.hpp @@ -0,0 +1,519 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include "doctest_compatibility.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +// Object types with a user-defined key type. The key types differ in what they +// offer to the library: a conversion to std::string (implicit or explicit), a +// comparison with ==, a to_json overload, or a c_str() member. + +namespace custom_key_test +{ +class key_base +{ + public: + key_base() = default; + + key_base(const char* value) + : m_value(value) + {} + + key_base(std::string value) + : m_value(std::move(value)) + {} + + // 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(); + } + + friend bool operator<(const key_base& lhs, const key_base& rhs) + { + return lhs.m_value < rhs.m_value; + } + + protected: + const std::string& str() const noexcept + { + return m_value; + } + + private: + std::string m_value; +}; + +// implicit conversion to std::string and operator== +class key_full : public key_base +{ + public: + key_full() = default; + using key_base::key_base; + + operator std::string() const + { + return str(); + } + + friend bool operator==(const key_full& lhs, const key_full& rhs) + { + return lhs.str() == rhs.str(); + } +}; + +// implicit conversion to std::string, but no operator== +class key_no_eq : public key_base +{ + public: + key_no_eq() = default; + using key_base::key_base; + + operator std::string() const + { + return str(); + } +}; + +// explicit conversion to std::string, no operator== +class key_explicit : public key_base +{ + public: + key_explicit() = default; + using key_base::key_base; + + explicit operator std::string() const + { + return str(); + } +}; + +// no conversion at all, only a to_json overload, no operator== +class key_to_json : public key_base +{ + public: + key_to_json() = default; + using key_base::key_base; + + const std::string& value() const + { + return str(); + } +}; + +template +void to_json(BasicJsonType& j, const key_to_json& k) +{ + j = k.value(); +} + +// like key_to_json, but with size() and c_str() +class key_c_str : public key_base +{ + public: + key_c_str() = default; + using key_base::key_base; + + const std::string& value() const + { + return str(); + } + + std::size_t size() const + { + return str().size(); + } + + const char* c_str() const + { + return str().c_str(); + } +}; + +template +void to_json(BasicJsonType& j, const key_c_str& k) +{ + j = k.value(); +} + +// std::map with key type K, ignoring the key type basic_json passes +template +struct object_for +{ + template + using pair_allocator = typename std::allocator_traits::template rebind_alloc>; + + template + using type = std::map, pair_allocator>; // NOLINT(modernize-use-transparent-functors) +}; + +using json_full = nlohmann::json::with_object_t::type>; +using json_no_eq = nlohmann::json::with_object_t::type>; +using json_explicit = nlohmann::json::with_object_t::type>; +using json_to_json = nlohmann::json::with_object_t::type>; +using json_c_str = nlohmann::json::with_object_t::type>; + +// a key that is long enough to need a length byte in CBOR and MessagePack +inline const char* long_key_name(std::size_t i, std::string& storage) +{ + storage = "a key longer than thirty-one characters " + std::to_string(i); + return storage.c_str(); +} + +// name of the key at nesting level i of a deep value +inline std::string deep_name(std::size_t i, bool long_keys) +{ + std::string storage; + return (long_keys && i % 2 == 1) ? std::string(long_key_name(i, storage)) : "k" + std::to_string(i); +} + +// {"a": 1, "b": [true, null, "x"], "c": {"d": 2.5}, } +// 23 is the longest CBOR length stored in the initial byte; 36 needs one +// length byte in CBOR and MessagePack, 300 needs two +template +J make_shallow() +{ + using key_t = typename J::object_t::key_type; + + J array = J::array(); + array.push_back(J(true)); + array.push_back(J(nullptr)); + array.push_back(J("x")); + + typename J::object_t inner; + inner.emplace(key_t("d"), J(2.5)); + + typename J::object_t object; + object.emplace(key_t("a"), J(1)); + object.emplace(key_t("b"), std::move(array)); + object.emplace(key_t("c"), J(std::move(inner))); + object.emplace(key_t(std::string(23, 'x')), J(2)); + object.emplace(key_t(std::string(36, 'y')), J(3)); + object.emplace(key_t(std::string(300, 'z')), J(4)); + return J(std::move(object)); +} + +// {"k0": {"k1": {... {"k": 1} ...}}} +template +J make_deep(std::size_t depth, bool long_keys) +{ + using key_t = typename J::object_t::key_type; + + J value = 1; + for (std::size_t i = depth; i > 0; --i) + { + typename J::object_t object; + object.emplace(key_t(deep_name(i - 1, long_keys)), std::move(value)); + value = J(std::move(object)); + } + return value; +} + +inline std::size_t deep_depth() +{ + return nlohmann::detail::recursion_depth_limit() + 10; +} + +// walk down the nesting levels without recursion and check the leaf +template +bool check_deep(const J& value, std::size_t depth, bool long_keys) +{ + using key_t = typename J::object_t::key_type; + + const J* current = &value; + for (std::size_t i = 0; i < depth; ++i) + { + if (!current->is_object() || current->size() != 1) + { + return false; + } + const auto it = current->find(key_t(deep_name(i, long_keys))); + if (it == current->end()) + { + return false; + } + current = &it.value(); + } + return current->is_number_integer() && current->template get() == 1; +} + +template +bool check_shallow(const J& value) +{ + using key_t = typename J::object_t::key_type; + + if (!value.is_object() || value.size() != 6) + { + return false; + } + + const auto a = value.find(key_t("a")); + const auto b = value.find(key_t("b")); + const auto c = value.find(key_t("c")); + if (a == value.end() || b == value.end() || c == value.end()) + { + return false; + } + + const auto d = c->find(key_t("d")); + // basic_json::operator== needs operator== on the keys, which most of the + // key types do not have, so the values are checked through get<>() + return a->template get() == 1 + && b->is_array() && b->size() == 3 && (*b)[0].template get() && (*b)[1].is_null() + && (*b)[2].template get() == "x" + && d != c->end() && std::abs(d->template get() - 2.5) < 1e-9 + && value.find(key_t(std::string(23, 'x')))->template get() == 2 + && value.find(key_t(std::string(36, 'y')))->template get() == 3 + && value.find(key_t(std::string(300, 'z')))->template get() == 4; +} + +template +bool is_missing(const J& value, const char* name) +{ + return value.find(typename J::object_t::key_type(name)) == value.end(); +} + +// member access through find(): at() does not compile for key types without +// size() or a conversion to string_t (key_to_json), as in version 3.12.0 +template +const J& member(const J& value, const char* name) +{ + const auto it = value.find(typename J::object_t::key_type(name)); + REQUIRE(it != value.end()); + return *it; +} + +// The test cases, as function templates: each unit-custom-key-type-*.cpp file +// runs them for one object type only. The MinGW linker fails on objects with +// more than 65535 sections, and every basic_json specialization adds many +// (see .github/workflows/windows.yml). + +// copy +template +void test_copy() +{ + SECTION("shallow") + { + const J original = custom_key_test::make_shallow(); + REQUIRE(custom_key_test::check_shallow(original)); + + const J copy(original); // NOLINT(performance-unnecessary-copy-initialization) + CHECK(custom_key_test::check_shallow(copy)); + + J assigned; + assigned = original; + CHECK(custom_key_test::check_shallow(assigned)); + + // the original is unchanged + CHECK(custom_key_test::check_shallow(original)); + } + + SECTION("deep") + { + const std::size_t depth = custom_key_test::deep_depth(); + + const J original = custom_key_test::make_deep(depth, false); + REQUIRE(custom_key_test::check_deep(original, depth, false)); + + const J copy(original); // NOLINT(performance-unnecessary-copy-initialization) + CHECK(custom_key_test::check_deep(copy, depth, false)); + + J assigned; + assigned = original; + CHECK(custom_key_test::check_deep(assigned, depth, false)); + + CHECK(custom_key_test::check_deep(original, depth, false)); + } +} + +// parse +template +void test_parse() +{ + const J j = J::parse(R"({"a":1,"b":{"c":[1,2]}})"); + + CHECK(j.size() == 2); + CHECK(custom_key_test::member(j, "a").template get() == 1); + CHECK(custom_key_test::member(custom_key_test::member(j, "b"), "c").size() == 2); + CHECK(custom_key_test::member(custom_key_test::member(j, "b"), "c")[1].template get() == 2); + + // a deeply nested document + const std::size_t depth = custom_key_test::deep_depth(); + std::string text; + for (std::size_t i = 0; i < depth; ++i) + { + text += "{\"k" + std::to_string(i) + "\":"; + } + text += '1'; + text.append(depth, '}'); + CHECK(custom_key_test::check_deep(J::parse(text), depth, false)); +} + +// merge_patch, update, and insert +template +void test_patch() +{ + SECTION("merge_patch") + { + J j = J::parse(R"({"a":1,"b":2,"n":{"x":1,"y":2}})"); + j.merge_patch(J::parse(R"({"b":null,"c":3,"n":{"y":null,"z":3}})")); + + CHECK(j.size() == 3); + CHECK(custom_key_test::member(j, "a").template get() == 1); + CHECK(custom_key_test::is_missing(j, "b")); + CHECK(custom_key_test::member(j, "c").template get() == 3); + CHECK(custom_key_test::member(j, "n").size() == 2); + CHECK(custom_key_test::member(custom_key_test::member(j, "n"), "x").template get() == 1); + CHECK(custom_key_test::member(custom_key_test::member(j, "n"), "z").template get() == 3); + } + + SECTION("update") + { + J j = J::parse(R"({"a":1,"b":2,"n":{"x":1}})"); + const J other = J::parse(R"({"b":3,"c":4,"n":{"y":2}})"); + + J replaced = j; + replaced.update(other); + CHECK(replaced.size() == 4); + CHECK(custom_key_test::member(replaced, "a").template get() == 1); + CHECK(custom_key_test::member(replaced, "b").template get() == 3); + CHECK(custom_key_test::member(replaced, "c").template get() == 4); + CHECK(custom_key_test::member(replaced, "n").size() == 1); + CHECK(custom_key_test::member(custom_key_test::member(replaced, "n"), "y").template get() == 2); + + j.update(other, true); + CHECK(j.size() == 4); + CHECK(custom_key_test::member(j, "n").size() == 2); + CHECK(custom_key_test::member(custom_key_test::member(j, "n"), "x").template get() == 1); + CHECK(custom_key_test::member(custom_key_test::member(j, "n"), "y").template get() == 2); + } + + SECTION("insert") + { + J j = J::parse(R"({"a":1,"b":2})"); + const J other = J::parse(R"({"b":3,"c":4})"); + j.insert(other.begin(), other.end()); + + CHECK(j.size() == 3); + CHECK(custom_key_test::member(j, "b").template get() == 2); + CHECK(custom_key_test::member(j, "c").template get() == 4); + } +} + +// at() reports a missing key +template +void test_at() +{ + // not for key_to_json: at() needs the key's size() or a conversion to + // string_t for its error message, which also was the case in version 3.12.0 + J j = J::parse(R"({"a":1})"); + const J& j_const = j; + + CHECK(j.at("a").template get() == 1); + CHECK(j_const.at("a").template get() == 1); +#if JSON_DIAGNOSTIC_POSITIONS + CHECK_THROWS_WITH_AS(j.at("missing"), "[json.exception.out_of_range.403] (bytes 0-7) key 'missing' not found", typename J::out_of_range&); + CHECK_THROWS_WITH_AS(j_const.at("missing"), "[json.exception.out_of_range.403] (bytes 0-7) key 'missing' not found", typename J::out_of_range&); +#else + CHECK_THROWS_WITH_AS(j.at("missing"), "[json.exception.out_of_range.403] key 'missing' not found", typename J::out_of_range&); + CHECK_THROWS_WITH_AS(j_const.at("missing"), "[json.exception.out_of_range.403] key 'missing' not found", typename J::out_of_range&); +#endif +} + +// BSON +template +void test_bson() +{ + SECTION("shallow") + { + const J value = custom_key_test::make_shallow(); + const nlohmann::json expected = custom_key_test::make_shallow(); + + const std::vector encoded = J::to_bson(value); + CHECK(encoded == nlohmann::json::to_bson(expected)); + CHECK(nlohmann::json::from_bson(encoded) == expected); + } + + SECTION("deep") + { + const std::size_t depth = custom_key_test::deep_depth(); + const J value = custom_key_test::make_deep(depth, false); + const nlohmann::json expected = custom_key_test::make_deep(depth, false); + + const std::vector encoded = J::to_bson(value); + CHECK(encoded == nlohmann::json::to_bson(expected)); + CHECK(nlohmann::json::from_bson(encoded) == expected); + } +} + +// CBOR +template +void test_cbor() +{ + SECTION("shallow") + { + const J value = custom_key_test::make_shallow(); + const nlohmann::json expected = custom_key_test::make_shallow(); + + const std::vector encoded = J::to_cbor(value); + CHECK(encoded == nlohmann::json::to_cbor(expected)); + CHECK(nlohmann::json::from_cbor(encoded) == expected); + } + + SECTION("deeper than the recursion depth limit") + { + const std::size_t depth = custom_key_test::deep_depth(); + const J value = custom_key_test::make_deep(depth, true); + const nlohmann::json expected = custom_key_test::make_deep(depth, true); + + const std::vector encoded = J::to_cbor(value); + CHECK(encoded == nlohmann::json::to_cbor(expected)); + CHECK(nlohmann::json::from_cbor(encoded) == expected); + } +} + +// MessagePack +template +void test_msgpack() +{ + SECTION("shallow") + { + const J value = custom_key_test::make_shallow(); + const nlohmann::json expected = custom_key_test::make_shallow(); + + const std::vector encoded = J::to_msgpack(value); + CHECK(encoded == nlohmann::json::to_msgpack(expected)); + CHECK(nlohmann::json::from_msgpack(encoded) == expected); + } + + SECTION("deeper than the recursion depth limit") + { + const std::size_t depth = custom_key_test::deep_depth(); + const J value = custom_key_test::make_deep(depth, true); + const nlohmann::json expected = custom_key_test::make_deep(depth, true); + + const std::vector encoded = J::to_msgpack(value); + CHECK(encoded == nlohmann::json::to_msgpack(expected)); + CHECK(nlohmann::json::from_msgpack(encoded) == expected); + } +} + +} // namespace custom_key_test diff --git a/tests/src/custom_object_key_type.hpp b/tests/src/custom_object_key_type.hpp new file mode 100644 index 000000000..ebf499844 --- /dev/null +++ b/tests/src/custom_object_key_type.hpp @@ -0,0 +1,75 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include +#include +#include + +namespace custom_object_key_test +{ +class key +{ + public: + key() = default; + + key(const char* value) + : m_value(value) + {} + + key(std::string value) + : m_value(std::move(value)) + {} + + operator std::string() const + { + return m_value; + } + + // 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(); + } + + friend bool operator<(const key& lhs, const key& rhs) + { + return lhs.m_value < rhs.m_value; + } + + private: + std::string m_value; +}; + +template +class object + : public std::map < + key, + Value, + std::less, // NOLINT(modernize-use-transparent-functors) + typename std::allocator_traits::template rebind_alloc < + std::pair>> +{ + private: + using allocator_type = + typename std::allocator_traits::template rebind_alloc < + std::pair>; + + using base_type = + std::map, allocator_type>; // NOLINT(modernize-use-transparent-functors) + + public: + using base_type::base_type; +}; + +using json = nlohmann::json::with_object_t; +} // namespace custom_object_key_test diff --git a/tests/src/float_hard_cases.hpp b/tests/src/float_hard_cases.hpp new file mode 100644 index 000000000..e579bccce --- /dev/null +++ b/tests/src/float_hard_cases.hpp @@ -0,0 +1,599 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // array +#include // uint32_t, uint64_t + +// Number tokens that are hard to round correctly, with the IEEE-754 binary64 +// and binary32 bits of their correctly rounded values (ties to even; infinity +// for an overflow, a signed zero for an underflow). +// +// For doubles and floats around 0, the smallest normal number, 1, 2^24, 2^53, +// 0.1, and the largest finite number, and for random ones, the exact midpoint +// m to the next number gives: m, m with one unit more and less in the last +// digit, m with "01" and "0...01" appended, m with trailing zeros, and m cut +// after 17 to 30 digits (rounded down and up, so that the rounding is decided +// after the 19th digit), in fixed and exponent notation, 30% of them negative. +// Tokens longer than 80 characters are left out, except for four of 700 digits +// and more. Zeros, underflow, overflow, huge exponents, and integers beyond 64 +// bits complete the set. Of the 508 tokens, 134 (as double) and 150 (as +// float) need the exact comparison with the midpoint (detail::digit_comparison()). +// +// The expected bits were computed with exact rational arithmetic in Python +// (fractions.Fraction) and cross-checked with Python's float(); strtod_l and +// strtof_l of Apple's libc and of glibc agree. Generated by +// compact_hard_cases.py 5 (with hard_cases.py), see the pull request that +// added this file. + +namespace float_hard_cases +{ + +struct hard_case +{ + const char* token; + std::uint64_t bits64; + std::uint32_t bits32; +}; + +inline const std::array& cases() +{ + static const std::array table = + { + { + {"-2.4703282292062327e-324", 0x8000000000000000u, 0x80000000u}, + {"24703282292062328e-340", 0x0000000000000001u, 0x00000000u}, + {"247032822920623272e-341", 0x0000000000000000u, 0x00000000u}, + {"-0.2470328229206232721e-323", 0x8000000000000001u, 0x80000000u}, + {"-0.24703282292062327208e-323", 0x8000000000000000u, 0x80000000u}, + {"-2.4703282292062327209e-324", 0x8000000000000001u, 0x80000000u}, + {"2.47032822920623272088e-324", 0x0000000000000000u, 0x00000000u}, + {"247032822920623272089e-344", 0x0000000000000001u, 0x00000000u}, + {"-247032822920623272088284396434e-353", 0x8000000000000000u, 0x80000000u}, + {"0.247032822920623272088284396435e-323", 0x0000000000000001u, 0x00000000u}, + {"-74109846876186981e-340", 0x8000000000000001u, 0x80000000u}, + {"0.74109846876186982e-323", 0x0000000000000002u, 0x00000000u}, + {"-0.7410984687618698162e-323", 0x8000000000000001u, 0x80000000u}, + {"-7.410984687618698163e-324", 0x8000000000000002u, 0x80000000u}, + {"7.4109846876186981626e-324", 0x0000000000000001u, 0x00000000u}, + {"-74109846876186981627e-343", 0x8000000000000002u, 0x80000000u}, + {"-741098468761869816264e-344", 0x8000000000000001u, 0x80000000u}, + {"0.741098468761869816265e-323", 0x0000000000000002u, 0x00000000u}, + {"0.741098468761869816264853189302e-323", 0x0000000000000001u, 0x00000000u}, + {"-7.41098468761869816264853189303e-324", 0x8000000000000002u, 0x80000000u}, + {"0.22250738585072006e-307", 0x000FFFFFFFFFFFFEu, 0x00000000u}, + {"2.2250738585072007e-308", 0x000FFFFFFFFFFFFFu, 0x00000000u}, + {"2.225073858507200641e-308", 0x000FFFFFFFFFFFFEu, 0x00000000u}, + {"-2225073858507200642e-326", 0x800FFFFFFFFFFFFFu, 0x80000000u}, + {"22250738585072006419e-327", 0x000FFFFFFFFFFFFEu, 0x00000000u}, + {"0.2225073858507200642e-307", 0x000FFFFFFFFFFFFFu, 0x00000000u}, + {"0.222507385850720064199e-307", 0x000FFFFFFFFFFFFEu, 0x00000000u}, + {"2.225073858507200642e-308", 0x000FFFFFFFFFFFFFu, 0x00000000u}, + {"-2.22507385850720064199176395546e-308", 0x800FFFFFFFFFFFFEu, 0x80000000u}, + {"222507385850720064199176395547e-337", 0x000FFFFFFFFFFFFFu, 0x00000000u}, + {"-2.2250738585072011e-308", 0x800FFFFFFFFFFFFFu, 0x80000000u}, + {"-22250738585072012e-324", 0x8010000000000000u, 0x80000000u}, + {"-2225073858507201136e-326", 0x800FFFFFFFFFFFFFu, 0x80000000u}, + {"0.2225073858507201137e-307", 0x0010000000000000u, 0x00000000u}, + {"0.2225073858507201136e-307", 0x000FFFFFFFFFFFFFu, 0x00000000u}, + {"-2.2250738585072011361e-308", 0x8010000000000000u, 0x80000000u}, + {"2.22507385850720113605e-308", 0x000FFFFFFFFFFFFFu, 0x00000000u}, + {"222507385850720113606e-328", 0x0010000000000000u, 0x00000000u}, + {"22250738585072011360574097967e-336", 0x000FFFFFFFFFFFFFu, 0x00000000u}, + {"0.222507385850720113605740979671e-307", 0x0010000000000000u, 0x00000000u}, + {"22250738585072016e-324", 0x0010000000000000u, 0x00000000u}, + {"0.22250738585072017e-307", 0x0010000000000001u, 0x00000000u}, + {"0.222507385850720163e-307", 0x0010000000000000u, 0x00000000u}, + {"2.225073858507201631e-308", 0x0010000000000001u, 0x00000000u}, + {"-2.2250738585072016301e-308", 0x8010000000000000u, 0x80000000u}, + {"22250738585072016302e-327", 0x0010000000000001u, 0x00000000u}, + {"-222507385850720163012e-328", 0x8010000000000000u, 0x80000000u}, + {"0.222507385850720163013e-307", 0x0010000000000001u, 0x00000000u}, + {"0.222507385850720163012305563795e-307", 0x0010000000000000u, 0x00000000u}, + {"-2.22507385850720163012305563796e-308", 0x8010000000000001u, 0x80000000u}, + {"0.17976931348623156E+309", 0x7FEFFFFFFFFFFFFEu, 0x7F800000u}, + {"1.7976931348623157e308", 0x7FEFFFFFFFFFFFFFu, 0x7F800000u}, + {"1.797693134862315608e308", 0x7FEFFFFFFFFFFFFEu, 0x7F800000u}, + {"-1797693134862315609e290", 0xFFEFFFFFFFFFFFFFu, 0xFF800000u}, + {"-17976931348623156083e289", 0xFFEFFFFFFFFFFFFEu, 0xFF800000u}, + {"-0.17976931348623156084E+309", 0xFFEFFFFFFFFFFFFFu, 0xFF800000u}, + {"0.179769313486231560835E+309", 0x7FEFFFFFFFFFFFFEu, 0x7F800000u}, + {"-1.79769313486231560836e308", 0xFFEFFFFFFFFFFFFFu, 0xFF800000u}, + {"1.79769313486231560835325876058e308", 0x7FEFFFFFFFFFFFFEu, 0x7F800000u}, + {"179769313486231560835325876059e279", 0x7FEFFFFFFFFFFFFFu, 0x7F800000u}, + {"1.7976931348623158e308", 0x7FEFFFFFFFFFFFFFu, 0x7F800000u}, + {"17976931348623159e292", 0x7FF0000000000000u, 0x7F800000u}, + {"1797693134862315807e290", 0x7FEFFFFFFFFFFFFFu, 0x7F800000u}, + {"0.1797693134862315808E+309", 0x7FF0000000000000u, 0x7F800000u}, + {"0.17976931348623158079E+309", 0x7FEFFFFFFFFFFFFFu, 0x7F800000u}, + {"-1.797693134862315808e308", 0xFFF0000000000000u, 0xFF800000u}, + {"1.79769313486231580793e308", 0x7FEFFFFFFFFFFFFFu, 0x7F800000u}, + {"179769313486231580794e288", 0x7FF0000000000000u, 0x7F800000u}, + {"179769313486231580793728971405e279", 0x7FEFFFFFFFFFFFFFu, 0x7F800000u}, + {"-0.179769313486231580793728971406E+309", 0xFFF0000000000000u, 0xFF800000u}, + {"100000000000000011102230246251565404236316680908203125e-53", 0x3FF0000000000000u, 0x3F800000u}, + {"-1.00000000000000011102230246251565404236316680908203126", 0xBFF0000000000001u, 0xBF800000u}, + {"1.00000000000000011102230246251565404236316680908203124e0", 0x3FF0000000000000u, 0x3F800000u}, + {"10000000000000001110223024625156540423631668090820312501e-55", 0x3FF0000000000001u, 0x3F800000u}, + {"1.00000000000000011102230246251565404236316680908203125000000000000000000001", 0x3FF0000000000001u, 0x3F800000u}, + {"10000000000000001e-16", 0x3FF0000000000000u, 0x3F800000u}, + {"1.0000000000000002", 0x3FF0000000000001u, 0x3F800000u}, + {"1.000000000000000111", 0x3FF0000000000000u, 0x3F800000u}, + {"1.000000000000000112e0", 0x3FF0000000000001u, 0x3F800000u}, + {"1.000000000000000111e0", 0x3FF0000000000000u, 0x3F800000u}, + {"-10000000000000001111e-19", 0xBFF0000000000001u, 0xBF800000u}, + {"-100000000000000011102e-20", 0xBFF0000000000000u, 0xBF800000u}, + {"-1.00000000000000011103", 0xBFF0000000000001u, 0xBF800000u}, + {"1.00000000000000011102230246251", 0x3FF0000000000000u, 0x3F800000u}, + {"1.00000000000000011102230246252e0", 0x3FF0000000000001u, 0x3F800000u}, + {"-0.999999999999999944488848768742172978818416595458984375", 0xBFF0000000000000u, 0xBF800000u}, + {"-9.99999999999999944488848768742172978818416595458984376e-1", 0xBFF0000000000000u, 0xBF800000u}, + {"999999999999999944488848768742172978818416595458984374e-54", 0x3FEFFFFFFFFFFFFFu, 0x3F800000u}, + {"0.99999999999999994448884876874217297881841659545898437501", 0x3FF0000000000000u, 0x3F800000u}, + {"9.99999999999999944488848768742172978818416595458984375000000000000000000001e-1", 0x3FF0000000000000u, 0x3F800000u}, + {"-0.99999999999999994", 0xBFEFFFFFFFFFFFFFu, 0xBF800000u}, + {"9.9999999999999995e-1", 0x3FF0000000000000u, 0x3F800000u}, + {"9.999999999999999444e-1", 0x3FEFFFFFFFFFFFFFu, 0x3F800000u}, + {"9999999999999999445e-19", 0x3FF0000000000000u, 0x3F800000u}, + {"99999999999999994448e-20", 0x3FEFFFFFFFFFFFFFu, 0x3F800000u}, + {"-0.99999999999999994449", 0xBFF0000000000000u, 0xBF800000u}, + {"0.999999999999999944488", 0x3FEFFFFFFFFFFFFFu, 0x3F800000u}, + {"-9.99999999999999944489e-1", 0xBFF0000000000000u, 0xBF800000u}, + {"9.99999999999999944488848768742e-1", 0x3FEFFFFFFFFFFFFFu, 0x3F800000u}, + {"999999999999999944488848768743e-30", 0x3FF0000000000000u, 0x3F800000u}, + {"-9.007199254740993e15", 0xC340000000000000u, 0xDA000000u}, + {"9007199254740994e0", 0x4340000000000001u, 0x5A000000u}, + {"9007199254740992", 0x4340000000000000u, 0x5A000000u}, + {"9.00719925474099301e15", 0x4340000000000001u, 0x5A000000u}, + {"9007199254740993000000000000000000001e-21", 0x4340000000000001u, 0x5A000000u}, + {"-9007199254740993.000000000000000000000000000000", 0xC340000000000000u, 0xDA000000u}, + {"90071992547409915e-1", 0x4340000000000000u, 0x5A000000u}, + {"-9007199254740991.6", 0xC340000000000000u, 0xDA000000u}, + {"9.0071992547409914e15", 0x433FFFFFFFFFFFFFu, 0x5A000000u}, + {"-9007199254740991501e-3", 0xC340000000000000u, 0xDA000000u}, + {"9007199254740991.5000000000000000000001", 0x4340000000000000u, 0x5A000000u}, + {"9.0071992547409915000000000000000000000000000000e15", 0x4340000000000000u, 0x5A000000u}, + {"0.100000000000000012490009027033011079765856266021728515625", 0x3FB999999999999Au, 0x3DCCCCCDu}, + {"1.00000000000000012490009027033011079765856266021728515626e-1", 0x3FB999999999999Bu, 0x3DCCCCCDu}, + {"100000000000000012490009027033011079765856266021728515624e-57", 0x3FB999999999999Au, 0x3DCCCCCDu}, + {"0.10000000000000001249000902703301107976585626602172851562501", 0x3FB999999999999Bu, 0x3DCCCCCDu}, + {"0.10000000000000001", 0x3FB999999999999Au, 0x3DCCCCCDu}, + {"1.0000000000000002e-1", 0x3FB999999999999Bu, 0x3DCCCCCDu}, + {"-1.000000000000000124e-1", 0xBFB999999999999Au, 0xBDCCCCCDu}, + {"1000000000000000125e-19", 0x3FB999999999999Bu, 0x3DCCCCCDu}, + {"-10000000000000001249e-20", 0xBFB999999999999Au, 0xBDCCCCCDu}, + {"0.1000000000000000125", 0x3FB999999999999Bu, 0x3DCCCCCDu}, + {"0.10000000000000001249", 0x3FB999999999999Au, 0x3DCCCCCDu}, + {"1.00000000000000012491e-1", 0x3FB999999999999Bu, 0x3DCCCCCDu}, + {"1.00000000000000012490009027033e-1", 0x3FB999999999999Au, 0x3DCCCCCDu}, + {"100000000000000012490009027034e-30", 0x3FB999999999999Bu, 0x3DCCCCCDu}, + {"2.45134755833537796875e14", 0x42EBDE5C4164D83Au, 0x575EF2E2u}, + {"-245134755833537796876e-6", 0xC2EBDE5C4164D83Au, 0xD75EF2E2u}, + {"-245134755833537.796874", 0xC2EBDE5C4164D839u, 0xD75EF2E2u}, + {"2.4513475583353779687501e14", 0x42EBDE5C4164D83Au, 0x575EF2E2u}, + {"245134755833537796875000000000000000000001e-27", 0x42EBDE5C4164D83Au, 0x575EF2E2u}, + {"245134755833537.796875000000000000000000000000000000", 0x42EBDE5C4164D83Au, 0x575EF2E2u}, + {"2.4513475583353779e14", 0x42EBDE5C4164D839u, 0x575EF2E2u}, + {"2451347558335378e-1", 0x42EBDE5C4164D83Au, 0x575EF2E2u}, + {"2451347558335377968e-4", 0x42EBDE5C4164D839u, 0x575EF2E2u}, + {"245134755833537.7969", 0x42EBDE5C4164D83Au, 0x575EF2E2u}, + {"245134755833537.79687", 0x42EBDE5C4164D839u, 0x575EF2E2u}, + {"2.4513475583353779688e14", 0x42EBDE5C4164D83Au, 0x575EF2E2u}, + {"181510327827821147441864013671875e-23", 0x41DB0C11CB91CE38u, 0x4ED8608Eu}, + {"-1815103278.27821147441864013671876", 0xC1DB0C11CB91CE38u, 0xCED8608Eu}, + {"1.81510327827821147441864013671874e9", 0x41DB0C11CB91CE37u, 0x4ED8608Eu}, + {"18151032782782114744186401367187501e-25", 0x41DB0C11CB91CE38u, 0x4ED8608Eu}, + {"1815103278.27821147441864013671875000000000000000000001", 0x41DB0C11CB91CE38u, 0x4ED8608Eu}, + {"-1.81510327827821147441864013671875000000000000000000000000000000e9", 0xC1DB0C11CB91CE38u, 0xCED8608Eu}, + {"18151032782782114e-7", 0x41DB0C11CB91CE37u, 0x4ED8608Eu}, + {"1815103278.2782115", 0x41DB0C11CB91CE38u, 0x4ED8608Eu}, + {"1815103278.278211474", 0x41DB0C11CB91CE37u, 0x4ED8608Eu}, + {"-1.815103278278211475e9", 0xC1DB0C11CB91CE38u, 0xCED8608Eu}, + {"1.8151032782782114744e9", 0x41DB0C11CB91CE37u, 0x4ED8608Eu}, + {"-18151032782782114745e-10", 0xC1DB0C11CB91CE38u, 0xCED8608Eu}, + {"181510327827821147441e-11", 0x41DB0C11CB91CE37u, 0x4ED8608Eu}, + {"1815103278.27821147442", 0x41DB0C11CB91CE38u, 0x4ED8608Eu}, + {"1815103278.27821147441864013671", 0x41DB0C11CB91CE37u, 0x4ED8608Eu}, + {"1.81510327827821147441864013672e9", 0x41DB0C11CB91CE38u, 0x4ED8608Eu}, + {"3809325632181785344", 0x43CA6EB8BD69FE2Au, 0x5E5375C6u}, + {"3.809325632181785345e18", 0x43CA6EB8BD69FE2Au, 0x5E5375C6u}, + {"3809325632181785343e0", 0x43CA6EB8BD69FE29u, 0x5E5375C6u}, + {"3809325632181785344.01", 0x43CA6EB8BD69FE2Au, 0x5E5375C6u}, + {"3.809325632181785344000000000000000000001e18", 0x43CA6EB8BD69FE2Au, 0x5E5375C6u}, + {"3809325632181785344000000000000000000000000000000e-30", 0x43CA6EB8BD69FE2Au, 0x5E5375C6u}, + {"3809325632181785300", 0x43CA6EB8BD69FE29u, 0x5E5375C6u}, + {"3.8093256321817854e18", 0x43CA6EB8BD69FE2Au, 0x5E5375C6u}, + {"4.046966549916366943359375e12", 0x428D7210076CE2F0u, 0x546B9080u}, + {"4046966549916366943359376e-12", 0x428D7210076CE2F0u, 0x546B9080u}, + {"4046966549916.366943359374", 0x428D7210076CE2EFu, 0x546B9080u}, + {"4.04696654991636694335937501e12", 0x428D7210076CE2F0u, 0x546B9080u}, + {"4046966549916366943359375000000000000000000001e-33", 0x428D7210076CE2F0u, 0x546B9080u}, + {"-4046966549916.366943359375000000000000000000000000000000", 0xC28D7210076CE2F0u, 0xD46B9080u}, + {"4.0469665499163669e12", 0x428D7210076CE2EFu, 0x546B9080u}, + {"4046966549916367e-3", 0x428D7210076CE2F0u, 0x546B9080u}, + {"4046966549916366943e-6", 0x428D7210076CE2EFu, 0x546B9080u}, + {"4046966549916.366944", 0x428D7210076CE2F0u, 0x546B9080u}, + {"4046966549916.3669433", 0x428D7210076CE2EFu, 0x546B9080u}, + {"4.0469665499163669434e12", 0x428D7210076CE2F0u, 0x546B9080u}, + {"-4.04696654991636694335e12", 0xC28D7210076CE2EFu, 0xD46B9080u}, + {"404696654991636694336e-8", 0x428D7210076CE2F0u, 0x546B9080u}, + {"28093802557000874e154", 0x63529C3B77330BDBu, 0x7F800000u}, + {"0.28093802557000875E+171", 0x63529C3B77330BDCu, 0x7F800000u}, + {"0.2809380255700087447E+171", 0x63529C3B77330BDBu, 0x7F800000u}, + {"2.809380255700087448e170", 0x63529C3B77330BDCu, 0x7F800000u}, + {"2.8093802557000874472e170", 0x63529C3B77330BDBu, 0x7F800000u}, + {"28093802557000874473e151", 0x63529C3B77330BDCu, 0x7F800000u}, + {"280938025570008744728e150", 0x63529C3B77330BDBu, 0x7F800000u}, + {"-0.280938025570008744729E+171", 0xE3529C3B77330BDCu, 0xFF800000u}, + {"-0.280938025570008744728403667979E+171", 0xE3529C3B77330BDBu, 0xFF800000u}, + {"2.8093802557000874472840366798e170", 0x63529C3B77330BDCu, 0x7F800000u}, + {"0.39523280297734525e-154", 0x1FE0F51BF17FD374u, 0x00000000u}, + {"-3.9523280297734526e-155", 0x9FE0F51BF17FD375u, 0x80000000u}, + {"3.952328029773452547e-155", 0x1FE0F51BF17FD374u, 0x00000000u}, + {"3952328029773452548e-173", 0x1FE0F51BF17FD375u, 0x00000000u}, + {"-39523280297734525478e-174", 0x9FE0F51BF17FD374u, 0x80000000u}, + {"0.39523280297734525479e-154", 0x1FE0F51BF17FD375u, 0x00000000u}, + {"-0.395232802977345254787e-154", 0x9FE0F51BF17FD374u, 0x80000000u}, + {"-3.95232802977345254788e-155", 0x9FE0F51BF17FD375u, 0x80000000u}, + {"-3.95232802977345254787245825501e-155", 0x9FE0F51BF17FD374u, 0x80000000u}, + {"395232802977345254787245825502e-184", 0x1FE0F51BF17FD375u, 0x00000000u}, + {"-1.0790205420931879e-276", 0x86A3209CA6233255u, 0x80000000u}, + {"-1079020542093188e-291", 0x86A3209CA6233256u, 0x80000000u}, + {"-1079020542093187947e-294", 0x86A3209CA6233255u, 0x80000000u}, + {"0.1079020542093187948e-275", 0x06A3209CA6233256u, 0x00000000u}, + {"0.1079020542093187947e-275", 0x06A3209CA6233255u, 0x00000000u}, + {"1.0790205420931879471e-276", 0x06A3209CA6233256u, 0x00000000u}, + {"1.07902054209318794701e-276", 0x06A3209CA6233255u, 0x00000000u}, + {"-107902054209318794702e-296", 0x86A3209CA6233256u, 0x80000000u}, + {"107902054209318794701153285302e-305", 0x06A3209CA6233255u, 0x00000000u}, + {"-0.107902054209318794701153285303e-275", 0x86A3209CA6233256u, 0x80000000u}, + {"58530471071351308e-228", 0x1413B446E6A16A3Bu, 0x00000000u}, + {"-0.58530471071351309e-211", 0x9413B446E6A16A3Cu, 0x80000000u}, + {"0.5853047107135130893e-211", 0x1413B446E6A16A3Bu, 0x00000000u}, + {"-5.853047107135130894e-212", 0x9413B446E6A16A3Cu, 0x80000000u}, + {"5.853047107135130893e-212", 0x1413B446E6A16A3Bu, 0x00000000u}, + {"58530471071351308931e-231", 0x1413B446E6A16A3Cu, 0x00000000u}, + {"-585304710713513089304e-232", 0x9413B446E6A16A3Bu, 0x80000000u}, + {"0.585304710713513089305e-211", 0x1413B446E6A16A3Cu, 0x00000000u}, + {"-0.585304710713513089304248824438e-211", 0x9413B446E6A16A3Bu, 0x80000000u}, + {"5.85304710713513089304248824439e-212", 0x1413B446E6A16A3Cu, 0x00000000u}, + {"0.19334214893983531e-78", 0x2F96ECBF1CFB10F6u, 0x00000000u}, + {"1.9334214893983532e-79", 0x2F96ECBF1CFB10F7u, 0x00000000u}, + {"1.933421489398353102e-79", 0x2F96ECBF1CFB10F6u, 0x00000000u}, + {"1933421489398353103e-97", 0x2F96ECBF1CFB10F7u, 0x00000000u}, + {"19334214893983531023e-98", 0x2F96ECBF1CFB10F6u, 0x00000000u}, + {"0.19334214893983531024e-78", 0x2F96ECBF1CFB10F7u, 0x00000000u}, + {"0.193342148939835310231e-78", 0x2F96ECBF1CFB10F6u, 0x00000000u}, + {"1.93342148939835310232e-79", 0x2F96ECBF1CFB10F7u, 0x00000000u}, + {"-1.93342148939835310231359014704e-79", 0xAF96ECBF1CFB10F6u, 0x80000000u}, + {"193342148939835310231359014705e-108", 0x2F96ECBF1CFB10F7u, 0x00000000u}, + {"2.9873358928024455e227", 0x6F2938807814E8A2u, 0x7F800000u}, + {"29873358928024456e211", 0x6F2938807814E8A3u, 0x7F800000u}, + {"298733589280244551e210", 0x6F2938807814E8A2u, 0x7F800000u}, + {"0.2987335892802445511E+228", 0x6F2938807814E8A3u, 0x7F800000u}, + {"0.29873358928024455109E+228", 0x6F2938807814E8A2u, 0x7F800000u}, + {"2.987335892802445511e227", 0x6F2938807814E8A3u, 0x7F800000u}, + {"-2.98733589280244551098e227", 0xEF2938807814E8A2u, 0xFF800000u}, + {"-298733589280244551099e207", 0xEF2938807814E8A3u, 0xFF800000u}, + {"298733589280244551098081559931e198", 0x6F2938807814E8A2u, 0x7F800000u}, + {"0.298733589280244551098081559932E+228", 0x6F2938807814E8A3u, 0x7F800000u}, + {"7.0064923216240853e-46", 0x3690000000000000u, 0x00000000u}, + {"70064923216240854e-62", 0x3690000000000000u, 0x00000001u}, + {"7006492321624085354e-64", 0x3690000000000000u, 0x00000000u}, + {"-0.7006492321624085355e-45", 0xB690000000000000u, 0x80000001u}, + {"0.70064923216240853546e-45", 0x3690000000000000u, 0x00000000u}, + {"7.0064923216240853547e-46", 0x3690000000000000u, 0x00000001u}, + {"7.00649232162408535461e-46", 0x3690000000000000u, 0x00000000u}, + {"-700649232162408535462e-66", 0xB690000000000000u, 0x80000001u}, + {"-700649232162408535461864791644e-75", 0xB690000000000000u, 0x80000000u}, + {"0.700649232162408535461864791645e-45", 0x3690000000000000u, 0x00000001u}, + {"21019476964872256e-61", 0x36A8000000000000u, 0x00000001u}, + {"0.21019476964872257e-44", 0x36A8000000000000u, 0x00000002u}, + {"-0.2101947696487225606e-44", 0xB6A8000000000000u, 0x80000001u}, + {"-2.101947696487225607e-45", 0xB6A8000000000000u, 0x80000002u}, + {"2.1019476964872256063e-45", 0x36A8000000000000u, 0x00000001u}, + {"21019476964872256064e-64", 0x36A8000000000000u, 0x00000002u}, + {"-210194769648722560638e-65", 0xB6A8000000000000u, 0x80000001u}, + {"-0.210194769648722560639e-44", 0xB6A8000000000000u, 0x80000002u}, + {"-0.210194769648722560638559437493e-44", 0xB6A8000000000000u, 0x80000001u}, + {"2.10194769648722560638559437494e-45", 0x36A8000000000000u, 0x00000002u}, + {"0.11754941406275178e-37", 0x380FFFFFA0000000u, 0x007FFFFEu}, + {"1.1754941406275179e-38", 0x380FFFFFA0000000u, 0x007FFFFFu}, + {"-1.175494140627517859e-38", 0xB80FFFFFA0000000u, 0x807FFFFEu}, + {"117549414062751786e-55", 0x380FFFFFA0000000u, 0x007FFFFFu}, + {"-11754941406275178592e-57", 0xB80FFFFFA0000000u, 0x807FFFFEu}, + {"0.11754941406275178593e-37", 0x380FFFFFA0000000u, 0x007FFFFFu}, + {"0.117549414062751785924e-37", 0x380FFFFFA0000000u, 0x007FFFFEu}, + {"1.17549414062751785925e-38", 0x380FFFFFA0000000u, 0x007FFFFFu}, + {"-1.17549414062751785924617589866e-38", 0xB80FFFFFA0000000u, 0x807FFFFEu}, + {"117549414062751785924617589867e-67", 0x380FFFFFA0000000u, 0x007FFFFFu}, + {"1.1754942807573642e-38", 0x380FFFFFDFFFFFFFu, 0x007FFFFFu}, + {"11754942807573643e-54", 0x380FFFFFE0000000u, 0x00800000u}, + {"1175494280757364291e-56", 0x380FFFFFE0000000u, 0x007FFFFFu}, + {"0.1175494280757364292e-37", 0x380FFFFFE0000000u, 0x00800000u}, + {"0.11754942807573642917e-37", 0x380FFFFFE0000000u, 0x007FFFFFu}, + {"-1.1754942807573642918e-38", 0xB80FFFFFE0000000u, 0x80800000u}, + {"-1.17549428075736429172e-38", 0xB80FFFFFE0000000u, 0x807FFFFFu}, + {"-117549428075736429173e-58", 0xB80FFFFFE0000000u, 0x80800000u}, + {"117549428075736429172788299103e-67", 0x380FFFFFE0000000u, 0x007FFFFFu}, + {"0.117549428075736429172788299104e-37", 0x380FFFFFE0000000u, 0x00800000u}, + {"11754944208872107e-54", 0x3810000010000000u, 0x00800000u}, + {"-0.11754944208872108e-37", 0xB810000010000000u, 0x80800001u}, + {"0.1175494420887210724e-37", 0x3810000010000000u, 0x00800000u}, + {"1.175494420887210725e-38", 0x3810000010000000u, 0x00800001u}, + {"1.1754944208872107242e-38", 0x3810000010000000u, 0x00800000u}, + {"-11754944208872107243e-57", 0xB810000010000000u, 0x80800001u}, + {"-11754944208872107242e-57", 0xB810000010000000u, 0x80800000u}, + {"0.117549442088721072421e-37", 0x3810000010000000u, 0x00800001u}, + {"0.11754944208872107242095900834e-37", 0x3810000010000000u, 0x00800000u}, + {"-1.17549442088721072420959008341e-38", 0xB810000010000000u, 0x80800001u}, + {"340282336497324057985868971510891282432", 0x47EFFFFFD0000000u, 0x7F7FFFFEu}, + {"-3.40282336497324057985868971510891282433e38", 0xC7EFFFFFD0000000u, 0xFF7FFFFFu}, + {"340282336497324057985868971510891282431e0", 0x47EFFFFFD0000000u, 0x7F7FFFFEu}, + {"340282336497324057985868971510891282432.01", 0x47EFFFFFD0000000u, 0x7F7FFFFFu}, + {"3.40282336497324057985868971510891282432000000000000000000001e38", 0x47EFFFFFD0000000u, 0x7F7FFFFFu}, + {"340282336497324057985868971510891282432000000000000000000000000000000e-30", 0x47EFFFFFD0000000u, 0x7F7FFFFEu}, + {"340282336497324050000000000000000000000", 0x47EFFFFFD0000000u, 0x7F7FFFFEu}, + {"3.4028233649732406e38", 0x47EFFFFFD0000000u, 0x7F7FFFFFu}, + {"3.402823364973240579e38", 0x47EFFFFFD0000000u, 0x7F7FFFFEu}, + {"340282336497324058e21", 0x47EFFFFFD0000000u, 0x7F7FFFFFu}, + {"34028233649732405798e19", 0x47EFFFFFD0000000u, 0x7F7FFFFEu}, + {"340282336497324057990000000000000000000", 0x47EFFFFFD0000000u, 0x7F7FFFFFu}, + {"340282336497324057985000000000000000000", 0x47EFFFFFD0000000u, 0x7F7FFFFEu}, + {"3.40282336497324057986e38", 0x47EFFFFFD0000000u, 0x7F7FFFFFu}, + {"3.4028233649732405798586897151e38", 0x47EFFFFFD0000000u, 0x7F7FFFFEu}, + {"340282336497324057985868971511e9", 0x47EFFFFFD0000000u, 0x7F7FFFFFu}, + {"3.40282356779733661637539395458142568448e38", 0x47EFFFFFF0000000u, 0x7F800000u}, + {"-340282356779733661637539395458142568449e0", 0xC7EFFFFFF0000000u, 0xFF800000u}, + {"340282356779733661637539395458142568447", 0x47EFFFFFF0000000u, 0x7F7FFFFFu}, + {"3.4028235677973366163753939545814256844801e38", 0x47EFFFFFF0000000u, 0x7F800000u}, + {"340282356779733661637539395458142568448000000000000000000001e-21", 0x47EFFFFFF0000000u, 0x7F800000u}, + {"340282356779733661637539395458142568448.000000000000000000000000000000", 0x47EFFFFFF0000000u, 0x7F800000u}, + {"3.4028235677973366e38", 0x47EFFFFFF0000000u, 0x7F7FFFFFu}, + {"-34028235677973367e22", 0xC7EFFFFFF0000000u, 0xFF800000u}, + {"3402823567797336616e20", 0x47EFFFFFF0000000u, 0x7F7FFFFFu}, + {"340282356779733661700000000000000000000", 0x47EFFFFFF0000000u, 0x7F800000u}, + {"340282356779733661630000000000000000000", 0x47EFFFFFF0000000u, 0x7F7FFFFFu}, + {"3.4028235677973366164e38", 0x47EFFFFFF0000000u, 0x7F800000u}, + {"3.40282356779733661637e38", 0x47EFFFFFF0000000u, 0x7F7FFFFFu}, + {"340282356779733661638e18", 0x47EFFFFFF0000000u, 0x7F800000u}, + {"-340282356779733661637539395458e9", 0xC7EFFFFFF0000000u, 0xFF7FFFFFu}, + {"340282356779733661637539395459000000000", 0x47EFFFFFF0000000u, 0x7F800000u}, + {"-1000000059604644775390625e-24", 0xBFF0000010000000u, 0xBF800000u}, + {"-1.000000059604644775390626", 0xBFF0000010000000u, 0xBF800001u}, + {"-1.000000059604644775390624e0", 0xBFF0000010000000u, 0xBF800000u}, + {"100000005960464477539062501e-26", 0x3FF0000010000000u, 0x3F800001u}, + {"1.000000059604644775390625000000000000000000001", 0x3FF0000010000000u, 0x3F800001u}, + {"1.000000059604644775390625000000000000000000000000000000e0", 0x3FF0000010000000u, 0x3F800000u}, + {"-10000000596046447e-16", 0xBFF0000010000000u, 0xBF800000u}, + {"1.0000000596046448", 0x3FF0000010000000u, 0x3F800001u}, + {"1.000000059604644775", 0x3FF0000010000000u, 0x3F800000u}, + {"-1.000000059604644776e0", 0xBFF0000010000000u, 0xBF800001u}, + {"-1.0000000596046447753e0", 0xBFF0000010000000u, 0xBF800000u}, + {"10000000596046447754e-19", 0x3FF0000010000000u, 0x3F800001u}, + {"100000005960464477539e-20", 0x3FF0000010000000u, 0x3F800000u}, + {"-1.0000000596046447754", 0xBFF0000010000000u, 0xBF800001u}, + {"0.9999999701976776123046875", 0x3FEFFFFFF0000000u, 0x3F800000u}, + {"9.999999701976776123046876e-1", 0x3FEFFFFFF0000000u, 0x3F800000u}, + {"9999999701976776123046874e-25", 0x3FEFFFFFF0000000u, 0x3F7FFFFFu}, + {"0.999999970197677612304687501", 0x3FEFFFFFF0000000u, 0x3F800000u}, + {"-9.999999701976776123046875000000000000000000001e-1", 0xBFEFFFFFF0000000u, 0xBF800000u}, + {"-9999999701976776123046875000000000000000000000000000000e-55", 0xBFEFFFFFF0000000u, 0xBF800000u}, + {"0.99999997019767761", 0x3FEFFFFFF0000000u, 0x3F7FFFFFu}, + {"-9.9999997019767762e-1", 0xBFEFFFFFF0000000u, 0xBF800000u}, + {"-9.999999701976776123e-1", 0xBFEFFFFFF0000000u, 0xBF7FFFFFu}, + {"9999999701976776124e-19", 0x3FEFFFFFF0000000u, 0x3F800000u}, + {"9999999701976776123e-19", 0x3FEFFFFFF0000000u, 0x3F7FFFFFu}, + {"0.99999997019767761231", 0x3FEFFFFFF0000000u, 0x3F800000u}, + {"-0.999999970197677612304", 0xBFEFFFFFF0000000u, 0xBF7FFFFFu}, + {"9.99999970197677612305e-1", 0x3FEFFFFFF0000000u, 0x3F800000u}, + {"-1.6777217e7", 0xC170000010000000u, 0xCB800000u}, + {"16777218e0", 0x4170000020000000u, 0x4B800001u}, + {"16777216", 0x4170000000000000u, 0x4B800000u}, + {"-1.677721701e7", 0xC17000001028F5C3u, 0xCB800001u}, + {"-16777217000000000000000000001e-21", 0xC170000010000000u, 0xCB800001u}, + {"16777217.000000000000000000000000000000", 0x4170000010000000u, 0x4B800000u}, + {"167772155e-1", 0x416FFFFFF0000000u, 0x4B800000u}, + {"16777215.6", 0x416FFFFFF3333333u, 0x4B800000u}, + {"1.67772154e7", 0x416FFFFFECCCCCCDu, 0x4B7FFFFFu}, + {"16777215501e-3", 0x416FFFFFF0083127u, 0x4B800000u}, + {"-16777215.5000000000000000000001", 0xC16FFFFFF0000000u, 0xCB800000u}, + {"-1.67772155000000000000000000000000000000e7", 0xC16FFFFFF0000000u, 0xCB800000u}, + {"0.1000000052154064178466796875", 0x3FB99999B0000000u, 0x3DCCCCCEu}, + {"1.000000052154064178466796876e-1", 0x3FB99999B0000000u, 0x3DCCCCCEu}, + {"-1000000052154064178466796874e-28", 0xBFB99999B0000000u, 0xBDCCCCCDu}, + {"0.100000005215406417846679687501", 0x3FB99999B0000000u, 0x3DCCCCCEu}, + {"1.000000052154064178466796875000000000000000000001e-1", 0x3FB99999B0000000u, 0x3DCCCCCEu}, + {"-1000000052154064178466796875000000000000000000000000000000e-58", 0xBFB99999B0000000u, 0xBDCCCCCEu}, + {"-0.10000000521540641", 0xBFB99999AFFFFFFFu, 0xBDCCCCCDu}, + {"-1.0000000521540642e-1", 0xBFB99999B0000000u, 0xBDCCCCCEu}, + {"1.000000052154064178e-1", 0x3FB99999B0000000u, 0x3DCCCCCDu}, + {"-1000000052154064179e-19", 0xBFB99999B0000000u, 0xBDCCCCCEu}, + {"10000000521540641784e-20", 0x3FB99999B0000000u, 0x3DCCCCCDu}, + {"0.10000000521540641785", 0x3FB99999B0000000u, 0x3DCCCCCEu}, + {"0.100000005215406417846", 0x3FB99999B0000000u, 0x3DCCCCCDu}, + {"1.00000005215406417847e-1", 0x3FB99999B0000000u, 0x3DCCCCCEu}, + {"5.429001220703125e3", 0x40B5350050000000u, 0x45A9A802u}, + {"-5429001220703126e-12", 0xC0B5350050000001u, 0xC5A9A803u}, + {"-5429.001220703124", 0xC0B535004FFFFFFFu, 0xC5A9A802u}, + {"5.42900122070312501e3", 0x40B5350050000000u, 0x45A9A803u}, + {"5429001220703125000000000000000000001e-33", 0x40B5350050000000u, 0x45A9A803u}, + {"-5429.001220703125000000000000000000000000000000", 0xC0B5350050000000u, 0xC5A9A802u}, + {"503719056e0", 0x41BE062490000000u, 0x4DF03124u}, + {"503719057", 0x41BE062491000000u, 0x4DF03125u}, + {"5.03719055e8", 0x41BE06248F000000u, 0x4DF03124u}, + {"50371905601e-2", 0x41BE062490028F5Cu, 0x4DF03125u}, + {"503719056.000000000000000000001", 0x41BE062490000000u, 0x4DF03125u}, + {"5.03719056000000000000000000000000000000e8", 0x41BE062490000000u, 0x4DF03124u}, + {"-92331620", 0xC196037990000000u, 0xCCB01BCCu}, + {"9.233163e7", 0x41960379B8000000u, 0x4CB01BCEu}, + {"9233161e1", 0x4196037968000000u, 0x4CB01BCBu}, + {"92331620.1", 0x4196037990666666u, 0x4CB01BCDu}, + {"9.233162000000000000000000001e7", 0x4196037990000000u, 0x4CB01BCDu}, + {"9233162000000000000000000000000000000e-29", 0x4196037990000000u, 0x4CB01BCCu}, + {"3.002458625e6", 0x4146E82D50000000u, 0x4A37416Au}, + {"3002458626e-3", 0x4146E82D5020C49Cu, 0x4A37416Bu}, + {"3002458.624", 0x4146E82D4FDF3B64u, 0x4A37416Au}, + {"-3.00245862501e6", 0xC146E82D500053E3u, 0xCA37416Bu}, + {"3002458625000000000000000000001e-24", 0x4146E82D50000000u, 0x4A37416Bu}, + {"3002458.625000000000000000000000000000000", 0x4146E82D50000000u, 0x4A37416Au}, + {"-1095485584696182596504479582065262592e1", 0xC7A07BA830000000u, 0xFD03DD42u}, + {"10954855846961825965044795820652625930", 0x47A07BA830000000u, 0x7D03DD42u}, + {"1.095485584696182596504479582065262591e37", 0x47A07BA830000000u, 0x7D03DD41u}, + {"109548558469618259650447958206526259201e-1", 0x47A07BA830000000u, 0x7D03DD42u}, + {"-10954855846961825965044795820652625920.00000000000000000001", 0xC7A07BA830000000u, 0xFD03DD42u}, + {"1.095485584696182596504479582065262592000000000000000000000000000000e37", 0x47A07BA830000000u, 0x7D03DD42u}, + {"10954855846961825e21", 0x47A07BA830000000u, 0x7D03DD41u}, + {"10954855846961826000000000000000000000", 0x47A07BA830000000u, 0x7D03DD42u}, + {"10954855846961825960000000000000000000", 0x47A07BA830000000u, 0x7D03DD41u}, + {"1.095485584696182597e37", 0x47A07BA830000000u, 0x7D03DD42u}, + {"1.0954855846961825965e37", 0x47A07BA830000000u, 0x7D03DD41u}, + {"-10954855846961825966e18", 0xC7A07BA830000000u, 0xFD03DD42u}, + {"10954855846961825965e18", 0x47A07BA830000000u, 0x7D03DD41u}, + {"10954855846961825965100000000000000000", 0x47A07BA830000000u, 0x7D03DD42u}, + {"10954855846961825965044795820600000000", 0x47A07BA830000000u, 0x7D03DD41u}, + {"-1.09548558469618259650447958207e37", 0xC7A07BA830000000u, 0xFD03DD42u}, + {"1.6449216019182103706535606608388384863861375606575165875256061553955078126e-21", 0x3B9F125A50000000u, 0x1CF892D3u}, + {"-16449216019182103706535606608388384863861375606575165875256061553955078124e-94", 0xBB9F125A50000000u, 0x9CF892D2u}, + {"-0.0000000000000000000016449216019182103", 0xBB9F125A50000000u, 0x9CF892D2u}, + {"-1.6449216019182104e-21", 0xBB9F125A50000000u, 0x9CF892D3u}, + {"-1.64492160191821037e-21", 0xBB9F125A50000000u, 0x9CF892D2u}, + {"-1644921601918210371e-39", 0xBB9F125A50000000u, 0x9CF892D3u}, + {"-16449216019182103706e-40", 0xBB9F125A50000000u, 0x9CF892D2u}, + {"0.0000000000000000000016449216019182103707", 0x3B9F125A50000000u, 0x1CF892D3u}, + {"0.00000000000000000000164492160191821037065", 0x3B9F125A50000000u, 0x1CF892D2u}, + {"1.64492160191821037066e-21", 0x3B9F125A50000000u, 0x1CF892D3u}, + {"1.64492160191821037065356066083e-21", 0x3B9F125A50000000u, 0x1CF892D2u}, + {"164492160191821037065356066084e-50", 0x3B9F125A50000000u, 0x1CF892D3u}, + {"6.565061509609222412109375e-1", 0x3FE5021930000000u, 0x3F2810CAu}, + {"6565061509609222412109376e-25", 0x3FE5021930000000u, 0x3F2810CAu}, + {"0.6565061509609222412109374", 0x3FE5021930000000u, 0x3F2810C9u}, + {"6.56506150960922241210937501e-1", 0x3FE5021930000000u, 0x3F2810CAu}, + {"6565061509609222412109375000000000000000000001e-46", 0x3FE5021930000000u, 0x3F2810CAu}, + {"-0.6565061509609222412109375000000000000000000000000000000", 0xBFE5021930000000u, 0xBF2810CAu}, + {"6.5650615096092224e-1", 0x3FE5021930000000u, 0x3F2810C9u}, + {"-65650615096092225e-17", 0xBFE5021930000000u, 0xBF2810CAu}, + {"6565061509609222412e-19", 0x3FE5021930000000u, 0x3F2810C9u}, + {"0.6565061509609222413", 0x3FE5021930000000u, 0x3F2810CAu}, + {"0.65650615096092224121", 0x3FE5021930000000u, 0x3F2810C9u}, + {"6.5650615096092224122e-1", 0x3FE5021930000000u, 0x3F2810CAu}, + {"-6.5650615096092224121e-1", 0xBFE5021930000000u, 0xBF2810C9u}, + {"656506150960922241211e-21", 0x3FE5021930000000u, 0x3F2810CAu}, + {"18014627239033005156980393746124491372029297053813934326171875e-77", 0x3CA9F63970000000u, 0x254FB1CCu}, + {"0.00000000000000018014627239033005156980393746124491372029297053813934326171876", 0x3CA9F63970000000u, 0x254FB1CCu}, + {"-1.8014627239033005156980393746124491372029297053813934326171874e-16", 0xBCA9F63970000000u, 0xA54FB1CBu}, + {"1801462723903300515698039374612449137202929705381393432617187501e-79", 0x3CA9F63970000000u, 0x254FB1CCu}, + {"18014627239033005e-32", 0x3CA9F63970000000u, 0x254FB1CBu}, + {"0.00000000000000018014627239033006", 0x3CA9F63970000000u, 0x254FB1CCu}, + {"0.0000000000000001801462723903300515", 0x3CA9F63970000000u, 0x254FB1CBu}, + {"-1.801462723903300516e-16", 0xBCA9F63970000000u, 0xA54FB1CCu}, + {"-1.8014627239033005156e-16", 0xBCA9F63970000000u, 0xA54FB1CBu}, + {"18014627239033005157e-35", 0x3CA9F63970000000u, 0x254FB1CCu}, + {"180146272390330051569e-36", 0x3CA9F63970000000u, 0x254FB1CBu}, + {"-0.00000000000000018014627239033005157", 0xBCA9F63970000000u, 0xA54FB1CCu}, + {"0.000000000000000180146272390330051569803937461", 0x3CA9F63970000000u, 0x254FB1CBu}, + {"1.80146272390330051569803937462e-16", 0x3CA9F63970000000u, 0x254FB1CCu}, + {"0.05534819327294826507568359375", 0x3FAC569930000000u, 0x3D62B4CAu}, + {"-5.534819327294826507568359376e-2", 0xBFAC569930000000u, 0xBD62B4CAu}, + {"5534819327294826507568359374e-29", 0x3FAC569930000000u, 0x3D62B4C9u}, + {"-0.0553481932729482650756835937501", 0xBFAC569930000000u, 0xBD62B4CAu}, + {"5.534819327294826507568359375000000000000000000001e-2", 0x3FAC569930000000u, 0x3D62B4CAu}, + {"5534819327294826507568359375000000000000000000000000000000e-59", 0x3FAC569930000000u, 0x3D62B4CAu}, + {"0.055348193272948265", 0x3FAC569930000000u, 0x3D62B4C9u}, + {"5.5348193272948266e-2", 0x3FAC569930000000u, 0x3D62B4CAu}, + {"5.534819327294826507e-2", 0x3FAC569930000000u, 0x3D62B4C9u}, + {"5534819327294826508e-20", 0x3FAC569930000000u, 0x3D62B4CAu}, + {"55348193272948265075e-21", 0x3FAC569930000000u, 0x3D62B4C9u}, + {"-0.055348193272948265076", 0xBFAC569930000000u, 0xBD62B4CAu}, + {"0.0553481932729482650756", 0x3FAC569930000000u, 0x3D62B4C9u}, + {"-5.53481932729482650757e-2", 0xBFAC569930000000u, 0xBD62B4CAu}, + {"5.179692133247783258005389047985340416e36", 0x478F2C9450000000u, 0x7C7964A2u}, + {"5179692133247783258005389047985340417e0", 0x478F2C9450000000u, 0x7C7964A3u}, + {"5179692133247783258005389047985340415", 0x478F2C9450000000u, 0x7C7964A2u}, + {"-5.17969213324778325800538904798534041601e36", 0xC78F2C9450000000u, 0xFC7964A3u}, + {"-5179692133247783258005389047985340416000000000000000000001e-21", 0xC78F2C9450000000u, 0xFC7964A3u}, + {"-5179692133247783258005389047985340416.000000000000000000000000000000", 0xC78F2C9450000000u, 0xFC7964A2u}, + {"5.1796921332477832e36", 0x478F2C9450000000u, 0x7C7964A2u}, + {"51796921332477833e20", 0x478F2C9450000000u, 0x7C7964A3u}, + {"-5179692133247783258e18", 0xC78F2C9450000000u, 0xFC7964A2u}, + {"5179692133247783259000000000000000000", 0x478F2C9450000000u, 0x7C7964A3u}, + {"5179692133247783258000000000000000000", 0x478F2C9450000000u, 0x7C7964A2u}, + {"5.1796921332477832581e36", 0x478F2C9450000000u, 0x7C7964A3u}, + {"-5.179692133247783258e36", 0xC78F2C9450000000u, 0xFC7964A2u}, + {"-517969213324778325801e16", 0xC78F2C9450000000u, 0xFC7964A3u}, + {"517969213324778325800538904798e7", 0x478F2C9450000000u, 0x7C7964A2u}, + {"5179692133247783258005389047990000000", 0x478F2C9450000000u, 0x7C7964A3u}, + { + "0.22250738585072011360574097967091319759348195463516456480234261097248222220210769455165295239081350" + "8791414915891303962110687008643869459464552765720740782062174337998814106326732925355228688137214901" + "2981122451451889849057222307285255133155755015914397476397983411801999323962548289017107081850690630" + "6666559949382757725720157630626906633326475653000092458883164330377797918696120494973903778297049050" + "5108060994073026293712895895000358379996720725430436028407889577179615094551674824347103070260914462" + "1572289880258182545180325707018860872113128079512233426288368622321503775666622503982534335974568884" + "4239002654981983854879482922068947216898310996983658468140228542433306603398508864458040010349339704" + "2756718644338377048603786162277173854562306587467901408672332763671875e-307", 0x0010000000000000u, 0x00000000u + }, + { + "2.22507385850720113605740979670913197593481954635164564802342610972482222202107694551652952390813508" + "7914149158913039621106870086438694594645527657207407820621743379988141063267329253552286881372149012" + "9811224514518898490572223072852551331557550159143974763979834118019993239625482890171070818506906306" + "6665599493827577257201576306269066333264756530000924588831643303777979186961204949739037782970490505" + "1080609940730262937128958950003583799967207254304360284078895771796150945516748243471030702609144621" + "5722898802581825451803257070188608721131280795122334262883686223215037756666225039825343359745688844" + "2390026549819838548794829220689472168983109969836584681402285424333066033985088644580400103493397042" + "756718644338377048603786162277173854562306587467901408672332763671875000000000000000000001e-308", 0x0010000000000000u, 0x00000000u + }, + { + "0.11754942807573642917278829910357665133228589927589904276829631184250030649651730385585324256680905" + "8189392089843750000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000e-37", 0x380FFFFFE0000000u, 0x00800000u + }, + { + "1175494280757364291727882991035766513322858992758990427682963118425003064965173038558532425668090581" + "8939208984375000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000001e-751", 0x380FFFFFE0000000u, 0x00800000u + }, + {"0", 0x0000000000000000u, 0x00000000u}, + {"-0", 0x8000000000000000u, 0x80000000u}, + {"0.0", 0x0000000000000000u, 0x00000000u}, + {"-0.0", 0x8000000000000000u, 0x80000000u}, + {"0e999999999999999999999", 0x0000000000000000u, 0x00000000u}, + {"-0.000e-99999", 0x8000000000000000u, 0x80000000u}, + {"1e-400", 0x0000000000000000u, 0x00000000u}, + {"-1e-400", 0x8000000000000000u, 0x80000000u}, + {"1e400", 0x7FF0000000000000u, 0x7F800000u}, + {"-1e400", 0xFFF0000000000000u, 0xFF800000u}, + {"1e-50", 0x358DEE7A4AD4B81Fu, 0x00000000u}, + {"-1e-50", 0xB58DEE7A4AD4B81Fu, 0x80000000u}, + {"1e39", 0x48078287F49C4A1Du, 0x7F800000u}, + {"-1e39", 0xC8078287F49C4A1Du, 0xFF800000u}, + {"1e99999999999999999999999999", 0x7FF0000000000000u, 0x7F800000u}, + {"1e-99999999999999999999999999", 0x0000000000000000u, 0x00000000u}, + {"1e0000000000000000000000000000000000000000308", 0x7FE1CCF385EBC8A0u, 0x7F800000u}, + {"123456789012345678901234567890e-30", 0x3FBF9ADD3746F65Fu, 0x3DFCD6EAu}, + {"18446744073709551615", 0x43F0000000000000u, 0x5F800000u}, + {"18446744073709551616", 0x43F0000000000000u, 0x5F800000u}, + {"-9223372036854775808", 0xC3E0000000000000u, 0xDF000000u}, + {"-9223372036854775809", 0xC3E0000000000000u, 0xDF000000u}, + } + }; + return table; +} + +} // namespace float_hard_cases diff --git a/tests/src/fuzzer-json_view_image.cpp b/tests/src/fuzzer-json_view_image.cpp new file mode 100644 index 000000000..d990600af --- /dev/null +++ b/tests/src/fuzzer-json_view_image.cpp @@ -0,0 +1,95 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +/* +This file implements a test of json_document images suitable for fuzz +testing. The input is used twice: + +- as an image: json_document::load() with image_check::full must either throw + a parse_error or yield a document that serializes to the JSON text it reads + as; with image_check::bounds, reading and serializing must be safe (checked + by the sanitizers), and serializing may only throw type_error.316 +- as a JSON text: if json_document::parse() accepts it, the image of the + document must load (with every check) and serialize to the same text + +The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer +drivers. +*/ + +#include +#include +#include +#include +#include +#include + +// the checks below are assertions; NDEBUG would compile them away +#ifdef NDEBUG + #error "the fuzzer drivers must be built without NDEBUG" +#endif + +using nlohmann::json; +using nlohmann::json_document; +using image_check = json_document::image_check; + +// see http://llvm.org/docs/LibFuzzer.html +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + // the input as an image + for (const image_check check : + { + image_check::full, image_check::bounds + }) + { + json_document d; + try + { + d = json_document::load(data, size, check); + } + catch (const json::parse_error& e) + { + assert(e.id == 116); + continue; + } + std::string dumped; + try + { + dumped = d.root().dump(); + } + catch (const json::type_error& e) + { + // invalid UTF-8 can only pass the bounds check + assert(check == image_check::bounds && e.id == 316); + continue; + } + const json j = d.root().materialize(); + if (check == image_check::full) + { + assert(json::parse(dumped) == j); + // an image of the loaded document is the input + assert(d.save() == std::vector(data, data + size)); + } + } + + // the input as a JSON text + const std::string text(reinterpret_cast(data), size); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + const json_document parsed = json_document::parse(text, false); + if (!parsed.is_discarded()) + { + const std::vector image = parsed.save(); + for (const image_check check : + { + image_check::full, image_check::bounds, image_check::none + }) + { + const json_document loaded = json_document::load(image, check); + assert(loaded.root().dump() == parsed.root().dump()); + } + } + return 0; +} diff --git a/tests/src/fuzzer-parse_bjdata.cpp b/tests/src/fuzzer-parse_bjdata.cpp index 6302db104..0387e3241 100644 --- a/tests/src/fuzzer-parse_bjdata.cpp +++ b/tests/src/fuzzer-parse_bjdata.cpp @@ -47,26 +47,29 @@ dumps is stable under exactly the same values that break operator==. The unit tests run the same checks on a fixed corpus (see the "BJData round-trip invariants" test case), so keep both in sync. +Finally, it reads the data from a stream, a non-contiguous iterator range, and +a FILE*, which read byte by byte, and checks that each gives the same value or +error (id, message, byte position) as the pointer range, which takes the bulk +fast paths (see check_input_adapters()). + The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer drivers. */ -#include -#include +#include "fuzzer_common.hpp" -// the round-trip checks below are assertions; NDEBUG would compile them away -#ifdef NDEBUG - #error "the fuzzer drivers must be built without NDEBUG" -#endif - -using json = nlohmann::json; - -// compares dumps rather than values, because NaN != NaN; keep writes strings -// byte for byte, so ill-formed UTF-8 that a binary reader accepts cannot throw -static bool same_value(const json& lhs, const json& rhs) +namespace { - return lhs.dump(-1, ' ', false, json::error_handler_t::keep) == rhs.dump(-1, ' ', false, json::error_handler_t::keep); -} +// json::from_bjdata for check_input_adapters() +struct read_bjdata +{ + template + json operator()(Args&& ... args) const + { + return json::from_bjdata(std::forward(args)...); + } +}; +} // namespace // value-stable comparison for the round-trip checks below; see the note // above on why this compares dump()s rather than the json values directly @@ -80,23 +83,9 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { std::vector const vec1(data, data + size); - // step 0: parse input without exceptions; a parse error must then be - // reported as a discarded value, never thrown - json j_noexcept; + // step 0: parse input without exceptions bool noexcept_threw = false; - try - { - j_noexcept = json::from_bjdata(vec1, true, false); - } - catch (const json::parse_error&) - { - assert(false); - } - catch (const json::exception&) - { - // type and out-of-range errors are not parse errors and still throw - noexcept_threw = true; - } + json const j_noexcept = parse_without_exceptions([&] { return json::from_bjdata(vec1, true, false); }, noexcept_threw); // whether step 1 succeeded; if not, the catch blocks below check that // step 0 failed, too bool parsed = false; @@ -155,6 +144,9 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) assert(parsed || noexcept_threw || j_noexcept.is_discarded()); } + // step 3: the other input adapters must read the data alike + check_input_adapters(data, size, read_bjdata()); + // return 0 - non-zero return values are reserved for future use return 0; } diff --git a/tests/src/fuzzer-parse_bon8.cpp b/tests/src/fuzzer-parse_bon8.cpp index 8d9a119fa..bec3e2f93 100644 --- a/tests/src/fuzzer-parse_bon8.cpp +++ b/tests/src/fuzzer-parse_bon8.cpp @@ -17,78 +17,38 @@ array data, it performs the following steps: - j2 = from_bon8(vec) - assert(to_bon8(j2) == vec) -It also checks that reading the data from a stream, which reads strings byte by -byte, gives the same value or error as reading it from contiguous memory, which -copies strings in bulk. +Finally, it reads the data from a stream, a non-contiguous iterator range, and +a FILE*, which read byte by byte, and checks that each gives the same value or +error (id, message, byte position) as the pointer range, which takes the bulk +fast paths (see check_input_adapters()). The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer drivers. */ -#include -#include -#include - -// the round-trip checks below are assertions; NDEBUG would compile them away -#ifdef NDEBUG - #error "the fuzzer drivers must be built without NDEBUG" -#endif - -using json = nlohmann::json; - -// compares dumps rather than values, because NaN != NaN; keep writes strings -// byte for byte, so ill-formed UTF-8 that a binary reader accepts cannot throw -static bool same_value(const json& lhs, const json& rhs) -{ - return lhs.dump(-1, ' ', false, json::error_handler_t::keep) == rhs.dump(-1, ' ', false, json::error_handler_t::keep); -} +#include "fuzzer_common.hpp" namespace { -// the serialization of the value read from @a input, or the error message -template -std::string read_bon8(InputType&& input) +// json::from_bon8 for check_input_adapters() +struct read_bon8 { - try + template + json operator()(Args&& ... args) const { - const auto vec = json::to_bon8(json::from_bon8(std::forward(input))); - return {vec.begin(), vec.end()}; + return json::from_bon8(std::forward(args)...); } - catch (const json::exception& e) - { - return e.what(); - } -} +}; } // namespace // see http://llvm.org/docs/LibFuzzer.html extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { - // contiguous and stream input must be read alike - { - std::istringstream stream(std::string(reinterpret_cast(data), size)); - assert(read_bon8(std::vector(data, data + size)) == read_bon8(stream)); - } - std::vector const vec1(data, data + size); - // step 0: parse input without exceptions; a parse error must then be - // reported as a discarded value, never thrown - json j_noexcept; + // step 0: parse input without exceptions bool noexcept_threw = false; - try - { - j_noexcept = json::from_bon8(vec1, true, false); - } - catch (const json::parse_error&) - { - assert(false); - } - catch (const json::exception&) - { - // type and out-of-range errors are not parse errors and still throw - noexcept_threw = true; - } + json const j_noexcept = parse_without_exceptions([&] { return json::from_bon8(vec1, true, false); }, noexcept_threw); // whether step 1 succeeded; if not, the catch blocks below check that // step 0 failed, too bool parsed = false; @@ -135,6 +95,9 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) assert(parsed || noexcept_threw || j_noexcept.is_discarded()); } + // step 3: the other input adapters must read the data alike + check_input_adapters(data, size, read_bon8()); + // return 0 - non-zero return values are reserved for future use return 0; } diff --git a/tests/src/fuzzer-parse_bson.cpp b/tests/src/fuzzer-parse_bson.cpp index 357bc7eb2..33179de2b 100644 --- a/tests/src/fuzzer-parse_bson.cpp +++ b/tests/src/fuzzer-parse_bson.cpp @@ -17,49 +17,38 @@ array data, it performs the following steps: - j2 = from_bson(vec) - assert(to_bson(j2) == vec) +Finally, it reads the data from a stream, a non-contiguous iterator range, and +a FILE*, which read byte by byte, and checks that each gives the same value or +error (id, message, byte position) as the pointer range, which takes the bulk +fast paths (see check_input_adapters()). + The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer drivers. */ -#include -#include +#include "fuzzer_common.hpp" -// the round-trip checks below are assertions; NDEBUG would compile them away -#ifdef NDEBUG - #error "the fuzzer drivers must be built without NDEBUG" -#endif - -using json = nlohmann::json; - -// compares dumps rather than values, because NaN != NaN; keep writes strings -// byte for byte, so ill-formed UTF-8 that a binary reader accepts cannot throw -static bool same_value(const json& lhs, const json& rhs) +namespace { - return lhs.dump(-1, ' ', false, json::error_handler_t::keep) == rhs.dump(-1, ' ', false, json::error_handler_t::keep); -} +// json::from_bson for check_input_adapters() +struct read_bson +{ + template + json operator()(Args&& ... args) const + { + return json::from_bson(std::forward(args)...); + } +}; +} // namespace // see http://llvm.org/docs/LibFuzzer.html extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { std::vector const vec1(data, data + size); - // step 0: parse input without exceptions; a parse error must then be - // reported as a discarded value, never thrown - json j_noexcept; + // step 0: parse input without exceptions bool noexcept_threw = false; - try - { - j_noexcept = json::from_bson(vec1, true, false); - } - catch (const json::parse_error&) - { - assert(false); - } - catch (const json::exception&) - { - // type and out-of-range errors are not parse errors and still throw - noexcept_threw = true; - } + json const j_noexcept = parse_without_exceptions([&] { return json::from_bson(vec1, true, false); }, noexcept_threw); // whether step 1 succeeded; if not, the catch blocks below check that // step 0 failed, too bool parsed = false; @@ -106,6 +95,9 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) assert(parsed || noexcept_threw || j_noexcept.is_discarded()); } + // step 3: the other input adapters must read the data alike + check_input_adapters(data, size, read_bson()); + // return 0 - non-zero return values are reserved for future use return 0; } diff --git a/tests/src/fuzzer-parse_cbor.cpp b/tests/src/fuzzer-parse_cbor.cpp index 1574e8aee..12e7d4104 100644 --- a/tests/src/fuzzer-parse_cbor.cpp +++ b/tests/src/fuzzer-parse_cbor.cpp @@ -17,49 +17,38 @@ array data, it performs the following steps: - j2 = from_cbor(vec) - assert(to_cbor(j2) == vec) +Finally, it reads the data from a stream, a non-contiguous iterator range, and +a FILE*, which read byte by byte, and checks that each gives the same value or +error (id, message, byte position) as the pointer range, which takes the bulk +fast paths (see check_input_adapters()). + The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer drivers. */ -#include -#include +#include "fuzzer_common.hpp" -// the round-trip checks below are assertions; NDEBUG would compile them away -#ifdef NDEBUG - #error "the fuzzer drivers must be built without NDEBUG" -#endif - -using json = nlohmann::json; - -// compares dumps rather than values, because NaN != NaN; keep writes strings -// byte for byte, so ill-formed UTF-8 that a binary reader accepts cannot throw -static bool same_value(const json& lhs, const json& rhs) +namespace { - return lhs.dump(-1, ' ', false, json::error_handler_t::keep) == rhs.dump(-1, ' ', false, json::error_handler_t::keep); -} +// json::from_cbor for check_input_adapters() +struct read_cbor +{ + template + json operator()(Args&& ... args) const + { + return json::from_cbor(std::forward(args)...); + } +}; +} // namespace // see http://llvm.org/docs/LibFuzzer.html extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { std::vector const vec1(data, data + size); - // step 0: parse input without exceptions; a parse error must then be - // reported as a discarded value, never thrown - json j_noexcept; + // step 0: parse input without exceptions bool noexcept_threw = false; - try - { - j_noexcept = json::from_cbor(vec1, true, false); - } - catch (const json::parse_error&) - { - assert(false); - } - catch (const json::exception&) - { - // type and out-of-range errors are not parse errors and still throw - noexcept_threw = true; - } + json const j_noexcept = parse_without_exceptions([&] { return json::from_cbor(vec1, true, false); }, noexcept_threw); // whether step 1 succeeded; if not, the catch blocks below check that // step 0 failed, too bool parsed = false; @@ -106,6 +95,9 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) assert(parsed || noexcept_threw || j_noexcept.is_discarded()); } + // step 3: the other input adapters must read the data alike + check_input_adapters(data, size, read_cbor()); + // return 0 - non-zero return values are reserved for future use return 0; } diff --git a/tests/src/fuzzer-parse_json.cpp b/tests/src/fuzzer-parse_json.cpp index c0dd969f3..d656bfb87 100644 --- a/tests/src/fuzzer-parse_json.cpp +++ b/tests/src/fuzzer-parse_json.cpp @@ -17,48 +17,51 @@ array data, it performs the following steps: - j2 = parse(s1) - s2 = serialize(j2) - assert(s1 == s2) +- for ignore_comments = false and true: assert(parse(data) and accept(data) + give the same result for a stream, a non-contiguous iterator range, and a + FILE* as for the pointer range) + +The pointer range takes the bulk fast paths, the other input adapters read byte +by byte; see check_input_adapters(). The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer drivers. */ -#include -#include +#include "fuzzer_common.hpp" -// the round-trip checks below are assertions; NDEBUG would compile them away -#ifdef NDEBUG - #error "the fuzzer drivers must be built without NDEBUG" -#endif - -using json = nlohmann::json; - -// compares dumps rather than values, because NaN != NaN; keep writes strings -// byte for byte, so ill-formed UTF-8 that a binary reader accepts cannot throw -static bool same_value(const json& lhs, const json& rhs) +namespace { - return lhs.dump(-1, ' ', false, json::error_handler_t::keep) == rhs.dump(-1, ' ', false, json::error_handler_t::keep); -} +// json::parse and json::accept for check_input_adapters() +struct parse_json +{ + bool ignore_comments; + + template + json operator()(Args&& ... args) const + { + return json::parse(std::forward(args)..., nullptr, true, ignore_comments); + } +}; + +struct accept_json +{ + bool ignore_comments; + + template + bool operator()(Args&& ... args) const + { + return json::accept(std::forward(args)..., ignore_comments); + } +}; +} // namespace // see http://llvm.org/docs/LibFuzzer.html extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { - // step 0: parse input without exceptions; a parse error must then be - // reported as a discarded value, never thrown - json j_noexcept; + // step 0: parse input without exceptions bool noexcept_threw = false; - try - { - j_noexcept = json::parse(data, data + size, nullptr, false); - } - catch (const json::parse_error&) - { - assert(false); - } - catch (const json::exception&) - { - // type and out-of-range errors are not parse errors and still throw - noexcept_threw = true; - } + json const j_noexcept = parse_without_exceptions([&] { return json::parse(data, data + size, nullptr, false); }, noexcept_threw); // whether step 1 succeeded; if not, the catch blocks below check that // step 0 failed, too bool parsed = false; @@ -105,6 +108,16 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) assert(parsed || noexcept_threw || j_noexcept.is_discarded()); } + // step 3: the other input adapters must read the data alike + for (const bool ignore_comments : + { + false, true + }) + { + check_input_adapters(data, size, parse_json{ignore_comments}); + check_input_adapters(data, size, accept_json{ignore_comments}); + } + // return 0 - non-zero return values are reserved for future use return 0; } diff --git a/tests/src/fuzzer-parse_json_view.cpp b/tests/src/fuzzer-parse_json_view.cpp new file mode 100644 index 000000000..5e41d6c79 --- /dev/null +++ b/tests/src/fuzzer-parse_json_view.cpp @@ -0,0 +1,155 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +/* +This file implements a parser test suitable for fuzz testing. It checks that +json_document (the zero-copy, read-only view of a parsed JSON text declared in +json_view.hpp) agrees with basic_json on every input, for the parse options +selected by the low bits of the first input byte (bit 0: ignore_comments, bit 1: +ignore_trailing_commas; the byte stays part of the text): + +- json_document::accept(data) must equal json::accept(data) +- if the input is accepted, json_document::parse(data).root().materialize() + must equal json::parse(data) +- if the input is rejected, json_document::parse(data) (with exceptions + enabled) must throw a json::parse_error or json::out_of_range whose what() + is identical to the one json::parse(data) throws + +This is checked for two kinds of input: a std::string, which the document +borrows and which ends in the NUL the parser uses as sentinel, and an +exact-size byte vector, which has no NUL after its last byte and takes the +parser's bounds-checked path (AddressSanitizer reports any read past the end). + +The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer +drivers. +*/ + +#include +#include +#include +#include +#include +#include +#include + +// the checks below are assertions; NDEBUG would compile them away +#ifdef NDEBUG + #error "the fuzzer drivers must be built without NDEBUG" +#endif + +using nlohmann::json; +using nlohmann::json_document; + +namespace +{ +// what json::parse does with a text: the value, or the message of the exception +struct reference_result +{ + bool accepted = false; + json value{}; + std::string what{}; // NOLINT(readability-redundant-member-init) +}; + +reference_result parse_reference(const std::uint8_t* data, std::size_t size, bool comments, bool trailing_commas) +{ + reference_result r; + r.accepted = json::accept(data, data + size, comments, trailing_commas); + bool json_threw = false; + try + { + r.value = json::parse(data, data + size, nullptr, true, comments, trailing_commas); + } + catch (const json::parse_error& e) + { + r.what = e.what(); + json_threw = true; + } + catch (const json::out_of_range& e) + { + r.what = e.what(); + json_threw = true; + } + // json::accept and json::parse must agree + assert(json_threw == !r.accepted); + static_cast(json_threw); + return r; +} + +// json_document must agree with the reference for this input (a container +// that json_document::parse borrows) +template +void check_input(const Input& input, const reference_result& expected, bool comments, bool trailing_commas) +{ + // json_document::accept must agree with json::accept on every input + const bool accepted_by_view = json_document::accept(input, comments, trailing_commas); + assert(expected.accepted == accepted_by_view); + static_cast(accepted_by_view); + + if (expected.accepted) + { + // both parsers must agree on the resulting value + json_document const doc = json_document::parse(input, true, comments, trailing_commas); + assert(!doc.is_discarded()); + json const j2 = doc.root().materialize(); + assert(expected.value == j2); + static_cast(j2); + + // (without exceptions, the same document) + json_document const quiet = json_document::parse(input, false, comments, trailing_commas); + assert(!quiet.is_discarded()); + assert(quiet.node_count() == doc.node_count()); + } + else + { + // both parsers must reject the input the same way when exceptions are used + bool view_threw = false; + try + { + static_cast(json_document::parse(input, true, comments, trailing_commas)); + } + catch (const json::parse_error& e) + { + assert(e.what() == expected.what); + view_threw = true; + } + catch (const json::out_of_range& e) + { + assert(e.what() == expected.what); + view_threw = true; + } + assert(view_threw); + static_cast(view_threw); + + // and without exceptions, the document is discarded + json_document const quiet = json_document::parse(input, false, comments, trailing_commas); + assert(quiet.is_discarded()); + static_cast(quiet); + } +} +} // namespace + +// see http://llvm.org/docs/LibFuzzer.html +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + // the parse options are taken from the low bits of the first byte + const bool comments = size > 0 && (data[0] & 1U) != 0; + const bool trailing_commas = size > 0 && (data[0] & 2U) != 0; + + const reference_result expected = parse_reference(data, size, comments, trailing_commas); + + // a std::string: borrowed, with the NUL of std::string as sentinel + const std::string input(reinterpret_cast(data), size); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + check_input(input, expected, comments, trailing_commas); + + // an exact-size byte vector: borrowed, with nothing after its last byte + const std::vector exact(data, data + size); + check_input(exact, expected, comments, trailing_commas); + + // return 0 - non-zero return values are reserved for future use + return 0; +} diff --git a/tests/src/fuzzer-parse_msgpack.cpp b/tests/src/fuzzer-parse_msgpack.cpp index 9ab2aadd8..192cbffd8 100644 --- a/tests/src/fuzzer-parse_msgpack.cpp +++ b/tests/src/fuzzer-parse_msgpack.cpp @@ -17,49 +17,38 @@ array data, it performs the following steps: - j2 = from_msgpack(vec) - assert(to_msgpack(j2) == vec) +Finally, it reads the data from a stream, a non-contiguous iterator range, and +a FILE*, which read byte by byte, and checks that each gives the same value or +error (id, message, byte position) as the pointer range, which takes the bulk +fast paths (see check_input_adapters()). + The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer drivers. */ -#include -#include +#include "fuzzer_common.hpp" -// the round-trip checks below are assertions; NDEBUG would compile them away -#ifdef NDEBUG - #error "the fuzzer drivers must be built without NDEBUG" -#endif - -using json = nlohmann::json; - -// compares dumps rather than values, because NaN != NaN; keep writes strings -// byte for byte, so ill-formed UTF-8 that a binary reader accepts cannot throw -static bool same_value(const json& lhs, const json& rhs) +namespace { - return lhs.dump(-1, ' ', false, json::error_handler_t::keep) == rhs.dump(-1, ' ', false, json::error_handler_t::keep); -} +// json::from_msgpack for check_input_adapters() +struct read_msgpack +{ + template + json operator()(Args&& ... args) const + { + return json::from_msgpack(std::forward(args)...); + } +}; +} // namespace // see http://llvm.org/docs/LibFuzzer.html extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { std::vector const vec1(data, data + size); - // step 0: parse input without exceptions; a parse error must then be - // reported as a discarded value, never thrown - json j_noexcept; + // step 0: parse input without exceptions bool noexcept_threw = false; - try - { - j_noexcept = json::from_msgpack(vec1, true, false); - } - catch (const json::parse_error&) - { - assert(false); - } - catch (const json::exception&) - { - // type and out-of-range errors are not parse errors and still throw - noexcept_threw = true; - } + json const j_noexcept = parse_without_exceptions([&] { return json::from_msgpack(vec1, true, false); }, noexcept_threw); // whether step 1 succeeded; if not, the catch blocks below check that // step 0 failed, too bool parsed = false; @@ -106,6 +95,9 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) assert(parsed || noexcept_threw || j_noexcept.is_discarded()); } + // step 3: the other input adapters must read the data alike + check_input_adapters(data, size, read_msgpack()); + // return 0 - non-zero return values are reserved for future use return 0; } diff --git a/tests/src/fuzzer-parse_ubjson.cpp b/tests/src/fuzzer-parse_ubjson.cpp index 9750286ae..472857a07 100644 --- a/tests/src/fuzzer-parse_ubjson.cpp +++ b/tests/src/fuzzer-parse_ubjson.cpp @@ -26,49 +26,38 @@ array data, it performs the following steps: The unit tests run the same checks on a fixed corpus (see the "UBJSON round-trip invariants" test case), so keep both in sync. +Finally, it reads the data from a stream, a non-contiguous iterator range, and +a FILE*, which read byte by byte, and checks that each gives the same value or +error (id, message, byte position) as the pointer range, which takes the bulk +fast paths (see check_input_adapters()). + The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer drivers. */ -#include -#include +#include "fuzzer_common.hpp" -// the round-trip checks below are assertions; NDEBUG would compile them away -#ifdef NDEBUG - #error "the fuzzer drivers must be built without NDEBUG" -#endif - -using json = nlohmann::json; - -// compares dumps rather than values, because NaN != NaN; keep writes strings -// byte for byte, so ill-formed UTF-8 that a binary reader accepts cannot throw -static bool same_value(const json& lhs, const json& rhs) +namespace { - return lhs.dump(-1, ' ', false, json::error_handler_t::keep) == rhs.dump(-1, ' ', false, json::error_handler_t::keep); -} +// json::from_ubjson for check_input_adapters() +struct read_ubjson +{ + template + json operator()(Args&& ... args) const + { + return json::from_ubjson(std::forward(args)...); + } +}; +} // namespace // see http://llvm.org/docs/LibFuzzer.html extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { std::vector const vec1(data, data + size); - // step 0: parse input without exceptions; a parse error must then be - // reported as a discarded value, never thrown - json j_noexcept; + // step 0: parse input without exceptions bool noexcept_threw = false; - try - { - j_noexcept = json::from_ubjson(vec1, true, false); - } - catch (const json::parse_error&) - { - assert(false); - } - catch (const json::exception&) - { - // type and out-of-range errors are not parse errors and still throw - noexcept_threw = true; - } + json const j_noexcept = parse_without_exceptions([&] { return json::from_ubjson(vec1, true, false); }, noexcept_threw); // whether step 1 succeeded; if not, the catch blocks below check that // step 0 failed, too bool parsed = false; @@ -125,6 +114,9 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) assert(parsed || noexcept_threw || j_noexcept.is_discarded()); } + // step 3: the other input adapters must read the data alike + check_input_adapters(data, size, read_ubjson()); + // return 0 - non-zero return values are reserved for future use return 0; } diff --git a/tests/src/fuzzer_common.hpp b/tests/src/fuzzer_common.hpp new file mode 100644 index 000000000..8761b3a35 --- /dev/null +++ b/tests/src/fuzzer_common.hpp @@ -0,0 +1,145 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// code shared by the fuzzer drivers tests/src/fuzzer-parse_*.cpp + +#pragma once + +#include // assert +#include // size_t +#include // uint8_t +#include // FILE, fmemopen, fclose +#include // list +#include // istringstream +#include // string +#include // forward +#include // vector +#include + +using nlohmann::json; // NOLINT(google-global-names-in-headers): shared by all fuzzer drivers + +// the round-trip checks of the drivers are assertions; NDEBUG would compile them away +#ifdef NDEBUG + #error "the fuzzer drivers must be built without NDEBUG" +#endif + +// compares dumps rather than values, because NaN != NaN; keep writes strings +// byte for byte, so ill-formed UTF-8 that a binary reader accepts cannot throw +inline bool same_value(const json& lhs, const json& rhs) +{ + return lhs.dump(-1, ' ', false, json::error_handler_t::keep) == rhs.dump(-1, ' ', false, json::error_handler_t::keep); +} + +// step 0 of each driver: parse the input without exceptions; a parse error +// must then be reported as a discarded value, never thrown. Type and +// out-of-range errors are not parse errors and still throw; then @a threw is +// set and null is returned. +template +json parse_without_exceptions(Parse parse, bool& threw) +{ + threw = false; + try + { + return parse(); + } + catch (const json::parse_error&) + { + assert(false); + } + catch (const json::exception&) + { + threw = true; + } + return {}; +} + +// the outcome of reading an input with exceptions: the serialization of the +// value, or the id, message, and (for parse errors) byte of the exception +struct read_outcome +{ + std::string value{}; + int id = 0; + std::string what{}; + std::size_t byte = 0; + + bool operator==(const read_outcome& rhs) const + { + return value == rhs.value && id == rhs.id && what == rhs.what && byte == rhs.byte; + } +}; + +inline std::string outcome_value(const json& j) +{ + return j.dump(-1, ' ', false, json::error_handler_t::keep); +} + +inline std::string outcome_value(bool b) +{ + return b ? "true" : "false"; +} + +template +read_outcome read_with(const Read& parse, Args&& ... args) +{ + read_outcome result; + try + { + result.value = outcome_value(parse(std::forward(args)...)); + } + catch (const json::parse_error& e) + { + result.id = e.id; + result.what = e.what(); + result.byte = e.byte; + } + catch (const json::exception& e) + { + result.id = e.id; + result.what = e.what(); + } + return result; +} + +// The contiguous input (a pointer range) takes the bulk fast paths; the other +// input adapters read byte by byte. Reads @a data through each of them and +// checks that all give the same value, or the same exception with the same +// id, message, and byte position. +// +// @a parse is called with either an iterator range or a single input (stream +// or FILE*) and returns the parsed value (or the result of accept()). +template +void check_input_adapters(const std::uint8_t* data, std::size_t size, const Read& parse) +{ + const read_outcome expected = read_with(parse, data, data + size); + + // stream adapter + { + std::istringstream stream(std::string(reinterpret_cast(data), size)); + assert(read_with(parse, stream) == expected); + } + + // iterator adapter on a non-contiguous range + { + const std::list list(data, data + size); + assert(read_with(parse, list.begin(), list.end()) == expected); + } + +#if defined(__unix__) || defined(__APPLE__) + // FILE* adapter; fmemopen may refuse an empty buffer + { + std::vector buffer(data, data + size); + std::FILE* file = buffer.empty() ? nullptr : fmemopen(buffer.data(), buffer.size(), "rb"); + if (file != nullptr) + { + const read_outcome actual = read_with(parse, file); + std::fclose(file); + assert(actual == expected); + } + } +#endif +} diff --git a/tests/src/json_view_test_helpers.hpp b/tests/src/json_view_test_helpers.hpp new file mode 100644 index 000000000..b5af61798 --- /dev/null +++ b/tests/src/json_view_test_helpers.hpp @@ -0,0 +1,106 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +// helpers shared by the unit-json_view*.cpp files, which are split to keep every +// object file small enough for the MinGW linker (it fails to link objects with +// more than 32767 sections) + +#include + +#include // any_of +#include // function +#include // mt19937 +#include // string + +namespace json_view_test +{ +// a small deterministic generator of documents +struct generator +{ + std::mt19937 rng{5295}; // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) + + int r(int n) + { + return static_cast(rng() % static_cast(n)); + } + + void str(std::string& o) + { + static const char* const pieces[] = {"a", "Z", " ", "\\n", "\\\"", "\\u00e9", "\\ud83d\\ude00", "\xc3\xa9", "\xe3\x81\x82", "long text beyond the first sixteen bytes"}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + o += '"'; + for (int n = r(5); n > 0; --n) + { + o += pieces[r(10)]; + } + o += '"'; + } + + void value(std::string& o, int depth) + { + static const char* const scalars[] = {"0", "-1", "123456789012", "18446744073709551615", "18446744073709551616", "-9223372036854775809", // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + "1.5", "-2.25e-3", "1E2", "0.1", "true", "false", "null" + }; + const int k = depth > 5 ? 2 + r(4) : r(6); + if (k < 2) + { + const bool object = k == 0; + o += object ? '{' : '['; + for (int i = r(5); i > 0; --i) + { + if (object) + { + str(o); + o += r(2) == 0 ? ":" : " : "; + } + value(o, depth + 1); + o += i > 1 ? ", " : ""; + } + o += object ? '}' : ']'; + } + else if (k < 4) + { + str(o); + } + else + { + o += scalars[r(13)]; + } + } +}; + +// whether an object of the view repeats a key +inline bool has_duplicate_keys(const nlohmann::ordered_json_view& v) +{ + if (v.is_object() && v.size() != v.materialize().size()) + { + return true; + } + return std::any_of(v.begin(), v.end(), [](const nlohmann::ordered_json_view e) + { + return e.is_structured() && has_duplicate_keys(e); + }); +} + +#if !defined(JSON_NOEXCEPTION) +// the exception a call throws, or "" if it throws none +inline std::string exception_of_call(const std::function& f) +{ + try + { + f(); + } + catch (const nlohmann::json::exception& e) + { + return e.what(); + } + return ""; +} +#endif +} // namespace json_view_test diff --git a/tests/src/sax_event_loggers.hpp b/tests/src/sax_event_loggers.hpp new file mode 100644 index 000000000..e39ad9e56 --- /dev/null +++ b/tests/src/sax_event_loggers.hpp @@ -0,0 +1,185 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // size_t +#include // uint8_t (via json::binary_t) +#include // numeric_limits +#include // string, to_string +#include // vector + +#include + +// the loggers are defined in this header only, so their vtables are emitted in +// every test that includes it +#if defined(__clang__) + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wweak-vtables" +#endif + +namespace utils +{ +/// a SAX event consumer that records every event it receives as a +/// human-readable string, used by the deserialization tests to check the +/// exact sequence of SAX events a parse run produces +struct SaxEventLogger : public nlohmann::json_sax +{ + using json = nlohmann::json; + + bool null() override + { + events.emplace_back("null()"); + return true; + } + + bool boolean(bool val) override + { + events.emplace_back(val ? "boolean(true)" : "boolean(false)"); + return true; + } + + bool number_integer(json::number_integer_t val) override + { + events.push_back("number_integer(" + std::to_string(val) + ")"); + return true; + } + + bool number_unsigned(json::number_unsigned_t val) override + { + events.push_back("number_unsigned(" + std::to_string(val) + ")"); + return true; + } + + bool number_float(json::number_float_t /*val*/, const std::string& s) override + { + events.push_back("number_float(" + s + ")"); + return true; + } + + bool string(std::string& val) override + { + events.push_back("string(" + val + ")"); + return true; + } + + bool binary(json::binary_t& val) override + { + std::string binary_contents = "binary("; + std::string comma_space; + for (const auto b : val) + { + binary_contents.append(comma_space); + binary_contents.append(std::to_string(static_cast(b))); + comma_space = ", "; + } + binary_contents.append(")"); + events.push_back(binary_contents); + return true; + } + + bool start_object(std::size_t elements) override + { + if (elements == (std::numeric_limits::max)()) + { + events.emplace_back("start_object()"); + } + else + { + events.push_back("start_object(" + std::to_string(elements) + ")"); + } + return true; + } + + bool key(std::string& val) override + { + events.push_back("key(" + val + ")"); + return true; + } + + bool end_object() override + { + events.emplace_back("end_object()"); + return true; + } + + bool start_array(std::size_t elements) override + { + if (elements == (std::numeric_limits::max)()) + { + events.emplace_back("start_array()"); + } + else + { + events.push_back("start_array(" + std::to_string(elements) + ")"); + } + return true; + } + + bool end_array() override + { + events.emplace_back("end_array()"); + return true; + } + + bool parse_error(std::size_t position, const std::string& /*last_token*/, const json::exception& /*ex*/) override + { + errored = true; + events.push_back("parse_error(" + std::to_string(position) + ")"); + return false; + } + + std::vector events {}; // NOLINT(readability-redundant-member-init) + bool errored = false; +}; + +struct SaxEventLoggerExitAfterStartObject : public SaxEventLogger +{ + bool start_object(std::size_t elements) override + { + if (elements == (std::numeric_limits::max)()) + { + events.emplace_back("start_object()"); + } + else + { + events.push_back("start_object(" + std::to_string(elements) + ")"); + } + return false; + } +}; + +struct SaxEventLoggerExitAfterKey : public SaxEventLogger +{ + bool key(std::string& val) override + { + events.push_back("key(" + val + ")"); + return false; + } +}; + +struct SaxEventLoggerExitAfterStartArray : public SaxEventLogger +{ + bool start_array(std::size_t elements) override + { + if (elements == (std::numeric_limits::max)()) + { + events.emplace_back("start_array()"); + } + else + { + events.push_back("start_array(" + std::to_string(elements) + ")"); + } + return false; + } +}; +} // namespace utils + +#if defined(__clang__) + #pragma clang diagnostic pop +#endif diff --git a/tests/src/unit-algorithms.cpp b/tests/src/unit-algorithms.cpp index 90d6a08c7..2a9506590 100644 --- a/tests/src/unit-algorithms.cpp +++ b/tests/src/unit-algorithms.cpp @@ -9,6 +9,7 @@ #include "doctest_compatibility.h" #include +#include #include using nlohmann::json; diff --git a/tests/src/unit-binary_formats.cpp b/tests/src/unit-binary_formats.cpp index 03dbc94da..333252190 100644 --- a/tests/src/unit-binary_formats.cpp +++ b/tests/src/unit-binary_formats.cpp @@ -38,7 +38,7 @@ TEST_CASE("Binary Formats") const auto ubjson_2_size = json::to_ubjson(j, true).size(); const auto ubjson_3_size = json::to_ubjson(j, true, true).size(); - CHECK(json_size == 2090303); + CHECK(json_size == 2090234); CHECK(bjdata_1_size == 1112030); CHECK(bjdata_2_size == 1224148); CHECK(bjdata_3_size == 1224148); @@ -51,16 +51,16 @@ TEST_CASE("Binary Formats") CHECK(ubjson_3_size == 1169069); CHECK((100.0 * double(json_size) / double(json_size)) == Approx(100.0)); - CHECK((100.0 * double(bjdata_1_size) / double(json_size)) == Approx(53.199)); - CHECK((100.0 * double(bjdata_2_size) / double(json_size)) == Approx(58.563)); - CHECK((100.0 * double(bjdata_3_size) / double(json_size)) == Approx(58.563)); - CHECK((100.0 * double(bon8_size) / double(json_size)) == Approx(50.509)); - CHECK((100.0 * double(bson_size) / double(json_size)) == Approx(85.849)); - CHECK((100.0 * double(cbor_size) / double(json_size)) == Approx(50.497)); - CHECK((100.0 * double(msgpack_size) / double(json_size)) == Approx(50.526)); - CHECK((100.0 * double(ubjson_1_size) / double(json_size)) == Approx(53.199)); - CHECK((100.0 * double(ubjson_2_size) / double(json_size)) == Approx(58.563)); - CHECK((100.0 * double(ubjson_3_size) / double(json_size)) == Approx(55.928)); + CHECK((100.0 * double(bjdata_1_size) / double(json_size)) == Approx(53.201)); + CHECK((100.0 * double(bjdata_2_size) / double(json_size)) == Approx(58.565)); + CHECK((100.0 * double(bjdata_3_size) / double(json_size)) == Approx(58.565)); + CHECK((100.0 * double(bon8_size) / double(json_size)) == Approx(50.511)); + CHECK((100.0 * double(bson_size) / double(json_size)) == Approx(85.853)); + CHECK((100.0 * double(cbor_size) / double(json_size)) == Approx(50.499)); + CHECK((100.0 * double(msgpack_size) / double(json_size)) == Approx(50.528)); + CHECK((100.0 * double(ubjson_1_size) / double(json_size)) == Approx(53.201)); + CHECK((100.0 * double(ubjson_2_size) / double(json_size)) == Approx(58.565)); + CHECK((100.0 * double(ubjson_3_size) / double(json_size)) == Approx(55.930)); } SECTION("twitter.json") diff --git a/tests/src/unit-binary_utf8_error_handler.cpp b/tests/src/unit-binary_utf8_error_handler.cpp index c3f2435fd..114c92885 100644 --- a/tests/src/unit-binary_utf8_error_handler.cpp +++ b/tests/src/unit-binary_utf8_error_handler.cpp @@ -12,7 +12,11 @@ #include using nlohmann::json; +#include +#include +#include #include +#include #include namespace @@ -55,6 +59,53 @@ std::string dump_and_parse(const std::string& raw, eh error_handler) return json::parse(json(raw).dump(-1, ' ', false, error_handler)).get(); } +// an object key type that is not string_t, but converts implicitly to it; +// data() is only used when JSON_DIAGNOSTICS is enabled +DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-member-function") +class converting_key +{ + public: + converting_key(const char* s) : m_value(s) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) + converting_key(std::string s) : m_value(std::move(s)) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) + + // the conversion yields a temporary string_t + operator std::string() const // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) + { + return m_value; + } + + // read by the exception messages when JSON_DIAGNOSTICS is enabled + const char* data() const noexcept + { + return m_value.data(); + } + + friend bool operator<(const converting_key& lhs, const converting_key& rhs) + { + return lhs.m_value < rhs.m_value; + } + + private: + std::string m_value; +}; +DOCTEST_CLANG_SUPPRESS_WARNING_POP + +// ObjectType using converting_key; the Key template argument is ignored +template +class converting_key_object : public std::map, // NOLINT(modernize-use-transparent-functors) + typename std::allocator_traits::template rebind_alloc>> +{ + using base_type = std::map, // NOLINT(modernize-use-transparent-functors) + typename std::allocator_traits::template rebind_alloc>>; + + public: + using base_type::base_type; + using base_type::operator=; +}; + +using converting_key_json = nlohmann::basic_json; + } // namespace TEST_CASE("UTF-8 error_handler for the binary readers and writers") @@ -370,3 +421,109 @@ TEST_CASE("UTF-8 error_handler for the binary readers and writers") CHECK(json::from_bson(bson_bytes)["k"].get() == ill_formed_cases()[0].bytes); } } + +// The UBJSON and BJData writers bind the (possibly sanitized) key to a const +// string_t&. If key_type is not string_t but converts to it, the converted +// temporary must outlive that reference; this was a use-after-scope found by +// AddressSanitizer. Keys exceed the small string optimization on purpose. +TEST_CASE("UBJSON and BJData writers with an object_t whose key_type is not string_t") +{ + const std::string long_prefix(70, 'k'); + + SECTION("well-formed keys, every error_handler") + { + const std::string key1 = long_prefix + "-first"; + const std::string key2 = long_prefix + "-second"; + + converting_key_json::object_t o; + o.emplace(converting_key(key1), 1); + o.emplace(converting_key(key2), "value"); + const converting_key_json v(std::move(o)); + + json expected; + expected[key1] = 1; + expected[key2] = "value"; + + const std::array, 3> combos = {{{false, false}, {true, false}, {true, true}}}; + for (const auto h : all_handlers()) + { + CAPTURE(static_cast(h)) + for (const auto& combo : combos) + { + const bool use_count = combo.first; + const bool use_type = combo.second; + CAPTURE(use_count) + CAPTURE(use_type) + + CHECK(json::from_ubjson(converting_key_json::to_ubjson(v, use_count, use_type, h)) == expected); + CHECK(json::from_bjdata(converting_key_json::to_bjdata(v, use_count, use_type, json::bjdata_version_t::draft2, h)) == expected); + CHECK(json::from_bjdata(converting_key_json::to_bjdata(v, use_count, use_type, json::bjdata_version_t::draft3, h)) == expected); + } + } + } + + SECTION("ill-formed keys") + { + for (const auto& c : ill_formed_cases()) + { + CAPTURE(c.name) + const std::string key = long_prefix + c.bytes; + + converting_key_json::object_t o; + o.emplace(converting_key(key), 1); + const converting_key_json v(std::move(o)); + + CHECK_THROWS_AS(converting_key_json::to_ubjson(v, false, false, eh::strict), converting_key_json::type_error&); + CHECK_THROWS_AS(converting_key_json::to_bjdata(v, false, false, json::bjdata_version_t::draft2, eh::strict), converting_key_json::type_error&); + + for (const auto h : + { + eh::replace, eh::ignore + }) + { + CAPTURE(static_cast(h)) + const std::string expected = dump_and_parse(key, h); + + CHECK(json::from_ubjson(converting_key_json::to_ubjson(v, false, false, h)).begin().key() == expected); + CHECK(json::from_bjdata(converting_key_json::to_bjdata(v, false, false, json::bjdata_version_t::draft2, h)).begin().key() == expected); + } + + CHECK(json::from_ubjson(converting_key_json::to_ubjson(v, false, false, eh::keep)).begin().key() == key); + CHECK(json::from_bjdata(converting_key_json::to_bjdata(v, false, false, json::bjdata_version_t::draft2, eh::keep)).begin().key() == key); + } + } + + SECTION("nested deeper than the recursion limit") + { + // wrap the previous value, innermost first + converting_key_json v = 42; + json expected = 42; + for (int i = 199; i >= 0; --i) + { + const std::string key = "level-" + std::to_string(i) + "-" + std::string(64, 'x'); + + converting_key_json::object_t o; + o.emplace(converting_key(key), std::move(v)); + v = converting_key_json(std::move(o)); + + json e; + e[key] = std::move(expected); + expected = std::move(e); + } + + for (const auto h : all_handlers()) + { + CAPTURE(static_cast(h)) + for (const bool use_count : + { + false, true + }) + { + CAPTURE(use_count) + + CHECK(json::from_ubjson(converting_key_json::to_ubjson(v, use_count, false, h)) == expected); + CHECK(json::from_bjdata(converting_key_json::to_bjdata(v, use_count, false, json::bjdata_version_t::draft2, h)) == expected); + } + } + } +} diff --git a/tests/src/unit-binary_writer_sinks.cpp b/tests/src/unit-binary_writer_sinks.cpp index b780dc0b5..1e18c3f15 100644 --- a/tests/src/unit-binary_writer_sinks.cpp +++ b/tests/src/unit-binary_writer_sinks.cpp @@ -11,8 +11,10 @@ #include using nlohmann::json; +#include #include #include +#include #include #include @@ -35,6 +37,17 @@ std::vector test_values() big_object[std::to_string(i)] = i; } + // large strings and binary values between small ones: a write too large + // for the space the vector sink has ahead takes a different path than a + // small one, and the small writes after it must still land behind it + json large_payloads = json::array(); + for (int i = 0; i < 50; ++i) + { + large_payloads.push_back(std::string(300, static_cast('a' + (i % 26)))); + large_payloads.push_back(i); + large_payloads.push_back(json::binary(std::vector(static_cast(100 * i), static_cast(i)))); + } + return { json(nullptr), json(true), json(false), @@ -46,10 +59,23 @@ std::vector test_values() json::array(), json::object(), json::array({1, 2, 3}), json({{"a", 1}, {"b", nullptr}}), json({{"nested", {{"deep", json::array({1, "two", 3.0, nullptr})}}}}), - big_array, big_object + big_array, big_object, large_payloads, + json::binary(std::vector(5000, 0x42)) }; } +// explicit char -> uint8_t conversion (the iterator-range constructor converts implicitly, which -fsanitize=integer flags) +std::vector to_bytes(const std::string& str) +{ + std::vector result; + result.reserve(str.size()); + for (const char c : str) + { + result.push_back(static_cast(c)); + } + return result; +} + // BON8 has no integers above the int64 range, so to_bon8() rejects them bool bon8_representable(const json& j) { @@ -170,6 +196,258 @@ TEST_CASE("binary writer output sinks") } } +namespace +{ + +// the CBOR head of an integer, encoded independently of binary_writer +void reference_cbor_head(std::vector& out, const std::uint8_t major_type, const std::uint64_t argument) +{ + if (argument <= 23) + { + out.push_back(static_cast(major_type + argument)); + return; + } + + int length = 8; + std::uint8_t additional = 27; + if (argument <= 0xFF) + { + length = 1; + additional = 24; + } + else if (argument <= 0xFFFF) + { + length = 2; + additional = 25; + } + else if (argument <= 0xFFFFFFFF) + { + length = 4; + additional = 26; + } + + out.push_back(static_cast(major_type + additional)); + for (int i = length - 1; i >= 0; --i) + { + out.push_back(static_cast(argument >> (8 * i))); + } +} + +// the CBOR encoding of an integer value +void reference_cbor_integer(std::vector& out, const json& j) +{ + if (j.is_number_unsigned()) + { + reference_cbor_head(out, 0x00, j.get()); + return; + } + + const auto n = j.get(); + if (n >= 0) + { + reference_cbor_head(out, 0x00, static_cast(n)); + } + else + { + reference_cbor_head(out, 0x20, static_cast(-1 - n)); + } +} + +// the MessagePack encoding of an integer value, encoded independently of +// binary_writer +void reference_msgpack_integer(std::vector& out, const json& j) +{ + const auto put = [&out](const std::uint8_t marker, const std::uint64_t bits, const int length) + { + out.push_back(marker); + for (int i = length - 1; i >= 0; --i) + { + out.push_back(static_cast(bits >> (8 * i))); + } + }; + + if (j.is_number_unsigned() || j.get() >= 0) + { + const auto u = j.get(); + if (u < 128) + { + out.push_back(static_cast(u)); + } + else if (u <= 0xFF) + { + put(0xCC, u, 1); + } + else if (u <= 0xFFFF) + { + put(0xCD, u, 2); + } + else if (u <= 0xFFFFFFFF) + { + put(0xCE, u, 4); + } + else + { + put(0xCF, u, 8); + } + return; + } + + const auto n = j.get(); + const auto bits = static_cast(n); + if (n >= -32) + { + out.push_back(static_cast(bits)); + } + else if (n >= -128) + { + put(0xD0, bits, 1); + } + else if (n >= -32768) + { + put(0xD1, bits, 2); + } + else if (n >= -2147483648LL) + { + put(0xD2, bits, 4); + } + else + { + put(0xD3, bits, 8); + } +} + +// integers at every boundary of the CBOR and MessagePack sizes, signed and +// unsigned +std::vector boundary_integers() +{ + std::vector result; + const std::array boundaries = + { + { + 0, 1, 22, 23, 24, 25, 31, 32, 33, 126, 127, 128, 129, 254, 255, 256, 257, + 32766, 32767, 32768, 32769, 65534, 65535, 65536, 65537, + 2147483646ULL, 2147483647ULL, 2147483648ULL, 2147483649ULL, + 4294967294ULL, 4294967295ULL, 4294967296ULL, 4294967297ULL, + 9223372036854775806ULL, 9223372036854775807ULL, 9223372036854775808ULL, + 18446744073709551614ULL, 18446744073709551615ULL + } + }; + for (const std::uint64_t u : boundaries) + { + result.emplace_back(u); + if (u <= static_cast((std::numeric_limits::max)())) + { + const auto n = static_cast(u); + result.emplace_back(n); + result.emplace_back(-n); + result.emplace_back(-n - 1); + } + } + return result; +} + +} // namespace + +// Arrays of integers are written in runs straight into space reserved from the +// sink, with branch-free encodings; check them against independent encoders for +// runs shorter and longer than one reservation, broken up by other values, and +// through each sink. +TEST_CASE("CBOR and MessagePack integer arrays") +{ + const std::vector integers = boundary_integers(); + + for (const std::size_t length : + { + std::size_t{1}, std::size_t{27}, std::size_t{28}, std::size_t{29}, std::size_t{57}, std::size_t{1000} + }) + { + for (const std::size_t break_every : + { + std::size_t{0}, std::size_t{1}, std::size_t{7}, std::size_t{28} + }) + { + CAPTURE(length) + CAPTURE(break_every) + + json array = json::array(); + std::vector expected_cbor; + std::vector expected_msgpack; + for (std::size_t i = 0; i < length; ++i) + { + if (break_every != 0 && i % (break_every + 1) == break_every) + { + array.push_back("x"); + expected_cbor.push_back(0x61); + expected_cbor.push_back('x'); + expected_msgpack.push_back(0xA1); + expected_msgpack.push_back('x'); + } + else + { + const json& value = integers[(i * 7) % integers.size()]; + array.push_back(value); + reference_cbor_integer(expected_cbor, value); + reference_msgpack_integer(expected_msgpack, value); + } + } + + std::vector head; + reference_cbor_head(head, 0x80, length); + expected_cbor.insert(expected_cbor.begin(), head.begin(), head.end()); + + head.clear(); + if (length <= 15) + { + head.push_back(static_cast(0x90 + length)); + } + else + { + head = {0xDC, static_cast(length >> 8), static_cast(length)}; + } + expected_msgpack.insert(expected_msgpack.begin(), head.begin(), head.end()); + + CHECK(json::to_cbor(array) == expected_cbor); + CHECK(json::to_msgpack(array) == expected_msgpack); + + std::vector through_adapter = {0xAA}; + json::to_cbor(array, through_adapter); + CHECK(std::vector(through_adapter.begin() + 1, through_adapter.end()) == expected_cbor); + through_adapter = {0xAA}; + json::to_msgpack(array, through_adapter); + CHECK(std::vector(through_adapter.begin() + 1, through_adapter.end()) == expected_msgpack); + + std::ostringstream stream; + json::to_cbor(array, stream); + std::string streamed = stream.str(); + CHECK(to_bytes(streamed) == expected_cbor); + stream.str(""); + json::to_msgpack(array, stream); + streamed = stream.str(); + CHECK(to_bytes(streamed) == expected_msgpack); + + CHECK(json::from_cbor(expected_cbor) == array); + CHECK(json::from_msgpack(expected_msgpack) == array); + + // the same array as object members + const json object = {{"a", array}, {"b", array}}; + CHECK(json::from_cbor(json::to_cbor(object)) == object); + CHECK(json::from_msgpack(json::to_msgpack(object)) == object); + } + } + + // single values, outside an array + for (const auto& value : integers) + { + CAPTURE(value) + std::vector expected; + reference_cbor_integer(expected, value); + CHECK(json::to_cbor(value) == expected); + expected.clear(); + reference_msgpack_integer(expected, value); + CHECK(json::to_msgpack(value) == expected); + } +} + // binary_reserve_hint() is documented as a *lower* bound on the serialized size, // so that reserving it up front can never leave the returned vector holding // capacity beyond what the value actually needs. @@ -214,3 +492,278 @@ TEST_CASE("binary_reserve_hint never over-reserves") CHECK(nlohmann::detail::binary_reserve_hint(json({{"a", 1}, {"b", 2}})) == 5); } } + +namespace +{ + +// numbers around every boundary of the integer encodings, and floats of each +// kind (exact in float, needing double, special values) +std::vector boundary_numbers() +{ + std::vector numbers; + for (int k = 0; k < 64; ++k) + { + const std::uint64_t p = std::uint64_t{1} << k; + for (const std::uint64_t d : + { + p - 1, p, p + 1 + }) + { + numbers.emplace_back(d); + if (d <= static_cast((std::numeric_limits::max)())) + { + numbers.emplace_back(static_cast(d)); + numbers.emplace_back(-static_cast(d)); + numbers.emplace_back(-static_cast(d) - 1); + } + } + } + numbers.emplace_back((std::numeric_limits::min)()); + numbers.emplace_back((std::numeric_limits::max)()); + for (const double f : + { + 0.0, -0.0, 1.0, -1.0, 0.5, 0.1, 3.25, 1e300, -1e-300, 3.4028234663852886e38, 1e39, + std::numeric_limits::infinity(), -std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN() + }) + { + numbers.emplace_back(f); + } + return numbers; +} + +// the concatenation of the encodings of the elements of @a numbers[first, first + count) +template +std::vector concat_singles(const std::vector& numbers, const std::size_t first, const std::size_t count, Single single) +{ + std::vector bytes; + for (std::size_t i = first; i < first + count; ++i) + { + const auto one = single(numbers[i]); + bytes.insert(bytes.end(), one.begin(), one.end()); + } + return bytes; +} + +} // namespace + +// Runs of numbers in arrays are encoded straight into space reserved from the +// sink, by code that is separate from the encoding of a single value. Both have +// to agree on every number: an array is its head and the encodings of its +// elements, one after the other. +TEST_CASE("runs of numbers in arrays are encoded as the numbers one by one") +{ + const std::vector numbers = boundary_numbers(); + + // arrays of every length that a one-byte head covers, so that runs are as + // long as, and longer and shorter than, what fits one reservation + for (const std::size_t count : + { + 1u, 2u, 3u, 5u, 14u, 15u, 23u + }) + { + for (std::size_t first = 0; first + count <= numbers.size(); first += 7) + { + const json array(std::vector(numbers.begin() + static_cast(first), + numbers.begin() + static_cast(first + count))); + CAPTURE(array.dump(-1, ' ', false, json::error_handler_t::replace)) + + std::vector expected{static_cast(0x80 + count)}; + auto elements = concat_singles(numbers, first, count, [](const json & j) + { + return json::to_cbor(j); + }); + expected.insert(expected.end(), elements.begin(), elements.end()); + CHECK(json::to_cbor(array) == expected); + + expected.assign(1, static_cast(count <= 15 ? 0x90 + count : 0xDC)); + if (count > 15) + { + expected.push_back(0); + expected.push_back(static_cast(count)); + } + elements = concat_singles(numbers, first, count, [](const json & j) + { + return json::to_msgpack(j); + }); + expected.insert(expected.end(), elements.begin(), elements.end()); + CHECK(json::to_msgpack(array) == expected); + + for (const bool bjdata : + { + false, true + }) + { + expected.assign(1, static_cast('[')); + elements = concat_singles(numbers, first, count, [bjdata](const json & j) + { + return bjdata ? json::to_bjdata(j) : json::to_ubjson(j); + }); + expected.insert(expected.end(), elements.begin(), elements.end()); + expected.push_back(static_cast(']')); + CHECK((bjdata ? json::to_bjdata(array) : json::to_ubjson(array)) == expected); + } + + // BON8 has no integers above the int64 range + bool representable = true; + for (std::size_t i = first; i < first + count; ++i) + { + representable = representable && bon8_representable(numbers[i]); + } + if (representable) + { + expected.assign(1, static_cast(count <= 4 ? 0x80 + count : 0x85)); + elements = concat_singles(numbers, first, count, [](const json & j) + { + return json::to_bon8(j); + }); + expected.insert(expected.end(), elements.begin(), elements.end()); + if (count > 4) + { + expected.push_back(0xFE); + } + CHECK(json::to_bon8(array) == expected); + } + else + { + CHECK_THROWS_AS(json::to_bon8(array), json::out_of_range&); + } + } + } +} + +// An array that is written in more than one reservation (more numbers than fit +// one), and arrays of an index with more digits, round trip for every format. +TEST_CASE("large arrays and many digits of indices round trip") +{ + json array = json::array(); + for (int i = 0; i < 12000; ++i) + { + switch (i % 4) + { + case 0: + array.push_back(i); + break; + case 1: + array.push_back(-i); + break; + case 2: + array.push_back(i * 0.25); + break; + default: + array.push_back(i % 8 == 3); + break; + } + } + json document = json::object(); + document["numbers"] = array; + document["nested"] = json::array({array, "text", array}); + + CHECK(json::from_cbor(json::to_cbor(document)) == document); + CHECK(json::from_msgpack(json::to_msgpack(document)) == document); + CHECK(json::from_ubjson(json::to_ubjson(document)) == document); + CHECK(json::from_ubjson(json::to_ubjson(document, true, true)) == document); + CHECK(json::from_bjdata(json::to_bjdata(document)) == document); + CHECK(json::from_bson(json::to_bson(document)) == document); + CHECK(json::from_bon8(json::to_bon8(document)) == document); +} + +// Strings and keys are written with their head in one reservation up to a +// length; around that length, and with every kind of head, they must read back. +TEST_CASE("strings of every head length round trip") +{ + json document = json::object(); + json array = json::array(); + for (const std::size_t length : + { + 0u, 1u, 2u, 3u, 4u, 7u, 8u, 15u, 16u, 17u, 23u, 24u, 25u, 31u, 32u, 33u, 63u, 127u, 128u, 129u, 200u, 245u, 246u, 247u, 248u, 249u, 250u, 251u, 252u, 253u, 254u, 255u, 256u, 257u, 300u, 1000u, 65535u, 65536u, 70000u + }) + { + const std::string text(length, 'x'); + array.push_back(text); + document[std::string(length, 'k')] = text; + } + document["array"] = array; + + CHECK(json::from_cbor(json::to_cbor(document)) == document); + CHECK(json::from_msgpack(json::to_msgpack(document)) == document); + CHECK(json::from_ubjson(json::to_ubjson(document)) == document); + CHECK(json::from_ubjson(json::to_ubjson(document, true, true)) == document); + CHECK(json::from_bjdata(json::to_bjdata(document)) == document); + CHECK(json::from_bson(json::to_bson(document)) == document); + CHECK(json::from_bon8(json::to_bon8(document)) == document); +} + +#if !defined(JSON_NOEXCEPTION) +// What a write that throws leaves behind in an output adapter is what was written +// before it, whatever the adapter and however the adapter sink buffers. (Without +// exceptions, the writes in CHECK_THROWS_AS do not run.) +TEST_CASE("a write that throws leaves the same bytes in every kind of output") +{ + json array = json::array(); + for (int i = 0; i < 700; ++i) + { + array.push_back(i * 1000); + array.push_back("text " + std::to_string(i)); + } + array.push_back("bad \xFF byte"); + json document = json::object(); + document["first"] = array; + document["second"] = 1; + + const auto strict = json::error_handler_t::strict; + + SECTION("cbor") + { + std::vector as_vector; + std::string as_string; + std::ostringstream as_stream; + CHECK_THROWS_AS(json::to_cbor(document, as_vector, strict), json::type_error&); + CHECK_THROWS_AS(json::to_cbor(document, as_string, strict), json::type_error&); + CHECK_THROWS_AS(json::to_cbor(document, as_stream, strict), json::type_error&); + CHECK(!as_vector.empty()); + CHECK(std::string(reinterpret_cast(as_vector.data()), as_vector.size()) == as_string); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + CHECK(as_stream.str() == as_string); + } + + SECTION("msgpack") + { + std::vector as_vector; + std::string as_string; + std::ostringstream as_stream; + CHECK_THROWS_AS(json::to_msgpack(document, as_vector, strict), json::type_error&); + CHECK_THROWS_AS(json::to_msgpack(document, as_string, strict), json::type_error&); + CHECK_THROWS_AS(json::to_msgpack(document, as_stream, strict), json::type_error&); + CHECK(!as_vector.empty()); + CHECK(std::string(reinterpret_cast(as_vector.data()), as_vector.size()) == as_string); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + CHECK(as_stream.str() == as_string); + } + + SECTION("ubjson") + { + std::vector as_vector; + std::string as_string; + std::ostringstream as_stream; + CHECK_THROWS_AS(json::to_ubjson(document, as_vector, false, false, strict), json::type_error&); + CHECK_THROWS_AS(json::to_ubjson(document, as_string, false, false, strict), json::type_error&); + CHECK_THROWS_AS(json::to_ubjson(document, as_stream, false, false, strict), json::type_error&); + CHECK(!as_vector.empty()); + CHECK(std::string(reinterpret_cast(as_vector.data()), as_vector.size()) == as_string); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + CHECK(as_stream.str() == as_string); + } + + SECTION("bson") + { + // the size pass finds the string before anything is written + std::vector as_vector; + std::string as_string; + std::ostringstream as_stream; + CHECK_THROWS_AS(json::to_bson(document, as_vector, strict), json::type_error&); + CHECK_THROWS_AS(json::to_bson(document, as_string, strict), json::type_error&); + CHECK_THROWS_AS(json::to_bson(document, as_stream, strict), json::type_error&); + CHECK(as_vector.empty()); + CHECK(as_string.empty()); + CHECK(as_stream.str().empty()); + } +} +#endif diff --git a/tests/src/unit-bjdata.cpp b/tests/src/unit-bjdata.cpp index 2518f3386..67e9e0a05 100644 --- a/tests/src/unit-bjdata.cpp +++ b/tests/src/unit-bjdata.cpp @@ -19,7 +19,7 @@ #include using nlohmann::json; -using ordered_json = nlohmann::ordered_json; +using nlohmann::ordered_json; #include #include @@ -28,7 +28,6 @@ using ordered_json = nlohmann::ordered_json; #include #include #include "make_test_data_available.hpp" -#include "round_trip_corpus.hpp" #include "test_utils.hpp" #include "sax_countdown.hpp" using utils::SaxCountdown; @@ -4441,70 +4440,6 @@ TEST_CASE("BJData use_type requires use_size") } } -TEST_CASE("BJData round-trip invariants") -{ - // This checks what the parse_bjdata_fuzzer driver checks (see - // tests/src/fuzzer-parse_bjdata.cpp), so that a regression shows up in CI - // rather than as an OSS-Fuzz report: every value from_bjdata() returns - // (j1) can be serialized with any combination of options, the result can - // be parsed back (j2), and serializing j2 again with the same options - // yields a value-equal result. - // - // Beyond the driver, this also checks that j2 equals j1 and that - // serializing j2 reproduces the exact bytes, both except for values that - // contain a binary value: a binary value is only written as a binary - // value with Draft 3's optimized binary array, and otherwise read back as - // an array of integers, for which the writer may choose different (but - // equally valid) type markers when it is serialized again (see #5494). - // - // Values are compared with dump() rather than operator==, because a NaN - // never compares equal to itself. - struct options - { - bool use_size; - bool use_type; - json::bjdata_version_t version; - }; - const std::vector all_options = - { - {false, false, json::bjdata_version_t::draft2}, - {true, false, json::bjdata_version_t::draft2}, - {true, true, json::bjdata_version_t::draft2}, - {false, false, json::bjdata_version_t::draft3}, - {true, false, json::bjdata_version_t::draft3}, - {true, true, json::bjdata_version_t::draft3}, - }; - - for (const auto& j0 : utils::round_trip_corpus::values()) - { - // turn the corpus value into a value as from_bjdata() returns it - for (const auto& initial : all_options) - { - const json j1 = json::from_bjdata(json::to_bjdata(j0, initial.use_size, initial.use_type, initial.version)); - const bool has_binary = utils::round_trip_corpus::contains_binary(j1); - - for (const auto& o : all_options) - { - INFO("j1 = " << j1.dump() << ", use_size = " << o.use_size << ", use_type = " << o.use_type - << ", draft3 = " << (o.version == json::bjdata_version_t::draft3)); - - const std::vector vec = json::to_bjdata(j1, o.use_size, o.use_type, o.version); - json j2; - // anything the library writes must be parsable by the library - REQUIRE_NOTHROW(j2 = json::from_bjdata(vec)); - const std::vector vec2 = json::to_bjdata(j2, o.use_size, o.use_type, o.version); - CHECK(json::from_bjdata(vec2).dump() == j2.dump()); - - if (!has_binary) - { - CHECK(j2.dump() == j1.dump()); - CHECK(vec2 == vec); - } - } - } - } -} - TEST_CASE("BJData round trip of a binary value is value-stable, not byte-stable") { // OSS-Fuzz issue 474480402: a Draft 3 optimized binary array is read as a diff --git a/tests/src/unit-bjdata_round_trip_draft2.cpp b/tests/src/unit-bjdata_round_trip_draft2.cpp new file mode 100644 index 000000000..6bbe6ea06 --- /dev/null +++ b/tests/src/unit-bjdata_round_trip_draft2.cpp @@ -0,0 +1,22 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; + +#include "bjdata_round_trip.hpp" + +// split from unit-bjdata.cpp, see bjdata_round_trip.hpp; the other half is in +// unit-bjdata_round_trip_draft3.cpp + +TEST_CASE("BJData round-trip invariants (j1 written with Draft 2)") +{ + utils::check_bjdata_round_trip(json::bjdata_version_t::draft2); +} diff --git a/tests/src/unit-bjdata_round_trip_draft3.cpp b/tests/src/unit-bjdata_round_trip_draft3.cpp new file mode 100644 index 000000000..97e7517b6 --- /dev/null +++ b/tests/src/unit-bjdata_round_trip_draft3.cpp @@ -0,0 +1,22 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; + +#include "bjdata_round_trip.hpp" + +// split from unit-bjdata.cpp, see bjdata_round_trip.hpp; the other half is in +// unit-bjdata_round_trip_draft2.cpp + +TEST_CASE("BJData round-trip invariants (j1 written with Draft 3)") +{ + utils::check_bjdata_round_trip(json::bjdata_version_t::draft3); +} diff --git a/tests/src/unit-bon8.cpp b/tests/src/unit-bon8.cpp index b1d82ca29..1e24d32d4 100644 --- a/tests/src/unit-bon8.cpp +++ b/tests/src/unit-bon8.cpp @@ -82,9 +82,56 @@ TEST_CASE("BON8") { SECTION("discarded") { - // discarded values are not serialized + // a discarded value cannot be serialized to BON8 const json j = json::value_t::discarded; - CHECK(json::to_bon8(j).empty()); + CHECK_THROWS_WITH_AS(json::to_bon8(j), "[json.exception.type_error.321] cannot serialize discarded value to BON8", json::type_error&); + } + + SECTION("discarded values nested in a container") + { + const json discarded = json::value_t::discarded; + + SECTION("in a small array") + { + const json j = {1, discarded}; +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(json::to_bon8(j), "[json.exception.type_error.321] (/1) cannot serialize discarded value to BON8", json::type_error&); +#else + CHECK_THROWS_WITH_AS(json::to_bon8(j), "[json.exception.type_error.321] cannot serialize discarded value to BON8", json::type_error&); +#endif + } + + SECTION("as a small object value") + { + json j; + j["a"] = discarded; +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(json::to_bon8(j), "[json.exception.type_error.321] (/a) cannot serialize discarded value to BON8", json::type_error&); +#else + CHECK_THROWS_WITH_AS(json::to_bon8(j), "[json.exception.type_error.321] cannot serialize discarded value to BON8", json::type_error&); +#endif + } + + SECTION("in a large array") + { + const json j = {1, 2, 3, 4, 5, discarded}; +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(json::to_bon8(j), "[json.exception.type_error.321] (/5) cannot serialize discarded value to BON8", json::type_error&); +#else + CHECK_THROWS_WITH_AS(json::to_bon8(j), "[json.exception.type_error.321] cannot serialize discarded value to BON8", json::type_error&); +#endif + } + + SECTION("as a large object value") + { + json j = {{"a", 1}, {"b", 2}, {"c", 3}, {"d", 4}, {"e", 5}}; + j["f"] = discarded; +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(json::to_bon8(j), "[json.exception.type_error.321] (/f) cannot serialize discarded value to BON8", json::type_error&); +#else + CHECK_THROWS_WITH_AS(json::to_bon8(j), "[json.exception.type_error.321] cannot serialize discarded value to BON8", json::type_error&); +#endif + } } SECTION("null") diff --git a/tests/src/unit-bson.cpp b/tests/src/unit-bson.cpp index ea5ca3130..03c901cee 100644 --- a/tests/src/unit-bson.cpp +++ b/tests/src/unit-bson.cpp @@ -46,9 +46,7 @@ class huge_binary_t : public std::vector } }; -using huge_binary_json = nlohmann::basic_json < - std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, - double, std::allocator, nlohmann::adl_serializer, huge_binary_t, void >; +using huge_binary_json = nlohmann::json::with_binary_t; // a string type that can be made to report a size beyond INT32_MAX without // allocating that much memory, so BSON length overflow can be tested for @@ -96,9 +94,7 @@ class huge_string_t : public std::string bool pretend_huge = false; }; -using huge_string_json = nlohmann::basic_json < - std::map, std::vector, huge_string_t, bool, std::int64_t, std::uint64_t, - double, std::allocator, nlohmann::adl_serializer, std::vector, void >; +using huge_string_json = nlohmann::json::with_string_t; } // namespace TEST_CASE("BSON") diff --git a/tests/src/unit-class_lexer.cpp b/tests/src/unit-class_lexer.cpp index 860af81d6..2926ef88b 100644 --- a/tests/src/unit-class_lexer.cpp +++ b/tests/src/unit-class_lexer.cpp @@ -13,16 +13,24 @@ using nlohmann::json; #include // array -#include // FLT_EVAL_METHOD +#include // toupper #include // uint32_t, uint64_t +#include // snprintf +#include // isfinite #include // strtod #include // memcpy #include // numeric_limits +#include // initializer_list +#include // list +#include // map +#include // mt19937 #include // stringstream #include // string #include // pair #include // vector +#include "float_hard_cases.hpp" + namespace { // shortcut to scan a string literal @@ -258,7 +266,7 @@ TEST_CASE("lexer number fast path") "123456789012345678901234567890", // huge -> float "0.30000000000000004", "2.2250738585072014e-308", "1e308", // high-precision / wide-exponent values that exercise the - // std::from_chars (Eisel-Lemire) path beyond the Clinger subset + // Eisel-Lemire path beyond the Clinger subset "1.7976931348623157e308", "1.2345678901234567e-250", "9007199254740993", "5e-324", "1e-320" }; @@ -280,20 +288,18 @@ TEST_CASE("lexer number fast path") } } - SECTION("significant-digit gate for the Clinger fast path") + SECTION("significant digits around Clinger's fast path") { - // Clinger's fast path needs a significand below 2^53, so it cannot - // succeed once the mantissa has 17 or more significant digits (the - // significand would be at least 10^16). The lexer skips the attempt - // there. That is only allowed to save work: every value must still come - // out bit-exactly, and both scanners must agree. In particular the gate - // must not fire for tokens whose leading zeros merely look like extra - // digits - "0.1234567890123456" has 16 significant digits, not 17. + // Clinger's fast path needs a significand of at most 2^53, which + // tokens with 17 or more significant digits exceed. The conversion + // splits the token at the positions the scanners recorded, so leading + // zeros must not count as digits - "0.1234567890123456" has 16 + // significant digits, not 17 - and both scanners must agree. const std::vector numbers = { "1234567890123456", // 16 significant digits - "12345678901234567", // 17 -> attempt skipped - "123456789012345678", // 18 -> attempt skipped + "12345678901234567", // 17 + "123456789012345678", // 18 "0.1234567890123456", // 16: the leading "0" is not significant "0.12345678901234567", // 17 "0.00000000000000001", // 1, in a long token @@ -330,6 +336,112 @@ TEST_CASE("lexer number fast path") } } +#if !defined(JSON_NOEXCEPTION) + // this section reports parse errors, which abort when exceptions are off + SECTION("digits accumulated while scanning") + { + // The fast path accumulates the digits while it validates them, eight + // at a time where possible, and converts tokens with at most 19 + // digits (floats) or 18 digits (integers) from that value. Every digit + // count from 1 to 25, in the integer and in the fraction part, with + // and without sign and exponent, must give the byte path's result. A + // std::list has no contiguous storage, so parsing it takes the byte + // path. + // type and value, or the error (values beyond the range of double + // are rejected with out_of_range.406 on both paths) + const auto outcome = [](const std::string & doc, bool byte_path) -> std::string + { + JSON_TRY + { + const std::list chars(doc.begin(), doc.end()); + const json j = byte_path ? json::parse(chars.begin(), chars.end()) : json::parse(doc); + return std::string(j[0].type_name()) + '|' + j.dump(); + } + JSON_CATCH (const json::exception & e) + { + return e.what(); + } + }; + + // the concatenation of @a parts + const auto join = [](std::initializer_list parts) + { + std::string result; + for (const auto& part : parts) + { + result += part; + } + return result; + }; + + std::vector numbers; + for (std::size_t digits = 1; digits <= 25; ++digits) + { + std::string run; + for (std::size_t i = 0; i < digits; ++i) + { + run += static_cast('1' + static_cast((i * 7) % 9)); + } + const std::string nines(digits, '9'); + for (const char* sign_text : + {"", "-" + }) + { + const std::string sign = sign_text; + numbers.push_back(join({sign, run})); + numbers.push_back(join({sign, nines})); + numbers.push_back(join({sign, "1", std::string(digits - 1, '0')})); + numbers.push_back(join({sign, "0.", run})); + numbers.push_back(join({sign, "0.", std::string(digits, '0'), "1"})); + numbers.push_back(join({sign, run, ".", run})); + numbers.push_back(join({sign, run, "e-", std::to_string(digits)})); + numbers.push_back(join({sign, "1.", run, "E+", std::to_string(digits * 13)})); + } + } + // exponents beyond every range, and zeros + numbers.insert(numbers.end(), {"1e400", "1e-400", "-1e99999999999999999999", "1e-99999999999999999999", + "0e99999999999999999999", "0.0e-5", "-0.000", "123456789e-0000000000000000000000330" + }); + + for (const auto& n : numbers) + { + CAPTURE(n) + const std::string doc = "[" + n + "]"; + const std::string a = outcome(doc, false); + CHECK(a == outcome(doc, true)); + const double expected = std::strtod(n.c_str(), nullptr); + if (a.compare(0, 6, "number") == 0 && std::isfinite(expected) && n.find_first_of(".eE") != std::string::npos) + { + CHECK(json::parse(doc)[0].get() == expected); + } + } + } +#endif + + SECTION("integers that do not fit a narrower type become floats") + { + // up to 18 digits, the fast path stores the accumulated value without + // an overflow check; a narrower number_integer_t/number_unsigned_t + // must still see the value rejected and converted as a float + using json32 = nlohmann::basic_json; + for (const char* n : + {"4294967295", "4294967296", "2147483647", "-2147483648", "-2147483649", "99999999999", "-99999999999" + }) + { + CAPTURE(n) + const std::string doc = std::string("[") + n + "]"; + const std::list chars(doc.begin(), doc.end()); + const json32 a = json32::parse(doc); + const json32 b = json32::parse(chars.begin(), chars.end()); + CHECK(a[0].type() == b[0].type()); + CHECK(a == b); + } + CHECK(json32::parse("[4294967295]")[0].is_number_unsigned()); + CHECK(json32::parse("[4294967296]")[0].is_number_float()); + CHECK(json32::parse("[-2147483648]")[0].is_number_integer()); + CHECK(json32::parse("[-2147483649]")[0].is_number_float()); + } + SECTION("token type classification") { CHECK((scan_string("0") == json::lexer::token_type::value_unsigned)); @@ -488,6 +600,67 @@ TEST_CASE("lexer number fast path") #endif } +#if !defined(JSON_NOEXCEPTION) +// this test case compares the errors of both lexer paths, which abort when +// exceptions are off +TEST_CASE("lexer literal fast path") +{ + // For contiguous input, the rest of a literal is compared at once, and a + // mismatch is left to the byte path, which reports the error at the + // character that differs. Every truncation and every wrong character of + // each literal, at the end of the input or followed by more text, must + // give the byte path's result (a std::list is read one character at a + // time). + const auto outcome = [](const std::string & doc, bool byte_path) -> std::string + { + JSON_TRY + { + const std::list chars(doc.begin(), doc.end()); + return (byte_path ? json::parse(chars.begin(), chars.end()) : json::parse(doc)).dump(); + } + JSON_CATCH (const json::exception & e) + { + return e.what(); + } + }; + + std::vector mismatches; + for (const std::string literal : + {"true", "false", "null" + }) + { + std::vector variants; + for (std::size_t length = 1; length <= literal.size(); ++length) + { + variants.push_back(literal.substr(0, length)); + } + for (std::size_t i = 1; i < literal.size(); ++i) + { + std::string changed = literal; + changed[i] = 'X'; + variants.push_back(changed); + changed[i] = static_cast(std::toupper(static_cast(literal[i]))); + variants.push_back(changed); + } + for (const auto& variant : variants) + { + for (const std::string& doc : + { + variant, "[" + variant + "]", "[" + variant, variant + " ", variant + "1", "{\"k\":" + variant + "}" + }) + { + if (outcome(doc, false) != outcome(doc, true)) + { + mismatches.push_back(doc); + } + } + } + } + CAPTURE(mismatches) + CHECK(mismatches.empty()); +} +#endif + TEST_CASE("lexer string fast path") { // Build a byte string from explicit values: a hex escape in a string @@ -608,6 +781,73 @@ TEST_CASE("lexer string fast path") CAPTURE(mismatches) CHECK(mismatches.empty()); } + + SECTION("special bytes at every offset of the vector blocks") + { + // With SSE2 or NEON, runs are scanned 16 bytes at a time (after one + // SWAR word, or four with NEON), and runs of non-ASCII text are + // checked 16 bytes at a time as well. Place every kind of byte that + // ends a run at each offset across several blocks, after ASCII and + // after CJK text, so sequences also straddle the block boundaries. + // The reference is a std::list, which has no contiguous storage and + // is therefore read one character at a time. + const auto byte_path = [](const std::string & doc) -> std::string + { + const std::list chars(doc.begin(), doc.end()); + JSON_TRY + { + return json::parse(chars.begin(), chars.end()).dump(); + } + JSON_CATCH (const json::exception & e) + { + return e.what(); + } + }; + const std::vector specials = + { + "\"", "\\", "\\u00e9", bytes({0x01}), bytes({0x1F}), bytes({0x7F}), + bytes({0xC3, 0xA9}), bytes({0xE4, 0xB8, 0xAD}), bytes({0xF0, 0x9F, 0x98, 0x80}), + bytes({0xFF}), bytes({0xC3}), bytes({0xE4, 0xB8}), bytes({0xED, 0xA0, 0x80}), bytes({0xC0, 0x80}) + }; + const std::string cjk = bytes({0xE4, 0xB8, 0xAD}); // U+4E2D + + std::vector mismatches; + for (std::size_t offset = 0; offset <= 80; ++offset) + { + std::string text; + while (text.size() < offset) + { + text += cjk; + } + for (const auto& prefix : + { + std::string(offset, 'a'), text + }) + { + for (const auto& special : specials) + { + // the closing quote right behind, or in a later block + for (const int tail : + { + 0, 1, 3, 20 + }) + { + std::string doc = "[\""; + doc += prefix; + doc += special; + doc.append(static_cast(tail), 'b'); + doc += "\"]"; + if (outcome(doc, false) != byte_path(doc)) + { + mismatches.push_back(doc); + } + } + } + } + } + CAPTURE(mismatches) + CHECK(mismatches.empty()); + } #endif // json::accept() never throws, so the ranges stay covered without exceptions @@ -664,46 +904,323 @@ TEST_CASE("lexer string fast path") } } -TEST_CASE("parse_float_fast declines what it cannot convert exactly") +TEST_CASE("lexer escape fast path") { - // The lexer only hands well-formed numbers to parse_float_fast, so the - // malformed ones below can only be passed to it directly. Declining is - // always safe: the caller then falls back to a slower, exact conversion. - const auto fast = [](const std::string & s, double & out) + // json::accept() never throws, so this section stays covered without + // exceptions; it pins which of the cases below are valid/invalid and + // checks the contiguous and streaming paths agree on that classification. + SECTION("accept() parity") { - return nlohmann::detail::parse_float_fast(s.data(), s.data() + s.size(), out); + const std::vector> cases = + { + {"\\u0041", true}, {"\\u00e4", true}, {"\\u00E4", true}, + {"\\uD83D\\uDE00", true}, + {"\\u12", false}, {"\\u12G4", false}, {"\\uXYZW", false}, + {"\\uD800", false}, {"\\uD800A", false}, {"\\uD800\\u0041", false}, + {"\\uDC00", false}, {"\\u", false} + }; + + for (const auto& c : cases) + { + for (const std::size_t offset : + { + std::size_t{0}, std::size_t{9} + }) + { + const std::string doc = "[\"" + std::string(offset, 'a') + c.first + "\"]"; + CAPTURE(doc) + CHECK(json::accept(doc) == c.second); + std::stringstream ss(doc); + CHECK(json::accept(ss) == c.second); + } + } + } + +#if !defined(JSON_NOEXCEPTION) + // the full outcome of parsing @a doc: the parsed value, or the exact + // error message, so a mismatch in either is caught + const auto outcome = [](const std::string & doc, bool streaming) -> std::string + { + try + { + if (streaming) + { + std::stringstream ss(doc); + const json j = json::parse(ss); + return j.dump(); + } + const json j = json::parse(doc); + return j.dump(); + } + catch (const json::exception& e) + { + return {e.what()}; + } }; - double out = 0; -#if defined(FLT_EVAL_METHOD) && FLT_EVAL_METHOD != 0 - // without true double precision, the fast path declines everything - CHECK_FALSE(fast("1.5", out)); -#else - CHECK(fast("1.5", out)); - CHECK(out == 1.5); - CHECK(fast("+2.5e1", out)); - CHECK(out == 25.0); - CHECK(fast("-25E-1", out)); - CHECK(out == -2.5); - CHECK(fast("1e", out)); - CHECK(out == 1.0); + SECTION("contiguous vs streaming parity") + { + const std::vector escapes = + { + "\\u0041", // "A" + "\\u00e4", // "ä" (lowercase hex) + "\\u00E4", // "ä" (uppercase hex) + "\\uD83D\\uDE00", // valid surrogate pair (an emoji) + "\\u12", // truncated: only 2 hex digits before the closing quote + "\\u12G4", // invalid hex digit at the 3rd position + "\\uXYZW", // all 4 bytes invalid + "\\uD800", // lone high surrogate, string ends right after + "\\uD800A", // high surrogate not followed by another \u escape + "\\uD800\\u0041", // high surrogate followed by \u, but not a low surrogate + "\\uDC00", // lone low surrogate + "\\u", // '\u' with nothing after (closing quote right away) + }; + + // once at the start of the string and once past the first 8-byte SWAR + // word of the outer string_bulk_run, so the escape is reached both + // right after the opening quote and mid-run + for (const auto& escape : escapes) + { + for (const std::size_t offset : + { + std::size_t{0}, std::size_t{9} + }) + { + const std::string doc = "[\"" + std::string(offset, 'a') + escape + "\"]"; + CAPTURE(doc) + CHECK(outcome(doc, false) == outcome(doc, true)); + } + + // the escape is the last thing before end of input: no closing + // quote at all + const std::string truncated_doc = "[\"" + escape; + CAPTURE(truncated_doc) + CHECK(outcome(truncated_doc, false) == outcome(truncated_doc, true)); + } + } + + SECTION("truncated \\u escape at every distance from the end of input") + { + // ia.bulk_remaining() must correctly report fewer than 4 bytes for + // every possible count of trailing hex-looking bytes (0, 1, 2, or 3) + // before end of input, so the fast path declines and the byte path + // alone reports the "must be followed by 4 hex digits" error, at the + // same position, in every case + for (const std::string& tail : + { + std::string{}, std::string("1"), std::string("12"), std::string("123") + }) + { + const std::string doc = "[\"\\u" + tail; + CAPTURE(doc) + CHECK(outcome(doc, false) == outcome(doc, true)); + CHECK(outcome(doc, false).find("must be followed by 4 hex digits") != std::string::npos); + } + } + + SECTION("invalid hex digit at every position of the 4") + { + // the fast path must decline for *any* invalid byte among the 4, not + // just the first, and the byte path must then stop at exactly that + // position - same as it always has + for (std::size_t bad_pos = 0; bad_pos < 4; ++bad_pos) + { + std::string digits = "1234"; + digits[bad_pos] = 'g'; // not a hex digit + const std::string doc = "[\"\\u" + digits + "\"]"; + CAPTURE(doc) + CHECK(outcome(doc, false) == outcome(doc, true)); + CHECK(outcome(doc, false).find("must be followed by 4 hex digits") != std::string::npos); + } + } + + SECTION("random escapes") + { + // A seeded PRNG builds the 4 bytes following `\u` from a mix of hex + // digits and non-hex bytes, at varying distances from the start of + // the string, to compare the two scanners on many more shapes than + // are practical to enumerate by hand. + std::mt19937 gen(7654321); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) + const std::string hex_alphabet = "0123456789AaBbCcDdEeFf"; + std::uniform_int_distribution pick_hex(0, hex_alphabet.size() - 1); + std::uniform_int_distribution pick_byte(1, 255); // never NUL + std::uniform_int_distribution pick_is_hex(0, 4); // 4-in-5 chance of a hex digit + std::uniform_int_distribution pick_offset(0, 12); + + std::vector mismatches; + for (int iter = 0; iter < 3000; ++iter) + { + std::string digits; + for (int i = 0; i < 4; ++i) + { + if (pick_is_hex(gen) != 0) + { + digits += hex_alphabet[pick_hex(gen)]; + } + else + { + char c = static_cast(pick_byte(gen)); + if (c == '"' || c == '\\') + { + // keep the string well-formed apart from the escape + // itself, so any mismatch is attributable to the \u + // handling and not to an unrelated quote/escape + c = 'z'; + } + digits += c; + } + } + const std::string doc = "[\"" + std::string(pick_offset(gen), 'a') + "\\u" + digits + "\"]"; + if (outcome(doc, false) != outcome(doc, true)) + { + mismatches.push_back(doc); + } + } + CAPTURE(mismatches) + CHECK(mismatches.empty()); + } #endif +} - // not a number - CHECK_FALSE(fast("", out)); - CHECK_FALSE(fast("-", out)); - CHECK_FALSE(fast(".", out)); - CHECK_FALSE(fast("1.2.3", out)); - CHECK_FALSE(fast("1x", out)); - CHECK_FALSE(fast("1e+", out)); - CHECK_FALSE(fast("1e1x", out)); +namespace +{ +// the index of the decimal point (or npos) and of the end of the mantissa of a +// number token, which the lexer records while scanning it +std::pair float_token_layout(const std::string& s) +{ + std::size_t dot = std::string::npos; + std::size_t mantissa_end = s.size(); + for (std::size_t i = 0; i < s.size(); ++i) + { + if (s[i] == '.') + { + dot = i; + } + else if (s[i] == 'e' || s[i] == 'E') + { + mantissa_end = i; + break; + } + } + return {dot, mantissa_end}; +} - // numbers that are not represented exactly on the fast path - CHECK_FALSE(fast("12345678901234567890", out)); - CHECK_FALSE(fast("1e10000", out)); - CHECK_FALSE(fast("9007199254740993", out)); - CHECK_FALSE(fast("1e23", out)); - CHECK_FALSE(fast("1e-23", out)); +template +FloatType parse_native(const std::string& s) +{ + const auto layout = float_token_layout(s); + return nlohmann::detail::parse_float_native(s.data(), s.data() + s.size(), layout.first, layout.second); +} + +std::uint64_t bits_of(double d) +{ + std::uint64_t b = 0; + std::memcpy(&b, &d, sizeof(b)); + return b; +} + +std::uint32_t bits_of(float f) +{ + std::uint32_t b = 0; + std::memcpy(&b, &f, sizeof(b)); + return b; +} + +std::uint64_t native_bits64(const std::string& s) +{ + return bits_of(parse_native(s)); +} + +std::uint32_t native_bits32(const std::string& s) +{ + return bits_of(parse_native(s)); +} +} // namespace + +TEST_CASE("parse_float_native rounds correctly") +{ + SECTION("double") + { + CHECK(native_bits64("1.5") == 0x3FF8000000000000u); + CHECK(native_bits64("0.1") == 0x3FB999999999999Au); + CHECK(native_bits64("-0.0") == 0x8000000000000000u); + CHECK(native_bits64("0e999999999999999999999") == 0u); + // 2^53 + 1 is exactly between two doubles: ties to even, unless more digits follow + CHECK(native_bits64("9007199254740993") == 0x4340000000000000u); + CHECK(native_bits64("9007199254740993.0000000000000000001") == 0x4340000000000001u); + CHECK(native_bits64("9007199254740992.9999999999999999999") == 0x4340000000000000u); + // 1 + 2^-53 exactly (a tie), and one unit in the 55th digit around it + CHECK(native_bits64("1.00000000000000011102230246251565404236316680908203125") == 0x3FF0000000000000u); + CHECK(native_bits64("1.00000000000000011102230246251565404236316680908203126") == 0x3FF0000000000001u); + CHECK(native_bits64("1.00000000000000011102230246251565404236316680908203124") == 0x3FF0000000000000u); + // subnormal and overflow boundaries + CHECK(native_bits64("2.4703282292062327e-324") == 0u); + CHECK(native_bits64("2.4703282292062328e-324") == 1u); + CHECK(native_bits64("2.2250738585072011e-308") == 0x000FFFFFFFFFFFFFu); + CHECK(native_bits64("2.2250738585072012e-308") == 0x0010000000000000u); + CHECK(native_bits64("1.7976931348623157e308") == 0x7FEFFFFFFFFFFFFFu); + CHECK(native_bits64("1.7976931348623159e308") == 0x7FF0000000000000u); + CHECK(native_bits64("-1e400") == 0xFFF0000000000000u); + CHECK(native_bits64("-1e-400") == 0x8000000000000000u); + // exponents and zeros far beyond the range cancel out + CHECK(native_bits64("0." + std::string(1000, '0') + "1e1001") == 0x3FF0000000000000u); + CHECK(native_bits64("1" + std::string(1000, '0') + "e-1000") == 0x3FF0000000000000u); + CHECK(native_bits64("1e-99999999999999999999999") == 0u); + CHECK(native_bits64("1E+99999999999999999999999") == 0x7FF0000000000000u); + // more digits than any midpoint has (769): only whether a nonzero digit follows matters + const std::string tie = "1.00000000000000011102230246251565404236316680908203125"; + CHECK(native_bits64(tie + std::string(800, '0')) == 0x3FF0000000000000u); + CHECK(native_bits64(tie + std::string(800, '0') + "1") == 0x3FF0000000000001u); + } + + SECTION("float") + { + CHECK(native_bits32("1.5") == 0x3FC00000u); + CHECK(native_bits32("0.1") == 0x3DCCCCCDu); + CHECK(native_bits32("-0.0") == 0x80000000u); + // 2^24 + 1 is exactly between two floats + CHECK(native_bits32("16777217") == 0x4B800000u); + CHECK(native_bits32("16777217.000000000000000000001") == 0x4B800001u); + CHECK(native_bits32("16777218.999999999999999999999") == 0x4B800001u); + CHECK(native_bits32("16777219") == 0x4B800002u); + // subnormal and overflow boundaries + CHECK(native_bits32("3.4028235677973366e38") == 0x7F7FFFFFu); + CHECK(native_bits32("3.4028235677973367e38") == 0x7F800000u); + CHECK(native_bits32("7.006492321624085e-46") == 0u); + CHECK(native_bits32("7.006492321624086e-46") == 1u); + CHECK(native_bits32("1.1754942e-38") == 0x007FFFFFu); + CHECK(native_bits32("-1.17549435e-38") == 0x80800000u); + CHECK(native_bits32("1e39") == 0x7F800000u); + CHECK(native_bits32("-1e-50") == 0x80000000u); + // not rounded through double: its double would round to another float + CHECK(native_bits32("1.00000005960464477539062500000000001") == 0x3F800001u); + CHECK(native_bits32("9007199254740993") == 0x5A000000u); + } + + SECTION("the conversion shared with other parsers") + { + // convert_float() gives the lexer's results, for every type + const std::vector tokens = + { + "0", "-0.0", "1.5", "0.1", "1e-400", "-2.5E+3", "123456789012345678901234567890", + "9007199254740993.0000000000000000001", "4.9406564584124654e-324" + }; + using float_json = nlohmann::basic_json; + using long_double_json = nlohmann::basic_json; + for (const auto& t : tokens) + { + CAPTURE(t) + const auto layout = float_token_layout(t); + const char* const first = t.data(); + const char* const last = first + t.size(); + const auto d = nlohmann::detail::convert_float(first, last, layout.first, layout.second); + const auto f = nlohmann::detail::convert_float(first, last, layout.first, layout.second); + const auto ld = nlohmann::detail::convert_float(first, last, layout.first, layout.second); + CHECK(bits_of(d) == bits_of(json::parse(t).get())); + CHECK(bits_of(f) == bits_of(float_json::parse(t).get())); + CHECK(ld == long_double_json::parse(t).get()); + } + } } namespace @@ -807,40 +1324,6 @@ std::size_t big_bit_length(const big_uint& a) } return n; } - -std::uint64_t bits_of(double d) -{ - std::uint64_t b = 0; - std::memcpy(&b, &d, sizeof(b)); - return b; -} - -bool eisel_lemire(const std::string& s, double& out) -{ - return nlohmann::detail::parse_float_eisel_lemire(s.data(), s.data() + s.size(), out); -} - -// significant digits of a token, without trailing zeros -std::size_t significant_digits(const std::string& s) -{ - std::string digits; - for (const char c : s) - { - if (c == 'e' || c == 'E') - { - break; - } - if (c >= '0' && c <= '9' && !(digits.empty() && c == '0')) - { - digits += c; - } - } - while (!digits.empty() && digits.back() == '0') - { - digits.pop_back(); - } - return digits.size(); -} } // namespace TEST_CASE("Eisel-Lemire float conversion") @@ -938,6 +1421,10 @@ TEST_CASE("Eisel-Lemire float conversion") const int k = i % 64; const std::uint64_t x = (std::uint64_t{1} << k) | (a & ((std::uint64_t{1} << k) - 1)); CHECK(nlohmann::detail::count_leading_zeros(x) == 63 - k); + + // the mirror image: bit k set, arbitrary bits above it + const std::uint64_t y = (std::uint64_t{1} << k) | (k == 63 ? 0 : (a << (k + 1))); + CHECK(nlohmann::detail::count_trailing_zeros(y) == k); } } @@ -1268,26 +1755,33 @@ TEST_CASE("Eisel-Lemire float conversion") for (const auto& c : known) { CAPTURE(c.first) - double out = 0; - if (eisel_lemire(c.first, out)) - { - CHECK(bits_of(out) == c.second); - } - else - { - // only tokens with more than 19 significant digits are left to - // strtod: those whose value lies too close to a tie - CHECK(significant_digits(c.first) > 19); - } + CHECK(native_bits64(c.first) == c.second); } } + SECTION("binary32") + { + using binary32 = nlohmann::detail::ieee_binary_format<24>; + CHECK(nlohmann::detail::eisel_lemire(0, 1) == 0x3F800000u); + CHECK(nlohmann::detail::eisel_lemire(-1, 1) == 0x3DCCCCCDu); + CHECK(nlohmann::detail::eisel_lemire(-1, 15) == 0x3FC00000u); + CHECK(nlohmann::detail::eisel_lemire(0, 16777217) == 0x4B800000u); // tie, to even + CHECK(nlohmann::detail::eisel_lemire(0, 16777219) == 0x4B800002u); // tie, to even + CHECK(nlohmann::detail::eisel_lemire(-45, 1) == 0x00000001u); + CHECK(nlohmann::detail::eisel_lemire(-46, 7) == 0x00000000u); + CHECK(nlohmann::detail::eisel_lemire(-46, 8) == 0x00000001u); + CHECK(nlohmann::detail::eisel_lemire(-65, 9999999999999999999u) == 0x00000000u); + CHECK(nlohmann::detail::eisel_lemire(20, 3402823466385288598u) == 0x7F7FFFFFu); + CHECK(nlohmann::detail::eisel_lemire(20, 3402823669209384635u) == 0x7F800000u); + CHECK(nlohmann::detail::eisel_lemire(39, 1) == 0x7F800000u); + CHECK(nlohmann::detail::eisel_lemire(-5, 0) == 0x00000000u); + } + SECTION("round trip") { - // every double written by to_chars and read back, also with trailing - // digits that make the token longer than 19 digits + // every double written by to_chars and read back, and its 17-digit + // form with trailing digits that make the token longer than 19 digits std::uint64_t state = 5295; - std::size_t declined = 0; for (int i = 0; i < 200000; ++i) { state ^= state << 13u; @@ -1309,30 +1803,51 @@ TEST_CASE("Eisel-Lemire float conversion") const char* end = nlohmann::detail::to_chars(buffer.data(), buffer.data() + buffer.size(), d); const std::string token(buffer.data(), static_cast(end - buffer.data())); CAPTURE(token) - double out = 0; - REQUIRE(eisel_lemire(token, out)); - CHECK(bits_of(out) == b); + CHECK(native_bits64(token) == b); - // insert digits before the exponent: the value moves by far less - // than the distance to the rounding boundary, so it must not change - std::string longer = token; + // insert digits before the exponent of the 17-digit form: that + // form lies strictly inside the rounding interval of the double + // (the shortest one may lie on its boundary), and the digits move + // it by far less than the distance to the boundary, so the value + // must not change + std::array digits17{}; + static_cast(std::snprintf(digits17.data(), digits17.size(), "%.17g", d)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + std::string longer = digits17.data(); const std::size_t e = longer.find('e'); const std::size_t dot = longer.find('.'); const std::string extra = dot == std::string::npos ? ".000000000000000000001" : "000000000000000000001"; longer.insert(e == std::string::npos ? longer.size() : e, extra); CAPTURE(longer) - if (eisel_lemire(longer, out)) + CHECK(native_bits64(longer) == b); + } + } + + SECTION("round trip, binary32") + { + std::uint32_t state = 5295; + for (int i = 0; i < 100000; ++i) + { + state ^= state << 13u; + state ^= state >> 17u; + state ^= state << 5u; + std::uint32_t b = state; + if ((b & 0x7F800000u) == 0x7F800000u) { - CHECK(bits_of(out) == b); + continue; // infinity or NaN } - else + if (i % 4 == 0) { - // w and w + 1 round differently: only when the value is very - // close to a rounding boundary - ++declined; + b &= 0x807FFFFFu; // subnormals } + float f = 0; + std::memcpy(&f, &b, sizeof(f)); + + std::array buffer{}; + const char* end = nlohmann::detail::to_chars(buffer.data(), buffer.data() + buffer.size(), f); + const std::string token(buffer.data(), static_cast(end - buffer.data())); + CAPTURE(token) + CHECK(native_bits32(token) == b); } - CHECK(declined < 1000); // 107 of the 200,000 } SECTION("used by the lexer") @@ -1346,3 +1861,299 @@ TEST_CASE("Eisel-Lemire float conversion") "[json.exception.out_of_range.406] number overflow parsing '1.7976931348623159e308'", json::out_of_range&); } } + +namespace +{ +using float_json = nlohmann::basic_json; + +// the bits of the float that parse() gives for a token, via both scanners; +// the value must be the same for both +template +void check_parse(const std::string& token, Bits expected, Bits infinity) +{ + std::stringstream stream(token); + if ((expected & ~(Bits{1} << ((8 * sizeof(Bits)) - 1))) == infinity) + { + Json _; + CHECK_THROWS_WITH_AS(_ = Json::parse(token), ("[json.exception.out_of_range.406] number overflow parsing '" + token + "'").c_str(), typename Json::out_of_range&); + CHECK_THROWS_WITH_AS(_ = Json::parse(stream), ("[json.exception.out_of_range.406] number overflow parsing '" + token + "'").c_str(), typename Json::out_of_range&); + return; + } + const Json contiguous = Json::parse(token); + const Json streamed = Json::parse(stream); + if (contiguous.is_number_float()) // not an integer that fits + { + CHECK(bits_of(contiguous.template get()) == expected); + CHECK(bits_of(streamed.template get()) == expected); + } + else + { + CHECK(streamed.is_number_integer()); + } +} +} // namespace + +TEST_CASE("float conversion of hard cases") +{ + // see float_hard_cases.hpp + for (const auto& c : float_hard_cases::cases()) + { + const std::string token = c.token; + CAPTURE(token) + CHECK(native_bits64(token) == c.bits64); + CHECK(native_bits32(token) == c.bits32); + check_parse(token, c.bits64, std::uint64_t{0x7FF0000000000000u}); + check_parse(token, c.bits32, std::uint32_t{0x7F800000u}); + } +} + +TEST_CASE("float overflow and underflow in the parser") +{ + SECTION("double") + { + check_parse("1.7976931348623157e308", std::uint64_t{0x7FEFFFFFFFFFFFFFu}, std::uint64_t{0x7FF0000000000000u}); + check_parse("1.7976931348623159e308", std::uint64_t{0x7FF0000000000000u}, std::uint64_t{0x7FF0000000000000u}); + check_parse("-1e309", std::uint64_t{0xFFF0000000000000u}, std::uint64_t{0x7FF0000000000000u}); + check_parse("1" + std::string(400, '0'), std::uint64_t{0x7FF0000000000000u}, std::uint64_t{0x7FF0000000000000u}); + check_parse("1e99999999999999999999", std::uint64_t{0x7FF0000000000000u}, std::uint64_t{0x7FF0000000000000u}); + // an underflow gives a zero with the sign of the token + check_parse("1e-400", std::uint64_t{0}, std::uint64_t{0x7FF0000000000000u}); + check_parse("-1e-400", std::uint64_t{0x8000000000000000u}, std::uint64_t{0x7FF0000000000000u}); + check_parse("-2.4703282292062327e-324", std::uint64_t{0x8000000000000000u}, std::uint64_t{0x7FF0000000000000u}); + check_parse("0." + std::string(400, '0') + "1", std::uint64_t{0}, std::uint64_t{0x7FF0000000000000u}); + } + + SECTION("float") + { + check_parse("3.4028234e38", std::uint32_t{0x7F7FFFFFu}, std::uint32_t{0x7F800000u}); + check_parse("3.4028236e38", std::uint32_t{0x7F800000u}, std::uint32_t{0x7F800000u}); + check_parse("-1e39", std::uint32_t{0xFF800000u}, std::uint32_t{0x7F800000u}); + check_parse("1e-46", std::uint32_t{0}, std::uint32_t{0x7F800000u}); + check_parse("-1e-46", std::uint32_t{0x80000000u}, std::uint32_t{0x7F800000u}); + check_parse("-7.006492321624085e-46", std::uint32_t{0x80000000u}, std::uint32_t{0x7F800000u}); + check_parse("-7.006492321624086e-46", std::uint32_t{0x80000001u}, std::uint32_t{0x7F800000u}); + } +} + +TEST_CASE("string scanning kernels") +{ + // the word-at-a-time kernels must stop exactly where a byte-by-byte scan + // stops, for any content, length, and alignment + const auto reference_special = [](const unsigned char* data, std::size_t n) + { + std::size_t i = 0; + while (i < n && !nlohmann::detail::is_string_special(data[i])) + { + ++i; + } + return i; + }; + const auto reference_copyable = [](const unsigned char* data, std::size_t n) + { + std::size_t i = 0; + while (i < n && nlohmann::detail::is_ascii_copyable(data[i])) + { + ++i; + } + return i; + }; + const auto reference_bulk_run = [](const unsigned char* data, std::size_t n) + { + std::size_t i = 0; + while (i < n) + { + if (data[i] < 0x80u) + { + if (nlohmann::detail::is_string_special(data[i])) + { + break; + } + ++i; + continue; + } + const std::size_t seq = nlohmann::detail::validate_one_utf8(data + i, n - i); + if (seq == 0) + { + break; + } + i += seq; + } + return i; + }; + + // pieces: ordinary ASCII, stops, DEL, well-formed sequences of every + // length, and ill-formed or truncated ones + const std::vector pieces = + { + "a", "Z", " ", "~", "0123456789", "\"", "\\", std::string(1, '\0'), "\n", "\x1F", "\x7F", + "\xC3\xA4", "\xE2\x82\xAC", "\xE6\x97\xA5\xE6\x9C\xAC", "\xF0\x9F\x98\x80", "\xED\x9F\xBF", + "\x80", "\xC0\x80", "\xC3", "\xE2\x82", "\xED\xA0\x80", "\xF4\x90\x80\x80", "\xFF", + }; + std::uint64_t state = 5295; + const auto next = [&state]() + { + state ^= state << 13u; + state ^= state >> 7u; + state ^= state << 17u; + return state; + }; + // the upper half as a 32-bit value: converts to std::size_t implicitly on + // every platform (a cast of std::uint64_t is useless where both are the + // same type, and required where std::size_t is 32 bits wide) + const auto next_small = [&next]() + { + return static_cast(next() >> 32u); + }; + for (int round = 0; round < 100000; ++round) + { + // mostly ordinary text, so that runs span several words + std::string text(next_small() % 8u, '.'); + const std::size_t count = next_small() % 12u; + for (std::size_t k = 0; k < count; ++k) + { + const std::size_t p = (next() % 4 == 0) ? next_small() % pieces.size() : 0; + text += pieces[p]; + text += std::string(next_small() % 10u, 'x'); + } + const auto* data = reinterpret_cast(text.data()); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + for (std::size_t offset = 0; offset < 3 && offset <= text.size(); ++offset) + { + const std::size_t n = text.size() - offset; + CAPTURE(text) + CAPTURE(offset) + CHECK(nlohmann::detail::find_string_special(data + offset, n) == reference_special(data + offset, n)); + CHECK(nlohmann::detail::find_ascii_copyable_run(data + offset, n) == reference_copyable(data + offset, n)); + CHECK(nlohmann::detail::scalar_string_bulk_run(data + offset, n) == reference_bulk_run(data + offset, n)); + } + } + + // the trailing-zero count, whichever implementation the compiler gets + for (int k = 0; k < 64; ++k) + { + const std::uint64_t bit = std::uint64_t{1} << k; + CHECK(nlohmann::detail::count_trailing_zeros(bit) == k); + CHECK(nlohmann::detail::count_trailing_zeros(bit | (bit << 1u) | 0x8000000000000000u) == k); + } + + // eight bytes as a little-endian word, at any alignment + const std::array bytes = {{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF}}; + CHECK(nlohmann::detail::read_eight_bytes(bytes.data()) == 0x0807060504030201u); + CHECK(nlohmann::detail::read_eight_bytes(bytes.data() + 1) == 0x0908070605040302u); + CHECK(nlohmann::detail::read_eight_bytes(bytes.data() + 8) == 0xFF0F0E0D0C0B0A09u); + CHECK(nlohmann::detail::read_eight_bytes(reinterpret_cast(bytes.data()) + 3) == 0x0B0A090807060504u); +} + +TEST_CASE("find_ascii_copyable_run agrees with is_ascii_copyable for every byte") +{ + // the SWAR mask in find_ascii_copyable_run() encodes the predicate a + // second time; check every byte value at every position of a 24-byte buffer + // (three words) and for several lengths, with the byte at the end or inside + for (unsigned int value = 0; value < 256; ++value) + { + for (std::size_t position = 0; position < 24; ++position) + { + std::array buffer{}; + buffer.fill(static_cast('a')); + buffer[position] = static_cast(value); + const bool copyable = nlohmann::detail::is_ascii_copyable(static_cast(value)); + for (const std::size_t n : + { + position, position + 1, std::size_t{24} + }) + { + if (n > 24) + { + continue; + } + const std::size_t expected = (copyable || n <= position) ? n : position; + CAPTURE(value) + CAPTURE(position) + CAPTURE(n) + CHECK(nlohmann::detail::find_ascii_copyable_run(buffer.data(), n) == expected); + } + } + } +} + +TEST_CASE("UTF-8 validators accept the same sequences") +{ + // The library checks UTF-8 in several places: detail::decode (serializer), + // the per-lead-byte switch of the lexer (json::accept), validate_one_utf8 + // (bulk string scan), utf8_shape, and the byte path of the BON8 reader. + // They must accept exactly the same sequences (RFC 3629, section 4). + using nlohmann::json; + + // bytes for the third and fourth position: the first two pairs are checked + // against all validators, the others only against the cheap ones + const std::array, 6> tails = + { + { + {{0x80, 0x80}}, {{0xBF, 0xBF}}, {{0x7F, 0x80}}, {{0xC0, 0x80}}, {{0x80, 0x7F}}, {{0x80, 0xC0}} + } + }; + + for (unsigned lead = 0x80; lead <= 0xFF; ++lead) + { + const auto shape = nlohmann::detail::utf8_shape(static_cast(lead)); + for (unsigned second = 0; second <= 0xFF; ++second) + { + for (std::size_t t = 0; t < tails.size(); ++t) + { + const std::array bytes = {{static_cast(lead), static_cast(second), tails[t][0], tails[t][1]}}; + + // expectation from the shape: the sequence is bytes[0..length) + const std::size_t length = shape.length; + bool expected = length >= 2 && second >= shape.second_lo && second <= shape.second_hi; + for (std::size_t i = 2; expected && i < length; ++i) + { + expected = bytes[i] >= 0x80 && bytes[i] <= 0xBF; + } + + // the DFA of the serializer + bool dfa = length >= 2; + if (dfa) + { + std::uint8_t state = nlohmann::detail::UTF8_ACCEPT; + std::uint32_t codep = 0; + for (std::size_t i = 0; i < length; ++i) + { + nlohmann::detail::decode(state, codep, bytes[i]); + } + dfa = state == nlohmann::detail::UTF8_ACCEPT; + } + + // the bulk path: exactly the sequence, and one byte short + const std::size_t bulk = nlohmann::detail::validate_one_utf8(bytes.data(), length); + const std::size_t bulk_short = length > 0 ? nlohmann::detail::validate_one_utf8(bytes.data(), length - 1) : 0; + + CAPTURE(lead) + CAPTURE(second) + CAPTURE(t) + CHECK((dfa == expected && bulk == (expected ? length : 0) && bulk_short == 0)); + + if (t < 2 && length >= 2) + { + // the lexer's byte path + const std::string sequence(reinterpret_cast(bytes.data()), length); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + CHECK(json::accept("\"" + sequence + "\"") == expected); + + // the byte path of the BON8 reader (a string ends at 0xFF) + if (lead >= 0xC2 && lead <= 0xF7) + { + std::vector packed(bytes.begin(), bytes.begin() + static_cast(length)); + packed.push_back(0xFF); + const json j = json::from_bon8(packed, true, false); + if (expected) + { + CHECK(j == json(sequence)); + } + else + { + CHECK_FALSE((j.is_string() && j.get() == sequence)); + } + } + } + } + } + } +} diff --git a/tests/src/unit-class_parser.cpp b/tests/src/unit-class_parser.cpp index 978d94f92..dd78f7ff4 100644 --- a/tests/src/unit-class_parser.cpp +++ b/tests/src/unit-class_parser.cpp @@ -30,201 +30,24 @@ using nlohmann::json; #include #include #include +#include #include +#include #include #include #include #include #include +#include "sax_countdown.hpp" +#include "sax_event_loggers.hpp" #include "test_utils.hpp" +using utils::SaxCountdown; +using utils::SaxEventLogger; + namespace { -class SaxEventLogger -{ - public: - bool null() - { - events.emplace_back("null()"); - return true; - } - - bool boolean(bool val) - { - events.emplace_back(val ? "boolean(true)" : "boolean(false)"); - return true; - } - - bool number_integer(json::number_integer_t val) - { - events.push_back("number_integer(" + std::to_string(val) + ")"); - return true; - } - - bool number_unsigned(json::number_unsigned_t val) - { - events.push_back("number_unsigned(" + std::to_string(val) + ")"); - return true; - } - - bool number_float(json::number_float_t /*unused*/, const std::string& s) - { - events.push_back("number_float(" + s + ")"); - return true; - } - - bool string(std::string& val) - { - events.push_back("string(" + val + ")"); - return true; - } - - bool binary(json::binary_t& val) - { - std::string binary_contents = "binary("; - std::string comma_space; - for (auto b : val) - { - binary_contents.append(comma_space); - binary_contents.append(std::to_string(static_cast(b))); - comma_space = ", "; - } - binary_contents.append(")"); - events.push_back(binary_contents); - return true; - } - - bool start_object(std::size_t elements) - { - if (elements == (std::numeric_limits::max)()) - { - events.emplace_back("start_object()"); - } - else - { - events.push_back("start_object(" + std::to_string(elements) + ")"); - } - return true; - } - - bool key(std::string& val) - { - events.push_back("key(" + val + ")"); - return true; - } - - bool end_object() - { - events.emplace_back("end_object()"); - return true; - } - - bool start_array(std::size_t elements) - { - if (elements == (std::numeric_limits::max)()) - { - events.emplace_back("start_array()"); - } - else - { - events.push_back("start_array(" + std::to_string(elements) + ")"); - } - return true; - } - - bool end_array() - { - events.emplace_back("end_array()"); - return true; - } - - bool parse_error(std::size_t position, const std::string& /*unused*/, const json::exception& /*unused*/) - { - errored = true; - events.push_back("parse_error(" + std::to_string(position) + ")"); - return false; - } - - std::vector events {}; // NOLINT(readability-redundant-member-init) - bool errored = false; -}; - -class SaxCountdown : public nlohmann::json::json_sax_t -{ - public: - explicit SaxCountdown(const int count) : events_left(count) - {} - - bool null() override - { - return events_left-- > 0; - } - - bool boolean(bool /*val*/) override - { - return events_left-- > 0; - } - - bool number_integer(json::number_integer_t /*val*/) override - { - return events_left-- > 0; - } - - bool number_unsigned(json::number_unsigned_t /*val*/) override - { - return events_left-- > 0; - } - - bool number_float(json::number_float_t /*val*/, const std::string& /*s*/) override - { - return events_left-- > 0; - } - - bool string(std::string& /*val*/) override - { - return events_left-- > 0; - } - - bool binary(json::binary_t& /*val*/) override - { - return events_left-- > 0; - } - - bool start_object(std::size_t /*elements*/) override - { - return events_left-- > 0; - } - - bool key(std::string& /*val*/) override - { - return events_left-- > 0; - } - - bool end_object() override - { - return events_left-- > 0; - } - - bool start_array(std::size_t /*elements*/) override - { - return events_left-- > 0; - } - - bool end_array() override - { - return events_left-- > 0; - } - - bool parse_error(std::size_t /*position*/, const std::string& /*last_token*/, const json::exception& /*ex*/) override - { - return false; - } - - private: - int events_left = 0; -}; - json parser_helper(const std::string& s); bool accept_helper(const std::string& s); void comments_helper(const std::string& s); @@ -2629,6 +2452,109 @@ std::string parse_error_message_range(IteratorType first, IteratorType last) } } // namespace +TEST_CASE("object keys are moved into the value") +{ + // json_sax_dom_parser::key() moves the key out of the lexer's buffer into + // the object, and the lexer starts the next token with an empty buffer. + // Keys of every length (inline and allocated strings), keys with escapes, + // and duplicate keys must give the same objects as reading the input one + // character at a time, and as ordered_json, whose map copies the key. + const std::string long_key(100, 'k'); + const std::string doc = R"({"a": 1, ")" + long_key + R"(": {"b": [2, 3], "sixteen_chars_xx": 4}, )" + R"("esc\"apedé": 5, "a": 6, ")" + long_key + R"(x": 7, "": 8, "dup": 9, "dup": 10})"; + + const json j = json::parse(doc); + const std::list chars(doc.begin(), doc.end()); + CHECK(j == json::parse(chars.begin(), chars.end())); + + CHECK(j.size() == 6); + CHECK(j["a"] == 6); + CHECK(j[long_key]["b"] == json({2, 3})); + CHECK(j[long_key]["sixteen_chars_xx"] == 4); + CHECK(j["esc\"aped\xc3\xa9"] == 5); + CHECK(j[long_key + "x"] == 7); + CHECK(j[""] == 8); + CHECK(j["dup"] == 10); + + const auto o = nlohmann::ordered_json::parse(doc); + CHECK(o.size() == 6); + CHECK(o[long_key + "x"] == 7); + CHECK(json(o) == j); +} + +TEST_CASE("text of float tokens") +{ + // For contiguous input, the lexer copies the text of a float token into + // its buffer only when a SAX handler asks for it (the library's own + // handlers do not). A user handler must still see the exact text, for + // every input adapter and in every position of a document. + const std::string doc = R"([1.5, -0.0, 0.1, 1e5, -2.5E-4, 12345678901234567890.5, 0.12345678901234567890123, + {"a": 3.25, "b": [6.02e23, 1]}, "x", true, 7e-1])"; + const std::vector expected_floats = + { + "number_float(1.5)", "number_float(-0.0)", "number_float(0.1)", "number_float(1e5)", "number_float(-2.5E-4)", + "number_float(12345678901234567890.5)", "number_float(0.12345678901234567890123)", "number_float(3.25)", + "number_float(6.02e23)", "number_float(7e-1)" + }; + + const auto floats = [](const std::vector& events) + { + std::vector result; + std::copy_if(events.begin(), events.end(), std::back_inserter(result), [](const std::string & e) + { + return e.compare(0, 13, "number_float(") == 0; + }); + return result; + }; + + SECTION("contiguous input") + { + SaxEventLogger l; + CHECK(json::sax_parse(doc, &l)); + CHECK(floats(l.events) == expected_floats); + } + + SECTION("byte input") + { + const std::vector bytes(doc.begin(), doc.end()); + SaxEventLogger l; + CHECK(json::sax_parse(bytes, &l)); + CHECK(floats(l.events) == expected_floats); + } + + SECTION("stream input") + { + std::istringstream is(doc); + SaxEventLogger l; + CHECK(json::sax_parse(is, &l)); + CHECK(floats(l.events) == expected_floats); + } + + SECTION("non-contiguous iterators") + { + const std::list chars(doc.begin(), doc.end()); + SaxEventLogger l; + CHECK(json::sax_parse(chars.begin(), chars.end(), &l)); + CHECK(floats(l.events) == expected_floats); + } + + SECTION("the library's own handlers do not need the text") + { + using lexer_adapter = nlohmann::detail::iterator_input_adapter; + CHECK(nlohmann::detail::sax_ignores_number_text>::value); + CHECK(nlohmann::detail::sax_ignores_number_text>::value); + CHECK(nlohmann::detail::sax_ignores_number_text>::value); + CHECK_FALSE(nlohmann::detail::sax_ignores_number_text::value); + + // and the values are the same as when the text is read + const json j = json::parse(doc); + CHECK(j[0] == 1.5); + CHECK(j[5].get() == 12345678901234567890.5); + CHECK(j[7]["b"][0] == 6.02e23); + CHECK(json::accept(doc)); + } +} + TEST_CASE("last-read diagnostics are identical across input adapters") { // The lexer reconstructs the "last read" token lazily for seekable adapters @@ -3026,3 +2952,81 @@ TEST_CASE("diagnostic positions: value lifetime, input adapters, and SAX") } } #endif + +TEST_CASE("accept() agrees with parse() on floats around the overflow limit") +{ + // accept() does not convert a float that cannot overflow; the tokens + // around the limit (and beyond it) must still be rejected exactly as + // parse() rejects them + const std::vector numbers = + { + "1e0", "123.456e-5", "1e-400", "-1e-400", "1e307", "1e308", "1e309", "-1e309", + "10e307", "100e307", "0.1e309", "0.1e310", "1E+308", "1E+309", + "1.7976931348623157e308", "1.7976931348623159e308", "1.8e308", "-1.7976931348623157e308", + "12345678901234567e291", "12345678901234567e292", "123456789012345678e290", "123456789012345678e291", + "1234567890123456789e289", "1234567890123456789e290", "9999999999999999999e289", "9999999999999999999e290", + "0.00000000000000000001e328", "0.00000000000000000001e329" + }; + + for (const char* number : numbers) + { + for (const std::string& text : + { + std::string(number), "[" + std::string(number) + "]", "{\"a\":" + std::string(number) + "}" + }) + { + INFO(text); + const bool parses = !json::parse(text, nullptr, false).is_discarded(); + CHECK(json::accept(text) == parses); + } + } +} + +namespace +{ +// a DOM parser that reports parse errors without throwing, as in the sax_no_exception example +template +class quiet_dom_parser : public nlohmann::detail::json_sax_dom_parser +{ + public: + explicit quiet_dom_parser(BasicJsonType& j) + : nlohmann::detail::json_sax_dom_parser(j, false) + {} + + bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, const nlohmann::detail::exception& /*unused*/) + { + return false; + } +}; + +template +std::string read_before_error(const std::string& text) +{ + BasicJsonType j; + quiet_dom_parser sax(j); + CHECK(!BasicJsonType::sax_parse(text, &sax)); + return j.dump(); +} +} // namespace + +TEST_CASE("a json_sax_dom_parser keeps the values read before a parse error") +{ + // a json_sax_dom_parser that a user constructs stores every value in place, so a + // handler that does not throw on a parse error sees what was read before it + const std::vector> cases = + { + {"[1,2,3,]", "[1,2,3]"}, + {"[[1,[2,3],4],[5,", "[[1,[2,3],4],[5]]"}, + {R"({"a":1,"b":[1,2,{"c":)", R"({"a":1,"b":[1,2,{"c":null}]})"}, + {R"({"a":1,"a":)", R"({"a":1})"}, + {"[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,]", "[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19]"}, + {R"({"k0":0,"k1":1,"k2":{"x":[true,)", R"({"k0":0,"k1":1,"k2":{"x":[true]}})"}, + {R"([{},[],{"a":[]},x)", R"([{},[],{"a":[]}])"}, + }; + for (const auto& c : cases) + { + CAPTURE(c.first) + CHECK(read_before_error(c.first) == c.second); + CHECK(read_before_error(c.first) == c.second); + } +} diff --git a/tests/src/unit-comparison.cpp b/tests/src/unit-comparison.cpp index 1e409bfb0..4ac43320b 100644 --- a/tests/src/unit-comparison.cpp +++ b/tests/src/unit-comparison.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -1149,3 +1150,157 @@ TEST_CASE("operator<=> of binary values with a different subtype does not depend } } #endif + +TEST_CASE("comparison of scalars (inline fast path)") +{ + const json::number_integer_t neg = -1; + const json::number_integer_t pos = 5; + const json::number_unsigned_t big = (std::numeric_limits::max)(); + + SECTION("same type") + { + CHECK(json(pos) == json(pos)); + CHECK(json(pos) != json(neg)); + CHECK(json(neg) < json(pos)); + CHECK_FALSE(json(pos) < json(neg)); + CHECK_FALSE(json(pos) < json(pos)); + CHECK(json(pos) <= json(pos)); + + CHECK(json(1u) == json(1u)); + CHECK(json(1u) < json(2u)); + CHECK_FALSE(json(2u) < json(1u)); + + CHECK(json(1.5) == json(1.5)); + CHECK(json(1.5) < json(2.5)); + CHECK_FALSE(json(2.5) < json(1.5)); + CHECK(json(-0.0) == json(0.0)); + } + + SECTION("mixed number types") + { + // negative signed vs unsigned must not wrap around + CHECK(json(neg) < json(1u)); + CHECK(json(1u) > json(neg)); + CHECK(json(neg) != json(big)); + CHECK(json(neg) < json(big)); + CHECK_FALSE(json(big) < json(neg)); + + CHECK(json(pos) == json(5u)); + CHECK(json(5u) == json(pos)); + CHECK(json(pos) < json(6u)); + CHECK(json(pos) == json(5.0)); + CHECK(json(5.0) == json(pos)); + CHECK(json(5u) == json(5.0)); + CHECK(json(neg) < json(0.5)); + CHECK(json(0.5) < json(1u)); + } + + SECTION("boolean and null") + { + CHECK(json(true) == json(true)); + CHECK(json(true) != json(false)); + CHECK(json(false) < json(true)); + CHECK_FALSE(json(true) < json(false)); + CHECK_FALSE(json(true) < json(true)); + + CHECK(json(nullptr) == json(nullptr)); + CHECK_FALSE(json(nullptr) < json(nullptr)); + CHECK(json(nullptr) <= json(nullptr)); + CHECK(json(nullptr) < json(false)); + CHECK(json(nullptr) != json(false)); + CHECK(json(nullptr) != json(0)); + } + + SECTION("string vs number") + { + CHECK(json("5") != json(5)); + CHECK(json(5) != json("5")); + CHECK(json(5) < json("5")); // number < string by type order + CHECK_FALSE(json("5") < json(5)); + CHECK(json(1.5) < json("")); + CHECK(json("a") == json("a")); + CHECK(json("a") < json("b")); + } + + SECTION("NaN") + { + const json nan = std::numeric_limits::quiet_NaN(); + const json one = 1.0; + + CHECK_FALSE(nan == nan); + CHECK(nan != nan); + CHECK_FALSE(nan < nan); + CHECK_FALSE(nan <= nan); + CHECK_FALSE(nan >= nan); + CHECK_FALSE(nan > nan); + + CHECK_FALSE(nan == one); + CHECK_FALSE(one == nan); + CHECK_FALSE(nan < one); + CHECK_FALSE(one < nan); + CHECK_FALSE(nan <= one); + CHECK_FALSE(one <= nan); + CHECK_FALSE(nan >= one); + CHECK_FALSE(one >= nan); + + // NaN against other number types + CHECK_FALSE(nan == json(1)); + CHECK_FALSE(json(1) == nan); + CHECK_FALSE(nan < json(1u)); + CHECK_FALSE(json(1u) < nan); + +#if JSON_HAS_THREE_WAY_COMPARISON + CHECK((nan <=> nan) == std::partial_ordering::unordered); // *NOPAD* + CHECK((nan <=> one) == std::partial_ordering::unordered); // *NOPAD* + CHECK((one <=> nan) == std::partial_ordering::unordered); // *NOPAD* + CHECK((one <=> json(2.0)) == std::partial_ordering::less); // *NOPAD* + CHECK((json(2.0) <=> one) == std::partial_ordering::greater); // *NOPAD* + CHECK((one <=> one) == std::partial_ordering::equivalent); // *NOPAD* + CHECK((json(neg) <=> json(1u)) == std::partial_ordering::less); // *NOPAD* + CHECK((json(nullptr) <=> json(nullptr)) == std::partial_ordering::equivalent); // *NOPAD* + CHECK((json(true) <=> json(false)) == std::partial_ordering::greater); // *NOPAD* +#endif + } + + SECTION("discarded") + { + const json d(json::value_t::discarded); + + CHECK_FALSE(d == d); + CHECK_FALSE(d < d); + CHECK_FALSE(d == json(1)); + CHECK_FALSE(json(1) == d); + CHECK_FALSE(d < json(1)); + CHECK_FALSE(json(1) < d); + CHECK_FALSE(d == json(nullptr)); + CHECK_FALSE(json(nullptr) == d); + +#if JSON_HAS_THREE_WAY_COMPARISON + CHECK((d <=> d) == std::partial_ordering::unordered); // *NOPAD* + CHECK((d <=> json(1)) == std::partial_ordering::unordered); // *NOPAD* + CHECK((json(1) <=> d) == std::partial_ordering::unordered); // *NOPAD* +#endif + } + + SECTION("scalar overloads") + { + CHECK(json(5) == 5); + CHECK(5 == json(5)); + CHECK(json(5) < 6); + CHECK(4 < json(5)); + CHECK(json(1.5) == 1.5); + CHECK(json(true) == true); + CHECK(json(5) != 6); + CHECK(json(1u) == 1.0); + } + + SECTION("structured values still compare") + { + CHECK(json({1, 2}) == json({1, 2})); + CHECK(json({1, 2}) < json({1, 3})); + CHECK(json({{"a", 1}}) == json({{"a", 1}})); + CHECK(json::binary({1, 2}) == json::binary({1, 2})); + CHECK(json(1) < json::array()); + CHECK(json::object() < json::array()); + } +} diff --git a/tests/src/unit-constructor1.cpp b/tests/src/unit-constructor1.cpp index 600eb0069..96a64e721 100644 --- a/tests/src/unit-constructor1.cpp +++ b/tests/src/unit-constructor1.cpp @@ -361,6 +361,40 @@ TEST_CASE("constructors") CHECK_THROWS_WITH_AS((j.get>()), "[json.exception.out_of_range.401] array index 1 is out of range", json::out_of_range&); CHECK_THROWS_WITH_AS((j.get>()), "[json.exception.out_of_range.401] array index 1 is out of range", json::out_of_range&); CHECK_THROWS_WITH_AS((j.get>()), "[json.exception.out_of_range.401] array index 1 is out of range", json::out_of_range&); +#if JSON_DIAGNOSTICS + // the exception names the element that has the wrong type + CHECK_THROWS_WITH_AS((j.get>()), "[json.exception.type_error.302] (/0) type must be string, but is number", json::type_error&); +#else + CHECK_THROWS_WITH_AS((j.get>()), "[json.exception.type_error.302] type must be string, but is number", json::type_error&); +#endif + + // the elements are converted in order, so a type error comes before a missing element + json const j2{1, "two"}; + std::array a{{0, 0, 0}}; +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS((j2.get>()), "[json.exception.type_error.302] (/1) type must be number, but is string", json::type_error&); + CHECK_THROWS_WITH_AS(j2.get_to(a), "[json.exception.type_error.302] (/1) type must be number, but is string", json::type_error&); +#else + CHECK_THROWS_WITH_AS((j2.get>()), "[json.exception.type_error.302] type must be number, but is string", json::type_error&); + CHECK_THROWS_WITH_AS(j2.get_to(a), "[json.exception.type_error.302] type must be number, but is string", json::type_error&); +#endif + CHECK_THROWS_WITH_AS(j.get_to(a), "[json.exception.out_of_range.401] array index 1 is out of range", json::out_of_range&); +#if !defined(JSON_NOEXCEPTION) // without exceptions, the get_to calls in CHECK_THROWS_WITH_AS do not run + CHECK(a[0] == 1); +#endif + + json const j3{"one", "two"}; + CHECK_THROWS_WITH_AS((j3.get>()), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&); + } + + SECTION("std::array from a longer array or from booleans") + { + json const j{1, 2.5, true, 4u}; + CHECK(j.get>() == (std::array {{1, 2, 1}})); + std::array a{{0, 0, 0, 0}}; + j.get_to(a); + CHECK(a == (std::array {{1, 2, 1, 4}})); + CHECK(j.get>().empty()); } SECTION("std::forward_list") diff --git a/tests/src/unit-constructor2.cpp b/tests/src/unit-constructor2.cpp index d1ff56452..f17d0ad3a 100644 --- a/tests/src/unit-constructor2.cpp +++ b/tests/src/unit-constructor2.cpp @@ -8,6 +8,9 @@ #include "doctest_compatibility.h" +#include +#include + #include using nlohmann::json; @@ -184,3 +187,193 @@ TEST_CASE("other constructors and destructor") } } } + +TEST_CASE("copy constructor and destructor across value types") +{ + SECTION("copy of a scalar is independent of the source") + { + const json src_null = nullptr; + const json src_bool = true; + const json src_int = -7; + const json src_uint = 7u; + const json src_float = 2.5; + + json c_null(src_null); // NOLINT(performance-unnecessary-copy-initialization): tests the copy constructor + json c_bool(src_bool); // NOLINT(performance-unnecessary-copy-initialization): tests the copy constructor + json c_int(src_int); // NOLINT(performance-unnecessary-copy-initialization): tests the copy constructor + json c_uint(src_uint); // NOLINT(performance-unnecessary-copy-initialization): tests the copy constructor + json c_float(src_float); // NOLINT(performance-unnecessary-copy-initialization): tests the copy constructor + + CHECK(c_null.is_null()); + CHECK(c_bool == true); + CHECK(c_int == -7); + CHECK(c_uint == 7u); + CHECK(c_float == 2.5); + + c_int = 8; + c_float = 3.5; + CHECK(src_int == -7); + CHECK(src_float == 2.5); + } + + SECTION("copy of an owning value, then mutate the copy") + { + const json src_string = "hello"; + const json src_array = {1, 2, 3}; + const json src_object = {{"a", 1}, {"b", {{"c", 2}}}}; + const json src_binary = json::binary({1, 2, 3}); + + json c_string(src_string); + json c_array(src_array); + json c_object(src_object); + json c_binary(src_binary); + + CHECK(c_string == src_string); + CHECK(c_array == src_array); + CHECK(c_object == src_object); + CHECK(c_binary == src_binary); + + c_string.get_ref() += " world"; + c_array.push_back(4); + c_object["b"]["c"] = 3; + c_object["d"] = 4; + c_binary.get_binary().push_back(4); + + CHECK(src_string == "hello"); + CHECK(src_array.size() == 3); + CHECK(src_object["b"]["c"] == 2); + CHECK_FALSE(src_object.contains("d")); + CHECK(src_binary.get_binary().size() == 3); + + CHECK(c_string == "hello world"); + CHECK(c_array.size() == 4); + CHECK(c_object["b"]["c"] == 3); + CHECK(c_binary.get_binary().size() == 4); + } + + SECTION("copy keeps parent pointers valid (JSON_DIAGNOSTICS)") + { + const json src = {{"a", {1, 2}}, {"b", "x"}}; + json c(src); + c["a"].push_back(3); + CHECK(c["a"].size() == 3); + CHECK(src["a"].size() == 2); + } + + SECTION("vector of mixed types: build, copy, and destroy") + { + { + std::vector v; + for (int i = 0; i < 70; ++i) + { + switch (i % 9) + { + case 0: + v.emplace_back(nullptr); + break; + case 1: + v.emplace_back(i % 2 == 0); + break; + case 2: + v.emplace_back(-i); + break; + case 3: + v.emplace_back(static_cast(i)); + break; + case 4: + v.emplace_back(i * 0.5); + break; + case 5: + v.emplace_back(std::string(static_cast(i), 'x')); + break; + case 6: + v.emplace_back(json::array({i, "s", nullptr})); + break; + case 7: + v.emplace_back(json::object({{"k", i}})); + break; + default: + v.emplace_back(json::binary({1, 2, 3})); + break; + } + } + REQUIRE(v.size() == 70); + + std::vector copy(v); + CHECK((copy == v)); + + // mutating the copy leaves the original alone + copy[5] = "changed"; + copy[6].push_back(1); + CHECK((copy != v)); + CHECK(v[5] == std::string(5, 'x')); + CHECK(v[6].size() == 3); + + v.erase(v.begin(), v.begin() + 10); + CHECK(v.size() == 60); + } + + // destroying the discarded and default-constructed values + std::vector w(4); + w.emplace_back(json::value_t::discarded); + w.emplace_back(json::value_t::object); + w.emplace_back(json::value_t::array); + w.emplace_back(json::value_t::string); + w.emplace_back(json::value_t::binary); + CHECK(w.size() == 9); + } +} + +TEST_CASE_TEMPLATE("destructor of nested containers", Json, nlohmann::json, nlohmann::ordered_json) +{ + SECTION("empty containers") + { + { + Json j = Json::array(); + } + { + Json j = Json::object(); + } + { + Json j = Json::array({Json::array(), Json::object(), Json::array({Json::array()})}); + } + } + + SECTION("leaves mixed with nested containers") + { + Json j = Json::object(); + j["leaf"] = 1; + j["str"] = "text"; + j["empty_a"] = Json::array(); + j["empty_o"] = Json::object(); + j["arr"] = Json::array({1, "a", Json::array({1, 2}), Json::object({{"k", Json::array({Json::object()})}}), nullptr}); + j["obj"] = Json::object({{"x", Json::object({{"y", Json::array({1, Json::object({{"z", 1}})})}})}, {"w", 2}}); + Json copy = j; + CHECK(copy == j); + j = Json::array({std::move(copy), j, 3, Json::array()}); + } + + SECTION("deep nesting") + { + for (int kind = 0; kind < 3; ++kind) + { + Json j = 1; + for (int i = 0; i < 5000; ++i) + { + switch (kind) + { + case 0: + j = Json::array({std::move(j)}); + break; + case 1: + j = Json::object({{"k", std::move(j)}}); + break; + default: + // siblings next to the nested child + j = (i % 2 == 0) ? Json::array({1, Json::array(), std::move(j)}) : Json::object({{"a", 1}, {"b", Json::object()}, {"c", std::move(j)}}); + break; + } + } + } + } +} diff --git a/tests/src/unit-custom-array-type.cpp b/tests/src/unit-custom-array-type.cpp index 6f015d234..e602b4716 100644 --- a/tests/src/unit-custom-array-type.cpp +++ b/tests/src/unit-custom-array-type.cpp @@ -9,7 +9,9 @@ #include "doctest_compatibility.h" #include +using nlohmann::json; +#include #include #include #include @@ -164,3 +166,47 @@ TEST_CASE("array type without at()") CHECK(j.at(no_at_json::json_pointer("/1")) == 2); CHECK_THROWS_AS(j.at(no_at_json::json_pointer("/3")), no_at_json::out_of_range); } + +TEST_CASE("regression test #5476 - array type without reserve()") +{ + // the capacity reserved for definite-length arrays must not require the + // array type to have a reserve() member function + SECTION("std::deque") + { + const auto j = deque_json::parse(R"({"a":[1,[2,3]],"b":[]})"); + CHECK(j.dump() == R"({"a":[1,[2,3]],"b":[]})"); + + // the binary formats pass a definite length to start_array() + CHECK(deque_json::from_cbor(deque_json::to_cbor(j)) == j); + CHECK(deque_json::from_msgpack(deque_json::to_msgpack(j)) == j); + CHECK(deque_json::from_bon8(deque_json::to_bon8(j)) == j); + + // parse() instantiates the callback parser as well, which reserves too + const auto with_callback = deque_json::parse(R"([1,2,3])", [](int /*depth*/, deque_json::parse_event_t /*event*/, deque_json& /*parsed*/) noexcept + { + return true; + }); + CHECK(with_callback == deque_json({1, 2, 3})); + } + + SECTION("std::vector still reserves") + { + json array = json::array(); + for (int i = 0; i < 100; ++i) + { + array.push_back(i); + } + + const auto j = json::from_cbor(json::to_cbor(array)); + CHECK(j == array); + CHECK(j.get_ref().capacity() >= 100); + } + + SECTION("the reservation stays capped") + { + // CBOR array announcing 2^32-1 elements, but truncated right after the + // header: the input must be rejected without reserving that capacity + const std::vector truncated = {0x9A, 0xFF, 0xFF, 0xFF, 0xFF}; + CHECK(json::from_cbor(truncated, true, false).is_discarded()); + } +} diff --git a/tests/src/unit-custom-base-class.cpp b/tests/src/unit-custom-base-class.cpp index 9718d1cda..673c7ddbb 100644 --- a/tests/src/unit-custom-base-class.cpp +++ b/tests/src/unit-custom-base-class.cpp @@ -400,20 +400,7 @@ class base_class_with_hidden_members std::size_t m_size = 42; }; -using json_with_hidden_base_members = - nlohmann::basic_json < - std::map, - std::vector, - std::string, - bool, - std::int64_t, - std::uint64_t, - double, - std::allocator, - nlohmann::adl_serializer, - std::vector, - base_class_with_hidden_members - >; +using json_with_hidden_base_members = nlohmann::json::with_base_class_t; TEST_CASE("JSON Node as_base_class") { @@ -459,19 +446,7 @@ struct const_member_base const int id = 7; // NOLINT(misc-non-private-member-variables-in-classes) }; -using json_with_const_base = nlohmann::basic_json < - std::map, - std::vector, - std::string, - bool, - std::int64_t, - std::uint64_t, - double, - std::allocator, - nlohmann::adl_serializer, - std::vector, - const_member_base - >; +using json_with_const_base = nlohmann::json::with_base_class_t; // build an array nested @a depth levels deep, with the innermost value 1; // every level is constructed (never assigned), since const_member_base does diff --git a/tests/src/unit-custom-binary-type.cpp b/tests/src/unit-custom-binary-type.cpp index d885376cb..e44a3c88c 100644 --- a/tests/src/unit-custom-binary-type.cpp +++ b/tests/src/unit-custom-binary-type.cpp @@ -8,6 +8,13 @@ #include "doctest_compatibility.h" +// capture whether JSON_DISABLE_ENUM_SERIALIZATION was enabled on the command +// line before including json.hpp, which #undefs it; std::byte is an enum, so +// converting std::vector needs enum serialization +#if defined(JSON_DISABLE_ENUM_SERIALIZATION) && (JSON_DISABLE_ENUM_SERIALIZATION == 1) + #define SKIP_TESTS_FOR_ENUM_SERIALIZATION +#endif + #include #include @@ -26,15 +33,11 @@ namespace // a BinaryType whose value type is signed: the elements must still be // processed as the numbers 0..255 -using char_binary_json = nlohmann::basic_json < - std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, - double, std::allocator, nlohmann::adl_serializer, std::vector, void >; +using char_binary_json = nlohmann::json::with_binary_t>; #ifdef JSON_HAS_CPP_17 // a BinaryType whose value type is not an integer type at all - using byte_binary_json = nlohmann::basic_json < - std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, - double, std::allocator, nlohmann::adl_serializer, std::vector, void >; + using byte_binary_json = nlohmann::json::with_binary_t>; #endif } // namespace @@ -89,3 +92,53 @@ TEST_CASE("binary type whose value type is not std::uint8_t") } #endif } + +#ifdef JSON_HAS_CPP_17 +// moved from unit-regression3.cpp, so that file does not instantiate a second +// basic_json specialization (MinGW section limit, see windows.yml) +TEST_CASE("std::vector as binary type") +{ + SECTION("issue #4804: from_cbor incompatible with std::vector as binary_t") + { + const std::vector data = {0x80}; + const auto decoded = byte_binary_json::from_cbor(data); + CHECK((decoded == byte_binary_json::array())); + } + +#ifndef SKIP_TESTS_FOR_ENUM_SERIALIZATION + SECTION("discussion #4209 - custom BinaryType direct assignment and round-tripping") + { + // Test that assigning a custom BinaryType directly creates a binary value, not an array + const std::vector original{std::byte{1}, std::byte{2}, std::byte{3}}; + const byte_binary_json j = original; + CHECK(j.is_binary()); + CHECK(!j.is_array()); + + // Test round-tripping: extracting the binary value back as the custom container type + const auto extracted = j.get>(); + CHECK(extracted == original); + + // Test that the default json alias behavior is unchanged: std::vector -> array + const nlohmann::json default_json = std::vector {1, 2, 3}; + CHECK(default_json.is_array()); + CHECK(!default_json.is_binary()); + } + + SECTION("discussion #4209 - custom BinaryType extraction from parsed array") + { + // Test that extracting a custom BinaryType from a parsed JSON array still works + // (not just from a binary-typed node) + const auto j = byte_binary_json::parse("[1,2,3]"); + CHECK(j.is_array()); + CHECK(!j.is_binary()); + + // Extracting as custom BinaryType should work from arrays + const auto extracted = j.get>(); + CHECK(extracted.size() == 3); + CHECK(extracted[0] == std::byte{1}); + CHECK(extracted[1] == std::byte{2}); + CHECK(extracted[2] == std::byte{3}); + } +#endif +} +#endif diff --git a/tests/src/unit-custom-key-type-c-str.cpp b/tests/src/unit-custom-key-type-c-str.cpp new file mode 100644 index 000000000..5ad1edc90 --- /dev/null +++ b/tests/src/unit-custom-key-type-c-str.cpp @@ -0,0 +1,46 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include + +#include "custom_key_test.hpp" + +// object type whose key type has no conversion, but size() and c_str() (see custom_key_test.hpp) +using custom_key_test::json_c_str; + +TEST_CASE("custom object key types: copy (json_c_str)") +{ + custom_key_test::test_copy(); +} + +TEST_CASE("custom object key types: parse (json_c_str)") +{ + custom_key_test::test_parse(); +} + +TEST_CASE("custom object key types: merge_patch, update, and insert (json_c_str)") +{ + custom_key_test::test_patch(); +} + +TEST_CASE("custom object key types: at() reports a missing key (json_c_str)") +{ + custom_key_test::test_at(); +} + +TEST_CASE("custom object key types: CBOR (json_c_str)") +{ + custom_key_test::test_cbor(); +} + +TEST_CASE("custom object key types: MessagePack (json_c_str)") +{ + custom_key_test::test_msgpack(); +} diff --git a/tests/src/unit-custom-key-type-explicit.cpp b/tests/src/unit-custom-key-type-explicit.cpp new file mode 100644 index 000000000..2ef3e97f1 --- /dev/null +++ b/tests/src/unit-custom-key-type-explicit.cpp @@ -0,0 +1,46 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include + +#include "custom_key_test.hpp" + +// object type whose key type has explicit conversion to std::string, no operator== (see custom_key_test.hpp) +using custom_key_test::json_explicit; + +TEST_CASE("custom object key types: copy (json_explicit)") +{ + custom_key_test::test_copy(); +} + +TEST_CASE("custom object key types: parse (json_explicit)") +{ + custom_key_test::test_parse(); +} + +TEST_CASE("custom object key types: merge_patch, update, and insert (json_explicit)") +{ + custom_key_test::test_patch(); +} + +TEST_CASE("custom object key types: at() reports a missing key (json_explicit)") +{ + custom_key_test::test_at(); +} + +TEST_CASE("custom object key types: CBOR (json_explicit)") +{ + custom_key_test::test_cbor(); +} + +TEST_CASE("custom object key types: MessagePack (json_explicit)") +{ + custom_key_test::test_msgpack(); +} diff --git a/tests/src/unit-custom-key-type-full.cpp b/tests/src/unit-custom-key-type-full.cpp new file mode 100644 index 000000000..b72bd7ae0 --- /dev/null +++ b/tests/src/unit-custom-key-type-full.cpp @@ -0,0 +1,51 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include + +#include "custom_key_test.hpp" + +// object type whose key type has implicit conversion to std::string and operator== (see custom_key_test.hpp) +using custom_key_test::json_full; + +TEST_CASE("custom object key types: copy (json_full)") +{ + custom_key_test::test_copy(); +} + +TEST_CASE("custom object key types: parse (json_full)") +{ + custom_key_test::test_parse(); +} + +TEST_CASE("custom object key types: merge_patch, update, and insert (json_full)") +{ + custom_key_test::test_patch(); +} + +TEST_CASE("custom object key types: at() reports a missing key (json_full)") +{ + custom_key_test::test_at(); +} + +TEST_CASE("custom object key types: BSON (json_full)") +{ + custom_key_test::test_bson(); +} + +TEST_CASE("custom object key types: CBOR (json_full)") +{ + custom_key_test::test_cbor(); +} + +TEST_CASE("custom object key types: MessagePack (json_full)") +{ + custom_key_test::test_msgpack(); +} diff --git a/tests/src/unit-custom-key-type-no-eq.cpp b/tests/src/unit-custom-key-type-no-eq.cpp new file mode 100644 index 000000000..288b9fdd2 --- /dev/null +++ b/tests/src/unit-custom-key-type-no-eq.cpp @@ -0,0 +1,51 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include + +#include "custom_key_test.hpp" + +// object type whose key type has implicit conversion to std::string, no operator== (see custom_key_test.hpp) +using custom_key_test::json_no_eq; + +TEST_CASE("custom object key types: copy (json_no_eq)") +{ + custom_key_test::test_copy(); +} + +TEST_CASE("custom object key types: parse (json_no_eq)") +{ + custom_key_test::test_parse(); +} + +TEST_CASE("custom object key types: merge_patch, update, and insert (json_no_eq)") +{ + custom_key_test::test_patch(); +} + +TEST_CASE("custom object key types: at() reports a missing key (json_no_eq)") +{ + custom_key_test::test_at(); +} + +TEST_CASE("custom object key types: BSON (json_no_eq)") +{ + custom_key_test::test_bson(); +} + +TEST_CASE("custom object key types: CBOR (json_no_eq)") +{ + custom_key_test::test_cbor(); +} + +TEST_CASE("custom object key types: MessagePack (json_no_eq)") +{ + custom_key_test::test_msgpack(); +} diff --git a/tests/src/unit-custom-key-type-to-json.cpp b/tests/src/unit-custom-key-type-to-json.cpp new file mode 100644 index 000000000..fde2a8ad3 --- /dev/null +++ b/tests/src/unit-custom-key-type-to-json.cpp @@ -0,0 +1,41 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include + +#include "custom_key_test.hpp" + +// object type whose key type has no conversion, only a to_json overload (see custom_key_test.hpp) +using custom_key_test::json_to_json; + +TEST_CASE("custom object key types: copy (json_to_json)") +{ + custom_key_test::test_copy(); +} + +TEST_CASE("custom object key types: parse (json_to_json)") +{ + custom_key_test::test_parse(); +} + +TEST_CASE("custom object key types: merge_patch, update, and insert (json_to_json)") +{ + custom_key_test::test_patch(); +} + +TEST_CASE("custom object key types: CBOR (json_to_json)") +{ + custom_key_test::test_cbor(); +} + +TEST_CASE("custom object key types: MessagePack (json_to_json)") +{ + custom_key_test::test_msgpack(); +} diff --git a/tests/src/unit-custom-number-types.cpp b/tests/src/unit-custom-number-types.cpp new file mode 100644 index 000000000..e324417db --- /dev/null +++ b/tests/src/unit-custom-number-types.cpp @@ -0,0 +1,419 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include + +#include +#include +#include +#include +#include +#include + +#if JSON_HAS_THREE_WAY_COMPARISON + #include +#endif + +namespace custom_number_types +{ + +// a signed integer of class type: std::is_signed is only true for arithmetic +// types, so the library has to take the signedness from std::numeric_limits, +// as it does for 128-bit and multiprecision integer classes such as +// absl::int128 or boost::multiprecision::cpp_int +class class_int +{ + public: + // trivial, like the 128-bit integer classes: the value is stored in a union + class_int() = default; + + // implicit from built-in integers and explicit from floats, like absl::int128; + // non-template overloads (as absl::int128 has) because clang 3.4 and 3.5 + // reject basic_json's defaulted data() for a number type with template + // constructors + class_int(int v) : value(v) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) + class_int(unsigned v) : value(v) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) + class_int(long v) : value(v) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) + class_int(unsigned long v) : value(static_cast(v)) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) + class_int(long long v) : value(v) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) + class_int(unsigned long long v) : value(static_cast(v)) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) + explicit class_int(double v) : value(static_cast(v)) {} + + template::value, int>::type = 0> + explicit operator T() const + { + return static_cast(value); + } + + friend bool operator==(class_int lhs, class_int rhs) + { + return lhs.value == rhs.value; + } + friend bool operator!=(class_int lhs, class_int rhs) + { + return lhs.value != rhs.value; + } + friend bool operator<(class_int lhs, class_int rhs) + { + return lhs.value < rhs.value; + } +#if JSON_HAS_THREE_WAY_COMPARISON + friend std::strong_ordering operator<=>(class_int lhs, class_int rhs) // *NOPAD* + { + return lhs.value <=> rhs.value; // *NOPAD* + } +#endif + + private: + std::int64_t value; +}; + +} // namespace custom_number_types + +using custom_number_types::class_int; + +namespace std +{ +// only the members the library uses +template<> +class numeric_limits +{ + public: + static constexpr bool is_signed = true; + static constexpr int digits = std::numeric_limits::digits; +}; +} // namespace std + +namespace +{ + +using class_int_json = nlohmann::basic_json; + +class_int_json make_class_int(std::int64_t v) +{ + class_int_json j(class_int_json::value_t::number_integer); + j.get_ref() = class_int(v); + return j; +} + +} // namespace + +// the serializer cannot print an integer of class type, so doctest must not +// try when an assertion fails +namespace doctest +{ +template<> +struct StringMaker +{ + static String convert(const class_int_json& j) + { + return j.type_name(); + } +}; +} // namespace doctest + +// __int128 as number type needs the std::numeric_limits (for the range checks) +// and std::is_integral (for the serializer) specializations, which libc++ +// always provides and libstdc++ only outside strict ISO modes; the MSVC +// standard library has none +#if defined(__SIZEOF_INT128__) && (defined(_LIBCPP_VERSION) || defined(__GLIBCXX_TYPE_INT_N_0)) + #define JSON_TEST_INT128_NUMBER_TYPES 1 +#else + #define JSON_TEST_INT128_NUMBER_TYPES 0 +#endif + +#if JSON_TEST_INT128_NUMBER_TYPES +namespace +{ + +// __extension__ keeps -Wpedantic from flagging the non-standard type +__extension__ typedef __int128 int128; // NOLINT(modernize-use-using) +__extension__ typedef unsigned __int128 uint128; // NOLINT(modernize-use-using) + +static_assert(std::numeric_limits::digits == 127 && std::numeric_limits::digits == 128 && + std::is_integral::value && std::is_integral::value, // NOLINT(misc-redundant-expression) + "__int128 is not fully supported by the standard library"); + +using wide_json = nlohmann::basic_json; + +wide_json make_int(int128 v) +{ + wide_json j(wide_json::value_t::number_integer); + j.get_ref() = v; + return j; +} + +wide_json make_uint(uint128 v) +{ + wide_json j(wide_json::value_t::number_unsigned); + j.get_ref() = v; + return j; +} + +wide_json wrap(const wide_json& value) +{ + wide_json o(wide_json::value_t::object); + o["v"] = value; + return o; +} + +} // namespace + +TEST_CASE("custom number types: integers beyond 64 bits") +{ + using out_of_range = wide_json::out_of_range; + + const int128 two_64 = static_cast(1) << 64; + const int128 two_100 = static_cast(1) << 100; + const int128 max_int64 = (std::numeric_limits::max)(); + const int128 min_int64 = (std::numeric_limits::min)(); + const uint128 max_uint64 = (std::numeric_limits::max)(); + + // before the range checks, these values were silently truncated, e.g., + // 2^100 was written as 0 + const wide_json int_big = make_int(two_100); + const wide_json int_big_negative = make_int(-two_100); + const wide_json uint_big = make_uint(static_cast(two_100)); + std::vector _; + + SECTION("CBOR") + { + CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(int_big), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by CBOR as it does not fit [-2^64, 2^64-1]", out_of_range&); + CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(int_big_negative), "[json.exception.out_of_range.407] integer number -1267650600228229401496703205376 cannot be represented by CBOR as it does not fit [-2^64, 2^64-1]", out_of_range&); + CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(uint_big), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by CBOR as it does not fit uint64", out_of_range&); + // the extremes of the 128-bit types, whose digits are produced without + // negating the minimum as a whole + CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(make_int((std::numeric_limits::min)())), "[json.exception.out_of_range.407] integer number -170141183460469231731687303715884105728 cannot be represented by CBOR as it does not fit [-2^64, 2^64-1]", out_of_range&); + CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(make_int((std::numeric_limits::max)())), "[json.exception.out_of_range.407] integer number 170141183460469231731687303715884105727 cannot be represented by CBOR as it does not fit [-2^64, 2^64-1]", out_of_range&); + CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(make_uint((std::numeric_limits::max)())), "[json.exception.out_of_range.407] integer number 340282366920938463463374607431768211455 cannot be represented by CBOR as it does not fit uint64", out_of_range&); + CHECK_THROWS_AS(_ = wide_json::to_cbor(make_int(two_64)), out_of_range&); + CHECK_THROWS_AS(_ = wide_json::to_cbor(make_int(-two_64 - 1)), out_of_range&); + CHECK_THROWS_AS(_ = wide_json::to_cbor(make_uint(static_cast(max_uint64) + 1)), out_of_range&); + + // CBOR's integers cover [-2^64, 2^64-1] + CHECK(wide_json::to_cbor(make_int(two_64 - 1)) == std::vector({0x1B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF})); + CHECK(wide_json::to_cbor(make_int(-two_64)) == std::vector({0x3B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF})); + CHECK(wide_json::to_cbor(make_uint(max_uint64)) == std::vector({0x1B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF})); + } + + SECTION("MessagePack") + { + CHECK_THROWS_WITH_AS(_ = wide_json::to_msgpack(int_big), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by MessagePack as it does not fit [-2^63, 2^64-1]", out_of_range&); + CHECK_THROWS_WITH_AS(_ = wide_json::to_msgpack(int_big_negative), "[json.exception.out_of_range.407] integer number -1267650600228229401496703205376 cannot be represented by MessagePack as it does not fit [-2^63, 2^64-1]", out_of_range&); + CHECK_THROWS_WITH_AS(_ = wide_json::to_msgpack(uint_big), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by MessagePack as it does not fit uint64", out_of_range&); + CHECK_THROWS_AS(_ = wide_json::to_msgpack(make_int(two_64)), out_of_range&); + CHECK_THROWS_AS(_ = wide_json::to_msgpack(make_int(min_int64 - 1)), out_of_range&); + + // MessagePack's integers cover [-2^63, 2^64-1] + CHECK(wide_json::to_msgpack(make_int(two_64 - 1)) == std::vector({0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF})); + CHECK(wide_json::to_msgpack(make_int(min_int64)) == std::vector({0xD3, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})); + CHECK(wide_json::to_msgpack(make_uint(max_uint64)) == std::vector({0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF})); + } + + SECTION("BSON") + { + // wrap() stores the value at /v, which JSON_DIAGNOSTICS adds to the message +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(_ = wide_json::to_bson(wrap(int_big)), "[json.exception.out_of_range.407] (/v) integer number 1267650600228229401496703205376 cannot be represented by BSON as it does not fit int64", out_of_range&); + CHECK_THROWS_WITH_AS(_ = wide_json::to_bson(wrap(int_big_negative)), "[json.exception.out_of_range.407] (/v) integer number -1267650600228229401496703205376 cannot be represented by BSON as it does not fit int64", out_of_range&); + CHECK_THROWS_WITH_AS(_ = wide_json::to_bson(wrap(uint_big)), "[json.exception.out_of_range.407] (/v) integer number 1267650600228229401496703205376 cannot be represented by BSON as it does not fit uint64", out_of_range&); +#else + CHECK_THROWS_WITH_AS(_ = wide_json::to_bson(wrap(int_big)), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by BSON as it does not fit int64", out_of_range&); + CHECK_THROWS_WITH_AS(_ = wide_json::to_bson(wrap(int_big_negative)), "[json.exception.out_of_range.407] integer number -1267650600228229401496703205376 cannot be represented by BSON as it does not fit int64", out_of_range&); + CHECK_THROWS_WITH_AS(_ = wide_json::to_bson(wrap(uint_big)), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by BSON as it does not fit uint64", out_of_range&); +#endif + CHECK_THROWS_AS(_ = wide_json::to_bson(wrap(make_int(max_int64 + 1))), out_of_range&); + CHECK_THROWS_AS(_ = wide_json::to_bson(wrap(make_int(min_int64 - 1))), out_of_range&); + + // nested values are checked as well, before anything is written + wide_json nested(wide_json::value_t::object); + nested["a"] = wide_json::array({wrap(int_big)}); + std::vector out; + CHECK_THROWS_AS(wide_json::to_bson(nested, out), out_of_range&); + CHECK(out.empty()); + + CHECK(wide_json::from_bson(wide_json::to_bson(wrap(make_int(max_int64)))) == wrap(make_int(max_int64))); + CHECK(wide_json::from_bson(wide_json::to_bson(wrap(make_int(min_int64)))) == wrap(make_int(min_int64))); + CHECK_NOTHROW(wide_json::to_bson(wrap(make_uint(max_uint64)))); + } + + SECTION("BON8") + { + CHECK_THROWS_WITH_AS(_ = wide_json::to_bon8(int_big), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by BON8 as it does not fit int64", out_of_range&); + CHECK_THROWS_WITH_AS(_ = wide_json::to_bon8(int_big_negative), "[json.exception.out_of_range.407] integer number -1267650600228229401496703205376 cannot be represented by BON8 as it does not fit int64", out_of_range&); + CHECK_THROWS_WITH_AS(_ = wide_json::to_bon8(uint_big), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by BON8 as it does not fit int64", out_of_range&); + CHECK_THROWS_AS(_ = wide_json::to_bon8(make_int(max_int64 + 1)), out_of_range&); + CHECK_THROWS_AS(_ = wide_json::to_bon8(make_int(min_int64 - 1)), out_of_range&); + + CHECK(wide_json::from_bon8(wide_json::to_bon8(make_int(max_int64))) == make_int(max_int64)); + CHECK(wide_json::from_bon8(wide_json::to_bon8(make_int(min_int64))) == make_int(min_int64)); + } + + SECTION("UBJSON and BJData") + { + // integers beyond 64 bits are written exactly as high-precision numbers + const std::string digits = "1267650600228229401496703205376"; + std::vector expected = {'H', 'i', static_cast(digits.size())}; + expected.insert(expected.end(), digits.begin(), digits.end()); + CHECK(wide_json::to_ubjson(int_big) == expected); + CHECK(wide_json::to_ubjson(uint_big) == expected); + CHECK(wide_json::to_bjdata(int_big) == expected); + // BJData's uint64 marker 'M' was used for any unsigned value beyond + // int64, which truncated the ones beyond 64 bits + CHECK(wide_json::to_bjdata(uint_big) == expected); + + // the uint64 marker is still used where the value fits + CHECK(wide_json::to_bjdata(make_uint(max_uint64)) == std::vector({'M', 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF})); + + // the elements of an optimized container are written the same way; + // BJData does not allow 'H' as the type of an optimized container + std::vector expected_ubjson_array = {'[', '$', 'H', '#', 'i', 2}; + std::vector expected_bjdata_array = {'[', '#', 'i', 2}; + for (int i = 0; i < 2; ++i) + { + expected_ubjson_array.insert(expected_ubjson_array.end(), expected.begin() + 1, expected.end()); + expected_bjdata_array.insert(expected_bjdata_array.end(), expected.begin(), expected.end()); + } + CHECK(wide_json::to_ubjson(wide_json::array({uint_big, uint_big}), true, true) == expected_ubjson_array); + CHECK(wide_json::to_bjdata(wide_json::array({uint_big, uint_big}), true, true) == expected_bjdata_array); + } + + SECTION("integers in arrays and objects") + { + // runs of numbers and scalar object members are encoded by loops of + // their own for the number types up to 64 bits; a wider number type + // takes the generic path, which checks the range of every integer + const wide_json run = wide_json::array({make_int(1), make_int(-3), make_uint(7), wide_json(1.5), int_big, make_int(5)}); + const wide_json run_negative = wide_json::array({make_int(1), make_int(-3), int_big_negative}); + const wide_json run_unsigned = wide_json::array({make_uint(1), make_uint(7), uint_big}); + const wide_json members = wide_json::object({{"a", make_int(1)}, {"b", wide_json(1.5)}, {"c", int_big}}); + + const std::vector failing = {run, run_negative, run_unsigned, members, wrap(run), wrap(members)}; + for (const auto& value : failing) + { + CHECK_THROWS_AS(_ = wide_json::to_cbor(value), out_of_range&); + CHECK_THROWS_AS(_ = wide_json::to_msgpack(value), out_of_range&); + CHECK_THROWS_AS(_ = wide_json::to_bson(wrap(value)), out_of_range&); + CHECK_THROWS_AS(_ = wide_json::to_bon8(value), out_of_range&); + } + +#if !defined(JSON_NOEXCEPTION) + // a failing write leaves no more than the elements before the failing one + // (without exceptions, the writes in CHECK_THROWS_AS do not run) + std::vector out; + CHECK_THROWS_AS(wide_json::to_cbor(run, out), out_of_range&); + // (the head of the array, 1, -3, 7, and 1.5 as a single-precision float) + CHECK(out == std::vector({0x86, 0x01, 0x22, 0x07, 0xFA, 0x3F, 0xC0, 0x00, 0x00})); + out.clear(); + CHECK_THROWS_AS(wide_json::to_bon8(run, out), out_of_range&); + CHECK(!out.empty()); +#endif + + // the same values that fit are written exactly as with 64-bit number types + const wide_json fits = wide_json::array({make_int(1), make_int(-3), make_uint(7), wide_json(1.5), make_int(min_int64), make_int(max_int64), make_int(200), make_int(-70000)}); + const nlohmann::json fits64 = nlohmann::json::array({1, -3, 7, 1.5, (std::numeric_limits::min)(), (std::numeric_limits::max)(), 200, -70000}); + CHECK(wide_json::to_cbor(fits) == nlohmann::json::to_cbor(fits64)); + CHECK(wide_json::to_msgpack(fits) == nlohmann::json::to_msgpack(fits64)); + CHECK(wide_json::to_ubjson(fits) == nlohmann::json::to_ubjson(fits64)); + CHECK(wide_json::to_ubjson(fits, true, true) == nlohmann::json::to_ubjson(fits64, true, true)); + CHECK(wide_json::to_bjdata(fits) == nlohmann::json::to_bjdata(fits64)); + CHECK(wide_json::to_bjdata(fits, true, true) == nlohmann::json::to_bjdata(fits64, true, true)); + CHECK(wide_json::to_bon8(fits) == nlohmann::json::to_bon8(fits64)); + CHECK(wide_json::to_bson(wrap(fits)) == nlohmann::json::to_bson(nlohmann::json::object({{"v", fits64}}))); + + const wide_json fit_members = wide_json::object({{"a", make_int(1)}, {"b", make_int(-300)}, {"c", make_uint(max_uint64)}, {"d", wide_json(2.5)}}); + const nlohmann::json fit_members64 = nlohmann::json::object({{"a", 1}, {"b", -300}, {"c", (std::numeric_limits::max)()}, {"d", 2.5}}); + CHECK(wide_json::to_cbor(fit_members) == nlohmann::json::to_cbor(fit_members64)); + CHECK(wide_json::to_msgpack(fit_members) == nlohmann::json::to_msgpack(fit_members64)); + CHECK(wide_json::to_ubjson(fit_members) == nlohmann::json::to_ubjson(fit_members64)); + CHECK(wide_json::to_bjdata(fit_members) == nlohmann::json::to_bjdata(fit_members64)); + CHECK(wide_json::to_bson(fit_members) == nlohmann::json::to_bson(fit_members64)); + } + + SECTION("BJData ND-array") + { + // an ND-array element beyond 64 bits does not fit any dtype, so the + // annotated object is written as a plain object instead of truncating + // the element into range + wide_json element(wide_json::value_t::object); + element["_ArrayType_"] = "uint8"; + element["_ArraySize_"] = wide_json::array({make_uint(2), make_uint(1)}); + element["_ArrayData_"] = wide_json::array({make_uint(1), make_uint(static_cast(two_64) + 1)}); + const auto element_bytes = wide_json::to_bjdata(element, true, true); + REQUIRE(!element_bytes.empty()); + CHECK(element_bytes[0] == '{'); + + wide_json signed_element = element; + signed_element["_ArrayType_"] = "int8"; + signed_element["_ArrayData_"] = wide_json::array({make_int(1), make_int(-two_64 + 1)}); + const auto signed_element_bytes = wide_json::to_bjdata(signed_element, true, true); + REQUIRE(!signed_element_bytes.empty()); + CHECK(signed_element_bytes[0] == '{'); + + // the same for a dimension beyond 64 bits, which wrapped into a + // dimension matching the size of _ArrayData_ (here: 1) + wide_json dimension = element; + dimension["_ArraySize_"] = wide_json::array({make_uint(2), make_uint(static_cast(two_64) + 1)}); + dimension["_ArrayData_"] = wide_json::array({make_uint(1), make_uint(2)}); + const auto dimension_bytes = wide_json::to_bjdata(dimension, true, true); + REQUIRE(!dimension_bytes.empty()); + CHECK(dimension_bytes[0] == '{'); + + // in range, the ND-array is still written + wide_json fits = element; + fits["_ArrayData_"] = wide_json::array({make_uint(1), make_uint(2)}); + const auto fits_bytes = wide_json::to_bjdata(fits, true, true); + REQUIRE(!fits_bytes.empty()); + CHECK(fits_bytes[0] == '['); + } +} + +#endif + +TEST_CASE("custom number types") +{ + SECTION("signed integer of class type compared with a float") + { + // std::is_signed is false for a class type, which made the comparison + // treat any float below zero as less than every integer + const class_int_json minus_five = make_class_int(-5); + const class_int_json minus_two = make_class_int(-2); + const class_int_json five = make_class_int(5); + const class_int_json minus_two_and_a_half = -2.5; + const class_int_json two_and_a_half = 2.5; + const class_int_json huge_negative = -1e30; + const class_int_json huge_positive = 1e30; + + CHECK(minus_five < minus_two_and_a_half); + CHECK_FALSE(minus_two_and_a_half < minus_five); + CHECK(minus_two_and_a_half > minus_five); + CHECK(minus_five <= minus_two_and_a_half); + CHECK(minus_two_and_a_half >= minus_five); + CHECK(minus_five != minus_two_and_a_half); + + CHECK(minus_two_and_a_half < minus_two); + CHECK_FALSE(minus_two < minus_two_and_a_half); + + CHECK(two_and_a_half < five); + CHECK(minus_five < two_and_a_half); + + // floats beyond the integer's range + CHECK(huge_negative < minus_five); + CHECK_FALSE(minus_five < huge_negative); + CHECK(five < huge_positive); + CHECK_FALSE(huge_positive < five); + + // equality + const class_int_json minus_two_float = -2.0; + CHECK(minus_two == minus_two_float); + CHECK(minus_two_float == minus_two); + } + +} diff --git a/tests/src/unit-custom-object-key-type.cpp b/tests/src/unit-custom-object-key-type.cpp new file mode 100644 index 000000000..9c64ebce0 --- /dev/null +++ b/tests/src/unit-custom-object-key-type.cpp @@ -0,0 +1,127 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include + +#include +#include +#include + +#include "custom_object_key_type.hpp" + +// These tests instantiate a second basic_json specialization. They live in +// their own file rather than in unit-cbor.cpp and unit-msgpack.cpp to keep +// those objects below 65535 sections: the MinGW linker stores the section a +// COMDAT section is associated with in 16 bits, so it misplaces the jump +// tables of larger objects (see the clang job in windows.yml). + +TEST_CASE("CBOR supports custom object key types") +{ + using custom_json = custom_object_key_test::json; + using custom_key = custom_object_key_test::key; + + custom_json::object_t object; + object.emplace(custom_key{"short"}, 1); + object.emplace( + custom_key{"a key longer than twenty-three characters"}, + 2); + + const custom_json value(std::move(object)); + const auto encoded = custom_json::to_cbor(value); + + CHECK(nlohmann::json::from_cbor(encoded) == nlohmann::json + { + {"short", 1}, + {"a key longer than twenty-three characters", 2} + }); +} + +TEST_CASE("CBOR supports custom object key types nested deeper than the recursion depth limit") +{ + // below detail::recursion_depth_limit(), keys are written by + // write_cbor_iterative instead of write_cbor + using custom_json = custom_object_key_test::json; + using custom_key = custom_object_key_test::key; + + const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 10; + + custom_json value = 1; + nlohmann::json expected = 1; + for (std::size_t i = 0; i < depth; ++i) + { + // alternate short keys with ones long enough to need a length byte + const std::string name = (i % 2 == 0) ? "k" + std::to_string(i) + : "a key longer than thirty-one characters " + std::to_string(i); + + custom_json::object_t object; + object.emplace(custom_key{name}, std::move(value)); + value = custom_json(std::move(object)); + + nlohmann::json::object_t expected_object; + expected_object.emplace(name, std::move(expected)); + expected = nlohmann::json(std::move(expected_object)); + } + + const auto encoded = custom_json::to_cbor(value); + CHECK(encoded == nlohmann::json::to_cbor(expected)); + CHECK(nlohmann::json::from_cbor(encoded) == expected); +} + +TEST_CASE("MessagePack supports custom object key types") +{ + using custom_json = custom_object_key_test::json; + using custom_key = custom_object_key_test::key; + + custom_json::object_t object; + object.emplace(custom_key{"short"}, 1); + object.emplace( + custom_key{"a key longer than thirty-one characters"}, + 2); + + const custom_json value(std::move(object)); + const auto encoded = custom_json::to_msgpack(value); + + CHECK(nlohmann::json::from_msgpack(encoded) == nlohmann::json + { + {"short", 1}, + {"a key longer than thirty-one characters", 2} + }); +} + +TEST_CASE("MessagePack supports custom object key types nested deeper than the recursion depth limit") +{ + // below detail::recursion_depth_limit(), keys are written by + // write_msgpack_iterative instead of write_msgpack + using custom_json = custom_object_key_test::json; + using custom_key = custom_object_key_test::key; + + const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 10; + + custom_json value = 1; + nlohmann::json expected = 1; + for (std::size_t i = 0; i < depth; ++i) + { + // alternate short keys with ones long enough to need a length byte + const std::string name = (i % 2 == 0) ? "k" + std::to_string(i) + : "a key longer than thirty-one characters " + std::to_string(i); + + custom_json::object_t object; + object.emplace(custom_key{name}, std::move(value)); + value = custom_json(std::move(object)); + + nlohmann::json::object_t expected_object; + expected_object.emplace(name, std::move(expected)); + expected = nlohmann::json(std::move(expected_object)); + } + + const auto encoded = custom_json::to_msgpack(value); + CHECK(encoded == nlohmann::json::to_msgpack(expected)); + CHECK(nlohmann::json::from_msgpack(encoded) == expected); +} diff --git a/tests/src/unit-deserialization.cpp b/tests/src/unit-deserialization.cpp index ca9b63112..e82685311 100644 --- a/tests/src/unit-deserialization.cpp +++ b/tests/src/unit-deserialization.cpp @@ -35,156 +35,17 @@ using nlohmann::json; #include #include #include +#include + +#include "sax_event_loggers.hpp" + +using utils::SaxEventLogger; +using utils::SaxEventLoggerExitAfterKey; +using utils::SaxEventLoggerExitAfterStartArray; +using utils::SaxEventLoggerExitAfterStartObject; namespace { -struct SaxEventLogger : public nlohmann::json_sax -{ - bool null() override - { - events.emplace_back("null()"); - return true; - } - - bool boolean(bool val) override - { - events.emplace_back(val ? "boolean(true)" : "boolean(false)"); - return true; - } - - bool number_integer(json::number_integer_t val) override - { - events.push_back("number_integer(" + std::to_string(val) + ")"); - return true; - } - - bool number_unsigned(json::number_unsigned_t val) override - { - events.push_back("number_unsigned(" + std::to_string(val) + ")"); - return true; - } - - bool number_float(json::number_float_t /*val*/, const std::string& s) override - { - events.push_back("number_float(" + s + ")"); - return true; - } - - bool string(std::string& val) override - { - events.push_back("string(" + val + ")"); - return true; - } - - bool binary(json::binary_t& val) override - { - std::string binary_contents = "binary("; - std::string comma_space; - for (auto b : val) - { - binary_contents.append(comma_space); - binary_contents.append(std::to_string(static_cast(b))); - comma_space = ", "; - } - binary_contents.append(")"); - events.push_back(binary_contents); - return true; - } - - bool start_object(std::size_t elements) override - { - if (elements == (std::numeric_limits::max)()) - { - events.emplace_back("start_object()"); - } - else - { - events.push_back("start_object(" + std::to_string(elements) + ")"); - } - return true; - } - - bool key(std::string& val) override - { - events.push_back("key(" + val + ")"); - return true; - } - - bool end_object() override - { - events.emplace_back("end_object()"); - return true; - } - - bool start_array(std::size_t elements) override - { - if (elements == (std::numeric_limits::max)()) - { - events.emplace_back("start_array()"); - } - else - { - events.push_back("start_array(" + std::to_string(elements) + ")"); - } - return true; - } - - bool end_array() override - { - events.emplace_back("end_array()"); - return true; - } - - bool parse_error(std::size_t position, const std::string& /*last_token*/, const json::exception& /*ex*/) override - { - events.push_back("parse_error(" + std::to_string(position) + ")"); - return false; - } - - std::vector events {}; // NOLINT(readability-redundant-member-init) -}; - -struct SaxEventLoggerExitAfterStartObject : public SaxEventLogger -{ - bool start_object(std::size_t elements) override - { - if (elements == (std::numeric_limits::max)()) - { - events.emplace_back("start_object()"); - } - else - { - events.push_back("start_object(" + std::to_string(elements) + ")"); - } - return false; - } -}; - -struct SaxEventLoggerExitAfterKey : public SaxEventLogger -{ - bool key(std::string& val) override - { - events.push_back("key(" + val + ")"); - return false; - } -}; - -struct SaxEventLoggerExitAfterStartArray : public SaxEventLogger -{ - bool start_array(std::size_t elements) override - { - if (elements == (std::numeric_limits::max)()) - { - events.emplace_back("start_array()"); - } - else - { - events.push_back("start_array(" + std::to_string(elements) + ")"); - } - return false; - } -}; - template class proxy_iterator { @@ -1390,3 +1251,139 @@ TEST_CASE_TEMPLATE("deserialization of different character types (UTF-32)", T, c CHECK(json::sax_parse(v, &l)); CHECK(l.events.size() == 1); } + +namespace +{ +// records the position and message of a parse error; reporting the error via +// SAX does not throw, so the test also runs with JSON_NOEXCEPTION +struct SaxErrorRecorder : public SaxEventLogger +{ + bool parse_error(std::size_t position, const std::string& /*last_token*/, const json::exception& ex) override + { + error = std::to_string(position) + " " + ex.what(); + return false; + } + + std::string error {}; // NOLINT(readability-redundant-member-init) +}; +} // namespace + +TEST_CASE("parse error positions are the same for contiguous and stream input") +{ + // contiguous input derives the position from the input on error, stream + // input counts it while reading (see lexer::lazy_position); the cases + // cover newlines before the error, ungetting a newline after a number, + // newlines inside a string, a comment, and reads at the end of input + struct test_case + { + std::string input; + std::size_t byte; + std::string message; + }; + const std::vector cases = + { + {"[\n 1,\n 2\n", 12, "[json.exception.parse_error.101] parse error at line 4, column 1: syntax error while parsing array - unexpected end of input; expected ']'"}, + {"[1\n,2,\n]", 8, "[json.exception.parse_error.101] parse error at line 3, column 1: syntax error while parsing value - unexpected ']'; expected '[', '{', or a literal"}, + {"[1\n x]", 5, "[json.exception.parse_error.101] parse error at line 2, column 2: syntax error while parsing array - invalid literal; last read: '1 x'; expected ']'"}, + {"1\n2", 3, "[json.exception.parse_error.101] parse error at line 2, column 1: syntax error while parsing value - unexpected number literal; expected end of input"}, + { + "{\"a\":\n\"b\nc\"}", 9, R"([json.exception.parse_error.101] parse error at line 3, column 0: syntax error while parsing value - invalid string: control character U+000A (LF) must be escaped to \u000A or \n; last read: '"b')" + }, + {"/* a\n b */\n x", 13, "[json.exception.parse_error.101] parse error at line 3, column 2: syntax error while parsing value - invalid literal; last read: '/* a b */ x'"}, + {"\n\n", 3, "[json.exception.parse_error.101] parse error at line 3, column 1: syntax error while parsing value - unexpected end of input; expected '[', '{', or a literal"}, + {"[\"a\",\n 1e\n]", 10, "[json.exception.parse_error.101] parse error at line 3, column 0: syntax error while parsing value - invalid number; expected '+', '-', or digit after exponent; last read: '1e'"}, + {"[tru\n]", 5, "[json.exception.parse_error.101] parse error at line 2, column 0: syntax error while parsing value - invalid literal; last read: '[tru'"}, + }; + + for (const auto& c : cases) + { + CAPTURE(c.input) + const std::string expected = std::to_string(c.byte) + " " + c.message; + + SaxErrorRecorder string_sax; + CHECK_FALSE(json::sax_parse(c.input, &string_sax, json::input_format_t::json, true, true)); + CHECK(string_sax.error == expected); + + SaxErrorRecorder pointer_sax; + CHECK_FALSE(json::sax_parse(c.input.data(), c.input.data() + c.input.size(), &pointer_sax, json::input_format_t::json, true, true)); + CHECK(pointer_sax.error == expected); + + std::istringstream is(c.input); + SaxErrorRecorder stream_sax; + CHECK_FALSE(json::sax_parse(is, &stream_sax, json::input_format_t::json, true, true)); + CHECK(stream_sax.error == expected); + } +} + +#if !defined(JSON_NOEXCEPTION) +TEST_CASE("indentation runs of every length around the 8-byte skip") +{ + // contiguous input skips the spaces after a newline 8 bytes at a time (see + // lexer::skip_indentation), stream input reads them one by one; results, + // byte offsets, and messages must be the same for runs that end anywhere + // relative to a word boundary, including at the end of the input + const auto parse_result = [](const std::string & input, bool contiguous) + { + try + { + if (contiguous) + { + return json::parse(input.data(), input.data() + input.size(), nullptr, true, true).dump(); + } + std::istringstream is(input); + return json::parse(is, nullptr, true, true).dump(); + } + catch (const json::parse_error& e) + { + return std::to_string(e.byte).append(" ").append(e.what()); + } + }; + + // replace every '~' in a pattern by an indentation run + const auto indented = [](const std::string & pattern, const std::string & indent) + { + std::string result; + for (const char c : pattern) + { + if (c == '~') + { + result += indent; + } + else + { + result += c; + } + } + return result; + }; + + const std::vector patterns = + { + "[\n~1,\n~2\n]", + "[\n~1,\n~x]", + "[\n~1,\n~", + "1\n~", + "[\n~\t1,\n~\r\n~2]", + "[\n~/* c */ 1]", + "[\n~1\n~2]", + }; + + for (std::size_t n = 0; n <= 33; ++n) + { + const std::string indent(n, ' '); + for (const auto& pattern : patterns) + { + const std::string input = indented(pattern, indent); + CAPTURE(n) + CAPTURE(input) + const std::string expected = parse_result(input, false); + CHECK(parse_result(input, true) == expected); + CHECK(json::accept(input.data(), input.data() + input.size(), true) == json::accept(input, true)); + } + } + + // and against fixed expectations, in case both paths broke the same way + CHECK(parse_result(indented("[\n~x]", std::string(16, ' ')), true) == "19 [json.exception.parse_error.101] parse error at line 2, column 17: syntax error while parsing value - invalid literal; last read: '[ x'"); + CHECK(parse_result(indented("[\n~1]", std::string(17, ' ')), true) == "[1]"); +} +#endif diff --git a/tests/src/unit-diagnostic-positions.cpp b/tests/src/unit-diagnostic-positions.cpp index f5c6bc648..9e0625115 100644 --- a/tests/src/unit-diagnostic-positions.cpp +++ b/tests/src/unit-diagnostic-positions.cpp @@ -14,7 +14,7 @@ #define JSON_DIAGNOSTIC_POSITIONS 1 #include -using json = nlohmann::json; +using nlohmann::json; TEST_CASE("Better diagnostics with positions") { diff --git a/tests/src/unit-diagnostics.cpp b/tests/src/unit-diagnostics.cpp index 1ada023b7..504a95322 100644 --- a/tests/src/unit-diagnostics.cpp +++ b/tests/src/unit-diagnostics.cpp @@ -13,9 +13,11 @@ #endif #define JSON_DIAGNOSTICS 1 +#define JSON_TESTS_PRIVATE #include using nlohmann::json; +using nlohmann::ordered_json; #include #include @@ -567,9 +569,9 @@ TEST_CASE("Regression tests for extended diagnostics") } } -TEST_CASE("Better diagnostics past the descent bound of update() and merge_patch()") +TEST_CASE("Better diagnostics past the descent bound of update(), merge_patch() and merge_diff()") { - // Both merge objects nested more than detail::recursion_depth_limit() + // All three handle objects nested more than detail::recursion_depth_limit() // (128) levels deep without recursing; the values they add or replace // there must still know their parents. // The values are built rather than parsed, so that the expected messages @@ -615,4 +617,254 @@ TEST_CASE("Better diagnostics past the descent bound of update() and merge_patch CHECK_THROWS_WITH_AS(p->at("x").at(0), expected_x.c_str(), json::type_error); CHECK_THROWS_WITH_AS(p->at("y").at(0), expected_y.c_str(), json::type_error); } + + SECTION("merge_diff()") + { + // target and patch differ only in their innermost members, so the + // diff of the two is nested as deeply: {"a": ... {"x": null, "y": 2}} + const json diff = json::merge_diff(target, patch); // NOLINT(readability-suspicious-call-argument) + + const json* p = &diff; + for (std::size_t i = 0; i < depth; ++i) + { + p = &p->at("a"); + } + CHECK(p->at("x").is_null()); + CHECK_THROWS_WITH_AS(p->at("y").at(0), expected_y.c_str(), json::type_error); + } +} + +namespace +{ +// check that every child's parent pointer points to the containing value +template +bool parents_consistent(const BasicJsonType& j) +{ + bool ok = true; + if (j.is_array() || j.is_object()) + { + for (const auto& child : j) + { + ok = ok && child.m_parent == &j && parents_consistent(child); + } + } + return ok; +} +} // namespace + +TEST_CASE_TEMPLATE("Parent pointers stay consistent", J, json, ordered_json) +{ + SECTION("parse, copy, move, assign, swap") + { + J j = J::parse(R"({"a":[1,{"b":[2,3]},[4]],"c":{"d":{"e":[]}}})"); + CHECK(parents_consistent(j)); + + J copy(j); + CHECK(parents_consistent(copy)); + + J moved(std::move(copy)); + CHECK(parents_consistent(moved)); + + J assigned; + assigned = j; + CHECK(parents_consistent(assigned)); + + J other = {1, 2, {{"k", J::array({1, 2})}}}; + other.swap(assigned); + CHECK(parents_consistent(other)); + CHECK(parents_consistent(assigned)); + } + + SECTION("push_back growth") + { + J arr = J::array(); + for (int i = 0; i < 500; ++i) + { + arr.push_back(J::array({i})); + } + CHECK(parents_consistent(arr)); + + J obj = J::object(); + for (int i = 0; i < 500; ++i) + { + obj.push_back({std::to_string(i), J::array({i})}); + } + CHECK(parents_consistent(obj)); + } + + SECTION("emplace_back and emplace growth") + { + J arr = J::array(); + J obj = J::object(); + for (int i = 0; i < 500; ++i) + { + arr.emplace_back(J::array({i})); + obj.emplace(std::to_string(i), J::array({i})); + } + CHECK(parents_consistent(arr)); + CHECK(parents_consistent(obj)); + } + + SECTION("operator[] growth and at()") + { + J obj = J::object(); + for (int i = 0; i < 500; ++i) + { + obj[std::to_string(i)] = J::array({i}); + CHECK(obj[std::to_string(i / 2)].m_parent == &obj); + } + CHECK(parents_consistent(obj)); + + J arr = J::array(); + for (int i = 0; i < 500; ++i) + { + arr[static_cast(i)] = J::object({{"x", i}}); + } + for (size_t i = 0; i < 500; ++i) + { + CHECK(arr.at(i).m_parent == &arr); + } + CHECK(obj.at("3").m_parent == &obj); + CHECK(parents_consistent(arr)); + CHECK(parents_consistent(obj)); + } +} + +TEST_CASE_TEMPLATE("update() and merge_patch() keep parent pointers and diagnostic paths", J, json, ordered_json) +{ + // enough keys to make the vector behind ordered_json reallocate several times + const auto make_object = [](int first, int last) -> J + { + J o = J::object(); + for (int i = first; i < last; ++i) + { + o[std::to_string(i)] = J::object({{"x", J::array({i})}}); + } + return o; + }; + + // const access, so operator[] cannot repair the parent pointers + const auto check_path = [](const J & j, const std::string & key) + { + const std::string expected = "[json.exception.type_error.304] (/" + key + "/x) cannot use at() with array"; + CHECK_THROWS_WITH_AS(j[key]["x"].at("y"), expected.c_str(), json::type_error); + }; + + SECTION("update(const_reference)") + { + J j = make_object(0, 200); + j.update(make_object(100, 400)); + CHECK(j.size() == 400); + CHECK(parents_consistent(j)); + check_path(j, "0"); + check_path(j, "150"); + check_path(j, "399"); + } + + SECTION("update(const_reference, merge_objects)") + { + J j = make_object(0, 200); + j["50"]["y"] = 1; + J other = make_object(100, 400); + other["50"] = J::object({{"z", J::array({1})}}); + j.update(other, true); + CHECK(j.size() == 400); + CHECK(j["50"].size() == 3); + CHECK(parents_consistent(j)); + check_path(j, "0"); + check_path(j, "399"); + } + + SECTION("update(first, last)") + { + J j = make_object(0, 200); + J other = make_object(100, 400); + j.update(other.begin(), other.end()); + CHECK(j.size() == 400); + CHECK(parents_consistent(j)); + check_path(j, "0"); + check_path(j, "399"); + + J k = make_object(0, 200); + k.update(other.begin(), other.end(), true); + CHECK(k.size() == 400); + CHECK(parents_consistent(k)); + check_path(k, "399"); + } + + SECTION("merge_patch()") + { + J j = make_object(0, 200); + J patch = make_object(100, 400); + patch["0"] = nullptr; + j.merge_patch(patch); + CHECK(j.size() == 399); + CHECK_FALSE(j.contains("0")); + CHECK(parents_consistent(j)); + check_path(j, "1"); + check_path(j, "399"); + } +} + +namespace +{ +// an object type that, like a sorted flat map, moves its members when a key is +// added even if its capacity does not change; it has a capacity() member, but +// is neither node-based nor nlohmann::ordered_map, so the library must not +// trust its capacity to tell whether the members moved +template, + class Allocator = std::allocator>> + struct relocating_map : nlohmann::ordered_map +{ + using base_type = nlohmann::ordered_map; + using iterator = typename base_type::iterator; + using Container = typename base_type::Container; + using base_type::base_type; + + template + std::pair emplace(Args&& ... args) + { + auto result = base_type::emplace(std::forward(args)...); + const auto index = result.first - this->begin(); + if (result.second) + { + // move every member to a new buffer of the same capacity + Container relocated; + relocated.reserve(this->capacity()); + for (auto& member : *this) + { + relocated.emplace_back(member.first, std::move(member.second)); + } + this->Container::swap(relocated); + } + return {this->begin() + index, result.second}; + } +}; + +using relocating_json = nlohmann::basic_json; +} // namespace + +TEST_CASE("Parent pointers stay consistent for object types not known to keep their members in place") +{ + relocating_json obj = relocating_json::object(); + for (int i = 0; i < 100; ++i) + { + obj[std::to_string(i)] = relocating_json::array({i}); + obj.emplace(std::to_string(i + 1000), relocating_json::array({i})); + } + CHECK(parents_consistent(obj)); + + obj.erase("0"); + CHECK(parents_consistent(obj)); + + relocating_json other = relocating_json::object(); + for (int i = 0; i < 100; ++i) + { + other[std::to_string(i + 2000)] = relocating_json::array({i}); + } + obj.update(other); + CHECK(parents_consistent(obj)); + + obj["1"].push_back(42); + CHECK_THROWS_WITH_AS(obj["1"].at(5), "[json.exception.out_of_range.401] (/1) array index 5 is out of range", relocating_json::out_of_range&); } diff --git a/tests/src/unit-disabled_exceptions.cpp b/tests/src/unit-disabled_exceptions.cpp index 40a37c339..0e36d622e 100644 --- a/tests/src/unit-disabled_exceptions.cpp +++ b/tests/src/unit-disabled_exceptions.cpp @@ -13,7 +13,7 @@ DOCTEST_GCC_SUPPRESS_WARNING_PUSH DOCTEST_GCC_SUPPRESS_WARNING("-Wnoexcept") #include -using json = nlohmann::json; +using nlohmann::json; #ifdef JSON_TEST_NO_GLOBAL_UDLS using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) #endif diff --git a/tests/src/unit-element_access2.cpp b/tests/src/unit-element_access2.cpp index 1b59d8add..9f4d67fe8 100644 --- a/tests/src/unit-element_access2.cpp +++ b/tests/src/unit-element_access2.cpp @@ -535,6 +535,8 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j // Test malformed index (non-numeric) throws parse_error CHECK_THROWS_WITH_AS(j_array.value("/foo"_json_pointer, 1), "[json.exception.parse_error.109] parse error: array index 'foo' is not a number", typename Json::parse_error&); CHECK_THROWS_WITH_AS(j_array_const.value("/foo"_json_pointer, 1), "[json.exception.parse_error.109] parse error: array index 'foo' is not a number", typename Json::parse_error&); + CHECK_THROWS_WITH_AS(j_array.value("/x"_json_pointer, 1), "[json.exception.parse_error.109] parse error: array index 'x' is not a number", typename Json::parse_error&); + CHECK_THROWS_WITH_AS(j_array_const.value("/x"_json_pointer, 1), "[json.exception.parse_error.109] parse error: array index 'x' is not a number", typename Json::parse_error&); // Test leading-zero index throws parse_error CHECK_THROWS_WITH_AS(j_array.value("/01"_json_pointer, 1), "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", typename Json::parse_error&); @@ -1946,7 +1948,7 @@ TEST_CASE_TEMPLATE("element access 2 (additional value() tests)", Json, nlohmann #ifdef JSON_HAS_CPP_17 TEST_CASE("operator[] with user-defined std::string_view-convertible types") { - using json = nlohmann::json; + using nlohmann::json; class TestClass { @@ -2029,7 +2031,7 @@ TEST_CASE("keys convertible to std::string_view work with all lookup functions ( SECTION("nlohmann::json") { - using json = nlohmann::json; + using nlohmann::json; SECTION("ViewKey") { @@ -2070,7 +2072,7 @@ TEST_CASE("keys convertible to std::string_view work with all lookup functions ( SECTION("nlohmann::ordered_json") { - using ordered_json = nlohmann::ordered_json; + using nlohmann::ordered_json; SECTION("ViewKey") { diff --git a/tests/src/unit-explicit_instantiation.cpp b/tests/src/unit-explicit_instantiation.cpp index 71132a59f..ddb5e40f9 100644 --- a/tests/src/unit-explicit_instantiation.cpp +++ b/tests/src/unit-explicit_instantiation.cpp @@ -15,7 +15,7 @@ #include "doctest_compatibility.h" #include -using json = nlohmann::json; +using nlohmann::json; ///////////////////////////////////////////////////////////////////// // for #4825 - explicitly instantiating basic_json must compile; this diff --git a/tests/src/unit-file_input.cpp b/tests/src/unit-file_input.cpp new file mode 100644 index 000000000..e88b6f4d0 --- /dev/null +++ b/tests/src/unit-file_input.cpp @@ -0,0 +1,239 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Parsing from a FILE* locks the file once and reads it with the unlocked +// stdio functions (see JSON_DISABLE_UNLOCKED_FILE_IO). These tests pin that +// this is not observable: the results are the same, the file is unlocked +// again when parsing ends - also with an exception - and the same thread can +// keep using the file while it is locked, e.g., from a SAX callback. + +namespace +{ +// a file name of its own for every run: the test runs for several C++ +// standards and configurations at the same time, in the same directory +std::string temp_file_name() +{ + std::random_device rd; + return "unit-file_input-" + std::to_string(rd()) + ".tmp"; +} + +using file_ptr = std::unique_ptr; + +// write @a content to the file @a name and open it for reading +file_ptr open_with(const char* name, const std::string& content) +{ + { + std::ofstream file(name, std::ios::binary); + file << content; + } + return {std::fopen(name, "rb"), &std::fclose}; +} + +// The stdio lock is recursive, so whether parsing released it can only be +// seen from another thread: ftrylockfile() fails there while this thread +// holds the lock. ftrylockfile() is POSIX; elsewhere, the check is skipped. +#if defined(__GLIBC__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) +void check_unlocked(std::FILE* f) +{ + int result = -1; + std::thread other([f, &result] + { + result = ftrylockfile(f); + if (result == 0) + { + funlockfile(f); + } + }); + other.join(); + CHECK(result == 0); +} +#else +void check_unlocked(std::FILE* /*f*/) {} +#endif + +// SAX consumer that also reads the position of the file it is parsing, which +// takes the file's lock again on the same thread +class position_reading_sax : public nlohmann::json_sax +{ + public: + explicit position_reading_sax(std::FILE* f) : m_file(f) {} + position_reading_sax(const position_reading_sax&) = delete; + position_reading_sax(position_reading_sax&&) = delete; + position_reading_sax& operator=(const position_reading_sax&) = delete; + position_reading_sax& operator=(position_reading_sax&&) = delete; + ~position_reading_sax() override = default; + + bool null() override + { + return record(); + } + bool boolean(bool /*val*/) override + { + return record(); + } + bool number_integer(number_integer_t /*val*/) override + { + return record(); + } + bool number_unsigned(number_unsigned_t /*val*/) override + { + return record(); + } + bool number_float(number_float_t /*val*/, const string_t& /*s*/) override + { + return record(); + } + bool string(string_t& /*val*/) override + { + return record(); + } + bool binary(binary_t& /*val*/) override + { + return record(); + } + bool start_object(std::size_t /*elements*/) override + { + return record(); + } + bool key(string_t& /*val*/) override + { + return record(); + } + bool end_object() override + { + return record(); + } + bool start_array(std::size_t /*elements*/) override + { + return record(); + } + bool end_array() override + { + return record(); + } + bool parse_error(std::size_t /*position*/, const std::string& /*last_token*/, const nlohmann::detail::exception& /*ex*/) override + { + return false; + } + + const std::vector& positions() const + { + return m_positions; + } + + private: + bool record() + { + m_positions.push_back(std::ftell(m_file)); + return true; + } + + std::FILE* m_file; + std::vector m_positions{}; // NOLINT(readability-redundant-member-init) +}; +} // namespace + +TEST_CASE("parsing from a FILE*") +{ + const std::string temp_name = temp_file_name(); + const char* const name = temp_name.c_str(); + const std::string input = "{\"a\": [1, -2.5, \"three\", true, null], \"b\": {\"c\": \"caf\xc3\xa9\"}}"; + + SECTION("values are the same as when parsing from a string") + { + const file_ptr f = open_with(name, input); + REQUIRE(f != nullptr); + CHECK(json::parse(f.get()) == json::parse(input)); + // the whole input was consumed + std::array rest{}; + CHECK(std::fgets(rest.data(), static_cast(rest.size()), f.get()) == nullptr); + check_unlocked(f.get()); + } + +#if !defined(JSON_NOEXCEPTION) + SECTION("the file is unlocked after a parse error") + { + const file_ptr f = open_with(name, "[1, 2, x]"); + REQUIRE(f != nullptr); + json _; + CHECK_THROWS_WITH_AS(_ = json::parse(f.get()), + "[json.exception.parse_error.101] parse error at line 1, column 8: syntax error while parsing value - invalid literal; last read: '2, x'", + json::parse_error&); + check_unlocked(f.get()); + + // the file is still usable: the rest of the input is there + std::array rest{}; + CHECK(std::fgets(rest.data(), static_cast(rest.size()), f.get()) != nullptr); + CHECK(std::string(rest.data()) == "]"); + } + +#endif // !defined(JSON_NOEXCEPTION) + + SECTION("the file can be parsed again and read with stdio afterwards") + { + const file_ptr f = open_with(name, "[1] [2] tail"); + REQUIRE(f != nullptr); + + // sax_parse with strict == false stops after the first value; an array + // ends with its closing bracket, so the space after it is not read + json j; + nlohmann::detail::json_sax_dom_parser sax1(j); + CHECK(json::sax_parse(f.get(), &sax1, json::input_format_t::json, false)); + CHECK(j == json({1})); + check_unlocked(f.get()); + + json k; + nlohmann::detail::json_sax_dom_parser sax2(k); + CHECK(json::sax_parse(f.get(), &sax2, json::input_format_t::json, false)); + CHECK(k == json({2})); + check_unlocked(f.get()); + + std::array rest{}; + CHECK(std::fgets(rest.data(), static_cast(rest.size()), f.get()) != nullptr); + CHECK(std::string(rest.data()) == " tail"); + } + + SECTION("the same thread can use the file while it is parsed") + { + const file_ptr f = open_with(name, "[10, 20, 30]"); + REQUIRE(f != nullptr); + position_reading_sax sax(f.get()); + CHECK(json::sax_parse(f.get(), &sax)); + // start_array, three numbers, end_array + CHECK(sax.positions().size() == 5); + CHECK(sax.positions().front() == 1); + check_unlocked(f.get()); + } + + SECTION("binary formats read from a FILE*") + { + const json j = {{"string", std::string(300, 'x')}, {"array", {1, 2, 3}}, {"number", 1.5}}; + const std::vector cbor = json::to_cbor(j); + const file_ptr f = open_with(name, std::string(reinterpret_cast(cbor.data()), cbor.size())); + REQUIRE(f != nullptr); + CHECK(json::from_cbor(f.get()) == j); + check_unlocked(f.get()); + } + + static_cast(std::remove(name)); +} diff --git a/tests/src/unit-format-as.cpp b/tests/src/unit-format-as.cpp index b275f505c..61821f9ac 100644 --- a/tests/src/unit-format-as.cpp +++ b/tests/src/unit-format-as.cpp @@ -9,8 +9,8 @@ #include "doctest_compatibility.h" #include -using json = nlohmann::json; -using ordered_json = nlohmann::ordered_json; +using nlohmann::json; +using nlohmann::ordered_json; namespace { diff --git a/tests/src/unit-guard-page.cpp b/tests/src/unit-guard-page.cpp new file mode 100644 index 000000000..a4b12bb1a --- /dev/null +++ b/tests/src/unit-guard-page.cpp @@ -0,0 +1,503 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// The fast paths for contiguous input (bulk string scanning, SWAR searches, +// UTF-8 prefix validation, the 8-digit number loop, the 8-byte indentation +// skip, the BSON/BON8 bulk string readers) load several bytes at a time. This +// test places every input directly in front of (or directly behind) an +// inaccessible page, so reading even one byte outside the input crashes, with +// or without sanitizers. ASan alone misses over-reads into the slack of the +// same heap allocation. + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; + +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) + #define JSON_TEST_GUARD_PAGE_WINDOWS + #ifndef NOMINMAX + #define NOMINMAX + #endif + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #include +#elif defined(__unix__) || defined(__APPLE__) + #define JSON_TEST_GUARD_PAGE_POSIX + #include + #include +#endif + +#if defined(JSON_TEST_GUARD_PAGE_WINDOWS) || defined(JSON_TEST_GUARD_PAGE_POSIX) + +namespace +{ +// Three pages: an inaccessible one, a readable one, an inaccessible one. An +// input is copied either to the end of the middle page (any read past its last +// byte faults) or to its start (any read before its first byte faults). +class guarded_buffer +{ + public: + guarded_buffer() + : page(page_size()) + { +#if defined(JSON_TEST_GUARD_PAGE_WINDOWS) + void* const p = VirtualAlloc(nullptr, 3 * page, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS); + REQUIRE(p != nullptr); + base = static_cast(p); + DWORD old_protect = 0; + REQUIRE(VirtualProtect(base + page, page, PAGE_READWRITE, &old_protect) != 0); +#else + void* const p = mmap(nullptr, 3 * page, PROT_NONE, MAP_PRIVATE | MAP_ANON, -1, 0); + REQUIRE(p != MAP_FAILED); + base = static_cast(p); + REQUIRE(mprotect(base + page, page, PROT_READ | PROT_WRITE) == 0); +#endif + } + + ~guarded_buffer() + { +#if defined(JSON_TEST_GUARD_PAGE_WINDOWS) + VirtualFree(base, 0, MEM_RELEASE); +#else + munmap(base, 3 * page); +#endif + } + + guarded_buffer(const guarded_buffer&) = delete; + guarded_buffer& operator=(const guarded_buffer&) = delete; + guarded_buffer(guarded_buffer&&) = delete; + guarded_buffer& operator=(guarded_buffer&&) = delete; + + // copies @a input so its last byte is the last readable byte + const std::uint8_t* at_end(const std::string& input) + { + REQUIRE(input.size() <= page); + unsigned char* const first = base + (2 * page) - input.size(); + std::transform(input.begin(), input.end(), first, [](const char c) + { + return static_cast(c); + }); + return first; + } + + // copies @a input so its first byte is the first readable byte + const std::uint8_t* at_start(const std::string& input) + { + REQUIRE(input.size() <= page); + unsigned char* const first = base + page; + std::transform(input.begin(), input.end(), first, [](const char c) + { + return static_cast(c); + }); + return first; + } + + private: + static std::size_t page_size() + { +#if defined(JSON_TEST_GUARD_PAGE_WINDOWS) + SYSTEM_INFO info; + GetSystemInfo(&info); + return static_cast(info.dwPageSize); +#else + return static_cast(sysconf(_SC_PAGESIZE)); +#endif + } + + std::size_t page = 0; + unsigned char* base = nullptr; +}; + +// the serialization of the result of @a f, or the exception message; @a f +// takes the allow_exceptions flag, which is false without exceptions, where +// an error yields a discarded value instead +template +std::string outcome(F f) +{ +#if defined(JSON_NOEXCEPTION) + return "value: " + f(false).dump(-1, ' ', false, json::error_handler_t::keep); +#else + try + { + return "value: " + f(true).dump(-1, ' ', false, json::error_handler_t::keep); + } + catch (const json::exception& e) + { + return std::string("error: ") + e.what(); + } +#endif +} + +// a SAX handler that only counts events, so sax_parse runs without a DOM +struct counting_sax : nlohmann::json_sax +{ + std::size_t events = 0; + std::string error{}; // NOLINT(readability-redundant-member-init) + + bool null() override + { + return count(); + } + bool boolean(bool /*val*/) override + { + return count(); + } + bool number_integer(number_integer_t /*val*/) override + { + return count(); + } + bool number_unsigned(number_unsigned_t /*val*/) override + { + return count(); + } + bool number_float(number_float_t /*val*/, const string_t& /*s*/) override + { + return count(); + } + bool string(string_t& /*val*/) override + { + return count(); + } + bool binary(binary_t& /*val*/) override + { + return count(); + } + bool start_object(std::size_t /*elements*/) override + { + return count(); + } + bool key(string_t& /*val*/) override + { + return count(); + } + bool end_object() override + { + return count(); + } + bool start_array(std::size_t /*elements*/) override + { + return count(); + } + bool end_array() override + { + return count(); + } + bool parse_error(std::size_t position, const std::string& /*last_token*/, const nlohmann::detail::exception& ex) override + { + error = std::to_string(position) + " " + ex.what(); + return false; + } + + private: + bool count() + { + ++events; + return true; + } +}; + +// all prefixes of each seed, so the input ends at every byte of it +std::vector all_prefixes(const std::vector& seeds) +{ + std::vector result; + for (const auto& seed : seeds) + { + for (std::size_t n = 0; n <= seed.size(); ++n) + { + result.push_back(seed.substr(0, n)); + } + } + return result; +} + +std::vector json_seeds() +{ + std::vector seeds = + { + // string runs: ASCII, escapes, \u escapes, surrogate pairs + "\"abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\"", + R"("ab\ncd\"ef\\gh\u00e4ij\uD83D\uDE00kl\u12")", + // 2-, 3- and 4-byte UTF-8 sequences, then invalid UTF-8 + std::string("\"\xC3\xA4\xE2\x82\xAC\xF0\x9F\x98\x80\xC3\xA4\xE2\x82\xAC\xF0\x9F\x98\x80") + "abcdefgh\"", + "\"abcdefgh\xC3\x28 \xED\xA0\x80 \xF4\x90\x80\x80 \xF8 \xC0\xAF\"", + std::string("\"abcdefg\x01") + "abcdefgh\"", + // numbers, so the 8-digit loop runs up to the end + "12345678", + "1234567890123456", + "12345678901234567890123", + "-0.000000001234567890123456789", + "3.14159265358979323846264338327950288e+100", + "-1.2345678901234567e-12", + "1E-5", + "[1.5,2e10,-3E-5,0.000000001,123456789012345678]", + // literals + "[true,false,null]", + // comments (only parsed with ignore_comments) + "/* comment */ [1, // line comment\n 2] /**/", + // a pretty-printed document + json::parse(R"({"a": [1, 2.5, "three", {"b": null, "c": [true, false]}], "d": {"e": {"f": {"g": "h"}}}})").dump(4), + json::parse(R"({"a": [1, 2.5, "three", {"b": null, "c": [true, false]}], "d": {"e": {"f": {"g": "h"}}}})").dump(8, '\t'), + }; + + // indentation runs of every length, with and without a closing bracket + for (std::size_t n = 0; n <= 20; ++n) + { + seeds.push_back("[\n" + std::string(n, ' ') + "1,\n" + std::string(n, ' ') + "2\n]"); + } + + return all_prefixes(seeds); +} + +// a value whose binary serializations contain long strings and keys, numbers +// of every width, and nested containers +json binary_document() +{ + json j; + j["a key that is longer than eight bytes"] = "a string value that is longer than eight bytes"; + j["\xC3\xA4\xE2\x82\xAC\xF0\x9F\x98\x80"] = "\xF0\x9F\x98\x80\xE2\x82\xAC\xC3\xA4 utf-8 0123456789"; + j["numbers"] = {0, 1, -1, 255, -256, 65536, -2147483649LL, 4294967296ULL, 1.5, -2.25e100}; + j["nested"] = {{"x", {{"y", {true, false, nullptr}}}}}; + j["s"] = ""; + return j; +} + +// readers for check_binary(), callable with a container or an iterator range +// followed by the allow_exceptions flag +#define JSON_TEST_GUARD_PAGE_READER(name, from) \ + struct name \ + { \ + template \ + json operator()(bool allow_exceptions, Args&& ... args) const \ + { \ + return json::from(std::forward(args)..., true, allow_exceptions); \ + } \ + }; +JSON_TEST_GUARD_PAGE_READER(read_bson, from_bson) +JSON_TEST_GUARD_PAGE_READER(read_bon8, from_bon8) +JSON_TEST_GUARD_PAGE_READER(read_cbor, from_cbor) +JSON_TEST_GUARD_PAGE_READER(read_msgpack, from_msgpack) +JSON_TEST_GUARD_PAGE_READER(read_ubjson, from_ubjson) +JSON_TEST_GUARD_PAGE_READER(read_bjdata, from_bjdata) +#undef JSON_TEST_GUARD_PAGE_READER + +// @a bytes as characters (explicitly converted: the iterator-range constructor +// would convert implicitly, which -fsanitize=integer reports) +std::string as_chars(const std::vector& bytes) +{ + std::string result; + result.reserve(bytes.size()); + for (const std::uint8_t b : bytes) + { + result.push_back(static_cast(b)); + } + return result; +} + +// reads every prefix of @a bytes at both ends of the guarded page and from a +// std::string, which must give the same outcome +template +void check_binary(guarded_buffer& guard, const std::vector& bytes, Reader parse) +{ + for (const auto& input : all_prefixes({as_chars(bytes)})) + { + CAPTURE(input) + const std::string expected = outcome([&](bool allow) + { + return parse(allow, input); + }); + for (int placement = 0; placement < 2; ++placement) + { + const std::uint8_t* const first = placement == 0 ? guard.at_end(input) : guard.at_start(input); + CHECK(outcome([&](bool allow) + { + return parse(allow, first, first + input.size()); + }) == expected); + } + } +} + +// parse_float_native() at both ends of the guarded page, compared bit for bit +// with converting the same token from a std::string +template +std::uint64_t float_bits(FloatType value) +{ + static_assert(sizeof(FloatType) <= sizeof(std::uint64_t), "unsupported floating-point width"); + std::uint64_t bits = 0; + std::memcpy(&bits, &value, sizeof(value)); + return bits; +} + +template +void check_float_native(guarded_buffer& guard, const std::string& token) +{ + const auto decimal_point = token.find('.'); + const auto exponent = token.find_first_of("eE"); + const auto mantissa_end = exponent == std::string::npos ? token.size() : exponent; + const auto expected = nlohmann::detail::parse_float_native(token.data(), token.data() + token.size(), decimal_point, mantissa_end); + for (int placement = 0; placement < 2; ++placement) + { + const char* const first = reinterpret_cast(placement == 0 ? guard.at_end(token) : guard.at_start(token)); + const auto value = nlohmann::detail::parse_float_native(first, first + token.size(), decimal_point, mantissa_end); + CHECK(float_bits(value) == float_bits(expected)); + } +} +} // namespace + +TEST_CASE("guard page") +{ + guarded_buffer guard; + + SECTION("string scanning") + { + // fills that drive each function to a different stop + const std::vector fills = + { + std::string(64, 'a'), + std::string(64, '"'), + std::string(64, '\\'), + std::string(64, '\x01'), + std::string(64, '\x7F'), + std::string(64, '\xFF'), + }; + std::vector patterns = fills; + { + std::string utf8; + while (utf8.size() < 64) + { + utf8 += "\xC3\xA4\xE2\x82\xAC\xF0\x9F\x98\x80" "ab"; + } + patterns.push_back(utf8); + } + { + std::string bytes; + for (unsigned int i = 0; i < 64; ++i) + { + bytes.push_back(static_cast((i * 37u) & 0xFFu)); + } + patterns.push_back(bytes); + } + + for (const auto& pattern : patterns) + { + for (std::size_t n = 0; n <= 64; ++n) + { + const std::string input = pattern.substr(0, n); + for (int placement = 0; placement < 2; ++placement) + { + const unsigned char* const p = placement == 0 ? guard.at_end(input) : guard.at_start(input); + + CHECK(nlohmann::detail::find_string_special(p, n) <= n); + CHECK(nlohmann::detail::find_ascii_copyable_run(p, n) <= n); + CHECK(nlohmann::detail::valid_utf8_prefix(p, n) <= n); +#if defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17) + CHECK(nlohmann::detail::find_string_delimiter(p, n) <= n); +#endif + CHECK(nlohmann::detail::string_bulk_run(p, n) <= n); + CHECK(nlohmann::detail::scalar_string_bulk_run(p, n) <= n); + for (std::size_t i = 0; i < n; ++i) + { + CHECK(nlohmann::detail::validate_one_utf8(p + i, n - i) <= n - i); + } + } + } + } + } + + SECTION("float conversion") + { + // the lexer converts floats from its token buffer, so the JSON section + // below never places a float token at the page boundary; this calls the + // conversion directly: the 8-digit loops of parse_float_significand() + // and digit_comparison() read up to the end of the token + const std::vector tokens = all_prefixes( + { + "1234567812345678123456781234567812345678", + "-1.2345678901234567890123456789e-123", + "0.00000000000000000000000000000000000001", + "98765432.10987654", + "123456789012345678901234567890e-30", + // halfway between 1 and the next double, then just above it + "1.00000000000000011102230246251565404236316680908203125", + "1.000000000000000111022302462515654042363166809082031250000000001", + }); + for (const auto& token : tokens) + { + // only complete number tokens: the conversion expects validated input + if (!token.empty() && json::accept(token)) + { + CAPTURE(token) + check_float_native(guard, token); + check_float_native(guard, token); + } + } + } + + SECTION("JSON") + { + for (const auto& input : json_seeds()) + { + CAPTURE(input) + const std::string expected = outcome([&](bool allow) + { + return json::parse(input, nullptr, allow); + }); + const std::string expected_comments = outcome([&](bool allow) + { + return json::parse(input, nullptr, allow, true); + }); + const bool expected_accept = json::accept(input); + counting_sax expected_sax; + const bool expected_sax_result = json::sax_parse(input, &expected_sax); + + for (int placement = 0; placement < 2; ++placement) + { + const std::uint8_t* const first = placement == 0 ? guard.at_end(input) : guard.at_start(input); + const std::uint8_t* const last = first + input.size(); + + CHECK(outcome([&](bool allow) + { + return json::parse(first, last, nullptr, allow); + }) == expected); + CHECK(outcome([&](bool allow) + { + return json::parse(first, last, nullptr, allow, true); + }) == expected_comments); + CHECK(json::accept(first, last) == expected_accept); + + counting_sax sax; + CHECK(json::sax_parse(first, last, &sax) == expected_sax_result); + CHECK(sax.events == expected_sax.events); + CHECK(sax.error == expected_sax.error); + } + } + } + + SECTION("binary formats") + { + // every prefix ends inside a value, e.g., inside a BSON key or BON8 + // string without its terminator + const json j = binary_document(); + check_binary(guard, json::to_bson(j), read_bson()); + check_binary(guard, json::to_bon8(j), read_bon8()); + check_binary(guard, json::to_cbor(j), read_cbor()); + check_binary(guard, json::to_msgpack(j), read_msgpack()); + check_binary(guard, json::to_ubjson(j), read_ubjson()); + check_binary(guard, json::to_bjdata(j), read_bjdata()); + } +} + +#endif diff --git a/tests/src/unit-hash.cpp b/tests/src/unit-hash.cpp index 117bc0151..e544cff9b 100644 --- a/tests/src/unit-hash.cpp +++ b/tests/src/unit-hash.cpp @@ -9,8 +9,8 @@ #include "doctest_compatibility.h" #include -using json = nlohmann::json; -using ordered_json = nlohmann::ordered_json; +using nlohmann::json; +using nlohmann::ordered_json; #include #include diff --git a/tests/src/unit-iterators1.cpp b/tests/src/unit-iterators1.cpp index b7b0aa9eb..1f0a6ba4b 100644 --- a/tests/src/unit-iterators1.cpp +++ b/tests/src/unit-iterators1.cpp @@ -10,6 +10,8 @@ #define JSON_TESTS_PRIVATE #include +#include +#include using nlohmann::json; TEST_CASE("iterators 1") @@ -1628,3 +1630,109 @@ TEST_CASE("iterators 1") } } } + +TEST_CASE_TEMPLATE("reverse iteration and member access", Json, nlohmann::json, nlohmann::ordered_json) +{ + SECTION("array: -- and rbegin/rend") + { + Json j = {1, 2, 3, 4}; + + const std::vector reversed = {4, 3, 2, 1}; + auto it = j.end(); + std::vector seen; + while (it != j.begin()) + { + --it; + seen.push_back(it->template get()); + } + CHECK(seen == reversed); + + auto it2 = j.end(); + auto old = it2--; + CHECK(old == j.end()); + CHECK(*it2 == 4); + + seen.clear(); + for (auto r = j.rbegin(); r != j.rend(); ++r) + { + seen.push_back(r->template get()); + } + CHECK(seen == reversed); + + seen.clear(); + for (auto r = j.crbegin(); r != j.crend(); ++r) + { + seen.push_back(r->template get()); + } + CHECK(seen == reversed); + } + + SECTION("object: -- and rbegin/rend") + { + Json j = {{"a", 1}, {"b", 2}, {"c", 3}}; + // order of keys depends on the object type; use the forward order as reference + std::vector fwd; + for (auto it = j.begin(); it != j.end(); ++it) + { + fwd.push_back(it.key()); + } + std::vector rev(fwd.rbegin(), fwd.rend()); + + std::vector seen; + auto it = j.end(); + while (it != j.begin()) + { + --it; + seen.push_back(it.key()); + } + CHECK(seen == rev); + + auto it2 = j.end(); + auto old = it2--; + CHECK(old == j.end()); + CHECK(it2.key() == rev.front()); + + seen.clear(); + for (auto r = j.rbegin(); r != j.rend(); ++r) + { + seen.push_back(r.key()); + } + CHECK(seen == rev); + } + + SECTION("object: operator-> reads and writes the mapped value") + { + Json j = {{"a", 1}, {"s", "str"}}; + auto it = j.find("s"); + REQUIRE(it != j.end()); + CHECK(it->is_string()); + CHECK(it->size() == 1); + *it = Json::array({1, 2}); + CHECK(it->is_array()); + it->push_back(3); + CHECK(j["s"].size() == 3); + + const Json& cj = j; + auto cit = cj.find("a"); + REQUIRE(cit != cj.end()); + CHECK(cit->is_number_integer()); + CHECK(cit->template get() == 1); + } + + SECTION("primitives: -- and ->") + { + Json j = 42; + auto it = j.end(); + --it; + CHECK(it == j.begin()); + CHECK(it->template get() == 42); + } + + SECTION("comparing iterators of different containers throws") + { + Json a = {1, 2}; + Json b = {1, 2}; + CHECK_THROWS_WITH_AS(a.begin() == b.begin(), "[json.exception.invalid_iterator.212] cannot compare iterators of different containers", nlohmann::json::invalid_iterator); + CHECK_THROWS_WITH_AS(a.begin() < b.begin(), "[json.exception.invalid_iterator.212] cannot compare iterators of different containers", nlohmann::json::invalid_iterator); + } +} diff --git a/tests/src/unit-json_patch.cpp b/tests/src/unit-json_patch.cpp index 55775c0b6..67ffee6dd 100644 --- a/tests/src/unit-json_patch.cpp +++ b/tests/src/unit-json_patch.cpp @@ -1713,6 +1713,21 @@ TEST_CASE("JSON patch - move where 'from' is a proper prefix of 'path' (regressi CHECK(doc.patch(patch) == R"({"b": 1})"_json); } + SECTION("array index tokens that are not a number") + { + json const doc = R"({"a": [1, 2]})"_json; + + // "-" cannot be removed and is reported as unresolved + json const patch_dash = {{{"op", "remove"}, {"path", "/a/-"}}}; + CHECK_THROWS_WITH_AS(doc.patch(patch_dash), "[json.exception.out_of_range.404] unresolved reference token '-'", json::out_of_range&); + + // a single character is reported like a longer token + json const patch_x = {{{"op", "remove"}, {"path", "/a/x"}}}; + CHECK_THROWS_WITH_AS(doc.patch(patch_x), "[json.exception.parse_error.109] parse error: array index 'x' is not a number", json::parse_error&); + json const patch_xy = {{{"op", "remove"}, {"path", "/a/xy"}}}; + CHECK_THROWS_WITH_AS(doc.patch(patch_xy), "[json.exception.parse_error.109] parse error: array index 'xy' is not a number", json::parse_error&); + } + SECTION("the array-append token '-' is an ordinary child token") { // "-" (append-to-array) addresses a location *inside* the array, @@ -2051,3 +2066,40 @@ TEST_CASE("JSON patch - every operation on ordered_json") } } } + +TEST_CASE_TEMPLATE("JSON patch - diff round trip on hand-made documents", Json, nlohmann::json, nlohmann::ordered_json) +{ + const auto roundtrip = [](const Json & a, const Json & b) + { + CAPTURE(a.dump()) + CAPTURE(b.dump()) + const Json p = Json::diff(a, b); + CHECK(a.patch(p) == b); + CHECK(Json::diff(b, b).empty()); + }; + + SECTION("reordered keys") + { + const Json a = Json::parse(R"({"x":1,"y":2,"z":{"p":1,"q":2}})"); + const Json b = Json::parse(R"({"z":{"q":2,"p":1},"y":2,"x":1})"); + roundtrip(a, b); + roundtrip(b, a); + } + + SECTION("nested add, remove and replace") + { + const Json a = Json::parse(R"({"a":[1,2,{"k":"v","gone":true}],"b":{"c":{"d":1}},"e":"s"})"); + const Json b = Json::parse(R"({"a":[1,3,{"k":"w","new":[]}],"b":{"c":{"d":[1,2]}},"f":null})"); + roundtrip(a, b); + roundtrip(b, a); + } + + SECTION("arrays of different length and type changes") + { + roundtrip(Json::parse("[1,2,3,4,5]"), Json::parse("[1,3]")); + roundtrip(Json::parse("[1]"), Json::parse("[1,[2],{\"a\":3}]")); + roundtrip(Json::parse("[]"), Json::parse("{}")); + roundtrip(Json::parse("{\"a\":[]}"), Json::parse("{\"a\":{}}")); + roundtrip(Json(1), Json::parse("[1]")); + } +} diff --git a/tests/src/unit-json_pointer.cpp b/tests/src/unit-json_pointer.cpp index 86293b116..e09b4dc9f 100644 --- a/tests/src/unit-json_pointer.cpp +++ b/tests/src/unit-json_pointer.cpp @@ -23,8 +23,14 @@ using nlohmann::json; using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) #endif +#include +#include +#include #include +#include #include +#include +#include TEST_CASE("JSON pointers") { @@ -421,6 +427,27 @@ TEST_CASE("JSON pointers") CHECK_THROWS_WITH_AS(json({{"/list/0", 1}, {"/list/1", 2}, {"/list/three", 3}}).unflatten(), "[json.exception.parse_error.109] parse error: array index 'three' is not a number", json::parse_error&); + // a single-character token that is not a digit is reported like a + // longer one (parse_error.109), not as unresolved (out_of_range.404) + CHECK_THROWS_WITH_AS(j["/x"_json_pointer] = 1, + "[json.exception.parse_error.109] parse error: array index 'x' is not a number", json::parse_error&); + CHECK_THROWS_WITH_AS(j_const["/x"_json_pointer] == 1, + "[json.exception.parse_error.109] parse error: array index 'x' is not a number", json::parse_error&); + CHECK_THROWS_WITH_AS(j.at("/x"_json_pointer) = 1, + "[json.exception.parse_error.109] parse error: array index 'x' is not a number", json::parse_error&); + CHECK_THROWS_WITH_AS(j_const.at("/x"_json_pointer) == 1, + "[json.exception.parse_error.109] parse error: array index 'x' is not a number", json::parse_error&); + CHECK_THROWS_WITH_AS(j.at("/+"_json_pointer), + "[json.exception.parse_error.109] parse error: array index '+' is not a number", json::parse_error&); + CHECK(!j.contains("/x"_json_pointer)); + CHECK(!j_const.contains("/x"_json_pointer)); + CHECK_THROWS_WITH_AS(json({{"/list/0", 1}, {"/list/x", 2}}).unflatten(), + "[json.exception.parse_error.109] parse error: array index 'x' is not a number", json::parse_error&); + + // "-" is a valid reference token, so it is still reported as unresolved + CHECK_THROWS_WITH_AS(json({{"/list/0", 1}, {"/list/-", 2}}).unflatten(), + "[json.exception.out_of_range.404] unresolved reference token '-'", json::out_of_range&); + // assign to "-" j["/-"_json_pointer] = 99; CHECK(j == json({1, 13, 3, 33, nullptr, 55, 99})); @@ -1005,3 +1032,353 @@ TEST_CASE("unescaping keeps a '~' that does not start an escape sequence") nlohmann::detail::unescape(s); CHECK(s == "~/~"); } + +TEST_CASE("flatten of structured values") +{ + SECTION("values nested too deeply for the call stack (#5393)") + { + // flatten() used to recurse once per nesting level + const std::size_t depth = 100000; + for (const bool objects : + { + false, true + }) + { + CAPTURE(objects) + std::string text; + std::string path; + for (std::size_t i = 0; i < depth; ++i) + { + text += objects ? "{\"a\":" : "["; + path += objects ? "/a" : "/0"; + } + text += "0"; + text += std::string(depth, objects ? '}' : ']'); + const auto value = json::parse(text); + + const auto flat = value.flatten(); + REQUIRE(flat.size() == 1); + REQUIRE(flat.begin().key().size() == path.size()); + CHECK(flat.begin().key() == path); + CHECK(flat.begin().value() == 0); + + // unflatten() is linear in the depth, so the value roundtrips + CHECK(flat.unflatten() == value); + } + } + + SECTION("unflatten of a deeply nested pointer") + { + const std::size_t depth = 100000; + for (const bool objects : + { + false, true + }) + { + CAPTURE(objects) + std::string path; + for (std::size_t i = 0; i < depth; ++i) + { + path += objects ? "/a" : "/0"; + } + + json flat = json::object(); + flat[path] = 1; + const json value = flat.unflatten(); + + // walk down iteratively + std::size_t levels = 0; + const json* current = &value; + while (objects ? current->is_object() : current->is_array()) + { + REQUIRE(current->size() == 1); + current = objects ? ¤t->at("a") : ¤t->at(0); + ++levels; + } + CHECK(levels == depth); + CHECK(*current == 1); + } + } + + SECTION("unflatten does not depend on the iteration order") + { + // the "0" key comes after its sibling in iteration order + const nlohmann::ordered_json flat_array = nlohmann::ordered_json::parse(R"({"/a/1": 2, "/a/0": 1})"); + CHECK(flat_array.unflatten() == nlohmann::ordered_json::parse(R"({"a": [1, 2]})")); + + const nlohmann::ordered_json flat_object = nlohmann::ordered_json::parse(R"({"/b/1": 2})"); + CHECK(flat_object.unflatten() == nlohmann::ordered_json::parse(R"({"b": {"1": 2}})")); + } + + SECTION("objects and arrays interleaved") + { + const json value = + { + {"a", {1, {{"b", json::array()}, {"c", json::object()}}, json::array({{{"x~/", {true, nullptr}}}})}}, + {"a/b", {{"~", 1}}}, + {"z", "s"} + }; + + const json expected = + { + {"/a/0", 1}, + {"/a/1/b", nullptr}, + {"/a/1/c", nullptr}, + {"/a/2/0/x~0~1/0", true}, + {"/a/2/0/x~0~1/1", nullptr}, + {"/a~1b/~0", 1}, + {"/z", "s"} + }; + + CHECK(value.flatten() == expected); + } + + SECTION("order of the entries of an ordered_json") + { + const auto value = nlohmann::ordered_json::parse( + R"({"z":"s","a/b":{"~":1,"k":[]},"a":[1,{"c":{},"b":[]},[{"x~/":[true,null],"w":2}]]})"); + + const auto flat = value.flatten(); + CHECK(flat.dump() == + R"({"/z":"s","/a~1b/~0":1,"/a~1b/k":null,"/a/0":1,"/a/1/c":null,"/a/1/b":null,"/a/2/0/x~0~1/0":true,"/a/2/0/x~0~1/1":null,"/a/2/0/w":2})"); + } +} + +TEST_CASE("flatten of ordered_json") +{ + using nlohmann::ordered_json; + + SECTION("keeps the document order of the leaves") + { + const ordered_json j = ordered_json::parse(R"({ + "z": 1, "a": {"y": [true, null, {}], "b": "x", "": []}, + "m~n": 2.5, "o/p": {"k": {}}, "0": [[1], [2, 3]] + })"); + + const ordered_json flat = j.flatten(); + std::vector keys; + for (const auto& el : flat.items()) + { + keys.push_back(el.key()); + } + const std::vector expected = + { + "/z", "/a/y/0", "/a/y/1", "/a/y/2", "/a/b", "/a/", "/m~0n", "/o~1p/k", "/0/0/0", "/0/1/0", "/0/1/1" + }; + CHECK(keys == expected); + CHECK(flat.size() == expected.size()); + CHECK(flat["/m~0n"] == 2.5); + CHECK(flat["/a/y/2"].is_null()); + } + + SECTION("round-trips and agrees with json") + { + ordered_json j; + for (int i = 0; i < 200; ++i) + { + j["k" + std::to_string(i)] = {{"v", i}, {"l", {i, i + 1, "s"}}}; + } + const ordered_json flat = j.flatten(); + CHECK(flat.size() == 800); + CHECK(flat.unflatten() == j); + + // same entries as for a sorted object + const nlohmann::json plain = nlohmann::json::parse(j.dump()); + CHECK(nlohmann::json::parse(flat.dump()) == plain.flatten()); + } + + SECTION("primitive and empty root") + { + CHECK(ordered_json(1).flatten() == ordered_json({{"", 1}})); + CHECK(ordered_json::array().flatten() == ordered_json({{"", nullptr}})); + } +} +namespace +{ +// reference implementation of the array index rules (RFC 6901, Sect. 4) based on strtoull +enum class index_class +{ + ok, + leading_zero, + not_a_number, + unresolved, + exceeds_size_type +}; + +index_class classify_index(const std::string& s, std::size_t& idx) +{ + if (s.size() > 1 && s[0] == '0') + { + return index_class::leading_zero; + } + if (!s.empty() && s != "-" && !(s[0] >= '0' && s[0] <= '9')) + { + return index_class::not_a_number; + } + char* end = nullptr; + errno = 0; + const unsigned long long res = std::strtoull(s.c_str(), &end, 10); // NOLINT(runtime/int) + if (end == s.c_str() || errno == ERANGE || static_cast(end - s.c_str()) != s.size()) + { + return index_class::unresolved; + } + if (res >= static_cast((std::numeric_limits::max)())) // NOLINT(runtime/int) + { + return index_class::exceeds_size_type; + } + idx = static_cast(res); + return index_class::ok; +} + +// the observable behavior of json_pointer::at() and contains() for an array index token +void check_index_token(const std::string& token) +{ + const json array = {10, 20, 30}; + const json::json_pointer ptr("/" + token); + + std::size_t idx = 0; + const auto cls = classify_index(token, idx); + const bool exists = cls == index_class::ok && idx < array.size(); + + CAPTURE(token) + CHECK(array.contains(ptr) == (exists && token != "-")); + + if (token == "-") + { + CHECK_THROWS_AS(array.at(ptr), json::out_of_range&); + return; + } + + switch (cls) + { + case index_class::leading_zero: + CHECK_THROWS_AS(array.at(ptr), json::parse_error&); + break; + case index_class::not_a_number: + CHECK_THROWS_AS(array.at(ptr), json::parse_error&); + break; + case index_class::unresolved: + case index_class::exceeds_size_type: + CHECK_THROWS_AS(array.at(ptr), json::out_of_range&); + break; + case index_class::ok: + default: + if (exists) + { + CHECK(array.at(ptr) == array[idx]); + } + else + { + CHECK_THROWS_AS(array.at(ptr), json::out_of_range&); + } + break; + } +} +} // namespace + +TEST_CASE("JSON pointer array index tokens match strtoull semantics") +{ + SECTION("boundary values") + { + const std::string max_digits = std::to_string((std::numeric_limits::max)()); + const std::vector tokens = + { + "", "-", "0", "1", "2", "3", "00", "01", "007", "+1", "-1", " 1", "1 ", "1a", "a1", "0x1", "1e1", "1.0", + "999999999", "1000000000", "99999999999999999", "999999999999999999", "1000000000000000000", + "9999999999999999999", "18446744073709551614", max_digits, "18446744073709551616", + "99999999999999999999999", "000000000000000000000" + }; + for (const auto& token : tokens) + { + check_index_token(token); + } + } + + SECTION("random tokens") + { + std::mt19937 gen(42); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible + const std::string alphabet = "0123456789012345678901234567890123456789-+ ex"; + for (int i = 0; i < 5000; ++i) + { + std::string token(gen() % 23, '0'); + for (auto& c : token) + { + c = alphabet[gen() % alphabet.size()]; + } + check_index_token(token); + } + } +} + +TEST_CASE("JSON pointer operations on pointers with escaped tokens") +{ + const std::vector strings = + { + "", "/", "//", "/a", "/a/b", "/a~1b", "/~0", "/~01", "/~10", "/a~0~1/b/", "/0/1/2", "/ ", "/a/", "/~1/~0" + }; + + for (const auto& str : strings) + { + CAPTURE(str) + const json::json_pointer ptr(str); + CHECK(ptr.to_string() == str); + CHECK(ptr / "x" / "y" / std::size_t{3} == json::json_pointer(str + "/x/y/3")); + CHECK(ptr.parent_pointer() == (str.empty() ? ptr : json::json_pointer(str.substr(0, str.rfind('/'))))); + + // comparison with a string, in place, and with every other pointer +#if !defined(JSON_TEST_DEPRECATED_FUNCTIONS_DELETED) + for (const auto& other : strings) + { + CAPTURE(other) + CHECK((ptr == other) == (str == other)); + CHECK((other == ptr) == (str == other)); + CHECK((ptr != other) == (str != other)); + } + CHECK_THROWS_AS(ptr == "x", json::parse_error&); + CHECK_THROWS_AS(ptr == "/~", json::parse_error&); + CHECK_THROWS_AS(ptr == "/b/~2", json::parse_error&); +#endif + } +} + +TEST_CASE("JSON pointer lookups") +{ + json j = {{"a", {{"b", {1, 2, 3}}}}, {"", 7}, {"~/", {{"x", nullptr}}}}; + const json& cj = j; + + CHECK(cj[json::json_pointer("/a/b/2")] == 3); + CHECK(cj.at(json::json_pointer("/a/b/1")) == 2); + CHECK(cj.at(json::json_pointer("/")) == 7); + CHECK(cj.contains(json::json_pointer("/~0~1/x"))); + CHECK(!cj.contains(json::json_pointer("/a/c"))); + CHECK(cj.value(json::json_pointer("/a/b/0"), 5) == 1); + CHECK(cj.value(json::json_pointer("/a/b/3"), 5) == 5); + CHECK(cj.value(json::json_pointer("/a/zz"), 5) == 5); + + CHECK_THROWS_AS(cj.at(json::json_pointer("/a/c")), json::out_of_range&); + CHECK_THROWS_AS(cj.at(json::json_pointer("/a/b/3")), json::out_of_range&); + + // non-const access reuses existing entries and creates missing ones + j[json::json_pointer("/a/b/1")] = 20; + j[json::json_pointer("/n/m")] = 1; + CHECK(j["a"]["b"] == json({1, 20, 3})); + CHECK(j["n"]["m"] == 1); +} + +TEST_CASE("JSON patch with moves and copies") +{ + json doc = {{"a", {{"b", {1, 2, 3}}}}, {"c", {{"d", "x"}}}}; + + CHECK(doc.patch(json::parse(R"([{"op":"move","from":"/a/b","path":"/c/e"}])")) == + json::parse(R"({"a":{},"c":{"d":"x","e":[1,2,3]}})")); + CHECK(doc.patch(json::parse(R"([{"op":"copy","from":"/a/b","path":"/c/e"}])")) == + json::parse(R"({"a":{"b":[1,2,3]},"c":{"d":"x","e":[1,2,3]}})")); + CHECK(doc.patch(json::parse(R"([{"op":"move","from":"/a/b/0","path":"/a/b/2"}])")) == + json::parse(R"({"a":{"b":[2,3,1]},"c":{"d":"x"}})")); + + // a failing move leaves a copy-patched document untouched + CHECK_THROWS_AS(doc.patch(json::parse(R"([{"op":"move","from":"","path":""}])")), json::out_of_range&); + json inplace = doc; + CHECK_THROWS_AS(inplace.patch_inplace(json::parse(R"([{"op":"move","from":"","path":""}])")), json::out_of_range&); + CHECK(inplace == doc); +} diff --git a/tests/src/unit-json_view.cpp b/tests/src/unit-json_view.cpp new file mode 100644 index 000000000..e87162d98 --- /dev/null +++ b/tests/src/unit-json_view.cpp @@ -0,0 +1,1595 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; +using nlohmann::ordered_json; +using nlohmann::json_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; +using json_view_test::has_duplicate_keys; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef JSON_HAS_CPP_17 + #include +#endif + +namespace +{ +// the value of a text, through a named document: the views of a temporary +// document would dangle (root() of an rvalue document does not compile) +template +auto materialized(Args&& ... args) -> decltype(std::declval().materialize()) +{ + const Document d = Document::parse(std::forward(args)...); + return d.root().materialize(); +} + +template +auto materialized_copy(Input&& input) -> decltype(std::declval().materialize()) +{ + const Document d = Document::parse_copy(std::forward(input)); + return d.root().materialize(); +} + +// a "byte container" that claims to hold `size` bytes, to reach the limit on +// the size of the input without allocating gigabytes; nothing past the first +// bytes is ever read, because the size is checked before the parse starts +struct oversized_input +{ + using value_type = char; + std::size_t claimed; + + const char* data() const // NOLINT(readability-convert-member-functions-to-static): container interface + { + return "[1]"; + } + + std::size_t size() const + { + return claimed; + } +}; + +// detection of calls that must not compile +template +using parse_call_t = decltype(json_document::parse(std::declval()...)); +template +using parse_copy_call_t = decltype(json_document::parse_copy(std::declval()...)); +template +using accept_call_t = decltype(json_document::accept(std::declval()...)); +template +using read_call_t = decltype(std::declval().read(std::declval()...)); +template +using root_call_t = decltype(std::declval().root()); +template +using bool_conversion_t = decltype(static_cast(std::declval())); + +#if !defined(JSON_NOEXCEPTION) +// 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) +{ + try + { + const json j = json::parse(text, nullptr, true, comments, trailing_commas); + static_cast(j); + } + catch (const json::exception& e) + { + return e.what(); + } + return ""; +} + +std::string view_exception(const std::string& text, bool comments = false, bool trailing_commas = false) +{ + try + { + const json_document d = json_document::parse(text, true, comments, trailing_commas); + static_cast(d); + } + catch (const json::exception& e) + { + return e.what(); + } + return ""; +} +#endif +} // namespace + +TEST_CASE("json_view") +{ + SECTION("types and capacity") + { + for (const char* text : + {"null", "true", "false", "0", "-1", "18446744073709551615", "-9223372036854775808", "18446744073709551616", "1.5", + "\"\"", "\"text\"", "[]", "[1,2,3]", "{}", "{\"a\":1,\"b\":2}" // NOLINT(modernize-raw-string-literal) + }) + { + CAPTURE(text) + const json j = json::parse(text); + const json_document d = json_document::parse(text); + const json_view v = d.root(); + CHECK(v.type() == j.type()); + CHECK(v.is_null() == j.is_null()); + CHECK(v.is_boolean() == j.is_boolean()); + CHECK(v.is_number() == j.is_number()); + CHECK(v.is_number_integer() == j.is_number_integer()); + CHECK(v.is_number_unsigned() == j.is_number_unsigned()); + CHECK(v.is_number_float() == j.is_number_float()); + CHECK(v.is_string() == j.is_string()); + CHECK(v.is_array() == j.is_array()); + CHECK(v.is_object() == j.is_object()); + CHECK(v.is_binary() == j.is_binary()); + CHECK(v.is_primitive() == j.is_primitive()); + CHECK(v.is_structured() == j.is_structured()); + CHECK(!v.is_discarded()); + CHECK(v.size() == j.size()); + CHECK(v.empty() == j.empty()); + CHECK(v.materialize() == j); + } + + const json_view invalid{}; + CHECK(invalid.is_discarded()); + CHECK(invalid.type() == json::value_t::discarded); + CHECK(invalid.size() == 0); + CHECK(invalid.empty()); + CHECK(invalid.materialize().is_discarded()); + CHECK(invalid.source_offset() == static_cast(-1)); + } + + SECTION("materialize") + { + generator g; + for (int i = 0; i < 2000; ++i) + { + std::string text; + g.value(text, 0); + CAPTURE(text) + CHECK(materialized(text) == json::parse(text)); + // member order as ordered_json::parse keeps it + CHECK(materialized(text).dump() == ordered_json::parse(text).dump()); + } + // duplicate keys: the last value, at the position of the first key + CHECK(materialized(R"({"a":1,"b":2,"a":3})") == json::parse(R"({"a":1,"b":2,"a":3})")); + CHECK(materialized(R"({"a":1,"b":2,"a":3})").dump() == R"({"a":3,"b":2})"); + // scalars (made without the handler), and nesting around the depth up to which the stack of open containers is local + for (const char* scalar : + {"null", "true", "false", "0", "-7", "18446744073709551615", "1.5e300", "-0.0", "\"\"", "\"short\"", "\"a string that is longer than fifteen bytes\"", R"("\u00e4\n")" + }) + { + CAPTURE(scalar) + CHECK(materialized(scalar) == json::parse(scalar)); + const std::string in_array = std::string("[1,") + scalar + "]"; + const json_document d = json_document::parse(in_array); + CHECK(d.root()[1].materialize() == json::parse(scalar)); + CHECK(d.root()[1].get() == json::parse(scalar)); + } + for (const std::size_t levels : + { + std::size_t{1}, std::size_t{15}, std::size_t{16}, std::size_t{17}, std::size_t{32}, std::size_t{33} + }) + { + CAPTURE(levels) + const std::string nested = std::string(levels, '[') + R"({"k":[1,{"j":2}]})" + std::string(levels, ']'); + CHECK(materialized(nested) == json::parse(nested)); + std::string wide = R"({"a":)"; + wide += nested; + wide += R"(,"b":)"; + wide += nested; + wide += "}"; + CHECK(materialized(wide) == json::parse(wide)); + } + // very deep nesting (iterative, as parse()) + const std::string deep = std::string(100000, '[') + std::string(100000, ']'); + // (one check per step, so that a failure shows which step it is) + const json_document deep_document = json_document::parse(deep); + CHECK(deep_document.root().is_array()); + const json deep_value = deep_document.root().materialize(); + CHECK(deep_value.is_array()); + const json deep_parsed = json::parse(deep); + CHECK(deep_parsed.is_array()); + CHECK(deep_value == deep_parsed); +#if JSON_DIAGNOSTICS + // the parents are set, so errors name the path + const json m = materialized(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&); +#endif + } + + SECTION("parse errors are those of parse()") + { + for (const char* text : + { + "", " ", "[", "]", "{", "[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) + "1e400", "-1e400", "[1.7976931348623159e308]", "{\"a\":\n{\"b\": [1, 2,\n 3 x]}}" + }) + { + CAPTURE(text) +#if !defined(JSON_NOEXCEPTION) + const std::string expected = parse_exception(text); + REQUIRE(!expected.empty()); + CHECK(view_exception(text) == expected); +#endif + CHECK(!json_document::accept(text)); + const json_document d = json_document::parse(text, false); + CHECK(d.is_discarded()); + CHECK(d.root().is_discarded()); + CHECK(d.node_count() == 0); + } + // the exception types + json_document _; + CHECK_THROWS_AS(_ = json_document::parse("[1,"), json::parse_error&); + CHECK_THROWS_AS(_ = json_document::parse("1e400"), json::out_of_range&); + } + + SECTION("parse options") + { + for (const char* text : + {"// c\n[1]", "[1, /* c */ 2]", "[1,]", "{\"a\":1,}", "[1,/* c */]", "/", "/* ", "[1,,]" + }) + { + CAPTURE(text) + for (int options = 0; options < 4; ++options) + { + const bool comments = (options & 1) != 0; + const bool trailing_commas = (options & 2) != 0; + CHECK(json_document::accept(text, comments, trailing_commas) == json::accept(text, comments, trailing_commas)); +#if !defined(JSON_NOEXCEPTION) + CHECK(view_exception(text, comments, trailing_commas) == parse_exception(text, comments, trailing_commas)); +#endif + } + } + } + + SECTION("overflow of the floating-point type") + { + // the overflow is that of the document's number_float_t: 1e39 + // overflows a float, the largest float 3.4028235e38 does not, but + // 3.4028236e38 rounds beyond it; a double document is not affected + using json_float = nlohmann::basic_json; + using float_document = nlohmann::basic_json_document; + CHECK_FALSE(json_float::accept("1e39")); + CHECK(json_float::accept("3.4028235e38")); + CHECK_FALSE(json_float::accept("3.4028236e38")); + for (const char* text : + { + "1e39", "-1e39", "[3.4028235e38]", "[3.4028236e38]", "{\"a\": [1e38, -3.4028235e38]}", "{\"a\": [1e-50, 1e39]}" + }) + { + CAPTURE(text) + const bool accepted = json_float::accept(text); + CHECK(float_document::accept(text) == accepted); + CHECK(float_document::parse(text, false).is_discarded() == !accepted); + CHECK(json_document::accept(text)); + if (accepted) + { + CHECK(materialized(text) == json_float::parse(text)); + } + } + float_document f; + CHECK_THROWS_WITH_AS(f = float_document::parse("1e39"), "[json.exception.out_of_range.406] number overflow parsing '1e39'", json::out_of_range&); + CHECK_THROWS_WITH_AS(f = float_document::parse("[3.4028236e38]"), "[json.exception.out_of_range.406] number overflow parsing '3.4028236e38'", json::out_of_range&); + } + + SECTION("NUL and BOM") + { + const std::string with_nul("[1]\0garbage", 11); + CHECK(json_document::accept(with_nul) == json::accept(with_nul)); + const std::string nul_in_comment("[1, // c\0\n2]", 12); + CHECK(json_document::accept(nul_in_comment, true) == json::accept(nul_in_comment, true)); + CHECK(materialized("\xEF\xBB\xBF[1]") == json::parse("\xEF\xBB\xBF[1]")); +#if !defined(JSON_NOEXCEPTION) + CHECK(view_exception("\xEF\xBB") == parse_exception("\xEF\xBB")); +#endif + } + + SECTION("inputs") + { + const std::string text = R"([1, "two", {"three": 3.5}])"; + const json expected = json::parse(text); + + // borrowed: the text must outlive the document + const json_document borrowed = json_document::parse(text); + CHECK(!borrowed.owns_source()); + CHECK(borrowed.source().data() == text.data()); + CHECK(borrowed.root().materialize() == expected); + CHECK(materialized(text.c_str()) == expected); + CHECK(materialized(R"([1, "two", {"three": 3.5}])") == expected); + CHECK(materialized(text.data(), text.data() + text.size()) == expected); + const std::vector chars(text.begin(), text.end()); + CHECK(!json_document::parse(chars).owns_source()); + CHECK(materialized(chars) == expected); + const std::vector bytes(text.begin(), text.end()); + CHECK(materialized(bytes) == expected); +#ifdef JSON_HAS_CPP_17 + const std::string_view sv = text; + CHECK(!json_document::parse(sv).owns_source()); + CHECK(materialized(sv) == expected); +#endif + + // owned + std::string moved = text; + const json_document from_rvalue = json_document::parse(std::move(moved)); + CHECK(from_rvalue.owns_source()); + CHECK(from_rvalue.root().materialize() == expected); + CHECK(json_document::parse(std::vector(text.begin(), text.end())).owns_source()); + // a const rvalue cannot be moved from, and is not borrowed (it may be a + // temporary): it is copied, as is a const rvalue of any container + const std::string const_text = text; // NOLINT(performance-unnecessary-copy-initialization) + const json_document from_const_rvalue = json_document::parse(std::move(const_text)); // NOLINT(performance-move-const-arg,hicpp-move-const-arg) + CHECK(from_const_rvalue.owns_source()); + CHECK(from_const_rvalue.source().data() != const_text.data()); // NOLINT(bugprone-use-after-move,hicpp-invalid-access-moved): const, not moved from + CHECK(from_const_rvalue.root().materialize() == expected); + json_document read_const_rvalue; + read_const_rvalue.read(std::move(const_text)); // NOLINT(performance-move-const-arg,hicpp-move-const-arg,bugprone-use-after-move,hicpp-invalid-access-moved) + CHECK(read_const_rvalue.owns_source()); + CHECK(read_const_rvalue.root().materialize() == expected); + const std::vector const_chars(text.begin(), text.end()); + CHECK(json_document::parse(std::move(const_chars)).owns_source()); // NOLINT(performance-move-const-arg,hicpp-move-const-arg) + CHECK(json_document::parse_copy(text).owns_source()); + CHECK(materialized_copy(text) == expected); + std::istringstream stream(text); + const json_document from_stream = json_document::parse(stream); + CHECK(from_stream.owns_source()); + CHECK(from_stream.root().materialize() == expected); + const std::list list(text.begin(), text.end()); + CHECK(json_document::parse(list.begin(), list.end()).owns_source()); + CHECK(materialized(list.begin(), list.end()) == expected); + + // iterator pairs: pointers are borrowed, and so are contiguous library + // iterators where the input adapter detects them (C++20) + CHECK(!json_document::parse(text.data(), text.data() + text.size()).owns_source()); + const bool contiguous = nlohmann::detail::iterator_input_adapter::const_iterator>::supports_bulk_scan; + const json_document from_iterators = json_document::parse(chars.cbegin(), chars.cend()); + CHECK(from_iterators.owns_source() != contiguous); + CHECK((from_iterators.source().data() == chars.data()) == contiguous); + CHECK(from_iterators.root().materialize() == expected); + const std::string padded = "x" + text + "x"; + CHECK(materialized(padded.begin() + 1, padded.end() - 1) == expected); + CHECK(json_document::parse(chars.cbegin(), chars.cbegin(), false).is_discarded()); + const std::wstring wide = L"[\"\u00e4\u20ac\", 1]"; + CHECK(materialized(wide) == json::parse(wide)); + CHECK(json_document::parse(static_cast(nullptr), false).is_discarded()); + CHECK(json_document::parse("", false).is_discarded()); + + // wide-character arrays and pointers are converted to UTF-8, as by + // parse(); the pointers are NUL-terminated + CHECK(materialized(L"[\"ä€\", 1]") == json::parse(L"[\"ä€\", 1]")); + CHECK(materialized(u"[\"ä€\", 1]") == json::parse(u"[\"ä€\", 1]")); + CHECK(materialized(U"[\"ä€\", 1]") == json::parse(U"[\"ä€\", 1]")); + const wchar_t* const wide_pointer = wide.c_str(); + const json_document from_wide_pointer = json_document::parse(wide_pointer); + CHECK(from_wide_pointer.owns_source()); + CHECK(from_wide_pointer.root().materialize() == json::parse(wide)); + const std::u16string wide16 = u"{\"\U0001F600\": [true]}"; + CHECK(materialized(wide16.c_str()) == json::parse(wide16)); + const std::u32string wide32 = U"{\"\U0001F600\": [true]}"; + CHECK(materialized(wide32.c_str()) == json::parse(wide32)); + CHECK(json_document::parse(static_cast(nullptr), false).is_discarded()); + + // FILE*: read to its end + std::FILE* file = std::tmpfile(); // NOLINT(cppcoreguidelines-owning-memory) + if (file != nullptr) + { + CHECK(std::fputs(text.c_str(), file) >= 0); + CHECK(std::fseek(file, 0, SEEK_SET) == 0); + const json_document from_file = json_document::parse(file); + CHECK(from_file.owns_source()); + CHECK(from_file.root().materialize() == expected); + CHECK(std::fclose(file) == 0); // NOLINT(cppcoreguidelines-owning-memory) + } + } + + SECTION("streams") + { + // longer than one block of the bulk read + std::string text = "["; + for (int i = 0; i < 5000; ++i) + { + text += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\""; + } + text += ']'; + std::istringstream stream(text); + const json_document from_stream = json_document::parse(stream); + CHECK(from_stream.source() == text); + CHECK(from_stream.root().materialize() == json::parse(text)); + // the stream is read to its end, and eofbit set, as by parse() + CHECK(stream.eof()); + CHECK(!stream.bad()); + + std::istringstream empty_stream; + CHECK(json_document::parse(empty_stream, false).is_discarded()); + CHECK(empty_stream.eof()); + std::istringstream rvalue_source("[1]"); + CHECK(json_document::parse_copy(std::move(rvalue_source)).owns_source()); + + // a C stream is read in blocks as well (more than one block, and a + // text whose length is a multiple of the block size) + for (const std::size_t extra : + { + std::size_t{0}, std::size_t{1} + }) + { + std::FILE* long_file = std::tmpfile(); // NOLINT(cppcoreguidelines-owning-memory) + if (long_file != nullptr) + { + std::string padded = text; + padded.append(((4096 - (padded.size() % 4096)) % 4096) + extra, ' '); + CHECK(std::fwrite(padded.data(), 1, padded.size(), long_file) == padded.size()); + CHECK(std::fseek(long_file, 0, SEEK_SET) == 0); + const json_document from_long_file = json_document::parse(long_file); + CHECK(from_long_file.source() == padded); + CHECK(from_long_file.root().materialize() == json::parse(text)); + CHECK(std::fclose(long_file) == 0); // NOLINT(cppcoreguidelines-owning-memory) + } + } + { + json_document from_null; + CHECK_THROWS_AS(from_null = json_document::parse(static_cast(nullptr)), json::parse_error&); + } + + // a stream without a buffer is an empty input + std::istream no_buffer(nullptr); + CHECK(json_document::parse(no_buffer, false).is_discarded()); + +#if !defined(JSON_NOEXCEPTION) + // an exception while reading the input leaves a discarded document + json_document d = json_document::parse(std::vector(text.begin(), text.end())); + std::istringstream throwing("[1]"); + throwing.exceptions(std::ios::eofbit); + CHECK_THROWS_AS(d.read(throwing), std::ios_base::failure&); + CHECK(d.is_discarded()); + CHECK(d.root().is_discarded()); + d.read("[2]"); + CHECK(d.root().materialize() == json::parse("[2]")); +#endif + } + + SECTION("read() reuses the owned copy") + { + std::string big = "["; + for (int i = 0; i < 1000; ++i) + { + big += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\""; + } + big += ']'; + const std::string small = "{\"a\": [1, 2]}"; + + // copied inputs are written into the existing buffer + json_document d = json_document::parse(std::vector(big.begin(), big.end())); + REQUIRE(d.owns_source()); + const char* const buffer = d.source().data(); + d.read(std::vector(small.begin(), small.end())); + CHECK(d.owns_source()); + CHECK(d.source().data() == buffer); + CHECK(d.root().materialize() == json::parse(small)); + std::istringstream stream(small); + d.read(stream); + CHECK(d.source().data() == buffer); + CHECK(d.root().materialize() == json::parse(small)); + d.read(std::wstring(L"[3]")); + CHECK(d.source().data() == buffer); + CHECK(d.root().materialize() == json::parse("[3]")); + + // a moved-in string replaces it (long enough to be on the heap) + std::string moved = big; + const char* const moved_buffer = moved.data(); + d.read(std::move(moved)); + CHECK(d.source().data() == moved_buffer); + CHECK(d.root().materialize() == json::parse(big)); + + // a borrowed input leaves nothing owned + d.read(big); + CHECK(!d.owns_source()); + CHECK(d.source().data() == big.data()); + } + + SECTION("integer arguments do not compile") + { + using nlohmann::detail::is_detected; + + static_assert(is_detected::value, "parse(ptr, bool) is valid"); + static_assert(is_detected::value, "parse(ptr, bool, bool, bool) is valid"); + static_assert(is_detected::value, "parse(first, last) is valid"); + static_assert(is_detected::value, "parse(first, last, bool) is valid"); + static_assert(is_detected::value, "parse_copy(ptr, bool) is valid"); + static_assert(is_detected::value, "accept(ptr, bool) is valid"); + static_assert(is_detected::value, "read(ptr, bool) is valid"); + + // a length is not a flag: parse(ptr, len) would convert len to + // allow_exceptions and read ptr as a C string, which need not end + // + // MSVC 2015 does not treat selecting a deleted function in decltype as + // a substitution failure, so it detects the deleted overloads as + // callable; calling them still fails to compile +#if !(defined(_MSC_VER) && _MSC_VER < 1910) + static_assert(!is_detected::value, "parse(ptr, len) must not compile"); + static_assert(!is_detected::value, "parse(ptr, int) must not compile"); + static_assert(!is_detected::value, "parse(ptr, char) must not compile"); + static_assert(!is_detected::value, "parse(ptr, len, bool) must not compile"); + static_assert(!is_detected::value, "parse(string, len) must not compile"); + static_assert(!is_detected&, std::size_t>::value, "parse(vector, len) must not compile"); + static_assert(!is_detected::value, "parse_copy(ptr, len) must not compile"); + static_assert(!is_detected::value, "accept(ptr, len) must not compile"); + static_assert(!is_detected::value, "read(ptr, len) must not compile"); +#endif + + // json_view has no conversion to bool: unlike basic_json's, it would + // mean "exists", not "is not null"; use is_discarded() + static_assert(!is_detected::value, "json_view must not convert to bool"); + + // the valid calls still work + const char* const text = "[1]"; + CHECK(materialized(text, true) == json::parse(text)); + CHECK(json_document::accept(text, true, true)); + } + + SECTION("root of a temporary document does not compile") + { + using nlohmann::detail::is_detected; + + // the view would dangle: auto v = json_document::parse(text).root(); + static_assert(is_detected::value, "root() of an lvalue is valid"); + static_assert(is_detected::value, "root() of a const lvalue is valid"); + static_assert(!is_detected::value, "root() of an rvalue must not compile"); + static_assert(!is_detected < root_call_t, json_document && >::value, "root() of an rvalue must not compile"); + static_assert(!is_detected < root_call_t, const json_document && >::value, "root() of a const rvalue must not compile"); + static_assert(!is_detected::value, "root() of an rvalue must not compile"); + + // a named document is fine, also after a move + json_document d = json_document::parse("[1]"); + CHECK(d.root().size() == 1); + const json_document moved = std::move(d); + CHECK(moved.root().size() == 1); + } + + SECTION("input size limit") + { + // 32-bit offsets: the limit is 4 GiB minus 16 bytes (a margin below 2^32), + // which is what the exception message and the documentation say + const std::size_t limit = nlohmann::detail::view::max_input_size(); + CHECK(limit == std::size_t{4294967279u}); + + const oversized_input input{limit + 1}; + CHECK(!json_document::accept(input)); + CHECK(json_document::parse(input, false).is_discarded()); +#if !defined(JSON_NOEXCEPTION) + json_document d; + CHECK_THROWS_WITH_AS(d = json_document::parse(input), "[json.exception.out_of_range.416] input of 4294967280 bytes or more is not supported by json_document", json::out_of_range&); +#endif + } + + SECTION("document lifetime and reuse") + { + json_document d; + CHECK(d.is_discarded()); + CHECK(d.root().is_discarded()); + CHECK(d.node_count() == 0); + CHECK(d.memory_usage() == 0); + CHECK(d.source().empty()); + + const std::string a = "[1,2,3]"; + const std::string b = "{\"x\":[true]}"; + d.read(a); + CHECK(d.node_count() == 4); + CHECK(d.root().materialize() == json::parse(a)); + d.read(b); + CHECK(d.node_count() == 4); + CHECK(d.root().materialize() == json::parse(b)); + d.read("[", false); + CHECK(d.is_discarded()); + + // views stay valid when the document moves + json_document first = json_document::parse(a); + const json_view root = first.root(); + const json_document second = std::move(first); + CHECK(root.materialize() == json::parse(a)); + CHECK(second.root().materialize() == json::parse(a)); + } + + SECTION("memory") + { + std::string big = "["; + for (int i = 0; i < 10000; ++i) + { + big += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\""; + } + big += ']'; + json_document d = json_document::parse(big); + CHECK(d.node_count() == 10001); + const std::size_t before = d.memory_usage(); + d.shrink_to_fit(); // (invalidates views, like std::vector::shrink_to_fit) + CHECK(d.memory_usage() <= before); + CHECK(d.node_count() == 10001); + CHECK(d.root().materialize() == json::parse(big)); + CHECK(d.root().size() == 10000); + d.shrink_to_fit(); // nothing left to release + + // after reading a smaller text, both the index and the decoded strings + // shrink, and the strings are found in their new place + std::string escaped = "["; + for (int i = 0; i < 1000; ++i) + { + escaped += (i != 0 ? ",\"a\\n" : "\"a\\n") + std::to_string(i) + "\""; + } + escaped += ']'; + json_document reused = json_document::parse(escaped); + const std::string smaller = "[\"x\\ty\", [true, \"\\u00e4\"]]"; // NOLINT(modernize-raw-string-literal) + reused.read(smaller); + const std::size_t grown = reused.memory_usage(); + reused.shrink_to_fit(); + CHECK(reused.memory_usage() < grown); + CHECK(reused.root().materialize() == json::parse(smaller)); + + // an empty document has nothing to release + json_document empty; + empty.shrink_to_fit(); + CHECK(empty.memory_usage() == 0); + + // a small document stays in the storage block of the header + json_document small = json_document::parse("[1,[2,3],{\"a\":\"b\\n\"}]"); // NOLINT(modernize-raw-string-literal) + small.shrink_to_fit(); + CHECK(small.root().materialize() == json::parse("[1,[2,3],{\"a\":\"b\\n\"}]")); + } + + SECTION("source offsets") + { + const std::string text = R"( {"key": "value", "escaped": "a\nb", "n": 42})"; + const json_document d = json_document::parse(text); + CHECK(d.root().source_offset() == 2); + CHECK(d.root()["key"].source_offset() == text.find("value")); + CHECK(d.root()["escaped"].source_offset() == static_cast(-1)); + CHECK(d.root()["n"].source_offset() == text.find("42")); + } +} + +namespace +{ +#if !defined(JSON_NOEXCEPTION) +// the exception a call throws, or "" if it throws none +template +std::string exception_of(F f) +{ + try + { + f(); + } + catch (const json::exception& e) + { + return e.what(); + } + return ""; +} +#endif + +// compares a view with the ordered_json value materialize() gives for it: +// types, sizes, elements and members (by index, key, and iteration), in +// document order; duplicate keys are found as their first occurrence +void check_access(const ordered_json_view& v, const ordered_json& j) +{ + REQUIRE(v.type() == j.type()); + CHECK(std::string(v.type_name()) == j.type_name()); + if (v.is_array()) + { + REQUIRE(v.size() == j.size()); + std::size_t i = 0; + for (const ordered_json_view e : v) + { + CHECK(v[i].materialize() == e.materialize()); + CHECK(v.at(i).materialize() == e.materialize()); + check_access(e, j[i]); + ++i; + } + CHECK(i == v.size()); + CHECK(v[v.size()].is_discarded()); + std::size_t index = 0; + for (const auto& item : v.items()) + { + CHECK(item.key() == std::to_string(index)); + CHECK(item.value().materialize() == j[index]); + ++index; + } + if (!v.empty()) + { + CHECK(v.front().materialize() == j.front()); + CHECK(v.back().materialize() == j.back()); + } + } + else if (v.is_object()) + { + std::vector keys; // first occurrences, in order + std::size_t members = 0; + for (auto it = v.begin(); it != v.end(); ++it) + { + ++members; + const std::string key(it.key().data(), it.key().size()); + CHECK(v.contains(key)); + std::size_t occurrences = 0; + for (auto other = v.begin(); other != v.end(); ++other) + { + occurrences += other.key() == it.key() ? 1u : 0u; + } + CHECK(v.count(key) == occurrences); + // lookups find the first member with the key, which is this one if + // there is no earlier one + if (std::find(keys.begin(), keys.end(), key) != keys.end()) + { + continue; + } + keys.push_back(key); + CHECK(v.find(key) == it); + CHECK(v[key].materialize() == it->materialize()); + CHECK(v.at(key).materialize() == it.value().materialize()); + CHECK(v[key.c_str()].materialize() == (*it).materialize()); + } + CHECK(members == v.size()); + REQUIRE(keys.size() == j.size()); + std::size_t k = 0; + for (const auto& member : j.items()) + { + CHECK(keys[k++] == member.key()); + } + if (keys.size() == members) + { + // no duplicates: the values are those of the object + for (const auto& key : keys) + { + check_access(v[key], j[key]); + } + if (!v.empty()) + { + CHECK(v.front().materialize() == j.front()); + CHECK(v.back().materialize() == j.back()); + } + } + CHECK(v["not a key in the generated documents"].is_discarded()); + CHECK(v.find("not a key in the generated documents") == v.end()); + } + else + { + // a primitive is a range of one element; null is empty + CHECK(static_cast(std::distance(v.begin(), v.end())) == (v.is_null() ? 0u : 1u)); + if (!v.is_null()) + { + CHECK((*v.begin()).materialize() == j); + CHECK(v.front().materialize() == j); + CHECK(v.back().materialize() == j); + } + } +} +} // namespace + +TEST_CASE("json_view element access and iteration") +{ + SECTION("generated documents") + { + generator g; + for (int i = 0; i < 2000; ++i) + { + std::string text; + g.value(text, 0); + CAPTURE(text) + const ordered_json_document d = ordered_json_document::parse(text); + check_access(d.root(), ordered_json::parse(text)); + } + } + + SECTION("keys") + { + // keys of every length around the 2/4/8/16-byte loads, with escapes + std::string text = "{"; + std::vector keys = {"", "x"}; + for (std::size_t n = 1; n <= 40; ++n) + { + keys.emplace_back(n, 'k'); + keys.push_back(std::string(n, 'k') + "x"); + keys.push_back("x" + std::string(n, 'k')); + } + for (std::size_t i = 0; i < keys.size(); ++i) + { + text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i); + } + text += ",\"esc\\u0061ped\":\"escaped key\"}"; // NOLINT(modernize-raw-string-literal) + const json_document d = json_document::parse(text); + const json_view root = d.root(); + for (std::size_t i = 0; i < keys.size(); ++i) + { + CAPTURE(keys[i]) + CHECK(root[keys[i]].materialize() == i); + CHECK(root.at(keys[i]).materialize() == i); + CHECK(root.find(keys[i]).key() == keys[i]); + CHECK(!root.contains(keys[i] + "y")); + } + CHECK(root["escaped"].materialize() == "escaped key"); + CHECK(!root.contains("esc\\u0061ped")); +#ifdef JSON_HAS_CPP_17 + CHECK(root[std::string_view("kkk")].materialize() == root["kkk"].materialize()); +#endif + } + + SECTION("duplicate keys: lookups find the first member, iteration all") + { + const json_document d = json_document::parse(R"({"a":1,"b":2,"a":3})"); + const json_view v = d.root(); + CHECK(v.size() == 3); + CHECK(v["a"].materialize() == 1); + CHECK(v.at("a").materialize() == 1); + CHECK(v.find("a") == v.begin()); + CHECK(v.find("a").value().materialize() == 1); + CHECK(v.find("b") == std::next(v.begin())); + CHECK(v.count("a") == 2); // every member, unlike materialize() + CHECK(v.count("b") == 1); + CHECK(v.contains("a")); + CHECK(v.value("a", 0) == 1); + CHECK(v.materialize()["a"] == 3); // materialize() keeps the last value, unlike the lookups + // JSON pointers resolve to the first member at every level + const json_document dp = json_document::parse(R"({"a":{"b":1},"a":{"b":2,"b":3}})"); + CHECK(dp.root()[json::json_pointer("/a/b")].materialize() == 1); + CHECK(dp.root().at(json::json_pointer("/a/b")).materialize() == 1); + CHECK(dp.root().value(json::json_pointer("/a/b"), 0) == 1); + CHECK(dp.root().materialize()[json::json_pointer("/a/b")] == 3); + // get keeps the last value, as materialize() + CHECK((v.get>() == std::map {{"a", 3}, {"b", 2}})); + // keys of every length class (the 16-byte short compare and memcmp) + for (const std::size_t n : + { + 0u, 1u, 3u, 7u, 8u, 15u, 16u, 17u, 40u + }) + { + const std::string key(n, 'k'); + const json_document dk = json_document::parse("{\"" + key + "\":1,\"" + key + "x\":2,\"" + key + "\":3,\"" + key + "\":4}"); // NOLINT(performance-inefficient-string-concatenation) + CAPTURE(n) + CHECK(dk.root()[key].materialize() == 1); + CHECK(dk.root().at(key).materialize() == 1); + CHECK(dk.root().find(key) == dk.root().begin()); + CHECK(dk.root().value(key, 0) == 1); + CHECK(dk.root()[key + "x"].materialize() == 2); + } + std::string order; + for (auto it = v.begin(); it != v.end(); ++it) + { + order += std::string(it.key().data(), it.key().size()) + it->materialize().dump(); + } + CHECK(order == "a1b2a3"); + CHECK(v.back().materialize() == 3); + CHECK(v.materialize() == json::parse(R"({"a":1,"b":2,"a":3})")); // the last value, as parse() + } + + SECTION("errors are those of const basic_json") + { + for (const char* text : + {"null", "true", "42", "-1", "1.5", "\"s\"", "[]", "[1,2]", "{}", "{\"a\":1}" + }) + { + CAPTURE(text) + const json_document d = json_document::parse(text); + const json_view v = d.root(); + const json j = v.materialize(); +#if !defined(JSON_NOEXCEPTION) + if (!j.is_object()) + { + CHECK(exception_of([&] { static_cast(v["a"]); }) == exception_of([&] { static_cast(j["a"]); })); + } + if (!j.is_array()) + { + CHECK(exception_of([&] { static_cast(v[0]); }) == exception_of([&] { static_cast(j[0]); })); + } + CHECK(exception_of([&] { static_cast(v.at("a")); }) == exception_of([&] { static_cast(j.at("a")); })); + CHECK(exception_of([&] { static_cast(v.at("missing")); }) == exception_of([&] { static_cast(j.at("missing")); })); + CHECK(exception_of([&] { static_cast(v.at(0)); }) == exception_of([&] { static_cast(j.at(0)); })); + CHECK(exception_of([&] { static_cast(v.at(5)); }) == exception_of([&] { static_cast(j.at(5)); })); + if (!(j.is_object() && j.empty())) // (key() of an end iterator) + { + CHECK(exception_of([&] { static_cast(v.begin().key()); }) == exception_of([&] { static_cast(j.begin().key()); })); + } + if (!j.empty() || j.is_null()) + { + CHECK(exception_of([&] { static_cast(v.front()); }) == exception_of([&] { static_cast(j.front()); })); + CHECK(exception_of([&] { static_cast(v.back()); }) == exception_of([&] { static_cast(j.back()); })); + } +#endif + CHECK(v.contains("a") == j.contains("a")); + CHECK(v.count("a") == j.count("a")); + CHECK((v.find("a") == v.end()) == (j.find("a") == j.end())); // NOLINT(readability-container-contains): find() is what is tested + } + + // where basic_json has undefined behavior, the view answers safely + const json_document d = json_document::parse(R"({"a":[]})"); + CHECK(d.root()["b"].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"].back(), "[json.exception.invalid_iterator.214] cannot get value", json::invalid_iterator&); + const json_view invalid{}; + CHECK(invalid.begin() == invalid.end()); + CHECK(std::string(invalid.type_name()) == "discarded"); + } + + SECTION("chained access is safe: operator[] of a discarded view is discarded") + { + const json_document d = json_document::parse(R"({"a":{"b":[10,20]},"s":"str"})"); + const json_view v = d.root(); + // missing keys and indexes + CHECK(v["x"].is_discarded()); + CHECK(v["x"]["y"].is_discarded()); + CHECK(v["x"]["y"]["z"].is_discarded()); + CHECK(v["x"][0].is_discarded()); + CHECK(v["x"][0u][1L].is_discarded()); + CHECK(v["a"]["b"][2].is_discarded()); + CHECK(v["a"]["b"][2]["c"].is_discarded()); + CHECK(v["a"]["b"][2][json_view::json_pointer("/c")].is_discarded()); + CHECK(v["x"][json_view::json_pointer("/a/b")].is_discarded()); + CHECK(v["x"][json_view::json_pointer("")].is_discarded()); + CHECK(v["x"]["y"].is_discarded()); + CHECK(v["x"][std::string("y")].is_discarded()); + // a resolvable path still resolves + CHECK(v["a"]["b"][1].materialize() == 20); + CHECK(v[json_view::json_pointer("/a/b/1")].materialize() == 20); + // the discarded view of an unresolved pointer is discarded too + CHECK(v[json_view::json_pointer("/x/y")]["z"].is_discarded()); + CHECK(v[json_view::json_pointer("/a/b/5")][0].is_discarded()); +#if !defined(JSON_NOEXCEPTION) + // 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["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["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"][0], "[json.exception.type_error.305] cannot use operator[] with a numeric argument with string", 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&); + // at() keeps throwing on a discarded view + const json_view invalid{}; + CHECK(invalid["a"].is_discarded()); + CHECK(invalid[0].is_discarded()); + CHECK(invalid[json_view::json_pointer("/a")].is_discarded()); + CHECK_THROWS_WITH_AS(invalid.at("a"), "[json.exception.type_error.304] cannot use at() with discarded", json::type_error&); + CHECK_THROWS_WITH_AS(invalid.at(0), "[json.exception.type_error.304] cannot use at() with discarded", json::type_error&); + CHECK_THROWS_AS(v.at("x").at("y"), json::out_of_range&); + CHECK_THROWS_AS(v["x"].at("y"), json::type_error&); + CHECK_THROWS_AS(v.at(json_view::json_pointer("/x/y")), json::out_of_range&); + CHECK_THROWS_AS(v["x"].at(json_view::json_pointer("/y")), json::out_of_range&); +#endif + } + + SECTION("integer types as array indexes") + { + const json_document d = json_document::parse("[10,20,30]"); + const json_view v = d.root(); + const json j = v.materialize(); + // (a variable: a cast of a constant to int32_t is a useless cast to GCC) + const std::int32_t int32_index = 1; + // (compile-time: no overload is ambiguous) + CHECK(v[0].materialize() == 10); + CHECK(v[1].materialize() == 20); + CHECK(v[0u].materialize() == 10); + CHECK(v[1u].materialize() == 20); + CHECK(v[1L].materialize() == 20); + CHECK(v[2UL].materialize() == 30); + CHECK(v[1LL].materialize() == 20); + CHECK(v[2ULL].materialize() == 30); + CHECK(v[static_cast(1)].materialize() == 20); + CHECK(v[static_cast(2)].materialize() == 30); + CHECK(v[static_cast(1)].materialize() == 20); + CHECK(v[static_cast(2)].materialize() == 30); + CHECK(v[std::int8_t(1)].materialize() == 20); + CHECK(v[std::int16_t(2)].materialize() == 30); + CHECK(v[int32_index].materialize() == 20); + CHECK(v[std::int64_t(2)].materialize() == 30); + CHECK(v[std::uint32_t(0)].materialize() == 10); + CHECK(v[std::uint64_t(1)].materialize() == 20); + CHECK(v[std::size_t(2)].materialize() == 30); + CHECK(v[std::ptrdiff_t(1)].materialize() == 20); + CHECK(j[0u] == 10); // as basic_json + + CHECK(v.at(0).materialize() == 10); + CHECK(v.at(1u).materialize() == 20); + CHECK(v.at(1L).materialize() == 20); + CHECK(v.at(2LL).materialize() == 30); + CHECK(v.at(2ULL).materialize() == 30); + CHECK(v.at(static_cast(1)).materialize() == 20); + CHECK(v.at(static_cast(2)).materialize() == 30); + CHECK(v.at(int32_index - 1).materialize() == 10); + CHECK(v.at(std::uint32_t(0)).materialize() == 10); + CHECK(v.at(std::int64_t(0)).materialize() == 10); + CHECK(v.at(std::uint64_t(1)).materialize() == 20); + CHECK(v.at(std::size_t(2)).materialize() == 30); + CHECK(j.at(std::uint32_t(0)) == 10); // as basic_json + + // out of range, including negative values (no wrap-around) + CHECK(v[3].is_discarded()); + CHECK(v[3u].is_discarded()); + CHECK(v[3L].is_discarded()); + CHECK(v[-1].is_discarded()); + CHECK(v[-1L].is_discarded()); + CHECK(v[-1LL].is_discarded()); + CHECK(v[static_cast(-1)].is_discarded()); + CHECK(v[std::int64_t(-3)].is_discarded()); + CHECK(v[(std::numeric_limits::min)()].is_discarded()); + CHECK(v[(std::numeric_limits::max)()].is_discarded()); + CHECK(v[(std::numeric_limits::max)()].is_discarded()); + CHECK(v[std::numeric_limits::max()].is_discarded()); +#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(3u), "[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.exception.out_of_range.401] array index 3 is out of range", 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(std::int64_t(-1)), json::out_of_range&); + CHECK_THROWS_AS(v.at((std::numeric_limits::min)()), json::out_of_range&); + CHECK_THROWS_AS(v.at((std::numeric_limits::max)()), json::out_of_range&); + // not an array + const json_document o = json_document::parse("{}"); + CHECK_THROWS_AS(o.root()[0u], json::type_error&); + CHECK_THROWS_AS(o.root()[1L], json::type_error&); + CHECK_THROWS_AS(o.root().at(std::uint32_t(0)), json::type_error&); + CHECK_THROWS_AS(o.root().at(std::int64_t(0)), json::type_error&); +#endif + } + + SECTION("arrays whose elements have no subtrees: index and back()") + { + // (the elements are adjacent in the index; an array, an object, or an + // empty container anywhere makes the lookup skip element by element) + const std::vector texts = + { + "[]", "[1]", "[1,2,3]", "[1,\"two\",3.5,true,null,-7,18446744073709551615]", "[[]]", "[{}]", "[1,[2],3]", + "[1,2,{\"a\":3}]", "[[1,2],3,4]", "[1,2,3,[4,5,6,7,8],9,10,11,12]", "[{\"a\":[1]},2]", R"(["a\u00e4","b"])" + }; + for (const std::string& text : texts) + { + CAPTURE(text) + const json_document d = json_document::parse(text); + const json_view v = d.root(); + const json expected = json::parse(text); + for (std::size_t i = 0; i < expected.size(); ++i) + { + CHECK(v[i].materialize() == expected[i]); + CHECK(v.at(i).materialize() == expected[i]); + } + CHECK(v[expected.size()].is_discarded()); + if (!expected.empty()) + { + CHECK(v.back().materialize() == expected.back()); + } + } + // a long array (the direct lookup is not limited to short ones), also with a container at its end + std::string text = "[0"; + for (int i = 1; i < 1000; ++i) + { + text += "," + std::to_string(i); + } + const json_document scalars = json_document::parse(text + "]"); + CHECK(scalars.root()[999].get() == 999); + CHECK(scalars.root()[500].get() == 500); + CHECK(scalars.root().at(1).get() == 1); + CHECK(scalars.root().back().get() == 999); + const json_document container_last = json_document::parse(text + ",[1000]]"); + CHECK(container_last.root()[999].get() == 999); + CHECK(container_last.root()[1000][0].get() == 1000); + CHECK(container_last.root().back()[0].get() == 1000); + } + + SECTION("iterators") + { + const json_document d = json_document::parse(R"({"x":[1,{"y":2}],"z":null})"); + const json_view v = d.root(); + json_view::iterator it = v.begin(); + CHECK(it.is_object_iterator()); + CHECK(it->is_array()); + CHECK(it->size() == 2); + const json_view::iterator previous = it++; + CHECK(previous.key() == "x"); + CHECK(it.key() == "z"); + CHECK(it.value().is_null()); + CHECK(++it == v.end()); + CHECK(v.cbegin() == v.begin()); + CHECK(v.cend() == v.end()); + CHECK(!v["x"].begin().is_object_iterator()); + CHECK(json_view::iterator() == json_view::iterator()); + // standard algorithms + CHECK(std::count_if(v["x"].begin(), v["x"].end(), [](const json_view & e) + { + return e.is_object(); + }) == 1); + } + + SECTION("items") + { + const json_document d = json_document::parse(R"({"a":1,"b":[true,false]})"); + std::string keys; + for (const auto& item : d.root().items()) + { + keys += std::string(item.key().data(), item.key().size()); + CHECK(item.value().materialize() == d.root()[item.key()].materialize()); + } + CHECK(keys == "ab"); + auto items = d.root()["b"].items(); + auto first = items.begin(); + CHECK((*first++).key() == "0"); + CHECK((*first).key() == "1"); + CHECK(++first == items.end()); + + // the keys of array elements are their indexes, whether or not they are asked for + std::string numbers = "[0"; + for (int i = 1; i < 1234; ++i) + { + numbers += "," + std::to_string(i); + } + const json_document array = json_document::parse(numbers + "]"); + std::size_t position = 0; + for (const auto& item : array.root().items()) + { + CHECK(item.value().get() == position); + if (position % 3 != 0) // (not for every item: values alone do not make the key) + { + CHECK(item.key() == std::to_string(position)); + CHECK(item.key().data() == item.key().data()); // (the same text every time) + const json_view::item copy = item; + CHECK(copy.key() == std::to_string(position)); + } + ++position; + } + CHECK(position == 1234); +#ifdef JSON_HAS_CPP_17 + std::string pairs; + for (const auto [key, value] : d.root().items()) + { + pairs += std::string(key) + "=" + value.materialize().dump() + ";"; + } + CHECK(pairs == "a=1;b=[true,false];"); + static_assert(std::tuple_size::value == 2, "tuple_size of an item is 2"); + static_assert(std::is_same::type, json_view>::value, "the second element of an item is a view"); +#endif + } +} + +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 +std::uint64_t bits(double x) +{ + std::uint64_t r = 0; + std::memcpy(&r, &x, sizeof(r)); + return r; +} + +std::uint32_t bits(float x) +{ + std::uint32_t r = 0; + std::memcpy(&r, &x, sizeof(r)); + return r; +} + +// compares the conversions of a view with those of ordered_json +void check_values(const ordered_json_view& v, const ordered_json& j, const std::string& text) +{ + CHECK(v.get() == j); + switch (j.type()) + { + case json::value_t::number_integer: + case json::value_t::number_unsigned: + case json::value_t::number_float: + { + // (converting a float out of range of the target type is undefined) + if (j.is_number_unsigned()) + { + CHECK(v.get() == j.get()); + } + else if (j.is_number_integer()) + { + CHECK(v.get() == j.get()); + } + CHECK(bits(v.get()) == bits(j.get())); + if (std::abs(j.get()) < 1e9) + { + CHECK(v.get() == j.get()); + } + const auto token = v.number_token(); + CHECK(text.compare(v.source_offset(), token.size(), token.data(), token.size()) == 0); + break; + } + case json::value_t::string: + CHECK(v.get() == j.get()); + CHECK(std::string(v.get_string().data(), v.get_string().size()) == j.get()); + break; + case json::value_t::boolean: + CHECK(v.get() == j.get()); + CHECK(v.get() == j.get()); + break; + case json::value_t::null: + CHECK(v.get() == nullptr); + break; + case json::value_t::array: + CHECK(v.get>() == j.get>()); + break; + case json::value_t::object: + CHECK((v.get>() == j.get>())); + break; + case json::value_t::binary: + case json::value_t::discarded: + default: + break; + } + +#if !defined(JSON_NOEXCEPTION) + // conversions to the wrong type throw what basic_json throws + if (!j.is_number()) + { + CHECK(exception_of([&] { static_cast(v.get()); }) == without_path(exception_of([&] { static_cast(j.get()); }))); + } + CHECK(exception_of([&] { static_cast(v.get()); }) == without_path(exception_of([&] { static_cast(j.get()); }))); + CHECK(exception_of([&] { static_cast(v.get()); }) == without_path(exception_of([&] { static_cast(j.get()); }))); + CHECK(exception_of([&] { static_cast(v.get()); }) == without_path(exception_of([&] { static_cast(j.get()); }))); + if (!j.is_array()) + { + CHECK(exception_of([&] { static_cast(v.get>()); }) == without_path(exception_of([&] { static_cast(j.get>()); }))); + } + if (!j.is_object()) + { + CHECK(exception_of([&] { static_cast(v.get>()); }) == without_path(exception_of([&] { static_cast(j.get>()); }))); + } +#endif + + if (v.is_array()) + { + std::size_t i = 0; + for (const ordered_json_view e : v) + { + check_values(e, j[i++], text); + } + } + else 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.size() == j.size()) // (no duplicate keys) + { + check_values(it.value(), j[key], text); + } + } + } +} + +struct record +{ + std::string name{}; // NOLINT(readability-redundant-member-init) + int count = 0; +}; + +void from_json(const json& j, record& r) +{ + j.at("name").get_to(r.name); + j.at("count").get_to(r.count); +} + +struct defaults +{ + int a = 1; + int b = 2; +}; + +// sets only the members the JSON value has +void from_json(const json& j, defaults& d) +{ + if (j.contains("a")) + { + j.at("a").get_to(d.a); + } + if (j.contains("b")) + { + j.at("b").get_to(d.b); + } +} +} // namespace + +TEST_CASE("json_view values") +{ + SECTION("generated documents") + { + generator g; + for (int i = 0; i < 2000; ++i) + { + std::string text; + g.value(text, 0); + CAPTURE(text) + const ordered_json_document d = ordered_json_document::parse(text); + check_values(d.root(), ordered_json::parse(text), text); + } + } + + SECTION("floats are converted as parse() converts them") + { + std::mt19937_64 rng(5295); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) + std::vector tokens = {"0.1", "-0.0", "1e308", "1.7976931348623157e308", "2.2250738585072011e-308", "4.9e-324", "5e-324", + "0.1000000000000000055511151231257827021181583404541015625", "123456789012345678901234567890", + "9007199254740993", "1.00000000000000011102230246251565404236316680908203125", "7.2057594037927933e16", + // around the limits of the conversion from the digit layout: 19 and 20 + // digits, and those of Clinger's fast path (2^53, 10^22) + "1234567890.123456789", "1234567890.1234567891", "0.0000000000000000001", "123456789012345678.9", + "9007199254740992.0", "9007199254740993.0", "9007199254740994.0", "1.5e22", "1.5e23", "15e-22", "15e-23", + "1e-400", "0.0e0", "-0.0e-5", "12E+3", "12e-0", + // and of float: 2^24 + 1 and 2^24 + 3 (ties), the subnormal and normal limits + "16777217", "16777219", "1.4e-45", "7.006492321624085e-46", "7.006492321624086e-46", + "1.17549435e-38", "0.30000001192092896", "3.4028234e37" + }; + for (int i = 0; i < 20000; ++i) + { + const std::uint64_t bits = rng(); + double d = 0; + std::memcpy(&d, &bits, sizeof(d)); + if (!std::isfinite(d)) + { + continue; + } + std::array buf{}; + switch (i % 5) // NOLINT(hicpp-multiway-paths-covered) + { + case 0: + static_cast(std::snprintf(buf.data(), buf.size(), "%.17g", d)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + break; + case 1: + static_cast(std::snprintf(buf.data(), buf.size(), "%.15g", d)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + break; + case 2: + static_cast(std::snprintf(buf.data(), buf.size(), "%.3e", d)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + break; + case 3: + static_cast(std::snprintf(buf.data(), buf.size(), "%.25g", d)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + break; + default: + static_cast(std::snprintf(buf.data(), buf.size(), "%.0f", d)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + break; + } + tokens.emplace_back(buf.data()); + } + using json_float = nlohmann::basic_json; + for (const auto& token : tokens) + { + CAPTURE(token) + const std::string text = "[" + token + "]"; + const double b = json::parse(text)[0].get(); + const json_document dd = json_document::parse(text); + CHECK(bits(dd.root()[0].get()) == bits(b)); + if (std::abs(b) < 1e38) + { + const nlohmann::basic_json_document df = nlohmann::basic_json_document::parse(text); + CHECK(bits(df.root()[0].get()) == bits(json_float::parse(text)[0].get())); + } + } + } + + SECTION("number tokens") + { + const json_document d = json_document::parse(R"([1.50, 1E2, -0, 123456789012345678901234567890, -12, 7, "x"])"); + const json_view v = d.root(); + CHECK(v[0].number_token() == "1.50"); + CHECK(v[1].number_token() == "1E2"); + CHECK(v[2].number_token() == "-0"); + CHECK(v[3].number_token() == "123456789012345678901234567890"); + CHECK(v[4].number_token() == "-12"); + 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.get_string(), "[json.exception.type_error.302] type must be string, but is array", json::type_error&); + } + + SECTION("conversions") + { + const std::string text = R"({"name": "widget", "count": 3, "tags": ["a", "b\n"], "sizes": {"s": 1, "m": 2}, "pair": [1, "x"]})"; + const json_document d = json_document::parse(text); + const json_view v = d.root(); + const json j = json::parse(text); + + // user types with from_json, and other types, through basic_json + const auto r = v.get(); + CHECK(r.name == "widget"); + CHECK(r.count == 3); + CHECK((v["pair"].get>() == j["pair"].get>())); + CHECK(v["tags"].get>() == j["tags"].get>()); + CHECK((v["sizes"].get>() == j["sizes"].get>())); + CHECK(v["tags"].get>() == std::vector {"a", "b\n"}); + + // views of the elements + const auto views = v["tags"].get>(); + CHECK(views.size() == 2); + CHECK(views[1].get_string() == "b\n"); + const auto members = v.get>(); + CHECK(members.at("count").get() == 3); + CHECK(v.get()["name"].get_string() == "widget"); + + // strings without a copy point into the source text + CHECK(v["name"].get_string().data() == text.data() + text.find("widget")); +#ifdef JSON_HAS_CPP_17 + CHECK(v["name"].get() == "widget"); +#endif + + std::string name; + int count = 0; + CHECK(&v["name"].get_to(name) == &name); + v["count"].get_to(count); + CHECK(name == "widget"); + CHECK(count == 3); + + // other types go to from_json with the existing value, as basic_json::get_to() + defaults def; + def.b = 5; + const json_document partial = json_document::parse(R"({"a":4})"); + partial.root().get_to(def); + CHECK(def.a == 4); + CHECK(def.b == 5); + + // a duplicate key: the last value, as parse() + const json_document dup = json_document::parse(R"({"a":1,"a":2})"); + CHECK((dup.root().get>() == std::map {{"a", 2}})); + + const json_view invalid{}; + CHECK_THROWS_WITH_AS(invalid.get(), "[json.exception.type_error.302] type must be number, but is discarded", json::type_error&); + CHECK(invalid.get().is_discarded()); + } + + SECTION("value") + { + const json_document d = json_document::parse(R"({"n": 1, "s": "text", "o": {"x": [10, 20]}})"); + const json_view v = d.root(); + const json j = v.materialize(); + CHECK(v.value("n", 0) == j.value("n", 0)); + CHECK(v.value("missing", 42) == j.value("missing", 42)); + CHECK(v.value("s", "default") == j.value("s", "default")); + CHECK(v.value("missing", "default") == j.value("missing", "default")); + CHECK(v.value(std::string("n"), 2.5) == j.value(std::string("n"), 2.5)); + CHECK(v.value(json::json_pointer("/o/x/1"), 0) == j.value(json::json_pointer("/o/x/1"), 0)); + CHECK(v.value(json::json_pointer("/o/x/5"), 0) == j.value(json::json_pointer("/o/x/5"), 0)); + CHECK(v.value(json::json_pointer("/o/y"), "none") == j.value(json::json_pointer("/o/y"), "none")); + // with a JSON pointer, arrays can be asked as well + 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)); +#if !defined(JSON_NOEXCEPTION) + CHECK(exception_of([&] { static_cast(v["o"]["x"].value("k", 0)); }) == without_path(exception_of([&] { static_cast(j["o"]["x"].value("k", 0)); }))); + CHECK(exception_of([&] { static_cast(v.value("s", 0)); }) == without_path(exception_of([&] { static_cast(j.value("s", 0)); }))); + CHECK(exception_of([&] { static_cast(v["n"].value("x", 0)); }) == without_path(exception_of([&] { static_cast(j["n"].value("x", 0)); }))); + CHECK(exception_of([&] { static_cast(v["n"].value(json::json_pointer("/x"), 0)); }) == without_path(exception_of([&] { static_cast(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 + CHECK(exception_of([&] { static_cast(v.value("n", "default")); }) == without_path(exception_of([&] { static_cast(j.value("n", "default")); }))); + CHECK(exception_of([&] { static_cast(v["n"].value("x", "default")); }) == without_path(exception_of([&] { static_cast(j["n"].value("x", "default")); }))); + CHECK(exception_of([&] { static_cast(v.value(json::json_pointer("/n"), "default")); }) == without_path(exception_of([&] { static_cast(j.value(json::json_pointer("/n"), "default")); }))); + CHECK(exception_of([&] { static_cast(v["s"].value(json::json_pointer("/x"), "default")); }) == without_path(exception_of([&] { static_cast(j["s"].value(json::json_pointer("/x"), "default")); }))); +#endif + // (defaults and members longer than the short string optimization) + CHECK(v.value("s", "a default longer than fifteen characters") == "text"); + CHECK(v.value("missing", "a default longer than fifteen characters") == "a default longer than fifteen characters"); + CHECK(v.value(json::json_pointer("/s"), "a default longer than fifteen characters") == "text"); + CHECK(v.value(json::json_pointer("/zz"), "a default longer than fifteen characters") == "a default longer than fifteen characters"); + } +} + +TEST_CASE("json_view JSON pointers") +{ + SECTION("every value of generated documents") + { + generator g; + for (int i = 0; i < 1000; ++i) + { + std::string text; + g.value(text, 0); + const ordered_json_document d = ordered_json_document::parse(text); + if (has_duplicate_keys(d.root())) + { + continue; + } + CAPTURE(text) + const ordered_json j = ordered_json::parse(text); + const ordered_json flat = j.flatten(); + for (const auto& leaf : flat.items()) + { + // the leaf and each of its parents + for (ordered_json::json_pointer p(leaf.key());; p = p.parent_pointer()) + { + CAPTURE(p.to_string()) + CHECK(d.root()[p].materialize() == j[p]); + CHECK(d.root().at(p).materialize() == j.at(p)); + CHECK(d.root().contains(p)); + if (p.empty()) + { + break; + } + } + } + } + } + +#if !defined(JSON_NOEXCEPTION) + SECTION("errors are those of basic_json") + { + const std::string text = R"({"a": [1, {"b": null}], "c": "s", "": {"": 0}, "a~b": 1, "c/d": 2})"; + const json_document d = json_document::parse(text); + const json_view v = d.root(); + const json j = v.materialize(); + for (const char* pointer : + {"", "/", "//", "/a", "/a/0", "/a/1/b", "/a/-", "/a/01", "/a/00", "/a/1a", "/a/a", "/a/", "/a/2", "/a/99", "/a/99999999999999999999", + "/a/18446744073709551615", "/a/-1", "/a/+1", "/a/ 1", "/x", "/c/x", "/a/0/x", "/a/1/b/c", "/a~0b", "/c~1d", "/c~1d/x", "/a/1/-" + }) + { + CAPTURE(pointer) + const json::json_pointer p(pointer); + const std::string at_error = without_path(exception_of([&] { static_cast(j.at(p)); })); + CHECK(exception_of([&] { static_cast(v.at(p)); }) == at_error); + if (at_error.empty()) + { + CHECK(v.at(p).materialize() == j.at(p)); + CHECK(v[p].materialize() == j[p]); + } + else if (at_error.find("out_of_range.401") != std::string::npos || at_error.find("out_of_range.403") != std::string::npos) // NOLINT(abseil-string-find-str-contains) + { + // undefined behavior for const basic_json::operator[] + CHECK(v[p].is_discarded()); + } + else + { + CHECK(exception_of([&] { static_cast(v[p]); }) == without_path(exception_of([&] { static_cast(j[p]); }))); + } + // (basic_json::contains() throws out_of_range.404 for an empty + // array index token, although it is not meant to throw; the view + // answers false) + const std::string contains_error = exception_of([&] + { + const bool found = j.contains(p); + static_cast(found); + }); + CHECK(v.contains(p) == (contains_error.empty() && j.contains(p))); + CHECK(exception_of([&] { static_cast(v.value(p, 5)); }) == without_path(exception_of([&] { static_cast(j.value(p, 5)); }))); + } + } +#endif +} diff --git a/tests/src/unit-json_view_builder.cpp b/tests/src/unit-json_view_builder.cpp new file mode 100644 index 000000000..6935981fb --- /dev/null +++ b/tests/src/unit-json_view_builder.cpp @@ -0,0 +1,662 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include +#if JSON_TEST_USING_MULTIPLE_HEADERS + #include + #include + #include +#else + #include // the single header contains the internal headers +#endif +using nlohmann::json; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace +{ +using nlohmann::detail::view::document_data; +using nlohmann::detail::view::node; + +// the node index of a text; a vector input has no terminating NUL, so that +// AddressSanitizer catches any read past the last byte +struct built +{ + std::unique_ptr data{}; // NOLINT(readability-redundant-member-init) + std::vector copy{}; // NOLINT(readability-redundant-member-init) + bool ok = false; + nlohmann::detail::view::parse_failure failure{}; +}; + +template +built build(const std::string& text, bool comments, bool trailing_commas, bool sentinel) +{ + built r; + r.data.reset(document_data::create(nlohmann::detail::view::estimate_nodes(text.data(), text.size()))); + const char* src = text.c_str(); + if (!sentinel) + { + r.copy.assign(text.begin(), text.end()); + 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); + r.data->src = src; + r.data->base[0] = src; + r.data->base[1] = r.data->arena.data(); + return r; +} + +// the value of a subtree, as json::parse would build it +json value_of(const document_data& d, const node*& n) +{ + const node& x = *n; + ++n; + switch (static_cast(x.kind)) + { + case json::value_t::object: + { + json o = json::object(); + const node* const end = &x + x.next; + while (n != end) + { + const std::string key(d.str(*n), n->len); + ++n; + o[key] = value_of(d, n); + } + return o; + } + case json::value_t::array: + { + json a = json::array(); + const node* const end = &x + x.next; + while (n != end) + { + a.push_back(value_of(d, n)); + } + return a; + } + case json::value_t::string: + return std::string(d.str(x), x.len); + case json::value_t::boolean: + return (x.flags & nlohmann::detail::view::node_flags::is_true) != 0; + case json::value_t::number_integer: + return static_cast(nlohmann::detail::view::integer_bits(x)); + case json::value_t::number_unsigned: + return nlohmann::detail::view::integer_bits(x); + case json::value_t::number_float: + return json::parse(std::string(d.src + x.off, x.len)).get(); + case json::value_t::null: + case json::value_t::binary: + case json::value_t::discarded: + default: + return nullptr; + } +} + +json value_of(const built& b) +{ + const node* n = b.data->nodes; + json v = value_of(*b.data, n); + CHECK(n == b.data->nodes + b.data->node_count); + return v; +} + +// accept/reject and the value must match json::parse, for all options and +// with and without a NUL after the text +void check_same(const std::string& text) +{ + CAPTURE(text) + 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); + for (const bool sentinel : + { + true, false + }) + { + const built b = build(text, comments, trailing_commas, sentinel); + CHECK(b.ok == accepted); + if (b.ok && accepted) + { + CHECK(value_of(b) == json::parse(text, nullptr, true, comments, trailing_commas)); + } + } + } +} + +// a string value and a key must be accepted or rejected as json::parse does, +// and give its value (cheaper than check_same: the options do not matter) +void check_string(const std::string& content) +{ + for (const std::string& text : + { + "[\"" + content + "\"]", "{\"" + content + "\":1}" + }) + { + const bool accepted = json::accept(text); + for (const bool sentinel : + { + true, false + }) + { + const built b = build(text, false, false, sentinel); + if (b.ok != accepted || (accepted && value_of(b) != json::parse(text))) + { + CAPTURE(text) + CHECK(b.ok == accepted); + CHECK((b.ok && accepted ? value_of(b) == json::parse(text) : true)); + } + } + } +} + +// a small deterministic generator of documents +struct generator +{ + std::mt19937 rng{5295}; // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) + + int r(int n) + { + return static_cast(rng() % static_cast(n)); + } + + void ws(std::string& o) + { + for (int n = r(4) == 0 ? r(12) : r(2); n > 0; --n) + { + o += " \n\t\r "[r(6)]; + } + } + + void str(std::string& o) + { + static const char* const pieces[] = {"a", "Z", " ", "~", "\\n", "\\\"", "\\\\", "\\/", "\\u00e9", "\\ud83d\\ude00", "\xc3\xa9", "\xe3\x81\x82", "\xf0\x9f\x98\x80", "\x7f", "\\u001f", "long enough text to leave the first 16 bytes"}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + o += '"'; + for (int n = r(3) == 0 ? r(20) : r(6); n > 0; --n) + { + o += pieces[r(16)]; + } + o += '"'; + } + + void num(std::string& o) + { + static const char* const numbers[] = {"0", "-0", "1", "-1", "12", "123456789", "1234567890123456789", "9223372036854775807", "-9223372036854775808", // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + "9223372036854775808", "18446744073709551615", "18446744073709551616", "-9223372036854775809", + "1.5", "-2.25e-3", "1e10", "1E+2", "0.000001", "3.141592653589793238462643", "1e308", "-1e-400", "123.456e7" + }; + o += numbers[r(22)]; + } + + void value(std::string& o, int depth) + { + ws(o); + const int k = depth > 5 ? 2 + r(6) : r(8); + if (k == 0 || k == 1) + { + const bool object = k == 0; + o += object ? '{' : '['; + for (int i = r(5); i > 0; --i) + { + ws(o); + if (object) + { + str(o); + ws(o); + o += ':'; + } + value(o, depth + 1); + o += i > 1 ? "," : ""; + } + ws(o); + o += object ? '}' : ']'; + } + else if (k < 4) + { + str(o); + } + else if (k < 6) + { + num(o); + } + else + { + static const char* const literals[] = {"true", "false", "null"}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + o += literals[r(3)]; + } + ws(o); + } +}; +} // namespace + +TEST_CASE("json_view string_ref") +{ + // std::string_view in C++17, a stand-in with the same members before + using nlohmann::detail::view::string_ref; + const std::string text = "abc"; + const string_ref r(text); + CHECK(r.length() == 3); + CHECK(std::string(r.begin(), r.end()) == "abc"); + CHECK(r[1] == 'b'); + CHECK(r != string_ref("abd")); + CHECK_FALSE(r != string_ref("abcd", 3)); + std::ostringstream o; + o << r; + CHECK(o.str() == "abc"); +} + +TEST_CASE("json_view builder") +{ + SECTION("scalars and containers") + { + for (const char* text : + { + "null", "true", "false", "0", "-0", "42", "-42", "1.5", "\"\"", "\"abc\"", "[]", "{}", "[1,2,3]", "{\"a\":1,\"b\":[true,null]}", // NOLINT(modernize-raw-string-literal) + " [ 1 , 2 ] ", "{\"a\" : {\"b\" : {}}}", "[[[]]]", "\"\\u00e4\\n\\ud83d\\ude00\"", "{\"a\":1,\"a\":2}", "18446744073709551616", // NOLINT(modernize-raw-string-literal) + "-9223372036854775809", "123456789012345678901234567890", "1e400", "-1e400", "1.7976931348623157e308" + }) + { + check_same(text); + } + + // the midpoint between the largest double and 2^1024 rounds to + // infinity (an overflow), one less to the largest double: with more + // than 19 digits, Eisel-Lemire cannot decide these, and the overflow + // check needs the exact comparison with the midpoint + const std::string midpoint = "179769313486231580793728971405303415079934132710037826936173778980444968292764750946649017977587207096330286416692887910946555547851940402630657488671505820681908902000708383676273854845817711531764475730270069855571366959622842914819860834936475292719074168444365510704342711559699508093042880177904174497792"; + const std::string below = "179769313486231580793728971405303415079934132710037826936173778980444968292764750946649017977587207096330286416692887910946555547851940402630657488671505820681908902000708383676273854845817711531764475730270069855571366959622842914819860834936475292719074168444365510704342711559699508093042880177904174497791"; + check_same(midpoint); + check_same("-" + midpoint); + check_same(below); + check_same("[" + below + "," + midpoint + "]"); + + // the check uses the floating-point type of the document: with float, + // the view rejects what parse() rejects (out_of_range.406), and a + // double document is not affected + using float_json = nlohmann::basic_json; + CHECK_FALSE(float_json::accept("1e39")); + CHECK(float_json::accept("3.4028235e38")); + CHECK_FALSE(float_json::accept("3.4028236e38")); + for (const char* text : + { + "1e39", "-1e39", "3.4028235e38", "-3.4028235e38", "3.4028236e38", "-3.4028236e38", "3.4028234663852886e38", "1e38", + "340282356779733661637539395458142568448", "340282356779733661637539395458142568447.99", "0.00034028236e42", + "[1.5e38, 3.5e38]", R"({"a": 1e-50, "b": 1e39})" + }) + { + CAPTURE(text) + const bool float_accepted = float_json::accept(text); + for (const bool sentinel : + { + true, false + }) + { + const built f = build(text, false, false, sentinel); + CHECK(f.ok == float_accepted); + if (!f.ok) + { + CHECK(f.failure.code == nlohmann::detail::view::error_code::number_overflow); + } + CHECK(build(text, false, false, sentinel).ok == json::accept(text)); + } + } + } + + SECTION("malformed input") + { + for (const char* text : + { + "", " ", "[", "]", "{", "}", "[1,]", "{\"a\":1,}", "[1 2]", "{\"a\" 1}", "{1:2}", "tru", "nul", "fals", "truex", "-", "01", "1.", ".5", "1e", "1e+", + "\"", "\"abc", "\"\\x\"", "\"\\u12\"", "\"\\u12G4\"", "\"\\ud800\"", "\"\\udc00\"", "\"\\ud800\\u0041\"", "\"\x01\"", "\"\xff\"", "\"\xc3\"", // NOLINT(modernize-raw-string-literal) + "\"\xe0\x80\x80\"", "\"\xed\xa0\x80\"", "[1]x", "[1] [2]", "/", "/*", "/* */ 1", "// c\n1", "1 // c", "[1,/*c*/2]", "[1,2,]" + }) + { + check_same(text); + } + } + + SECTION("NUL, BOM, and whitespace") + { + // a NUL inside a string is a control character, as for json::parse + // (where a NUL ends the input, it does so only between values) + for (const bool sentinel : + { + true, false + }) + { + const built b = build(std::string("[\"ab\0cd\"]", 9), false, false, sentinel); + CHECK(!b.ok); + CHECK(b.failure.code == nlohmann::detail::view::error_code::string_control_character); + CHECK(b.failure.offset == 4); + } + check_same(std::string("[1]\0garbage", 11)); + check_same(std::string("[1\0]", 4)); + check_same(std::string("[1, // c\0\n2]", 12)); + check_same(std::string("[1, /* c\0 */ 2]", 15)); + check_same("\xEF\xBB\xBF[1]"); + check_same("\xEF\xBB[1]"); + check_same(" \t\r\n 7 \n"); + for (const char* text : + {"[1]\r", "[1]\n", "[1]\r\n", "[1,\r2]", "[1,\r\n2]", "7\r", "\"x\"\r", "{\"a\":\r\n1}\r", "[\n 1,\n 2\n]", "{\n \"a\": [\n 1\n ]\n}" + }) + { + check_same(text); + } + } + + SECTION("deep nesting") + { + // the deepest nesting is recorded (materialize() sizes its stack from it) + CHECK(build("1", false, false, false).data->max_depth == 0); + CHECK(build("[]", false, false, false).data->max_depth == 1); + CHECK(build(R"([[1],{"a":[[]]},[]])", false, false, false).data->max_depth == 4); + // a stack that turns out too small still grows + const std::string nested = R"([[1],{"a":[[{"b":[]}]]},[]])"; + const built shallow = build(nested, false, false, false); + REQUIRE(shallow.ok); + shallow.data->max_depth = 1; + CHECK(nlohmann::detail::view::materialize(*shallow.data, shallow.data->nodes) == json::parse(nested)); + + // the open containers beyond 64 levels live on the heap + for (const std::size_t depth : + { + 63u, 64u, 65u, 1000u, 100000u + }) + { + const std::string arrays = std::string(depth, '[') + std::string(depth, ']'); + const built b = build(arrays, false, false, false); + REQUIRE(b.ok); + CHECK(b.data->node_count == depth); + CHECK(b.data->nodes[0].next == depth); + CHECK(b.data->max_depth == depth); + std::string objects; + for (std::size_t i = 0; i < depth; ++i) + { + objects += "{\"a\":"; + } + objects += '1' + std::string(depth, '}'); + const built o = build(objects, false, false, false); + REQUIRE(o.ok); + CHECK(o.data->node_count == (2 * depth) + 1); + CHECK(o.data->max_depth == depth); + CHECK(!build(std::string(depth, '[') + std::string(depth - 1, ']'), false, false, false).ok); + } + } + + SECTION("generated documents and damaged copies") + { + generator g; + for (int i = 0; i < 3000; ++i) + { + std::string text; + g.value(text, 0); + check_same(text); + // damage: flip one byte, or cut the text + std::string damaged = text; + const auto at = static_cast(g.r(static_cast(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_same(damaged); + check_same(text.substr(0, at)); + } + } + + SECTION("test files") + { + for (const char* name : + { + "/json.org/1.json", "/json.org/2.json", "/json.org/3.json", "/json.org/4.json", "/json.org/5.json", + "/json_testsuite/sample.json", "/nativejson-benchmark/canada.json", "/nativejson-benchmark/citm_catalog.json", + "/nativejson-benchmark/twitter.json", "/json_tests/pass1.json", "/json_tests/pass2.json", "/json_tests/pass3.json" + }) + { + CAPTURE(name) + const std::ifstream f(std::string(TEST_DATA_DIRECTORY) + name, std::ios::binary); + std::stringstream ss; + ss << f.rdbuf(); + const std::string text = ss.str(); + REQUIRE(!text.empty()); + const built b = build(text, false, false, true); + REQUIRE(b.ok); + CHECK(value_of(b) == json::parse(text)); + } + } +} + +TEST_CASE("json_view builder: strings across vector blocks") +{ + // Strings are scanned 8 or 16 bytes at a time (NEON, SSE2, or SWAR) and + // non-ASCII text with the vector UTF-8 check (NEON, SSSE3) or one sequence + // at a time. Sequences are placed so that they start at every offset + // around the block boundaries of keys (16, 32) and values (8, 24), with + // text of several lengths after them. + const std::array prefixes = {{0, 6, 7, 8, 13, 14, 15, 16, 21, 22, 23, 24, 29, 30, 31, 32}}; + const std::array suffixes = {{0, 3, 17}}; + const auto around = [&](const std::string & seq, std::size_t prefix, std::size_t suffix) + { + return std::string(prefix, 'a') + seq + std::string(suffix, 'b'); + }; + + SECTION("every two-byte sequence") + { + for (unsigned lead = 0x80; lead <= 0xFF; ++lead) + { + for (unsigned second = 0; second <= 0xFF; ++second) + { + if (second == '"' || second == '\\') + { + continue; + } + const std::string seq = {static_cast(lead), static_cast(second)}; + check_string(around(seq, prefixes[(lead + second) % 16], suffixes[second % 3])); + } + } + } + + SECTION("three- and four-byte sequences") + { + const std::array conts = {{0x7F, 0x80, 0x8F, 0x90, 0x9F, 0xA0, 0xBF, 0xC0}}; + for (unsigned lead = 0xE0; lead <= 0xF7; ++lead) + { + for (const unsigned b2 : conts) + { + for (const unsigned b3 : conts) + { + for (const std::size_t prefix : prefixes) + { + std::string seq = {static_cast(lead), static_cast(b2), static_cast(b3)}; + if (lead >= 0xF0) + { + seq += static_cast(prefix % 2 == 0 ? 0x80 : 0xBF); + } + check_string(around(seq, prefix, suffixes[prefix % 3])); + // cut short before the end of the string + check_string(around(seq.substr(0, seq.size() - 1), prefix, suffixes[prefix % 3])); + } + } + } + } + } + + SECTION("long runs of text with one damaged byte") + { + const std::array chars = {{"a", "\xc3\xa9", "\xe3\x81\x82", "\xf0\x9f\x98\x80", "\xed\x9f\xbf", "\xef\xbf\xbf", "\xf4\x8f\xbf\xbf"}}; + const std::array damage = {{'\x80', '\xbf', '\xc0', '\xc1', '\xe0', '\xed', '\xf5', '\xff', '\x1f', '"', '\\'}}; + std::mt19937 rng(5295); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible + for (int i = 0; i < 4000; ++i) + { + std::string text; + const auto n = rng() % 60; + for (unsigned k = 0; k < n; ++k) + { + text += chars[rng() % chars.size()]; + } + check_string(text); + if (!text.empty()) + { + text[rng() % text.size()] = damage[rng() % damage.size()]; + check_string(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 + // that many bytes left, and with the UTF-8 check where there is + // non-ASCII text. Runs of every length from 0 to 40 between valid and + // invalid escapes, UTF-8 sequences, and control characters, in strings + // that are cut at all places near the end of the text. + const std::array valid = {{"\\n", "\\\"", "\\\\", "\\/", "\\t", "\\u00e4", "\\u20ac", "\\ud83d\\ude00", "\\u0000"}}; + const std::array invalid = {{"\\x", "\\ud800", "\\udc00", "\\u12G4", "\\u12", "\\ud83dx", "\\ud83d\\u0041", "\\", "\x01"}}; + const std::array utf8 = {{"\xc3\xa4", "\xe2\x82\xac", "\xf0\x9f\x98\x80", "\xc3", "\xff", "\x80"}}; + std::mt19937 rng(2026); // 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 < 60; ++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('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; + } + } + check_string(content); + check_string(content.substr(0, content.size() - (rng() % 4))); + } + } + // the text before the first escape, and runs and escapes at the end of the text + for (std::size_t before = 0; before <= 40; ++before) + { + for (const char* escape : valid) + { + check_string(std::string(before, 'a') + escape); + check_string(std::string(before, 'a') + escape + std::string(before % 19, 'z')); + check_string(std::string(before, 'a') + escape + escape + std::string(before % 19, 'z')); + } + } +} + +TEST_CASE("json_view node integer bits") +{ + using nlohmann::detail::view::integer_bits; + using nlohmann::detail::view::set_integer_bits; + + // an integer lives in len (low half) and next (high half), on any byte + // order; a big-endian target must not store the native word over both + SECTION("set_integer_bits and integer_bits") + { + node n = {}; + for (const std::uint64_t v : + { + std::uint64_t{0}, std::uint64_t{1}, std::uint64_t{0xFFFFFFFFu}, std::uint64_t{0x100000000u}, + std::uint64_t{0x0000000200000003u}, std::uint64_t{0x0123456789ABCDEFu}, std::uint64_t{0xFFFFFFFFFFFFFFFEu} + }) + { + CAPTURE(v) + n.kind = 0x5A; + n.flags = 0xA5; + n.extra = 0x1234; + n.off = 0x89ABCDEFu; + set_integer_bits(n, v); + CHECK(integer_bits(n) == v); + CHECK(n.len == static_cast(v)); + CHECK(n.next == static_cast(v >> 32)); + // the other fields are untouched + CHECK(n.kind == 0x5A); + CHECK(n.flags == 0xA5); + CHECK(n.extra == 0x1234); + CHECK(n.off == 0x89ABCDEFu); + } + } + + SECTION("parsed integers") + { + struct integer_case + { + const char* text; + std::uint64_t bits; + }; + for (const integer_case c : + { + integer_case{"[8589934595]", 0x0000000200000003u}, integer_case{"[4294967296]", 0x100000000u}, integer_case{"[4294967295]", 0xFFFFFFFFu}, + integer_case{"[-2]", 0xFFFFFFFFFFFFFFFEu}, integer_case{"[-4294967297]", 0xFFFFFFFEFFFFFFFFu}, integer_case{"[18446744073709551615]", 0xFFFFFFFFFFFFFFFFu}, + integer_case{"[7]", 7u} + }) + { + CAPTURE(c.text) + const built b = build(c.text, false, false, true); + REQUIRE(b.ok); + const node& n = b.data->nodes[1]; + CHECK(integer_bits(n) == c.bits); + CHECK(n.len == static_cast(c.bits)); + CHECK(n.next == static_cast(c.bits >> 32)); + } + } +} + +TEST_CASE("json_view node array size limit") +{ + // a node array larger than the address space is refused, not wrapped to a + // small allocation (the size computation overflows on 32-bit targets, and + // for absurd counts everywhere) + std::unique_ptr d(document_data::create(0)); + d->reserve(8); + REQUIRE(d->node_capacity >= 8); + d->node_count = 2; + const std::size_t cap = d->node_capacity; + node* const nodes = d->nodes; + +#if !defined(JSON_NOEXCEPTION) + const std::size_t too_many = document_data::max_nodes() + 1; + CHECK_THROWS_AS(d->reserve(too_many), std::bad_alloc&); + CHECK_THROWS_AS(d->reserve((std::numeric_limits::max)()), std::bad_alloc&); + // the array is unchanged + CHECK(d->nodes == nodes); + CHECK(d->node_capacity == cap); + CHECK(d->node_count == 2); +#endif + + // the largest count that fits is not refused by the check (nothing is + // allocated for a count that is already there) + d->reserve(cap); + CHECK(d->nodes == nodes); +} diff --git a/tests/src/unit-json_view_dump.cpp b/tests/src/unit-json_view_dump.cpp new file mode 100644 index 000000000..f3ccc004e --- /dev/null +++ b/tests/src/unit-json_view_dump.cpp @@ -0,0 +1,938 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include +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; +using json_view_test::has_duplicate_keys; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// These tests were split off unit-json_view.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") +{ + SECTION("the output of ordered_json::dump()") + { + generator g; + for (int i = 0; i < 2000; ++i) + { + std::string text; + g.value(text, 0); + const ordered_json_document d = ordered_json_document::parse(text); + if (has_duplicate_keys(d.root())) + { + continue; + } + CAPTURE(text) + const ordered_json j = ordered_json::parse(text); + for (const int indent : + { + -1, 0, 2 + }) + { + for (const bool ensure_ascii : + { + false, true + }) + { + CHECK(d.root().dump(indent, i % 2 == 0 ? ' ' : '\t', ensure_ascii) == j.dump(indent, i % 2 == 0 ? ' ' : '\t', ensure_ascii)); + } + } + // also of each element + for (const ordered_json_view e : d.root()) + { + CHECK(e.dump() == e.materialize().dump()); + } + } + } + + SECTION("strings") + { + const std::string text = R"(["plain", "\u0000\u0001\u001f\u007f\u0080é€￿😀", "\"\\\/\b\f\n\r\t", "aéあ😀b", "long text beyond the eight bytes of a word \n with an escape in the middle"])"; + const ordered_json_document d = ordered_json_document::parse(text); + const ordered_json j = ordered_json::parse(text); + CHECK(d.root().dump() == j.dump()); + CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true)); + CHECK(d.root().dump(4, ' ', true) == j.dump(4, ' ', true)); + const std::string key_text = R"({"é\n": {"\"": [], "": {}}})"; + const ordered_json_document keys = ordered_json_document::parse(key_text); + const ordered_json key_json = ordered_json::parse(key_text); + CHECK(keys.root().dump(2, ' ', true) == key_json.dump(2, ' ', true)); + } + + SECTION("numbers") + { + const std::string text = "[1.50, 1E2, -0, -0.0, 123456789012345678901234567890, 18446744073709551615, -9223372036854775808, 0.1, 1e-7, 5e-324]"; + const json_document d = json_document::parse(text); + CHECK(d.root().dump() == json::parse(text).dump()); + CHECK(d.root().dump() == "[1.5,100.0,0,-0.0,1.2345678901234568e+29,18446744073709551615,-9223372036854775808,0.1,1e-07,5e-324]"); + CHECK(d.root().dump(-1, ' ', false, json_view::number_format::source) == "[1.50,1E2,-0,-0.0,123456789012345678901234567890,18446744073709551615,-9223372036854775808,0.1,1e-7,5e-324]"); + // also indented, and with ensure_ascii + 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]"); + + // float tokens of up to 17 significant digits in every spelling: those + // of at most 15 digits are written from their digits, the others + // through the conversion; both as dump() writes them + { + std::mt19937_64 tokens(1170); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) + // a number below n; the remainder is a std::uint64_t, which is + // std::size_t on some platforms and wider on others + const auto draw = [&tokens](std::size_t n) + { + const std::uint64_t r = tokens() % n; + return static_cast(r); + }; + std::string many_tokens = "["; + for (int i = 0; i < 20000; ++i) + { + const std::size_t length = 1 + draw(17); + std::string digits(1, static_cast('1' + draw(9))); + for (std::size_t k = 1; k < length; ++k) + { + digits += static_cast('0' + draw(10)); + } + digits += std::string(draw(4), '0'); // trailing zeros + std::string token = draw(3) == 0 ? "-" : ""; + const std::size_t point = draw(digits.size() + 1); + if (point == 0) + { + token += "0." + std::string(draw(5), '0') + digits; + } + else + { + token += digits.substr(0, point) + (point < digits.size() ? "." + digits.substr(point) : ""); + } + // an exponent that keeps the value between about 1e-320 and 1e300 + const int exponent = static_cast(draw(600)) - 300 - static_cast(point); + if (draw(4) != 0) + { + token += (draw(2) == 0 ? "e" : "E") + std::string(exponent >= 0 && draw(2) == 0 ? "+" : "") + std::to_string(exponent); + } + else if (point == digits.size()) + { + token += ".0"; // (a float, not an integer) + } + many_tokens += (i != 0 ? "," : "") + token; + } + many_tokens += ']'; + const json_document many_doc = json_document::parse(many_tokens); + CHECK(many_doc.root().dump() == json::parse(many_tokens).dump()); + } + + // 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::string many = "["; + for (int i = 0; i < 5000; ++i) + { + const std::uint64_t bits = rng(); + double x = 0; + std::memcpy(&x, &bits, sizeof(x)); + if (std::isfinite(x)) + { + many += (many.size() > 1 ? "," : "") + json(x).dump(); + } + } + many += ']'; + const json_document many_document = json_document::parse(many); + CHECK(many_document.root().dump() == json::parse(many).dump()); + + using json_float = nlohmann::basic_json; + const nlohmann::basic_json_document float_document = nlohmann::basic_json_document::parse("[0.1, 1.5e10, 3.4028235e38]"); + 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 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(cp); + } + else if (cp < 0x800) + { + text += static_cast(0xC0 | (cp >> 6)); + text += static_cast(0x80 | (cp & 0x3F)); + } + else if (cp < 0x10000) + { + text += static_cast(0xE0 | (cp >> 12)); + text += static_cast(0x80 | ((cp >> 6) & 0x3F)); + text += static_cast(0x80 | (cp & 0x3F)); + } + else + { + text += static_cast(0xF0 | (cp >> 18)); + text += static_cast(0x80 | ((cp >> 12) & 0x3F)); + text += static_cast(0x80 | ((cp >> 6) & 0x3F)); + text += static_cast(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 comparison") +{ + SECTION("equality of the values parse() produces") + { + generator g; + std::vector 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() == i); + CHECK(v.contains(key)); + CHECK(v.find(key).key() == key); + CHECK(v.at(key).get() == i); + CHECK(!v.contains(key + "x")); + } + CHECK(v[""].get_string() == "empty key"); + CHECK(v["k1"].get() == 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" 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 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 triple = {{3, 17, n - 1}}; + const std::array twice = {{5, n / 2}}; + std::vector ordered = keys; + for (const std::size_t i : triple) + { + ordered.insert(ordered.begin() + static_cast(ordered.size() / 2), keys[i]); + } + for (const std::size_t i : twice) + { + ordered.insert(ordered.begin() + static_cast(ordered.size() / 3), keys[i]); + } + for (const std::size_t i : triple) + { + ordered.push_back(keys[i]); + } + std::vector 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 members = make_members(total); + REQUIRE(members.size() >= total); + REQUIRE(members.size() >= 100); + std::string text = "{"; + std::map first; + std::map last; + std::map 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() == expected); + CHECK(v.at(entry.first).get() == expected); + CHECK(v.find(entry.first).value().get() == 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() == expected); + CHECK(v.at(json::json_pointer(pointer)).get() == 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() == last.at(entry.first)); + CHECK(m[entry.first].get() == 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 colliding; + std::vector 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('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& 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() == i % 10); + CHECK(v.at((*pair)[i]).get() == 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& keys) + { + std::size_t slots = 16; + while (slots < 2 * keys.size()) + { + slots *= 2; + } + std::vector used(slots, false); + std::size_t farthest = 0; + for (const std::string& key : keys) + { + std::size_t i = nlohmann::detail::conditional_static_cast(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(pattern[conversion] - '0'); + ++conversion; + } + const bool hex = pattern[conversion] == 'x'; + std::vector keys; + for (int i = 0; i < count; ++i) + { + std::ostringstream key; + key << pattern.substr(0, percent) << std::setfill('0') << std::setw(static_cast(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 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('a' + (rng() % 26)); + } + distinct.insert(key); + } + CHECK(max_displacement(std::vector(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 keys; + std::set 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('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() == i); + CHECK(v.at(keys[i]).get() == 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(object)]; + for (int i = 0, n = 150 + object; i < n; ++i) + { + CHECK(v[std::to_string(i)].get() == 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() == 299); + CHECK(d.root()[1]["x"]["m150"].get() == 150); + CHECK(d.root()[2]["m0"].get() == 0); + const std::size_t with_index = d.memory_usage(); + d.read(std::string("{\"small\": 1}")); + CHECK(d.root()["small"].get() == 1); + d.read(text); + CHECK(d.root()[2]["m7"].get() == 7); + CHECK(d.memory_usage() >= with_index / 2); + } +} diff --git a/tests/src/unit-json_view_edit.cpp b/tests/src/unit-json_view_edit.cpp new file mode 100644 index 000000000..4a6fbe69d --- /dev/null +++ b/tests/src/unit-json_view_edit.cpp @@ -0,0 +1,943 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; +using nlohmann::ordered_json; +using nlohmann::json_document; +using nlohmann::json_editable_document; +using nlohmann::json_editable_view; +using nlohmann::ordered_json_document; +using nlohmann::ordered_json_editable_document; +using nlohmann::ordered_json_editable_view; +using ptr_t = ordered_json::json_pointer; + +#include "json_view_test_helpers.hpp" +#if !defined(JSON_NOEXCEPTION) + using json_view_test::exception_of_call; +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +std::uint32_t rng() +{ + static std::mt19937 generator(5295); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible + // result_type is std::uint_fast32_t, which may be wider than 32 bits + const std::mt19937::result_type value = generator(); + return static_cast(value); +} + +int r(int n) +{ + return static_cast(rng() % static_cast(n)); +} + +int counter = 0; + +std::uint64_t bits(double x) +{ + std::uint64_t b = 0; + std::memcpy(&b, &x, sizeof(b)); + return b; +} + +std::string random_string() +{ + static const std::array pieces = {{"a", "Z", " ", "~", "\n", "\"", "\\", "/", "\xc3\xa9", "\xe3\x81\x82", "\xf0\x9f\x98\x80", "\x7f", "\x1f", "0", "key"}}; + std::string s; + for (int i = r(3) == 0 ? r(30) : r(6); i > 0; --i) + { + s += pieces[static_cast(r(15))]; + } + return s; +} + +ordered_json random_scalar() +{ + switch (r(9)) + { + case 0: + return nullptr; + case 1: + return r(2) == 0; + case 2: + return static_cast(rng()) - 2147483648LL; + case 3: + return (static_cast(rng()) * 4294967296ULL) + rng(); + case 4: + return static_cast(static_cast(rng())) / (1 + r(1000)); + case 5: + return r(2) == 0 ? 1e300 * (r(2) == 0 ? 1 : -1) : 5e-324; + case 6: + return -0.0; + default: + return random_string(); + } +} + +ordered_json random_value(int depth) +{ + const int k = depth > 3 ? 4 : r(7); + if (k == 0) + { + ordered_json o = ordered_json::object(); + for (int i = r(5); i > 0; --i) + { + o[random_string() + "#" + std::to_string(counter++)] = random_value(depth + 1); + } + return o; + } + if (k == 1) + { + ordered_json a = ordered_json::array(); + for (int i = r(5); i > 0; --i) + { + a.push_back(random_value(depth + 1)); + } + return a; + } + return random_scalar(); +} + +void collect(const ordered_json& j, const ptr_t& p, std::vector& out) +{ + out.push_back(p); + if (j.is_object()) + { + for (const auto& kv : j.items()) + { + collect(kv.value(), p / kv.key(), out); + } + } + else if (j.is_array()) + { + for (std::size_t i = 0; i < j.size(); ++i) + { + collect(j[i], p / i, out); + } + } +} + + +// the edited view and the ordered_json value read the same through the whole +// read API +void compare(const ordered_json_editable_view& v, const ordered_json& j) +{ + REQUIRE(v.type() == j.type()); + REQUIRE(v.size() == j.size()); + if (j.is_object()) + { + auto jt = j.begin(); + for (auto it = v.begin(); it != v.end(); ++it, ++jt) + { + CHECK(std::string(it.key().data(), it.key().size()) == jt.key()); + CHECK(v[jt.key()].dump() == jt.value().dump()); + CHECK(v.contains(jt.key())); + compare(*it, jt.value()); + } + } + else if (j.is_array()) + { + for (std::size_t i = 0; i < j.size(); ++i) + { + CHECK(v[i].dump() == j[i].dump()); + } + std::size_t i = 0; + for (const auto e : v) + { + compare(e, j[i++]); + } + if (!j.empty()) + { + CHECK(v.back().dump() == j.back().dump()); + } + } + else if (j.is_string()) + { + CHECK(v.get() == j.get()); + } + else if (j.is_number_float()) + { + CHECK(bits(v.get()) == bits(j.get())); + } + else if (j.is_number_integer()) + { + CHECK(v.get() == j.get()); + CHECK(v.get() == j.get()); + } +} + +// the text of j, in which some objects repeat a key of theirs after their +// members: lookups of the edited document find the first member of a key, so +// j (the first members) is what they must agree with, while parse() would +// keep the last value +std::string text_with_duplicates(const ordered_json& j) +{ + if (j.is_object()) + { + std::vector keys; + for (const auto& kv : j.items()) + { + keys.push_back(kv.key()); + } + const auto repeated = [&keys]() + { + return ordered_json(keys[static_cast(r(static_cast(keys.size())))]).dump() + ":" + random_value(2).dump(); + }; + std::string text = "{"; + bool first = true; + for (const auto& kv : j.items()) + { + text += (first ? "" : ",") + ordered_json(kv.key()).dump() + ":" + text_with_duplicates(kv.value()); + first = false; + } + for (int i = keys.empty() ? 0 : r(3); i > 0; --i) + { + text += "," + repeated(); + } + return text + "}"; + } + if (j.is_array()) + { + std::string text = "["; + for (std::size_t i = 0; i < j.size(); ++i) + { + text += (i != 0 ? "," : "") + text_with_duplicates(j[i]); + } + return text + "]"; + } + return j.dump(); +} + +// the number of members of an object view with this key, by iteration +std::size_t occurrences(const ordered_json_editable_view& v, const std::string& key) +{ + std::size_t result = 0; + for (const auto& item : v.items()) + { + result += std::string(item.key().data(), item.key().size()) == key ? 1u : 0u; + } + return result; +} + +// every lookup of the edited view finds the first member of its key, which +// the iteration of the view shows +void check_lookups(const ordered_json_editable_view& v) +{ + if (v.is_object()) + { + std::set seen; + for (const auto item : v.items()) + { + const std::string key(item.key().data(), item.key().size()); + if (!seen.insert(key).second) + { + continue; // (not the first member of the key) + } + const ptr_t ptr = ptr_t() / key; + CAPTURE(key) + const ordered_json expected = item.value().materialize(); + const ordered_json_editable_view m = v[key]; + REQUIRE(!m.is_discarded()); + CHECK(m.materialize() == expected); + CHECK(v.at(key).materialize() == expected); + CHECK(v[ptr].materialize() == expected); + CHECK(v.at(ptr).materialize() == expected); + CHECK(v.contains(key)); + CHECK(v.contains(ptr)); + CHECK(v.count(key) == occurrences(v, key)); + const auto it = v.find(key); + REQUIRE(it != v.end()); + CHECK((*it).materialize() == expected); + CHECK(v.value(ptr, ordered_json(nullptr)) == expected); + if (expected.is_string()) + { + CHECK(v.value(key, std::string("-")) == expected.get()); + } + check_lookups(m); + } + CHECK(v["missing#key"].is_discarded()); + CHECK(!v.contains("missing#key")); + CHECK(v.find("missing#key") == v.end()); + } + else if (v.is_array()) + { + for (std::size_t i = 0; i < v.size(); ++i) // NOLINT(modernize-loop-convert): both v[i] and v.at(i) are tested + { + check_lookups(v[i]); + check_lookups(v.at(i)); + } + } +} + +// every lookup of the edited view finds what the first members of j hold +void check_first_members(const ordered_json_editable_view& v, const ordered_json& j) +{ + REQUIRE(v.type() == j.type()); + if (j.is_object()) + { + for (const auto& kv : j.items()) + { + const std::string& key = kv.key(); + const ptr_t ptr = ptr_t() / key; + CAPTURE(key) + REQUIRE(v.contains(key)); + CHECK(v.contains(ptr)); + CHECK(v.count(key) == occurrences(v, key)); + CHECK(v.find(key) != v.end()); + CHECK(v.at(ptr).type() == kv.value().type()); + check_first_members(v[key], kv.value()); + check_first_members(v.at(key), kv.value()); + check_first_members(v[ptr], kv.value()); + } + CHECK(!v.contains("missing#key")); + } + else if (j.is_array()) + { + REQUIRE(v.size() == j.size()); + for (std::size_t i = 0; i < j.size(); ++i) + { + check_first_members(v[i], j[i]); + } + } + else + { + CHECK(v.materialize() == j); + } +} + +// (for documents whose objects repeat keys: j holds the first members, the +// lookups must find them; dump(), size(), and iteration list all members, but +// materialize() keeps the last value of a key, as parse() does of the dump) +void check_duplicates(const ordered_json_editable_document& d, const ordered_json& j) +{ + check_first_members(d.root(), j); + check_lookups(d.root()); + CHECK(d.root().materialize() == ordered_json::parse(d.root().dump())); +} + +void check_all(const ordered_json_editable_document& d, const ordered_json& j, bool deep) +{ + const std::string text = d.root().dump(); + REQUIRE(text == j.dump()); + CHECK(d.root().materialize() == j); + if (deep) + { + CHECK(d.root().dump(2) == j.dump(2)); + CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true)); + CHECK(d.root() == j); + compare(d.root(), j); + // a fresh document of the text reads the same + const ordered_json_document fresh = ordered_json_document::parse(text); + CHECK(fresh.root() == d.root()); + } +} +} // namespace + +TEST_CASE("json_view edits: differential") +{ + // random edits are applied to an ordered_json_editable_document and to the + // ordered_json parse() produces; after every edit both must serialize, + // materialize, and read back the same + for (int n = 0; n < 250; ++n) + { + // the last 100 documents repeat keys in some of their objects; j then + // holds the first member of each key, which the edits address + const bool duplicates = n >= 150; + ordered_json j = random_value(0); + if (r(4) == 0) + { + j = ordered_json::object({{"a", random_value(1)}, {"b", random_value(1)}}); + } + if (duplicates) + { + j = ordered_json::parse(j.dump()); // (normalized as the document reads it) + } + const std::string text = duplicates ? text_with_duplicates(j) : j.dump(r(2) == 0 ? -1 : 2); + CAPTURE(text) + ordered_json_editable_document d = ordered_json_editable_document::parse(text); + if (!duplicates) + { + j = ordered_json::parse(text); + } + const ordered_json_document other = ordered_json_document::parse(random_value(0).dump()); + const int edits = 30; + for (int e = 0; e < edits; ++e) + { + std::vector paths; + collect(j, ptr_t(), paths); + const ptr_t p = paths[static_cast(r(static_cast(paths.size())))]; + const ordered_json& target = j[p]; + const ordered_json_editable_view tv = d.root().at(p); + const int op = r(12); + { + if (op == 0) // assign a scalar + { + const ordered_json v = random_scalar(); + if (v.is_string() && r(2) == 0) + { + d.set(tv, v.get()); + } + else if (v.is_number_unsigned() && r(2) == 0) + { + d.set(tv, v.get()); + } + else + { + d.set(tv, v); + } + j[p] = v; + } + else if (op == 1) // assign a new array/object (or anything), sometimes via the pointer API + { + const ordered_json v = random_value(2); + if (r(2) == 0) + { + d.set(p, v); + } + else + { + d.set(tv, v); + } + j[p] = v; + } + else if (op == 2) // copy a value of the same document + { + const ptr_t& q = paths[static_cast(r(static_cast(paths.size())))]; + const ordered_json v = j[q]; + d.set(tv, d.root().at(q)); + j[p] = v; + } + else if (op == 3) // copy a value of another document + { + d.set(tv, other.root()); + j[p] = other.root().materialize(); + } + else if ((op == 4 || op == 5) && target.is_object()) // set a member (new or existing) + { + std::string key = random_string() + "#" + std::to_string(counter++); + if (op == 5 && !target.empty()) + { + key = std::next(target.begin(), r(static_cast(target.size()))).key(); + } + const ordered_json v = random_value(2); + d.set(tv, key, v); + j[p][key] = v; + } + else if (op == 6 && target.is_object() && !target.empty()) // erase a member + { + const std::string key = std::next(target.begin(), r(static_cast(target.size()))).key(); + if (r(2) == 0) + { + d.erase(tv, key); + } + else + { + d.erase(p / key); + } + j[p].erase(key); + } + else if (op == 7 && (target.is_array() || target.is_null())) // push_back + { + const ordered_json v = random_value(2); + d.push_back(tv, v); + j[p].push_back(v); + } + else if (op == 8 && target.is_array()) // insert + { + const auto i = static_cast(r(static_cast(target.size()) + 1)); + const ordered_json v = random_value(2); + d.insert(tv, i, v); + j[p].insert(j[p].begin() + static_cast(i), v); + } + else if (op == 9 && target.is_array() && !target.empty()) // erase an element + { + const auto i = static_cast(r(static_cast(target.size()))); + if (r(2) == 0) + { + d.erase(tv, i); + } + else + { + d.erase(p / i); + } + j[p].erase(i); + } + else if (op == 10 && target.is_array() && !target.empty()) // assign an element + { + const auto i = static_cast(r(static_cast(target.size()))); + const ordered_json v = random_value(2); + d.set(tv, i, v); + j[p][i] = v; + } + else if (op == 11) // a held view sees the assignment + { + const ordered_json v = random_value(2); + const ordered_json_editable_view held = d.root().at(p); + d.set(p, v); + j[p] = v; + CHECK(held.dump() == v.dump()); + } + else + { + continue; + } + } + CAPTURE(p.to_string()) + CAPTURE(op) + if (duplicates) + { + check_duplicates(d, j); + } + else + { + check_all(d, j, e % 8 == 7 || e == edits - 1); + } + } + } +} + +TEST_CASE("json_view edits: errors") +{ + SECTION("an empty document") + { + json_editable_document d; + CHECK_THROWS_WITH_AS(d.set(d.root(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&); + } + + json_editable_document d = json_editable_document::parse(R"({"o": {"a": 1}, "a": [1, 2], "n": 1, "z": null})"); + const json_editable_view root = d.root(); + const json_document other = json_document::parse("[1]"); + json_editable_document other_editable = json_editable_document::parse("[1]"); + + 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(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["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["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"], -1, 1), "[json.exception.out_of_range.401] array index -1 is out of range", 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.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["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.erase(root["n"], "k"), "[json.exception.type_error.307] cannot use erase() with number", 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["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(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.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::binary({1, 2})), "[json.exception.type_error.319] cannot store a binary value in a json_document", json::type_error&); + +#if !defined(JSON_NOEXCEPTION) + // invalid UTF-8 is rejected when it enters the document, with the error + // basic_json::dump() reports for the same string + for (const std::string bad : + {"\xC3\x28", "a\xE2\x28\xA1", "\xF0\x28\x8C\xBC", "\xE2\x82", "x\xF0\x9F\x98", "\xFF", "\xED\xA0\x80", "\xC0\xAF" + }) + { + CAPTURE(bad) + const std::string expected = exception_of_call([&] + { + const std::string text = json(bad).dump(); + static_cast(text); + }); + CHECK(!expected.empty()); + CHECK(exception_of_call([&] { d.set(root["z"], bad); }) == expected); + CHECK(exception_of_call([&] { d.set(root["o"], bad, 1); }) == expected); + CHECK(exception_of_call([&] { d.set(root["z"], json{{"k", bad}}); }) == expected); + } +#endif + // nothing of the failed edits is visible + CHECK(root.dump() == R"({"o":{"a":1},"a":[1,2],"n":1,"z":null})"); + static_cast(other); +} + +#if !defined(JSON_NOEXCEPTION) +TEST_CASE("json_view edits: the size of a text arena") +{ + using nlohmann::detail::view::text_capacity; + constexpr std::size_t limit = 0xFFFFFFFFu; + // grows by doubling, or to what is needed (plus some room) + CHECK(text_capacity(0, 0, 10) == 266); + CHECK(text_capacity(1000, 990, 20) == 2000); + CHECK(text_capacity(100, 100, 5000) == 5356); + // an arena beyond 2 GiB: doubling is clamped to 4 GiB - 1 + CHECK(text_capacity(0x90000000u, 0x8FFFFFFFu, 2) == limit); + CHECK(text_capacity(limit, limit - 10, 10) == limit); + // exactly what fits is accepted, without room to spare + CHECK(text_capacity(100, 90, limit - 90) == limit); + CHECK(text_capacity(limit - 100, limit - 100, 100) == limit); + // what does not fit is an error + CHECK_THROWS_WITH_AS(text_capacity(100, 90, limit - 89), "[json.exception.out_of_range.416] edits of 4 GiB or more are not supported by json_document", json::out_of_range&); + CHECK_THROWS_WITH_AS(text_capacity(limit, limit, 1), "[json.exception.out_of_range.416] edits of 4 GiB or more are not supported by json_document", json::out_of_range&); +} +#endif + +TEST_CASE("json_view edits: the chunks of new nodes") +{ + using nlohmann::detail::view::alloc_nodes; + using nlohmann::detail::view::document_data; + const std::unique_ptr d(document_data::create(0)); + // the first chunk has room for 64 nodes, the next one for 128 + const nlohmann::detail::view::node* const first = alloc_nodes(*d, 10); + CHECK(d->edits->chunks.size() == 1); + // a request of half of the next chunk (128 nodes) or more gets a chunk of + // its own + const nlohmann::detail::view::node* const large = alloc_nodes(*d, 100); + CHECK(d->edits->chunks.size() == 2); + CHECK(large == d->edits->chunks.back().get()); + // and the rest of the first chunk is still used + CHECK(alloc_nodes(*d, 50) == first + 10); + CHECK(d->edits->chunks.size() == 2); + // a smaller request that does not fit starts a new (doubled) chunk; the + // call is a statement of its own, as the operands of == are unsequenced + // and MSVC reads chunks.back() before the call adds the chunk + const nlohmann::detail::view::node* const doubled = alloc_nodes(*d, 10); + CHECK(doubled == d->edits->chunks.back().get()); + CHECK(d->edits->chunks.size() == 3); + CHECK(d->edits->chunk_end - d->edits->chunk_cur == 128 - 10); + CHECK(d->edits->bytes == (64 + 100 + 128) * sizeof(nlohmann::detail::view::node)); +} + +TEST_CASE("json_view edits: replacing arrays and objects by scalars") +{ + // the first assignment switches the parent to links; the later ones do + // not need to look for the parent again + std::string text = "["; + json expected = json::array(); + for (int i = 0; i < 300; ++i) + { + text += (i != 0 ? ",[" : "[") + std::to_string(i) + ",{\"k\":" + std::to_string(i) + "}]"; + expected.push_back(json::array({i, json{{"k", i}}})); + } + text += ']'; + json_editable_document d = json_editable_document::parse(text); + CHECK(d.root().dump() == expected.dump()); + for (int i = 0; i < 300; i += 2) + { + d.set(d.root()[static_cast(i)], i); + expected[static_cast(i)] = i; + } + CHECK(d.root().dump() == expected.dump()); + for (int i = 1; i < 300; i += 2) // (elements that are still in the parsed layout of their parent) + { + d.set(d.root()[static_cast(i)][1], "x"); // replaces an object + expected[static_cast(i)][1] = "x"; + } + CHECK(d.root().dump() == expected.dump()); + + // new values, and values in new values + d.push_back(d.root(), json::parse(R"([[1,2],{"a":[3]}])")); + expected.push_back(json::parse(R"([[1,2],{"a":[3]}])")); + d.set(d.root()[300][0], 7); + expected[300][0] = 7; + d.insert(d.root(), 0, json::array({1, 2})); + expected.insert(expected.begin(), json::array({1, 2})); + d.set(d.root()[0], nullptr); + expected[0] = nullptr; + d.set(d.root()[301], 5); + expected[301] = 5; + CHECK(d.root().dump() == expected.dump()); + CHECK(d.root().materialize() == expected); +} + +TEST_CASE("json_view edits: arrays whose elements have no subtrees") +{ + // such an array is indexed directly while it is in its parsed layout; edits + // that replace elements by scalars keep it, others move it + json_editable_document d = json_editable_document::parse("[10,20,30,40]"); + json expected = json::array({10, 20, 30, 40}); + const auto check = [&] + { + CHECK(d.root().size() == expected.size()); + for (std::size_t i = 0; i < expected.size(); ++i) + { + CHECK(d.root()[i].materialize() == expected[i]); + CHECK(d.root().at(i).materialize() == expected[i]); + } + CHECK(d.root()[expected.size()].is_discarded()); + CHECK(d.root().back().materialize() == expected.back()); + CHECK(d.root().dump() == expected.dump()); + }; + check(); + d.set(d.root()[1], "x"); + expected[1] = "x"; + check(); + d.set(d.root()[3], 2.5); + expected[3] = 2.5; + check(); + d.set(d.root()[2], json::array({1, 2})); + expected[2] = json::array({1, 2}); + check(); + d.push_back(d.root(), 50); + expected.push_back(50); + check(); + d.insert(d.root(), 0, true); + expected.insert(expected.begin(), true); + check(); + d.erase(d.root(), 1); + expected.erase(expected.begin() + 1); + check(); + d.set(d.root()[2], nullptr); + expected[2] = nullptr; + check(); +} + +TEST_CASE("json_view edits: materializing values nested deeper than the document was parsed") +{ + // the stack of open containers of materialize() is sized from the nesting of the parsed text; + // a value added by an edit can be deeper than that (and than the stack that is local to the call) + json_editable_document d = json_editable_document::parse("[1,[2]]"); + json expected = json::array({1, json::array({2})}); + json nested = 3; + for (int level = 0; level < 40; ++level) + { + nested = json::array({nested, json{{"k", level}}}); + } + d.push_back(d.root(), nested); + expected.push_back(nested); + CHECK(d.root().materialize() == expected); + CHECK(d.root()[2].materialize() == nested); + d.set(d.root()[1][0], nested); + expected[1][0] = nested; + CHECK(d.root().materialize() == expected); + CHECK(d.root().get() == expected); +} + +TEST_CASE("json_view edits: an editable document before and after its first edit") +{ + // until the first edit, an editable document is read like a read-only one; views made before the edit + // keep working after it + const std::string text = R"({"a":[1,2,{"x":"y"},4],"b":{"c":true,"d":null},"e":"text","f":[]})"; + const json_document read_only = json_document::parse(text); + json_editable_document d = json_editable_document::parse(text); + const auto root = d.root(); + const auto same_as_read_only = [&] + { + CHECK(root.dump() == read_only.root().dump()); + CHECK(root.dump(2) == read_only.root().dump(2)); + CHECK(root.materialize() == read_only.root().materialize()); + CHECK(root["a"][2]["x"].get_string() == "y"); + CHECK(root["a"].at(3).get() == 4); + CHECK(root["a"].back().get() == 4); + CHECK(root.contains("b")); + CHECK(!root.contains("zz")); + CHECK(root.find("e")->get_string() == "text"); + CHECK(root.find("zz") == root.end()); + CHECK(root["b"]["c"].get()); + CHECK(root.value("e", "none") == "text"); + std::size_t count = 0; + for (const auto m : root["a"]) + { + CHECK(m.materialize() == read_only.root()["a"][count].materialize()); + ++count; + } + CHECK(count == 4); + }; + same_as_read_only(); + // an edit that changes a scalar in place, then one that moves an array + d.set(root["e"], "other"); + CHECK(root["e"].get_string() == "other"); + CHECK(root["a"][2]["x"].get_string() == "y"); + CHECK(root["a"].back().get() == 4); + d.push_back(root["f"], 7); + CHECK(root["f"][0].get() == 7); + CHECK(root["f"].back().get() == 7); + CHECK(root.dump() == R"({"a":[1,2,{"x":"y"},4],"b":{"c":true,"d":null},"e":"other","f":[7]})"); + d.set(root["a"], 2, json::array({1, 2})); + CHECK(root["a"][2][1].get() == 2); + CHECK(root["a"].dump() == "[1,2,[1,2],4]"); + CHECK(root.materialize() == json::parse(R"({"a":[1,2,[1,2],4],"b":{"c":true,"d":null},"e":"other","f":[7]})")); +} + +namespace +{ +// reads of "a" in an object that repeats it: the first member wins, however the +// object is stored +template +void check_first_wins(const View& o, int expected) +{ + CAPTURE(expected) + CHECK(o["a"].template get() == expected); + CHECK(o.at("a").template get() == expected); + CHECK(o.find("a")->template get() == expected); + CHECK(o.value("a", -1) == expected); + CHECK(o[json::json_pointer("/a")].template get() == expected); + CHECK(o.at(json::json_pointer("/a")).template get() == expected); + CHECK(o.value(json::json_pointer("/a"), -1) == expected); + CHECK(o.contains("a")); + CHECK(o.contains(json::json_pointer("/a"))); + std::size_t members_a = 0; + for (const auto& item : o.items()) + { + members_a += std::string(item.key().data(), item.key().size()) == "a" ? 1u : 0u; + } + CHECK(o.count("a") == members_a); // every member, unlike the lookups +} +} // namespace + +TEST_CASE("json_view edits: duplicate keys") +{ + SECTION("an object in its parsed layout, then moved by edits") + { + json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})"); + check_first_wins(d.root(), 1); + CHECK(d.root().size() == 3); + CHECK(d.root().dump() == R"({"a":1,"b":2,"a":3})"); + CHECK(d.root().materialize()["a"] == 3); // (parse() keeps the last value) + + // an edit of a value does not move the object + d.set(d.root()["b"], 5); + check_first_wins(d.root(), 1); + d.set(d.root()["a"], 4); // the member that lookups find + check_first_wins(d.root(), 4); + CHECK(d.root().dump() == R"({"a":4,"b":5,"a":3})"); + + // an appended member moves it + d.set(d.root(), "c", true); + check_first_wins(d.root(), 4); + CHECK(d.root().size() == 4); + CHECK(d.root().dump() == R"({"a":4,"b":5,"a":3,"c":true})"); + d.set(d.root()["a"], 6); + check_first_wins(d.root(), 6); + CHECK(d.root().dump() == R"({"a":6,"b":5,"a":3,"c":true})"); + d.set(json::json_pointer("/a"), 7); + check_first_wins(d.root(), 7); + } + + SECTION("set assigns the member that lookups find") + { + // the key stays where its first occurrence is, the later members are dropped + ordered_json_editable_document d = ordered_json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3, "c": 4, "a": 5})"); + const ordered_json_editable_view held = d.root()["a"]; + CHECK(held.get() == 1); + const ordered_json_editable_view assigned = d.set(d.root(), "a", "x"); + CHECK(held.get() == "x"); + CHECK(assigned.get() == "x"); + CHECK(d.root().dump() == R"({"a":"x","b":2,"c":4})"); + CHECK(d.root().materialize() == ordered_json::parse(R"({"a": "x", "b": 2, "c": 4})")); + CHECK(d.root().size() == 3); + + // as for the object that parse() makes of the text + ordered_json j = ordered_json::parse(R"({"a": 1, "b": 2, "a": 3, "c": 4, "a": 5})"); + j["a"] = "x"; + CHECK(d.root().materialize().dump() == j.dump()); + + // through a pointer, below a duplicate + d = ordered_json_editable_document::parse(R"({"a": {"x": 1}, "b": 2, "a": {"x": 3}})"); + d.set(ptr_t("/a/x"), 4); + CHECK(d.root().dump() == R"({"a":{"x":4},"b":2,"a":{"x":3}})"); + d.set(ptr_t("/a"), 0); + CHECK(d.root().dump() == R"({"a":0,"b":2})"); + } + + SECTION("erase removes every member") + { + json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})"); + d.set(d.root(), "c", 4); + check_first_wins(d.root(), 1); + CHECK(d.erase(d.root(), "a") == 2); + CHECK(d.root().dump() == R"({"b":2,"c":4})"); + CHECK(!d.root().contains("a")); + CHECK(d.root()["a"].is_discarded()); + CHECK(d.erase(d.root(), "a") == 0); + d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})"); + CHECK(d.erase(json::json_pointer("/a")) == 2); + CHECK(d.root().dump() == R"({"b":2})"); + } + + SECTION("a large object with an index") + { + std::string text = R"({"a":0,"k7":"first")"; + for (int i = 0; i < 200; ++i) + { + text += ",\"k" + std::to_string(i) + "\":" + std::to_string(i); + } + text += R"(,"a":1,"k7":"last","a":2})"; + json_editable_document d = json_editable_document::parse(text); + check_first_wins(d.root(), 0); + CHECK(d.root()["k7"].get_string() == "first"); + CHECK(d.root()["k199"].get() == 199); + + // a value assigned in place: the index stays in use + d.set(d.root()["a"], 3); + check_first_wins(d.root(), 3); + d.set(d.root()["k7"], "changed"); + CHECK(d.root()["k7"].get_string() == "changed"); + CHECK(d.root()["k199"].get() == 199); + + // an appended member moves the members: the lookup scans them + d.set(d.root(), "new", 1); + check_first_wins(d.root(), 3); + CHECK(d.root()["k7"].get_string() == "changed"); + CHECK(d.root()["k7"].get_string() == d.root().at("k7").get_string()); + CHECK(d.root()["k199"].get() == 199); + CHECK(d.root()["new"].get() == 1); + + // a value replaced by a container, in an object that was not moved + d = json_editable_document::parse(text); + d.set(d.root()["a"], json{{"x", 1}}); + CHECK(d.root()["a"]["x"].get() == 1); + CHECK(d.root()["k7"].get_string() == "first"); + CHECK(d.erase(d.root(), "k7") == 3); // (the key occurs three times) + CHECK(d.root()["k7"].is_discarded()); + CHECK(d.root()["a"]["x"].get() == 1); + } + + SECTION("objects in moved arrays and in new values") + { + json_editable_document d = json_editable_document::parse(R"([{"a": 1, "a": 2}, {"a": 3, "b": 0, "a": 4}])"); + check_first_wins(d.root()[0], 1); + check_first_wins(d.root()[1], 3); + d.insert(d.root(), 0, json::parse(R"({"a": 0})")); + d.push_back(d.root(), json::parse(R"({"a": 5, "a": 6})")); // (a basic_json value has one member) + check_first_wins(d.root()[1], 1); + check_first_wins(d.root()[2], 3); + CHECK(d.root()[3]["a"].get() == 6); + d.set(d.root()[1], "a", 8); + check_first_wins(d.root()[1], 8); + CHECK(d.root()[1].dump() == R"({"a":8})"); + d.erase(d.root(), 0); + check_first_wins(d.root()[1], 3); + CHECK(d.root().dump() == R"([{"a":8},{"a":3,"b":0,"a":4},{"a":6}])"); + } + + SECTION("values of other documents") + { + const json_document source = json_document::parse(R"({"list": [{"a": 1, "a": 2}], "o": {"b": {"a": 3, "a": 4}, "a": 5, "a": 6}})"); + json_editable_document d = json_editable_document::parse("{}"); + d.set(d.root(), "copy", source.root()["list"]); + d.set(d.root(), "o", source.root()["o"]); + // (the copies keep the repeated members) + CHECK(d.root().dump() == R"({"copy":[{"a":1,"a":2}],"o":{"b":{"a":3,"a":4},"a":5,"a":6}})"); + check_first_wins(d.root()["copy"][0], 1); + check_first_wins(d.root()["o"]["b"], 3); + check_first_wins(d.root()["o"], 5); + d.set(d.root()["o"]["b"], "c", 0); + check_first_wins(d.root()["o"]["b"], 3); + d.set(d.root()["o"], "d", 0); + check_first_wins(d.root()["o"], 5); + CHECK(d.root()["o"]["b"]["a"].get() == 3); + CHECK(d.root()["o"]["d"].get() == 0); + } +} diff --git a/tests/src/unit-json_view_edit_values.cpp b/tests/src/unit-json_view_edit_values.cpp new file mode 100644 index 000000000..59aee3a9d --- /dev/null +++ b/tests/src/unit-json_view_edit_values.cpp @@ -0,0 +1,357 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; +using nlohmann::json_document; +using nlohmann::json_editable_document; +using nlohmann::json_editable_view; + +#include "json_view_test_helpers.hpp" +#if !defined(JSON_NOEXCEPTION) + using json_view_test::exception_of_call; +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// These tests were split off unit-json_view_edit.cpp, whose object file got too +// large for the MinGW linker (see json_view_test_helpers.hpp). + +TEST_CASE("json_view edits: views and values") +{ + SECTION("a value that is no longer part of the document") + { + json_editable_document d = json_editable_document::parse("[[[1,2]]]"); + const json_editable_view inner = d.root()[0][0]; + d.set(d.root()[0], json::array({7})); + d.set(inner, 5); + CHECK(d.root().dump() == "[[7]]"); + CHECK(inner.get() == 5); + } + + SECTION("views keep referring to their value") + { + json_editable_document d = json_editable_document::parse(R"({"a": [10, 20, 30], "b": {"c": "text"}})"); + const json_editable_view a = d.root()["a"]; + const json_editable_view twenty = a[1]; + const json_editable_view c = d.root()["b"]["c"]; + d.insert(a, 0, 5); + d.push_back(a, 40); + CHECK(twenty.get() == 20); + CHECK(a[2].get() == 20); + d.erase(a, 2); + CHECK(twenty.get() == 20); // an erased value keeps its last value + d.set(c, 7); + CHECK(c.get() == 7); // a held view sees an assignment + d.set(d.root()["b"], json::array({1, 2})); + CHECK(d.root()["b"].dump() == "[1,2]"); + CHECK(d.root().dump() == R"({"a":[5,10,30,40],"b":[1,2]})"); + CHECK(d.root()["a"][0].source_offset() == static_cast(-1)); // a new value + CHECK(d.root()["a"][1].source_offset() != static_cast(-1)); + } + + SECTION("strings stay valid while more edits come") + { + json_editable_document d = json_editable_document::parse("[]"); + const auto first = d.push_back(d.root(), std::string(100, 'x')).get_string(); + for (int i = 0; i < 1000; ++i) + { + d.push_back(d.root(), std::string(static_cast(i % 50), 'y')); + } + CHECK(std::string(first.data(), first.size()) == std::string(100, 'x')); + CHECK(d.root().size() == 1001); + } + + SECTION("numbers") + { + json_editable_document d = json_editable_document::parse(R"([1.50, 1E2, 3])"); + d.set(d.root(), 2, 0.1); + d.push_back(d.root(), std::numeric_limits::quiet_NaN()); + d.push_back(d.root(), -std::numeric_limits::infinity()); + d.push_back(d.root(), (std::numeric_limits::max)()); + d.push_back(d.root(), (std::numeric_limits::min)()); + CHECK(d.root().dump() == "[1.5,100.0,0.1,null,null,18446744073709551615,-9223372036854775808]"); + CHECK(d.root().dump(-1, ' ', false, json_editable_view::number_format::source) == "[1.50,1E2,0.1,null,null,18446744073709551615,-9223372036854775808]"); + CHECK(std::isnan(d.root()[3].get())); + CHECK(std::isinf(d.root()[4].get())); + CHECK(d.root()[2].number_token() == "0.1"); + CHECK(d.root()[5].get() == 18446744073709551615u); + CHECK(d.root()[6].number_token() == "-9223372036854775808"); + CHECK(d.root().materialize().dump() == json::parse(R"([1.5, 100.0, 0.1, null, null, 18446744073709551615, -9223372036854775808])").dump()); + } + + SECTION("numbers of other float types") + { + // doubles have their own path to the output; other float types are + // written as basic_json writes them, non-finite values as null + using json_float = nlohmann::basic_json; + using document_float = nlohmann::basic_json_document; + document_float d = document_float::parse("[1.5]"); + d.push_back(d.root(), std::numeric_limits::quiet_NaN()); + d.push_back(d.root(), -std::numeric_limits::infinity()); + CHECK(d.root().dump() == "[1.5,null,null]"); + CHECK(d.root().dump(2) == json_float::parse("[1.5, null, null]").dump(2)); + } + + SECTION("nulls become containers, and the root can be replaced") + { + json_editable_document d = json_editable_document::parse("[null, null]"); + d.set(d.root()[0], "k", 1); + d.push_back(d.root()[1], true); + CHECK(d.root().dump() == R"([{"k":1},[true]])"); + d.set(d.root(), "scalar"); + CHECK(d.root().dump() == R"("scalar")"); + d.set(d.root(), json{{"x", {1, 2}}}); + d.set(json::json_pointer("/x/-"), 3); + d.set(json::json_pointer("/x/3"), 4); // the size of the array appends too + d.set(json::json_pointer("/y"), false); + CHECK(d.root().dump() == R"({"x":[1,2,3,4],"y":false})"); + CHECK(d.erase(json::json_pointer("/x/0")) == 1); + CHECK(d.erase(json::json_pointer("/y")) == 1); + CHECK(d.erase(json::json_pointer("/nothing")) == 0); + CHECK(d.root().dump() == R"({"x":[2,3,4]})"); + } + + SECTION("duplicate keys") + { + json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})"); + d.set(d.root(), "a", 4); // the first member is assigned, the others dropped + CHECK(d.root().dump() == R"({"a":4,"b":2})"); + d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})"); + CHECK(d.erase(d.root(), "a") == 2); + CHECK(d.root().dump() == R"({"b":2})"); + } + + SECTION("values from other documents") + { + const json_document source = json_document::parse(R"({"list": [1, "two", {"three": 3.5}], "text": "a\nb"})"); + json_editable_document edited = json_editable_document::parse("[0]"); + edited.set(edited.root(), 0, json{{"inner", {1, 2}}}); + json_editable_document d = json_editable_document::parse("{}"); + d.set(d.root(), "copy", source.root()["list"]); + d.set(d.root(), "text", source.root()["text"]); + d.set(d.root(), "edited", edited.root()[0]); + d.set(d.root(), "self", d.root()["copy"]); + // (a raw string with a backslash must not be a macro argument: MSVC C2017) + const std::string expected = R"({"copy":[1,"two",{"three":3.5}],"text":"a\nb","edited":{"inner":[1,2]},"self":[1,"two",{"three":3.5}]})"; + CHECK(d.root().dump() == expected); + CHECK(d.root()["copy"] == source.root()["list"]); + CHECK(source.root()["list"] == d.root()["self"]); + CHECK(d.root() != source.root()); + } + + SECTION("large objects") + { + std::string text = "{"; + for (int i = 0; i < 200; ++i) + { + text += (i != 0 ? ",\"k" : "\"k") + std::to_string(i) + "\":" + std::to_string(i); + } + text += '}'; + json_editable_document d = json_editable_document::parse(text); + d.set(d.root(), "k7", "seven"); // assigned in place: the index stays in use + CHECK(d.root()["k7"].get_string() == "seven"); + d.set(d.root(), "new", 1); // appended: the members move, the lookup is linear + CHECK(d.root()["new"].get() == 1); + CHECK(d.root()["k199"].get() == 199); + d.erase(d.root(), "k0"); + CHECK(!d.root().contains("k0")); + CHECK(d.root().size() == 200); + } + + SECTION("reuse and memory") + { + json_editable_document d = json_editable_document::parse("[1, 2, 3]"); + const std::size_t before = d.memory_usage(); + for (int i = 0; i < 100; ++i) + { + d.push_back(d.root(), "some text"); + } + CHECK(d.memory_usage() > before); + const json_editable_view first = d.root()[0]; + d.shrink_to_fit(); // (with edits, the index stays in place) + CHECK(first.get() == 1); + d.read(std::string("[true]")); + CHECK(d.root().dump() == "[true]"); + d.push_back(d.root(), false); + CHECK(d.root().dump() == "[true,false]"); + } +} + +TEST_CASE("json_view edits: deeply nested values") +{ + // copying a value into a document must not recurse per nesting level + const std::size_t depth = 100000; + const std::string brackets = std::string(depth, '[') + std::string(depth, ']'); + std::string braces; + for (std::size_t i = 0; i < depth; ++i) + { + braces += "{\"a\":"; + } + braces += '1'; + braces += std::string(depth, '}'); + + SECTION("a view of a read-only document") + { + const json_document source = json_document::parse(brackets); + json_editable_document d = json_editable_document::parse("[]"); + d.push_back(d.root(), source.root()); + CHECK(d.root().dump() == "[" + brackets + "]"); + } + + SECTION("a view of an editable document") + { + const json_editable_document source = json_editable_document::parse(braces); + json_editable_document d = json_editable_document::parse("{}"); + d.set(d.root(), "deep", source.root()); + CHECK(d.root().dump() == "{\"deep\":" + braces + "}"); + } + + SECTION("a view of an edited document (values behind links)") + { + const json_document source = json_document::parse(brackets); + json_editable_document edited = json_editable_document::parse("[[]]"); + edited.push_back(edited.root()[0], source.root()); + edited.push_back(edited.root(), source.root()); + json_editable_document d = json_editable_document::parse("null"); + d.set(d.root(), edited.root()); + CHECK(d.root().dump() == "[[" + brackets + "]," + brackets + "]"); + } + + SECTION("a basic_json value") + { + json deep = json::array(); + json* inner = &deep; + for (std::size_t i = 1; i < depth; ++i) + { + inner->push_back(json::array()); + inner = &inner->back(); + } + json_editable_document d = json_editable_document::parse("[]"); + d.push_back(d.root(), deep); + CHECK(d.root().dump() == "[" + brackets + "]"); + } + + SECTION("a basic_json value with objects") + { + json deep = 1; + for (std::size_t i = 0; i < depth; ++i) + { + json outer = json::object(); + outer["a"] = std::move(deep); + deep = std::move(outer); + } + json_editable_document d = json_editable_document::parse("{}"); + d.set(d.root(), "deep", deep); + CHECK(d.root().dump() == "{\"deep\":" + braces + "}"); + } +} + +#if !defined(JSON_NOEXCEPTION) +TEST_CASE("json_view edits: pointers below a null value") +{ + // a null value on the way becomes what basic_json makes of it: an array + // for "-" and for digits, an object otherwise + struct test_case + { + const char* document; + const char* pointer; + }; + const std::array cases = + { + { + {R"({"a":null})", "/a/0"}, + {R"({"a":null})", "/a/-"}, + {R"({"a":null})", "/a/3"}, + {R"({"a":null})", "/a/x"}, + {R"({"a":null})", "/a/+1"}, + {R"({"a":null})", "/a/01"}, + {R"({"a":null})", "/a/"}, + {R"({"a":{"b":null}})", "/a/b/1"}, + {R"({"a":{"b":null}})", "/a/b/-"}, + {R"({"a":[null]})", "/a/0/0"}, + {R"({"a":[null,null]})", "/a/1/k"}, + {R"([null])", "/0"}, + {"null", "/0"}, + {"null", "/-"}, + {"null", "/k"}, + {"null", ""}, + } + }; + for (const test_case& c : cases) + { + CAPTURE(c.document) + CAPTURE(c.pointer) + json expected = json::parse(c.document); + const std::string error = exception_of_call([&] + { + expected[json::json_pointer(c.pointer)] = 1; + }); + json_editable_document d = json_editable_document::parse(c.document); + if (error.empty()) + { + d.set(json::json_pointer(c.pointer), 1); + CHECK(d.root().dump() == expected.dump()); + CHECK(d.root().materialize() == expected); + } + else + { + // the same error, and the document is not changed + CHECK(exception_of_call([&] { d.set(json::json_pointer(c.pointer), 1); }) == error); + CHECK(d.root().dump() == json::parse(c.document).dump()); + } + } +} + +TEST_CASE("json_view edits: strings of other documents are checked") +{ + // A document borrows the text it was parsed from, and sees later changes + // of the text: a way to get ill-formed UTF-8 into a view. Copying it into + // an editable document is an error, as for any other string. + std::string text = R"({"key":"abc","list":["abc"]})"; + const json_document source = json_document::parse(text); + const auto message_of = [](const std::string & bad) + { + return exception_of_call([&] + { + const std::string dumped = json(bad).dump(); + static_cast(dumped); + }); + }; + json_editable_document d = json_editable_document::parse("[1]"); + d.push_back(d.root(), source.root()); + CHECK(d.root().dump() == R"([1,{"key":"abc","list":["abc"]}])"); + + text[text.find("abc") + 1] = '\xC3'; // "a\xC3c" + text[text.rfind("abc") + 1] = '\xC3'; + const std::string bad_value = message_of(std::string("a\xC3" "c")); + CHECK(!bad_value.empty()); + CHECK(exception_of_call([&] { d.push_back(d.root(), source.root()["key"]); }) == bad_value); + CHECK(exception_of_call([&] { d.set(d.root()[0], source.root()["list"][0]); }) == bad_value); + CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()["list"]); }) == bad_value); + CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()); }) == bad_value); + + text[text.find("key") + 1] = '\xC3'; // a key is checked as well + const std::string bad_key = message_of(std::string("k\xC3" "y")); + CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()); }) == bad_key); + CHECK(exception_of_call([&] { d.push_back(d.root(), source.root()); }) == bad_key); + + // nothing of the failed edits is visible + CHECK(d.root().dump() == R"([1,{"key":"abc","list":["abc"]}])"); +} +#endif diff --git a/tests/src/unit-json_view_image.cpp b/tests/src/unit-json_view_image.cpp new file mode 100644 index 000000000..eaf27eb9f --- /dev/null +++ b/tests/src/unit-json_view_image.cpp @@ -0,0 +1,1282 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; +using nlohmann::ordered_json; +using nlohmann::json_document; +using nlohmann::json_editable_document; +using nlohmann::ordered_json_document; +using nlohmann::ordered_json_editable_document; +using image_check = json_document::image_check; +using nlohmann::detail::view::node; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#if !(defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) + +namespace +{ +#if !defined(JSON_NOEXCEPTION) +std::string exception_of(const std::function& f) +{ + try + { + f(); + } + catch (const json::exception& e) + { + return e.what(); + } + return ""; +} + +const char* const check_failed = "[json.exception.parse_error.116] parse error: invalid json_document image: the check failed"; +#endif + +std::string read_file(const std::string& name) +{ + const std::ifstream f(std::string(TEST_DATA_DIRECTORY) + name, std::ios::binary); + std::stringstream ss; + ss << f.rdbuf(); + return ss.str(); +} + +// the dump of a loaded image, through a named document (root() of a temporary +// document does not compile, and its views would dangle) +std::string loaded_dump(const std::vector& image, image_check check = image_check::full) +{ + const json_document d = json_document::load(image, check); + return d.root().dump(); +} + +// the offsets of the parts of an image +constexpr std::size_t header_size = 64; + +std::uint64_t header_field(const std::vector& image, std::size_t offset) +{ + std::uint64_t v = 0; + std::memcpy(&v, image.data() + offset, sizeof(v)); + return v; +} + +// a header field as a size; the cast is from a variable, which GCC's +// -Wuseless-cast does not flag where std::uint64_t and std::size_t coincide +std::size_t header_size_field(const std::vector& image, std::size_t offset) +{ + const std::uint64_t v = header_field(image, offset); + return static_cast(v); +} + +void set_header_field(std::vector& image, std::size_t offset, std::uint64_t v) +{ + std::memcpy(image.data() + offset, &v, sizeof(v)); +} + +std::size_t node_count(const std::vector& image) +{ + return header_size_field(image, 8); +} + +std::size_t text_at(const std::vector& image) +{ + return header_size + (node_count(image) * sizeof(node)); +} + +node node_at(const std::vector& image, std::size_t i) +{ + node n{}; + std::memcpy(&n, image.data() + header_size + (i * sizeof(node)), sizeof(node)); + return n; +} + +void set_node(std::vector& image, std::size_t i, const node& n) +{ + std::memcpy(image.data() + header_size + (i * sizeof(node)), &n, sizeof(node)); +} + +#if !defined(JSON_NOEXCEPTION) +/// the result of loading an image with a check: "" or the exception message +std::string load_result(const std::vector& image, image_check check) +{ + return exception_of([&] + { + const json_document d = json_document::load(image, check); + static_cast(d); + }); +} + +/// a copy of the image with node i changed by f +template +std::vector corrupted(const std::vector& image, std::size_t i, F f) +{ + std::vector b = image; + node n = node_at(b, i); + f(n); + set_node(b, i, n); + return b; +} +#endif + +/// a document and the documents loaded from its image must be equal +template +void check_round_trip(const Document& d) +{ + const std::vector image = d.save(); + for (const image_check check : + { + image_check::full, image_check::bounds, image_check::none + }) + { + const json_document l = json_document::load(image, check); + CHECK(l.root().dump() == d.root().dump()); + CHECK(l.root().dump(2) == d.root().dump(2)); + CHECK(l.root().materialize() == json(d.root().materialize())); + // an image of a loaded document is the same image + CHECK(l.save() == image); + } + // an editable document can be loaded, too + const ordered_json_editable_document e = ordered_json_editable_document::load(image); + CHECK(e.root().dump() == d.root().dump()); +} + +std::uint32_t rng() +{ + static std::mt19937 generator(5295); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible + // result_type is std::uint_fast32_t, which may be wider than 32 bits + const std::mt19937::result_type value = generator(); + return static_cast(value); +} +} // namespace + +TEST_CASE("json_view images: round trips") +{ + SECTION("small documents") + { + for (const char* text : + { + "null", "true", "false", "0", "-0", "42", "-42", "18446744073709551615", "-9223372036854775808", + "123456789012345678901234567890", "1.5", "-1.25e-300", "1E308", "0.1000000000000000000000000001", + "\"\"", "\"text\"", R"("esc\"aped\n\u00e9\ud83d\ude00")", "\"\xc3\xa9\xe3\x81\x82\"", + "[]", "{}", "[[]]", "[{}]", "{\"\":{}}", + R"({"a": [1, 2.5, "x\ty", true, null, {"b": []}], "c": {"d": -3, "eA": "f"}})", + R"({"k": 1, "k": 2, "l": [], "k": 3})", + " [1 , 2 ] " + }) + { + CAPTURE(text) + // false positive: parse() returns a document with a root + // @infer-ignore NULLPTR_DEREFERENCE + check_round_trip(json_document::parse(text)); + // false positive: parse() returns a document with a root + // @infer-ignore NULLPTR_DEREFERENCE + check_round_trip(ordered_json_document::parse(text)); + } + } + + SECTION("files") + { + for (const char* name : + { + "/json_testsuite/sample.json", "/nativejson-benchmark/canada.json", "/nativejson-benchmark/citm_catalog.json", + "/nativejson-benchmark/twitter.json", "/json_tests/pass1.json", "/json_tests/pass2.json", "/json_tests/pass3.json" + }) + { + CAPTURE(name) + const std::string text = read_file(name); + const json_document d = json_document::parse(text); + check_round_trip(d); + // what a loaded document reads is what parse() produces + const json_document l = json_document::load(d.save()); + CHECK(l.root().materialize() == json::parse(text)); + } + } + + SECTION("images are deterministic") + { + const std::string text = R"({"b": [1, 2, {"c": "\u00e9"}], "a": 1.5})"; + const json_document d = json_document::parse(text); + CHECK(d.save() == json_document::parse(text).save()); + CHECK(d.save() == json_editable_document::parse(text).save()); + CHECK(d.save() == ordered_json_document::parse(text).save()); + const json_document copy = json_document::parse_copy(text); + CHECK(copy.save() == d.save()); + } + + SECTION("large objects get their hash index again") + { + std::string text = "{"; + for (int i = 0; i < 1000; ++i) + { + text += (i != 0 ? ",\"k" : "\"k") + std::to_string(i) + "\":" + std::to_string(i); + } + text += R"(,"k7":"a duplicate","inner":{)"; + for (int i = 0; i < 200; ++i) + { + text += (i != 0 ? ",\"m" : "\"m") + std::to_string(i) + "\":" + std::to_string(-i); + } + text += "}}"; + const json_document d = json_document::parse(text); + const std::vector image = d.save(); + for (const image_check check : + { + image_check::full, image_check::none + }) + { + const json_document l = json_document::load(image, check); + for (int i = 0; i < 1000; ++i) + { + CHECK(l.root()["k" + std::to_string(i)] == d.root()["k" + std::to_string(i)]); + } + CHECK(l.root()["k7"].get() == 7); // the first of duplicate keys + CHECK(l.root()["inner"]["m199"].get() == -199); + CHECK(!l.root().contains("k1000")); + // the index is not part of the image + CHECK(l.save() == image); + } + // the nodes of objects in the image do not carry the number of an index + CHECK(node_at(image, 0).extra == 0); + } + + SECTION("duplicate keys of a large object: lookups return the first member") + { + std::string text = "{"; + for (int i = 0; i < 200; ++i) + { + text += (i != 0 ? ",\"k" : "\"k") + std::to_string(i) + "\":" + std::to_string(i); + } + // three members for k5 (the first is a number), and duplicates of k0 and k199 + text += R"(,"k5":"two","k0":null,"k199":[],"k5":"three"})"; + const json_document d = json_document::parse(text); + REQUIRE(d.root().size() == 204); + CHECK(d.root()["k5"] == 5); + const std::vector image = d.save(); + for (const image_check check : + { + image_check::full, image_check::bounds, image_check::none + }) + { + const json_document l = json_document::load(image, check); + CHECK(l.root().size() == 204); + CHECK(l.root()["k5"] == 5); + CHECK(l.root().at("k5") == 5); + CHECK(l.root().find("k5").value() == 5); + CHECK(l.root()["k0"] == 0); + CHECK(l.root()["k199"] == 199); + CHECK(l.root()["k100"] == 100); + // materialize() keeps the semantics of parse(): the last value + CHECK(l.root().materialize() == d.root().materialize()); + CHECK(l.root().materialize()["k5"] == "three"); + } + } + + SECTION("objects with colliding keys") + { + // the hash is not seeded: keys can be found that land in one slot of + // a table (see json_view: "colliding keys") + const auto keys_for = [](std::size_t table_size, std::size_t colliding_count, std::size_t spread_count) + { + std::vector colliding; + std::vector spread; + for (std::uint64_t counter = 0; colliding.size() < colliding_count || spread.size() < spread_count; ++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('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() < colliding_count) + { + colliding.push_back(key); + } + else if (!lands_in_slot_zero && spread.size() < spread_count) + { + spread.push_back(key); + } + } + colliding.insert(colliding.end(), spread.begin(), spread.end()); + return colliding; + }; + const auto make_text = [](const std::vector& keys) + { + std::string text = "{"; + for (std::size_t i = 0; i < keys.size(); ++i) + { + text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i); + } + return text + "}"; + }; + + // 300 keys in one slot (the table is 1024 slots): the table would + // exceed the probe limit, so the object has none and is searched + // linearly; 40 of 200 keys in one slot (512 slots): a table with a + // long chain + const struct // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays): an unnamed type + { + std::size_t table_size; + std::size_t colliding; + std::size_t spread; + } cases[] = {{1024, 300, 0}, {512, 40, 160}}; + for (const auto& c : cases) + { + CAPTURE(c.table_size) + std::vector keys = keys_for(c.table_size, c.colliding, c.spread); + const std::size_t members = keys.size(); + // a duplicate of a colliding key, of the first and of a spread key + const std::vector duplicated = {keys[c.colliding - 1], keys[0], keys[members - 1]}; + std::string text = make_text(keys); + text.pop_back(); + for (const std::string& key : duplicated) + { + text += ",\"" + key + R"(":"last")"; + } + text += "}"; + const json_document d = json_document::parse(text); + const std::vector image = d.save(); + for (const image_check check : + { + image_check::full, image_check::bounds, image_check::none + }) + { + const json_document l = json_document::load(image, check); + REQUIRE(l.root().size() == members + duplicated.size()); + for (std::size_t i = 0; i < members; ++i) + { + CAPTURE(i) + // lookups find the first member, also of the duplicated keys + CHECK(l.root()[keys[i]] == i); + CHECK(l.root().at(keys[i]) == l.root()[keys[i]]); + CHECK(l.root().find(keys[i]).key() == keys[i]); + CHECK(!l.root().contains(keys[i] + "x")); + } + CHECK(!l.root().contains("missing")); + CHECK(l.root().materialize() == json::parse(text)); + } + } + } + + SECTION("loading releases the list of large objects") + { + std::string text = "["; + for (int object = 0; object < 400; ++object) + { + text += object != 0 ? ",{" : "{"; + for (int i = 0; i < 128; ++i) + { + text += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\":" + std::to_string(i); + } + text += '}'; + } + text += "]"; + json_document parsed = json_document::parse(text); + const std::vector image = parsed.save(); + json_document loaded = json_document::load(image); + CHECK(loaded.root()[399]["127"] == 127); + // Parsing and loading build the same tables, and keep nothing else: + // not the positions of the objects to index (2 KiB here). The slack + // covers the nodes in the header of the document, which are sized + // differently. + parsed.shrink_to_fit(); + loaded.shrink_to_fit(); + CHECK(loaded.memory_usage() <= parsed.memory_usage() + 512); + } +} + +TEST_CASE("json_view images: edited documents") +{ + const std::string text = R"({"name": "x", "n": 1, "f": 2.5, "list": [1, 2, 3], "obj": {"a": "\u00e9", "b": [true]}, "s": "a\"b"})"; + + SECTION("every kind of edit") + { + ordered_json_editable_document d = ordered_json_editable_document::parse(text); + d.set(d.root()["name"], "a new \"name\""); // string in the edit arena + d.set(d.root()["n"], -17); // negative integer + d.set(d.root(), "p", 5); // non-negative number_integer + d.set(d.root(), "u", 18446744073709551615u); // unsigned + d.set(d.root()["f"], 0.1); // float token + d.set(d.root(), "nan", std::numeric_limits::quiet_NaN()); + d.set(d.root(), "inf", -std::numeric_limits::infinity()); + d.push_back(d.root()["list"], "pushed"); // moved array + d.insert(d.root()["list"], 0, ordered_json::object({{"new", {1, 2}}})); + d.erase(d.root()["list"], 2); + d.erase(d.root(), "s"); + d.set(d.root()["obj"], "c", ordered_json::array({1, "two", 3.5, nullptr, false})); // new object member with a new array + d.set(d.root(), "copy", d.root()["obj"]); // a copy of a subtree + d.set(d.root(), "key \xc3\xa9", true); // a key in the edit arena + + const std::vector image = d.save(); + const ordered_json expected = ordered_json::parse(d.root().dump()); + for (const image_check check : + { + image_check::full, image_check::bounds, image_check::none + }) + { + const ordered_json_document l = ordered_json_document::load(image, check); + CHECK(l.root().dump() == d.root().dump()); + CHECK(l.root().materialize() == expected); + CHECK(l.root()["nan"].is_null()); + CHECK(l.root()["inf"].is_null()); + CHECK(l.root()["p"].is_number_integer()); + CHECK(l.root()["p"].get() == 5); + CHECK(l.root()["f"].get() == 0.1); + CHECK(l.root()["u"].get() == 18446744073709551615u); + } + // the node index is in document order again: an image of the loaded + // document is the same image + CHECK(ordered_json_document::load(image).save() == image); + // number tokens of edits follow the source; the text is the source's + // prefix + const ordered_json_document l = ordered_json_document::load(image); + REQUIRE(l.source().size() > text.size()); + CHECK(std::string(l.source().data(), text.size()) == text); // NOLINT(bugprone-suspicious-stringview-data-usage): the size is given and checked above + } + + SECTION("a loaded document can be edited and saved again") + { + const std::vector first = json_editable_document::parse(text).save(); + json_editable_document d = json_editable_document::load(first); + d.set(d.root()["obj"]["a"], "changed"); + d.push_back(d.root()["list"], 4); + d.set(d.root(), "z", json::array({json::object()})); + const std::vector second = d.save(); + const json_document l = json_document::load(second); + CHECK(l.root().dump() == d.root().dump()); + CHECK(l.root()["obj"]["a"] == "changed"); + CHECK(l.root()["list"].size() == 4); + } + + SECTION("the targets of links do not reach the image") + { + // Edits that make an entry of a moved sequence link to a value mark the + // value as linked (node_flags::linked). An image has no links and no + // such flag: the full and the bounds check reject it. + const std::string nested = R"({"a": [1, 2, {"x": [3]}, "s", 1.5, true, null, [4, 5]], "b": {"c": 1, "d": [1, 2, 3], "e": {"f": "g"}}, "h": "str"})"; + const auto check_image = [](const json_editable_document & d) + { + const std::vector image = d.save(); + for (std::size_t i = 0; i < node_count(image); ++i) + { + CAPTURE(i) + CHECK((node_at(image, i).flags & nlohmann::detail::view::node_flags::linked) == 0); + CHECK(node_at(image, i).kind != nlohmann::detail::view::kind_link); + } + for (const image_check check : + { + image_check::full, image_check::bounds + }) + { +#if !defined(JSON_NOEXCEPTION) + CHECK(load_result(image, check).empty()); +#else + const json_document loaded = json_document::load(image, check); + CHECK(loaded.root().dump() == d.root().dump()); +#endif + } + check_round_trip(d); + // a loaded document is edited and saved again + json_editable_document e = json_editable_document::load(image); + CHECK(e.root().dump() == d.root().dump()); + e.set(e.root(), "after", 1); + CHECK(loaded_dump(e.save()) == e.root().dump()); + }; + + SECTION("a container replaced by a scalar after an earlier edit") + { + json_editable_document d = json_editable_document::parse(nested); + d.set(d.root()["a"][0], 9); + d.set(d.root()["a"][2], 5); + check_image(d); + d.set(d.root()["a"][2], "now a string"); + check_image(d); + d.set(d.root()["a"][7], 0.25); + check_image(d); + d.set(d.root()["b"]["e"], true); + d.set(d.root()["b"]["d"], 7); + check_image(d); + } + + SECTION("insert and push_back into parsed arrays") + { + json_editable_document d = json_editable_document::parse(nested); + d.push_back(d.root()["a"], 6); + check_image(d); + d.insert(d.root()["a"], 0, "first"); + check_image(d); + d.insert(d.root()["b"]["d"], 1, json::array({1, 2})); + d.push_back(d.root()["b"]["d"], json::object({{"k", nullptr}})); + check_image(d); + // links to values that are replaced afterwards + d.set(d.root()["a"][3], json::object()); + d.set(d.root()["a"][4], false); + d.set(d.root()["a"][5], "replaced"); + d.set(d.root()["a"][9], 1e300); + check_image(d); + } + + SECTION("members set in parsed objects") + { + json_editable_document d = json_editable_document::parse(nested); + d.set(d.root(), "new", 1); + d.set(d.root()["b"], "c", "changed"); + d.set(d.root()["b"], "e", json::array({1, {{"z", 2}}})); + d.set(d.root()["b"], "more", d.root()["a"]); + check_image(d); + d.set(d.root()["a"][2], 3); // a link target in a copied subtree + d.erase(d.root()["b"], "c"); + d.set(d.root(), "h", json::object()); + check_image(d); + } + + SECTION("erase") + { + json_editable_document d = json_editable_document::parse(nested); + d.push_back(d.root()["a"], 6); + d.erase(d.root()["a"], 0); + d.erase(d.root()["a"], 1); + d.set(d.root()["a"][0], 1); + d.erase(d.root()["b"], "d"); + check_image(d); + } + } + +#if !defined(JSON_NOEXCEPTION) + SECTION("invalid UTF-8 of a loaded image is not copied into an editable document") + { + // loading with the bounds check (or none) does not look at the encoding + // of strings: dump() of such a view throws, and an editable document + // must not take the string over + const auto patched = [](const std::string & json_text) + { + std::vector image = json_document::parse(json_text).save(); + bool found = false; + for (std::size_t i = 0; i + 1 < image.size(); ++i) + { + if (image[i] == 'Q' && image[i + 1] == 'Z') + { + image[i] = 0xC3; + image[i + 1] = 0x28; // an invalid sequence + found = true; + } + } + REQUIRE(found); + return image; + }; + + for (const image_check check : + { + image_check::bounds, image_check::none + }) + { + // as a value + { + const json_document loaded = json_document::load(patched(R"(["abQZ"])"), check); + json_editable_document e = json_editable_document::parse("[]"); + CHECK_THROWS_WITH_AS(e.push_back(e.root(), loaded.root()[0]), "[json.exception.type_error.316] invalid UTF-8 byte at index 3: 0x28", json::type_error&); + CHECK_THROWS_WITH_AS(e.insert(e.root(), 0, loaded.root()[0]), "[json.exception.type_error.316] invalid UTF-8 byte at index 3: 0x28", json::type_error&); + CHECK_THROWS_WITH_AS(e.set(e.root(), loaded.root()[0]), "[json.exception.type_error.316] invalid UTF-8 byte at index 3: 0x28", json::type_error&); + CHECK_THROWS_WITH_AS(e.push_back(e.root(), loaded.root()), "[json.exception.type_error.316] invalid UTF-8 byte at index 3: 0x28", json::type_error&); + // nothing changed + CHECK(e.root().dump() == "[]"); + CHECK(e.root().size() == 0); + CHECK(loaded_dump(e.save()) == "[]"); + // the valid part of the same document can be copied + e.push_back(e.root(), loaded.root().size()); + CHECK(e.root().dump() == "[1]"); + } + // as a key, and in a nested value + { + const json_document loaded = json_document::load(patched(R"([{"abQZ": 1}, [["x", "QZ"]]])"), check); + json_editable_document e = json_editable_document::parse(R"({"keep": [1]})"); + CHECK_THROWS_WITH_AS(e.set(e.root(), "k", loaded.root()[0]), "[json.exception.type_error.316] invalid UTF-8 byte at index 3: 0x28", json::type_error&); + CHECK_THROWS_WITH_AS(e.set(e.root(), "k", loaded.root()[1]), "[json.exception.type_error.316] invalid UTF-8 byte at index 1: 0x28", json::type_error&); + CHECK_THROWS_WITH_AS(e.push_back(e.root()["keep"], loaded.root()[1]), "[json.exception.type_error.316] invalid UTF-8 byte at index 1: 0x28", json::type_error&); + CHECK(e.root().dump() == R"({"keep":[1]})"); + CHECK(loaded_dump(e.save()) == R"({"keep":[1]})"); + } + } + } +#endif + + SECTION("the root replaced") + { + json_editable_document d = json_editable_document::parse(text); + d.set(d.root(), json::array({1, "x"})); + check_round_trip(d); + d.set(d.root(), 3.5); + check_round_trip(d); + d.set(d.root(), "text"); + check_round_trip(d); + } +} + +TEST_CASE("json_view images: ownership") +{ + const std::string text = R"({"a": "esc\u00e9aped", "b": [1, 2]})"; + const std::vector image = json_document::parse(text).save(); + + SECTION("borrowed") + { + const json_document d = json_document::load(image); + CHECK(!d.owns_source()); + CHECK(d.root()["a"] == "esc\xc3\xa9" "aped"); + const json_document p = json_document::load(image.data(), image.size()); + CHECK(!p.owns_source()); + CHECK(p.root() == d.root()); + // the text is the image's + CHECK(d.source().data() == reinterpret_cast(image.data() + text_at(image))); + } + + SECTION("owned") + { + std::vector copy = image; + const std::uint8_t* const data = copy.data(); + json_document d = json_document::load(std::move(copy)); + CHECK(d.owns_source()); + CHECK(d.source().data() == reinterpret_cast(data + text_at(image))); + CHECK(d.memory_usage() >= image.size()); + CHECK(d.root()["b"][1] == 2); + // read() replaces the image + d.read(std::string("[1]")); + CHECK(d.owns_source()); + CHECK(d.root().dump() == "[1]"); + const std::string borrowed = "[2]"; + d.read(borrowed); + CHECK(!d.owns_source()); + } + + SECTION("shrink_to_fit keeps the decoded strings of the image") + { + json_document d = json_document::parse(R"(["\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9"])"); + d.shrink_to_fit(); + const std::vector img = d.save(); + json_document l = json_document::load(img); + l.shrink_to_fit(); + CHECK(l.root().dump() == d.root().dump()); + CHECK(l.save() == img); + } +} + +// the remaining tests are about the exceptions of load() and save() +#if !defined(JSON_NOEXCEPTION) +TEST_CASE("json_view images: errors") +{ + SECTION("invalid UTF-8 of a decoded string: the compact dump writes it, the others throw") + { + const json_document d = json_document::parse(R"({"k\n":"\u00e9x"})"); + std::vector image = d.save(); + const std::size_t arena = text_at(image) + header_size_field(image, 16) + 1; + image[arena + 3] = 0x28; // the key is "k\n"; the value starts at 3: 0xC3 0x28 is invalid + const json_document loaded = json_document::load(image, image_check::bounds); + CHECK(loaded.root().dump().size() == 13); + for (const int indent : + { + 0, 2 + }) + { + CHECK_THROWS_AS(loaded.root().dump(indent), json::type_error); + CHECK_THROWS_AS(loaded.root().dump(indent, ' ', true), json::type_error); + } + CHECK_THROWS_AS(loaded.root().dump(-1, ' ', true), json::type_error); + CHECK_THROWS_WITH_AS(loaded.root().dump(2), "[json.exception.type_error.316] invalid UTF-8 byte at index 1: 0x28", json::type_error&); + } + + SECTION("a literal as the root: dump() after loading") + { + for (const char* text : + { + "null", "true", "false" + }) + { + std::vector image = json_document::parse(text).save(); + node n = node_at(image, 0); + n.off = static_cast(image.size()); + set_node(image, 0, n); + CHECK(load_result(image, image_check::full) == check_failed); + CHECK(load_result(image, image_check::bounds) == check_failed); + } + } + + SECTION("saving a discarded document") + { + const json_document empty{}; + CHECK(exception_of([&] { static_cast(empty.save()); }) == "[json.exception.type_error.320] cannot save a discarded json_document"); + const json_document failed = json_document::parse("[1,", false); + CHECK(exception_of([&] { static_cast(failed.save()); }) == "[json.exception.type_error.320] cannot save a discarded json_document"); + } + + const std::vector image = json_document::parse(R"({"a": [1, "\u00e9"]})").save(); + const std::string prefix = "[json.exception.parse_error.116] parse error: invalid json_document image: "; + + SECTION("header and sizes") + { + CHECK(exception_of([] + { + const json_document d = json_document::load(nullptr, 0); + static_cast(d); + }) == prefix + "too short"); + CHECK(exception_of([&] + { + const json_document d = json_document::load(image.data(), 63); + static_cast(d); + }) == prefix + "too short"); + + std::vector bad = image; + bad[0] = 'X'; + CHECK(load_result(bad, image_check::full) == prefix + "unknown format"); + bad = image; + bad[4] = 2; // version + CHECK(load_result(bad, image_check::full) == prefix + "unknown format"); + for (std::size_t reserved = 32; reserved < 64; reserved += 8) + { + bad = image; + bad[reserved + 3] = 1; + CHECK(load_result(bad, image_check::none) == prefix + "unknown format"); + } + + const auto sizes = [&](std::size_t offset, std::uint64_t v) + { + std::vector b = image; + set_header_field(b, offset, v); + return load_result(b, image_check::none); + }; + CHECK(sizes(8, 0) == prefix + "sizes out of range"); // no nodes + CHECK(sizes(8, 1000) == prefix + "sizes out of range"); // more nodes than bytes + CHECK(sizes(8, 0xFFFFFFF0u) == prefix + "sizes out of range"); + CHECK(sizes(16, 0xFFFFFFF0u) == prefix + "sizes out of range"); // text size + CHECK(sizes(16, header_field(image, 16) + 1) == prefix + "sizes out of range"); + CHECK(sizes(16, image.size()) == prefix + "sizes out of range"); + CHECK(sizes(24, 0xFFFFFFF0u) == prefix + "sizes out of range"); // decoded string size + CHECK(sizes(24, header_field(image, 24) - 1) == prefix + "sizes out of range"); + + // the NULs after the text and the decoded strings + bad = image; + bad[text_at(image) + header_size_field(image, 16)] = 'x'; + CHECK(load_result(bad, image_check::none) == prefix + "sizes out of range"); + bad = image; + bad.back() = 'x'; + CHECK(load_result(bad, image_check::none) == prefix + "sizes out of range"); + // nothing after the image + bad = image; + bad.push_back(0); + CHECK(load_result(bad, image_check::none) == prefix + "sizes out of range"); + // nodes, but not even room for the NULs + bad.assign(image.begin(), image.begin() + static_cast(text_at(image))); + CHECK(load_result(bad, image_check::none) == prefix + "sizes out of range"); + } +} + +TEST_CASE("json_view images: check") +{ + // nodes: 0 { 1 "s" 2 "x\"y" (escaped) 3 "i" 4 -12 5 "u" 6 7 7 "f" 8 1.5e300 9 "b" 10 true 11 "n" 12 null + // 13 "a" 14 [ 15 "t" 16 {} ] + const std::string text = R"({"s":"x\"y","i":-12,"u":7,"f":1.5e300,"b":true,"n":null,"a":["t",{}]})"; + const std::vector image = json_document::parse(text).save(); + REQUIRE(load_result(image, image_check::full).empty()); + REQUIRE(node_count(image) == 17); + + // bounds: rejected by both checks; content: only by the full one + const auto rejected = [&](const std::vector& b, bool bounds) + { + CHECK(load_result(b, image_check::full) == check_failed); + CHECK(load_result(b, image_check::bounds) == (bounds ? check_failed : "")); + }; + + SECTION("kinds") + { + const std::array kinds = {{8, 9, 10, 200}}; // binary, discarded, link, unknown + for (const std::uint8_t kind : kinds) + { + rejected(corrupted(image, 12, [&](node & n) + { + n.kind = kind; + }), true); + } + // a key that is not a string + rejected(corrupted(image, 1, [](node & n) + { + n.kind = 0; + n.len = 0; + n.off = 0; + }), true); + } + + SECTION("flags and extra") + { + rejected(corrupted(image, 12, [](node & n) + { + n.flags = 4; + }), true); + rejected(corrupted(image, 12, [](node & n) + { + n.extra = 1; + }), true); + rejected(corrupted(image, 10, [](node & n) + { + n.flags = 5; + }), true); + rejected(corrupted(image, 10, [](node & n) + { + n.extra = 1; + }), true); + rejected(corrupted(image, 1, [](node & n) + { + n.flags = 2; // a string in the edit arena + }), true); + rejected(corrupted(image, 1, [](node & n) + { + n.extra = 3; + }), true); + rejected(corrupted(image, 4, [](node & n) + { + n.flags = 2; + }), true); + rejected(corrupted(image, 4, [](node & n) + { + n.extra = static_cast(n.extra | 0x100u); // an integer with fraction digits + }), true); + rejected(corrupted(image, 0, [](node & n) + { + n.flags = 8; // moved + }), true); + rejected(corrupted(image, 0, [](node & n) + { + n.extra = 1; // a hash index + }), true); + // the flag of the targets of links (editable documents) is not part of an image + for (std::size_t i = 0; i < node_count(image); ++i) + { + CAPTURE(i) + rejected(corrupted(image, i, [](node & n) + { + n.flags = static_cast(n.flags | nlohmann::detail::view::node_flags::linked); + }), true); + } + } + + SECTION("bounds") + { + const std::size_t text_size = header_size_field(image, 16); + const std::size_t arena_size = header_size_field(image, 24); + rejected(corrupted(image, 1, [&](node & n) + { + n.off = static_cast(text_size + 1); + }), true); + rejected(corrupted(image, 1, [&](node & n) + { + n.len = static_cast(text_size); + }), true); + rejected(corrupted(image, 2, [&](node & n) + { + n.len = static_cast(arena_size + 1); + }), true); + rejected(corrupted(image, 6, [&](node & n) + { + n.off = static_cast(text_size); + }), true); + rejected(corrupted(image, 6, [&](node & n) + { + n.off = static_cast(text_size + 5); + }), true); + rejected(corrupted(image, 6, [](node & n) + { + n.extra = 0; // no digits + }), true); + rejected(corrupted(image, 8, [&](node & n) + { + n.len = static_cast(text_size); + }), true); + rejected(corrupted(image, 8, [](node & n) + { + n.len = 2; // shorter than the recorded digits + }), true); + rejected(corrupted(image, 14, [&](node & n) + { + n.off = static_cast(text_size + 1); + }), true); + // literals: their offset sizes the output of dump() + rejected(corrupted(image, 10, [&](node & n) + { + n.off = static_cast(text_size + 1); + }), true); + rejected(corrupted(image, 12, [&](node & n) + { + n.off = 0xFFFFFFFFu; + }), true); + } + + SECTION("structure") + { + rejected(corrupted(image, 0, [](node & n) + { + n.next = 0; + }), true); + rejected(corrupted(image, 0, [](node & n) + { + n.next = 18; // beyond the image + }), true); + rejected(corrupted(image, 14, [](node & n) + { + n.next = 4; // beyond the enclosing object + }), true); + rejected(corrupted(image, 0, [](node & n) + { + n.len = 6; // member count + }), true); + rejected(corrupted(image, 14, [](node & n) + { + n.len = 3; // element count + }), true); + rejected(corrupted(image, 0, [](node & n) + { + n.next = 14; // the object ends after the key "a" + n.len = 7; + }), true); + rejected(corrupted(image, 0, [](node & n) + { + n.next = 13; // nodes after the root + n.len = 6; + }), true); + rejected(corrupted(image, 0, [](node & n) + { + n.kind = 2; // an array: the "keys" are values, and the counts do not match + }), true); + const std::vector as_array = corrupted(image, 16, [](node & n) + { + n.kind = 2; // {} as []: fine + }); + CHECK(load_result(as_array, image_check::full).empty()); + const std::string expected_dump = R"({"s":"x\"y","i":-12,"u":7,"f":1.5e+300,"b":true,"n":null,"a":["t",[]]})"; + CHECK(loaded_dump(as_array) == expected_dump); + } + + SECTION("strings") + { + // a quote in a source string (the full check only) + std::vector b = image; + const std::size_t t = text_at(image); + const node t15 = node_at(image, 15); + b[t + t15.off] = '"'; + rejected(b, false); + // a control character + b[t + t15.off] = '\n'; + rejected(b, false); + // invalid UTF-8 in a decoded string + b = image; + const node s2 = node_at(image, 2); + b[t + header_size_field(image, 16) + 1 + s2.off] = 0xFF; + rejected(b, false); + } + + SECTION("numbers") + { + const std::size_t t = text_at(image); + const node i4 = node_at(image, 4); + const node u6 = node_at(image, 6); + const node f8 = node_at(image, 8); + const auto at_token = [&](const node & n, std::size_t k, std::uint8_t c) + { + std::vector b = image; + b[t + n.off + k] = c; + return b; + }; + rejected(at_token(i4, 1, 'x'), false); // -x2 + rejected(at_token(i4, 1, '0'), false); // -02 + rejected(at_token(i4, 0, '1'), false); // 112 != -12 + rejected(at_token(f8, 1, 'x'), false); // 1x5e300 + rejected(at_token(f8, 2, 'e'), false); // 1.ee300 + rejected(at_token(f8, 4, 'x'), false); // 1.5ex00 + rejected(at_token(f8, 3, '0'), false); // 1.50300: another layout + rejected(at_token(f8, 4, '9'), false); // 1.5e900: overflow + rejected(at_token(f8, 0, 'x'), false); + rejected(at_token(u6, 0, '8'), false); // 8 != 7 + rejected(corrupted(image, 4, [](node & n) + { + n.kind = 6; // "-12" as unsigned: a sign + n.extra = 3; + }), false); + // a non-negative number_integer (as edits write it): fine + const std::vector positive = corrupted(image, 6, [](node & n) + { + n.kind = 5; + n.extra = 0; + }); + CHECK(load_result(positive, image_check::full).empty()); + const json_document pos = json_document::load(positive); + CHECK(pos.root()["u"].is_number_integer()); + rejected(corrupted(image, 8, [](node & n) + { + n.kind = 6; // a float token as integer + n.extra = 7; + }), false); + } + + SECTION("float tokens of an image checked for bounds only") + { + // A float node whose layout records "many" digits is converted from + // its token alone; a token that is not a JSON number reads as 0. + const std::vector img = json_document::parse("[1.5e300,2]").save(); + const std::size_t t = text_at(img); + for (const char* token : + { + "x.5e300", "01.5e30", "1.xe300", "1.5ex00", "1.5e+x0", "1.5e30x", "-.5e300", "1.5E300" + }) + { + CAPTURE(token) + std::vector b = img; + node n = node_at(b, 1); + n.extra = 0xFFFFu; + set_node(b, 1, n); + std::memcpy(b.data() + t + n.off, token, n.len); + const json_document d = json_document::load(b, image_check::bounds); + const auto v = d.root()[0].get(); + CHECK(v == (std::string(token) == "1.5E300" ? 1.5e300 : 0.0)); + CHECK(load_result(b, image_check::full) == (std::string(token) == "1.5E300" ? "" : check_failed)); + } + } + + SECTION("float tokens with fewer digits than the layout records") + { + // The bytes of the token are not digits (they read as zeros or as + // other values), so the value has fewer (or more) digits than the + // layout says: dump() must still write a number. + for (const auto& source : std::vector> + { + {"123456789012345678.5", std::string("@") + std::string(16, '0') + "1.1"}, // 19 digits + {"1234.5", "@001.1"}, // 5 digits + {"1234.5", "9??.??"} // more than 5 digits + }) + { + CAPTURE(source.second) + const std::vector img = json_document::parse("[" + source.first + "]").save(); + const node n = node_at(img, 1); + REQUIRE(source.second.size() == n.len); + std::vector b = img; + std::memcpy(b.data() + text_at(img) + n.off, source.second.data(), n.len); + const json_document d = json_document::load(b, image_check::bounds); + const std::string dumped = d.root().dump(); + CAPTURE(dumped) + CHECK(json::parse(dumped)[0].is_number()); + } + } + + SECTION("nodes that share a range") + { + // [big string, then n strings made to point to the big string]: every + // node shares the one range (the check reads it once) + const std::size_t n = 200; + std::string long_text = "[\"" + std::string(5000, 'a') + "\""; + for (std::size_t k = 0; k < n; ++k) + { + long_text += ",\"x\""; + } + long_text += "]"; + const std::vector img = json_document::parse(long_text).save(); + const node big = node_at(img, 1); + std::vector b = img; + for (std::size_t k = 0; k < n; ++k) + { + set_node(b, 2 + k, big); + } + CHECK(load_result(b, image_check::full).empty()); + const json_document d = json_document::load(b); + CHECK(d.root().size() == n + 1); + CHECK(d.root()[n].get() == std::string(5000, 'a')); + CHECK(d.root().dump() == loaded_dump(b, image_check::none)); + + // the same for decoded strings and float tokens, with a node that + // records another digit layout than its token + const std::vector img2 = json_document::parse(R"(["a\"b", "a\"b", 1.25, 1.25, 1.25e3])").save(); + CHECK(load_result(img2, image_check::full).empty()); + std::vector same = img2; + set_node(same, 2, node_at(img2, 1)); + set_node(same, 4, node_at(img2, 3)); + CHECK(load_result(same, image_check::full).empty()); + // (a raw string with a backslash must not be a macro argument: MSVC C2017) + const std::string expected_same = R"(["a\"b","a\"b",1.25,1.25,1250.0])"; + CHECK(loaded_dump(same) == expected_same); + std::vector layout = same; + node f4 = node_at(layout, 4); + f4.extra = 0x0100u; // the layout of "1.", and not that of "1.25" + set_node(layout, 4, f4); + rejected(layout, false); + f4.extra = 0xFFFFu; // "many" digits: fine, as compaction writes it + set_node(layout, 4, f4); + CHECK(load_result(layout, image_check::full).empty()); + } + + SECTION("ranges that overlap") + { + // save() writes every string and every token to a place of its own + // (nodes that share a value share the whole range): ranges that + // overlap without being identical are a damaged image, though each + // range is a valid string or token + // nodes: 0 [ 1 "abcdef" 2 "ghijkl" 3 1.2525 4 9.9 5 "a\"bcd" (decoded) 6 "e\"fgh" (decoded) + const std::vector img = json_document::parse(R"(["abcdef", "ghijkl", 1.2525, 9.9, "a\"bcd", "e\"fgh"])").save(); + REQUIRE(load_result(img, image_check::full).empty()); + // node i with the range (off of node of + shift, length), and extra + const auto aliased = [&](std::size_t i, std::size_t of, std::uint32_t shift, std::uint32_t length, std::uint16_t extra) + { + std::vector b = img; + node n = node_at(b, of); + n.off += shift; + n.len = length; + n.extra = extra; + set_node(b, i, n); + return b; + }; + // source strings + rejected(aliased(2, 1, 0, 4, 0), false); // "abcd": the start of another string + rejected(aliased(2, 1, 1, 4, 0), false); // "bcde": inside + rejected(aliased(2, 1, 2, 4, 0), false); // "cdef": the end + CHECK(load_result(aliased(2, 1, 0, 6, 0), image_check::full).empty()); // the whole range: identical + // decoded strings: inside, and partially overlapping (the arena holds a"bcde"fgh) + rejected(aliased(6, 5, 1, 3, 0), false); + rejected(aliased(6, 5, 3, 5, 0), false); + CHECK(load_result(aliased(6, 5, 0, 5, 0), image_check::full).empty()); + // float tokens: "1.25" and "525" (an integer token as a float) inside "1.2525" + rejected(aliased(4, 3, 0, 4, 0x0201u), false); + rejected(aliased(4, 3, 3, 3, 0x0003u), false); + CHECK(load_result(aliased(4, 3, 0, 6, 0x0401u), image_check::full).empty()); + // a string and a float token may use the same bytes (each is checked by its own kind) + std::vector shared = img; + node as_string = node_at(img, 2); + as_string.off = node_at(img, 3).off; + as_string.len = 6; + set_node(shared, 2, as_string); + CHECK(load_result(shared, image_check::full).empty()); + // empty strings inside others are not ranges + CHECK(load_result(aliased(2, 1, 2, 0, 0), image_check::full).empty()); + } + + SECTION("copies within an edited document") + { + // copies within a document share the value: identical ranges + json_editable_document d = json_editable_document::parse(R"({"s": "ab", "e": "x\"y", "f": 1.5, "g": 1.5e300, "a": [1.5, "ab"]})"); + d.set(d.root(), "c1", d.root()["a"]); + d.set(d.root(), "c2", d.root()["a"]); + d.set(d.root(), "c3", d.root()); + d.push_back(d.root()["a"], d.root()["e"]); + d.push_back(d.root()["a"], d.root()["g"]); + d.push_back(d.root()["a"], d.root()["g"]); + d.set(d.root()["s"], "changed"); + d.set(d.root()["f"], 7.25); + const std::vector saved = d.save(); + CHECK(load_result(saved, image_check::full).empty()); + CHECK(loaded_dump(saved) == d.root().dump()); + check_round_trip(d); + } + + SECTION("integer ranges") + { + // tokens of many digits, which the parser stores as floats + const std::string big = R"([123456789012345678901234, 99999999999999999999, 9223372036854775808])"; + const std::vector img = json_document::parse(big).save(); + const auto as_integer = [&](std::size_t i, std::uint8_t kind, std::uint16_t extra) + { + std::vector b = img; + node n = node_at(b, i); + n.kind = kind; + n.extra = extra; + set_node(b, i, n); + return load_result(b, image_check::full); + }; + CHECK(as_integer(1, 6, 24) == check_failed); // more than 20 digits + CHECK(as_integer(2, 6, 20) == check_failed); // more than 2^64 - 1 + CHECK(as_integer(3, 5, 18) == check_failed); // more than 2^63 - 1 as number_integer + } +} + +TEST_CASE("json_view images: damaged images") +{ + // A damaged image must be rejected, or read safely; with the full check, + // it also serializes to the JSON it reads as. + const std::vector texts = + { + R"({"a": [1, -2, 3.25, "x\u00e9y", true, null], "b": {"c": "\"q\"", "d": 1e10}, "e": ""})", + R"([[[[]]], {"k": {"k": {"k": 12345678901234567890}}}, "\ud83d\ude00", -0.0, 0])", + }; + for (const std::string& text : texts) + { + const std::vector image = json_document::parse(text).save(); + for (int round = 0; round < 3000; ++round) + { + std::vector b = image; + const std::uint32_t flips = 1 + (rng() % 3); + for (std::uint32_t k = 0; k < flips; ++k) + { + // mostly the nodes, where the damage matters most + const std::size_t at = rng() % 4 != 0 ? header_size + (rng() % (b.size() - header_size)) : rng() % b.size(); + b[at] = static_cast(rng() % 3 == 0 ? rng() : b[at] ^ (1u << (rng() % 8))); + } + for (const image_check check : + { + image_check::full, image_check::bounds + }) + { + json_document d; + try + { + d = json_document::load(b, check); + } + catch (const json::parse_error& e) + { + CHECK(e.id == 116); + continue; + } + std::string dumped; + std::string dumped_ascii; + try + { + dumped = d.root().dump(); + dumped_ascii = d.root().dump(-1, ' ', true); + } + catch (const json::type_error& e) + { + // invalid UTF-8 (the bounds check only) + CHECK(check == image_check::bounds); + CHECK(e.id == 316); + continue; + } + const json j = d.root().materialize(); + if (check == image_check::full) + { + CHECK(json::parse(dumped) == j); + CHECK(json::parse(dumped_ascii) == j); + } + } + } + } +} +#endif + +#else + +TEST_CASE("json_view images: big-endian targets") +{ + const json_document d = json_document::parse("[1]"); + CHECK_THROWS_WITH_AS(d.save(), "[json.exception.type_error.320] json_document images need a little-endian target", json::type_error&); +} + +#endif diff --git a/tests/src/unit-json_view_macros.cpp b/tests/src/unit-json_view_macros.cpp new file mode 100644 index 000000000..2cc8b7744 --- /dev/null +++ b/tests/src/unit-json_view_macros.cpp @@ -0,0 +1,47 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +// All other tests keep the library's macros (JSON_TEST_KEEP_MACROS). This one +// includes json_view.hpp as users do, so that json.hpp undefines its macros +// (JSON_HAS_CPP_17, JSON_STRICT_NUL_HANDLING, ...) before the view is compiled. +#undef JSON_TEST_KEEP_MACROS +#include + +#include +#include + +#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) + #include + #define JSON_VIEW_TEST_HAS_STRING_VIEW 1 +#else + #define JSON_VIEW_TEST_HAS_STRING_VIEW 0 +#endif + +// 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) \ + || defined(NLOHMANN_VIEW_NODISCARD) || defined(NLOHMANN_VIEW_THROW) || defined(NLOHMANN_VIEW_HAS_CPP_17) || defined(NLOHMANN_VIEW_LITTLE_ENDIAN) \ + || defined(NLOHMANN_VIEW_REPEAT16) + #error "json_view.hpp leaks a macro" +#endif + +TEST_CASE("json_view without the library's macros") +{ + // (this file also gets C++17 builds: it mentions JSON_HAS_CPP_17) +#if JSON_VIEW_TEST_HAS_STRING_VIEW + CHECK(std::is_same::value); +#endif + const std::string text = "[1, 2.5, \"x\"]"; + const nlohmann::json_document d = nlohmann::json_document::parse(text); + CHECK(d.root().size() == 3); + CHECK(d.root().materialize() == nlohmann::json::parse(text)); + // the NUL handling of the library's configuration + const std::string with_nul("[1]\0x", 5); + CHECK(nlohmann::json_document::accept(with_nul) == nlohmann::json::accept(with_nul)); +} diff --git a/tests/src/unit-large_json.cpp b/tests/src/unit-large_json.cpp index cd615f4c3..ed988c242 100644 --- a/tests/src/unit-large_json.cpp +++ b/tests/src/unit-large_json.cpp @@ -13,8 +13,6 @@ using nlohmann::json; #include #include -#include -#include TEST_CASE("tests on very large JSONs") { @@ -55,24 +53,6 @@ const json* innermost_value(const json& j, std::size_t& depth) return current; } -// The text of a value nested depth levels deep around the number 0. Level i is -// an array if pattern[i % pattern.size()] is '[', and otherwise an object with -// the single member "a", which every object type enumerates in the same order. -std::string nested_text(std::size_t depth, const std::string& pattern) -{ - std::string text; - std::string closing; - for (std::size_t i = 0; i < depth; ++i) - { - const bool array = pattern[i % pattern.size()] == '['; - text += array ? "[" : "{\"a\":"; - closing += array ? ']' : '}'; - } - text += '0'; - text.append(closing.rbegin(), closing.rend()); - return text; -} - } // namespace TEST_CASE("tests on deeply nested JSONs") @@ -244,322 +224,4 @@ TEST_CASE("tests on deeply nested JSONs") CHECK(*innermost_value(j, unused) == 0); } } - - SECTION("issue #5650 - stack overflow converting between specializations") - { - const std::vector patterns = {"[", "{", "[{"}; - - SECTION("json to ordered_json") - { - for (const auto& pattern : patterns) - { - CAPTURE(pattern) - const std::string text = nested_text(depth, pattern); - const json j = json::parse(text); - - const nlohmann::ordered_json converted = j; - CHECK(converted.dump() == text); - } - } - - SECTION("ordered_json to json") - { - for (const auto& pattern : patterns) - { - CAPTURE(pattern) - const std::string text = nested_text(depth, pattern); - const nlohmann::ordered_json o = nlohmann::ordered_json::parse(text); - - const json converted = o; - CHECK(converted.dump() == text); - } - } - - SECTION("get()") - { - for (const auto& pattern : patterns) - { - CAPTURE(pattern) - const std::string text = nested_text(depth, pattern); - const json j = json::parse(text); - - CHECK(j.get().dump() == text); - } - } - - SECTION("depths around the bound of the recursive descent") - { - for (std::size_t d = 1; d <= 300; ++d) - { - CAPTURE(d) - for (const auto& pattern : patterns) - { - CAPTURE(pattern) - const std::string text = nested_text(d, pattern); - const json j = json::parse(text); - - const nlohmann::ordered_json converted = j; - CHECK(converted.dump() == text); - const json back = converted; - CHECK(back.dump() == text); - } - } - } - - SECTION("values below the bound are converted as values above it") - { - // Bury a value below the bound, where it is converted without the - // call stack, and compare it with the same value converted on its - // own by the containers' range constructors. Its objects have - // members that the two object types enumerate in different orders. - const auto bury = [](nlohmann::ordered_json value) - { - for (std::size_t i = 0; i < 200; ++i) - { - value = nlohmann::ordered_json::array({std::move(value)}); - } - return value; - }; - const auto dig = [](const json & value) - { - const json* current = &value; - for (std::size_t i = 0; i < 200; ++i) - { - current = ¤t->at(0); - } - return current; - }; - - nlohmann::ordered_json value = nlohmann::ordered_json::object(); - value["z"] = {1, -2, 3U, 4.5, true, nullptr, "six", nlohmann::ordered_json::binary({7, 8}, 9), - nlohmann::ordered_json::binary({10}), nlohmann::ordered_json::array(), nlohmann::ordered_json::object() - }; - value["y"] = {{"x", {{"w", 1}, {"v", 2}}}, {"u", {3, {{"t", 4}, {"s", 5}}}}}; - value["r"] = nlohmann::ordered_json::array({nlohmann::ordered_json(nlohmann::ordered_json::value_t::discarded)}); - - const json converted_above = value; - const json buried = bury(value); - const json& converted_below = *dig(buried); - - CHECK(converted_below.dump() == converted_above.dump()); - CHECK(converted_below.at("z").at(7).get_binary().subtype() == 9); - CHECK_FALSE(converted_below.at("z").at(8).get_binary().has_subtype()); - CHECK(converted_below.at("r").at(0).is_discarded()); - - // a discarded value is never equal to anything, so compare the rest - value.erase("r"); - const json without_discarded_above = value; - const json without_discarded_buried = bury(value); - CHECK(*dig(without_discarded_buried) == without_discarded_above); - } - } } - -namespace -{ -json nested_array(const std::size_t depth, json leaf) -{ - json j = std::move(leaf); - for (std::size_t i = 0; i < depth; ++i) - { - json a = json::array(); - a.push_back(std::move(j)); - j = std::move(a); - } - return j; -} - -json nested_object(const std::size_t depth, json leaf) -{ - json j = std::move(leaf); - for (std::size_t i = 0; i < depth; ++i) - { - json o = json::object(); - o["k"] = std::move(j); - j = std::move(o); - } - return j; -} -} // namespace - -TEST_CASE("issue #5392 - binary writers on deeply nested values") -{ - // 200 is past the point where the writers stop recursing, and still - // shallow enough that from_* and operator== (which still recurse) are fine. - const json deep_array = nested_array(200, json(0)); - const json deep_object = nested_object(200, json("x")); - const json empty_array = nested_array(200, json::array()); - const json empty_object = nested_object(200, json::object()); - const json mixed = nested_object(80, nested_array(80, json(true))); - - SECTION("roundtrip past the recursion bound") - { - CHECK(json::from_cbor(json::to_cbor(deep_array)) == deep_array); - CHECK(json::from_msgpack(json::to_msgpack(deep_array)) == deep_array); - CHECK(json::from_ubjson(json::to_ubjson(deep_array)) == deep_array); - CHECK(json::from_ubjson(json::to_ubjson(deep_array, true, false)) == deep_array); - CHECK(json::from_ubjson(json::to_ubjson(deep_array, true, true)) == deep_array); - CHECK(json::from_bjdata(json::to_bjdata(deep_array)) == deep_array); - - CHECK(json::from_cbor(json::to_cbor(deep_object)) == deep_object); - CHECK(json::from_msgpack(json::to_msgpack(deep_object)) == deep_object); - CHECK(json::from_ubjson(json::to_ubjson(deep_object)) == deep_object); - CHECK(json::from_ubjson(json::to_ubjson(deep_object, true, true)) == deep_object); - CHECK(json::from_bjdata(json::to_bjdata(deep_object)) == deep_object); - - CHECK(json::from_cbor(json::to_cbor(empty_array)) == empty_array); - CHECK(json::from_msgpack(json::to_msgpack(empty_array)) == empty_array); - CHECK(json::from_ubjson(json::to_ubjson(empty_array)) == empty_array); - CHECK(json::from_ubjson(json::to_ubjson(empty_array, true, true)) == empty_array); - - CHECK(json::from_cbor(json::to_cbor(empty_object)) == empty_object); - CHECK(json::from_msgpack(json::to_msgpack(empty_object)) == empty_object); - CHECK(json::from_ubjson(json::to_ubjson(empty_object)) == empty_object); - - CHECK(json::from_cbor(json::to_cbor(mixed)) == mixed); - CHECK(json::from_msgpack(json::to_msgpack(mixed)) == mixed); - CHECK(json::from_ubjson(json::to_ubjson(mixed)) == mixed); - CHECK(json::from_bjdata(json::to_bjdata(mixed)) == mixed); - } - - SECTION("the two ways of writing a value meet at the bound") - { - for (std::size_t depth = 120; depth <= 140; ++depth) - { - CAPTURE(depth) - - const json array = nested_array(depth, json(7)); - CHECK(json::from_cbor(json::to_cbor(array)) == array); - CHECK(json::from_msgpack(json::to_msgpack(array)) == array); - CHECK(json::from_ubjson(json::to_ubjson(array, true, true)) == array); - - const json object = nested_object(depth, json(7)); - CHECK(json::from_cbor(json::to_cbor(object)) == object); - CHECK(json::from_msgpack(json::to_msgpack(object)) == object); - CHECK(json::from_bjdata(json::to_bjdata(object)) == object); - } - } - - SECTION("a BJData ndarray below the bound is still an ndarray") - { - const json ndarray = json({{"_ArrayType_", "uint8"}, {"_ArraySize_", {2, 3}}, {"_ArrayData_", {1, 2, 3, 4, 5, 6}}}); - const json invalid = json({{"_ArrayType_", "nope"}, {"_ArraySize_", {1}}, {"_ArrayData_", {1}}}); - - const json deep_ndarray = nested_array(140, ndarray); - const json deep_invalid = nested_array(140, invalid); - - CHECK(json::from_bjdata(json::to_bjdata(deep_ndarray)) == deep_ndarray); - CHECK(json::from_bjdata(json::to_bjdata(deep_invalid)) == deep_invalid); - CHECK(json::from_bjdata(json::to_bjdata(ndarray)) == ndarray); - } - - SECTION("byte-exact across the switch-over") - { - // nested one-element arrays around the recursion bound: the exact - // bytes a writer produces do not depend on whether it stayed on the - // call stack or moved to the heap one partway through - for (const std::size_t depth : - { - nlohmann::detail::recursion_depth_limit() - 1, nlohmann::detail::recursion_depth_limit(), - nlohmann::detail::recursion_depth_limit() + 1, nlohmann::detail::recursion_depth_limit() + 2 - }) - { - CAPTURE(depth) - const json array = nested_array(depth, json(0)); - - std::vector expected_cbor(depth, 0x81); - expected_cbor.push_back(0x00); - CHECK(json::to_cbor(array) == expected_cbor); - - std::vector expected_msgpack(depth, 0x91); - expected_msgpack.push_back(0x00); - CHECK(json::to_msgpack(array) == expected_msgpack); - - std::string expected_ubjson(depth, '['); - expected_ubjson += "i"; - expected_ubjson += '\0'; - expected_ubjson.append(depth, ']'); - const auto packed_ubjson = json::to_ubjson(array); - CHECK(std::string(packed_ubjson.begin(), packed_ubjson.end()) == expected_ubjson); - } - } - - SECTION("a deep object, and a BJData ndarray, past the recursion bound") - { - const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 50; - - const json object = nested_object(depth, json(42)); - CHECK(json::from_cbor(json::to_cbor(object)) == object); - CHECK(json::from_msgpack(json::to_msgpack(object)) == object); - CHECK(json::from_ubjson(json::to_ubjson(object, true, true)) == object); - CHECK(json::from_bjdata(json::to_bjdata(object)) == object); - - const json ndarray = json({{"_ArrayType_", "uint8"}, {"_ArraySize_", {2, 3}}, {"_ArrayData_", {1, 2, 3, 4, 5, 6}}}); - const json deep_ndarray = nested_array(depth, ndarray); - CHECK(json::from_bjdata(json::to_bjdata(deep_ndarray)) == deep_ndarray); - } - - SECTION("a discarded value past the recursion bound still throws type_error.321") - { - const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 50; - const json discarded_leaf(json::value_t::discarded); - const json deep_discarded = nested_array(depth, discarded_leaf); - - // with diagnostics, the message names the path to the discarded leaf -#if JSON_DIAGNOSTICS - std::string path; - for (std::size_t i = 0; i < depth; ++i) - { - path += "/0"; - } - const std::string prefix = "[json.exception.type_error.321] (" + path + ") "; -#else - const std::string prefix = "[json.exception.type_error.321] "; -#endif - - CHECK_THROWS_WITH_AS(json::to_cbor(deep_discarded), (prefix + "cannot serialize discarded value to CBOR").c_str(), json::type_error); - CHECK_THROWS_WITH_AS(json::to_msgpack(deep_discarded), (prefix + "cannot serialize discarded value to MessagePack").c_str(), json::type_error); - CHECK_THROWS_WITH_AS(json::to_ubjson(deep_discarded), (prefix + "cannot serialize discarded value to UBJSON").c_str(), json::type_error); - CHECK_THROWS_WITH_AS(json::to_bjdata(deep_discarded), (prefix + "cannot serialize discarded value to BJData").c_str(), json::type_error); - } - - SECTION("does not overflow the C++ stack") - { - const std::size_t depth = 100000; - const json j = json::parse(std::string(depth, '[') + "0" + std::string(depth, ']')); - - std::vector packed; - CHECK_NOTHROW(packed = json::to_cbor(j)); - CHECK(json::from_cbor(packed) == j); - - CHECK_NOTHROW(packed = json::to_msgpack(j)); - CHECK(json::from_msgpack(packed) == j); - - CHECK_NOTHROW(packed = json::to_ubjson(j)); - CHECK(json::from_ubjson(packed) == j); - - CHECK_NOTHROW(packed = json::to_ubjson(j, true, false)); - CHECK(json::from_ubjson(packed) == j); - - CHECK_NOTHROW(packed = json::to_bjdata(j)); - CHECK(json::from_bjdata(packed) == j); - } - - SECTION("regression test for https://issues.oss-fuzz.com/issues/566583014") - { - // 200000 nested one-element CBOR arrays, the innermost holding null; - // round-tripping this used to recurse once per level on the way back - // out through to_cbor(), deep enough to overflow the stack - std::vector v(200000, 0x81); - v.push_back(0xf6); - const json j = json::from_cbor(v); - CHECK(json::to_cbor(j) == v); - - // the MessagePack analogue: fixarray of 1 nesting down to nil - std::vector v_msgpack(200000, 0x91); - v_msgpack.push_back(0xc0); - const json j_msgpack = json::from_msgpack(v_msgpack); - CHECK(json::to_msgpack(j_msgpack) == v_msgpack); - } -} - diff --git a/tests/src/unit-large_json_binary_writers.cpp b/tests/src/unit-large_json_binary_writers.cpp new file mode 100644 index 000000000..339c62135 --- /dev/null +++ b/tests/src/unit-large_json_binary_writers.cpp @@ -0,0 +1,294 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; + +#include +#include +#include +#include +#include + +// split from unit-large_json.cpp so that both run in parallel and stay below +// the ctest timeout under Valgrind + +namespace +{ +json nested_array(const std::size_t depth, json leaf) +{ + json j = std::move(leaf); + for (std::size_t i = 0; i < depth; ++i) + { + json a = json::array(); + a.push_back(std::move(j)); + j = std::move(a); + } + return j; +} + +json nested_object(const std::size_t depth, json leaf) +{ + json j = std::move(leaf); + for (std::size_t i = 0; i < depth; ++i) + { + json o = json::object(); + o["k"] = std::move(j); + j = std::move(o); + } + return j; +} +} // namespace + +TEST_CASE("issue #5392 - binary writers on deeply nested values") +{ + // 200 is past the point where the writers stop recursing, and still + // shallow enough that from_* and operator== (which still recurse) are fine. + const json deep_array = nested_array(200, json(0)); + const json deep_object = nested_object(200, json("x")); + const json empty_array = nested_array(200, json::array()); + const json empty_object = nested_object(200, json::object()); + const json mixed = nested_object(80, nested_array(80, json(true))); + + SECTION("roundtrip past the recursion bound") + { + CHECK(json::from_cbor(json::to_cbor(deep_array)) == deep_array); + CHECK(json::from_msgpack(json::to_msgpack(deep_array)) == deep_array); + CHECK(json::from_ubjson(json::to_ubjson(deep_array)) == deep_array); + CHECK(json::from_ubjson(json::to_ubjson(deep_array, true, false)) == deep_array); + CHECK(json::from_ubjson(json::to_ubjson(deep_array, true, true)) == deep_array); + CHECK(json::from_bjdata(json::to_bjdata(deep_array)) == deep_array); + CHECK(json::from_bon8(json::to_bon8(deep_array)) == deep_array); + + CHECK(json::from_cbor(json::to_cbor(deep_object)) == deep_object); + CHECK(json::from_msgpack(json::to_msgpack(deep_object)) == deep_object); + CHECK(json::from_ubjson(json::to_ubjson(deep_object)) == deep_object); + CHECK(json::from_ubjson(json::to_ubjson(deep_object, true, true)) == deep_object); + CHECK(json::from_bjdata(json::to_bjdata(deep_object)) == deep_object); + CHECK(json::from_bon8(json::to_bon8(deep_object)) == deep_object); + + CHECK(json::from_cbor(json::to_cbor(empty_array)) == empty_array); + CHECK(json::from_msgpack(json::to_msgpack(empty_array)) == empty_array); + CHECK(json::from_ubjson(json::to_ubjson(empty_array)) == empty_array); + CHECK(json::from_ubjson(json::to_ubjson(empty_array, true, true)) == empty_array); + CHECK(json::from_bon8(json::to_bon8(empty_array)) == empty_array); + + CHECK(json::from_cbor(json::to_cbor(empty_object)) == empty_object); + CHECK(json::from_msgpack(json::to_msgpack(empty_object)) == empty_object); + CHECK(json::from_ubjson(json::to_ubjson(empty_object)) == empty_object); + CHECK(json::from_bon8(json::to_bon8(empty_object)) == empty_object); + + CHECK(json::from_cbor(json::to_cbor(mixed)) == mixed); + CHECK(json::from_msgpack(json::to_msgpack(mixed)) == mixed); + CHECK(json::from_ubjson(json::to_ubjson(mixed)) == mixed); + CHECK(json::from_bjdata(json::to_bjdata(mixed)) == mixed); + CHECK(json::from_bon8(json::to_bon8(mixed)) == mixed); + } + + SECTION("the two ways of writing a value meet at the bound") + { + for (std::size_t depth = 120; depth <= 140; ++depth) + { + CAPTURE(depth) + + const json array = nested_array(depth, json(7)); + CHECK(json::from_cbor(json::to_cbor(array)) == array); + CHECK(json::from_msgpack(json::to_msgpack(array)) == array); + CHECK(json::from_ubjson(json::to_ubjson(array, true, true)) == array); + CHECK(json::from_bon8(json::to_bon8(array)) == array); + + const json object = nested_object(depth, json(7)); + CHECK(json::from_cbor(json::to_cbor(object)) == object); + CHECK(json::from_msgpack(json::to_msgpack(object)) == object); + CHECK(json::from_bjdata(json::to_bjdata(object)) == object); + CHECK(json::from_bon8(json::to_bon8(object)) == object); + } + } + + SECTION("a BJData ndarray below the bound is still an ndarray") + { + const json ndarray = json({{"_ArrayType_", "uint8"}, {"_ArraySize_", {2, 3}}, {"_ArrayData_", {1, 2, 3, 4, 5, 6}}}); + const json invalid = json({{"_ArrayType_", "nope"}, {"_ArraySize_", {1}}, {"_ArrayData_", {1}}}); + + const json deep_ndarray = nested_array(140, ndarray); + const json deep_invalid = nested_array(140, invalid); + + CHECK(json::from_bjdata(json::to_bjdata(deep_ndarray)) == deep_ndarray); + CHECK(json::from_bjdata(json::to_bjdata(deep_invalid)) == deep_invalid); + CHECK(json::from_bjdata(json::to_bjdata(ndarray)) == ndarray); + } + + SECTION("byte-exact across the switch-over") + { + // nested one-element arrays around the recursion bound: the exact + // bytes a writer produces do not depend on whether it stayed on the + // call stack or moved to the heap one partway through + for (const std::size_t depth : + { + nlohmann::detail::recursion_depth_limit() - 1, nlohmann::detail::recursion_depth_limit(), + nlohmann::detail::recursion_depth_limit() + 1, nlohmann::detail::recursion_depth_limit() + 2 + }) + { + CAPTURE(depth) + const json array = nested_array(depth, json(0)); + + std::vector expected_cbor(depth, 0x81); + expected_cbor.push_back(0x00); + CHECK(json::to_cbor(array) == expected_cbor); + + std::vector expected_msgpack(depth, 0x91); + expected_msgpack.push_back(0x00); + CHECK(json::to_msgpack(array) == expected_msgpack); + + std::string expected_ubjson(depth, '['); + expected_ubjson += "i"; + expected_ubjson += '\0'; + expected_ubjson.append(depth, ']'); + const auto packed_ubjson = json::to_ubjson(array); + CHECK(std::string(packed_ubjson.begin(), packed_ubjson.end()) == expected_ubjson); + + std::vector expected_bon8(depth, 0x81); + expected_bon8.push_back(0x90); + CHECK(json::to_bon8(array) == expected_bon8); + } + } + + SECTION("a deep object, and a BJData ndarray, past the recursion bound") + { + const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 50; + + const json object = nested_object(depth, json(42)); + CHECK(json::from_cbor(json::to_cbor(object)) == object); + CHECK(json::from_msgpack(json::to_msgpack(object)) == object); + CHECK(json::from_ubjson(json::to_ubjson(object, true, true)) == object); + CHECK(json::from_bjdata(json::to_bjdata(object)) == object); + CHECK(json::from_bon8(json::to_bon8(object)) == object); + + const json ndarray = json({{"_ArrayType_", "uint8"}, {"_ArraySize_", {2, 3}}, {"_ArrayData_", {1, 2, 3, 4, 5, 6}}}); + const json deep_ndarray = nested_array(depth, ndarray); + CHECK(json::from_bjdata(json::to_bjdata(deep_ndarray)) == deep_ndarray); + } + + SECTION("a discarded value past the recursion bound still throws type_error.321") + { + const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 50; + const json discarded_leaf(json::value_t::discarded); + const json deep_discarded = nested_array(depth, discarded_leaf); + + // with diagnostics, the message names the path to the discarded leaf +#if JSON_DIAGNOSTICS + std::string path; + for (std::size_t i = 0; i < depth; ++i) + { + path += "/0"; + } + const std::string prefix = "[json.exception.type_error.321] (" + path + ") "; +#else + const std::string prefix = "[json.exception.type_error.321] "; +#endif + + CHECK_THROWS_WITH_AS(json::to_cbor(deep_discarded), (prefix + "cannot serialize discarded value to CBOR").c_str(), json::type_error); + CHECK_THROWS_WITH_AS(json::to_msgpack(deep_discarded), (prefix + "cannot serialize discarded value to MessagePack").c_str(), json::type_error); + CHECK_THROWS_WITH_AS(json::to_ubjson(deep_discarded), (prefix + "cannot serialize discarded value to UBJSON").c_str(), json::type_error); + CHECK_THROWS_WITH_AS(json::to_bjdata(deep_discarded), (prefix + "cannot serialize discarded value to BJData").c_str(), json::type_error); + } + + SECTION("BON8 past the recursion bound: discarded values and errors") + { + const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 50; + + // BON8 rejects a discarded value (type_error.321), deep or not + const json deep_discarded = nested_array(depth, json(json::value_t::discarded)); + CHECK_THROWS_AS(json::to_bon8(deep_discarded), json::type_error); + + // errors are thrown from below the bound as from above it + const json deep_invalid_string = nested_array(depth, json(std::string("\x80"))); + CHECK_THROWS_AS(json::to_bon8(deep_invalid_string), json::type_error); + + json deep_invalid_key = json::object(); + deep_invalid_key[std::string("\x80")] = 1; + deep_invalid_key = nested_object(depth, deep_invalid_key); + CHECK_THROWS_AS(json::to_bon8(deep_invalid_key), json::type_error); + + const json deep_too_large = nested_array(depth, json(9223372036854775808u)); + CHECK_THROWS_AS(json::to_bon8(deep_too_large), json::out_of_range); + } + + SECTION("does not overflow the C++ stack") + { + const std::size_t depth = 100000; + const json j = json::parse(std::string(depth, '[') + "0" + std::string(depth, ']')); + + std::vector packed; + CHECK_NOTHROW(packed = json::to_cbor(j)); + CHECK(json::from_cbor(packed) == j); + + CHECK_NOTHROW(packed = json::to_msgpack(j)); + CHECK(json::from_msgpack(packed) == j); + + CHECK_NOTHROW(packed = json::to_ubjson(j)); + CHECK(json::from_ubjson(packed) == j); + + CHECK_NOTHROW(packed = json::to_ubjson(j, true, false)); + CHECK(json::from_ubjson(packed) == j); + + CHECK_NOTHROW(packed = json::to_bjdata(j)); + CHECK(json::from_bjdata(packed) == j); + + CHECK_NOTHROW(packed = json::to_bon8(j)); + CHECK(json::from_bon8(packed) == j); + } + + SECTION("BON8 containers with more than four elements past the recursion bound") + { + // such containers are closed with 0xFE, which the iterative writer + // must emit when it leaves them; strings next to each other must + // still be separated with 0xFF + const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 50; + json wide_array = json::array({1, "a", "b", json::array(), 5}); + json wide_object = json({{"k1", "v"}, {"k2", 2}, {"k3", ""}, {"k4", json::object()}, {"k5", nullptr}}); + for (std::size_t i = 0; i < depth; ++i) + { + wide_array = json::array({"s", wide_array, "t", wide_object, true}); + wide_object = json({{"a", wide_object}, {"b", "x"}, {"c", "y"}, {"d", wide_array.size()}, {"e", "z"}}); + } + + CHECK(json::from_bon8(json::to_bon8(wide_array)) == wide_array); + CHECK(json::from_bon8(json::to_bon8(wide_object)) == wide_object); + } + + SECTION("regression test for https://issues.oss-fuzz.com/issues/566583014") + { + // 200000 nested one-element CBOR arrays, the innermost holding null; + // round-tripping this used to recurse once per level on the way back + // out through to_cbor(), deep enough to overflow the stack + std::vector v(200000, 0x81); + v.push_back(0xf6); + const json j = json::from_cbor(v); + CHECK(json::to_cbor(j) == v); + + // the MessagePack analogue: fixarray of 1 nesting down to nil + std::vector v_msgpack(200000, 0x91); + v_msgpack.push_back(0xc0); + const json j_msgpack = json::from_msgpack(v_msgpack); + CHECK(json::to_msgpack(j_msgpack) == v_msgpack); + } + + SECTION("regression test for https://issues.oss-fuzz.com/issues/572238015") + { + // the BON8 analogue: 200000 nested one-element arrays down to null + std::vector v(200000, 0x81); + v.push_back(0xFA); + const json j = json::from_bon8(v); + CHECK(json::to_bon8(j) == v); + } +} + diff --git a/tests/src/unit-large_json_conversions.cpp b/tests/src/unit-large_json_conversions.cpp new file mode 100644 index 000000000..1fd30aa89 --- /dev/null +++ b/tests/src/unit-large_json_conversions.cpp @@ -0,0 +1,156 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; + +#include +#include +#include +#include + +// split from unit-large_json.cpp so that both run in parallel and stay below +// the ctest timeout under Valgrind + +namespace +{ + +// The text of a value nested depth levels deep around the number 0. Level i is +// an array if pattern[i % pattern.size()] is '[', and otherwise an object with +// the single member "a", which every object type enumerates in the same order. +std::string nested_text(std::size_t depth, const std::string& pattern) +{ + std::string text; + std::string closing; + for (std::size_t i = 0; i < depth; ++i) + { + const bool array = pattern[i % pattern.size()] == '['; + text += array ? "[" : "{\"a\":"; + closing += array ? ']' : '}'; + } + text += '0'; + text.append(closing.rbegin(), closing.rend()); + return text; +} + +} // namespace + +TEST_CASE("issue #5650 - stack overflow converting between specializations") +{ + // deep enough to exhaust the call stack, but small enough to stay cheap: + // parsing is iterative, so building the values below costs little + const std::size_t depth = 100000; + + const std::vector patterns = {"[", "{", "[{"}; + + SECTION("json to ordered_json") + { + for (const auto& pattern : patterns) + { + CAPTURE(pattern) + const std::string text = nested_text(depth, pattern); + const json j = json::parse(text); + + const nlohmann::ordered_json converted = j; + CHECK(converted.dump() == text); + } + } + + SECTION("ordered_json to json") + { + for (const auto& pattern : patterns) + { + CAPTURE(pattern) + const std::string text = nested_text(depth, pattern); + const nlohmann::ordered_json o = nlohmann::ordered_json::parse(text); + + const json converted = o; + CHECK(converted.dump() == text); + } + } + + SECTION("get()") + { + for (const auto& pattern : patterns) + { + CAPTURE(pattern) + const std::string text = nested_text(depth, pattern); + const json j = json::parse(text); + + CHECK(j.get().dump() == text); + } + } + + SECTION("depths around the bound of the recursive descent") + { + for (std::size_t d = 1; d <= 300; ++d) + { + CAPTURE(d) + for (const auto& pattern : patterns) + { + CAPTURE(pattern) + const std::string text = nested_text(d, pattern); + const json j = json::parse(text); + + const nlohmann::ordered_json converted = j; + CHECK(converted.dump() == text); + const json back = converted; + CHECK(back.dump() == text); + } + } + } + + SECTION("values below the bound are converted as values above it") + { + // Bury a value below the bound, where it is converted without the + // call stack, and compare it with the same value converted on its + // own by the containers' range constructors. Its objects have + // members that the two object types enumerate in different orders. + const auto bury = [](nlohmann::ordered_json value) + { + for (std::size_t i = 0; i < 200; ++i) + { + value = nlohmann::ordered_json::array({std::move(value)}); + } + return value; + }; + const auto dig = [](const json & value) + { + const json* current = &value; + for (std::size_t i = 0; i < 200; ++i) + { + current = ¤t->at(0); + } + return current; + }; + + nlohmann::ordered_json value = nlohmann::ordered_json::object(); + value["z"] = {1, -2, 3U, 4.5, true, nullptr, "six", nlohmann::ordered_json::binary({7, 8}, 9), + nlohmann::ordered_json::binary({10}), nlohmann::ordered_json::array(), nlohmann::ordered_json::object() + }; + value["y"] = {{"x", {{"w", 1}, {"v", 2}}}, {"u", {3, {{"t", 4}, {"s", 5}}}}}; + value["r"] = nlohmann::ordered_json::array({nlohmann::ordered_json(nlohmann::ordered_json::value_t::discarded)}); + + const json converted_above = value; + const json buried = bury(value); + const json& converted_below = *dig(buried); + + CHECK(converted_below.dump() == converted_above.dump()); + CHECK(converted_below.at("z").at(7).get_binary().subtype() == 9); + CHECK_FALSE(converted_below.at("z").at(8).get_binary().has_subtype()); + CHECK(converted_below.at("r").at(0).is_discarded()); + + // a discarded value is never equal to anything, so compare the rest + value.erase("r"); + const json without_discarded_above = value; + const json without_discarded_buried = bury(value); + CHECK(*dig(without_discarded_buried) == without_discarded_above); + } +} diff --git a/tests/src/unit-locale-cpp.cpp b/tests/src/unit-locale-cpp.cpp index 4865e5687..b665b6531 100644 --- a/tests/src/unit-locale-cpp.cpp +++ b/tests/src/unit-locale-cpp.cpp @@ -260,10 +260,11 @@ struct LocaleSwitchingSax final: public nlohmann::json_sax TEST_CASE("locale changes between lexer construction and number conversion (#5198)") { - // The numbers are chosen so that the conversion also takes the strtod - // fallback, which honors the locale that is current at conversion time: - // too many significant digits for Clinger's fast path, an underflow that - // std::from_chars rejects, and a plain value. + // float and double are converted without the locale. A long double that + // is not binary64 can take the strtold fallback, which honors the locale + // that is current at conversion time. The numbers are chosen so that it + // does: too many significant digits for Clinger's fast path, an underflow + // that std::from_chars rejects, and a plain value. const std::vector numbers = {"3.14159265358979323846", "1.5e-400", "12.34", "-0.000123456789012345678"}; std::string text = "["; for (const auto& n : numbers) @@ -327,7 +328,8 @@ TEST_CASE("locale changes between lexer construction and number conversion (#519 } } - // a long double goes through std::strtold unless std::from_chars supports it + // a long double goes through std::strtold unless it is binary64 or + // std::from_chars supports it { bool switched = false; const auto cb = [&](int /*depth*/, long_double_json::parse_event_t event, long_double_json& /*parsed*/) noexcept @@ -353,8 +355,15 @@ TEST_CASE("locale with a multi-byte decimal point") { // Some locales use a decimal point that is not a single character, e.g. // U+066B ARABIC DECIMAL SEPARATOR (two bytes in UTF-8). It cannot be - // substituted in place for '.', so the strtod fallback stops early. The - // conversion must still terminate rather than retry forever. + // substituted in place for '.', so the strtold fallback (only for long + // double formats other than binary64) converts a copy of the token with + // the whole decimal point instead (#5660). The values must be those of the + // "C" locale. + using long_double_json = nlohmann::basic_json; + const char* const long_double_numbers = "[3.14159265358979323846, 1.5e-400, -0.000123456789012345678]"; + REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr); + const long_double_json expected_long_double = long_double_json::parse(long_double_numbers); + const std::array names = {{"ar_EG.UTF-8", "ar_SA.UTF-8", "fa_IR.UTF-8", "ps_AF.UTF-8", "ar_EG", "fa_IR"}}; bool tested = false; for (const char* name : names) @@ -372,12 +381,20 @@ TEST_CASE("locale with a multi-byte decimal point") tested = true; // too many significant digits for Clinger's fast path, and an underflow - // that std::from_chars rejects: both reach the strtod fallback + // that std::from_chars rejects: double does not depend on the locale json j; CHECK_NOTHROW(j = json::parse("[3.14159265358979323846, 1.5e-400, -0.000123456789012345678]")); CHECK(j.is_array()); + CHECK(j[0] == 3.14159265358979323846); + CHECK(j[1] == 0.0); + CHECK(j[2] == -0.000123456789012345678); CHECK(json::accept("3.14159265358979323846")); + // a long double that reaches the strtold fallback is not truncated + long_double_json ld; + CHECK_NOTHROW(ld = long_double_json::parse(long_double_numbers)); + CHECK(ld == expected_long_double); + // a value the locale-independent paths convert is not affected CHECK(json::parse("12.5") == 12.5); } diff --git a/tests/src/unit-merge_diff.cpp b/tests/src/unit-merge_diff.cpp new file mode 100644 index 000000000..77fa58768 --- /dev/null +++ b/tests/src/unit-merge_diff.cpp @@ -0,0 +1,1024 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; +using nlohmann::ordered_json; +#ifdef JSON_TEST_NO_GLOBAL_UDLS + using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) +#endif + +#include +#include + +namespace +{ +// merge_diff() recursing once per nesting level, without the bound on its +// descent; only usable on values nested a few hundred levels deep +template +BasicJsonType reference_merge_diff(const BasicJsonType& source, const BasicJsonType& target) +{ + if (!source.is_object() || !target.is_object()) + { + return target; + } + + BasicJsonType result = BasicJsonType::object(); + for (auto it = source.begin(); it != source.end(); ++it) + { + const auto itf = target.find(it.key()); + if (itf == target.end()) + { + result[it.key()] = nullptr; + } + else if (it.value().is_object() && itf.value().is_object()) + { + auto diff = reference_merge_diff(it.value(), itf.value()); + if (!diff.empty()) + { + result[it.key()] = std::move(diff); + } + } + else if (it.value() != itf.value()) + { + result[it.key()] = itf.value(); + } + } + for (auto it = target.begin(); it != target.end(); ++it) + { + if (source.find(it.key()) == source.end()) + { + result[it.key()] = it.value(); + } + } + return result; +} + +// objects nested `depth` levels deep under the key "a", with members before +// and after it that differ by `variant` on the way down, so that diffing two +// variants removes, adds and replaces members on many levels, and diffs nested +// objects that are equal, that differ, or that are not objects in one of the +// variants; only variant 2 has members that are null +template +BasicJsonType nested_objects(const std::size_t depth, const int variant) +{ + BasicJsonType value = BasicJsonType::object({{"v", variant}}); + for (std::size_t i = 0; i < depth; ++i) + { + BasicJsonType object = BasicJsonType::object(); + if ((i + static_cast(variant)) % 3 == 0) + { + object["s" + std::to_string(variant)] = i; + } + object["c"] = i % 4 == 0 ? variant : 0; + if (i % 5 == 0) + { + if (variant == 0) + { + object["o"] = BasicJsonType::object({{"p", i}}); + } + else if (variant == 1) + { + object["o"] = BasicJsonType::array({i}); + } + else + { + object["o"] = nullptr; + } + } + if (variant == 2 && i % 4 == 2) + { + object["n"] = nullptr; + } + object["a"] = std::move(value); + if (i % 6 == 0) + { + object["d"] = BasicJsonType::object({{"e", variant == 1 ? 1 : 0}}); + } + if (variant == 1 && i % 7 == 0) + { + object["z"] = "z"; + } + value = std::move(object); + } + return value; +} +} // namespace + +TEST_CASE("JSON Merge Patch diff") +{ + SECTION("examples from RFC 7396") + { + SECTION("Section 1") + { + json document = R"({ + "a": "b", + "c": { + "d": "e", + "f": "g" + } + })"_json; + + json expected = R"({ + "a": "z", + "c": { + "d": "e" + } + })"_json; + + json const expected_patch = R"({ + "a": "z", + "c": { + "f": null + } + })"_json; + + auto patch = json::merge_diff(document, expected); + CHECK(patch == expected_patch); + + document.merge_patch(patch); + CHECK(document == expected); + } + + SECTION("Section 3") + { + json document = R"({ + "title": "Goodbye!", + "author": { + "givenName": "John", + "familyName": "Doe" + }, + "tags": [ + "example", + "sample" + ], + "content": "This will be unchanged" + })"_json; + + json expected = R"({ + "title": "Hello!", + "author": { + "givenName": "John" + }, + "tags": [ + "example" + ], + "content": "This will be unchanged", + "phoneNumber": "+01-123-456-7890" + })"_json; + + json const expected_patch = R"({ + "title": "Hello!", + "phoneNumber": "+01-123-456-7890", + "author": { + "familyName": null + }, + "tags": [ + "example" + ] + })"_json; + + auto patch = json::merge_diff(document, expected); + CHECK(patch == expected_patch); + + document.merge_patch(patch); + CHECK(document == expected); + } + + SECTION("Appendix A") + { + SECTION("Example 1") + { + json original = R"({"a":"b"})"_json; + json result = R"({"a":"c"})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":"c"})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 2") + { + json original = R"({"a":"b"})"_json; + json result = R"({"a":"b", "b":"c"})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"b":"c"})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 3") + { + json original = R"({"a":"b"})"_json; + json result = R"({})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":null})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 4") + { + json original = R"({"a":"b","b":"c"})"_json; + json result = R"({"b":"c"})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":null})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 5") + { + json original = R"({"a":["b"]})"_json; + json result = R"({"a":"c"})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":"c"})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 6") + { + json original = R"({"a":"c"})"_json; + json result = R"({"a":["b"]})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":["b"]})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 7") + { + json original = R"({"a":{"b": "c"}})"_json; + json result = R"({"a": {"b": "d"}})"_json; + + auto patch = json::merge_diff(original, result); + // differs from the RFC 7396, "c": null is not in the patch + // because neither the source nor the target have the "c" key + CHECK(patch == R"({"a":{"b":"d"}})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 8") + { + json original = R"({"a":[{"b":"c"}]})"_json; + json result = R"({"a":[1]})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":[1]})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 9") + { + json original = R"(["a","b"])"_json; + json result = R"(["c","d"])"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"(["c","d"])"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 10") + { + json original = R"({"a":"b"})"_json; + json result = R"(["c"])"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"(["c"])"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 11") + { + json original = R"({"a":"foo"})"_json; + json result = R"(null)"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"(null)"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 12") + { + json original = R"({"a":"foo"})"_json; + json result = R"("bar")"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"("bar")"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 13") + { + json original = R"({"e":null})"_json; + json result = R"({"e":null,"a":1})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":1})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 14") + { + json original = R"([1,2])"_json; + json result = R"({"a":"b"})"_json; + + auto patch = json::merge_diff(original, result); + // differs from the RFC 7396, "c": null is not in the patch + // because neither the source nor the target have the "c" key + CHECK(patch == R"({"a":"b"})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 15") + { + json original = R"({})"_json; + json result = R"({"a":{"bb":{}}})"_json; + + auto patch = json::merge_diff(original, result); + // differs from the RFC 7396, "ccc": null is not in the patch + // because neither the source nor the target have the "ccc" key + CHECK(patch == R"({"a":{"bb":{}}})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + } + } + + SECTION("null values") + { + SECTION("object with null value to object") + { + json original = R"({"a":null})"_json; + json result = R"({"a":{"b":"c"}})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":{"b":"c"}})"_json); + + original.merge_patch(patch); + CHECK(original == R"({"a":{"b":"c"}})"_json); + } + + SECTION("object to object with null value") + { + json original = R"({"a":{"b":"c"}})"_json; + json result = R"({"a":null})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":null})"_json); + + original.merge_patch(patch); + CHECK(original == R"({})"_json); + } + + SECTION("primitive to object with null value") + { + json original = R"({"a":1})"_json; + json result = R"({"a":null})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":null})"_json); + + original.merge_patch(patch); + CHECK(original == R"({})"_json); + } + + SECTION("nested primitive to object with null value") + { + json original = R"({"a":{"b":1}})"_json; + json result = R"({"a":{"b":null}})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":{"b":null}})"_json); + + original.merge_patch(patch); + CHECK(original == R"({"a":{}})"_json); + } + + SECTION("array value to object with null value") + { + json original = R"({"a":[1,2]})"_json; + json result = R"({"a":null})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":null})"_json); + + original.merge_patch(patch); + CHECK(original == R"({})"_json); + } + + SECTION("nested object to object with null value") + { + json original = R"({"a":{"b":{"c":"d"}}})"_json; + json result = R"({"a":{"b":null}})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":{"b":null}})"_json); + + original.merge_patch(patch); + CHECK(original == R"({"a":{}})"_json); + } + + SECTION("nested primitive to object with null value with sibling") + { + json original = R"({"x":"y","a":{"b":"c"}})"_json; + json result = R"({"x":"y","a":{"b":null, "c":"d"}})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":{"b":null,"c":"d"}})"_json); + + original.merge_patch(patch); + CHECK(original == R"({"x":"y","a":{"c":"d"}})"_json); + } + + SECTION("empty object to object with null value") + { + json original = R"({})"_json; + json result = R"({"a":null})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":null})"_json); + + original.merge_patch(patch); + CHECK(original == R"({})"_json); + } + + SECTION("null to object with null value") + { + json original = R"(null)"_json; + json result = R"({"a":null})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":null})"_json); + + original.merge_patch(patch); + CHECK(original == R"({})"_json); + } + + SECTION("array to object with null value") + { + json original = R"([])"_json; + json result = R"({"a":null})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":null})"_json); + + original.merge_patch(patch); + CHECK(original == R"({})"_json); + } + + SECTION("empty object to nested null value") + { + json original = R"({})"_json; + json result = R"({"a":{"b":null}})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":{"b":null}})"_json); + + original.merge_patch(patch); + CHECK(original == R"({"a":{}})"_json); + } + + SECTION("object to nested null value") + { + json original = R"({"a": 1})"_json; + json result = R"({"a":{"b":null}})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":{"b":null}})"_json); + + original.merge_patch(patch); + CHECK(original == R"({"a":{}})"_json); + } + + SECTION("primitive to nested null value") + { + json original = R"(1)"_json; + json result = R"({"a":{"b":null}})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":{"b":null}})"_json); + + original.merge_patch(patch); + CHECK(original == R"({"a":{}})"_json); + } + + SECTION("object with null value unchanged") + { + json original = R"({"a":null})"_json; + json result = R"({"a":null})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("object with null value to empty object") + { + json original = R"({"a":null})"_json; + json result = R"({})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":null})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("object with null value to primitive") + { + json original = R"({"a":null})"_json; + json result = R"({"a":1})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":1})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("array to array with null value") + { + json original = R"({"a":[1]})"_json; + json result = R"({"a":[null]})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":[null]})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("empty object to array with null value") + { + json original = R"({})"_json; + json result = R"({"a":[null]})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":[null]})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("doubly nested primitive to null value") + { + json original = R"({"a":{"b":{"c":1}}})"_json; + json result = R"({"a":{"b":{"c":null}}})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":{"b":{"c":null}}})"_json); + + original.merge_patch(patch); + CHECK(original == R"({"a":{"b":{}}})"_json); + } + } + + SECTION("no change") + { + SECTION("object") + { + json original = R"({"a":"b","b":"c"})"_json; + json result = R"({"a":"b","b":"c"})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch.empty()); + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("empty object") + { + json original = R"({})"_json; + json result = R"({})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch.empty()); + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("array") + { + json original = R"([1,2,3])"_json; + json result = R"([1,2,3])"_json; + + auto patch = json::merge_diff(original, result); + CHECK(!patch.empty()); + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("null") + { + json original = R"(null)"_json; + json result = R"(null)"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch.empty()); + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("string") + { + json original = R"("ab")"_json; + json result = R"("ab")"_json; + + auto patch = json::merge_diff(original, result); + CHECK(!patch.empty()); + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("number") + { + json original = R"(42)"_json; + json result = R"(42)"_json; + + auto patch = json::merge_diff(original, result); + CHECK(!patch.empty()); + original.merge_patch(patch); + CHECK(original == result); + } + } + + SECTION("primitives") + { + SECTION("string") + { + json original = R"("a")"_json; + json result = R"("b")"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"("b")"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("number") + { + json original = R"(1)"_json; + json result = R"(2)"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"(2)"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("boolean") + { + json original = R"(false)"_json; + json result = R"(true)"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"(true)"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("object to primitive") + { + json original = R"({"a":{"b":"c"}})"_json; + json result = R"({"a":1})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":1})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("primitive to object") + { + json original = R"({"a":1})"_json; + json result = R"({"a":{"b":"c"}})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":{"b":"c"}})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("object to empty string") + { + json original = R"({"a":{"b":"c"}})"_json; + json result = R"({"a":""})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":""})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("object to zero") + { + json original = R"({"a":{"b":"c"}})"_json; + json result = R"({"a":0})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":0})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("object to empty object") + { + json original = R"({"a":{"b":"c"}})"_json; + json result = R"({"a":{}})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":{"b":null}})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + } + + SECTION("arrays") + { + SECTION("array to array") + { + json original = R"([1,2,3])"_json; + json result = R"([1,2,4])"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"([1,2,4])"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("array to empty array") + { + json original = R"([1,2,3])"_json; + json result = R"([])"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"([])"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("empty array to array") + { + json original = R"([])"_json; + json result = R"([1,2,3])"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"([1,2,3])"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("object to empty array") + { + json original = R"({"a":{"b":"c"}})"_json; + json result = R"({"a":[]})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":[]})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("same keys") + { + json original = R"(["a","b"])"_json; + json result = R"({"a":1,"b":2})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":1,"b":2})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + } + + SECTION("ordered_json") + { + SECTION("changed value keeps member order") + { + auto original = ordered_json::parse(R"({"c":1,"b":2,"a":3})"); + auto result = ordered_json::parse(R"({"c":1,"b":5,"a":3})"); + + auto patch = ordered_json::merge_diff(original, result); + CHECK(patch.dump() == R"({"b":5})"); + + original.merge_patch(patch); + CHECK(original == result); + CHECK(original.dump() == R"({"c":1,"b":5,"a":3})"); + } + + SECTION("added members follow target order") + { + auto original = ordered_json::parse(R"({"b":1})"); + auto result = ordered_json::parse(R"({"b":1,"z":2,"a":3})"); + + auto patch = ordered_json::merge_diff(original, result); + CHECK(patch.dump() == R"({"z":2,"a":3})"); + + original.merge_patch(patch); + CHECK(original == result); + CHECK(original.dump() == R"({"b":1,"z":2,"a":3})"); + } + + SECTION("removed members follow source order") + { + auto original = ordered_json::parse(R"({"c":1,"b":2,"a":3})"); + auto result = ordered_json::parse(R"({"b":2})"); + + auto patch = ordered_json::merge_diff(original, result); + CHECK(patch.dump() == R"({"c":null,"a":null})"); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("nested objects keep member order") + { + auto original = ordered_json::parse(R"({"z":{"y":1,"x":2},"a":0})"); + auto result = ordered_json::parse(R"({"z":{"y":1,"x":3,"w":4},"a":0})"); + + auto patch = ordered_json::merge_diff(original, result); + CHECK(patch.dump() == R"({"z":{"x":3,"w":4}})"); + + original.merge_patch(patch); + CHECK(original == result); + CHECK(original.dump() == R"({"z":{"y":1,"x":3,"w":4},"a":0})"); + } + + SECTION("reordered members cannot be expressed") + { + auto original = ordered_json::parse(R"({"a":1,"b":2})"); + auto result = ordered_json::parse(R"({"b":2,"a":1})"); + + auto patch = ordered_json::merge_diff(original, result); + CHECK(patch.empty()); + + original.merge_patch(patch); + CHECK(original != result); + CHECK(original.dump() == R"({"a":1,"b":2})"); + } + + SECTION("member added before existing ones is appended") + { + auto original = ordered_json::parse(R"({"b":1})"); + auto result = ordered_json::parse(R"({"a":0,"b":1})"); + + auto patch = ordered_json::merge_diff(original, result); + CHECK(patch.dump() == R"({"a":0})"); + + original.merge_patch(patch); + CHECK(original != result); + CHECK(original.dump() == R"({"b":1,"a":0})"); + } + } +} + +TEST_CASE("JSON Merge Patch diff of deeply nested values") +{ + SECTION("the patch is the same past the descent bound") + { + // depths on either side of the nesting depth up to which merge_diff() + // recurses (detail::recursion_depth_limit(), 128); not every depth up + // to 300, to keep the test fast under Valgrind + std::vector depths; + for (std::size_t depth = 0; depth <= 16; ++depth) + { + depths.push_back(depth); + } + for (std::size_t depth = 120; depth <= 136; ++depth) + { + depths.push_back(depth); + } + depths.push_back(300); + + for (const auto depth : depths) + { + CAPTURE(depth) + for (int from = 0; from < 3; ++from) + { + for (int to = 0; to < 3; ++to) + { + CAPTURE(from) + CAPTURE(to) + const auto source = nested_objects(depth, from); + const auto target = nested_objects(depth, to); + const auto patch = json::merge_diff(source, target); + CHECK(patch == reference_merge_diff(source, target)); + CHECK(patch.empty() == (from == to)); + + // a merge patch cannot set a member to null, so it only + // reproduces a target without null members + if (to != 2) + { + json patched = source; + patched.merge_patch(patch); + CHECK(patched == target); + } + + // the members of the patch also come in the same order + const auto ordered_source = nested_objects(depth, from); + const auto ordered_target = nested_objects(depth, to); + CHECK(ordered_json::merge_diff(ordered_source, ordered_target).dump() == reference_merge_diff(ordered_source, ordered_target).dump()); + } + } + } + } + + SECTION("a difference only in the innermost value is a patch nested as deeply") + { + for (std::size_t depth = 0; depth <= 300; ++depth) + { + CAPTURE(depth) + json source = {{"v", 1}}; + json target = {{"v", 2}}; + json expected = {{"v", 2}}; + for (std::size_t i = 0; i < depth; ++i) + { + source = json{{"a", std::move(source)}, {"b", i}}; + target = json{{"a", std::move(target)}, {"b", i}}; + expected = json{{"a", std::move(expected)}}; + } + CHECK(json::merge_diff(source, target) == expected); + } + } + + SECTION("values nested too deeply for the call stack") + { + // recursing once per nesting level would exhaust the call stack here. + // The patch is walked rather than compared as a whole, to keep the + // test fast. + const std::size_t depth = 100000; + std::string opening; + for (std::size_t i = 0; i < depth; ++i) + { + opening += R"({"a":)"; + } + const std::string closing(depth, '}'); + const auto source = json::parse(opening + R"({"v":1,"w":0})" + closing); + + const auto patch = json::merge_diff(source, json::parse(opening + R"({"v":2,"w":0})" + closing)); + std::size_t members = 0; + const json* p = &patch; + for (std::size_t i = 0; i < depth; ++i) + { + members += p->size(); + p = &p->at("a"); + } + CHECK(members == depth); + CHECK(*p == json({{"v", 2}})); + + CHECK(json::merge_diff(source, json::parse(opening + R"({"v":1,"w":0})" + closing)).empty()); + } +} diff --git a/tests/src/unit-msgpack-custom-types.cpp b/tests/src/unit-msgpack-custom-types.cpp new file mode 100644 index 000000000..5c4ab6173 --- /dev/null +++ b/tests/src/unit-msgpack-custom-types.cpp @@ -0,0 +1,296 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// MessagePack tests with basic_json specializations other than nlohmann::json; +// they are kept apart from unit-msgpack.cpp because every specialization adds +// many sections to the object file, and MinGW cannot link objects with more +// than 65535 sections (see the clang job in .github/workflows/windows.yml) + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; + +#include // SIZE_MAX, UINT32_MAX +#include +#include +#include +#include +#include +#include +#include + +// the fake sizes below do not fit into a 32-bit std::size_t +// with clang and libstdc++ 10, the std::filesystem::path conversion that +// C++17 builds consider for every string type is ambiguous for a class +// derived from std::string, so the string case is not tested there +#if !(defined(__clang__) && defined(_GLIBCXX_RELEASE) && _GLIBCXX_RELEASE < 11) + #define JSON_TEST_BEYOND_UINT32_STRING 1 +#endif + +#if SIZE_MAX > UINT32_MAX +template> +struct huge_array : std::vector +{ + using base = std::vector; + using base::base; + + bool fake_size = false; + + std::size_t size() const noexcept + { + if (fake_size) + { + return (std::numeric_limits::max)() + 1ULL; + } + + return base::size(); + } +}; + +using huge_array_json = nlohmann::json::with_array_t; + +TEST_CASE("MessagePack Size above uint32 for array") +{ + huge_array_json j = huge_array_json::array(); + + j.push_back(1); + j.push_back(2); + j.push_back(3); + + auto& array = j.get_ref(); + array.fake_size = true; + + // write into a caller-owned vector: to_msgpack(j) reserves space based on + // the (faked) element count, which fails with bad_alloc on Windows + std::vector result; + CHECK_THROWS_WITH_AS( + huge_array_json::to_msgpack(j, result), + "[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295", + json::out_of_range&); + + array.fake_size = false; +} + +template, + typename A = std::allocator>> + struct huge_map : std::map +{ + using base = std::map; + using base::base; + + bool fake_size = false; + + std::size_t size() const noexcept + { + if (fake_size) + { + return static_cast(UINT32_MAX) + 1ULL; + } + + return base::size(); + } +}; + +using huge_object_json = nlohmann::json::with_object_t; + +TEST_CASE("MessagePack Size above uint32 for object") +{ + + huge_object_json j = huge_object_json::object(); + + j["one"] = 1; + j["two"] = 2; + + auto& object = j.get_ref(); + object.fake_size = true; + + // write into a caller-owned vector: to_msgpack(j) reserves space based on + // the (faked) element count, which fails with bad_alloc on Windows + std::vector result; + CHECK_THROWS_WITH_AS( + huge_object_json::to_msgpack(j, result), + "[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295", + json::out_of_range&); + + object.fake_size = false; +} + +#ifdef JSON_TEST_BEYOND_UINT32_STRING +struct huge_string : std::string +{ + using std::string::string; + + std::size_t size() const noexcept // NOLINT(readability-convert-member-functions-to-static) + { + return static_cast(UINT32_MAX) + 1ULL; + } +}; + +using huge_string_json = nlohmann::json::with_string_t; + +TEST_CASE("MessagePack Size above uint32 for string") +{ + const huge_string_json j = "hello"; + + CHECK_THROWS_WITH_AS( + huge_string_json::to_msgpack(j), + "[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295", + json::out_of_range&); +} +#endif + +struct huge_binary : std::vector +{ + using std::vector::vector; + + std::size_t size() const noexcept // NOLINT(readability-convert-member-functions-to-static) + { + return static_cast(UINT32_MAX) + 1ULL; + } +}; + +using huge_binary_json = nlohmann::json::with_binary_t; + +TEST_CASE("MessagePack Size above uint32 for binary") +{ + + huge_binary_json j = huge_binary_json::binary(huge_binary{}); + + j.get_binary().push_back(0x01); + j.get_binary().push_back(0x02); + + CHECK_THROWS_WITH_AS( + huge_binary_json::to_msgpack(j), + "[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295", + json::out_of_range&); +} +#endif + +namespace +{ +// types that report a size beyond UINT32_MAX without allocating that much +// memory, so the MessagePack length limit can be tested cheaply; see the +// similar types in unit-bson.cpp +std::size_t beyond_uint32_size() +{ + return static_cast((std::numeric_limits::max)()) + 1; +} + +class beyond_uint32_binary_t : public std::vector +{ + public: + using std::vector::vector; + + size_type size() const noexcept // NOLINT(readability-convert-member-functions-to-static) + { + return beyond_uint32_size(); + } +}; + +#ifdef JSON_TEST_BEYOND_UINT32_STRING +class beyond_uint32_string_t : public std::string +{ + public: + using std::string::string; + + size_type size() const noexcept // NOLINT(readability-convert-member-functions-to-static) + { + return beyond_uint32_size(); + } +}; + +using beyond_uint32_string_json = nlohmann::json::with_string_t; +#endif + +using beyond_uint32_binary_json = nlohmann::json::with_binary_t; +} // namespace + +TEST_CASE("MessagePack lengths beyond UINT32_MAX cannot be serialized") +{ + // MessagePack stores the length of a string, binary value, array, or + // object in at most 32 bits; a larger one used to be written without any + // length at all +#if SIZE_MAX > UINT32_MAX + { + const char* const expected = "[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295"; + + const beyond_uint32_binary_json binary = beyond_uint32_binary_json::binary(beyond_uint32_binary_t{}); + CHECK_THROWS_WITH_AS(beyond_uint32_binary_json::to_msgpack(binary), expected, beyond_uint32_binary_json::out_of_range&); + + const beyond_uint32_binary_json ext = beyond_uint32_binary_json::binary(beyond_uint32_binary_t{}, 42); + CHECK_THROWS_WITH_AS(beyond_uint32_binary_json::to_msgpack(ext), expected, beyond_uint32_binary_json::out_of_range&); + +#ifdef JSON_TEST_BEYOND_UINT32_STRING + // created from its type rather than from a beyond_uint32_string_t: + // that would consider the std::filesystem::path conversion, which + // libstdc++ 10 cannot decide for a class derived from std::string + const beyond_uint32_string_json string(beyond_uint32_string_json::value_t::string); + CHECK_THROWS_WITH_AS(beyond_uint32_string_json::to_msgpack(string), expected, beyond_uint32_string_json::out_of_range&); +#endif + } +#endif +} + +TEST_CASE("MessagePack numbers use the active union member (see #5644)") +{ + // when number_integer_t is narrower than number_unsigned_t, to_msgpack() + // used to read the union member that was not the active one, writing + // wrong bytes for some values; std::int64_t/std::uint64_t (the default + // types, where both members have the same width) were not affected + using int32_json = nlohmann::json::with_integers_t; + using int16_json = nlohmann::json::with_integers_t; + + SECTION("number_integer_t = std::int32_t") + { + SECTION("6442450944 (uint 64; the low 32 bits used to be sign-extended)") + { + const int32_json j = 6442450944ULL; + CHECK(j.is_number_unsigned()); + + std::vector const expected{0xcf, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00}; + const auto result = int32_json::to_msgpack(j); + CHECK(result == expected); + CHECK(int32_json::from_msgpack(result) == j); + } + + SECTION("4294967496 (uint 64; the low 32 bits used to be the whole value)") + { + const int32_json j = 4294967496ULL; + CHECK(j.is_number_unsigned()); + + std::vector const expected{0xcf, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xc8}; + const auto result = int32_json::to_msgpack(j); + CHECK(result == expected); + CHECK(int32_json::from_msgpack(result) == j); + } + } + + SECTION("number_integer_t = std::int16_t, 98304 (uint 32)") + { + const int16_json j = 98304ULL; + CHECK(j.is_number_unsigned()); + + std::vector const expected{0xce, 0x00, 0x01, 0x80, 0x00}; + const auto result = int16_json::to_msgpack(j); + CHECK(result == expected); + CHECK(int16_json::from_msgpack(result) == j); + } + + SECTION("default types (std::int64_t/std::uint64_t) are unaffected") + { + const json j = 4294967496ULL; + CHECK(j.is_number_unsigned()); + + std::vector const expected{0xcf, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xc8}; + const auto result = json::to_msgpack(j); + CHECK(result == expected); + CHECK(json::from_msgpack(result) == j); + } +} diff --git a/tests/src/unit-msgpack.cpp b/tests/src/unit-msgpack.cpp index d29769738..2890a240c 100644 --- a/tests/src/unit-msgpack.cpp +++ b/tests/src/unit-msgpack.cpp @@ -2173,316 +2173,6 @@ TEST_CASE("MessagePack roundtrips" * doctest::skip()) } } -// the fake sizes below do not fit into a 32-bit std::size_t -// with clang and libstdc++ 10, the std::filesystem::path conversion that -// C++17 builds consider for every string type is ambiguous for a class -// derived from std::string, so the string case is not tested there -#if !(defined(__clang__) && defined(_GLIBCXX_RELEASE) && _GLIBCXX_RELEASE < 11) - #define JSON_TEST_BEYOND_UINT32_STRING 1 -#endif - -#if SIZE_MAX > UINT32_MAX -template> -struct huge_array : std::vector -{ - using base = std::vector; - using base::base; - - bool fake_size = false; - - std::size_t size() const noexcept - { - if (fake_size) - { - return (std::numeric_limits::max)() + 1ULL; - } - - return base::size(); - } -}; - -using huge_array_json = nlohmann::basic_json < - std::map, huge_array, std::string, bool, std::int64_t, std::uint64_t, - double, std::allocator, nlohmann::adl_serializer, - std::vector, void >; - -TEST_CASE("MessagePack Size above uint32 for array") -{ - huge_array_json j = huge_array_json::array(); - - j.push_back(1); - j.push_back(2); - j.push_back(3); - - auto& array = j.get_ref(); - array.fake_size = true; - - // write into a caller-owned vector: to_msgpack(j) reserves space based on - // the (faked) element count, which fails with bad_alloc on Windows - std::vector result; - CHECK_THROWS_WITH_AS( - huge_array_json::to_msgpack(j, result), - "[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295", - json::out_of_range&); - - array.fake_size = false; -} - -template, - typename A = std::allocator>> - struct huge_map : std::map -{ - using base = std::map; - using base::base; - - bool fake_size = false; - - std::size_t size() const noexcept - { - if (fake_size) - { - return static_cast(UINT32_MAX) + 1ULL; - } - - return base::size(); - } -}; - -using huge_object_json = nlohmann::basic_json < - huge_map, - std::vector, - std::string, - bool, - std::int64_t, - std::uint64_t, - double, - std::allocator, - nlohmann::adl_serializer, - std::vector, - void >; - -TEST_CASE("MessagePack Size above uint32 for object") -{ - - huge_object_json j = huge_object_json::object(); - - j["one"] = 1; - j["two"] = 2; - - auto& object = j.get_ref(); - object.fake_size = true; - - // write into a caller-owned vector: to_msgpack(j) reserves space based on - // the (faked) element count, which fails with bad_alloc on Windows - std::vector result; - CHECK_THROWS_WITH_AS( - huge_object_json::to_msgpack(j, result), - "[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295", - json::out_of_range&); - - object.fake_size = false; -} - -#ifdef JSON_TEST_BEYOND_UINT32_STRING -struct huge_string : std::string -{ - using std::string::string; - - std::size_t size() const noexcept // NOLINT(readability-convert-member-functions-to-static) - { - return static_cast(UINT32_MAX) + 1ULL; - } -}; - -using huge_string_json = nlohmann::basic_json < - std::map, - std::vector, - huge_string, - bool, - std::int64_t, - std::uint64_t, - double, - std::allocator, - nlohmann::adl_serializer, - std::vector, - void >; - -TEST_CASE("MessagePack Size above uint32 for string") -{ - const huge_string_json j = "hello"; - - CHECK_THROWS_WITH_AS( - huge_string_json::to_msgpack(j), - "[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295", - json::out_of_range&); -} -#endif - -struct huge_binary : std::vector -{ - using std::vector::vector; - - std::size_t size() const noexcept // NOLINT(readability-convert-member-functions-to-static) - { - return static_cast(UINT32_MAX) + 1ULL; - } -}; - -using huge_binary_json = nlohmann::basic_json < - std::map, - std::vector, - std::string, - bool, - std::int64_t, - std::uint64_t, - double, - std::allocator, - nlohmann::adl_serializer, - huge_binary, - void >; - -TEST_CASE("MessagePack Size above uint32 for binary") -{ - - huge_binary_json j = huge_binary_json::binary(huge_binary{}); - - j.get_binary().push_back(0x01); - j.get_binary().push_back(0x02); - - CHECK_THROWS_WITH_AS( - huge_binary_json::to_msgpack(j), - "[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295", - json::out_of_range&); -} -#endif - -namespace -{ -// types that report a size beyond UINT32_MAX without allocating that much -// memory, so the MessagePack length limit can be tested cheaply; see the -// similar types in unit-bson.cpp -std::size_t beyond_uint32_size() -{ - return static_cast((std::numeric_limits::max)()) + 1; -} - -class beyond_uint32_binary_t : public std::vector -{ - public: - using std::vector::vector; - - size_type size() const noexcept // NOLINT(readability-convert-member-functions-to-static) - { - return beyond_uint32_size(); - } -}; - -#ifdef JSON_TEST_BEYOND_UINT32_STRING -class beyond_uint32_string_t : public std::string -{ - public: - using std::string::string; - - size_type size() const noexcept // NOLINT(readability-convert-member-functions-to-static) - { - return beyond_uint32_size(); - } -}; - -using beyond_uint32_string_json = nlohmann::basic_json < - std::map, std::vector, beyond_uint32_string_t, bool, std::int64_t, std::uint64_t, - double, std::allocator, nlohmann::adl_serializer, std::vector, void >; -#endif - -using beyond_uint32_binary_json = nlohmann::basic_json < - std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, - double, std::allocator, nlohmann::adl_serializer, beyond_uint32_binary_t, void >; -} // namespace - -TEST_CASE("MessagePack lengths beyond UINT32_MAX cannot be serialized") -{ - // MessagePack stores the length of a string, binary value, array, or - // object in at most 32 bits; a larger one used to be written without any - // length at all -#if SIZE_MAX > UINT32_MAX - { - const char* const expected = "[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295"; - - const beyond_uint32_binary_json binary = beyond_uint32_binary_json::binary(beyond_uint32_binary_t{}); - CHECK_THROWS_WITH_AS(beyond_uint32_binary_json::to_msgpack(binary), expected, beyond_uint32_binary_json::out_of_range&); - - const beyond_uint32_binary_json ext = beyond_uint32_binary_json::binary(beyond_uint32_binary_t{}, 42); - CHECK_THROWS_WITH_AS(beyond_uint32_binary_json::to_msgpack(ext), expected, beyond_uint32_binary_json::out_of_range&); - -#ifdef JSON_TEST_BEYOND_UINT32_STRING - // created from its type rather than from a beyond_uint32_string_t: - // that would consider the std::filesystem::path conversion, which - // libstdc++ 10 cannot decide for a class derived from std::string - const beyond_uint32_string_json string(beyond_uint32_string_json::value_t::string); - CHECK_THROWS_WITH_AS(beyond_uint32_string_json::to_msgpack(string), expected, beyond_uint32_string_json::out_of_range&); -#endif - } -#endif -} - -TEST_CASE("MessagePack numbers use the active union member (see #5644)") -{ - // when number_integer_t is narrower than number_unsigned_t, to_msgpack() - // used to read the union member that was not the active one, writing - // wrong bytes for some values; std::int64_t/std::uint64_t (the default - // types, where both members have the same width) were not affected - using int32_json = nlohmann::json::with_integers_t; - using int16_json = nlohmann::json::with_integers_t; - - SECTION("number_integer_t = std::int32_t") - { - SECTION("6442450944 (uint 64; the low 32 bits used to be sign-extended)") - { - const int32_json j = 6442450944ULL; - CHECK(j.is_number_unsigned()); - - std::vector const expected{0xcf, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00}; - const auto result = int32_json::to_msgpack(j); - CHECK(result == expected); - CHECK(int32_json::from_msgpack(result) == j); - } - - SECTION("4294967496 (uint 64; the low 32 bits used to be the whole value)") - { - const int32_json j = 4294967496ULL; - CHECK(j.is_number_unsigned()); - - std::vector const expected{0xcf, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xc8}; - const auto result = int32_json::to_msgpack(j); - CHECK(result == expected); - CHECK(int32_json::from_msgpack(result) == j); - } - } - - SECTION("number_integer_t = std::int16_t, 98304 (uint 32)") - { - const int16_json j = 98304ULL; - CHECK(j.is_number_unsigned()); - - std::vector const expected{0xce, 0x00, 0x01, 0x80, 0x00}; - const auto result = int16_json::to_msgpack(j); - CHECK(result == expected); - CHECK(int16_json::from_msgpack(result) == j); - } - - SECTION("default types (std::int64_t/std::uint64_t) are unaffected") - { - const json j = 4294967496ULL; - CHECK(j.is_number_unsigned()); - - std::vector const expected{0xcf, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xc8}; - const auto result = json::to_msgpack(j); - CHECK(result == expected); - CHECK(json::from_msgpack(result) == j); - } -} - TEST_CASE("MessagePack large strings and binaries (chunked reader)") { // get_msgpack_string()/get_msgpack_binary() both read through get_binary(), diff --git a/tests/src/unit-no_automatic_udls.cpp b/tests/src/unit-no_automatic_udls.cpp index 1a708da50..02f900077 100644 --- a/tests/src/unit-no_automatic_udls.cpp +++ b/tests/src/unit-no_automatic_udls.cpp @@ -19,7 +19,7 @@ #include #include -using json = nlohmann::json; +using nlohmann::json; // An argument type whose associated namespace is the library namespace, so // argument-dependent lookup of a literal operator called by its function name diff --git a/tests/src/unit-no_io_and_user_exceptions.cpp b/tests/src/unit-no_io_and_user_exceptions.cpp index 667d114e7..5a2b7da44 100644 --- a/tests/src/unit-no_io_and_user_exceptions.cpp +++ b/tests/src/unit-no_io_and_user_exceptions.cpp @@ -38,7 +38,7 @@ static int json_throw_user_call_count = 0; // NOLINT(cppcoreguidelines-avoid-non #include "doctest_compatibility.h" #include -using json = nlohmann::json; +using nlohmann::json; TEST_CASE("JSON_NO_IO") { diff --git a/tests/src/unit-ordered_json.cpp b/tests/src/unit-ordered_json.cpp index 135dedade..c328d8aea 100644 --- a/tests/src/unit-ordered_json.cpp +++ b/tests/src/unit-ordered_json.cpp @@ -12,6 +12,11 @@ using nlohmann::json; using nlohmann::ordered_json; +#include +#include +#include +#include + TEST_CASE("ordered_json") { json j; @@ -237,3 +242,276 @@ TEST_CASE("regression test for issue #5673 - ordered_json::emplace with a non-rv CHECK(oj.dump() == "{\"a\":1}"); } } + +namespace +{ +// the sizes around which the DOM parser changes how it stores the elements of +// an array (16) and the members of an object in a vector (32, and 64 for the +// key index) +const std::array container_sizes = {{0, 1, 2, 3, 15, 16, 17, 18, 31, 32, 33, 34, 63, 64, 65, 66, 200}}; + +template +std::string array_text(const std::size_t n, const std::size_t depth) +{ + std::string text = "["; + for (std::size_t i = 0; i < n; ++i) + { + text += (i == 0 ? "" : ","); + // alternate scalars, empty containers and nested containers + switch (i % 4) + { + case 0: + text += std::to_string(i); + break; + case 1: + text += "\"s" + std::to_string(i) + "\""; + break; + case 2: + text += depth == 0 ? "[]" : array_text(n / 2, depth - 1); + break; + default: + text += depth == 0 ? "{}" : "{\"a\":" + array_text(n / 3, depth - 1) + ",\"b\":null}"; + break; + } + } + return text + "]"; +} + +template +std::string object_text(const std::size_t n, const std::size_t depth, const bool duplicates) +{ + std::string text = "{"; + for (std::size_t i = 0; i < n; ++i) + { + text += (i == 0 ? "" : ","); + text += "\"key" + std::to_string(i) + "\":"; + switch (i % 4) + { + case 0: + text += std::to_string(i); + break; + case 1: + text += depth == 0 ? "[]" : array_text(n / 2, depth - 1); + break; + case 2: + text += depth == 0 ? "{}" : object_text(n / 2, depth - 1, duplicates); + break; + default: + text += "\"value\""; + break; + } + } + if (duplicates) + { + // keys that are there already, the first and the last one, with other values + text += n == 0 ? "" : ","; + text += R"("key0":[1,2],"key)"; + text += std::to_string(n - 1); + text += R"(":{"again":true})"; + } + return text + "}"; +} + +template +void check_same_dump(const std::string& text) +{ + // the text is what dump() writes for the parsed value, and the value + // survives a second round trip: arrays keep their elements in order, an + // ordered_json keeps its members in order of appearance + const Json parsed = Json::parse(text); + const Json again = Json::parse(parsed.dump()); + CHECK(parsed == again); + CHECK(parsed.dump() == again.dump()); + CHECK(Json::accept(text)); +} +} // namespace + +TEST_CASE("parsing containers of sizes around the limits of the DOM parser") +{ + SECTION("arrays") + { + for (const std::size_t n : container_sizes) + { + const std::string text = array_text(n, 2); + INFO("size ", n); + + const json parsed = json::parse(text); + CHECK(parsed.is_array()); + CHECK(parsed.size() == n); + for (std::size_t i = 0; i < n; i += 4) + { + CHECK(parsed[i] == i); + } + check_same_dump(text); + check_same_dump(text); + CHECK(ordered_json::parse(text).dump() == parsed.dump()); + } + } + + SECTION("objects") + { + for (const bool duplicates : + { + false, true + }) + { + for (const std::size_t n : container_sizes) + { + const std::string text = object_text(n, 2, duplicates); + INFO("size ", n, " duplicates ", duplicates); + + const json parsed = json::parse(text); + const ordered_json ordered = ordered_json::parse(text); + CHECK(parsed.is_object()); + CHECK(ordered.is_object()); + CHECK(parsed.size() == ordered.size()); + // the same members, in the order of their first appearance + std::size_t i = 0; + for (auto it = ordered.begin(); it != ordered.end(); ++it, ++i) + { + if (i < n) + { + CHECK(it.key() == "key" + std::to_string(i)); + } + CHECK(parsed.at(it.key()) == json(it.value())); + } + if (duplicates && n > 1) + { + // the later value replaced the earlier one in its place + CHECK(ordered.at("key0") == ordered_json::parse("[1,2]")); + CHECK(ordered.at("key" + std::to_string(n - 1)) == ordered_json::parse("{\"again\":true}")); + CHECK(ordered.begin().key() == "key0"); + CHECK(ordered.size() == n); + } + check_same_dump(text); + check_same_dump(text); + } + } + } + + SECTION("objects with many different and repeated keys") + { + // 300 members of which a third repeat an earlier key + std::string text = "{"; + for (std::size_t i = 0; i < 300; ++i) + { + text += (i == 0 ? "" : ","); + text += "\"k" + std::to_string(i % 200) + "\":" + std::to_string(i); + } + text += '}'; + + const ordered_json ordered = ordered_json::parse(text); + const json parsed = json::parse(text); + CHECK(ordered.size() == 200); + CHECK(parsed.size() == 200); + std::size_t i = 0; + for (auto it = ordered.begin(); it != ordered.end(); ++it, ++i) + { + CHECK(it.key() == "k" + std::to_string(i)); + // the last value written + CHECK(it.value() == (i < 100 ? i + 200 : i)); + CHECK(parsed.at(it.key()) == json(it.value())); + } + } +} + +namespace +{ +// a deterministic source of structure for parsing tests: builds a JSON text +// and, at the same time, the value it has to parse to +class tree_generator +{ + public: + explicit tree_generator(std::uint32_t seed) : state(seed) {} + + template + Json make(std::string& text, const std::size_t depth) + { + switch (next(depth == 0 ? 5 : 9)) + { + case 0: + text += "null"; + return Json(nullptr); + case 1: + text += "true"; + return Json(true); + case 2: + { + const int n = static_cast(next(2000)) - 1000; + text += std::to_string(n); + return Json(n); + } + case 3: + text += "\"text\""; + return Json("text"); + case 4: + text += "2.5"; + return Json(2.5); + case 5: + case 6: + case 7: + { + Json array = Json::array(); + text += '['; + const std::size_t n = size(); + for (std::size_t i = 0; i < n; ++i) + { + text += (i == 0 ? "" : ","); + array.push_back(make(text, depth - 1)); + } + text += ']'; + return array; + } + default: + { + Json object = Json::object(); + text += '{'; + const std::size_t n = size(); + for (std::size_t i = 0; i < n; ++i) + { + const std::string key = "k" + std::to_string(i); + text += (i == 0 ? "\"" : ",\"") + key + "\":"; + object[key] = make(text, depth - 1); + } + text += '}'; + return object; + } + } + } + + private: + std::uint32_t next(const std::uint32_t bound) + { + state = (state * 1664525U) + 1013904223U; + return (state >> 8) % bound; + } + + std::size_t size() + { + return container_sizes[next(static_cast(container_sizes.size()))]; + } + + std::uint32_t state; +}; +} // namespace + +TEST_CASE("parsing random trees of arrays and objects") +{ + for (std::uint32_t seed = 1; seed <= 150; ++seed) + { + INFO("seed ", seed); + + std::string text; + std::string ordered_text; + const json expected = tree_generator(seed).make(text, 3); + const auto ordered_expected = tree_generator(seed).make(ordered_text, 3); + CHECK(text == ordered_text); + + CHECK(json::parse(text) == expected); + CHECK(json::accept(text)); + const ordered_json ordered = ordered_json::parse(text); + CHECK(ordered == ordered_expected); + CHECK(ordered.dump() == ordered_expected.dump()); + CHECK(json::parse(text).dump() == expected.dump()); + } +} diff --git a/tests/src/unit-ordered_json2.cpp b/tests/src/unit-ordered_json2.cpp index 653820628..4f4e18844 100644 --- a/tests/src/unit-ordered_json2.cpp +++ b/tests/src/unit-ordered_json2.cpp @@ -217,16 +217,7 @@ void int_to_string(alt_string& target, std::size_t value) target = std::to_string(value).c_str(); } -using alt_json = nlohmann::basic_json < - std::map, - std::vector, - alt_string, - bool, - std::int64_t, - std::uint64_t, - double, - std::allocator, - nlohmann::adl_serializer >; +using alt_json = nlohmann::json::with_string_t; bool operator<(const char* op1, const alt_string& op2) noexcept { diff --git a/tests/src/unit-precise-stream-position.cpp b/tests/src/unit-precise-stream-position.cpp index e2d9bd0c5..0e8a6094f 100644 --- a/tests/src/unit-precise-stream-position.cpp +++ b/tests/src/unit-precise-stream-position.cpp @@ -20,6 +20,7 @@ #include using nlohmann::json; +#include #include #include #include @@ -72,6 +73,40 @@ class no_putback_streambuf : public std::streambuf std::size_t m_pos = 0; }; +// A streambuf that serves its input in get areas of a fixed size, so that the +// end of a number falls on every possible offset of a get area. The lexer scans +// the get area directly; the stream position must not depend on its size. +class chunked_streambuf : public std::streambuf +{ + public: + chunked_streambuf(std::string s, std::size_t chunk_size) + : m_data(std::move(s)), m_chunk_size(chunk_size) + {} + + protected: + int_type underflow() override + { + if (gptr() < egptr()) + { + return traits_type::to_int_type(*gptr()); + } + m_pos += static_cast(egptr() - eback()); + if (m_pos >= m_data.size()) + { + setg(nullptr, nullptr, nullptr); + return traits_type::eof(); + } + char* const begin = &m_data[m_pos]; + setg(begin, begin, begin + (std::min)(m_chunk_size, m_data.size() - m_pos)); + return traits_type::to_int_type(*gptr()); + } + + private: + std::string m_data; + std::size_t m_pos = 0; + std::size_t m_chunk_size; +}; + // read the characters that are left in a stream std::string remaining(std::istream& is) { @@ -263,4 +298,26 @@ TEST_CASE("JSON_PRECISE_STREAM_POSITION") json _; CHECK_THROWS_WITH_AS(_ = json::parse(is), "[json.exception.parse_error.101] parse error: attempting to parse an empty input; check that your input string or stream contains the expected JSON", json::parse_error&); } + + SECTION("the position does not depend on the get area of the streambuf") + { + const std::vector chunk_sizes = {1, 2, 3, 4, 5, 7, 8, 13, 4096}; + for (const std::size_t chunk_size : chunk_sizes) + { + CAPTURE(chunk_size) + chunked_streambuf buf("12345678901234567890true\"a long string value\"-1.25e+3,[1,2]42", chunk_size); + std::istream input(&buf); + json j; + + input >> j; + CHECK(j == 12345678901234567890u); + input >> j; + CHECK(j == true); + input >> j; + CHECK(j == "a long string value"); + input >> j; + CHECK(j == -1250.0); + CHECK(remaining(input) == ",[1,2]42"); + } + } } diff --git a/tests/src/unit-qt_keywords.cpp b/tests/src/unit-qt_keywords.cpp new file mode 100644 index 000000000..9805f5b88 --- /dev/null +++ b/tests/src/unit-qt_keywords.cpp @@ -0,0 +1,67 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +// Qt defines its keywords as macros (see qtmetamacros.h and qforeach.h) unless +// QT_NO_KEYWORDS is defined, and it is common to include Qt headers before this +// library; it must therefore not use these words as identifiers (see #5865) +#define slots +#define signals public +#define emit +#define Q_FOREACH(variable, container) for (variable : container) // NOLINT(bugprone-macro-parentheses): variable is a declaration, as in Qt +#define foreach Q_FOREACH +#define forever for (;;) + +#include +using nlohmann::json; + +TEST_CASE("Qt keyword macros") +{ + SECTION("parse and dump") + { + const json j = json::parse(R"({"a": [1, 2.5, "x", null, true]})"); + CHECK(j.dump() == R"({"a":[1,2.5,"x",null,true]})"); + } + + SECTION("flatten and unflatten") + { + const json j = {{"a", {{"b", {1, 2}}, {"c", "d"}}}, {"e", nullptr}}; + CHECK(j.flatten().unflatten() == j); + } + + SECTION("JSON pointer") + { + json j = {{"a", {{"b", {1, 2}}}}}; + CHECK(j.at(json::json_pointer("/a/b/1")) == 2); + j[json::json_pointer("/a/c")] = 3; + CHECK(j.contains(json::json_pointer("/a/c"))); + } + + SECTION("binary formats") + { + const json j = {{"a", {1, 2, 3}}, {"b", "c"}}; + CHECK(json::from_cbor(json::to_cbor(j)) == j); + CHECK(json::from_msgpack(json::to_msgpack(j)) == j); + CHECK(json::from_ubjson(json::to_ubjson(j)) == j); + CHECK(json::from_bjdata(json::to_bjdata(j)) == j); + CHECK(json::from_bson(json::to_bson(j)) == j); + } + + SECTION("patch and diff") + { + const json source = {{"a", 1}, {"b", {1, 2}}}; + const json target = {{"a", 2}, {"c", "d"}}; + CHECK(source.patch(json::diff(source, target)) == target); + } +} + +// testing for the macros also keeps -Wunused-macros from rejecting them +#if !defined(slots) || !defined(signals) || !defined(emit) || !defined(Q_FOREACH) || !defined(foreach) || !defined(forever) + #error "the Qt keyword macros must stay defined for the tests above" +#endif diff --git a/tests/src/unit-regression2.cpp b/tests/src/unit-regression2.cpp index b532e75ae..c7566c799 100644 --- a/tests/src/unit-regression2.cpp +++ b/tests/src/unit-regression2.cpp @@ -33,8 +33,8 @@ #define JSON_TESTS_PRIVATE #include -using json = nlohmann::json; -using ordered_json = nlohmann::ordered_json; +using nlohmann::json; +using nlohmann::ordered_json; #ifdef JSON_TEST_NO_GLOBAL_UDLS using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) #endif diff --git a/tests/src/unit-regression3.cpp b/tests/src/unit-regression3.cpp index f8d9740fa..148ce2658 100644 --- a/tests/src/unit-regression3.cpp +++ b/tests/src/unit-regression3.cpp @@ -36,14 +36,13 @@ #define JSON_TESTS_PRIVATE #include -using json = nlohmann::json; -using ordered_json = nlohmann::ordered_json; +using nlohmann::json; +using nlohmann::ordered_json; #ifdef JSON_TEST_NO_GLOBAL_UDLS using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) #endif #include -#include #include #include #include @@ -58,11 +57,6 @@ using ordered_json = nlohmann::ordered_json; #include #elif __has_include() #endif - - ///////////////////////////////////////////////////////////////////// - // for #4804 - ///////////////////////////////////////////////////////////////////// - using json_4804 = nlohmann::json::with_binary_t>; #endif #ifdef JSON_HAS_CPP_20 @@ -458,49 +452,6 @@ TEST_CASE("regression tests 3") #endif #ifdef JSON_HAS_CPP_17 - SECTION("issue #4804: from_cbor incompatible with std::vector as binary_t") - { - const std::vector data = {0x80}; - const auto decoded = json_4804::from_cbor(data); - CHECK((decoded == json_4804::array())); - } - -#ifndef SKIP_TESTS_FOR_ENUM_SERIALIZATION - SECTION("discussion #4209 - custom BinaryType direct assignment and round-tripping") - { - // Test that assigning a custom BinaryType directly creates a binary value, not an array - const std::vector original{std::byte{1}, std::byte{2}, std::byte{3}}; - const json_4804 j = original; - CHECK(j.is_binary()); - CHECK(!j.is_array()); - - // Test round-tripping: extracting the binary value back as the custom container type - const auto extracted = j.get>(); - CHECK(extracted == original); - - // Test that the default json alias behavior is unchanged: std::vector -> array - const json default_json = std::vector {1, 2, 3}; - CHECK(default_json.is_array()); - CHECK(!default_json.is_binary()); - } - - SECTION("discussion #4209 - custom BinaryType extraction from parsed array") - { - // Test that extracting a custom BinaryType from a parsed JSON array still works - // (not just from a binary-typed node) - const auto j = json_4804::parse("[1,2,3]"); - CHECK(j.is_array()); - CHECK(!j.is_binary()); - - // Extracting as custom BinaryType should work from arrays - const auto extracted = j.get>(); - CHECK(extracted.size() == 3); - CHECK(extracted[0] == std::byte{1}); - CHECK(extracted[1] == std::byte{2}); - CHECK(extracted[2] == std::byte{3}); - } -#endif - SECTION("issue #5046 - implicit conversion of return json to std::optional no longer implicit") { const json jval{}; @@ -915,52 +866,6 @@ TEST_CASE("issue #5402 - update(merge_objects=true) overwrites a primitive with } -TEST_CASE("regression test #5476 - array type without reserve()") -{ - // the capacity reserved for definite-length arrays must not require the - // array type to have a reserve() member function - using deque_json = nlohmann::json::with_array_t; - - SECTION("std::deque") - { - const auto j = deque_json::parse(R"({"a":[1,[2,3]],"b":[]})"); - CHECK(j.dump() == R"({"a":[1,[2,3]],"b":[]})"); - - // the binary formats pass a definite length to start_array() - CHECK(deque_json::from_cbor(deque_json::to_cbor(j)) == j); - CHECK(deque_json::from_msgpack(deque_json::to_msgpack(j)) == j); - CHECK(deque_json::from_bon8(deque_json::to_bon8(j)) == j); - - // parse() instantiates the callback parser as well, which reserves too - const auto with_callback = deque_json::parse(R"([1,2,3])", [](int /*depth*/, deque_json::parse_event_t /*event*/, deque_json& /*parsed*/) noexcept - { - return true; - }); - CHECK(with_callback == deque_json({1, 2, 3})); - } - - SECTION("std::vector still reserves") - { - json array = json::array(); - for (int i = 0; i < 100; ++i) - { - array.push_back(i); - } - - const auto j = json::from_cbor(json::to_cbor(array)); - CHECK(j == array); - CHECK(j.get_ref().capacity() >= 100); - } - - SECTION("the reservation stays capped") - { - // CBOR array announcing 2^32-1 elements, but truncated right after the - // header: the input must be rejected without reserving that capacity - const std::vector truncated = {0x9A, 0xFF, 0xFF, 0xFF, 0xFF}; - CHECK(json::from_cbor(truncated, true, false).is_discarded()); - } -} - TEST_CASE("issue #5317 - nested indefinite-length CBOR string chunks are rejected") { json _; diff --git a/tests/src/unit-same-number-types.cpp b/tests/src/unit-same-number-types.cpp new file mode 100644 index 000000000..ba42710c1 --- /dev/null +++ b/tests/src/unit-same-number-types.cpp @@ -0,0 +1,201 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include + +#include +#include +#include +#include +#include + +// basic_json with the same type for number_integer_t and number_unsigned_t +// (#2573): every integer is stored as number_integer + +namespace +{ + +template +using same_types_json = nlohmann::json::with_integers_t; + +using json64 = same_types_json; +using json32 = same_types_json; + +} // namespace + +TEST_CASE_TEMPLATE("same type for number_integer_t and number_unsigned_t", Json, json64, json32) +{ + using integer_t = typename Json::number_integer_t; + using limits = std::numeric_limits; + + SECTION("parsing stores integers as number_integer") + { + const Json j = Json::parse("[0, 1, -1, 2147483647, -2147483648]"); + for (const auto& element : j) + { + CHECK(element.is_number_integer()); + CHECK(!element.is_number_unsigned()); + CHECK(element.type() == nlohmann::detail::value_t::number_integer); + } + CHECK(j[1].template get() == 1); + CHECK(j[2].template get() == -1); + CHECK(j[3].template get() == 2147483647); + CHECK(j[4].template get() == -2147483648LL); + } + + SECTION("integers above the maximum are parsed as floats") + { + // a positive literal is parsed as unsigned first; it must not wrap + // around to a negative number_integer_t + const auto above_max = std::to_string(static_cast(limits::max()) + 1); + const Json j1 = Json::parse(above_max); + CHECK(j1.is_number_float()); + CHECK(j1.template get() == static_cast(static_cast(limits::max()) + 1)); + + const Json j2 = Json::parse("18446744073709551615"); + CHECK(j2.is_number_float()); + CHECK(j2.template get() == 18446744073709551615.0); + + const Json j3 = Json::parse(std::to_string(limits::max())); + CHECK(j3.is_number_integer()); + CHECK(j3.template get() == limits::max()); + } + + SECTION("serialization of the extremes") + { + CHECK(Json(limits::max()).dump() == std::to_string(limits::max())); + // the absolute value of the minimum does not fit into number_unsigned_t + CHECK(Json(limits::min()).dump() == std::to_string(limits::min())); + CHECK(Json::parse(std::to_string(limits::min())) == Json(limits::min())); + CHECK(Json(0).dump() == "0"); + } + + SECTION("construction from unsigned types") + { + const Json j1 = 5u; + CHECK(j1.is_number_integer()); + CHECK(!j1.is_number_unsigned()); + CHECK(j1 == 5); + + const Json j2 = static_cast(200); + CHECK(j2.is_number_integer()); + CHECK(j2.template get() == 200); + + const Json j3 = std::vector {1, 2, 3}; + CHECK(j3.dump() == "[1,2,3]"); + CHECK(j3[0].is_number_integer()); + + const Json j4 = true; + CHECK(j4.is_boolean()); + } + + SECTION("value_t::number_unsigned creates a number_integer") + { + const Json j(nlohmann::detail::value_t::number_unsigned); + CHECK(j.is_number_integer()); + CHECK(!j.is_number_unsigned()); + CHECK(j == 0); + CHECK(j.dump() == "0"); + } + + SECTION("conversions") + { + Json j = 42; + CHECK(j.template get() == 42u); + CHECK(j.template get() == 42u); + CHECK(j.template get() == 42); + CHECK(j.template get() == 42.0); + CHECK(j.template get_ptr() != nullptr); + CHECK(*j.template get_ptr() == 42); + CHECK(j.template get_ref() == 42); + j.template get_ref() = 43; + CHECK(j == 43); + + integer_t i = 0; + j.get_to(i); + CHECK(i == 43); + + const Json object = {{"a", 1}}; + CHECK(object.value("a", 0u) == 1u); + CHECK(object.value("b", 7u) == 7u); + } + + SECTION("conversion from a basic_json with distinct types") + { + const nlohmann::json j = {1, 2u, -3, 4.5}; + CHECK(j[1].is_number_unsigned()); + const Json converted = j; + CHECK(converted.dump() == "[1,2,-3,4.5]"); + CHECK(converted[1].is_number_integer()); + CHECK(!converted[1].is_number_unsigned()); + + const nlohmann::json back = converted; + CHECK(back == j); + } + + SECTION("comparison and hashing") + { + const Json j1 = 1; + const Json j2 = 2u; + CHECK(j1 < j2); + CHECK(j1 != j2); + CHECK(j2 == 2); + CHECK(j2 == 2u); + CHECK(j1 < 1.5); + CHECK(Json(-1) < Json(0u)); + CHECK(std::hash {}(Json(5u)) == std::hash {}(Json(5))); + } + + SECTION("JSON Patch, diff, and flatten") + { + const Json source = Json::parse(R"({"a": [1, 2, 3], "b": 4})"); + const Json target = Json::parse(R"({"a": [1, 5], "c": 6})"); + CHECK(source.patch(Json::diff(source, target)) == target); + CHECK(source.flatten().unflatten() == source); + } + + SECTION("binary formats") + { + const Json j = {{"min", limits::min()}, {"max", limits::max()}, {"zero", 0}, {"small", 100}, {"negative", -100}, {"float", 1.5}}; + + CHECK(Json::from_cbor(Json::to_cbor(j)) == j); + CHECK(Json::from_msgpack(Json::to_msgpack(j)) == j); + CHECK(Json::from_ubjson(Json::to_ubjson(j)) == j); + CHECK(Json::from_bjdata(Json::to_bjdata(j)) == j); + CHECK(Json::from_bson(Json::to_bson(j)) == j); + CHECK(Json::from_bon8(Json::to_bon8(j)) == j); + + for (const auto& element : Json::from_cbor(Json::to_cbor(j))) + { + CHECK(!element.is_number_unsigned()); + } + + // CBOR unsigned integer 2^64-1 does not fit and is read as float + const std::vector cbor_max = {0x1B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + const Json from_cbor = Json::from_cbor(cbor_max); + CHECK(from_cbor.is_number_float()); + CHECK(from_cbor.template get() == 18446744073709551615.0); + + // MessagePack uint 8 is read as number_integer + const std::vector msgpack_uint8 = {0xCC, 0xC8}; + const Json from_msgpack = Json::from_msgpack(msgpack_uint8); + CHECK(from_msgpack.is_number_integer()); + CHECK(from_msgpack == 200); + } +} + +TEST_CASE("same type for number_integer_t and number_unsigned_t: type traits") +{ + CHECK(std::is_same, nlohmann::detail::same_as_number_integer_type>::value); + CHECK(std::is_same, std::uint64_t>::value); + CHECK(nlohmann::detail::is_compatible_number_integer_type::value); + CHECK(!nlohmann::detail::is_compatible_number_integer_type::value); + CHECK(!nlohmann::detail::is_compatible_number_integer_type::value); +} diff --git a/tests/src/unit-serialization.cpp b/tests/src/unit-serialization.cpp index c56c4f5c1..1173b851b 100644 --- a/tests/src/unit-serialization.cpp +++ b/tests/src/unit-serialization.cpp @@ -20,8 +20,11 @@ using nlohmann::json; #include +#include #include #include +#include +#include #include "test_utils.hpp" @@ -923,66 +926,68 @@ TEST_CASE("serialization boundary values for the string buffer") // mid-string once fewer than 13 bytes remain (`string_buffer.size() - bytes // < 13`), 13 being one more than the most a single code point can ever // write at once (a surrogate pair: two back-to-back "\uXXXX" escapes, 12 - // bytes). Every write into string_buffer that this check protects happens - // in steps of 2 (a simple "\\x" escape) or 6 (one "\uXXXX" unit), so - // `bytes` only ever takes even values at the point the check runs - the - // tightest values actually reachable are therefore 498 (512 - 498 == 14, - // one simple escape away from the threshold) and 500 (512 - 500 == 12, - // where the flush fires immediately and resets bytes to 0). + // bytes). Characters below 0x60 (control characters, '"' and '\\') are + // escaped straight into the write buffer and never reach string_buffer, so + // under ensure_ascii it fills in steps of 6 (one "\uXXXX" unit for a + // character below U+10000) or 12 (a surrogate pair). With 2-byte characters + // such as U+00E4, `bytes` takes multiples of 6 at the point the check runs; + // the tightest values reachable are therefore 498 (512 - 498 == 14, after + // 83 characters) and 504 (512 - 504 == 8, after 84 characters, where the + // flush fires immediately and resets bytes to 0). - SECTION("a run of 2-byte escapes lands bytes on, and one step past, the flush threshold") + SECTION("a run of 6-byte escapes lands bytes on, and one step past, the flush threshold") { for (const int count : { - 249, 250, 251 + 83, 84, 85 }) { CAPTURE(count) - const json j = std::string(static_cast(count), '\n'); + std::string umlauts; std::string expected = "\""; for (int i = 0; i < count; ++i) { - expected += "\\n"; + umlauts += "\xC3\xA4"; + expected += "\\u00e4"; } expected += '"'; - CHECK(j.dump() == expected); + CHECK(json(umlauts).dump(-1, ' ', true) == expected); } } - SECTION("an ASCII prefix leaves the tightest reachable margin before a 12-byte surrogate pair") + SECTION("a prefix leaves the tightest reachable margin before a 12-byte surrogate pair") { // U+1F600 (the "\xF0\x9F\x98\x80" UTF-8 bytes) is dumped under // ensure_ascii as the 12-byte surrogate pair "\ud83d\ude00"; that // write happens in a single step with no intermediate flush check, so // it is the write most exposed by an off-by-one in the "< 13" guard. - // A prefix of 249 newlines leaves exactly 14 bytes of headroom + // A prefix of 83 U+00E4 characters leaves exactly 14 bytes of headroom // (512 - 498), the smallest margin the guard ever actually allows - // into a new code point; 250 newlines instead trigger the guard's own + // into a new code point; 84 characters instead trigger the guard's own // flush first, so the emoji starts from a freshly emptied (512-byte) - // buffer, and 251 repeats that with one more escape already past the + // buffer, and 85 repeats that with one more escape already past the // reset. Together they cover the margin the guard allows landing on, // one step before, and one step after - all must still produce the // identical, correct escapes. for (const int prefix_count : { - 249, 250, 251 + 83, 84, 85 }) { CAPTURE(prefix_count) - const std::string prefix(static_cast(prefix_count), '\n'); - const std::string emoji = "\xF0\x9F\x98\x80"; - const json j = prefix + emoji; - + std::string prefix; std::string expected_prefix; for (int i = 0; i < prefix_count; ++i) { - expected_prefix += "\\n"; + prefix += "\xC3\xA4"; + expected_prefix += "\\u00e4"; } + const std::string emoji = "\xF0\x9F\x98\x80"; + const json j = prefix + emoji; - // newline escaping does not depend on ensure_ascii: only the - // emoji differs (raw UTF-8 bytes vs. a \u-escaped surrogate pair) + // without ensure_ascii, the whole string is copied unchanged std::string expected_raw = "\""; - expected_raw += expected_prefix; + expected_raw += prefix; expected_raw += emoji; expected_raw += '"'; std::string expected_ascii = "\""; @@ -1027,3 +1032,642 @@ TEST_CASE("serialization boundary values for the string buffer") } } } + +namespace +{ +// appends the escaped form of the ASCII character c to out; written +// independently of the table in serializer::ascii_escapes() +void append_escaped_ascii(std::string& out, const char c, const bool ensure_ascii) +{ + const auto byte = static_cast(c); + switch (byte) + { + case '\b': + out += "\\b"; + break; + case '\t': + out += "\\t"; + break; + case '\n': + out += "\\n"; + break; + case '\f': + out += "\\f"; + break; + case '\r': + out += "\\r"; + break; + case '"': + out += "\\\""; + break; + case '\\': + out += "\\\\"; + break; + default: + if (byte < 0x20 || (ensure_ascii && byte == 0x7F)) + { + // byte < 0x80, so the first two hex digits are 0 + const char* const hex = "0123456789abcdef"; + out += "\\u00"; + out += hex[byte >> 4U]; + out += hex[byte & 0x0FU]; + } + else + { + out += c; + } + break; + } +} + +// reference escaper for ASCII strings +std::string reference_escape_ascii(const std::string& s, const bool ensure_ascii) +{ + std::string result = "\""; + for (const char c : s) + { + append_escaped_ascii(result, c, ensure_ascii); + } + result += '"'; + return result; +} +} // namespace + +TEST_CASE("characters below 0x60 are escaped from a table straight into the write buffer") +{ + SECTION("every ASCII character at every position") + { + // lengths up to 17 cover the 8-byte strides of the bulk scanner on + // both sides of the character, so the scanner stops at it from every + // offset + for (std::size_t length = 1; length <= 17; ++length) + { + for (std::size_t pos = 0; pos < length; ++pos) + { + for (unsigned int c = 0; c < 0x80; ++c) + { + std::string s(length, 'a'); + s[pos] = static_cast(c); + CAPTURE(length) + CAPTURE(pos) + CAPTURE(c) + const json j = s; + CHECK(j.dump(-1, ' ', false) == reference_escape_ascii(s, false)); + CHECK(j.dump(-1, ' ', true) == reference_escape_ascii(s, true)); + } + } + } + } + + SECTION("runs of escaped characters") + { + std::string s; + for (unsigned int c = 0; c < 0x20; ++c) + { + s += static_cast(c); + } + s += R"("\"\)"; + CHECK(json(s).dump() == reference_escape_ascii(s, false)); + CHECK(json(s).dump(-1, ' ', true) == reference_escape_ascii(s, true)); + } + + SECTION("escapes straddle the end of the write buffer") + { + // the 8-byte store of a table entry needs room for all 8 bytes, even + // when the escape itself is shorter; a prefix of k plain characters + // shifts the 6-byte escapes across every offset of the 1024-byte + // write buffer + for (std::size_t k = 0; k < 8; ++k) + { + CAPTURE(k) + const std::string s = std::string(k, 'a') + std::string(400, '\x01'); + CHECK(json(s).dump() == reference_escape_ascii(s, false)); + CHECK(json::array({s, s}).dump() == "[" + reference_escape_ascii(s, false) + "," + reference_escape_ascii(s, false) + "]"); + } + } + + SECTION("pending \\u escapes of non-ASCII characters are written first") + { + // under ensure_ascii, U+00E4 is \u-escaped into the string buffer; + // a following escape from the table must not overtake it + const json j = "\xC3\xA4\n\xC3\xA4\"\xF0\x9F\x98\x80\\"; + CHECK(j.dump(-1, ' ', true) == "\"\\u00e4\\n\\u00e4\\\"\\ud83d\\ude00\\\\\""); + CHECK(j.dump(-1, ' ', false) == "\"\xC3\xA4\\n\xC3\xA4\\\"\xF0\x9F\x98\x80\\\\\""); + } + + SECTION("escapes after invalid UTF-8 for every error handler") + { + const json j = "a\xFF\n\xC3\""; + CHECK_THROWS_WITH_AS(utils::ignore_return_value(j.dump()), "[json.exception.type_error.316] invalid UTF-8 byte at index 1: 0xFF", json::type_error&); + CHECK(j.dump(-1, ' ', false, json::error_handler_t::ignore) == "\"a\\n\\\"\""); + CHECK(j.dump(-1, ' ', true, json::error_handler_t::ignore) == "\"a\\n\\\"\""); + CHECK(j.dump(-1, ' ', false, json::error_handler_t::replace) == "\"a\xEF\xBF\xBD\\n\xEF\xBF\xBD\\\"\""); + CHECK(j.dump(-1, ' ', true, json::error_handler_t::replace) == "\"a\\ufffd\\n\\ufffd\\\"\""); + CHECK(j.dump(-1, ' ', false, json::error_handler_t::keep) == "\"a\xFF\\n\xC3\\\"\""); + CHECK(j.dump(-1, ' ', true, json::error_handler_t::keep) == "\"a\xFF\\n\xC3\\\"\""); + } +} + +namespace +{ +// expected result of dumping a string: the output, or (with +// error_handler_t::strict) the message of the type_error.316 exception +struct expected_dump +{ + bool throws = false; + std::string text{}; // NOLINT(readability-redundant-member-init) +}; + +std::string upper_hex_byte(const unsigned char byte) +{ + const char* const hex = "0123456789ABCDEF"; + return std::string(1, hex[byte >> 4U]) + hex[byte & 0x0FU]; +} + +void append_u_escape(std::string& out, const std::uint32_t code_unit) +{ + const char* const hex = "0123456789abcdef"; + out += "\\u"; + for (int shift = 12; shift >= 0; shift -= 4) + { + out += hex[(code_unit >> static_cast(shift)) & 0x0FU]; + } +} + +void append_replacement_character(std::string& out, const bool ensure_ascii) +{ + out += ensure_ascii ? "\\ufffd" : "\xEF\xBF\xBD"; +} + +// reference serializer for arbitrary byte strings, written from the documented +// behavior of dump() and independently of serializer::dump_escaped(): UTF-8 is +// decoded strictly (well-formed sequences as in Table 3-7 of the Unicode +// Standard), and each ill-formed sequence is handled as the error handler says +expected_dump reference_dump_string(const std::string& s, const bool ensure_ascii, const json::error_handler_t error_handler) +{ + expected_dump result; + std::string& out = result.text; + out = "\""; + + // handle the ill-formed bytes s[begin, end), of which s[index] is the byte + // that could not be decoded; returns false if an exception is expected + const auto ill_formed = [&](const std::size_t begin, const std::size_t end, const std::size_t index) + { + switch (error_handler) + { + case json::error_handler_t::strict: + result.throws = true; + out = "[json.exception.type_error.316] invalid UTF-8 byte at index " + std::to_string(index) + ": 0x" + upper_hex_byte(static_cast(s[index])); + return false; + case json::error_handler_t::ignore: + break; + case json::error_handler_t::replace: + append_replacement_character(out, ensure_ascii); + break; + case json::error_handler_t::keep: + default: + // kept as is, even with ensure_ascii + out.append(s, begin, end - begin); + break; + } + return true; + }; + + std::size_t i = 0; + while (i < s.size()) + { + const auto lead = static_cast(s[i]); + if (lead < 0x80) + { + append_escaped_ascii(out, s[i], ensure_ascii); + ++i; + continue; + } + + // number of bytes of the sequence and the range of its second byte + std::size_t length = 0; + std::uint32_t code_point = 0; + unsigned char lower = 0x80; + unsigned char upper = 0xBF; + if (lead >= 0xC2 && lead <= 0xDF) + { + length = 2; + code_point = lead & 0x1FU; + } + else if (lead >= 0xE0 && lead <= 0xEF) + { + length = 3; + code_point = lead & 0x0FU; + lower = (lead == 0xE0) ? 0xA0 : 0x80; // no overlong encodings + upper = (lead == 0xED) ? 0x9F : 0xBF; // no surrogates + } + else if (lead >= 0xF0 && lead <= 0xF4) + { + length = 4; + code_point = lead & 0x07U; + lower = (lead == 0xF0) ? 0x90 : 0x80; // no overlong encodings + upper = (lead == 0xF4) ? 0x8F : 0xBF; // nothing above U+10FFFF + } + else + { + // a byte that cannot start a sequence is consumed + if (!ill_formed(i, i + 1, i)) + { + return result; + } + ++i; + continue; + } + + std::size_t j = i + 1; + for (; j < i + length; ++j) + { + if (j == s.size()) + { + // the string ends in the middle of the sequence + switch (error_handler) + { + case json::error_handler_t::strict: + result.throws = true; + out = "[json.exception.type_error.316] incomplete UTF-8 string; last byte: 0x" + upper_hex_byte(static_cast(s.back())); + return result; + case json::error_handler_t::ignore: + break; + case json::error_handler_t::replace: + append_replacement_character(out, ensure_ascii); + break; + case json::error_handler_t::keep: + default: + out.append(s, i, s.size() - i); + break; + } + out += '"'; + return result; + } + + const auto byte = static_cast(s[j]); + if (byte < lower || byte > upper) + { + break; + } + code_point = (code_point << 6U) | (byte & 0x3FU); + lower = 0x80; + upper = 0xBF; + } + + if (j < i + length) + { + // the sequence was abandoned at s[j]; that byte is read again as + // the start of a new character + if (!ill_formed(i, j, j)) + { + return result; + } + i = j; + continue; + } + + if (!ensure_ascii) + { + out.append(s, i, length); + } + else if (code_point <= 0xFFFF) + { + append_u_escape(out, code_point); + } + else + { + append_u_escape(out, 0xD800U + ((code_point - 0x10000U) >> 10U)); + append_u_escape(out, 0xDC00U + ((code_point - 0x10000U) & 0x3FFU)); + } + i = j; + } + + out += '"'; + return result; +} + +std::string hex_dump(const std::string& s) +{ + std::string result; + for (const char c : s) + { + result += upper_hex_byte(static_cast(c)); + result += ' '; + } + return result; +} + +// compares dump() of s with the reference for every error handler, with +// ensure_ascii on and off; CHECKs only on a mismatch to keep the sweep fast; +// error_handler_t::strict can be skipped where the position of the string +// buffer does not matter, as throwing is slow on some platforms +void check_dump_against_reference(const std::string& s, const bool with_strict = true) +{ + const json j = s; + const std::array error_handlers = + { + { + json::error_handler_t::strict, json::error_handler_t::ignore, + json::error_handler_t::replace, json::error_handler_t::keep + } + }; + for (const auto error_handler : error_handlers) + { +#if defined(JSON_NOEXCEPTION) + // a strict dump of invalid UTF-8 calls std::abort() instead of throwing + static_cast(with_strict); + if (error_handler == json::error_handler_t::strict) +#else + if (!with_strict && error_handler == json::error_handler_t::strict) +#endif + { + continue; + } + for (const bool ensure_ascii : + { + false, true + }) + { + const expected_dump expected = reference_dump_string(s, ensure_ascii, error_handler); + expected_dump actual; + int id = 0; + try + { + actual.text = j.dump(-1, ' ', ensure_ascii, error_handler); + } + catch (const json::exception& e) + { + actual.throws = true; + actual.text = e.what(); + id = e.id; + } + if (actual.throws != expected.throws || actual.text != expected.text || (actual.throws && id != 316)) + { + const std::string input = hex_dump(s); + CAPTURE(input) + CAPTURE(ensure_ascii) + CAPTURE(static_cast(error_handler)) + CHECK(actual.throws == expected.throws); + CHECK(actual.text == expected.text); + CHECK((!actual.throws || id == 316)); + } + } + } +} +} // namespace + +TEST_CASE("every ASCII character at every position of a short string is written like the reference") +{ + // dump_escaped() copies strings of up to 32 bytes of printable ASCII after + // a table lookup per byte, and hands everything else to the scanners; put + // each ASCII character and a 2-byte character at every position of + // strings of up to 40 bytes to cover both sides of that bound + for (std::size_t length = 1; length <= 40; ++length) + { + for (std::size_t pos = 0; pos < length; ++pos) + { + for (int c = 0; c < 0x80; ++c) + { + std::string s(length, 'a'); + s[pos] = static_cast(c); + check_dump_against_reference(s); + } + check_dump_against_reference(std::string(pos, 'a') + "\xC3\xA4" + std::string(length - pos - 1, 'a')); + } + } +} + +TEST_CASE("invalid UTF-8 at every position is handled like the reference") +{ + // ill-formed byte sequences; those that end in a truncated sequence are + // also cut short by the filler that follows, or by the end of the string + const std::vector patterns = + { + // stray continuation bytes + "\x80", "\xBF", + // bytes that cannot start a sequence + "\xC0", "\xC1", "\xF5", "\xF6", "\xF7", "\xF8", "\xF9", "\xFA", "\xFB", "\xFC", "\xFD", "\xFE", "\xFF", + // overlong encodings + "\xC0\xAF", "\xE0\x80\x80", "\xF0\x80\x80\x80", + // UTF-16 surrogates + "\xED\xA0\x80", "\xED\xBF\xBF", + // a code point above U+10FFFF + "\xF4\x90\x80\x80", + // sequences cut short by ASCII, a control character, a quote, a + // backslash, DEL, or the lead byte of a well-formed character + "\xC3" "a", "\xE2\x82" "\n", "\xE2\x82\"", "\xF0\x9F\x98\\", "\xC3\x7F", "\xC3\xC3\xA4", "\xE2\xF0\x9F\x98\x80", // NOLINT(bugprone-suspicious-missing-comma): "\xC3" "a" ends the hex escape + // truncated sequences + "\xC3", "\xE2\x82", "\xF0\x9F\x98" + }; + + // well-formed characters of 1, 2, 3, and 4 bytes + const std::vector fillers = {"a", "\xC3\xA4", "\xE2\x82\xAC", "\xF0\x9F\x98\x80"}; + + const auto make_string = [](const std::string & filler, const std::size_t before, const std::string & pattern, const std::size_t after) + { + std::string s; + for (std::size_t k = 0; k < before; ++k) + { + s += filler; + } + s += pattern; + for (std::size_t k = 0; k < after; ++k) + { + s += filler; + } + return s; + }; + + SECTION("the reference") + { + // the reference itself, against hand-written results + const std::string s = "a\xFF\n\xC3\"\xE0\x80\xF0\x9F\x98\x80\xE2\x82"; + CHECK(reference_dump_string(s, false, json::error_handler_t::strict).text == "[json.exception.type_error.316] invalid UTF-8 byte at index 1: 0xFF"); + CHECK(reference_dump_string(s.substr(2), false, json::error_handler_t::strict).text == "[json.exception.type_error.316] invalid UTF-8 byte at index 2: 0x22"); + CHECK(reference_dump_string("\xF0\x9F\x98\x80\xE2\x82", true, json::error_handler_t::strict).text == "[json.exception.type_error.316] incomplete UTF-8 string; last byte: 0x82"); + CHECK(reference_dump_string(s, false, json::error_handler_t::ignore).text == "\"a\\n\\\"\xF0\x9F\x98\x80\""); + CHECK(reference_dump_string(s, true, json::error_handler_t::ignore).text == "\"a\\n\\\"\\ud83d\\ude00\""); + CHECK(reference_dump_string(s, false, json::error_handler_t::replace).text == "\"a\xEF\xBF\xBD\\n\xEF\xBF\xBD\\\"\xEF\xBF\xBD\xEF\xBF\xBD\xF0\x9F\x98\x80\xEF\xBF\xBD\""); + CHECK(reference_dump_string(s, true, json::error_handler_t::replace).text == "\"a\\ufffd\\n\\ufffd\\\"\\ufffd\\ufffd\\ud83d\\ude00\\ufffd\""); + CHECK(reference_dump_string(s, false, json::error_handler_t::keep).text == "\"" + std::string("a\xFF\\n\xC3\\\"\xE0\x80\xF0\x9F\x98\x80\xE2\x82") + "\""); + CHECK(reference_dump_string(s, true, json::error_handler_t::keep).text == "\"" + std::string("a\xFF\\n\xC3\\\"\xE0\x80\\ud83d\\ude00\xE2\x82") + "\""); + check_dump_against_reference(s); + } + + SECTION("short strings") + { + // strings of up to 17 bytes put the pattern at every offset of an + // 8-byte stride of the bulk scanners, with whole strides on both + // sides; lengths around 24, 32, and 40 bytes cross the 16- and 32-byte + // strides; error_handler_t::strict is checked for up to 17 bytes of + // ASCII and 8 bytes of other characters only, as throwing is slow on + // some platforms + for (const auto& filler : fillers) + { + for (std::size_t length = 0; length * filler.size() <= 40; ++length) + { + const std::size_t bytes = length * filler.size(); + if (filler.size() == 1 && bytes > 17 && (bytes + 1) % 8 > 2) + { + continue; + } + for (std::size_t pos = 0; pos <= length; ++pos) + { + for (const auto& pattern : patterns) + { + check_dump_against_reference(make_string(filler, pos, pattern, length - pos), bytes <= (filler.size() == 1 ? 17u : 8u)); + } + } + } + } + } + + SECTION("strings crossing the end of the string buffer") + { + // characters that are not copied in bulk go through a 512-byte string + // buffer that is written out once fewer than 13 bytes are left; with + // ensure_ascii, each 2-byte character takes 6 bytes of it and each + // 4-byte character 12, so the pattern meets the end of the buffer after + // about 84 or 42 characters (3-byte characters behave like 2-byte + // ones); plain ASCII, copied in bulk, is checked around 512 bytes; + // positions further away are sampled; error_handler_t::strict throws + // at the first ill-formed byte, regardless of the string buffer, and is + // covered above + for (const auto& filler : fillers) + { + if (filler.size() == 3) + { + continue; + } + const std::size_t escaped_size = filler.size() == 1 ? 1 : filler.size() * 3; + const std::size_t middle = 506 / escaped_size; + const std::size_t window = filler.size() == 1 ? 8 : 16; + for (std::size_t pos = 0; pos <= middle + window; pos += (pos + window >= middle) ? 1 : 29) + { + for (const std::size_t after : + { + std::size_t{0}, std::size_t{12} + }) + { + for (const auto& pattern : patterns) + { + check_dump_against_reference(make_string(filler, pos, pattern, after), false); + } + } + } + } + } + + SECTION("repeated invalid sequences at every offset of the string buffer") + { + // ill-formed sequences that are kept or replaced fill the string + // buffer without a bulk copy in between; a prefix of 0 to 12 bytes + // 0xFF (kept as 1 byte, replaced by 3 or 6) shifts them across every + // offset of the end of the buffer + for (const auto& pattern : patterns) + { + for (std::size_t shift = 0; shift <= 12; ++shift) + { + std::string s(shift, '\xFF'); + while (s.size() < 600) + { + s += pattern; + } + check_dump_against_reference(s, shift == 0); + check_dump_against_reference(s + "\xF0\x9F\x98\x80", false); + } + } + } +} + +TEST_CASE("short strings are written like the reference, as values and as keys") +{ + // strings of up to 16 characters (32 with vector instructions) that need no + // escaping are written as a whole (together with the quotes and, for a key, + // the colon); everything else, including a string that is merely short, + // takes the general path. + // Every length around the edges of the size classes of the fast path is + // combined with each kind of byte that must send a string to the general + // path, at each position, and compared with the reference for values and, + // via the value, for keys. + const std::vector specials = + { + "\"", "\\", "\n", "\x1f", "\x7f", "\xc3\xa9", "\xe2\x82\xac", "\xf0\x9f\x98\x80", "\xff", "\x80", "\xc3" + }; + + for (std::size_t length = 0; length <= 36; ++length) + { + const std::string plain(length, 'k'); + check_dump_against_reference(plain, false); + + for (const auto& special : specials) + { + for (std::size_t pos = 0; pos <= length; ++pos) + { + std::string s = plain; + s.insert(pos, special); + check_dump_against_reference(s, false); + + for (const bool ensure_ascii : + { + false, true + }) + { + // a key and a value are written alike, in compact and + // in pretty-printed form + const json value = s; + const json object = json::object({{s, 1}}); + for (const auto error_handler : + { + json::error_handler_t::replace, json::error_handler_t::keep + }) + { + const std::string quoted = value.dump(-1, ' ', ensure_ascii, error_handler); + CHECK(object.dump(-1, ' ', ensure_ascii, error_handler) == "{" + quoted + ":1}"); + CHECK(object.dump(2, ' ', ensure_ascii, error_handler) == "{\n " + quoted + ": 1\n}"); + } + } + } + } + } +} + +TEST_CASE("runs of multi-byte characters are escaped like the reference under ensure_ascii") +{ + // a run of well-formed multi-byte characters is escaped straight into the + // write buffer; it has to flush that mid-run, stop at an ASCII character or + // an ill-formed sequence and leave those to the general path, and pick up + // behind what the general path has pending + const std::vector characters = + { + "\xc2\x80", "\xdf\xbf", "\xe0\xa0\x80", "\xe2\x82\xac", "\xed\x9f\xbf", "\xef\xbf\xbf", + "\xf0\x90\x80\x80", "\xf0\x9f\x98\x80", "\xf4\x8f\xbf\xbf" + }; + const std::vector interruptions = + { + "", "a", "\n", "\"", "\x7f", "\xff", "\x80", "\xc3", "\xe2\x82", "\xf0\x9f\x98", "\xed\xa0\x80" + }; + + for (std::size_t shift = 0; shift <= 13; ++shift) + { + for (const auto& interruption : interruptions) + { + for (const std::size_t count : + { + std::size_t{1}, std::size_t{2}, std::size_t{85}, std::size_t{200} + }) + { + std::string s(shift, 'x'); + for (std::size_t i = 0; i < count; ++i) + { + s += characters[(i + shift) % characters.size()]; + } + s += interruption; + s += characters[shift % characters.size()]; + s += characters[(shift + 3) % characters.size()]; + check_dump_against_reference(s, shift < 2); + } + } + } +} diff --git a/tests/src/unit-std-format.cpp b/tests/src/unit-std-format.cpp index f2be8d5ec..9f32868e4 100644 --- a/tests/src/unit-std-format.cpp +++ b/tests/src/unit-std-format.cpp @@ -16,8 +16,8 @@ #include "doctest_compatibility.h" #include -using json = nlohmann::json; -using ordered_json = nlohmann::ordered_json; +using nlohmann::json; +using nlohmann::ordered_json; // JSON_HAS_CPP_20 (do not remove; see note at top of file) #if JSON_HAS_STD_FORMAT diff --git a/tests/src/unit-stl_helpers.cpp b/tests/src/unit-stl_helpers.cpp new file mode 100644 index 000000000..3d00236f8 --- /dev/null +++ b/tests/src/unit-stl_helpers.cpp @@ -0,0 +1,130 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include + +#include +#include +#include +#include + +// The detail helpers that stand in for standard library functions of newer +// standards: each uses the standard function where the library has it and a +// C++11 implementation otherwise, so this file is also built with +// JSON_HAS_CPP_17, JSON_HAS_CPP_20, and JSON_HAS_CPP_23 to test both. + +namespace +{ +struct with_constexpr_member +{ + static constexpr std::size_t lower = 64; // no out-of-line definition +}; +} // namespace + +TEST_CASE("detail::clamp") +{ + using nlohmann::detail::clamp; + + SECTION("integers") + { + CHECK(clamp(5, 1, 10) == 5); + CHECK(clamp(0, 1, 10) == 1); + CHECK(clamp(11, 1, 10) == 10); + CHECK(clamp(1, 1, 10) == 1); + CHECK(clamp(10, 1, 10) == 10); + CHECK(clamp(7, 7, 7) == 7); + CHECK(clamp(-5, -3, 3) == -3); + CHECK(clamp(std::size_t{0}, std::size_t{64}, std::size_t{0x10000}) == 64); + CHECK(clamp(std::size_t{0x20000}, std::size_t{64}, std::size_t{0x10000}) == 0x10000); + } + + SECTION("floating-point numbers") + { + CHECK(clamp(0.5, 0.0, 1.0) == 0.5); + CHECK(clamp(-0.5, 0.0, 1.0) == 0.0); + CHECK(clamp(1.5, 0.0, 1.0) == 1.0); + CHECK(clamp(-std::numeric_limits::infinity(), 0.0, 1.0) == 0.0); + } + + SECTION("by value: a static constexpr member needs no definition") + { + CHECK(clamp(std::size_t{1}, with_constexpr_member::lower, std::size_t{100}) == 64); + } + + SECTION("constexpr and noexcept") + { + static_assert(clamp(3, 1, 2) == 2, "clamp must be constexpr"); + static_assert(noexcept(clamp(3, 1, 2)), "clamp must be noexcept"); + } +} + +TEST_CASE("detail::bit_cast") +{ + using nlohmann::detail::bit_cast; + + CHECK(bit_cast(1.0) == 0x3FF0000000000000u); + CHECK(bit_cast(-2.0) == 0xC000000000000000u); + CHECK(bit_cast(1.0f) == 0x3F800000u); + CHECK(bit_cast(std::uint64_t{0x4009000000000000u}) == 3.125); + CHECK(bit_cast(std::uint32_t{0xC0490000u}) == -3.140625f); + CHECK(bit_cast(std::uint8_t{0xFF}) == -1); + + // to and from byte arrays + const auto bytes = bit_cast>(std::uint32_t{0x01020304u}); + // qualified: with a std::array argument, ADL would also find std::bit_cast in C++20 + CHECK(nlohmann::detail::bit_cast(bytes) == 0x01020304u); + + static_assert(noexcept(bit_cast(1.0)), "bit_cast must be noexcept"); +} + +TEST_CASE("detail::byteswap") +{ + using nlohmann::detail::byteswap; + + CHECK(byteswap(std::uint16_t{0x0102u}) == 0x0201u); + CHECK(byteswap(std::uint16_t{0xFF00u}) == 0x00FFu); + CHECK(byteswap(std::uint32_t{0x01020304u}) == 0x04030201u); + CHECK(byteswap(std::uint32_t{0xFF000080u}) == 0x800000FFu); + CHECK(byteswap(std::uint64_t{0x0102030405060708u}) == 0x0807060504030201u); + CHECK(byteswap(std::uint64_t{0xFF00000000000080u}) == 0x80000000000000FFu); + + // an involution + for (std::uint64_t x : + { + std::uint64_t{0}, std::uint64_t{1}, std::uint64_t{0x123456789ABCDEF0u}, (std::numeric_limits::max)() + }) + { + CHECK(byteswap(byteswap(x)) == x); + CHECK(byteswap(byteswap(static_cast(x))) == static_cast(x)); + CHECK(byteswap(byteswap(static_cast(x))) == static_cast(x)); + } + + static_assert(std::is_same < decltype(byteswap(std::uint16_t{})), std::uint16_t >::value, "byteswap keeps the type"); + static_assert(std::is_same < decltype(byteswap(std::uint32_t{})), std::uint32_t >::value, "byteswap keeps the type"); + static_assert(std::is_same < decltype(byteswap(std::uint64_t{})), std::uint64_t >::value, "byteswap keeps the type"); +} + +TEST_CASE("detail::count_leading_zeros and count_trailing_zeros") +{ + using nlohmann::detail::count_leading_zeros; + using nlohmann::detail::count_trailing_zeros; + + // a single bit, and that bit with all lower (resp. higher) bits set: both + // halves of the 32-bit fallbacks + for (int k = 0; k < 64; ++k) + { + const std::uint64_t bit = std::uint64_t{1} << k; + CAPTURE(k) + CHECK(count_leading_zeros(bit) == 63 - k); + CHECK(count_leading_zeros(bit | (bit - 1)) == 63 - k); + CHECK(count_trailing_zeros(bit) == k); + CHECK(count_trailing_zeros(bit | ~(bit | (bit - 1))) == k); + } +} diff --git a/tests/src/unit-stream-bulk-scan.cpp b/tests/src/unit-stream-bulk-scan.cpp new file mode 100644 index 000000000..d61ce4bd0 --- /dev/null +++ b/tests/src/unit-stream-bulk-scan.cpp @@ -0,0 +1,452 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +// the library undefines its configuration macros at the end of the header +#if defined(JSON_DISABLE_STREAM_BULK_SCAN) && JSON_DISABLE_STREAM_BULK_SCAN + #define TEST_STREAM_BULK_SCAN 0 +#else + #define TEST_STREAM_BULK_SCAN 1 +#endif + +#include +using nlohmann::json; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// The lexer scans the characters in the get area of a std::istream's +// streambuf directly instead of reading them one at a time. These tests pin +// that this is not observable: for any get area size - including a streambuf +// without a get area, which still takes the character-at-a-time path - the +// parsed values, the exception messages (including the "last read" token), +// the stream state, and the characters left in the stream are the same. See +// unit-stream-bulk-scan-disabled.cpp for JSON_DISABLE_STREAM_BULK_SCAN. + +namespace +{ +// A streambuf that serves its input in get areas of a fixed size, so that +// tokens, numbers, UTF-8 sequences, and indentation cross the end of the get +// area at every possible offset. With a size of 0, it keeps no get area at all +// and serves every character from underflow()/uflow(). +class chunked_streambuf : public std::streambuf +{ + public: + // The first @a unbuffered characters are served without a get area, the + // rest in get areas of @a chunk_size. + chunked_streambuf(std::string s, std::size_t chunk_size, std::size_t unbuffered = 0) + : m_data(std::move(s)), m_chunk_size(chunk_size), m_unbuffered(unbuffered) + {} + + protected: + int_type underflow() override + { + if (m_chunk_size == 0 || m_pos < m_unbuffered) + { + return m_pos < m_data.size() ? traits_type::to_int_type(m_data[m_pos]) : traits_type::eof(); + } + if (gptr() < egptr()) + { + return traits_type::to_int_type(*gptr()); + } + // the previous get area has been consumed + m_pos += static_cast(egptr() - eback()); + if (m_pos >= m_data.size()) + { + setg(nullptr, nullptr, nullptr); + return traits_type::eof(); + } + char* const begin = &m_data[m_pos]; + setg(begin, begin, begin + (std::min)(m_chunk_size, m_data.size() - m_pos)); + return traits_type::to_int_type(*gptr()); + } + + int_type uflow() override + { + if (m_chunk_size == 0 || m_pos < m_unbuffered) + { + return m_pos < m_data.size() ? traits_type::to_int_type(m_data[m_pos++]) : traits_type::eof(); + } + return std::streambuf::uflow(); + } + + private: + std::string m_data; + std::size_t m_pos = 0; + std::size_t m_chunk_size; + std::size_t m_unbuffered; +}; + +// how a test reads from the stream +enum class read_mode +{ + extract, // up to four times operator>>, as for concatenated values + parse, // json::parse(stream) + parse_lenient, // json::parse(stream) with comments +}; + +// Read @a input from a chunked_streambuf and describe everything observable: +// the values or the exception, the stream state after each value, and the +// characters left in the stream. +std::string observe(const std::string& input, std::size_t chunk_size, read_mode mode, std::size_t unbuffered = 0) +{ + chunked_streambuf buf(input, chunk_size, unbuffered); + std::istream is(&buf); + std::string result; + for (int i = 0; i < 4; ++i) + { + json j; + try + { + if (mode == read_mode::extract) + { + is >> j; + } + else + { + j = json::parse(is, nullptr, true, mode == read_mode::parse_lenient); + } + result += "value " + j.dump() + "; state " + std::to_string(static_cast(is.rdstate())) + "\n"; + } + catch (const json::exception& e) + { + result += std::string("exception ") + e.what() + "\n"; + break; + } + if (mode != read_mode::extract) + { + break; + } + is.clear(); + } + + is.clear(); + result += "remaining "; + char c = 0; + while (is.get(c)) + { + result += c; + } + return result; +} + +// get area sizes that put every boundary everywhere in short inputs, plus one +// that holds most inputs completely +std::vector chunk_sizes() +{ + return {1, 2, 3, 4, 5, 7, 8, 9, 13, 16, 64, 4096}; +} + +// valid and invalid inputs covering every token type and every bulk path: +// strings (plain runs, escapes, UTF-8), numbers (all grammar parts, too large +// for an integer), and indentation +std::vector corpus() +{ + return + { + // numbers, terminated in every way + "0", "-0", "123", "-123", "1.5", "-1.5e+10", "2E-3", "123456789012345678901234567890", + "18446744073709551615", "18446744073709551616", "-9223372036854775809", + "123 456", "123,456", "123]", "1true", "42\nfoo", "1.25e3}", "7\t8\r9", + // malformed numbers + "1.", "-", "1e", "1e+", "01", "-a", "1.x", "1ee5", + // strings + R"("")", R"("abc")", R"("a long string with a run of ordinary characters to scan in bulk")", + R"("escapes \" \\ \/ \b \f \n \r \t é 😀")", + "\"UTF-8: \xc3\xa9 \xe2\x82\xac \xf0\x9f\x98\x80 done\"", + // malformed strings + R"("unterminated)", "\"control\x01character\"", "\"ill-formed \xc3\x28 UTF-8\"", R"("bad \x escape")", + R"("\ud800 lone surrogate")", + // literals + "true", "false", "null", "tru", "nul", "falsey", "[true,false,null]", "nulL", "truex", "[fals e]", "t", + "true1", "null null", "[false]", + // structures + "[]", "{}", "[1,2,3]", R"({"a":1,"b":[true,false,null],"c":{"d":"e"}})", "[1,]", R"({"a" 1})", "[[[[[]]]]]", + // pretty-printed + "{\n \"key\": [\n 1,\n -2.5,\n \"three\"\n ]\n}", + "[\n \"deep\"\n ]", + "{\n \"a\": 1,\n \"b\": 2x\n}", + // concatenated values and whitespace + "1 2 3", R"("a""b")", "[1][2]", "{}{}", " \n\t 42 \n\t ", "", + // comments (only accepted by parse_lenient) + "/* comment */ 1", "// comment\n[1, /* c */ 2]", + // byte order mark + "\xef\xbb\xbf[1]", + }; +} +} // namespace + +TEST_CASE("parsing from a std::istream scans the get area in bulk") +{ +#if !defined(JSON_NOEXCEPTION) + SECTION("results do not depend on the get area") + { + const std::vector read_modes = {read_mode::extract, read_mode::parse, read_mode::parse_lenient}; + for (const auto& input : corpus()) + { + for (const read_mode mode : read_modes) + { + // no get area: the character-at-a-time path + const std::string expected = observe(input, 0, mode); + for (const std::size_t chunk_size : chunk_sizes()) + { + CAPTURE(input) + CAPTURE(chunk_size) + CAPTURE(static_cast(mode)) + CHECK(observe(input, chunk_size, mode) == expected); + } + } + } + } + + SECTION("results do not depend on a get area that appears late") + { + // the first characters come without a get area, then the streambuf + // starts to use one: the token that is read at that point is complete + for (const auto& input : corpus()) + { + const std::string expected = observe(input, 0, read_mode::extract); + for (const std::size_t unbuffered : + { + 1u, 2u, 3u, 5u, 8u, 13u, 21u + }) + { + for (const std::size_t chunk_size : + { + 1u, 2u, 3u, 7u, 64u + }) + { + CAPTURE(input) + CAPTURE(unbuffered) + CAPTURE(chunk_size) + CHECK(observe(input, chunk_size, read_mode::extract, unbuffered) == expected); + } + } + } + } +#endif // !defined(JSON_NOEXCEPTION) + + SECTION("values are the same as when parsing from a string") + { + const std::string input = R"({"name": "nlohmann::json", "numbers": [1, -2, 3.5, 4e10, 18446744073709551615],)" + "\n \"text\": \"caf\xc3\xa9 \\u00e9 \\\"quoted\\\"\", \"nested\": [[{}], [[]]], \"flags\": [true, false, null]}"; + const json expected = json::parse(input); + for (const std::size_t chunk_size : chunk_sizes()) + { + CAPTURE(chunk_size) + chunked_streambuf buf(input, chunk_size); + std::istream is(&buf); + CHECK(json::parse(is) == expected); + } + + std::istringstream ss(input); + CHECK(json::parse(ss) == expected); + } + +#if !defined(JSON_NOEXCEPTION) + SECTION("a token that spans several get areas is reported completely") + { + // the error is found after the string has crossed many get areas; the + // token string holds everything since the start of the last token + const std::string input = R"(["this string is longer than every get area", tru])"; + for (const std::size_t chunk_size : chunk_sizes()) + { + CAPTURE(chunk_size) + chunked_streambuf buf(input, chunk_size); + std::istream is(&buf); + json j; + CHECK_THROWS_WITH_AS(is >> j, + "[json.exception.parse_error.101] parse error at line 1, column 50: syntax error while parsing value - invalid literal; last read: '\"this string is longer than every get area\", tru]'", + json::parse_error&); + } + + const std::string unterminated = R"("this string is longer than every get area)"; + for (const std::size_t chunk_size : chunk_sizes()) + { + CAPTURE(chunk_size) + chunked_streambuf buf(unterminated, chunk_size); + std::istream is(&buf); + json j; + CHECK_THROWS_WITH_AS(is >> j, + "[json.exception.parse_error.101] parse error at line 1, column 43: syntax error while parsing value - invalid string: missing closing quote; last read: '\"this string is longer than every get area'", + json::parse_error&); + } + } + + SECTION("an error before the first token reports everything read") + { + chunked_streambuf buf(" \n x", 2); + std::istream is(&buf); + json j; + CHECK_THROWS_WITH_AS(is >> j, + "[json.exception.parse_error.101] parse error at line 2, column 3: syntax error while parsing value - invalid literal; last read: ' x'", + json::parse_error&); + } + + SECTION("lines and columns are counted across get areas") + { + const std::string input = "[\n 1,\n \"two\",\n 3.0,\n x\n]"; + for (const std::size_t chunk_size : chunk_sizes()) + { + CAPTURE(chunk_size) + chunked_streambuf buf(input, chunk_size); + std::istream is(&buf); + json j; + CHECK_THROWS_WITH_AS(is >> j, + "[json.exception.parse_error.101] parse error at line 5, column 5: syntax error while parsing value - invalid literal; last read: '3.0, x'", + json::parse_error&); + } + } + +#endif // !defined(JSON_NOEXCEPTION) + + SECTION("many values from one stream") + { + std::string input; + for (int i = 0; i < 200; ++i) + { + input += R"({"id":)" + std::to_string(i) + R"(,"name":"user)" + std::to_string(i) + R"(","score":)" + std::to_string(i) + ".5}\n"; + input += std::to_string(i * 7919) + "\n"; + } + + for (const std::size_t chunk_size : chunk_sizes()) + { + CAPTURE(chunk_size) + chunked_streambuf buf(input, chunk_size); + std::istream is(&buf); + json j; + int count = 0; + while (!(is >> std::ws).eof()) + { + is >> j; + if (count % 2 == 0) + { + CHECK(j["id"] == count / 2); + } + else + { + CHECK(j == (count / 2) * 7919); + } + ++count; + } + CHECK(count == 400); + } + } + + SECTION("the stream can be used after parsing") + { + for (const std::size_t chunk_size : chunk_sizes()) + { + CAPTURE(chunk_size) + chunked_streambuf buf(R"({"a": "some text"} trailing words 42)", chunk_size); + std::istream is(&buf); + json j; + is >> j; + CHECK(j == json({{"a", "some text"}})); + + std::string word; + int number = 0; + is >> word; + CHECK(word == "trailing"); + is >> word >> number; + CHECK(word == "words"); + CHECK(number == 42); + } + } + + SECTION("std::stringstream and std::ifstream") + { + const std::string input = R"([1, 2.5, "three", {"four": [true, null]}] 5)"; + + std::stringstream ss(input); + json j1; + json j2; + ss >> j1 >> j2; + CHECK(j1 == json::parse(R"([1, 2.5, "three", {"four": [true, null]}])")); + CHECK(j2 == 5); + CHECK(ss.eof()); + + // a file name of its own for every run: the test runs for several C++ + // standards and configurations at the same time, in the same directory + std::random_device rd; + const std::string temp_name = "unit-stream-bulk-scan-" + std::to_string(rd()) + ".tmp"; + const char* const filename = temp_name.c_str(); + { + std::ofstream file(filename); + file << input; + } + { + std::ifstream file(filename); + json k1; + json k2; + file >> k1 >> k2; + CHECK(k1 == j1); + CHECK(k2 == j2); + } + static_cast(std::remove(filename)); + } + + SECTION("binary formats read from a stream") + { + const json j = {{"string", std::string(300, 'x')}, {"array", {1, 2, 3}}, {"number", 1.5}}; + const std::vector cbor = json::to_cbor(j); + const std::string bytes(reinterpret_cast(cbor.data()), cbor.size()); + for (const std::size_t chunk_size : chunk_sizes()) + { + CAPTURE(chunk_size) + chunked_streambuf buf(bytes, chunk_size); + std::istream is(&buf); + CHECK(json::from_cbor(is) == j); + } + } + +#if TEST_STREAM_BULK_SCAN + SECTION("the adapter keeps no token unless the lexer starts one") + { + // the binary readers read through the same adapter, but never start a + // token; refilling the get area must then not collect the input + chunked_streambuf buf(std::string(1000, 'x'), 7); + std::istream is(&buf); + nlohmann::detail::input_stream_adapter ia(is); + for (int i = 0; i < 1000; ++i) + { + CHECK(ia.get_character() == 'x'); + } + + std::vector token; + ia.copy_token(token, false); + CHECK(token.empty()); + } +#endif + + SECTION("SAX parsing from a stream") + { + const std::string input = R"({"a": [1, "two", 3.5]} rest)"; + for (const std::size_t chunk_size : chunk_sizes()) + { + CAPTURE(chunk_size) + chunked_streambuf buf(input, chunk_size); + std::istream is(&buf); + json j; + nlohmann::detail::json_sax_dom_parser sax(j); + CHECK(json::sax_parse(is, &sax, json::input_format_t::json, false)); + CHECK(j == json::parse(R"({"a": [1, "two", 3.5]})")); + } + } +} diff --git a/tests/src/unit-testsuites.cpp b/tests/src/unit-testsuites.cpp index 672981967..52d189457 100644 --- a/tests/src/unit-testsuites.cpp +++ b/tests/src/unit-testsuites.cpp @@ -218,7 +218,11 @@ TEST_CASE("compliance tests from nativejson-benchmark") n1e308[0] = '['; n1e308[1] = '1'; n1e308[310] = ']'; +#if JSON_STRICT_NUL_HANDLING + n1e308.pop_back(); // a NUL byte after the value is a syntax error in strict mode +#else n1e308[311] = '\0'; +#endif TEST_DOUBLE(n1e308, 1E308); } diff --git a/tests/src/unit-throw_messages.cpp b/tests/src/unit-throw_messages.cpp new file mode 100644 index 000000000..d7546cc93 --- /dev/null +++ b/tests/src/unit-throw_messages.cpp @@ -0,0 +1,144 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; + +#include + +// The exceptions in hot core functions are constructed out of line. These tests pin the +// exact what() texts of the sites that moved, so the refactoring cannot change them. + +#define CHECK_MSG(expr, msg) CHECK_THROWS_WITH(expr, msg) + +TEST_CASE("exception messages of out-of-line throw sites") +{ + json arr = {1, 2, 3}; + json arr2 = {4, 5}; + json obj = {{"a", 1}, {"b", 2}}; + json obj2 = {{"c", 3}}; + json num = 1; + + SECTION("iterators") + { + CHECK_MSG((void)(arr.begin() == arr2.begin()), "[json.exception.invalid_iterator.212] cannot compare iterators of different containers"); + CHECK_MSG((void)(arr.begin() < arr2.begin()), "[json.exception.invalid_iterator.212] cannot compare iterators of different containers"); + CHECK_MSG((void)(obj.begin() < obj.end()), "[json.exception.invalid_iterator.213] cannot compare order of object iterators"); + CHECK_MSG(obj.begin() += 1, "[json.exception.invalid_iterator.209] cannot use offsets with object iterators"); + CHECK_MSG((void)(obj.begin() - 1), "[json.exception.invalid_iterator.209] cannot use offsets with object iterators"); + CHECK_MSG((void)obj.begin()[0], "[json.exception.invalid_iterator.208] cannot use operator[] for object iterators"); + CHECK_MSG((void)arr.begin().key(), "[json.exception.invalid_iterator.207] cannot use key() for non-object iterators"); + json null; + CHECK_MSG((void)*null.end(), "[json.exception.invalid_iterator.214] cannot get value"); + } + + SECTION("constructors") + { + CHECK_MSG(json(arr.begin(), arr2.end()), "[json.exception.invalid_iterator.201] iterators are not compatible"); + CHECK_MSG(json(num.begin() + 1, num.end() + 1), "[json.exception.invalid_iterator.204] iterators out of range"); + CHECK_MSG(json(json::initializer_list_t{1, 2}, false, json::value_t::object), "[json.exception.type_error.301] cannot create object from initializer list"); + } + + SECTION("get_ref and operator[]") + { + CHECK_MSG((void)num.get_ref(), "[json.exception.type_error.303] incompatible ReferenceType for get_ref, actual type is number"); + CHECK_MSG((void)num[0], "[json.exception.type_error.305] cannot use operator[] with a numeric argument with number"); + } + + SECTION("at") + { + const json& carr = arr; + const json& cobj = obj; + CHECK_MSG((void)arr.at(static_cast(10)), "[json.exception.out_of_range.401] array index 10 is out of range"); + CHECK_MSG((void)carr.at(static_cast(3)), "[json.exception.out_of_range.401] array index 3 is out of range"); + CHECK_MSG((void)num.at(static_cast(0)), "[json.exception.type_error.304] cannot use at() with number"); + CHECK_MSG((void)obj.at("z"), "[json.exception.out_of_range.403] key 'z' not found"); + CHECK_MSG((void)cobj.at("z"), "[json.exception.out_of_range.403] key 'z' not found"); + CHECK_MSG((void)obj.at(std::string("zz")), "[json.exception.out_of_range.403] key 'zz' not found"); + CHECK_MSG((void)num.at("a"), "[json.exception.type_error.304] cannot use at() with number"); + + nlohmann::ordered_json oobj = {{"a", 1}}; + const nlohmann::ordered_json& coobj = oobj; + CHECK_MSG((void)oobj.at("z"), "[json.exception.out_of_range.403] key 'z' not found"); + CHECK_MSG((void)coobj.at("z"), "[json.exception.out_of_range.403] key 'z' not found"); + CHECK_MSG((void)coobj.at(std::string("zz")), "[json.exception.out_of_range.403] key 'zz' not found"); + } + + SECTION("erase") + { + CHECK_MSG(arr.erase(arr2.begin()), "[json.exception.invalid_iterator.202] iterator does not fit current value"); + CHECK_MSG(num.erase(num.end()), "[json.exception.invalid_iterator.205] iterator out of range"); + CHECK_MSG(arr.erase(arr.begin(), arr2.end()), "[json.exception.invalid_iterator.203] iterators do not fit current value"); + CHECK_MSG(num.erase(num.begin() + 1, num.end()), "[json.exception.invalid_iterator.204] iterators out of range"); + CHECK_MSG(arr.erase(static_cast(10)), "[json.exception.out_of_range.401] array index 10 is out of range"); + } + + SECTION("insert") + { + CHECK_MSG(arr.insert(arr2.begin(), 1), "[json.exception.invalid_iterator.202] iterator does not fit current value"); + CHECK_MSG(arr.insert(arr2.begin(), 2, 1), "[json.exception.invalid_iterator.202] iterator does not fit current value"); + CHECK_MSG(arr.insert(arr2.begin(), arr2.begin(), arr2.end()), "[json.exception.invalid_iterator.202] iterator does not fit current value"); + CHECK_MSG(arr.insert(arr.begin(), arr.begin(), arr2.end()), "[json.exception.invalid_iterator.210] iterators do not fit"); + CHECK_MSG(arr.insert(arr.begin(), arr.begin(), arr.end()), "[json.exception.invalid_iterator.211] passed iterators may not belong to container"); + CHECK_MSG(arr.insert(arr.begin(), obj.begin(), obj.end()), "[json.exception.invalid_iterator.202] iterators first and last must point to arrays"); + CHECK_MSG(arr.insert(arr2.begin(), {1, 2}), "[json.exception.invalid_iterator.202] iterator does not fit current value"); + CHECK_MSG(obj.insert(obj2.begin(), arr.end()), "[json.exception.invalid_iterator.210] iterators do not fit"); + CHECK_MSG(obj.insert(arr.begin(), arr.end()), "[json.exception.invalid_iterator.202] iterators first and last must point to objects"); + CHECK_MSG(obj.update(arr.begin(), arr2.end()), "[json.exception.invalid_iterator.210] iterators do not fit"); + } + +#if !defined(JSON_NOEXCEPTION) // patch_msg() catches the exceptions, which JSON_NOEXCEPTION turns into std::abort() + SECTION("patch") + { + const auto patch_msg = [](const char* doc, const char* patch) -> std::string + { + try + { + json d = json::parse(doc); + d.patch(json::parse(patch)); + } + catch (const json::exception& e) + { + // with JSON_DIAGNOSTICS, what() names the offending value like "(/0) ", + // and with JSON_DIAGNOSTIC_POSITIONS its position like "(bytes 0-2) "; drop both + std::string msg = e.what(); + for (const char* marker : + { + " (/", " (bytes " + }) + { + const auto pos = msg.find(marker); + if (pos != std::string::npos) + { + msg.erase(pos + 1, msg.find(") ", pos) - pos + 1); + } + } + return msg; + } + return {}; + }; + + CHECK(patch_msg("{}", "{}") == "[json.exception.parse_error.104] parse error: JSON patch must be an array of objects"); + CHECK(patch_msg("{}", "[1]") == "[json.exception.parse_error.104] parse error: JSON patch must be an array of objects"); + CHECK(patch_msg("{}", "[{}]") == "[json.exception.parse_error.105] parse error: operation must have member 'op'"); + CHECK(patch_msg("{}", "[{\"op\":1}]") == "[json.exception.parse_error.105] parse error: operation must have string member 'op'"); + CHECK(patch_msg("{}", "[{\"op\":\"add\"}]") == "[json.exception.parse_error.105] parse error: operation 'add' must have member 'path'"); + CHECK(patch_msg("{}", "[{\"op\":\"add\",\"path\":1}]") == "[json.exception.parse_error.105] parse error: operation 'add' must have string member 'path'"); + CHECK(patch_msg("{}", "[{\"op\":\"foo\",\"path\":\"\"}]") == "[json.exception.parse_error.105] parse error: operation value 'foo' is invalid"); + CHECK(patch_msg("{\"a\":1}", "[{\"op\":\"remove\",\"path\":\"/b\"}]") == "[json.exception.out_of_range.403] key 'b' not found"); + CHECK(patch_msg("[1]", "[{\"op\":\"add\",\"path\":\"/5\",\"value\":0}]") == "[json.exception.out_of_range.401] array index 5 is out of range"); + CHECK(patch_msg("{\"a\":1}", "[{\"op\":\"add\",\"path\":\"/a/b\",\"value\":0}]") == "[json.exception.out_of_range.411] cannot add value: the JSON Patch 'add' target's parent is of type number, but must be an object or array"); + CHECK(patch_msg("{\"a\":1}", "[{\"op\":\"remove\",\"path\":\"/a/b\"}]") == "[json.exception.out_of_range.413] cannot remove value: the JSON Patch 'remove' target's parent is of type number, but must be an object or array"); + CHECK(patch_msg("{\"a\":{}}", "[{\"op\":\"move\",\"from\":\"/a\",\"path\":\"/a/b\"}]") == "[json.exception.out_of_range.414] cannot move value: 'from' path '/a' is a proper prefix of 'path' '/a/b'"); + CHECK(patch_msg("{\"a\":1}", "[{\"op\":\"test\",\"path\":\"/a\",\"value\":2}]") == "[json.exception.other_error.501] unsuccessful: {\"op\":\"test\",\"path\":\"/a\",\"value\":2}"); + CHECK(patch_msg("{\"a\":1}", "[{\"op\":\"move\",\"path\":\"/a\"}]") == "[json.exception.parse_error.105] parse error: operation 'move' must have member 'from'"); + } +#endif +} diff --git a/tests/src/unit-to_chars.cpp b/tests/src/unit-to_chars.cpp index def2793c2..f8863a5a9 100644 --- a/tests/src/unit-to_chars.cpp +++ b/tests/src/unit-to_chars.cpp @@ -13,7 +13,22 @@ #include "doctest_compatibility.h" #include -using nlohmann::detail::dtoa_impl::reinterpret_bits; +using nlohmann::detail::bit_cast; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(JSON_HAS_CPP_17) + #include +#endif namespace { @@ -29,7 +44,7 @@ float make_float(uint32_t sign_bit, uint32_t biased_exponent, uint32_t significa bits |= biased_exponent << 23; bits |= significand; - return reinterpret_bits(bits); + return bit_cast(bits); } // ldexp -- convert f * 2^e to IEEE single precision @@ -66,7 +81,7 @@ float make_float(uint64_t f, int e) : static_cast(e + kExponentBias); const uint64_t bits = (f & kSignificandMask) | (biased_exponent << kPhysicalSignificandSize); - return reinterpret_bits(static_cast(bits)); + return bit_cast(static_cast(bits)); } double make_double(uint64_t sign_bit, uint64_t biased_exponent, uint64_t significand) @@ -81,7 +96,7 @@ double make_double(uint64_t sign_bit, uint64_t biased_exponent, uint64_t signifi bits |= biased_exponent << 52; bits |= significand; - return reinterpret_bits(bits); + return bit_cast(bits); } // ldexp -- convert f * 2^e to IEEE double precision @@ -118,7 +133,7 @@ double make_double(uint64_t f, int e) : static_cast(e + kExponentBias); const uint64_t bits = (f & kSignificandMask) | (biased_exponent << kPhysicalSignificandSize); - return reinterpret_bits(bits); + return bit_cast(bits); } } // namespace @@ -450,7 +465,7 @@ TEST_CASE("formatting") check_double( 1.2345e+18, "1.2345e+18" ); // 1.2345e+18 1.2345e+18 1.2345e18 check_double( 1.2345e+19, "1.2345e+19" ); // 1.2345e+19 1.2345e+19 1.2345e19 check_double( 1.2345e+20, "1.2345e+20" ); // 1.2345e+20 1.2345e+20 1.2345e20 - check_double( 1.2345e+21, "1.2344999999999999e+21" ); // 1.2345e+21 1.2344999999999999e+21 1.2345e21 + check_double( 1.2345e+21, "1.2345e+21" ); // 1.2345e+21 1.2344999999999999e+21 1.2345e21 check_double( 1.2345e+22, "1.2345e+22" ); // 1.2345e+22 1.2345e+22 1.2345e22 } @@ -514,3 +529,402 @@ TEST_CASE("formatting") check_integer(1000000000000000000LL, "1000000000000000000"); } } + +namespace +{ +// a small unsigned big integer (32-bit limbs, least significant first), to +// recompute the powers of ten of the shortest double conversion +using big = std::vector; + +void big_mul_small(big& x, std::uint32_t m) +{ + std::uint64_t carry = 0; + for (auto& limb : x) + { + const std::uint64_t v = (static_cast(limb) * m) + carry; + limb = static_cast(v); + carry = v >> 32u; + } + if (carry != 0) + { + x.push_back(static_cast(carry)); + } +} + +void big_div_small(big& x, std::uint32_t d) +{ + std::uint64_t rest = 0; + for (std::size_t i = x.size(); i-- > 0;) + { + const std::uint64_t v = (rest << 32u) | x[i]; + x[i] = static_cast(v / d); + rest = v % d; + } + while (!x.empty() && x.back() == 0) + { + x.pop_back(); + } +} + +std::size_t big_bit_length(const big& x) +{ + std::size_t n = 32 * x.size(); + for (std::uint32_t top = x.back(); (top & 0x80000000u) == 0; top <<= 1u) + { + --n; + } + return n; +} + +bool big_bit(const big& x, std::size_t i) +{ + return ((x[i / 32] >> (i % 32)) & 1u) != 0; +} + +/// the 128 most significant bits of x (floor), shifted left if x has fewer bits +std::pair big_top128(const big& x) +{ + const std::size_t n = big_bit_length(x); + std::uint64_t high = 0; + std::uint64_t low = 0; + for (std::size_t k = 0; k < 128; ++k) + { + const bool bit = k < n && big_bit(x, n - 1 - k); + if (k < 64) + { + high = (high << 1u) | (bit ? 1u : 0u); + } + else + { + low = (low << 1u) | (bit ? 1u : 0u); + } + } + return {high, low}; +} + +/// the digits (without trailing zeros) and the decimal exponent of a +/// representation "[-]d[.ddd][e[+-]x]" +std::pair digits_and_exponent(const std::string& s) +{ + std::string digits; + int point = -1; + int exponent = 0; + for (std::size_t i = 0; i < s.size(); ++i) + { + const char c = s[i]; + if (c >= '0' && c <= '9') + { + digits += c; + } + else if (c == '.') + { + point = static_cast(digits.size()); + } + else if (c == 'e' || c == 'E') + { + exponent = std::stoi(s.substr(i + 1)); + break; + } + } + int e = exponent + (point < 0 ? static_cast(digits.size()) : point) - static_cast(digits.size()); + const std::size_t first = digits.find_first_not_of('0'); + digits = first == std::string::npos ? "0" : digits.substr(first); + while (digits.size() > 1 && digits.back() == '0') + { + digits.pop_back(); + ++e; + } + return {digits, e}; +} + +/// the correctly rounded double of a decimal text +/// (not std::strtod: the C runtimes of some platforms, e.g. MinGW's, round +/// some 16 and 17 digit inputs wrongly) +double parse_double(const std::string& text) +{ + const nlohmann::json j = nlohmann::json::parse(text, nullptr, false); + // (a discarded value: out of range, as strtod's HUGE_VAL) + return j.is_discarded() ? std::numeric_limits::infinity() : j.get(); +} + +/// whether the decimal digits * 10^e reads back as v +bool reads_back(const std::string& digits, int e, double v) +{ + const std::string text = digits + "e" + std::to_string(e); + // (compared bit for bit: v is positive and finite, and -Wfloat-equal) + return bit_cast(parse_double(text)) == bit_cast(v); +} + +/// Check the representation of a positive finite double: it reads back as +/// the same value, and no representation with fewer digits does. +void check_shortest(double v) +{ + std::array buf{}; + char* end = nlohmann::detail::to_chars(buf.data(), buf.data() + 32, v); + const std::string text(buf.data(), end); + CAPTURE(text) + CHECK(parse_double(text) == v); + // the layout is that of format_buffer() for the digits of Zmij + const auto sd = nlohmann::detail::zmij::to_shortest(bit_cast(v)); + const std::uint64_t significand = sd.has_digit ? (sd.integral * 10) + sd.digit : sd.integral; + int exponent = sd.has_digit ? sd.exponent : sd.exponent + 1; + std::string significand_digits = std::to_string(significand); + while (significand_digits.size() > 1 && significand_digits.back() == '0') + { + significand_digits.pop_back(); + ++exponent; + } + std::array reference{}; + std::copy(significand_digits.begin(), significand_digits.end(), reference.begin()); + const char* const reference_end = nlohmann::detail::dtoa_impl::format_buffer(reference.data(), static_cast(significand_digits.size()), exponent, -4, 15); + CHECK(text == std::string(reference.data(), static_cast(reference_end - reference.data()))); + // and write_positive() is what to_chars() calls + std::array positive{}; + const char* const positive_end = nlohmann::detail::dtoa_impl::write_positive(positive.data(), positive.data() + positive.size(), v); + CHECK(text == std::string(positive.data(), static_cast(positive_end - positive.data()))); + const auto de = digits_and_exponent(text); + const std::string& digits = de.first; + if (digits.size() > 1) + { + // the decimals of one digit fewer next to the value: v lies within + // half a unit of the last digit of text, so the closest ones are the + // truncated digits and their successor + // (not a stream with std::setprecision(digits.size() - 2): the + // standard library of MSVC 2017 ignores a precision of 0) + const std::string m = digits.substr(0, digits.size() - 1); + const int e = de.second + 1; + const std::uint64_t mid = std::stoull(m); + for (const std::uint64_t candidate : + { + mid - 1, mid, mid + 1 + }) + { + CAPTURE(candidate) + CHECK(!reads_back(std::to_string(candidate), e, v)); + } + } + // (icpc with libstdc++ 11 defines __cpp_lib_to_chars, but has no floating-point std::to_chars) +#if defined(JSON_HAS_CPP_17) && defined(__cpp_lib_to_chars) && !defined(__INTEL_COMPILER) + // the closest of the shortest representations, as std::to_chars finds it + std::array std_text{}; + const auto r = std::to_chars(std_text.data(), std_text.data() + std_text.size(), v, std::chars_format::scientific); + CHECK(digits_and_exponent(std::string(std_text.data(), r.ptr)) == de); +#endif +} +} // namespace + +TEST_CASE("shortest digits of doubles") +{ + SECTION("powers of ten") + { + // the 128-bit significands of 10^k, rounded down, recomputed + for (int k = -342; k <= 341; ++k) + { + CAPTURE(k) + big x{1}; + if (k >= 0) + { + for (int i = 0; i < k; ++i) + { + big_mul_small(x, 10); + } + } + else + { + // floor(2^b / 10^-k) for a b that leaves more than 128 bits + const int b = 128 + 64 + (4 * -k); + x.assign(static_cast(b / 32) + 1, 0); + x.back() = 1u << (b % 32); + for (int i = 0; i < -k; ++i) + { + big_div_small(x, 10); + } + } + const auto expected = big_top128(x); + const auto actual = nlohmann::detail::zmij::pow10(k); + CHECK(actual.high == expected.first); + CHECK(actual.low == expected.second); + } + } + + SECTION("boundary values") + { + for (const double v : + { + std::numeric_limits::min(), std::numeric_limits::max(), std::numeric_limits::denorm_min(), + std::nextafter(std::numeric_limits::min(), 0.0), 1.0, 2.0, 0.1, 0.3, 1e21, 1e22, 1e23, 5e-324, 9007199254740993.0, + 1.2345e+21, 2.2250738585072014e-308, 1.7976931348623157e308, 4.9406564584124654e-324, 123456789012345680.0 + }) + { + check_shortest(v); + } + // all powers of two (their rounding interval is narrower below) + for (int e = -1074; e <= 1023; ++e) + { + check_shortest(std::ldexp(1.0, e)); + } + // powers of ten and their neighbors + for (int e = -323; e <= 308; ++e) + { + const double p = std::strtod(("1e" + std::to_string(e)).c_str(), nullptr); + check_shortest(p); + check_shortest(std::nextafter(p, 0.0)); + check_shortest(std::nextafter(p, std::numeric_limits::infinity())); + } + } + + SECTION("random doubles") + { + std::mt19937_64 rng(5295); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible + for (int i = 0; i < 100000; ++i) + { + const std::uint64_t bits = rng() & 0x7FFFFFFFFFFFFFFFu; + const auto v = bit_cast(bits); + if (std::isfinite(v) && bits != 0) + { + check_shortest(v); + } + } + } +} + +TEST_CASE("choice of the conversion") +{ + using nlohmann::detail::dtoa_impl::is_binary64; + + SECTION("by the format of the type") + { + // Zmij needs binary64 numbers; everything else uses Grisu2 + static_assert(!is_binary64::value, "float is not binary64"); + static_assert(is_binary64::value == (std::numeric_limits::is_iec559 && std::numeric_limits::digits == 53), + "double is binary64 where it is IEEE 754 with 53 digits"); + static_assert(!is_binary64::value, "integers are not binary64"); + static_assert(is_binary64::value == (std::numeric_limits::is_iec559 && std::numeric_limits::digits == 53 && sizeof(long double) == 8), + "long double is binary64 where it has the format of a double"); + CHECK(!is_binary64::value); + CHECK(is_binary64::value); + } + + SECTION("float: Grisu2, double: Zmij") + { + // 5.3165205877497296e+16 is one of the doubles for which Grisu2 does not find the shortest digits + constexpr double value = 5.3165205877497296e+16; + std::array buf{}; + const char* const last = buf.data() + buf.size(); + + char* end = nlohmann::detail::dtoa_impl::write_positive(buf.data(), last, value); + CHECK(std::string(buf.data(), end) == "5.31652058774973e+16"); + end = nlohmann::detail::dtoa_impl::write_positive_grisu2(buf.data(), last, value); + CHECK(std::string(buf.data(), end) == "5.3165205877497296e+16"); + + constexpr float f = 1.1754944e-38f; + end = nlohmann::detail::dtoa_impl::write_positive(buf.data(), last, f); + const std::string dispatched(buf.data(), end); + end = nlohmann::detail::dtoa_impl::write_positive_grisu2(buf.data(), last, f); + CHECK(dispatched == std::string(buf.data(), end)); + } + + SECTION("long double with the format of a double: Zmij") + { + // (on platforms where long double is wider, Grisu2 does not apply either: the snprintf fallback does) + // (constant on a given platform) + DOCTEST_MSVC_SUPPRESS_WARNING_PUSH + DOCTEST_MSVC_SUPPRESS_WARNING(4127) + if (std::numeric_limits::digits == 53 && std::numeric_limits::is_iec559) + { + using long_double_json = nlohmann::json::with_float_t; + for (const double d : + { + 5.3165205877497296e+16, 1.0, 0.1, 123456.789, 2.2250738585072014e-308, 1.7976931348623157e+308, -5.3165205877497296e+16 + }) + { + CAPTURE(d) + CHECK(long_double_json(static_cast(d)).dump() == nlohmann::json(d).dump()); + } + CHECK(long_double_json(5.3165205877497296e+16L).dump() == "5.31652058774973e+16"); + } + DOCTEST_MSVC_SUPPRESS_WARNING_POP + } +} + +TEST_CASE("short decimals") +{ + // write_short_decimal() writes digits * 10^exp for the digits of a double + // that need no conversion (at most 15, the first not 0): as to_chars() + // writes the (positive) double that has these digits + const auto written = [](std::uint64_t digits, int exp) -> std::string + { + std::array buf{}; // (up to 41 bytes are written) + char* const end = nlohmann::detail::dtoa_impl::write_short_decimal(buf.data(), digits, exp); + return {buf.data(), end}; + }; + const auto written_counted = [](std::uint64_t digits, int count, int exp) -> std::string + { + std::array buf{}; + char* const end = nlohmann::detail::dtoa_impl::write_short_decimal(buf.data(), digits, count, exp); + return {buf.data(), end}; + }; + const auto expected = [](std::uint64_t digits, int exp) -> std::string + { + const double value = std::strtod((std::to_string(digits) + "e" + std::to_string(exp)).c_str(), nullptr); + std::array buf{}; + char* const end = nlohmann::detail::to_chars(buf.data(), buf.data() + 32, value); + return {buf.data(), end}; + }; + + SECTION("powers of ten") + { + const auto& powers = nlohmann::detail::dtoa_impl::powers_of_ten(); + CHECK(powers.front() == 1u); + for (std::size_t i = 1; i < powers.size(); ++i) + { + CHECK(powers[i] == powers[i - 1] * 10u); + } + CHECK(powers.back() == 10000000000000000000u); + } + + SECTION("examples") + { + CHECK(written(1, 0) == "1.0"); + CHECK(written(15, -1) == "1.5"); + CHECK(written(125, -2) == "1.25"); + CHECK(written(1, 22) == "1e+22"); + CHECK(written(123456789012345, -2) == "1234567890123.45"); + CHECK(written(999999999999999, -15) == "0.999999999999999"); + CHECK(written(5, -324) == "5e-324"); + CHECK(written_counted(1, 1, 0) == "1.0"); + CHECK(written_counted(125, 3, -2) == "1.25"); + CHECK(written_counted(100, 3, -2) == "1.0"); + CHECK(written_counted(999999999999999, 15, -15) == "0.999999999999999"); + } + + SECTION("random digits, exponents and trailing zeros") + { + std::mt19937_64 rng(1170); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) + for (int i = 0; i < 100000; ++i) + { + // 1 to 15 digits, the first not 0, and up to 14 of them trailing zeros + std::uint64_t count = 1 + (rng() % 15u); + std::uint64_t power = 1; + for (std::uint64_t k = 1; k < count; ++k) + { + power *= 10; + } + std::uint64_t digits = power + (rng() % (9 * power)); + const std::uint64_t zeros = (rng() % 3u == 0) ? (rng() % count) : 0; + for (std::uint64_t k = 0; k < zeros; ++k) + { + digits = (digits / 10) * 10; + } + // (a value between 1e-300 and 1e300) + const int exp = static_cast(rng() % 560u) - 300 - static_cast(count); + + CAPTURE(digits) + CAPTURE(count) + CAPTURE(exp) + const std::string want = expected(digits, exp); + CHECK(written(digits, exp) == want); + CHECK(written_counted(digits, static_cast(count), exp) == want); + } + } +} diff --git a/tests/src/unit-to_json-rvalue.cpp b/tests/src/unit-to_json-rvalue.cpp new file mode 100644 index 000000000..4aabda5a1 --- /dev/null +++ b/tests/src/unit-to_json-rvalue.cpp @@ -0,0 +1,252 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) + #if __has_include() + #include + #define TO_JSON_RVALUE_HAS_FS 1 + #endif +#endif + +namespace +{ +// element type that counts how it is converted to JSON: a conversion from an +// lvalue copies, a conversion from an rvalue moves +struct tracked +{ + std::string payload; + bool moved_from = false; + + explicit tracked(std::string p) noexcept : payload(std::move(p)) {} + tracked(const tracked&) = default; + tracked& operator=(const tracked&) = default; + tracked(tracked&& other) noexcept : payload(std::move(other.payload)), moved_from(other.moved_from) + { + other.moved_from = true; + } + // not needed by the tests (and an unused definition fails -Wunneeded-member-function) + tracked& operator=(tracked&&) = delete; + ~tracked() = default; +}; + +int tracked_copies = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables,misc-use-internal-linkage) +int tracked_moves = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables,misc-use-internal-linkage) + +void to_json(json& j, const tracked& t) +{ + ++tracked_copies; + j = t.payload; +} + +void to_json(json& j, tracked&& t) +{ + ++tracked_moves; + j = std::move(t.payload); + t.moved_from = true; +} + +// user type with its own rvalue-aware serializer +struct udt_container +{ + std::vector values; +}; +} // namespace + +namespace nlohmann +{ +template<> +struct adl_serializer +{ + static void to_json(json& j, const udt_container& c) + { + j = c.values; + j.push_back("copied"); + } + static void to_json(json& j, udt_container&& c) + { + j = std::move(c.values); + j.push_back("moved"); + } +}; +} // namespace nlohmann + +TEST_CASE("to_json from rvalue containers") +{ + const std::string long_a(64, 'a'); + const std::string long_b(64, 'b'); + + SECTION("vector: elements are moved") + { + std::vector v{long_a, long_b}; + const json j = std::move(v); + CHECK(j == json::array({long_a, long_b})); + for (const auto& s : v) // NOLINT(bugprone-use-after-move,hicpp-invalid-access-moved) + { + CHECK(s.empty()); // long strings leave the source element empty when moved + } + } + + SECTION("vector: lvalues are copied and left untouched") + { + const std::vector v{long_a, long_b}; + const json j = v; + CHECK(j == json::array({long_a, long_b})); + CHECK(v[0] == long_a); + CHECK(v[1] == long_b); + + std::vector v2{long_a, long_b}; + const json j2 = v2; + CHECK(j2 == j); + CHECK(v2[0] == long_a); + CHECK(v2[1] == long_b); + } + + SECTION("tracked elements: rvalue moves, lvalue copies") + { + tracked_copies = 0; + tracked_moves = 0; + + std::vector v; + v.emplace_back("x"); + v.emplace_back("y"); + + const json copy = v; + CHECK(copy == json::array({"x", "y"})); + CHECK(tracked_copies == 2); + CHECK(tracked_moves == 0); + CHECK_FALSE(v[0].moved_from); + + const json moved = std::move(v); + CHECK(moved == copy); + CHECK(tracked_copies == 2); + CHECK(tracked_moves == 2); + } + + SECTION("const rvalue is copied") + { + tracked_copies = 0; + tracked_moves = 0; + const std::vector v(2, tracked("z")); + const json j = std::move(v); // NOLINT(performance-move-const-arg,hicpp-move-const-arg) + CHECK(j == json::array({"z", "z"})); + CHECK(tracked_copies == 2); + CHECK(tracked_moves == 0); + } + + SECTION("other sequence containers") + { + CHECK(json(std::deque {long_a, long_b}) == json::array({long_a, long_b})); + CHECK(json(std::list {long_a, long_b}) == json::array({long_a, long_b})); + CHECK(json(std::forward_list {long_a, long_b}) == json::array({long_a, long_b})); + CHECK(json(std::array {{long_a, long_b}}) == json::array({long_a, long_b})); + CHECK(json(std::set {long_a, long_b}) == json::array({long_a, long_b})); + CHECK(json(std::vector {true, false}) == json::array({true, false})); + CHECK(json(std::vector {1, 2, 3}) == json::array({1, 2, 3})); + CHECK(json(std::vector> {{long_a}, {long_b}}) == json::array({json::array({long_a}), json::array({long_b})})); + CHECK(json(std::vector {1, "a"}) == json::array({1, "a"})); + } + + SECTION("valarray") + { + std::valarray va(3); + va[0] = 1; + va[1] = 2; + va[2] = 3; + CHECK(json(std::move(va)) == json::array({1, 2, 3})); // NOLINT(performance-move-const-arg,hicpp-move-const-arg) + CHECK(json(std::valarray(2)) == json::array({0, 0})); + } + + SECTION("array_t and json arrays") + { + json::array_t arr{1, 2}; + CHECK(json(std::move(arr)) == json::array({1, 2})); + json a = json::array({1, 2}); + const json b = std::move(a); + CHECK(b == json::array({1, 2})); + } + + SECTION("object-like containers") + { + std::map m{{"a", long_a}, {"b", long_b}}; + const json j = std::move(m); + CHECK(j == json({{"a", long_a}, {"b", long_b}})); + for (const auto& kv : m) // NOLINT(bugprone-use-after-move,hicpp-invalid-access-moved) + { + CHECK(kv.second.empty()); // values moved, keys (const) copied + } + + const std::map cm{{"a", long_a}}; + const json jc = cm; + CHECK(jc["a"] == long_a); + CHECK(cm.at("a") == long_a); + + std::unordered_map um{{"a", long_a}}; + CHECK(json(std::move(um)) == json({{"a", long_a}})); + + std::multimap mm{{"a", 1}}; + CHECK(json(std::move(mm)) == json({{"a", 1}})); + + tracked_copies = 0; + tracked_moves = 0; + std::map mt; + mt.emplace("k", tracked("v")); + const json jt = std::move(mt); + CHECK(jt == json({{"k", "v"}})); + CHECK(tracked_moves == 1); + CHECK(tracked_copies == 0); + + json::object_t obj{{"a", 1}}; + CHECK(json(std::move(obj)) == json({{"a", 1}})); + } + + SECTION("user type with its own to_json overloads") + { + udt_container c{{1, 2}}; + const json lv = c; + CHECK(lv == json::array({1, 2, "copied"})); + const json rv = std::move(c); + CHECK(rv == json::array({1, 2, "moved"})); + } + + SECTION("assignment and implicit conversion") + { + std::vector v{long_a}; + json j; + j = std::move(v); + CHECK(j == json::array({long_a})); + json k = {std::vector{long_b}}; + CHECK(k.size() == 1); + } + +#ifdef TO_JSON_RVALUE_HAS_FS + SECTION("std::filesystem::path rvalue is still a string") + { + CHECK(json(std::filesystem::path("a/b")) == json("a/b")); +#ifdef __cpp_lib_char8_t + CHECK(json(std::u8string(u8"a/b")) == json("a/b")); +#endif + } +#endif +} diff --git a/tools/generate_natvis/README.md b/tools/generate_natvis/README.md index 3995758c2..8f45f1877 100644 --- a/tools/generate_natvis/README.md +++ b/tools/generate_natvis/README.md @@ -1,6 +1,10 @@ # generate_natvis.py -Generate the Natvis debugger visualization file for all supported namespace combinations. +Generate the Natvis debugger visualization file for namespace combinations with up to two ABI tags. + +The checked-in `nlohmann_json.natvis` covers the default namespace (no ABI tags), every single ABI tag, and every pair +of ABI tags, with both versioned and unversioned namespace variants. Configurations that enable three or more ABI tags +are not covered by this reduced file; use `--abi-tags` or `--all` (see below) to generate a file for them. The ABI tag list and the library version are parsed from `include/nlohmann/detail/abi_macros.hpp`, so this script must be re-run (via @@ -15,13 +19,47 @@ file or the library version is bumped — otherwise the committed make natvis ``` -or, equivalently: +`make natvis` sets up a virtual environment `tools/generate_natvis/venv` with the dependencies from +`requirements.txt` (Jinja2) and runs the script with it. Alternatively, install the requirements yourself and run: ```shell -./generate_natvis.py [--version X.Y.Z] [repository_root/] +./generate_natvis.py [--version X.Y.Z] [--abi-tags TAGS]... [--all] [--list-tags] [--output FILE] [repository_root/] ``` -`--version` and the output/repository-root directory both default to values +`--version` and the repository root both default to values derived from this script's own location, so they only need to be given explicitly when generating a Natvis file for a different checkout or a version other than the one in `abi_macros.hpp`. + +| Option | Description | +|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--list-tags` | Print every ABI tag (in namespace order) with the macro that enables it and the `NLOHMANN_JSON_ABI_TAG_*` identifier, then exit. | +| `--abi-tags TAGS` | Add the namespaces of one configuration, given as a comma-separated list of ABI tags (`_diag,_dp,_vns`; the leading underscore is optional, and the order does not matter). Can be repeated for several configurations. Unknown tags are an error. | +| `--all` | Generate the namespaces for every combination of ABI tags. The file size doubles with each tag (about 60 MB for 12 tags). Cannot be combined with `--abi-tags`. | +| `--output FILE` | Write the Natvis file to `FILE` instead of `nlohmann_json.natvis` in the repository root. | + +Each configuration gets both the versioned (`json_abi_diag_dp_vns_v3_12_0`) and the unversioned (`json_abi_diag_dp_vns`, +used with `NLOHMANN_JSON_NAMESPACE_NO_VERSION`) namespace. The default namespaces (up to two ABI tags) are always +included, so a generated file can replace the checked-in one. + +### Generating a Natvis file for your configuration + +1. Check out the release tag of the library version you use (the ABI tags and the version are read from the checkout). +2. Find the ABI tags of the macros you enable: + + ```shell + make natvis NATVIS_ARGS="--list-tags" + ``` + +3. Generate a Natvis file with them, for example for `JSON_DIAGNOSTICS`, `JSON_DIAGNOSTIC_POSITIONS`, and + `JSON_VIEW_NO_SIMD`: + + ```shell + make natvis NATVIS_ARGS="--abi-tags _diag,_dp,_vns --output my_nlohmann_json.natvis" + ``` + +4. Use `my_nlohmann_json.natvis` instead of the shipped `nlohmann_json.natvis`, for example by adding it to your Visual + Studio project. + +Passing `--output` keeps the checked-in `nlohmann_json.natvis` unchanged; without it, `make check-amalgamation` would +report the file as out of date. diff --git a/tools/generate_natvis/generate_natvis.py b/tools/generate_natvis/generate_natvis.py index f4cd6fcd2..d660c4b63 100755 --- a/tools/generate_natvis/generate_natvis.py +++ b/tools/generate_natvis/generate_natvis.py @@ -20,9 +20,11 @@ def semver(v): def abi_info(repo_root): - """Derive the ABI tag list (in NLOHMANN_JSON_ABI_TAGS_CONCAT order) and the + """Derive the ABI tags (in NLOHMANN_JSON_ABI_TAGS_CONCAT order) and the library version from /include/nlohmann/detail/abi_macros.hpp, - so this script cannot drift from the header it visualizes.""" + so this script cannot drift from the header it visualizes. Each tag is + returned as a (suffix, configuration macro, tag macro) tuple, for example + ('_diag', 'JSON_DIAGNOSTICS', 'NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS').""" abi_macros_hpp = os.path.join(repo_root, 'include', 'nlohmann', 'detail', 'abi_macros.hpp') with open(abi_macros_hpp) as f: content = f.read() @@ -47,7 +49,9 @@ def abi_info(repo_root): match = re.search(r'#define\s+' + re.escape(ident) + r'\s+(_\w+)\s*\n', content) if not match: raise ValueError(f'could not find a non-empty #define for {ident} in {abi_macros_hpp}') - abi_tags.append(match.group(1)) + # the option that enables the tag is the #if/#ifdef right above it + option = re.search(r'#\s*(?:if|ifdef)\s+(\w+)\s*\n\s*#define\s+' + re.escape(ident) + r'\s+_', content) + abi_tags.append((match.group(1), option.group(1) if option else '?', ident)) version = {} for part in ('MAJOR', 'MINOR', 'PATCH'): @@ -59,37 +63,94 @@ def abi_info(repo_root): return abi_tags, '{MAJOR}.{MINOR}.{PATCH}'.format(**version) +def parse_tag_list(value, abi_tags): + """Turn one --abi-tags value ("_diag,dp") into the tuple of suffixes it + names, in namespace order; unknown tags and empty lists are errors.""" + known = [suffix for suffix, _, _ in abi_tags] + requested = set() + for tag in value.split(','): + tag = tag.strip() + if not tag: + continue + if not tag.startswith('_'): + tag = '_' + tag + if tag not in known: + raise ValueError(f'unknown ABI tag {tag!r}; valid tags: {", ".join(known)} (see --list-tags)') + requested.add(tag) + if not requested: + raise ValueError(f'empty ABI tag list {value!r}; the namespace without tags is always generated') + return tuple(tag for tag in known if tag in requested) + + if __name__ == '__main__': - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser( + description='Generate nlohmann_json.natvis. By default, it covers the namespaces with up to two ABI tags; ' + 'use --abi-tags or --all for other configurations.') parser.add_argument('--version', type=semver, help='Library version number (default: parsed from ' - 'include/nlohmann/detail/abi_macros.hpp below "output")') - parser.add_argument('output', nargs='?', default=REPO_ROOT, - help='Repository root: where include/nlohmann/detail/abi_macros.hpp is ' - 'read from and where nlohmann_json.natvis is written ' + 'include/nlohmann/detail/abi_macros.hpp below "repo_root")') + selection = parser.add_mutually_exclusive_group() + selection.add_argument('--abi-tags', action='append', default=[], metavar='TAGS', + help='Comma-separated list of ABI tags (e.g. "_diag,_dp,_vns", leading underscores are ' + 'optional) of one configuration to add to the default namespaces; can be repeated') + selection.add_argument('--all', action='store_true', + help='Generate the namespaces for every combination of ABI tags; the file size doubles ' + 'with each ABI tag (about 60 MB for 12 tags)') + parser.add_argument('--list-tags', action='store_true', + help='List the ABI tags with the macros that enable them, and exit') + parser.add_argument('--output', metavar='FILE', + help='Where to write the Natvis file (default: nlohmann_json.natvis below "repo_root")') + parser.add_argument('repo_root', nargs='?', default=REPO_ROOT, + help='Repository root: where include/nlohmann/detail/abi_macros.hpp is read from ' '(default: the repository root this script lives in)') args = parser.parse_args() - derived_tags, derived_version = abi_info(args.output) + abi_tags, derived_version = abi_info(args.repo_root) + + if args.list_tags: + width = max(len(suffix) for suffix, _, _ in abi_tags) + option_width = max(len(option) for _, option, _ in abi_tags) + for suffix, option, ident in abi_tags: + print(f'{suffix:<{width}} {option:<{option_width}} {ident}') + sys.exit(0) + + try: + extra_combinations = [parse_tag_list(value, abi_tags) for value in args.abi_tags] + except ValueError as e: + parser.error(str(e)) namespaces = ['nlohmann'] abi_prefix = 'json_abi' - abi_tags = derived_tags + suffixes = [suffix for suffix, _, _ in abi_tags] version = '_v' + (args.version or derived_version).replace('.', '_') - inline_namespaces = [] - # generate all combinations of inline namespace names - for n in range(0, len(abi_tags) + 1): - for tags in itertools.combinations(abi_tags, n): - ns = abi_prefix + ''.join(tags) - inline_namespaces += [ns, ns + version] + # by default, generate the namespace names for up to two ABI tags, plus + # the combinations requested with --abi-tags + max_tags = len(suffixes) if args.all else min(len(suffixes), 2) + combinations = [tags for n in range(0, max_tags + 1) for tags in itertools.combinations(suffixes, n)] + for tags in extra_combinations: + if tags not in combinations: + combinations.append(tags) + + inline_namespaces = [] + for tags in combinations: + ns = abi_prefix + ''.join(tags) + inline_namespaces += [ns, ns + version] namespaces += [f'{namespaces[0]}::{ns}' for ns in inline_namespaces] + # The template renders a local XML file, not a web response, and autoescape is enabled. + # nosemgrep: python.flask.security.xss.audit.direct-use-of-jinja2.direct-use-of-jinja2 env = jinja2.Environment(loader=jinja2.FileSystemLoader(searchpath=sys.path[0]), autoescape=True, trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True) template = env.get_template('nlohmann_json.natvis.j2') + # nosemgrep: python.flask.security.xss.audit.direct-use-of-jinja2.direct-use-of-jinja2 natvis = template.render(namespaces=namespaces) - with open(os.path.join(args.output, 'nlohmann_json.natvis'), 'w') as f: + output = args.output or os.path.join(args.repo_root, 'nlohmann_json.natvis') + with open(output, 'w') as f: f.write(natvis) + + if args.all: + print(f'wrote {len(namespaces)} namespaces ({os.path.getsize(output) / 1e6:.1f} MB) to {output}', + file=sys.stderr)