mirror of
https://github.com/nlohmann/json.git
synced 2026-10-31 01:45:43 +00:00
Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6ed821580 | ||
|
|
755bb16d7a | ||
|
|
12d2ad6da5 | ||
|
|
d20c4dcd89 | ||
|
|
ea03d601af | ||
|
|
e2cb15c479 | ||
|
|
c451fac1bc | ||
|
|
28f71914b5 | ||
|
|
0b91f9ddd8 | ||
|
|
0115bb47b3 | ||
|
|
4cf1946cba | ||
|
|
1aff0483d7 | ||
|
|
987a94db31 | ||
|
|
14f750834f | ||
|
|
9fad526888 | ||
|
|
34dbfa95eb | ||
|
|
072dbcedf8 | ||
|
|
2b734cfb1f | ||
|
|
ff1025c4e4 | ||
|
|
51abba08fd | ||
|
|
e08c181dee | ||
|
|
c227d4a920 | ||
|
|
bdcfc178af | ||
|
|
30e1adef93 | ||
|
|
afea570755 | ||
|
|
e7d245db7e | ||
|
|
d38a04bd3e |
No files matched your search
@@ -28,7 +28,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -38,14 +38,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
with:
|
||||
languages: c-cpp
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
uses: github/codeql-action/autobuild@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -48,6 +48,6 @@ jobs:
|
||||
output: 'flawfinder_results.sarif'
|
||||
|
||||
- name: Upload analysis results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
with:
|
||||
sarif_file: ${{github.workspace}}/flawfinder_results.sarif
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -80,6 +80,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
|
||||
# Upload SARIF file generated in previous step
|
||||
- name: Upload SARIF file
|
||||
uses: github/codeql-action/upload-sarif@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
with:
|
||||
sarif_file: semgrep.sarif
|
||||
if: always()
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
target: [ci_test_single_header, ci_cppcheck, ci_cpplint, ci_reproducible_tests, ci_non_git_tests, ci_offline_testdata, ci_reuse_compliance, ci_test_valgrind]
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -178,7 +178,7 @@ jobs:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -433,7 +433,7 @@ jobs:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -456,7 +456,7 @@ jobs:
|
||||
target: [ci_test_examples, ci_test_build_documentation, ci_test_documentation_mermaid]
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up MinGW
|
||||
uses: egor-tensin/setup-mingw@41b837e47d7f85214629d255b9c4bc3fcbe9fd63 # v3.0
|
||||
uses: egor-tensin/setup-mingw@d54f5ef0ce585bd3fbb1f8a71b5b254f59dbf931 # v3.0
|
||||
with:
|
||||
platform: ${{ matrix.architecture }}
|
||||
version: 12.2.0 # https://github.com/egor-tensin/setup-mingw/issues/14
|
||||
@@ -153,7 +153,7 @@ jobs:
|
||||
- name: Install Clang
|
||||
run: curl -fsSL -o LLVM${{ matrix.version }}.exe https://github.com/llvm/llvm-project/releases/download/llvmorg-${{ matrix.version }}/LLVM-${{ matrix.version }}-win64.exe ; 7z x LLVM${{ matrix.version }}.exe -y -o"C:/Program Files/LLVM"
|
||||
- name: Set up MinGW
|
||||
uses: egor-tensin/setup-mingw@41b837e47d7f85214629d255b9c4bc3fcbe9fd63 # v3.0
|
||||
uses: egor-tensin/setup-mingw@d54f5ef0ce585bd3fbb1f8a71b5b254f59dbf931 # v3.0
|
||||
with:
|
||||
platform: x64
|
||||
version: 12.2.0 # https://github.com/egor-tensin/setup-mingw/issues/14
|
||||
|
||||
@@ -1408,9 +1408,9 @@ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR I
|
||||
- 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 parts of [Google Abseil](https://github.com/abseil/abseil-cpp) which is licensed under the [Apache 2.0 License](https://opensource.org/licenses/Apache-2.0).
|
||||
- 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 class contains an adapted version of the Eisel-Lemire algorithm, its table of powers of five, its digit comparison for long numbers, and its parsing of eight digits at a time 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 (`<nlohmann/json_view.hpp>`) 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 (`<nlohmann/json_view.hpp>`) 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
|
||||
- The class's lexer and the view's parser (`<nlohmann/json_view.hpp>`) validate 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
|
||||
|
||||
<img align="right" src="https://git.fsfe.org/reuse/reuse-ci/raw/branch/master/reuse-horizontal.png" alt="REUSE Software">
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ The class contains a port of the shortest double-to-decimal conversion of [Żmij
|
||||
|
||||
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, 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 class contains an adapted version of the Eisel-Lemire algorithm, its table of powers of five, its digit comparison for long numbers, and its parsing of eight digits at a time 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 (`<nlohmann/json_view.hpp>`) 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 (`<nlohmann/json_view.hpp>`) 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
|
||||
The class's lexer and the view's parser (`<nlohmann/json_view.hpp>`) validate 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
|
||||
@@ -19,6 +19,11 @@
|
||||
#include <type_traits> // conditional
|
||||
|
||||
|
||||
// The shortest-digit conversion of doubles (see zmij.hpp) and the writing of
|
||||
// its digits, including the conversion of the digits to text by Xiang JunBo
|
||||
// and the SIMD instruction sequence by Dougall Johnson, are ported from Zmij
|
||||
// by Victor Zverovich (https://github.com/vitaut/zmij, MIT license).
|
||||
//
|
||||
// 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)))
|
||||
|
||||
@@ -44,6 +44,13 @@
|
||||
// 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.
|
||||
//
|
||||
// The Eisel-Lemire conversion (eisel_lemire()), the digit comparison for the
|
||||
// cases it cannot decide, and the eight-digits-at-a-time helpers
|
||||
// (is_eight_digits(), parse_eight_digits()) are adapted from fast_float
|
||||
// (https://github.com/fastfloat/fast_float, Daniel Lemire and contributors,
|
||||
// available under the MIT, Apache 2.0, and Boost Software licenses; used here
|
||||
// under the MIT license). The table of powers of five is in pow5_table.hpp.
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
namespace detail
|
||||
|
||||
@@ -30,6 +30,13 @@
|
||||
// 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.
|
||||
//
|
||||
// The vector UTF-8 check (utf8_lookup4) is adapted from simdjson
|
||||
// (https://github.com/simdjson/simdjson, Daniel Lemire, Geoff Langdale, John
|
||||
// Keiser, and contributors, available under the Apache 2.0 and MIT licenses;
|
||||
// used here under the MIT license): the "lookup4" algorithm and tables of
|
||||
// J. Keiser and D. Lemire, "Validating UTF-8 In Less Than One Instruction Per
|
||||
// Byte" (2021).
|
||||
#if !defined(JSON_NO_SIMD) && defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) && defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
#include <arm_neon.h>
|
||||
#define JSON_SCAN_NEON 1
|
||||
|
||||
@@ -1977,23 +1977,6 @@ class serializer
|
||||
return;
|
||||
}
|
||||
|
||||
// 17 to 20 digits: the first one to four, then twice eight
|
||||
if (magnitude_fits_64_bits(abs_value, std::integral_constant<bool, magnitude_has_at_most_64_bits>()))
|
||||
{
|
||||
const auto value = static_cast<std::uint64_t>(abs_value);
|
||||
const std::uint64_t top = value / 10000000000000000u; // 1 to 1844
|
||||
const std::uint64_t low16 = value - (top * 10000000000000000u);
|
||||
const std::uint64_t middle = low16 / 100000000u;
|
||||
const std::uint64_t first = dtoa_impl::eight_digit_bytes(top);
|
||||
const auto leading = static_cast<unsigned>(count_leading_zeros(first) / 8); // (first is not 0)
|
||||
char* const p = buffer_ptr + n_chars;
|
||||
dtoa_impl::store_msb_first(p, (first << (8 * leading)) + 0x3030303030303030u);
|
||||
dtoa_impl::store_msb_first(p + 8 - leading, dtoa_impl::eight_digit_bytes(middle) + 0x3030303030303030u);
|
||||
dtoa_impl::store_msb_first(p + 16 - leading, dtoa_impl::eight_digit_bytes(low16 - (middle * 100000000u)) + 0x3030303030303030u);
|
||||
write_buffer_pos += n_chars + 24 - leading;
|
||||
return;
|
||||
}
|
||||
|
||||
n_chars += count_digits(abs_value);
|
||||
|
||||
// spare 1 byte for '\0'
|
||||
@@ -2139,23 +2122,6 @@ class serializer
|
||||
}
|
||||
}
|
||||
|
||||
/// whether every number_magnitude_t fits 64 bits
|
||||
static constexpr bool magnitude_has_at_most_64_bits = std::numeric_limits<number_magnitude_t>::is_specialized
|
||||
&& std::numeric_limits<number_magnitude_t>::digits <= 64;
|
||||
|
||||
/// whether @a x fits 64 bits: always if number_magnitude_t has at most
|
||||
/// 64 bits (selected by type, as the comparison would be reported as
|
||||
/// always true), else compared
|
||||
static bool magnitude_fits_64_bits(const number_magnitude_t& /*unused*/, std::true_type /*unused*/) noexcept
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool magnitude_fits_64_bits(const number_magnitude_t& x, std::false_type /*unused*/) noexcept
|
||||
{
|
||||
return x <= 18446744073709551615u;
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper function for dump_integer
|
||||
*
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -26,6 +26,7 @@
|
||||
#include <nlohmann/detail/view/lookup.hpp>
|
||||
#include <nlohmann/detail/view/macro_scope.hpp>
|
||||
#include <nlohmann/detail/view/node.hpp>
|
||||
#include <nlohmann/detail/view/scan.hpp>
|
||||
#include <nlohmann/detail/view/view_mode.hpp>
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
@@ -46,10 +47,9 @@ struct first_true<false, Conditions...> : std::integral_constant < int, 1 + firs
|
||||
/// (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)
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE void check_utf8_bytewise(const unsigned char* p, std::size_t n, std::size_t i)
|
||||
{
|
||||
const auto* const p = reinterpret_cast<const unsigned char*>(s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
for (std::size_t i = 0; i < n;)
|
||||
while (i < n)
|
||||
{
|
||||
const unsigned char c = p[i];
|
||||
if (c < 0x80)
|
||||
@@ -79,6 +79,43 @@ inline void check_utf8(const char* s, std::size_t n)
|
||||
}
|
||||
}
|
||||
|
||||
/// The long-string path of check_utf8(): skips well-formed runs with the
|
||||
/// parser's string scan (which stops at a quote, a backslash, or a control
|
||||
/// character, all legal here, and at ill-formed UTF-8); the byte-wise check
|
||||
/// runs only from where the scan found ill-formed UTF-8, to report it.
|
||||
NLOHMANN_VIEW_NOINLINE inline void check_utf8_scan(const unsigned char* p, std::size_t n)
|
||||
{
|
||||
const unsigned char* const e = p + n;
|
||||
const unsigned char* q = p;
|
||||
for (;;)
|
||||
{
|
||||
q = scan_string_run<true>(q, e);
|
||||
if (q == e)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (*q >= 0x80)
|
||||
{
|
||||
check_utf8_bytewise(p, n, static_cast<std::size_t>(q - p));
|
||||
return; // not reached: the scan stopped at ill-formed UTF-8
|
||||
}
|
||||
++q; // a quote, backslash, or control character
|
||||
}
|
||||
}
|
||||
|
||||
inline void check_utf8(const char* s, std::size_t n)
|
||||
{
|
||||
const auto* const p = reinterpret_cast<const unsigned char*>(s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
if (NLOHMANN_VIEW_UNLIKELY(n >= 32))
|
||||
{
|
||||
check_utf8_scan(p, n);
|
||||
return;
|
||||
}
|
||||
// short strings: the vector scan costs more than it saves; (the loop is
|
||||
// inlined with a constant start, so this path is the plain byte-wise check)
|
||||
check_utf8_bytewise(p, n, 0);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief the edits of an editable basic_json_document
|
||||
|
||||
|
||||
@@ -258,6 +258,81 @@ struct number_token
|
||||
bool is_float; ///< a fraction or an exponent
|
||||
};
|
||||
|
||||
/// advance over ASCII digits, eight bytes at a time
|
||||
inline const unsigned char* digit_run(const unsigned char* p, const unsigned char* e) noexcept
|
||||
{
|
||||
constexpr std::uint64_t ones = 0x0101010101010101ull;
|
||||
while (e - p >= 8)
|
||||
{
|
||||
// (the digits become 0..9; a byte above 9, or with bit 7 set, is no digit. A carry
|
||||
// out of a byte with bit 7 set only affects the bytes above, after the first one
|
||||
// that count_trailing_zeros finds.)
|
||||
const std::uint64_t x = read_eight_bytes(p) ^ (ones * 0x30u);
|
||||
const std::uint64_t not_digit = ((x + (ones * 0x76u)) | x) & (ones * 0x80u);
|
||||
if (not_digit != 0)
|
||||
{
|
||||
return p + (static_cast<std::size_t>(count_trailing_zeros(not_digit)) / 8);
|
||||
}
|
||||
p += 8;
|
||||
}
|
||||
while (p != e && is_digit(*p))
|
||||
{
|
||||
++p;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/// whether the n bytes at p are ASCII digits (n > 0)
|
||||
inline bool all_digits(const unsigned char* p, std::size_t n) noexcept
|
||||
{
|
||||
const auto digits = [](const unsigned char* q)
|
||||
{
|
||||
constexpr std::uint64_t ones = 0x0101010101010101ull;
|
||||
const std::uint64_t x = read_eight_bytes(q) ^ (ones * 0x30u);
|
||||
return (((x + (ones * 0x76u)) | x) & (ones * 0x80u)) == 0;
|
||||
};
|
||||
if (n >= 8)
|
||||
{
|
||||
for (std::size_t i = 0; i + 8 < n; i += 8)
|
||||
{
|
||||
if (!digits(p + i))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return digits(p + n - 8); // (overlapping the bytes before: they are digits)
|
||||
}
|
||||
for (std::size_t i = 0; i < n; ++i)
|
||||
{
|
||||
if (!is_digit(p[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Whether a float token has the shape that the parser records for most of
|
||||
/// them: [-]digits.digits, with the numbers of digits of the node's layout
|
||||
/// (extra). Then the token is a number of that layout, and the general scan
|
||||
/// that follows is not needed.
|
||||
inline bool plain_float_token(const unsigned char* s, std::size_t len, std::uint16_t extra) noexcept
|
||||
{
|
||||
const std::size_t int_digits = extra & 0xFFu;
|
||||
const std::size_t frac_digits = extra >> 8u;
|
||||
if (int_digits == 0 || int_digits >= 255 || frac_digits == 0 || frac_digits >= 255 || len == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const std::size_t negative = *s == '-' ? 1 : 0;
|
||||
if (len != negative + int_digits + 1 + frac_digits)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const unsigned char* const p = s + negative;
|
||||
return !(int_digits > 1 && *p == '0') && p[int_digits] == '.' && all_digits(p, int_digits) && all_digits(p + int_digits + 1, frac_digits);
|
||||
}
|
||||
|
||||
/// 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)
|
||||
{
|
||||
@@ -276,11 +351,7 @@ inline bool scan_number_token(const unsigned char* s, std::size_t len, number_to
|
||||
}
|
||||
else if (*p >= '1' && *p <= '9')
|
||||
{
|
||||
do
|
||||
{
|
||||
++p;
|
||||
}
|
||||
while (p != e && is_digit(*p));
|
||||
p = digit_run(p + 1, e);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -292,10 +363,7 @@ inline bool scan_number_token(const unsigned char* s, std::size_t len, number_to
|
||||
if (p != e && *p == '.')
|
||||
{
|
||||
const unsigned char* const f0 = ++p;
|
||||
while (p != e && is_digit(*p))
|
||||
{
|
||||
++p;
|
||||
}
|
||||
p = digit_run(p, e);
|
||||
if (p == f0)
|
||||
{
|
||||
return false;
|
||||
@@ -406,10 +474,15 @@ struct float_range
|
||||
template<typename Check>
|
||||
bool check_distinct_ranges(std::vector<byte_range>& ranges, Check check)
|
||||
{
|
||||
std::sort(ranges.begin(), ranges.end(), [](const byte_range & a, const byte_range & b)
|
||||
// (save() writes the ranges in the order of the nodes, which is sorted already)
|
||||
const auto before = [](const byte_range & a, const byte_range & b)
|
||||
{
|
||||
return a.off != b.off ? a.off < b.off : a.len < b.len;
|
||||
});
|
||||
};
|
||||
if (!std::is_sorted(ranges.begin(), ranges.end(), before))
|
||||
{
|
||||
std::sort(ranges.begin(), ranges.end(), before);
|
||||
}
|
||||
std::size_t end = 0;
|
||||
for (std::size_t i = 0; i < ranges.size(); ++i)
|
||||
{
|
||||
@@ -431,7 +504,8 @@ bool check_distinct_ranges(std::vector<byte_range>& ranges, Check check)
|
||||
/// record layouts that match it), tokens must not overlap.
|
||||
inline bool check_float_ranges(std::vector<float_range>& ranges, const unsigned char* text)
|
||||
{
|
||||
std::sort(ranges.begin(), ranges.end(), [](const float_range & a, const float_range & b)
|
||||
// (save() writes the tokens in the order of the nodes, which is sorted already)
|
||||
const auto before = [](const float_range & a, const float_range & b)
|
||||
{
|
||||
if (a.off != b.off)
|
||||
{
|
||||
@@ -442,7 +516,11 @@ inline bool check_float_ranges(std::vector<float_range>& ranges, const unsigned
|
||||
return a.len < b.len;
|
||||
}
|
||||
return a.extra < b.extra;
|
||||
});
|
||||
};
|
||||
if (!std::is_sorted(ranges.begin(), ranges.end(), before))
|
||||
{
|
||||
std::sort(ranges.begin(), ranges.end(), before);
|
||||
}
|
||||
std::size_t end = 0;
|
||||
std::size_t i = 0;
|
||||
while (i < ranges.size())
|
||||
@@ -452,14 +530,18 @@ inline bool check_float_ranges(std::vector<float_range>& ranges, const unsigned
|
||||
{
|
||||
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))
|
||||
std::uint16_t layout = r.extra;
|
||||
if (!plain_float_token(s, r.len, r.extra))
|
||||
{
|
||||
return false;
|
||||
number_token t{};
|
||||
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)
|
||||
layout = float_layout(t);
|
||||
}
|
||||
// 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)
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <nlohmann/detail/view/macro_scope.hpp>
|
||||
#include <nlohmann/detail/view/node.hpp>
|
||||
#include <nlohmann/detail/view/scan.hpp>
|
||||
#include <nlohmann/detail/view/simd.hpp>
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
namespace detail
|
||||
@@ -103,12 +104,56 @@ NLOHMANN_VIEW_NOINLINE FloatType float_value(const char* first, const node& n)
|
||||
return convert_float<FloatType>(first, last, dot, mantissa_end);
|
||||
}
|
||||
|
||||
#if NLOHMANN_VIEW_VECTOR
|
||||
/*!
|
||||
@brief the value of the first 16 digits of a run of digits with a point after
|
||||
int_digits of them (or none, if int_digits >= 16)
|
||||
|
||||
The 17 bytes at p are read at once: the bytes after the point move down by one,
|
||||
then pairs, quadruples, and octets of digits are combined by multiplications
|
||||
(after simdjson). The bytes [p, p + 32) must be readable. Bytes that are not
|
||||
digits (only in a damaged image loaded with image_check::bounds) give a wrong
|
||||
value, but no undefined behavior.
|
||||
*/
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t sixteen_digits(const unsigned char* p, unsigned int_digits) noexcept
|
||||
{
|
||||
#if NLOHMANN_VIEW_NEON
|
||||
const uint8x16_t low = vld1q_u8(p);
|
||||
const uint8x16_t index = vcombine_u8(vcreate_u8(0x0706050403020100u), vcreate_u8(0x0F0E0D0C0B0A0908u));
|
||||
const uint8x16_t before_point = vcltq_u8(index, vdupq_n_u8(static_cast<std::uint8_t>(int_digits)));
|
||||
const uint8x16_t digits = vsubq_u8(vbslq_u8(before_point, low, vextq_u8(low, vld1q_u8(p + 16), 1)), vdupq_n_u8('0'));
|
||||
const uint16x8_t twos = vpaddlq_u8(vmulq_u8(digits, vreinterpretq_u8_u16(vdupq_n_u16(0x010A))));
|
||||
const uint32x4_t fours = vpaddlq_u16(vmulq_u16(twos, vreinterpretq_u16_u32(vdupq_n_u32(0x00010064))));
|
||||
const uint64x2_t eights = vpaddlq_u32(vmulq_u32(fours, vreinterpretq_u32_u64(vdupq_n_u64(0x0000000100002710u))));
|
||||
return (vgetq_lane_u64(eights, 0) * 100000000u) + vgetq_lane_u64(eights, 1);
|
||||
#else
|
||||
// NOLINTBEGIN(portability-simd-intrinsics)
|
||||
const __m128i low = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(p))); // NOLINT(bugprone-casting-through-void)
|
||||
const __m128i high = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(p + 16))); // NOLINT(bugprone-casting-through-void)
|
||||
const __m128i before_point = _mm_cmpgt_epi8(_mm_set1_epi8(static_cast<char>(int_digits)), _mm_setr_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15));
|
||||
const __m128i moved = _mm_or_si128(_mm_srli_si128(low, 1), _mm_slli_si128(high, 15));
|
||||
const __m128i digits = _mm_sub_epi8(_mm_or_si128(_mm_and_si128(before_point, low), _mm_andnot_si128(before_point, moved)), _mm_set1_epi8('0'));
|
||||
// (16-bit lanes: SSE2 has no multiply-add of bytes)
|
||||
const __m128i tens = _mm_setr_epi16(10, 1, 10, 1, 10, 1, 10, 1);
|
||||
const __m128i twos = _mm_packs_epi32(_mm_madd_epi16(_mm_unpacklo_epi8(digits, _mm_setzero_si128()), tens),
|
||||
_mm_madd_epi16(_mm_unpackhi_epi8(digits, _mm_setzero_si128()), tens));
|
||||
const __m128i fours = _mm_madd_epi16(twos, _mm_setr_epi16(100, 1, 100, 1, 100, 1, 100, 1));
|
||||
const __m128i eights = _mm_madd_epi16(_mm_packs_epi32(fours, fours), _mm_setr_epi16(10000, 1, 10000, 1, 10000, 1, 10000, 1));
|
||||
const auto upper = static_cast<std::uint32_t>(_mm_cvtsi128_si32(eights));
|
||||
const auto lower = static_cast<std::uint32_t>(_mm_cvtsi128_si32(_mm_srli_si128(eights, 4)));
|
||||
// NOLINTEND(portability-simd-intrinsics)
|
||||
return (std::uint64_t{upper} * 100000000u) + lower;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
/*!
|
||||
@brief the digits of a float token with at most 19 digits, from its layout
|
||||
|
||||
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.
|
||||
time without scanning (with SIMD, a token of 16 to 19 digits sixteen at a
|
||||
time).
|
||||
|
||||
@param[in] p first character of the token
|
||||
@param[in] e end of the token
|
||||
@@ -118,15 +163,34 @@ NLOHMANN_VIEW_ALWAYS_INLINE float_significand layout_decimal(const unsigned char
|
||||
{
|
||||
const bool negative = *p == '-';
|
||||
p += negative ? 1 : 0;
|
||||
std::uint64_t w = parse_upto19(p, int_digits, limit);
|
||||
p += int_digits;
|
||||
std::uint64_t w = 0;
|
||||
std::int64_t q = 0;
|
||||
if (frac_digits != 0)
|
||||
#if NLOHMANN_VIEW_VECTOR
|
||||
const unsigned count = int_digits + frac_digits;
|
||||
if (count >= 16 && limit - p >= 32)
|
||||
{
|
||||
w = (w * int_pow10(frac_digits)) + parse_upto19(p + 1, frac_digits, limit);
|
||||
p += 1 + frac_digits;
|
||||
// (the point, if any, follows the integer digits; the digits after
|
||||
// the first 16 are at most three)
|
||||
w = sixteen_digits(p, int_digits);
|
||||
for (unsigned i = 16; i < count; ++i)
|
||||
{
|
||||
w = (w * 10) + static_cast<std::uint64_t>(p[i + (i >= int_digits ? 1 : 0)] - '0');
|
||||
}
|
||||
p += count + (frac_digits != 0 ? 1 : 0);
|
||||
q = -static_cast<std::int64_t>(frac_digits);
|
||||
}
|
||||
else /* NOLINT(readability/braces) */
|
||||
#endif
|
||||
{
|
||||
w = parse_upto19(p, int_digits, limit);
|
||||
p += int_digits;
|
||||
if (frac_digits != 0)
|
||||
{
|
||||
w = (w * int_pow10(frac_digits)) + parse_upto19(p + 1, frac_digits, limit);
|
||||
p += 1 + frac_digits;
|
||||
q = -static_cast<std::int64_t>(frac_digits);
|
||||
}
|
||||
}
|
||||
if (p != e)
|
||||
{
|
||||
// [eE][+-]digits; huge exponents saturate (the parser rejected
|
||||
@@ -161,12 +225,22 @@ NLOHMANN_VIEW_ALWAYS_INLINE float_significand layout_decimal(const unsigned char
|
||||
The result is correctly rounded by the lexer's conversion
|
||||
(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.
|
||||
digits), so it is the value parse() produces. Tokens of 16 or more digits go
|
||||
to Eisel-Lemire directly: the fast path takes few of them (none of 17 digits),
|
||||
and its check is a branch that data like GeoJSON coordinates mispredicts.
|
||||
*/
|
||||
template<typename FloatType>
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE FloatType layout_float(const unsigned char* p, const unsigned char* e, unsigned int_digits, unsigned frac_digits, const unsigned char* limit) noexcept
|
||||
{
|
||||
return decimal_to_float<FloatType>(layout_decimal(p, e, int_digits, frac_digits, limit));
|
||||
const float_significand d = layout_decimal(p, e, int_digits, frac_digits, limit);
|
||||
if (int_digits + frac_digits >= 16)
|
||||
{
|
||||
using format = ieee_binary_format<std::numeric_limits<FloatType>::digits>;
|
||||
native_float_t<FloatType> value{};
|
||||
float_from_bits(eisel_lemire<format>(d.exponent, d.w) | (d.negative ? (std::uint64_t{1} << format::sign_bit()) : 0u), value);
|
||||
return value;
|
||||
}
|
||||
return decimal_to_float<FloatType>(d);
|
||||
}
|
||||
|
||||
/// the value of a float set by an edit: its token (the shortest round-trip
|
||||
@@ -185,6 +259,80 @@ NLOHMANN_VIEW_NOINLINE FloatType edited_float(const char* token, const node& n)
|
||||
return float_value<FloatType>(token, n);
|
||||
}
|
||||
|
||||
#if NLOHMANN_VIEW_VECTOR
|
||||
/// eisel_lemire() for the token w * 10^q of coordinate_float()
|
||||
template<typename Format>
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t coordinate_bits(std::int64_t q, std::uint64_t w, Format /*format*/) noexcept
|
||||
{
|
||||
return eisel_lemire<Format>(q, w);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief (binary64) eisel_lemire() without the cases that tokens of 16 or 17
|
||||
digits with 5 or more after the point cannot reach: a value of 0, a
|
||||
subnormal, an infinity, or a tie (q < -4)
|
||||
*/
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t coordinate_bits(std::int64_t q, std::uint64_t w, ieee_binary_format<53> format) noexcept
|
||||
{
|
||||
if (NLOHMANN_VIEW_UNLIKELY(w == 0 || q > -5))
|
||||
{
|
||||
return eisel_lemire<ieee_binary_format<53>>(q, w);
|
||||
}
|
||||
static_cast<void>(format);
|
||||
const int lz = count_leading_zeros(w);
|
||||
const std::uint64_t normalized = w << static_cast<unsigned>(lz);
|
||||
const auto index = static_cast<std::size_t>(2 * (q - pow5_128_smallest_power));
|
||||
const uint128_parts product = full_multiplication(normalized, pow5_128()[index]);
|
||||
std::uint64_t high = product.high;
|
||||
if (NLOHMANN_VIEW_UNLIKELY((high & 0x1FFu) == 0x1FFu))
|
||||
{
|
||||
// (the lower bits may carry into the result)
|
||||
const std::uint64_t second = full_multiplication(normalized, pow5_128()[index + 1]).high;
|
||||
high += product.low + second < product.low ? 1u : 0u;
|
||||
}
|
||||
const auto upperbit = static_cast<unsigned>(high >> 63u);
|
||||
std::uint64_t mantissa = high >> (upperbit + 9);
|
||||
auto power2 = static_cast<std::uint64_t>(((217706 * q) >> 16) + 63 + static_cast<std::int64_t>(upperbit) - lz + 1023);
|
||||
mantissa = (mantissa + (mantissa & 1u)) >> 1u;
|
||||
const bool carry = mantissa >= (std::uint64_t{2} << 52u);
|
||||
mantissa = carry ? (std::uint64_t{1} << 52u) : mantissa;
|
||||
power2 += carry ? 1u : 0u;
|
||||
return (mantissa & ~(std::uint64_t{1} << 52u)) | (power2 << 52u);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief (binary32 and binary64) the value of a float token of the source with
|
||||
16 or 17 digits, a point, and no exponent, the shape of most coordinates
|
||||
written with full precision; whether the token of n has this shape
|
||||
|
||||
These tokens skip the general layout of layout_decimal(): no digits beyond
|
||||
the 17th, no exponent (and Eisel-Lemire directly, as in layout_float()).
|
||||
*/
|
||||
template<typename FloatType>
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE bool coordinate_float(const document_data& d, const node& n, FloatType& value) noexcept
|
||||
{
|
||||
const unsigned int_digits = n.extra & 0xFFu;
|
||||
const unsigned frac_digits = n.extra >> 8u;
|
||||
const unsigned count = int_digits + frac_digits;
|
||||
const auto* const first = reinterpret_cast<const unsigned char*>(d.src + n.off); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
const unsigned negative = first[0] == '-' ? 1u : 0u;
|
||||
if (count - 16u > 1u || frac_digits == 0 || n.len != negative + count + 1 || d.size - n.off < 33)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const unsigned char* const digits = first + negative;
|
||||
const std::uint64_t w = sixteen_digits(digits, int_digits);
|
||||
// (the 17th digit follows the point)
|
||||
const std::uint64_t w17 = (w * 10) + static_cast<std::uint64_t>(digits[17] - '0');
|
||||
using format = ieee_binary_format<std::numeric_limits<FloatType>::digits>;
|
||||
native_float_t<FloatType> result{};
|
||||
float_from_bits(coordinate_bits(-static_cast<std::int64_t>(frac_digits), count == 17 ? w17 : w, format{}) | (static_cast<std::uint64_t>(negative) << format::sign_bit()), result);
|
||||
value = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/// the value of the float token of a node, as parse() converts it; floats and
|
||||
/// doubles with at most 19 digits are converted from the digit layout
|
||||
template<typename FloatType>
|
||||
@@ -200,6 +348,13 @@ FloatType float_value(const document_data& d, const node& n)
|
||||
template<typename FloatType>
|
||||
FloatType float_value(const document_data& d, const node& n, std::true_type /*binary32 or binary64*/)
|
||||
{
|
||||
#if NLOHMANN_VIEW_VECTOR
|
||||
FloatType value {};
|
||||
if (coordinate_float(d, n, value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
#endif
|
||||
const unsigned int_digits = n.extra & 0xFFu;
|
||||
const unsigned frac_digits = n.extra >> 8u;
|
||||
if (NLOHMANN_VIEW_LIKELY(int_digits + frac_digits <= 19)) // (255 marks "many")
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
#include <cstddef> // size_t
|
||||
#include <cstdint> // uint8_t, uint32_t
|
||||
#include <cstring> // memcpy, memset
|
||||
#include <functional> // less
|
||||
#include <limits> // numeric_limits
|
||||
#include <memory> // unique_ptr
|
||||
#include <type_traits> // integral_constant
|
||||
#include <vector> // vector
|
||||
|
||||
@@ -384,6 +386,143 @@ struct depth_counter<false>
|
||||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
@brief where the double x nearest to a token w * 10^q lies, for 10^15 <= w <
|
||||
10^17 and -17 <= q <= -1 (Eisel-Lemire, then a fixed-point position)
|
||||
|
||||
dump() writes the shortest decimal in the rounding interval of x (the closest
|
||||
one if there are several, see zmij::to_shortest()). Its digits are those of a
|
||||
multiple of 10^g of the token's last digit, with g = dec_exp + 1 - q for x's
|
||||
decimal exponent dec_exp: the multiple of 10^g nearest to x if it lies in the
|
||||
interval (at most one does), else the multiple of 10^(g - 1) nearest to x
|
||||
(see token_digits_1() and token_digits_2()). For g <= 0, the token itself is
|
||||
in the interval.
|
||||
|
||||
The position is exact in a 64-bit fixed-point number with 56 bits after the
|
||||
point (of the token's last digit): x / 10^q = 2c * 5^-q / 2^t for x = c * 2^e2
|
||||
and t = q + 1 - e2, with at most 56 bits after the point here, and |x / 10^q -
|
||||
w| is at most half the spacing of the doubles, below 12 units. So only the low
|
||||
64 bits of 2c * 5^-q are needed. c and e2 come from Eisel-Lemire's product
|
||||
directly (as eisel_lemire() rounds it: no tie is possible for q < -4, and its
|
||||
second product is added where it can carry into the result).
|
||||
|
||||
@param[out] offset x / 10^q - w + 32, with 56 bits after the point
|
||||
@param[out] radius half the spacing of the doubles at x, likewise (+ 1 if
|
||||
the interval is closed)
|
||||
@return g; 3 if the digits are not decided here (a power of two, whose
|
||||
interval is not symmetric, or out of range)
|
||||
*/
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE int locate_token(std::uint64_t w, int q, std::uint64_t& offset, std::uint64_t& radius) noexcept
|
||||
{
|
||||
const int lz = count_leading_zeros(w);
|
||||
const std::uint64_t normalized = w << static_cast<unsigned>(lz);
|
||||
const auto index = static_cast<std::size_t>(2 * (q - pow5_128_smallest_power));
|
||||
const uint128_parts product = full_multiplication(normalized, pow5_128()[index]);
|
||||
std::uint64_t high = product.high;
|
||||
if (NLOHMANN_VIEW_UNLIKELY((high & 0x1FFu) == 0x1FFu))
|
||||
{
|
||||
// (as eisel_lemire(): the lower bits may carry into the result)
|
||||
const std::uint64_t second = full_multiplication(normalized, pow5_128()[index + 1]).high;
|
||||
high += product.low + second < product.low ? 1u : 0u;
|
||||
}
|
||||
const auto upperbit = static_cast<unsigned>(high >> 63u);
|
||||
const std::uint64_t mantissa = high >> (upperbit + 9);
|
||||
const std::uint64_t c = (mantissa + (mantissa & 1u)) >> 1u;
|
||||
// e2 as eisel_lemire() computes the exponent (log2(10) ~ 217706 / 2^16)
|
||||
const int e2 = static_cast<int>((217706 * static_cast<std::int64_t>(q)) >> 16) + 63 + static_cast<int>(upperbit) - lz - 52;
|
||||
const int t = q + 1 - e2;
|
||||
const int g = ((e2 * 315653) >> 20) + 1 - q; // (dec_exp as in zmij::to_shortest(), for a regular x)
|
||||
static const std::array<std::uint64_t, 17> fives = {{1u, 5u, 25u, 125u, 625u, 3125u, 15625u, 78125u, 390625u, 1953125u, 9765625u, 48828125u, 244140625u, 1220703125u, 6103515625u, 30517578125u, 152587890625u}};
|
||||
const std::uint64_t five = fives[static_cast<std::size_t>(-q)]; // 5^-q
|
||||
const unsigned s = static_cast<unsigned>(56 - t) & 63u;
|
||||
offset = ((2 * c * five) << s) - (w << 56u) + (std::uint64_t{32} << 56u);
|
||||
radius = (five << s) + ((c & 1u) == 0 ? 1u : 0u);
|
||||
// (c = 2^52: a power of two; c = 2^53: rounded up to one. With 8 to 16
|
||||
// fraction digits, as the batch's tokens have, t is in [15, 40] and g at
|
||||
// most 2. x lies halfway between two candidates only if its distance from
|
||||
// the token is a whole unit (g == 2) or a half (g == 1): such tokens are
|
||||
// left to the full conversion)
|
||||
const bool decided = c - ((std::uint64_t{1} << 52u) + 1) < (std::uint64_t{1} << 52u) - 1
|
||||
&& (offset & ((std::uint64_t{1} << 55u) - 1)) != 0;
|
||||
return decided ? g : 3;
|
||||
}
|
||||
|
||||
/// all bits set if b, else none
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t mask_if(bool b) noexcept
|
||||
{
|
||||
return std::uint64_t{0} - static_cast<std::uint64_t>(b);
|
||||
}
|
||||
|
||||
/// a if b, else c, without a branch (the conditions of token_digits_1/2() are
|
||||
/// as good as random, and GCC compiled their ?: as branches: one misprediction
|
||||
/// per float of canada.json on x86-64)
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t select_if(bool b, std::uint64_t a, std::uint64_t c) noexcept
|
||||
{
|
||||
return c ^ ((a ^ c) & mask_if(b));
|
||||
}
|
||||
|
||||
/// the distance of a position (see locate_token()) from the biased point 32
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t biased_distance(std::uint64_t position) noexcept
|
||||
{
|
||||
constexpr std::uint64_t center = std::uint64_t{32} << 56u;
|
||||
// |position - center|, without a branch: negated (complemented, plus 1) if below
|
||||
const std::uint64_t below = mask_if(position < center);
|
||||
return ((position - center) ^ below) - below;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief (g == 1) the digits dump() writes for a token, from the position of its
|
||||
double (see locate_token()): the multiple of 10 nearest to x if it is in the
|
||||
interval, else w rounded to x (an exact tie between two candidates, like
|
||||
754570942.33984373, whose double is 754570942.33984375, is excluded by
|
||||
locate_token())
|
||||
|
||||
With |x - w| at most the radius, which is below half the spacing of the
|
||||
candidates, the multiple of 10 nearest to x is w - r10 or the one above it.
|
||||
*/
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t token_digits_1(std::uint64_t w, std::uint64_t offset, std::uint64_t radius, std::uint64_t r10) noexcept
|
||||
{
|
||||
constexpr std::uint64_t one = std::uint64_t{1} << 56u;
|
||||
const std::uint64_t from = offset + (r10 * one); // x - (w - r10) (+ 32)
|
||||
const bool up = from > 37 * one; // (nearer to the multiple above)
|
||||
const bool shorter = biased_distance(from - (mask_if(up) & (10 * one))) < radius;
|
||||
const std::uint64_t a = w - r10 + (mask_if(up) & 10u);
|
||||
const std::uint64_t b = w + ((offset + (one / 2)) >> 56u) - 32; // w rounded to x
|
||||
return select_if(shorter, a, b);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief (g == 2) the digits dump() writes for a token, as token_digits_1(): the
|
||||
multiple of 100 nearest to x if it is in the interval, else the multiple of 10
|
||||
nearest to x
|
||||
*/
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t token_digits_2(std::uint64_t w, std::uint64_t offset, std::uint64_t radius, std::uint64_t r10, std::uint64_t r100) noexcept
|
||||
{
|
||||
constexpr std::uint64_t one = std::uint64_t{1} << 56u;
|
||||
const std::uint64_t from = offset + (r100 * one);
|
||||
const bool up = from > 82 * one;
|
||||
const bool shorter = biased_distance(from - (mask_if(up) & (100 * one))) < radius;
|
||||
const std::uint64_t a = w - r100 + (mask_if(up) & 100u);
|
||||
// x - (w - r10), in [-12, 21]: the multiple of 10 nearest to x is that one,
|
||||
// or one or two above, or one below
|
||||
const std::uint64_t y = offset + (r10 * one);
|
||||
const std::uint64_t steps = static_cast<std::uint64_t>(y > 37 * one) + static_cast<std::uint64_t>(y > 47 * one);
|
||||
const bool down = y < 27 * one;
|
||||
const std::uint64_t b = w - r10 + (10 * steps) - (mask_if(down) & 10u);
|
||||
return select_if(shorter, a, b);
|
||||
}
|
||||
|
||||
/// the four ASCII digits of x < 10^4, the first in the lowest byte (as
|
||||
/// little-endian memory holds them)
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint32_t four_digit_text(std::uint32_t x) noexcept
|
||||
{
|
||||
const std::uint32_t ab = (x * 5243u) >> 19u; // x / 100
|
||||
std::uint32_t v = ab | ((x - (ab * 100u)) << 16u);
|
||||
const std::uint32_t a = ((v * 103u) >> 10u) & 0x000F000Fu; // / 10 per 16-bit lane
|
||||
v = a | ((v - (a * 10u)) << 8u);
|
||||
return v + 0x30303030u;
|
||||
}
|
||||
|
||||
/// how the view's dump() writes a value
|
||||
struct dump_style
|
||||
{
|
||||
@@ -711,6 +850,7 @@ class view_serializer
|
||||
std::array<frame, 32> buffer; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::vector<frame> heap;
|
||||
small_stack<frame> stack(buffer.data(), buffer.size(), heap);
|
||||
float_batch batch;
|
||||
const char* const src = m_doc.src;
|
||||
const char* const src_end = src + m_doc.size;
|
||||
char* w = m_out.cursor();
|
||||
@@ -850,7 +990,7 @@ class view_serializer
|
||||
else if (enabled(fast_double::value))
|
||||
{
|
||||
room(64);
|
||||
w = write_double_at(w, *n);
|
||||
w = write_double_ahead(w, src, n, batch);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -875,6 +1015,20 @@ class view_serializer
|
||||
case value_t::array:
|
||||
{
|
||||
const bool object = n->kind == node_kind::object;
|
||||
// (read-only documents, compact output) a pair of floats,
|
||||
// like the coordinates of GeoJSON, without a frame
|
||||
if (enabled(!Editable && !Pretty && !SourceNumbers && fast_double::value) && n->next == 3 && n->len == 2
|
||||
&& n[1].kind == node_kind::number_float && n[2].kind == node_kind::number_float)
|
||||
{
|
||||
room(2 * 64);
|
||||
*w++ = '[';
|
||||
w = write_double_ahead(w, src, n + 1, batch);
|
||||
*w++ = ',';
|
||||
w = write_double_ahead(w, src, n + 2, batch);
|
||||
*w++ = ']';
|
||||
n += 2; // (the element before the next node)
|
||||
break;
|
||||
}
|
||||
room(8);
|
||||
if (n->len == 0)
|
||||
{
|
||||
@@ -984,6 +1138,254 @@ class view_serializer
|
||||
}
|
||||
}
|
||||
|
||||
/// (float_batch) the mark of digits whose rounding carried beyond the
|
||||
/// token's last four (see mark())
|
||||
static constexpr std::uint64_t carried() noexcept
|
||||
{
|
||||
return std::uint64_t{1} << 63u;
|
||||
}
|
||||
|
||||
/// floats of 16 or 17 digits ahead of the walk, converted together (see
|
||||
/// convert_ahead()), with their digits as mark() keeps them
|
||||
struct float_batch
|
||||
{
|
||||
static constexpr std::size_t capacity() noexcept
|
||||
{
|
||||
return 256;
|
||||
}
|
||||
/// (on the heap, allocated by the first conversion: a frame of more
|
||||
/// than a page made every dump of a small document slower)
|
||||
struct entries
|
||||
{
|
||||
std::array<const node*, 256> at; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<std::uint64_t, 256> digits; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
};
|
||||
std::unique_ptr<entries> storage{};
|
||||
const node** at = nullptr;
|
||||
std::uint64_t* digits = nullptr;
|
||||
std::size_t count = 0;
|
||||
std::size_t next = 0;
|
||||
const node* scanned = nullptr; ///< where the last scan of the node array ended
|
||||
};
|
||||
|
||||
/// (doubles) the float at n as dump() writes it, at w (64 bytes of room):
|
||||
/// from the batch if it holds n
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE char* write_double_ahead(char* w, const char* src, const node* n, float_batch& batch)
|
||||
{
|
||||
#if NLOHMANN_VIEW_VECTOR && NLOHMANN_VIEW_LITTLE_ENDIAN
|
||||
// (digits 0: not written by the batch; carried(): rounding carried
|
||||
// beyond the last four digits)
|
||||
if (batch.next != batch.count && batch.at[batch.next] == n && batch.digits[batch.next] - 1 < carried() - 1)
|
||||
{
|
||||
return write_token_digits(w, src, *n, batch.digits[batch.next++]);
|
||||
}
|
||||
return write_double_beside(w, n, batch);
|
||||
#else
|
||||
static_cast<void>(src);
|
||||
static_cast<void>(batch);
|
||||
return write_double_at(w, *n);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if NLOHMANN_VIEW_VECTOR && NLOHMANN_VIEW_LITTLE_ENDIAN
|
||||
/// write_double_ahead() for a float that is not the batch's next one (out
|
||||
/// of line: the walk is faster with less code in its loop)
|
||||
NLOHMANN_VIEW_NOINLINE char* write_double_beside(char* w, const node* n, float_batch& batch)
|
||||
{
|
||||
// (an editable document's walk can skip nodes of the array)
|
||||
const std::less<const node*> before{}; // NOLINT(modernize-use-transparent-functors) C++11 has no std::less<>
|
||||
while (batch.next != batch.count && before(batch.at[batch.next], n))
|
||||
{
|
||||
++batch.next;
|
||||
}
|
||||
if ((batch.next != batch.count && batch.at[batch.next] == n) || convert_ahead(n, batch))
|
||||
{
|
||||
const std::uint64_t digits = batch.digits[batch.next++];
|
||||
if ((digits & carried()) != 0)
|
||||
{
|
||||
return write_carried_digits(w, *n, digits & ~carried());
|
||||
}
|
||||
if (digits != 0)
|
||||
{
|
||||
return write_token_digits(w, m_doc.src, *n, digits);
|
||||
}
|
||||
}
|
||||
return write_double_at(w, *n);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief convert the floats of 16 or 17 digits among the next nodes of the
|
||||
array at once; whether n is the first of them
|
||||
|
||||
Such tokens (like the coordinates of GeoJSON, written with 17 digits) are
|
||||
written as the token with its last four digits replaced
|
||||
(write_token_digits()). Each step (the digits, locate_token(),
|
||||
token_digits_1/2()) is a long chain of dependent operations; one loop per
|
||||
step over many floats lets the processor overlap the floats, which it does
|
||||
not across the steps of one float (on Apple M1, one loop for the digits and
|
||||
locate_token() made the dump of canada.json 12 % slower). Each node of the array
|
||||
is scanned once; a node that the walk does not reach in order (in an
|
||||
editable document) is written without the batch.
|
||||
*/
|
||||
NLOHMANN_VIEW_NOINLINE bool convert_ahead(const node* n, float_batch& batch)
|
||||
{
|
||||
const std::less<const node*> before{}; // NOLINT(modernize-use-transparent-functors) C++11 has no std::less<>
|
||||
const node* const last = m_doc.nodes + m_doc.node_count;
|
||||
if (before(n, batch.scanned) || before(n, m_doc.nodes) || !before(n, last))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const char* const src = m_doc.src;
|
||||
const char* const src_end = src + m_doc.size;
|
||||
const node* const stop = last - n > 1024 ? n + 1024 : last;
|
||||
batch.count = 0;
|
||||
batch.next = 0;
|
||||
// the digits of each token (and its last four from the text)
|
||||
std::array<std::uint64_t, 256> digits; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<int, 256> exponent; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<std::uint32_t, 256> last_four; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
const node* p = n;
|
||||
if (!batch.storage)
|
||||
{
|
||||
batch.storage.reset(new typename float_batch::entries); // NOLINT(cppcoreguidelines-owning-memory,modernize-make-unique): C++11
|
||||
batch.at = batch.storage->at.data();
|
||||
batch.digits = batch.storage->digits.data();
|
||||
}
|
||||
for (; p != stop && batch.count != float_batch::capacity(); ++p)
|
||||
{
|
||||
// a token of the source: 16 or 17 digits, at least 8 after the
|
||||
// point, no exponent, 32 bytes to read
|
||||
const unsigned frac_digits = p->extra >> 8u;
|
||||
const unsigned int_digits = p->extra & 0xFFu;
|
||||
const unsigned count = int_digits + frac_digits;
|
||||
if (p->kind == node_kind::number_float && (p->flags & node_flags::storage) == 0
|
||||
&& count - 16u <= 1u && frac_digits >= 8 && src_end - (src + p->off) >= 33)
|
||||
{
|
||||
const auto* const token = reinterpret_cast<const unsigned char*>(src + p->off); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
const unsigned sign = token[0] == '-' ? 1 : 0;
|
||||
const unsigned char* const first = token + sign;
|
||||
const std::uint64_t w = sixteen_digits(first, int_digits);
|
||||
// (the 17th digit follows the point; a token with an exponent
|
||||
// gets digits outside the range of locate_token(), and is not
|
||||
// written by the batch. A token 0.xxx with 16 significant
|
||||
// digits is: its double is written as "0." and its digits too)
|
||||
const std::uint64_t w17 = (w * 10) + static_cast<std::uint64_t>(first[17] - '0');
|
||||
const std::size_t i = batch.count++;
|
||||
batch.at[i] = p;
|
||||
if (p->len != sign + count + 1)
|
||||
{
|
||||
digits[i] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
digits[i] = count == 17 ? w17 : w;
|
||||
}
|
||||
exponent[i] = -static_cast<int>(frac_digits);
|
||||
// (byte 0: the last digit, byte 1: the last two, bits 16-29: the last four)
|
||||
std::uint32_t four = 0;
|
||||
std::memcpy(&four, token + p->len - 4, 4);
|
||||
four -= 0x30303030u;
|
||||
const std::uint32_t pairs = ((four & 0x0F0F0F0Fu) * 2561u) >> 8u; // (byte 0, 2: the pairs)
|
||||
last_four[i] = (four >> 24u) | ((pairs >> 16u & 0xFFu) << 8u) | ((((pairs & 0x00FF00FFu) * 6553601u) >> 16u) << 16u);
|
||||
}
|
||||
}
|
||||
batch.scanned = p;
|
||||
// the position of each double, and the floats of either kind
|
||||
std::array<std::uint64_t, 256> offset; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<std::uint64_t, 256> radius; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<std::uint8_t, 256> kind_1; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<std::uint8_t, 256> kind_2; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::size_t count_1 = 0;
|
||||
std::size_t count_2 = 0;
|
||||
for (std::size_t i = 0; i != batch.count; ++i)
|
||||
{
|
||||
const std::uint64_t w = digits[i];
|
||||
// (the range excludes the values of bytes that are not digits, only
|
||||
// in a damaged image loaded with image_check::bounds)
|
||||
const int g = w - 1000000000000000u < 99000000000000000u ? locate_token(w, exponent[i], offset[i], radius[i]) : 3;
|
||||
batch.digits[i] = mask_if(g <= 0) & ((last_four[i] >> 16u) + 1);
|
||||
kind_1[count_1] = static_cast<std::uint8_t>(i);
|
||||
count_1 += static_cast<std::size_t>(g == 1);
|
||||
kind_2[count_2] = static_cast<std::uint8_t>(i);
|
||||
count_2 += static_cast<std::size_t>(g == 2);
|
||||
}
|
||||
// the digits to write, in one loop per kind (see mark())
|
||||
for (std::size_t k = 0; k != count_1; ++k)
|
||||
{
|
||||
const std::size_t i = kind_1[k];
|
||||
const std::uint64_t s = token_digits_1(digits[i], offset[i], radius[i], last_four[i] & 0xFFu);
|
||||
batch.digits[i] = mark(digits[i], s, last_four[i] >> 16u);
|
||||
}
|
||||
for (std::size_t k = 0; k != count_2; ++k)
|
||||
{
|
||||
const std::size_t i = kind_2[k];
|
||||
const std::uint64_t s = token_digits_2(digits[i], offset[i], radius[i], last_four[i] & 0xFFu, (last_four[i] >> 8u) & 0xFFu);
|
||||
batch.digits[i] = mark(digits[i], s, last_four[i] >> 16u);
|
||||
}
|
||||
return batch.count != 0 && batch.at[0] == n;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief a float token of 16 or 17 digits as dump() writes it, at w (64 bytes
|
||||
of room), from the digits of its double (see locate_token()) whose
|
||||
rounding carried beyond the token's last four digits
|
||||
|
||||
(like 43.42027299999999 -> 43.420273): the digits go to the writer of
|
||||
Zmij's results as its 15 or 16 leading digits and one more, which omits
|
||||
the trailing zeros.
|
||||
*/
|
||||
NLOHMANN_VIEW_NOINLINE char* write_carried_digits(char* w, const node& n, std::uint64_t digits) const noexcept
|
||||
{
|
||||
const int exponent = -(n.extra >> 8u);
|
||||
const std::uint64_t leading = digits / 10;
|
||||
const auto last = static_cast<unsigned char>(digits - (leading * 10));
|
||||
const bool seventeen = digits >= 10000000000000000u;
|
||||
*w = '-';
|
||||
w += m_doc.src[n.off] == '-' ? 1 : 0;
|
||||
return ::nlohmann::detail::dtoa_impl::write_shortest(w, seventeen ? ::nlohmann::detail::zmij::shortest_decimal{leading, exponent, last, last != 0}
|
||||
: ::nlohmann::detail::zmij::shortest_decimal{digits, exponent - 1, 0, false});
|
||||
}
|
||||
|
||||
/// the digits s of a token with the digits w and the last four digits
|
||||
/// r4, as the batch keeps them: the new last four digits + 1 (only those
|
||||
/// are written over the token's), or s marked if it differs from w beyond
|
||||
/// them (0 stays for a float the batch does not write)
|
||||
static std::uint64_t mark(std::uint64_t w, std::uint64_t s, std::uint64_t r4) noexcept
|
||||
{
|
||||
const std::uint64_t last = s - (w - r4);
|
||||
return last < 10000 ? last + 1 : s | carried();
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief a float token of 16 or 17 digits as dump() writes it, at w (64 bytes
|
||||
of room): the token with its last four digits replaced by digits - 1 (see
|
||||
mark(); the others are the token's), and without trailing zeros after the
|
||||
point, but one
|
||||
*/
|
||||
char* write_token_digits(char* w, const char* src, const node& n, std::uint64_t digits)
|
||||
{
|
||||
const char* const token = src + n.off;
|
||||
std::memcpy(w, token, 32);
|
||||
char* const end = w + n.len;
|
||||
const std::uint32_t last = four_digit_text(static_cast<std::uint32_t>(digits - 1));
|
||||
std::memcpy(end - 4, &last, 4);
|
||||
// (the token's text before the new digits, from the source: reading
|
||||
// the output back would wait for the stores)
|
||||
std::uint64_t before = 0;
|
||||
std::memcpy(&before, token + n.len - 12, 8);
|
||||
const std::uint32_t last_zeros = last ^ 0x30303030u;
|
||||
const std::uint64_t before_zeros = before ^ 0x3030303030303030u;
|
||||
if (NLOHMANN_VIEW_UNLIKELY(last_zeros == 0 && before_zeros == 0))
|
||||
{
|
||||
return write_double_at(w, n); // (twelve or more trailing zeros)
|
||||
}
|
||||
const unsigned zeros = last_zeros != 0 ? (static_cast<unsigned>(count_leading_zeros(last_zeros)) / 8) - 4
|
||||
: 4 + (static_cast<unsigned>(count_leading_zeros(before_zeros)) / 8);
|
||||
const unsigned frac_digits = n.extra >> 8u;
|
||||
return end - (zeros < frac_digits ? zeros : frac_digits - 1);
|
||||
}
|
||||
#endif
|
||||
|
||||
/// a float node as dump() writes it
|
||||
void write_float_node(const node& n)
|
||||
{
|
||||
|
||||
@@ -29,6 +29,13 @@
|
||||
// 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.
|
||||
//
|
||||
// The vector UTF-8 check (utf8_lookup4) is adapted from simdjson
|
||||
// (https://github.com/simdjson/simdjson, Daniel Lemire, Geoff Langdale, John
|
||||
// Keiser, and contributors, available under the Apache 2.0 and MIT licenses;
|
||||
// used here under the MIT license): the "lookup4" algorithm and tables of
|
||||
// J. Keiser and D. Lemire, "Validating UTF-8 In Less Than One Instruction Per
|
||||
// Byte" (2021).
|
||||
#if !defined(JSON_NO_SIMD) && defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) && NLOHMANN_VIEW_LITTLE_ENDIAN
|
||||
#include <arm_neon.h>
|
||||
#define NLOHMANN_VIEW_NEON 1
|
||||
|
||||
@@ -763,6 +763,7 @@ class basic_json_view
|
||||
template<typename, view_mode> friend class basic_json_document;
|
||||
template<typename, view_mode> friend class basic_json_view;
|
||||
template<typename, typename> friend class detail::view::editor;
|
||||
template<typename, typename> friend class detail::view::view_side;
|
||||
friend iterator;
|
||||
|
||||
basic_json_view(const document_data* d, const node* n) noexcept
|
||||
|
||||
Generated
+19
-34
@@ -10970,6 +10970,13 @@ NLOHMANN_JSON_NAMESPACE_END
|
||||
// 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.
|
||||
//
|
||||
// The Eisel-Lemire conversion (eisel_lemire()), the digit comparison for the
|
||||
// cases it cannot decide, and the eight-digits-at-a-time helpers
|
||||
// (is_eight_digits(), parse_eight_digits()) are adapted from fast_float
|
||||
// (https://github.com/fastfloat/fast_float, Daniel Lemire and contributors,
|
||||
// available under the MIT, Apache 2.0, and Boost Software licenses; used here
|
||||
// under the MIT license). The table of powers of five is in pow5_table.hpp.
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
namespace detail
|
||||
@@ -12078,6 +12085,13 @@ NLOHMANN_JSON_NAMESPACE_END
|
||||
// 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.
|
||||
//
|
||||
// The vector UTF-8 check (utf8_lookup4) is adapted from simdjson
|
||||
// (https://github.com/simdjson/simdjson, Daniel Lemire, Geoff Langdale, John
|
||||
// Keiser, and contributors, available under the Apache 2.0 and MIT licenses;
|
||||
// used here under the MIT license): the "lookup4" algorithm and tables of
|
||||
// J. Keiser and D. Lemire, "Validating UTF-8 In Less Than One Instruction Per
|
||||
// Byte" (2021).
|
||||
#if !defined(JSON_NO_SIMD) && defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) && defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
#include <arm_neon.h>
|
||||
#define JSON_SCAN_NEON 1
|
||||
@@ -32495,6 +32509,11 @@ NLOHMANN_JSON_NAMESPACE_END
|
||||
#include <type_traits> // conditional
|
||||
|
||||
|
||||
// The shortest-digit conversion of doubles (see zmij.hpp) and the writing of
|
||||
// its digits, including the conversion of the digits to text by Xiang JunBo
|
||||
// and the SIMD instruction sequence by Dougall Johnson, are ported from Zmij
|
||||
// by Victor Zverovich (https://github.com/vitaut/zmij, MIT license).
|
||||
//
|
||||
// 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)))
|
||||
@@ -36205,23 +36224,6 @@ class serializer
|
||||
return;
|
||||
}
|
||||
|
||||
// 17 to 20 digits: the first one to four, then twice eight
|
||||
if (magnitude_fits_64_bits(abs_value, std::integral_constant<bool, magnitude_has_at_most_64_bits>()))
|
||||
{
|
||||
const auto value = static_cast<std::uint64_t>(abs_value);
|
||||
const std::uint64_t top = value / 10000000000000000u; // 1 to 1844
|
||||
const std::uint64_t low16 = value - (top * 10000000000000000u);
|
||||
const std::uint64_t middle = low16 / 100000000u;
|
||||
const std::uint64_t first = dtoa_impl::eight_digit_bytes(top);
|
||||
const auto leading = static_cast<unsigned>(count_leading_zeros(first) / 8); // (first is not 0)
|
||||
char* const p = buffer_ptr + n_chars;
|
||||
dtoa_impl::store_msb_first(p, (first << (8 * leading)) + 0x3030303030303030u);
|
||||
dtoa_impl::store_msb_first(p + 8 - leading, dtoa_impl::eight_digit_bytes(middle) + 0x3030303030303030u);
|
||||
dtoa_impl::store_msb_first(p + 16 - leading, dtoa_impl::eight_digit_bytes(low16 - (middle * 100000000u)) + 0x3030303030303030u);
|
||||
write_buffer_pos += n_chars + 24 - leading;
|
||||
return;
|
||||
}
|
||||
|
||||
n_chars += count_digits(abs_value);
|
||||
|
||||
// spare 1 byte for '\0'
|
||||
@@ -36367,23 +36369,6 @@ class serializer
|
||||
}
|
||||
}
|
||||
|
||||
/// whether every number_magnitude_t fits 64 bits
|
||||
static constexpr bool magnitude_has_at_most_64_bits = std::numeric_limits<number_magnitude_t>::is_specialized
|
||||
&& std::numeric_limits<number_magnitude_t>::digits <= 64;
|
||||
|
||||
/// whether @a x fits 64 bits: always if number_magnitude_t has at most
|
||||
/// 64 bits (selected by type, as the comparison would be reported as
|
||||
/// always true), else compared
|
||||
static bool magnitude_fits_64_bits(const number_magnitude_t& /*unused*/, std::true_type /*unused*/) noexcept
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool magnitude_fits_64_bits(const number_magnitude_t& x, std::false_type /*unused*/) noexcept
|
||||
{
|
||||
return x <= 18446744073709551615u;
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper function for dump_integer
|
||||
*
|
||||
|
||||
Generated
+1994
-442
File diff suppressed because it is too large.
Load diff
@@ -21,6 +21,10 @@ python3 tests/benchmarks/json_view/compare.py --data <json_test_data directory>
|
||||
- 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.
|
||||
- `--aligned` additionally builds and runs everything with all functions aligned to 64 bytes (`-mllvm
|
||||
-align-all-functions=6` for Clang, `-falign-functions=64 -falign-loops=64` for GCC), and reports both result
|
||||
tables as "default build" and "aligned build". Code layout alone can move a result by a few percent; a difference
|
||||
that appears in only one of the two tables is a layout artifact. Off by default, which doubles the run time.
|
||||
- `--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).
|
||||
@@ -39,7 +43,7 @@ where `json_view` stands on another architecture; they are not meant for publica
|
||||
|
||||
## What is measured
|
||||
|
||||
`bench_view.cpp` runs four workloads on twitter, citm_catalog, canada, jeopardy, a single tweet (`status`), and a
|
||||
`bench_view.cpp` runs these workloads on twitter, citm_catalog, canada, jeopardy, a single tweet (`status`), and a
|
||||
JSON-RPC request (`rpc`):
|
||||
|
||||
| workload | what it does |
|
||||
@@ -48,6 +52,7 @@ JSON-RPC request (`rpc`):
|
||||
| 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) |
|
||||
| parse+dump | build a fresh document, then serialize it (compact), in one timed call |
|
||||
|
||||
`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
|
||||
@@ -72,11 +77,32 @@ call follows an untimed call of the same engine: otherwise the engine after `jso
|
||||
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).
|
||||
|
||||
`json_view` is an index into the source text, not a tree of owning nodes. Its like-for-like peers are yyjson and
|
||||
simdjson DOM (immutable, indexed, no per-node allocation). Boost.JSON and `json::parse` build mutable, owning DOMs and
|
||||
are reference points for what that costs, not equals; simdjson On-Demand is lazy and forward-only, so it is the other
|
||||
way round: it has no random access to pay for. All engines validate UTF-8 and syntax by default.
|
||||
|
||||
Number conversion differs: `json_view` converts integers while parsing but converts floating-point numbers lazily,
|
||||
when they are read or dumped; yyjson (default flags) and simdjson DOM convert every number while parsing. The cost of
|
||||
float conversion therefore appears in the traverse and dump rows for `json_view`, and in the parse row for the others.
|
||||
For float-heavy documents (canada), the fair reading is a sum of parse and use: the `traverse` workload already
|
||||
includes the parse, and `parse+dump` measures a fresh document and its dump in one timed call; the `dump` row alone
|
||||
leaves the conversion in `json_view` and out of the others. Eager conversion at parse time was prototyped as an opt-in
|
||||
and measured: canada traverse (with its parse) was 4-7 % faster, but parse+dump 11-30 % slower and parse 36-90 % slower. It was
|
||||
dropped, so lazy conversion is the only mode.
|
||||
|
||||
`json_view` can also be edited: `json_editable_document` has the same read API, and the `(editable)` rows in
|
||||
`bench_view` measure it on the same workloads. They are the fair counterpart of Boost.JSON and `json::parse`, which are
|
||||
mutable too; the plain `json_view` rows are the counterpart of yyjson and simdjson DOM, which are not. `bench_edit` adds
|
||||
the read-modify-write workloads. Read the tables with that in mind: an immutable-only library is not compared with an
|
||||
editable `json_view` unless it says so.
|
||||
|
||||
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 |
|
||||
| `json_view` (editable) | index into the text plus edits | yes | yes, in place | `json_editable_document`; keeps member order and number spellings |
|
||||
| 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 |
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
|
||||
// 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
|
||||
// json_view nlohmann/json_view.hpp (fresh document per parse / reused;
|
||||
// "(editable)": json_editable_document, same read API)
|
||||
// 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):
|
||||
@@ -19,7 +20,11 @@
|
||||
//
|
||||
// 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).
|
||||
// dump (compact serialization of the parsed document), and parse+dump (a fresh
|
||||
// document, then its dump, in one timed call). json_view converts floating-point
|
||||
// numbers lazily (when read or dumped), yyjson and simdjson DOM while parsing:
|
||||
// for float-heavy documents, traverse (which includes the parse) and parse+dump
|
||||
// are the fair reading of the parse and dump rows.
|
||||
// All engines run interleaved in every round, each timed call after an untimed
|
||||
// one of the same engine; the best round is reported.
|
||||
#include <nlohmann/json_view.hpp>
|
||||
@@ -43,6 +48,8 @@
|
||||
|
||||
using nlohmann::json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::json_editable_document;
|
||||
using nlohmann::json_editable_view;
|
||||
using nlohmann::json_view;
|
||||
|
||||
static volatile double g_sink;
|
||||
@@ -55,7 +62,8 @@ struct stats
|
||||
|
||||
// ---------------- traversal ----------------
|
||||
|
||||
static void walk(json_view v, stats& st)
|
||||
template<class V>
|
||||
static void walk_view(V v, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
switch (v.type())
|
||||
@@ -64,32 +72,42 @@ static void walk(json_view v, stats& st)
|
||||
for (auto it = v.begin(); it != v.end(); ++it)
|
||||
{
|
||||
st.str += it.key().size();
|
||||
walk(*it, st);
|
||||
walk_view<V>(*it, st);
|
||||
}
|
||||
break;
|
||||
case json::value_t::array:
|
||||
for (const json_view e : v)
|
||||
for (const V e : v)
|
||||
{
|
||||
walk(e, st);
|
||||
walk_view<V>(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<double>(v.get<std::int64_t>());
|
||||
st.num += static_cast<double>(v.template get<std::int64_t>());
|
||||
break;
|
||||
case json::value_t::number_unsigned:
|
||||
st.num += static_cast<double>(v.get<std::uint64_t>());
|
||||
st.num += static_cast<double>(v.template get<std::uint64_t>());
|
||||
break;
|
||||
case json::value_t::number_float:
|
||||
st.num += v.get<double>();
|
||||
st.num += v.template get<double>();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void walk(json_view v, stats& st)
|
||||
{
|
||||
walk_view<json_view>(v, st);
|
||||
}
|
||||
|
||||
static void walk(json_editable_view v, stats& st)
|
||||
{
|
||||
walk_view<json_editable_view>(v, st);
|
||||
}
|
||||
|
||||
static void walk(const json& j, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
@@ -300,20 +318,21 @@ static void walk(const boost::json::value& v, stats& st)
|
||||
// status (one tweet): id, user.screen_name, retweet_count
|
||||
// rpc: method, params.minuend, id
|
||||
|
||||
static double pick(const std::string& name, json_view r)
|
||||
template<class V>
|
||||
static double pick_view(const std::string& name, V r)
|
||||
{
|
||||
double acc = 0;
|
||||
if (name == "twitter" || name == "status")
|
||||
{
|
||||
auto one = [&](json_view s)
|
||||
auto one = [&](V s)
|
||||
{
|
||||
acc += static_cast<double>(s["id"].get<std::uint64_t>());
|
||||
acc += static_cast<double>(s["id"].template get<std::uint64_t>());
|
||||
acc += static_cast<double>(s["user"]["screen_name"].get_string().size());
|
||||
acc += static_cast<double>(s["retweet_count"].get<std::int64_t>());
|
||||
acc += static_cast<double>(s["retweet_count"].template get<std::int64_t>());
|
||||
};
|
||||
if (name == "twitter")
|
||||
{
|
||||
for (const json_view s : r["statuses"])
|
||||
for (const V s : r["statuses"])
|
||||
{
|
||||
one(s);
|
||||
}
|
||||
@@ -325,20 +344,20 @@ static double pick(const std::string& name, json_view r)
|
||||
}
|
||||
else if (name == "citm_catalog")
|
||||
{
|
||||
for (const json_view p : r["performances"])
|
||||
for (const V p : r["performances"])
|
||||
{
|
||||
acc += static_cast<double>(p["id"].get<std::uint64_t>() + p["eventId"].get<std::uint64_t>() + p["seatCategories"].size());
|
||||
acc += static_cast<double>(p["id"].template get<std::uint64_t>() + p["eventId"].template get<std::uint64_t>() + p["seatCategories"].size());
|
||||
}
|
||||
acc += static_cast<double>(r["events"].size());
|
||||
}
|
||||
else if (name == "canada")
|
||||
{
|
||||
const json_view g = r["features"][0]["geometry"];
|
||||
const V g = r["features"][0]["geometry"];
|
||||
acc += static_cast<double>(r["type"].get_string().size() + g["type"].get_string().size() + g["coordinates"].size());
|
||||
}
|
||||
else if (name == "jeopardy")
|
||||
{
|
||||
for (const json_view q : r)
|
||||
for (const V q : r)
|
||||
{
|
||||
acc += q["round"].get_string() == "Final Jeopardy!" ? 1 : 0;
|
||||
acc += static_cast<double>(q["category"].get_string().size());
|
||||
@@ -347,11 +366,21 @@ static double pick(const std::string& name, json_view r)
|
||||
else if (name == "rpc")
|
||||
{
|
||||
acc += static_cast<double>(r["method"].get_string().size());
|
||||
acc += static_cast<double>(r["params"]["minuend"].get<std::int64_t>() + r["id"].get<std::int64_t>());
|
||||
acc += static_cast<double>(r["params"]["minuend"].template get<std::int64_t>() + r["id"].template get<std::int64_t>());
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
static double pick(const std::string& name, json_view r)
|
||||
{
|
||||
return pick_view<json_view>(name, r);
|
||||
}
|
||||
|
||||
static double pick(const std::string& name, json_editable_view r)
|
||||
{
|
||||
return pick_view<json_editable_view>(name, r);
|
||||
}
|
||||
|
||||
static double pick(const std::string& name, const json& r)
|
||||
{
|
||||
double acc = 0;
|
||||
@@ -634,15 +663,35 @@ int main(int argc, char** argv)
|
||||
walk(json::parse(dc.text), a);
|
||||
auto d = json_document::parse(dc.text);
|
||||
walk(d.root(), b);
|
||||
auto ed = json_editable_document::parse(dc.text);
|
||||
stats e;
|
||||
walk(ed.root(), e);
|
||||
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
|
||||
const bool ok = a.nodes == b.nodes && a.nodes == e.nodes && a.str == e.str && std::fabs(a.num - e.num) <= 1e-9 * std::fabs(a.num) && 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");
|
||||
const double pa = pick(dc.name, json::parse(dc.text)), pb = pick(dc.name, d.root()), pe = pick(dc.name, ed.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 == pe && pa == pc && pa == pd) ? "OK" : "MISMATCH");
|
||||
yyjson_doc_free(y);
|
||||
|
||||
// the dumps of the engines may format floats differently, so only: every dump is non-empty, and the
|
||||
// dump of json_view (also editable) and of json::parse parse back to the same number of nodes
|
||||
const std::string dj = json::parse(dc.text).dump();
|
||||
const std::string dv = d.root().dump();
|
||||
const std::string de = ed.root().dump();
|
||||
std::size_t yn = 0;
|
||||
yyjson_doc* y2 = yyjson_read(dc.text.data(), dc.text.size(), 0);
|
||||
char* yo = yyjson_write(y2, 0, &yn);
|
||||
const bool dumps_ok = !dj.empty() && !dv.empty() && !de.empty() && yo != nullptr && yn > 0 && !simdjson::to_string(p.parse(dc.text).value()).empty();
|
||||
std::free(yo);
|
||||
yyjson_doc_free(y2);
|
||||
stats sj_nodes, sv_nodes, se_nodes;
|
||||
walk(json::parse(dj), sj_nodes);
|
||||
walk(json::parse(dv), sv_nodes);
|
||||
walk(json::parse(de), se_nodes);
|
||||
std::printf("check %-13s dump %s\n", dc.name.c_str(), (dumps_ok && sj_nodes.nodes == a.nodes && sv_nodes.nodes == a.nodes && se_nodes.nodes == a.nodes) ? "OK" : "MISMATCH");
|
||||
}
|
||||
|
||||
std::FILE* csv = std::fopen("bench_view.csv", "w");
|
||||
@@ -669,6 +718,7 @@ int main(int argc, char** argv)
|
||||
workloads.push_back({"parse", {
|
||||
{"json_view", [&] { auto d = json_document::parse(s); g_sink = static_cast<double>(d.node_count()); }},
|
||||
{"json_view (reused)", [&] { reused.read(s); g_sink = static_cast<double>(reused.node_count()); }},
|
||||
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); g_sink = static_cast<double>(d.root().size()); }},
|
||||
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = static_cast<double>(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(); }},
|
||||
@@ -680,6 +730,7 @@ int main(int argc, char** argv)
|
||||
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; }},
|
||||
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); stats st; walk(d.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; }},
|
||||
@@ -691,15 +742,31 @@ int main(int argc, char** argv)
|
||||
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()); }},
|
||||
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); g_sink = pick(name, d.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); }},
|
||||
}});
|
||||
// pipeline: a fresh document, then a dump of it, in one timed call
|
||||
// (simdjson DOM reuses its parser, as in the parse row); the traverse
|
||||
// workload above is already parse+traverse
|
||||
workloads.push_back({"parse+dump", {
|
||||
{"json_view", [&] { auto d = json_document::parse(s); std::string o = d.root().dump(); g_sink = static_cast<double>(o.size()); }},
|
||||
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); std::string o = d.root().dump(); g_sink = static_cast<double>(o.size()); }},
|
||||
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); std::size_t n = 0; char* o = yyjson_write(d, 0, &n); g_sink = static_cast<double>(n); std::free(o); yyjson_doc_free(d); }},
|
||||
{"simdjson DOM", [&] { std::string o = simdjson::to_string(sj.parse(ps).value_unsafe()); g_sink = static_cast<double>(o.size()); }},
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
{"Boost.JSON", [&] { boost::json::monotonic_resource mr; auto v = boost::json::parse(s, &mr); std::string o = boost::json::serialize(v); g_sink = static_cast<double>(o.size()); }},
|
||||
#endif
|
||||
{"json::parse", [&] { json j = json::parse(s); std::string o = j.dump(); g_sink = static_cast<double>(o.size()); }},
|
||||
}});
|
||||
{
|
||||
// serialization of an already parsed document
|
||||
static json_document vd;
|
||||
vd.read(s);
|
||||
static json_editable_document ed = json_editable_document::parse(s);
|
||||
ed = json_editable_document::parse(s);
|
||||
static yyjson_doc* yd = nullptr;
|
||||
if (yd)
|
||||
{
|
||||
@@ -713,6 +780,7 @@ int main(int argc, char** argv)
|
||||
jd = json::parse(s);
|
||||
workloads.push_back({"dump", {
|
||||
{"json_view", [&] { std::string o = vd.root().dump(); g_sink = static_cast<double>(o.size()); }},
|
||||
{"json_view (editable)", [&] { std::string o = ed.root().dump(); g_sink = static_cast<double>(o.size()); }},
|
||||
{"yyjson", [&] { std::size_t n = 0; char* o = yyjson_write(yd, 0, &n); g_sink = static_cast<double>(n); std::free(o); }},
|
||||
{"simdjson DOM", [&] { std::string o = simdjson::to_string(se); g_sink = static_cast<double>(o.size()); }},
|
||||
{"json::parse", [&] { std::string o = jd.dump(); g_sink = static_cast<double>(o.size()); }},
|
||||
@@ -740,7 +808,7 @@ int main(int argc, char** argv)
|
||||
}
|
||||
}
|
||||
const double ref = best[0];
|
||||
std::printf("%-13s %-9s", dc.name.c_str(), wl.first.c_str());
|
||||
std::printf("%-13s %-14s", 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;
|
||||
|
||||
@@ -239,6 +239,8 @@ def main():
|
||||
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('--aligned', action='store_true',
|
||||
help='also build and run with all functions (and loops) aligned, to make code layout artifacts visible')
|
||||
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)')
|
||||
@@ -282,32 +284,46 @@ def main():
|
||||
+ ['-I' + d for d in lib.include])
|
||||
objects.append(obj)
|
||||
|
||||
binaries = {}
|
||||
for bench in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']:
|
||||
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
|
||||
cxx_version = output(cxx + ['--version'])
|
||||
is_clang = 'clang' in cxx_version.lower()
|
||||
# alignment flags: functions and (gcc) loops on 64-byte boundaries
|
||||
align_flags = ['-mllvm', '-align-all-functions=6'] if is_clang else ['-falign-functions=64', '-falign-loops=64']
|
||||
|
||||
# the builds: the default one and, with --aligned, one with aligned code
|
||||
variants = [('default build', args.build_dir, [])]
|
||||
if args.aligned:
|
||||
variants.append(('aligned build', os.path.join(args.build_dir, 'aligned'), align_flags))
|
||||
|
||||
# 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
|
||||
outputs['bench_decode'] = run([binaries['bench_decode'], 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)
|
||||
# label -> {benchmark: output}
|
||||
all_outputs = {}
|
||||
for label, build_dir, extra in variants:
|
||||
os.makedirs(build_dir, exist_ok=True)
|
||||
binaries = {}
|
||||
for bench in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']:
|
||||
exe = os.path.join(build_dir, bench)
|
||||
run(cxx + flags + extra + 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
|
||||
outputs = {}
|
||||
outputs['bench_view'] = run([binaries['bench_view'], args.data, str(args.rounds)], cwd=build_dir,
|
||||
capture_output=True, text=True).stdout
|
||||
outputs['bench_corpus'] = run([binaries['bench_corpus']] + corpus, cwd=build_dir,
|
||||
capture_output=True, text=True).stdout
|
||||
outputs['bench_edit'] = run([binaries['bench_edit'], args.data, str(max(1, args.rounds // 2))], cwd=build_dir,
|
||||
capture_output=True, text=True).stdout
|
||||
outputs['bench_decode'] = run([binaries['bench_decode'], args.data, str(max(1, args.rounds // 2))],
|
||||
cwd=build_dir, capture_output=True, text=True).stdout
|
||||
for name, text in outputs.items():
|
||||
print(text)
|
||||
all_outputs[label] = outputs
|
||||
|
||||
# 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}'),
|
||||
@@ -316,6 +332,7 @@ def main():
|
||||
('OS', f'{platform.system()} {platform.release()} ({platform.machine()})'),
|
||||
('compiler', compiler_version),
|
||||
('flags', ' '.join(flags)),
|
||||
('aligned results', ('included (flags: ' + ' '.join(align_flags) + ')') if args.aligned else 'not included'),
|
||||
('yyjson', libs['yyjson'].version),
|
||||
('simdjson', libs['simdjson'].version),
|
||||
('Boost.JSON', libs['boost'].version if with_boost else 'skipped (not found)'),
|
||||
@@ -328,15 +345,20 @@ def main():
|
||||
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')
|
||||
for label, outputs in all_outputs.items():
|
||||
# (without --aligned there is only the default build, and no extra heading level)
|
||||
prefix = f'{label}: ' if args.aligned else ''
|
||||
for name, text in outputs.items():
|
||||
f.write(f'\n## {prefix}{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', 'bench_decode']:
|
||||
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())
|
||||
for label, build_dir, _ in variants:
|
||||
for name in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']:
|
||||
path = os.path.join(build_dir, name + '.csv')
|
||||
if os.path.isfile(path):
|
||||
with open(path, encoding='utf-8') as f:
|
||||
# (the default build keeps its plain section names)
|
||||
out.write(f'# {name}' + (f' ({label})' if args.aligned else '') + '\n' + f.read())
|
||||
print(f'results: {stem}.md, {stem}.csv')
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json_view.hpp>
|
||||
using nlohmann::json;
|
||||
using nlohmann::ordered_json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::ordered_json_editable_document;
|
||||
using nlohmann::json_view;
|
||||
using nlohmann::ordered_json_document;
|
||||
|
||||
#include "json_view_test_helpers.hpp"
|
||||
using json_view_test::generator;
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// These tests were split off unit-json_view_dump.cpp, whose object file got too
|
||||
// large for the MinGW assembler ("file too big"). Like that file, this one is
|
||||
// also built with C++17, as it mentions JSON_HAS_CPP_17.
|
||||
|
||||
TEST_CASE("json_view comparison")
|
||||
{
|
||||
SECTION("equality of the values parse() produces")
|
||||
{
|
||||
generator g;
|
||||
std::vector<std::string> texts;
|
||||
for (int i = 0; i < 600; ++i)
|
||||
{
|
||||
std::string text;
|
||||
g.value(text, 0);
|
||||
texts.push_back(text);
|
||||
// the same value written differently: sorted keys, canonical numbers
|
||||
texts.push_back(json::parse(text).dump(1));
|
||||
}
|
||||
for (std::size_t i = 0; i + 2 < texts.size(); ++i)
|
||||
{
|
||||
for (std::size_t k = i; k < i + 3; ++k)
|
||||
{
|
||||
CAPTURE(texts[i])
|
||||
CAPTURE(texts[k])
|
||||
const json_document a = json_document::parse(texts[i]);
|
||||
const json_document b = json_document::parse(texts[k]);
|
||||
const json ja = json::parse(texts[i]);
|
||||
const json jb = json::parse(texts[k]);
|
||||
CHECK((a.root() == b.root()) == (ja == jb));
|
||||
CHECK((a.root() != b.root()) == (ja != jb));
|
||||
CHECK((a.root() == jb) == (ja == jb));
|
||||
CHECK((jb == a.root()) == (ja == jb));
|
||||
CHECK((a.root() != jb) == (ja != jb));
|
||||
CHECK((jb != a.root()) == (ja != jb));
|
||||
|
||||
// ordered_json compares members in order
|
||||
const ordered_json_document oa = ordered_json_document::parse(texts[i]);
|
||||
const ordered_json_document ob = ordered_json_document::parse(texts[k]);
|
||||
const ordered_json oja = ordered_json::parse(texts[i]);
|
||||
const ordered_json ojb = ordered_json::parse(texts[k]);
|
||||
CHECK((oa.root() == ob.root()) == (oja == ojb));
|
||||
CHECK((oa.root() == ojb) == (oja == ojb));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("objects whose first keys differ or repeat")
|
||||
{
|
||||
const std::array<const char*, 17> texts =
|
||||
{
|
||||
{
|
||||
R"({"a":1,"b":2})", R"({"a":1,"c":2})", R"({"b":1,"a":2})", R"({"a":1,"a":2,"b":3})", R"({"a":2,"b":3})",
|
||||
R"({"a":1,"b":1,"a":2})", R"({"a":2,"b":1})", R"({"a":1,"a":2})", R"({"b":1})", R"({"a":1,"b":2,"c":3,"d":4,"e":5})",
|
||||
R"({"a":1,"b":2,"c":3,"d":4,"f":5})", R"({"a":1,"b":2,"c":3,"a":4,"e":5})", R"({"a":4,"b":2,"c":3,"e":5})",
|
||||
R"({"\u0061":1,"b":2})", R"({})", R"([{"a":1,"b":2}])", R"([{"a":1,"c":2}])"
|
||||
}
|
||||
};
|
||||
for (const char* const x : texts)
|
||||
{
|
||||
for (const char* const y : texts)
|
||||
{
|
||||
CAPTURE(x)
|
||||
CAPTURE(y)
|
||||
const ordered_json_document dx = ordered_json_document::parse(x);
|
||||
const ordered_json_document dy = ordered_json_document::parse(y);
|
||||
const ordered_json_editable_document ex = ordered_json_editable_document::parse(x);
|
||||
const ordered_json jx = ordered_json::parse(x);
|
||||
const ordered_json jy = ordered_json::parse(y);
|
||||
CHECK((dx.root() == dy.root()) == (jx == jy));
|
||||
CHECK((dx.root() == jy) == (jx == jy));
|
||||
CHECK((ex.root() == dy.root()) == (jx == jy));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("objects with duplicate keys, small and large")
|
||||
{
|
||||
// n members with keys in descending order; with duplicates, some keys
|
||||
// repeat later (the first after unique ones in key order); in reverse,
|
||||
// the members are in ascending key order
|
||||
const auto object = [](int n, bool duplicates, bool reverse, int value)
|
||||
{
|
||||
std::string s = "{";
|
||||
for (int i = 0; i < n; ++i)
|
||||
{
|
||||
const int k = reverse ? i : n - 1 - i;
|
||||
s += (i != 0 ? ",\"k" : "\"k") + std::to_string(100 + k) + "\":" + std::to_string(value + k);
|
||||
}
|
||||
if (duplicates)
|
||||
{
|
||||
for (int k = n / 2; k < n; k += 3)
|
||||
{
|
||||
s += ",\"k" + std::to_string(100 + k) + "\":" + std::to_string(value + k);
|
||||
}
|
||||
}
|
||||
return s + "}";
|
||||
};
|
||||
std::vector<std::string> texts;
|
||||
for (const int n :
|
||||
{
|
||||
3, 16, 17, 40
|
||||
})
|
||||
{
|
||||
for (const int flags :
|
||||
{
|
||||
0, 1, 2, 3
|
||||
})
|
||||
{
|
||||
texts.push_back(object(n, (flags & 1) != 0, (flags & 2) != 0, 0));
|
||||
}
|
||||
texts.push_back(object(n, true, false, 1)); // other values
|
||||
}
|
||||
// many objects in one comparison (working memory reused across objects)
|
||||
std::string array = "[";
|
||||
std::string array_other = "[";
|
||||
for (std::size_t i = 0; i < texts.size(); ++i)
|
||||
{
|
||||
array += (i != 0 ? "," : "") + texts[i];
|
||||
array_other += (i != 0 ? "," : "") + texts[texts.size() - 1 - i];
|
||||
}
|
||||
texts.push_back(array + "]");
|
||||
texts.push_back(array_other + "]");
|
||||
for (const std::string& x : texts)
|
||||
{
|
||||
for (const std::string& y : texts)
|
||||
{
|
||||
CAPTURE(x)
|
||||
CAPTURE(y)
|
||||
const ordered_json_document dx = ordered_json_document::parse(x);
|
||||
const ordered_json_document dy = ordered_json_document::parse(y);
|
||||
const ordered_json jx = ordered_json::parse(x);
|
||||
const ordered_json jy = ordered_json::parse(y);
|
||||
CHECK((dx.root() == dy.root()) == (jx == jy));
|
||||
CHECK((dx.root() == jy) == (jx == jy));
|
||||
const json_document ux = json_document::parse(x);
|
||||
const json uy = json::parse(y);
|
||||
CHECK((ux.root() == uy) == (json::parse(x) == uy));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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("values that are the same text, or the same value written differently, at any depth")
|
||||
{
|
||||
// nested arrays and objects around depths where the walk over equal text gives up
|
||||
for (const int depth :
|
||||
{
|
||||
1, 5, 47, 48, 49, 50, 60, 300
|
||||
})
|
||||
{
|
||||
CAPTURE(depth)
|
||||
std::string head;
|
||||
std::string tail;
|
||||
for (int i = 0; i < depth; ++i)
|
||||
{
|
||||
head += i % 2 == 0 ? R"({"k":[1,"s",{"x":true},)" : R"([null,2.5,{"y":)";
|
||||
tail.insert(0, i % 2 == 0 ? "]}" : "}]");
|
||||
}
|
||||
// the same text, one number changed, and one number written differently
|
||||
const auto with_number = [&](const char* number)
|
||||
{
|
||||
std::string result = head;
|
||||
result += number;
|
||||
result += tail;
|
||||
return result;
|
||||
};
|
||||
const std::string text = with_number("7");
|
||||
const std::string changed = with_number("8");
|
||||
const std::string spelled = with_number("7.0");
|
||||
const json_document a = json_document::parse(text);
|
||||
const json_document b = json_document::parse(text);
|
||||
const json_document c = json_document::parse(changed);
|
||||
const json_document d = json_document::parse(spelled);
|
||||
CHECK(a.root() == b.root());
|
||||
CHECK(a.root() != c.root());
|
||||
CHECK(c.root() != a.root());
|
||||
CHECK(a.root() == d.root());
|
||||
CHECK(d.root() == a.root());
|
||||
CHECK(a.root() == json::parse(text));
|
||||
CHECK(a.root() != json::parse(changed));
|
||||
CHECK(json::parse(spelled) == a.root());
|
||||
const nlohmann::json_editable_document e = nlohmann::json_editable_document::parse(text);
|
||||
CHECK(a.root() == e.root());
|
||||
CHECK(e.root() == a.root());
|
||||
CHECK(c.root() != e.root());
|
||||
CHECK(e.root() == e.root());
|
||||
const ordered_json_document oa = ordered_json_document::parse(text);
|
||||
const ordered_json_document oc = ordered_json_document::parse(changed);
|
||||
CHECK(oa.root() != oc.root());
|
||||
CHECK(oa.root() == ordered_json::parse(text));
|
||||
CHECK(oc.root() != ordered_json::parse(text));
|
||||
}
|
||||
|
||||
// not the same text, but the same value
|
||||
const auto same = [](const char* x, const char* y)
|
||||
{
|
||||
const json_document dx = json_document::parse(x);
|
||||
const json_document dy = json_document::parse(y);
|
||||
const bool equal = dx.root() == dy.root();
|
||||
CHECK(equal == (json::parse(x) == json::parse(y)));
|
||||
CHECK(equal == (dx.root() == json::parse(y)));
|
||||
return equal;
|
||||
};
|
||||
CHECK(same(R"({"a":1,"a":2})", R"({"a":3,"a":2})"));
|
||||
CHECK(same(R"({"a":1,"b":[1,{"c":2,"c":3}]})", R"({"b":[1,{"c":9,"c":3}],"a":1})"));
|
||||
CHECK(!same(R"({"a":1,"a":2})", R"({"a":2,"a":3})"));
|
||||
CHECK(same("[1.0,-0,0,1e2]", "[1.00,0,-0,100.0]"));
|
||||
CHECK(same(R"(["\u00e9","\n"])", "[\"\xc3\xa9\",\"\\n\"]"));
|
||||
CHECK(!same(R"({"a":[1,2,{"b":null}]})", R"({"a":[1,2,{"b":false}]})"));
|
||||
CHECK(!same(R"({"a":[1,2,{"b":null}]})", R"({"a":[1,2,{"c":null}]})"));
|
||||
CHECK(!same(R"({"a":[1,2,{"b":null}]})", R"({"a":[1,2,{"b":null,"c":1}]})"));
|
||||
|
||||
// edited documents: moved sequences, links, and values of new nodes
|
||||
nlohmann::json_editable_document edited = nlohmann::json_editable_document::parse(R"({"a":[1,2,3],"b":{"c":"d"},"e":0.5})");
|
||||
const json_document plain = json_document::parse(R"({"a":[1,2,3,4],"b":{"c":"x","y":null},"e":0.5,"z":[]})");
|
||||
edited.push_back(edited.root()["a"], 4);
|
||||
edited.set(edited.root()["b"], "c", "x");
|
||||
edited.set(edited.root()["b"], "y", nullptr);
|
||||
edited.set(edited.root(), "z", json::array());
|
||||
CHECK(edited.root() == plain.root());
|
||||
CHECK(plain.root() == edited.root());
|
||||
edited.erase(edited.root()["a"], 0);
|
||||
CHECK(edited.root() != plain.root());
|
||||
CHECK(edited.root()["a"] == json::parse("[2,3,4]"));
|
||||
const json_document tail = json_document::parse("[2,3,4]");
|
||||
CHECK(edited.root()["a"] == tail.root());
|
||||
}
|
||||
|
||||
SECTION("large documents, with a difference at any place")
|
||||
{
|
||||
const auto make = [](std::size_t count, std::size_t changed, bool swap_members)
|
||||
{
|
||||
std::string text = R"({"b":1,"list":[)";
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
text += (i != 0 ? "," : "");
|
||||
const std::string id = std::to_string(i == changed ? i + 1 : i);
|
||||
if (swap_members && i % 3 == 0)
|
||||
{
|
||||
text += R"({"v":[1.5,"s",{"k":)";
|
||||
text += id;
|
||||
text += R"(}],"id":)";
|
||||
text += id;
|
||||
text += "}";
|
||||
}
|
||||
else
|
||||
{
|
||||
text += R"({"id":)";
|
||||
text += id;
|
||||
text += R"(,"v":[1.5,"s",{"k":)";
|
||||
text += id;
|
||||
text += "}]}";
|
||||
}
|
||||
}
|
||||
return text + R"(],"a":{"y":2,"x":3}})";
|
||||
};
|
||||
const std::size_t count = 3000;
|
||||
for (const std::size_t changed :
|
||||
{
|
||||
count, std::size_t{0}, std::size_t{1}, count / 2, count - 1
|
||||
})
|
||||
{
|
||||
for (const bool swap :
|
||||
{
|
||||
false, true
|
||||
})
|
||||
{
|
||||
CAPTURE(changed)
|
||||
CAPTURE(swap)
|
||||
const std::string text_a = make(count, count, false);
|
||||
const std::string text_b = make(count, changed, swap);
|
||||
const json_document a = json_document::parse(text_a);
|
||||
const json_document b = json_document::parse(text_b);
|
||||
const bool equal = changed == count;
|
||||
CHECK((a.root() == b.root()) == equal);
|
||||
CHECK((b.root() == a.root()) == equal);
|
||||
CHECK((a.root() == json::parse(text_b)) == equal);
|
||||
CHECK((json::parse(text_b) == a.root()) == equal);
|
||||
CHECK((a.root()["list"] == b.root()["list"]) == equal);
|
||||
const ordered_json_document oa = ordered_json_document::parse(text_a);
|
||||
const ordered_json_document ob = ordered_json_document::parse(text_b);
|
||||
CHECK((oa.root() == ob.root()) == (equal && !swap));
|
||||
CHECK((oa.root() == ordered_json::parse(text_b)) == (equal && !swap));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -35,9 +35,9 @@ using json_view_test::has_duplicate_keys;
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// 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.
|
||||
// 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")
|
||||
{
|
||||
@@ -150,6 +150,88 @@ TEST_CASE("json_view dump")
|
||||
CHECK(many_doc.root().dump() == json::parse(many_tokens).dump());
|
||||
}
|
||||
|
||||
// tokens of 16 and 17 digits without an exponent (like GeoJSON
|
||||
// coordinates), which dump() converts in batches: doubles exactly
|
||||
// halfway between two candidates for the shortest digits, random
|
||||
// digits, and digits near a shorter decimal (...99999997,
|
||||
// ...00000003), whose rounding carries; in pairs and among other
|
||||
// values, so that the batches are interrupted; the last token near the
|
||||
// end of the text
|
||||
{
|
||||
std::mt19937_64 tokens(5627); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
const auto draw = [&tokens](std::size_t n)
|
||||
{
|
||||
const std::uint64_t r = tokens() % n;
|
||||
return static_cast<std::size_t>(r);
|
||||
};
|
||||
std::string coordinates = "[754570942.33984373,257769445.56835938,858106901.94921875,97944964.876953125,"
|
||||
"[0.5,1e-7],[1.5,-0.0,2.5],[1E2,12.5,0.1,3.25,4.5,5.75,6.125,7.0,8.5],[],[1.5],[1,2.5],[2.5,\"x\"],[[1.5],2.5],";
|
||||
for (int i = 0; i < 20000; ++i)
|
||||
{
|
||||
const std::size_t length = 16 + draw(2);
|
||||
std::string digits(1, static_cast<char>('1' + draw(9)));
|
||||
const std::size_t kept = draw(3) == 0 ? length : 2 + draw(length - 3);
|
||||
for (std::size_t k = 1; k < kept; ++k)
|
||||
{
|
||||
digits += static_cast<char>('0' + draw(10));
|
||||
}
|
||||
digits += std::string(length - kept, draw(2) == 0 ? '9' : '0');
|
||||
digits.back() = static_cast<char>('0' + draw(10));
|
||||
const std::size_t point = 1 + draw(9);
|
||||
const std::string token = (draw(2) == 0 ? "-" : "") + digits.substr(0, point) + "." + digits.substr(point);
|
||||
if (i != 0)
|
||||
{
|
||||
coordinates += ',';
|
||||
}
|
||||
if (draw(8) == 0)
|
||||
{
|
||||
coordinates += R"({"x":)";
|
||||
coordinates += token;
|
||||
coordinates += R"(,"s":"a"})";
|
||||
}
|
||||
else
|
||||
{
|
||||
coordinates += '[';
|
||||
coordinates += token;
|
||||
coordinates += ',';
|
||||
coordinates += token;
|
||||
coordinates += ']';
|
||||
}
|
||||
}
|
||||
coordinates += ",-65.613616999999977]";
|
||||
const json_document parsed = json_document::parse(coordinates);
|
||||
const ordered_json j = ordered_json::parse(coordinates);
|
||||
CHECK(parsed.root().dump() == j.dump());
|
||||
CHECK(parsed.root().dump(1) == j.dump(1));
|
||||
CHECK(parsed.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(parsed.root().back().get<double>() == j.back().get<double>());
|
||||
CHECK(parsed.root().back().dump() == "-65.61361699999998");
|
||||
std::size_t i = 0;
|
||||
for (const json_view v : parsed.root())
|
||||
{
|
||||
const ordered_json& e = j[i++];
|
||||
CHECK(v.dump() == e.dump());
|
||||
if (v.is_array() && !v.empty() && v[0].is_number())
|
||||
{
|
||||
CHECK(v[0].get<double>() == e[0].get<double>());
|
||||
}
|
||||
}
|
||||
|
||||
// an editable document: edits move some arrays, so that the walk
|
||||
// leaves the order of the parsed nodes
|
||||
ordered_json_editable_document editable = ordered_json_editable_document::parse(coordinates);
|
||||
ordered_json expected = j;
|
||||
for (std::size_t k = 0; k < 20000; k += 97)
|
||||
{
|
||||
if (expected[k].is_array())
|
||||
{
|
||||
editable.push_back(editable.root()[k], ordered_json(1.5));
|
||||
expected[k].push_back(1.5);
|
||||
}
|
||||
}
|
||||
CHECK(editable.root().dump() == expected.dump());
|
||||
}
|
||||
|
||||
// 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 = "[";
|
||||
@@ -171,768 +253,4 @@ TEST_CASE("json_view dump")
|
||||
const nlohmann::basic_json_document<json_float> float_document = nlohmann::basic_json_document<json_float>::parse("[0.1, 1.5e10, 3.4028235e38]");
|
||||
CHECK(float_document.root().dump() == json_float::parse("[0.1, 1.5e10, 3.4028235e38]").dump());
|
||||
}
|
||||
|
||||
SECTION("members in document order, all of them")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"b": 1, "a": 2, "b": 3})");
|
||||
CHECK(d.root().dump() == R"({"b":1,"a":2,"b":3})");
|
||||
CHECK(d.root().dump(1) == "{\n \"b\": 1,\n \"a\": 2,\n \"b\": 3\n}");
|
||||
// dump() writes all members, though the lookup finds the first
|
||||
CHECK(d.root()["b"].dump() == "1");
|
||||
}
|
||||
|
||||
SECTION("deep nesting")
|
||||
{
|
||||
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
|
||||
const json_document deep_document = json_document::parse(deep);
|
||||
CHECK(deep_document.root().dump() == deep);
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii: a character that needs an escape at each position of a string")
|
||||
{
|
||||
// string lengths around the width of the vector steps, in a string value, a key, and a value near the end of the text
|
||||
for (const char* const special :
|
||||
{
|
||||
"\xC3\xA9", "\xE4\xB8\xAD", "\xF0\x9F\x98\x80", "\x7F", "\\n", "\\u0001", "\\\"", "\\\\"
|
||||
})
|
||||
{
|
||||
for (std::size_t length = 0; length <= 40; ++length)
|
||||
{
|
||||
for (std::size_t position = 0; position <= length; ++position)
|
||||
{
|
||||
std::string content(length, 'a');
|
||||
content.insert(position, special);
|
||||
const std::array<std::string, 3> texts =
|
||||
{
|
||||
{
|
||||
"\"" + content + "\"",
|
||||
"[\"" + content + "\"]",
|
||||
"{\"" + content + "\":1}",
|
||||
}
|
||||
};
|
||||
for (const std::string& text : texts)
|
||||
{
|
||||
CAPTURE(text)
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii: every character of the Basic Multilingual Plane and some beyond")
|
||||
{
|
||||
// (the two- and three-byte sequences, including the ranges that need a check: E0, ED, EF BF BF)
|
||||
std::string text = "[";
|
||||
std::size_t in_chunk = 0;
|
||||
const auto append = [&](std::uint32_t cp)
|
||||
{
|
||||
if (cp < 0x80)
|
||||
{
|
||||
if (cp < 0x20 || cp == '"' || cp == '\\')
|
||||
{
|
||||
return;
|
||||
}
|
||||
text += static_cast<char>(cp);
|
||||
}
|
||||
else if (cp < 0x800)
|
||||
{
|
||||
text += static_cast<char>(0xC0 | (cp >> 6));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
else if (cp < 0x10000)
|
||||
{
|
||||
text += static_cast<char>(0xE0 | (cp >> 12));
|
||||
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
else
|
||||
{
|
||||
text += static_cast<char>(0xF0 | (cp >> 18));
|
||||
text += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
||||
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
};
|
||||
for (std::uint32_t cp = 0x20; cp < 0x10400; ++cp)
|
||||
{
|
||||
if (cp >= 0xD800 && cp < 0xE000)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (in_chunk == 0)
|
||||
{
|
||||
text += text.size() > 1 ? ",\"" : "\"";
|
||||
}
|
||||
append(cp);
|
||||
if (++in_chunk == 37)
|
||||
{
|
||||
text += "\"";
|
||||
in_chunk = 0;
|
||||
}
|
||||
}
|
||||
text += in_chunk != 0 ? "\"]" : "]";
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
CHECK(d.root().dump(2) == j.dump(2));
|
||||
}
|
||||
|
||||
SECTION("deep nesting, indented")
|
||||
{
|
||||
// indentations of up to and beyond the width of the writer's fixed-size copy
|
||||
for (const int depth :
|
||||
{
|
||||
1, 5, 20, 70, 300
|
||||
})
|
||||
{
|
||||
std::string text;
|
||||
for (int i = 0; i < depth; ++i)
|
||||
{
|
||||
text += i % 2 == 0 ? "[1," : R"({"k":2,"\u00e9":)";
|
||||
}
|
||||
text += "[]";
|
||||
for (int i = depth - 1; i >= 0; --i)
|
||||
{
|
||||
text += i % 2 == 0 ? "]" : "}";
|
||||
}
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
for (const int indent :
|
||||
{
|
||||
0, 1, 2, 3, 8
|
||||
})
|
||||
{
|
||||
CHECK(d.root().dump(indent) == j.dump(indent));
|
||||
CHECK(d.root().dump(indent, '\t') == j.dump(indent, '\t'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("the output buffer of a small value is small")
|
||||
{
|
||||
// an escaped key after the value: its node lies in the arena, so the
|
||||
// source extent of the value cannot be read from the next node
|
||||
const std::string big(100000, 'a');
|
||||
const std::string text = R"({"small":1,"list":[1,2,3],"k\n":")" + big + R"("})";
|
||||
const json_document d = json_document::parse(text);
|
||||
const auto small = d.root()["small"].dump();
|
||||
CHECK(small == "1");
|
||||
CHECK(small.capacity() < 4096);
|
||||
const auto list = d.root()["list"].dump();
|
||||
CHECK(list == "[1,2,3]");
|
||||
CHECK(list.capacity() < 4096);
|
||||
CHECK(d.root()["list"].dump(2).capacity() < 4096);
|
||||
|
||||
// the whole document and the large value are unaffected
|
||||
CHECK(d.root().dump() == ordered_json::parse(text).dump());
|
||||
CHECK(d.root()["k\n"].dump() == "\"" + big + "\"");
|
||||
}
|
||||
|
||||
SECTION("output that outgrows the estimate")
|
||||
{
|
||||
// ensure_ascii writes six bytes for each two-byte character
|
||||
std::string chars;
|
||||
for (int i = 0; i < 5000; ++i)
|
||||
{
|
||||
chars += "\xC3\xA9";
|
||||
}
|
||||
const json_document d = json_document::parse(R"({"a":")" + chars + R"(","k\n":1})");
|
||||
const json expected = json::parse("\"" + chars + "\"");
|
||||
CHECK(d.root()["a"].dump(-1, ' ', true) == expected.dump(-1, ' ', true));
|
||||
CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6));
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii with strings of the source, decoded strings, and edited strings")
|
||||
{
|
||||
// plain ASCII, non-ASCII, escaped (decoded), and mixed strings, as values and as keys;
|
||||
// the second text has a DEL, which ensure_ascii writes as an escape
|
||||
for (const char* const del :
|
||||
{"", "\x7F"
|
||||
})
|
||||
{
|
||||
const std::string text = std::string("{\"plain\":\"abc\",\"caf\xC3\xA9\":\"\xC3\xA9\xE2\x82\xAC\xF0\x9F\x98\x80\",")
|
||||
+ "\"esc\\n\\\"\":\"a\\tb\\u00e4\\ud83d\\ude00\",\"del\":\"a" + del + "b\",\"\":\"\"," // NOLINT(modernize-raw-string-literal)
|
||||
+ "\"list\":[\"x\",\"\xC3\xBC\",\"\\\\\",\"long ASCII string with more than thirty-two bytes in it\","
|
||||
+ "\"long string with more than thirty-two bytes and one non-ASCII byte \xC3\xB6\"],"
|
||||
+ "\"n\":[1,2.5,-0,true,null]}";
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(4, ' ', true) == j.dump(4, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
CHECK(d.root()["list"].dump(-1, ' ', true) == j["list"].dump(-1, ' ', true));
|
||||
CHECK(d.root()["del"].dump(-1, ' ', true) == j["del"].dump(-1, ' ', true));
|
||||
|
||||
// strings that an edit wrote (the source strings that they replace were plain)
|
||||
ordered_json_editable_document e = ordered_json_editable_document::parse(text);
|
||||
ordered_json k = j;
|
||||
e.set(e.root()["plain"], std::string("\xC3\xA4gypten"));
|
||||
k["plain"] = "\xC3\xA4gypten";
|
||||
e.set(e.root()["list"][0], std::string("ascii after edit"));
|
||||
k["list"][0] = "ascii after edit";
|
||||
e.set(e.root()["list"][1], std::string("\xC3\xA4"));
|
||||
k["list"][1] = "\xC3\xA4";
|
||||
e.set(e.root()["del"], std::string("tab\there"));
|
||||
k["del"] = "tab\there";
|
||||
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
|
||||
CHECK(e.root().dump(2, ' ', true) == k.dump(2, ' ', true));
|
||||
CHECK(e.root().dump() == k.dump());
|
||||
|
||||
// strings that were edited from or to ASCII text
|
||||
e.set(e.root()["list"][2], std::string("\xC3\xA4"));
|
||||
k["list"][2] = "\xC3\xA4";
|
||||
e.set(e.root()["list"][4], std::string("plain again"));
|
||||
k["list"][4] = "plain again";
|
||||
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii with DEL or a non-ASCII character at every position")
|
||||
{
|
||||
// the check whether a string of the source has no byte >= 0x7F reads
|
||||
// eight bytes at a time and then the rest one by one
|
||||
for (std::size_t length = 1; length <= 20; ++length)
|
||||
{
|
||||
for (std::size_t pos = 0; pos < length; ++pos)
|
||||
{
|
||||
for (const char* const c :
|
||||
{
|
||||
"\x7F", "\x7E", "\xC3\xA4", "\xC3\xBF" // NOLINT(modernize-raw-string-literal)
|
||||
})
|
||||
{
|
||||
const std::string s = std::string(pos, 'a') + c + std::string(length - pos - 1, 'b');
|
||||
std::string text = "[\"";
|
||||
text.append(s).append("\",{\"").append(s).append("\":1}]");
|
||||
const json_document d = json_document::parse(text);
|
||||
const json j = json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(1, ' ', true) == j.dump(1, ' ', true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("streams and discarded views")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"a": [1, 2]})");
|
||||
std::ostringstream compact;
|
||||
compact << d.root();
|
||||
CHECK(compact.str() == R"({"a":[1,2]})");
|
||||
std::ostringstream pretty;
|
||||
pretty << std::setw(2) << std::setfill('.') << d.root() << d.root()["a"];
|
||||
CHECK(pretty.str() == "{\n..\"a\": [\n....1,\n....2\n..]\n}[1,2]");
|
||||
CHECK(json_view().dump() == json(json::value_t::discarded).dump());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view comparison")
|
||||
{
|
||||
SECTION("equality of the values parse() produces")
|
||||
{
|
||||
generator g;
|
||||
std::vector<std::string> texts;
|
||||
for (int i = 0; i < 600; ++i)
|
||||
{
|
||||
std::string text;
|
||||
g.value(text, 0);
|
||||
texts.push_back(text);
|
||||
// the same value written differently: sorted keys, canonical numbers
|
||||
texts.push_back(json::parse(text).dump(1));
|
||||
}
|
||||
for (std::size_t i = 0; i + 2 < texts.size(); ++i)
|
||||
{
|
||||
for (std::size_t k = i; k < i + 3; ++k)
|
||||
{
|
||||
CAPTURE(texts[i])
|
||||
CAPTURE(texts[k])
|
||||
const json_document a = json_document::parse(texts[i]);
|
||||
const json_document b = json_document::parse(texts[k]);
|
||||
const json ja = json::parse(texts[i]);
|
||||
const json jb = json::parse(texts[k]);
|
||||
CHECK((a.root() == b.root()) == (ja == jb));
|
||||
CHECK((a.root() != b.root()) == (ja != jb));
|
||||
CHECK((a.root() == jb) == (ja == jb));
|
||||
CHECK((jb == a.root()) == (ja == jb));
|
||||
CHECK((a.root() != jb) == (ja != jb));
|
||||
CHECK((jb != a.root()) == (ja != jb));
|
||||
|
||||
// ordered_json compares members in order
|
||||
const ordered_json_document oa = ordered_json_document::parse(texts[i]);
|
||||
const ordered_json_document ob = ordered_json_document::parse(texts[k]);
|
||||
const ordered_json oja = ordered_json::parse(texts[i]);
|
||||
const ordered_json ojb = ordered_json::parse(texts[k]);
|
||||
CHECK((oa.root() == ob.root()) == (oja == ojb));
|
||||
CHECK((oa.root() == ojb) == (oja == ojb));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("numbers, duplicate keys, member order")
|
||||
{
|
||||
const auto same = [](const char* x, const char* y)
|
||||
{
|
||||
const json_document dx = json_document::parse(x);
|
||||
const json_document dy = json_document::parse(y);
|
||||
return dx.root() == dy.root();
|
||||
};
|
||||
CHECK(same("1", "1.0"));
|
||||
CHECK(same("[1, -1, 2.5]", "[1.0, -1.0, 25e-1]"));
|
||||
CHECK(!same("1", "1.5"));
|
||||
CHECK(same("18446744073709551615", "18446744073709551615"));
|
||||
CHECK(same(R"({"a": 1, "a": 2})", R"({"a": 2})"));
|
||||
CHECK(!same(R"({"a": 1, "a": 2})", R"({"a": 1})"));
|
||||
CHECK(same(R"({"a": 1, "b": 2})", R"({"b": 2, "a": 1})"));
|
||||
CHECK(!same(R"({"a": 1})", R"({"a": 1, "b": 2})"));
|
||||
CHECK(!same("[1, 2]", "[2, 1]"));
|
||||
CHECK(!same("\"a\"", "\"b\""));
|
||||
CHECK(same("\"\\u00e9\"", "\"\xc3\xa9\""));
|
||||
CHECK(!same("null", "false"));
|
||||
CHECK(!same("[]", "{}"));
|
||||
const ordered_json_document dup = ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})");
|
||||
const ordered_json_document last = ordered_json_document::parse(R"({"a": 3, "b": 2})");
|
||||
CHECK(dup.root() == last.root());
|
||||
// (== compares as parse() resolves duplicates, a lookup finds the first)
|
||||
CHECK(dup.root()["a"].materialize() == 1);
|
||||
CHECK(dup.root()["a"] != last.root()["a"]);
|
||||
const ordered_json_document ab = ordered_json_document::parse(R"({"a": 1, "b": 2})");
|
||||
const ordered_json_document ba = ordered_json_document::parse(R"({"b": 2, "a": 1})");
|
||||
CHECK(ab.root() != ba.root());
|
||||
|
||||
// discarded values compare as basic_json's do
|
||||
const json discarded(json::value_t::discarded);
|
||||
CHECK((json_view() == json_view()) == (discarded == discarded)); // NOLINT(readability-container-size-empty): operator== is tested
|
||||
CHECK((json_view() == discarded) == (discarded == discarded)); // NOLINT(readability-container-size-empty)
|
||||
const json_document null_document = json_document::parse("null");
|
||||
CHECK(!(json_view() == null_document.root())); // NOLINT(readability-container-size-empty)
|
||||
CHECK(!(null_document.root() == discarded));
|
||||
}
|
||||
|
||||
SECTION("deep nesting")
|
||||
{
|
||||
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
|
||||
const json_document a = json_document::parse(deep);
|
||||
const json_document b = json_document::parse(deep);
|
||||
CHECK(a.root() == b.root());
|
||||
CHECK(a.root() == json::parse(deep));
|
||||
const std::string other = std::string(100000, '[') + "1" + std::string(100000, ']');
|
||||
const json_document c = json_document::parse(other);
|
||||
CHECK(a.root() != c.root());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view large objects")
|
||||
{
|
||||
// objects with 128 members or more are looked up with a hash index
|
||||
for (const std::size_t members :
|
||||
{
|
||||
127u, 128u, 129u, 10000u
|
||||
})
|
||||
{
|
||||
CAPTURE(members)
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\\n" : "") + "\":" + std::to_string(i);
|
||||
}
|
||||
text += R"(,"":"empty key","k1":"a duplicate of an earlier key"})";
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
const json j = json::parse(text);
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
const std::string key = std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\n" : "");
|
||||
CHECK(v[key].get<std::size_t>() == i);
|
||||
CHECK(v.contains(key));
|
||||
CHECK(v.find(key).key() == key);
|
||||
CHECK(v.at(key).get<std::size_t>() == i);
|
||||
CHECK(!v.contains(key + "x"));
|
||||
}
|
||||
CHECK(v[""].get_string() == "empty key");
|
||||
CHECK(v["k1"].get<int>() == 1); // the first of duplicate keys, as for small objects
|
||||
CHECK(!v.contains("missing"));
|
||||
CHECK_THROWS_WITH_AS(v.at("missing"), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&);
|
||||
CHECK(v == j);
|
||||
CHECK(v.materialize() == j);
|
||||
}
|
||||
|
||||
SECTION("duplicate keys: the first member wins, with and without a table")
|
||||
{
|
||||
// an object of `total` members: the keys "k0".."k<n-1>" in order, then
|
||||
// three keys repeated twice more (one copy in the middle, one at the
|
||||
// end), and two keys repeated once; the value of a member is its
|
||||
// position, so that the first member of a key can be told apart
|
||||
struct member
|
||||
{
|
||||
std::string key;
|
||||
std::size_t position;
|
||||
};
|
||||
const auto make_members = [](std::size_t total)
|
||||
{
|
||||
std::vector<std::string> keys;
|
||||
for (std::size_t i = 0; i + 8 < total; ++i)
|
||||
{
|
||||
keys.push_back("k" + std::to_string(i));
|
||||
}
|
||||
const std::size_t n = keys.size();
|
||||
const std::array<std::size_t, 3> triple = {{3, 17, n - 1}};
|
||||
const std::array<std::size_t, 2> twice = {{5, n / 2}};
|
||||
std::vector<std::string> ordered = keys;
|
||||
for (const std::size_t i : triple)
|
||||
{
|
||||
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 2), keys[i]);
|
||||
}
|
||||
for (const std::size_t i : twice)
|
||||
{
|
||||
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 3), keys[i]);
|
||||
}
|
||||
for (const std::size_t i : triple)
|
||||
{
|
||||
ordered.push_back(keys[i]);
|
||||
}
|
||||
std::vector<member> result;
|
||||
result.reserve(ordered.size());
|
||||
for (std::size_t i = 0; i < ordered.size(); ++i)
|
||||
{
|
||||
result.push_back({ordered[i], i});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// 100 and 127 members: no table; 128 members and more: a table
|
||||
for (const std::size_t total :
|
||||
{
|
||||
100u, 127u, 128u, 200u, 5000u
|
||||
})
|
||||
{
|
||||
CAPTURE(total)
|
||||
const std::vector<member> members = make_members(total);
|
||||
REQUIRE(members.size() >= total);
|
||||
REQUIRE(members.size() >= 100);
|
||||
std::string text = "{";
|
||||
std::map<std::string, std::size_t> first;
|
||||
std::map<std::string, std::size_t> last;
|
||||
std::map<std::string, std::size_t> occurrences;
|
||||
for (const member& m : members)
|
||||
{
|
||||
text += (text.size() > 1 ? ",\"" : "\"") + m.key + "\":" + std::to_string(m.position);
|
||||
first.insert({m.key, m.position});
|
||||
last[m.key] = m.position;
|
||||
++occurrences[m.key];
|
||||
}
|
||||
text += '}';
|
||||
REQUIRE(first.size() < members.size());
|
||||
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
const json j = json::parse(text);
|
||||
CHECK(v.size() == members.size()); // every occurrence is visited
|
||||
const json m = v.materialize();
|
||||
for (const auto& entry : first)
|
||||
{
|
||||
CAPTURE(entry.first)
|
||||
const std::size_t expected = entry.second;
|
||||
CHECK(v[entry.first].get<std::size_t>() == expected);
|
||||
CHECK(v.at(entry.first).get<std::size_t>() == expected);
|
||||
CHECK(v.find(entry.first).value().get<std::size_t>() == expected);
|
||||
CHECK(v.value(entry.first, std::size_t{0}) == expected);
|
||||
CHECK(v.contains(entry.first));
|
||||
CHECK(v.count(entry.first) == occurrences[entry.first]);
|
||||
const std::string pointer = "/" + entry.first;
|
||||
CHECK(v[json::json_pointer(pointer)].get<std::size_t>() == expected);
|
||||
CHECK(v.at(json::json_pointer(pointer)).get<std::size_t>() == expected);
|
||||
CHECK(v.value(json::json_pointer(pointer), std::size_t{0}) == expected);
|
||||
CHECK(v.contains(json::json_pointer(pointer)));
|
||||
// materialize() and parse() keep the last value instead
|
||||
CHECK(j[entry.first].get<std::size_t>() == last.at(entry.first));
|
||||
CHECK(m[entry.first].get<std::size_t>() == last.at(entry.first));
|
||||
}
|
||||
CHECK(!v.contains("k"));
|
||||
CHECK(v["missing"].is_discarded());
|
||||
CHECK(v.materialize() == j);
|
||||
CHECK(v == j);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("colliding keys")
|
||||
{
|
||||
// the hash is not seeded, so keys that all land in one place must not
|
||||
// make the table build quadratic: such an object gets no table and is
|
||||
// searched linearly
|
||||
constexpr std::size_t members = 300;
|
||||
constexpr std::size_t table_size = 1024; // the table size for 300 members: the next power of two >= 600
|
||||
std::vector<std::string> colliding;
|
||||
std::vector<std::string> spread;
|
||||
for (std::uint64_t counter = 0; colliding.size() < members || spread.size() < members; ++counter)
|
||||
{
|
||||
std::string key(8, 'a');
|
||||
std::uint64_t x = counter;
|
||||
for (std::size_t i = 0; i < 8; ++i, x /= 26)
|
||||
{
|
||||
key[i] = static_cast<char>('a' + (x % 26));
|
||||
}
|
||||
const bool lands_in_slot_zero = (nlohmann::detail::view::key_hash(key.data(), key.size()) & (table_size - 1)) == 0;
|
||||
if (lands_in_slot_zero && colliding.size() < members)
|
||||
{
|
||||
colliding.push_back(key);
|
||||
}
|
||||
else if (!lands_in_slot_zero && spread.size() < members)
|
||||
{
|
||||
spread.push_back(key);
|
||||
}
|
||||
}
|
||||
|
||||
const auto make_text = [](const std::vector<std::string>& keys)
|
||||
{
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i % 10);
|
||||
}
|
||||
return text + "}";
|
||||
};
|
||||
const std::string colliding_text = make_text(colliding);
|
||||
const std::string spread_text = make_text(spread);
|
||||
|
||||
json_document with_collisions = json_document::parse(colliding_text);
|
||||
json_document without_collisions = json_document::parse(spread_text);
|
||||
for (const auto* pair :
|
||||
{
|
||||
&colliding, &spread
|
||||
})
|
||||
{
|
||||
const json_view v = (pair == &colliding ? with_collisions : without_collisions).root();
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
CAPTURE(i)
|
||||
CHECK(v[(*pair)[i]].get<std::size_t>() == i % 10);
|
||||
CHECK(v.at((*pair)[i]).get<std::size_t>() == i % 10);
|
||||
CHECK(v.find((*pair)[i]).key() == (*pair)[i]);
|
||||
CHECK(!v.contains((*pair)[i] + "x"));
|
||||
}
|
||||
CHECK(!v.contains("missing"));
|
||||
}
|
||||
CHECK(with_collisions.root() == json::parse(colliding_text));
|
||||
|
||||
// only the object with the spread keys got a table (both texts have the
|
||||
// same length, so the tables are the only difference)
|
||||
with_collisions.shrink_to_fit();
|
||||
without_collisions.shrink_to_fit();
|
||||
CHECK(without_collisions.memory_usage() >= with_collisions.memory_usage() + (table_size * sizeof(std::uint32_t)));
|
||||
}
|
||||
|
||||
SECTION("keys that differ in their last bytes are spread over the table")
|
||||
{
|
||||
// The hash must spread the usual families of keys (numbered names, hex
|
||||
// strings, dates, short names): a key farther than index_max_displacement
|
||||
// from its home slot would leave the object without a table. The keys
|
||||
// are placed with linear probing, as the table does.
|
||||
const auto max_displacement = [](const std::vector<std::string>& keys)
|
||||
{
|
||||
std::size_t slots = 16;
|
||||
while (slots < 2 * keys.size())
|
||||
{
|
||||
slots *= 2;
|
||||
}
|
||||
std::vector<bool> used(slots, false);
|
||||
std::size_t farthest = 0;
|
||||
for (const std::string& key : keys)
|
||||
{
|
||||
std::size_t i = nlohmann::detail::conditional_static_cast<std::size_t>(nlohmann::detail::view::key_hash(key.data(), key.size())) & (slots - 1);
|
||||
std::size_t distance = 0;
|
||||
while (used[i])
|
||||
{
|
||||
i = (i + 1) & (slots - 1);
|
||||
++distance;
|
||||
}
|
||||
used[i] = true;
|
||||
farthest = (std::max)(farthest, distance);
|
||||
}
|
||||
return farthest;
|
||||
};
|
||||
// the keys of a printf-like pattern with one %d, %0Nd or %0Nx, formatted with a stream
|
||||
// (a format string that is not a literal is a Flawfinder error)
|
||||
const auto numbered = [](const char* format, int count, int step)
|
||||
{
|
||||
const std::string pattern(format);
|
||||
const std::size_t percent = pattern.find('%');
|
||||
std::size_t conversion = percent + 1;
|
||||
std::size_t width = 0;
|
||||
while (pattern[conversion] >= '0' && pattern[conversion] <= '9')
|
||||
{
|
||||
width = (width * 10) + static_cast<std::size_t>(pattern[conversion] - '0');
|
||||
++conversion;
|
||||
}
|
||||
const bool hex = pattern[conversion] == 'x';
|
||||
std::vector<std::string> keys;
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
std::ostringstream key;
|
||||
key << pattern.substr(0, percent) << std::setfill('0') << std::setw(static_cast<int>(width))
|
||||
<< (hex ? std::hex : std::dec) << (i * step) << pattern.substr(conversion + 1);
|
||||
keys.push_back(key.str());
|
||||
}
|
||||
return keys;
|
||||
};
|
||||
for (const char* format :
|
||||
{"key%d", "%d", "item_%04d", "a%05d", "%03dxyz", "id%d", "user_%d", "%d_suffix", "0x%04x", "A%07d", "12345678%d", "1234567%d", "123456789%d",
|
||||
"configuration_parameter_value_%04d_extra", "2026-01-%02dT00:00:00Z"
|
||||
})
|
||||
{
|
||||
CAPTURE(format)
|
||||
CHECK(max_displacement(numbered(format, std::string(format).find("%02d") != std::string::npos ? 60 : 1000, 1)) < nlohmann::detail::view::index_max_displacement); // NOLINT(abseil-string-find-str-contains)
|
||||
CHECK(max_displacement(numbered(format, 600, 7)) < nlohmann::detail::view::index_max_displacement);
|
||||
}
|
||||
std::mt19937_64 rng(7); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
for (const std::size_t length :
|
||||
{
|
||||
std::size_t{1}, std::size_t{2}, std::size_t{3}, std::size_t{5}, std::size_t{7}, std::size_t{8}, std::size_t{9}, std::size_t{12}, std::size_t{16}, std::size_t{17}, std::size_t{33}
|
||||
})
|
||||
{
|
||||
CAPTURE(length)
|
||||
std::set<std::string> distinct;
|
||||
// (there are 26 and 676 keys of one and two letters)
|
||||
std::size_t wanted = 700;
|
||||
if (length == 1)
|
||||
{
|
||||
wanted = 26;
|
||||
}
|
||||
else if (length == 2)
|
||||
{
|
||||
wanted = 500;
|
||||
}
|
||||
while (distinct.size() < wanted)
|
||||
{
|
||||
std::string key(length, 'a');
|
||||
for (char& c : key)
|
||||
{
|
||||
c = static_cast<char>('a' + (rng() % 26));
|
||||
}
|
||||
distinct.insert(key);
|
||||
}
|
||||
CHECK(max_displacement(std::vector<std::string>(distinct.begin(), distinct.end())) < nlohmann::detail::view::index_max_displacement);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("keys of every length that differ in one byte")
|
||||
{
|
||||
// In a large object, a key is found by comparing its bytes with those of the member the
|
||||
// table points to: up to 16 bytes as two overlapping words, longer ones with memcmp.
|
||||
// Keys of every length from 0 to 40, and keys that differ from them in one byte.
|
||||
std::mt19937_64 rng(2026); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
std::vector<std::string> keys;
|
||||
std::set<std::string> distinct;
|
||||
for (std::size_t length = 0; length <= 40; ++length)
|
||||
{
|
||||
for (int copy = 0; copy < 5; ++copy)
|
||||
{
|
||||
std::string key(length, 'a');
|
||||
for (char& c : key)
|
||||
{
|
||||
c = static_cast<char>('a' + (rng() % 26));
|
||||
}
|
||||
if (distinct.insert(key).second)
|
||||
{
|
||||
keys.push_back(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i);
|
||||
}
|
||||
text += "}";
|
||||
REQUIRE(keys.size() >= 128); // (a table is built for 128 members)
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
CAPTURE(keys[i])
|
||||
CHECK(v[keys[i]].get<std::size_t>() == i);
|
||||
CHECK(v.at(keys[i]).get<std::size_t>() == i);
|
||||
CHECK(v.contains(keys[i]));
|
||||
for (std::size_t at = 0; at < keys[i].size(); ++at)
|
||||
{
|
||||
std::string other = keys[i];
|
||||
other[at] = other[at] == 'z' ? 'y' : 'z';
|
||||
CAPTURE(other)
|
||||
CHECK(v.contains(other) == (distinct.count(other) != 0));
|
||||
}
|
||||
CHECK(v.contains(keys[i] + "a") == (distinct.count(keys[i] + "a") != 0));
|
||||
if (!keys[i].empty())
|
||||
{
|
||||
CHECK(v.contains(keys[i].substr(1)) == (distinct.count(keys[i].substr(1)) != 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("shrink_to_fit releases the tables' spare capacity")
|
||||
{
|
||||
const auto make_text = [](int objects, int members)
|
||||
{
|
||||
std::string text = "[";
|
||||
for (int object = 0; object < objects; ++object)
|
||||
{
|
||||
text += object != 0 ? ",{" : "{";
|
||||
for (int i = 0, n = members + object; i < n; ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\":" + std::to_string(i);
|
||||
}
|
||||
text += '}';
|
||||
}
|
||||
return text + "]";
|
||||
};
|
||||
const std::string small_text = make_text(5, 150);
|
||||
const std::string big_text = make_text(40, 400);
|
||||
|
||||
// reading a big text, and then a small one, leaves the spare capacity
|
||||
// of the big one: shrink_to_fit() brings the document to the size of
|
||||
// one parsed from the small text alone
|
||||
json_document d = json_document::parse(big_text);
|
||||
const std::size_t big = d.memory_usage();
|
||||
d.read(small_text);
|
||||
CHECK(d.memory_usage() >= big);
|
||||
d.shrink_to_fit();
|
||||
json_document fresh = json_document::parse(small_text);
|
||||
fresh.shrink_to_fit();
|
||||
CHECK(d.memory_usage() == fresh.memory_usage());
|
||||
CHECK(d.memory_usage() < big / 2);
|
||||
CHECK(d.root() == json::parse(small_text));
|
||||
for (int object = 0; object < 5; ++object)
|
||||
{
|
||||
const json_view v = d.root()[static_cast<std::size_t>(object)];
|
||||
for (int i = 0, n = 150 + object; i < n; ++i)
|
||||
{
|
||||
CHECK(v[std::to_string(i)].get<int>() == i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("nested, reused, and in arrays")
|
||||
{
|
||||
std::string inner = "{";
|
||||
for (int i = 0; i < 300; ++i)
|
||||
{
|
||||
inner += (i != 0 ? ",\"m" : "\"m") + std::to_string(i) + "\":" + std::to_string(i);
|
||||
}
|
||||
inner += '}';
|
||||
const std::string text = "[" + inner + ",{\"x\":" + inner + "}," + inner + "]";
|
||||
json_document d = json_document::parse(text);
|
||||
CHECK(d.root()[0]["m299"].get<int>() == 299);
|
||||
CHECK(d.root()[1]["x"]["m150"].get<int>() == 150);
|
||||
CHECK(d.root()[2]["m0"].get<int>() == 0);
|
||||
const std::size_t with_index = d.memory_usage();
|
||||
d.read(std::string("{\"small\": 1}"));
|
||||
CHECK(d.root()["small"].get<int>() == 1);
|
||||
d.read(text);
|
||||
CHECK(d.root()[2]["m7"].get<int>() == 7);
|
||||
CHECK(d.memory_usage() >= with_index / 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json_view.hpp>
|
||||
using nlohmann::json;
|
||||
using nlohmann::ordered_json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::ordered_json_editable_document;
|
||||
using nlohmann::json_view;
|
||||
using nlohmann::ordered_json_document;
|
||||
using nlohmann::ordered_json_view;
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <map>
|
||||
#include <random>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// These tests were split off unit-json_view_dump.cpp, whose object file got too large for the MinGW
|
||||
// linker (see json_view_test_helpers.hpp). Like that file, this one is also built
|
||||
// with C++17, as it mentions JSON_HAS_CPP_17.
|
||||
|
||||
TEST_CASE("json_view dump: escaping, nesting, and output buffers")
|
||||
{
|
||||
SECTION("members in document order, all of them")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"b": 1, "a": 2, "b": 3})");
|
||||
CHECK(d.root().dump() == R"({"b":1,"a":2,"b":3})");
|
||||
CHECK(d.root().dump(1) == "{\n \"b\": 1,\n \"a\": 2,\n \"b\": 3\n}");
|
||||
// dump() writes all members, though the lookup finds the first
|
||||
CHECK(d.root()["b"].dump() == "1");
|
||||
}
|
||||
|
||||
SECTION("deep nesting")
|
||||
{
|
||||
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
|
||||
const json_document deep_document = json_document::parse(deep);
|
||||
CHECK(deep_document.root().dump() == deep);
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii: a character that needs an escape at each position of a string")
|
||||
{
|
||||
// string lengths around the width of the vector steps, in a string value, a key, and a value near the end of the text
|
||||
for (const char* const special :
|
||||
{
|
||||
"\xC3\xA9", "\xE4\xB8\xAD", "\xF0\x9F\x98\x80", "\x7F", "\\n", "\\u0001", "\\\"", "\\\\"
|
||||
})
|
||||
{
|
||||
for (std::size_t length = 0; length <= 40; ++length)
|
||||
{
|
||||
for (std::size_t position = 0; position <= length; ++position)
|
||||
{
|
||||
std::string content(length, 'a');
|
||||
content.insert(position, special);
|
||||
const std::array<std::string, 3> texts =
|
||||
{
|
||||
{
|
||||
"\"" + content + "\"",
|
||||
"[\"" + content + "\"]",
|
||||
"{\"" + content + "\":1}",
|
||||
}
|
||||
};
|
||||
for (const std::string& text : texts)
|
||||
{
|
||||
CAPTURE(text)
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii: every character of the Basic Multilingual Plane and some beyond")
|
||||
{
|
||||
// (the two- and three-byte sequences, including the ranges that need a check: E0, ED, EF BF BF)
|
||||
std::string text = "[";
|
||||
std::size_t in_chunk = 0;
|
||||
const auto append = [&](std::uint32_t cp)
|
||||
{
|
||||
if (cp < 0x80)
|
||||
{
|
||||
if (cp < 0x20 || cp == '"' || cp == '\\')
|
||||
{
|
||||
return;
|
||||
}
|
||||
text += static_cast<char>(cp);
|
||||
}
|
||||
else if (cp < 0x800)
|
||||
{
|
||||
text += static_cast<char>(0xC0 | (cp >> 6));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
else if (cp < 0x10000)
|
||||
{
|
||||
text += static_cast<char>(0xE0 | (cp >> 12));
|
||||
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
else
|
||||
{
|
||||
text += static_cast<char>(0xF0 | (cp >> 18));
|
||||
text += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
||||
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
};
|
||||
for (std::uint32_t cp = 0x20; cp < 0x10400; ++cp)
|
||||
{
|
||||
if (cp >= 0xD800 && cp < 0xE000)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (in_chunk == 0)
|
||||
{
|
||||
text += text.size() > 1 ? ",\"" : "\"";
|
||||
}
|
||||
append(cp);
|
||||
if (++in_chunk == 37)
|
||||
{
|
||||
text += "\"";
|
||||
in_chunk = 0;
|
||||
}
|
||||
}
|
||||
text += in_chunk != 0 ? "\"]" : "]";
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
CHECK(d.root().dump(2) == j.dump(2));
|
||||
}
|
||||
|
||||
SECTION("deep nesting, indented")
|
||||
{
|
||||
// indentations of up to and beyond the width of the writer's fixed-size copy
|
||||
for (const int depth :
|
||||
{
|
||||
1, 5, 20, 70, 300
|
||||
})
|
||||
{
|
||||
std::string text;
|
||||
for (int i = 0; i < depth; ++i)
|
||||
{
|
||||
text += i % 2 == 0 ? "[1," : R"({"k":2,"\u00e9":)";
|
||||
}
|
||||
text += "[]";
|
||||
for (int i = depth - 1; i >= 0; --i)
|
||||
{
|
||||
text += i % 2 == 0 ? "]" : "}";
|
||||
}
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
for (const int indent :
|
||||
{
|
||||
0, 1, 2, 3, 8
|
||||
})
|
||||
{
|
||||
CHECK(d.root().dump(indent) == j.dump(indent));
|
||||
CHECK(d.root().dump(indent, '\t') == j.dump(indent, '\t'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("the output buffer of a small value is small")
|
||||
{
|
||||
// an escaped key after the value: its node lies in the arena, so the
|
||||
// source extent of the value cannot be read from the next node
|
||||
const std::string big(100000, 'a');
|
||||
const std::string text = R"({"small":1,"list":[1,2,3],"k\n":")" + big + R"("})";
|
||||
const json_document d = json_document::parse(text);
|
||||
const auto small = d.root()["small"].dump();
|
||||
CHECK(small == "1");
|
||||
CHECK(small.capacity() < 4096);
|
||||
const auto list = d.root()["list"].dump();
|
||||
CHECK(list == "[1,2,3]");
|
||||
CHECK(list.capacity() < 4096);
|
||||
CHECK(d.root()["list"].dump(2).capacity() < 4096);
|
||||
|
||||
// the whole document and the large value are unaffected
|
||||
CHECK(d.root().dump() == ordered_json::parse(text).dump());
|
||||
CHECK(d.root()["k\n"].dump() == "\"" + big + "\"");
|
||||
}
|
||||
|
||||
SECTION("output that outgrows the estimate")
|
||||
{
|
||||
// ensure_ascii writes six bytes for each two-byte character
|
||||
std::string chars;
|
||||
for (int i = 0; i < 5000; ++i)
|
||||
{
|
||||
chars += "\xC3\xA9";
|
||||
}
|
||||
const json_document d = json_document::parse(R"({"a":")" + chars + R"(","k\n":1})");
|
||||
const json expected = json::parse("\"" + chars + "\"");
|
||||
CHECK(d.root()["a"].dump(-1, ' ', true) == expected.dump(-1, ' ', true));
|
||||
CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6));
|
||||
}
|
||||
|
||||
SECTION("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());
|
||||
}
|
||||
}
|
||||
@@ -941,3 +941,123 @@ TEST_CASE("json_view edits: duplicate keys")
|
||||
CHECK(d.root()["o"]["d"].get<int>() == 0);
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
TEST_CASE("json_view edits: check_utf8 agrees with the byte-wise check")
|
||||
{
|
||||
namespace dv = nlohmann::detail::view;
|
||||
|
||||
// "ok" or the exception message
|
||||
const auto run = [](bool fast, const std::string & s) -> std::string
|
||||
{
|
||||
try
|
||||
{
|
||||
if (fast)
|
||||
{
|
||||
dv::check_utf8(s.data(), s.size());
|
||||
}
|
||||
else
|
||||
{
|
||||
dv::check_utf8_bytewise(reinterpret_cast<const unsigned char*>(s.data()), s.size(), 0);
|
||||
}
|
||||
}
|
||||
catch (const json::type_error& e)
|
||||
{
|
||||
return {e.what()};
|
||||
}
|
||||
return {"ok"};
|
||||
};
|
||||
std::size_t errors = 0;
|
||||
const auto same = [&](const std::string & s)
|
||||
{
|
||||
const std::string a = run(true, s);
|
||||
const std::string b = run(false, s);
|
||||
if (a != b)
|
||||
{
|
||||
INFO("fast: " << a << ", byte-wise: " << b << ", length " << s.size());
|
||||
CHECK(a == b);
|
||||
}
|
||||
errors += b != "ok" ? 1u : 0u;
|
||||
};
|
||||
|
||||
const std::vector<std::string> pieces =
|
||||
{
|
||||
"a", "Z", " ", "\"", "\\", "\n", "\x01", "\x7f", "\xc3\xa4", "\xe4\xb8\xad", "\xf0\x9f\x98\x80",
|
||||
"\x80", "\xbf", "\xc0\x80", "\xc1\xbf", "\xc2", "\xe0\x80\x80", "\xe0\x9f\xbf", "\xed\xa0\x80", "\xed\xbf\xbf",
|
||||
"\xf0\x80\x80\x80", "\xf0\x8f\xbf\xbf", "\xf4\x90\x80\x80", "\xf4\x8f\xbf\xbf", "\xf5", "\xf8", "\xff", "\xfe",
|
||||
"\xe4\xb8", "\xf0\x9f\x98", "\xf0\x9f", "\xf0"
|
||||
};
|
||||
|
||||
// every piece behind prefixes of every length, and followed by more bytes
|
||||
for (std::size_t prefix = 0; prefix <= 64; ++prefix)
|
||||
{
|
||||
for (const auto& piece : pieces)
|
||||
{
|
||||
same(std::string(prefix, 'x') + piece);
|
||||
same(std::string(prefix, 'x') + piece + "yz");
|
||||
same(std::string(prefix, '"') + piece);
|
||||
same(std::string(prefix, '\xc3') + piece);
|
||||
same(std::string(prefix / 2, '\xc3').append(prefix / 2, '\xa4') + piece);
|
||||
same(std::string(prefix, 'x') + "\xe4\xb8\xad" + piece);
|
||||
}
|
||||
}
|
||||
// an invalid byte at every position of valid strings of every length
|
||||
for (std::size_t len = 0; len <= 64; ++len)
|
||||
{
|
||||
const std::string ascii(len, 'a');
|
||||
std::string mixed;
|
||||
while (mixed.size() < len)
|
||||
{
|
||||
if (mixed.size() % 7 == 0)
|
||||
{
|
||||
mixed += "\xc3\xa4";
|
||||
}
|
||||
else if (mixed.size() % 11 == 0)
|
||||
{
|
||||
mixed += "\xf0\x9f\x98\x80";
|
||||
}
|
||||
else
|
||||
{
|
||||
mixed += "a";
|
||||
}
|
||||
}
|
||||
same(ascii);
|
||||
same(mixed);
|
||||
for (std::size_t pos = 0; pos < len; ++pos)
|
||||
{
|
||||
for (const char bad :
|
||||
{'\x80', '\xc0', '\xc2', '\xe0', '\xf4', '\xff', '"', '\\', '\x1f'
|
||||
})
|
||||
{
|
||||
std::string s = ascii;
|
||||
s[pos] = bad;
|
||||
same(s);
|
||||
s = mixed;
|
||||
s[pos] = bad;
|
||||
same(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
// random bytes, biased to the interesting ones
|
||||
std::mt19937 gen(5869); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
|
||||
static const std::array<unsigned char, 23> interesting = {{0x00, 0x1f, 0x22, 0x5c, 0x7f, 0x80, 0x8f, 0x90, 0x9f, 0xa0, 0xbf, 0xc0, 0xc2, 0xdf, 0xe0, 0xe1, 0xed, 0xef, 0xf0, 0xf1, 0xf4, 0xf5, 0xff}};
|
||||
for (int i = 0; i < 100000; ++i)
|
||||
{
|
||||
std::string s(gen() % 80, 'a');
|
||||
for (auto& c : s)
|
||||
{
|
||||
const unsigned r = gen() % 8;
|
||||
if (r == 0)
|
||||
{
|
||||
c = static_cast<char>(gen() & 0xff);
|
||||
}
|
||||
else if (r == 1)
|
||||
{
|
||||
c = static_cast<char>(interesting[gen() % interesting.size()]);
|
||||
}
|
||||
}
|
||||
same(s);
|
||||
}
|
||||
CHECK(errors > 1000);
|
||||
}
|
||||
#endif
|
||||
@@ -701,6 +701,64 @@ TEST_CASE("json_view images: ownership")
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
TEST_CASE("json_view images: errors")
|
||||
{
|
||||
SECTION("float tokens: a changed byte is accepted if the text is still a number with the digits the node records")
|
||||
{
|
||||
for (const char* const token :
|
||||
{
|
||||
"-12.375", "0.5", "123456.0", "-0.0", "9999999999999999999.5", "0.000000000000000000000001", "1.25e10", "5E-3"
|
||||
})
|
||||
{
|
||||
CAPTURE(token)
|
||||
const std::string text = std::string("[") + token + "]";
|
||||
const json_document d = json_document::parse(text);
|
||||
const std::vector<std::uint8_t> image = d.save();
|
||||
const std::size_t at = text_at(image) + 1; // the first byte of the token
|
||||
const std::uint16_t recorded = node_at(image, 1).extra;
|
||||
CHECK(loaded_dump(image) == json::parse(text).dump());
|
||||
for (std::size_t pos = 0; token[pos] != '\0'; ++pos)
|
||||
{
|
||||
for (unsigned int byte = 0; byte < 256; ++byte)
|
||||
{
|
||||
std::vector<std::uint8_t> changed = image;
|
||||
changed[at + pos] = static_cast<std::uint8_t>(byte);
|
||||
std::string t = token;
|
||||
t[pos] = static_cast<char>(byte);
|
||||
// the digits of the integer part and of the fraction of the changed token
|
||||
std::size_t integer = 0;
|
||||
std::size_t fraction = 0;
|
||||
std::size_t k = (t[0] == '-') ? 1 : 0;
|
||||
for (; k < t.size() && t[k] >= '0' && t[k] <= '9'; ++k)
|
||||
{
|
||||
++integer;
|
||||
}
|
||||
if (k < t.size() && t[k] == '.')
|
||||
{
|
||||
for (++k; k < t.size() && t[k] >= '0' && t[k] <= '9'; ++k)
|
||||
{
|
||||
++fraction;
|
||||
}
|
||||
}
|
||||
// (as a JSON text, the token may have white space around it)
|
||||
const bool float_token = json::accept(t) && t.find_first_of(std::string(" \t\r\n\0", 5)) == std::string::npos && (t.find('.') != std::string::npos || t.find_first_of("eE") != std::string::npos) // NOLINT(abseil-string-find-str-contains)
|
||||
&& ((integer | (fraction << 8U)) == recorded);
|
||||
CAPTURE(pos)
|
||||
CAPTURE(byte)
|
||||
std::string result;
|
||||
try
|
||||
{
|
||||
result = loaded_dump(changed);
|
||||
}
|
||||
catch (const json::parse_error& e)
|
||||
{
|
||||
CHECK(std::string(e.what()) == check_failed);
|
||||
result = "rejected";
|
||||
}
|
||||
CHECK((result != "rejected") == float_token);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"})");
|
||||
|
||||
@@ -0,0 +1,544 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json_view.hpp>
|
||||
using nlohmann::json;
|
||||
using nlohmann::ordered_json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::ordered_json_editable_document;
|
||||
using nlohmann::json_view;
|
||||
using nlohmann::ordered_json_document;
|
||||
using nlohmann::ordered_json_view;
|
||||
|
||||
#include "json_view_test_helpers.hpp"
|
||||
using json_view_test::generator;
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <map>
|
||||
#include <random>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// These tests were split off unit-json_view_dump.cpp, whose object file got too large for the MinGW
|
||||
// linker (see json_view_test_helpers.hpp). Like that file, this one is also built
|
||||
// with C++17, as it mentions JSON_HAS_CPP_17.
|
||||
|
||||
TEST_CASE("json_view comparison")
|
||||
{
|
||||
SECTION("equality of the values parse() produces")
|
||||
{
|
||||
generator g;
|
||||
std::vector<std::string> texts;
|
||||
for (int i = 0; i < 600; ++i)
|
||||
{
|
||||
std::string text;
|
||||
g.value(text, 0);
|
||||
texts.push_back(text);
|
||||
// the same value written differently: sorted keys, canonical numbers
|
||||
texts.push_back(json::parse(text).dump(1));
|
||||
}
|
||||
for (std::size_t i = 0; i + 2 < texts.size(); ++i)
|
||||
{
|
||||
for (std::size_t k = i; k < i + 3; ++k)
|
||||
{
|
||||
CAPTURE(texts[i])
|
||||
CAPTURE(texts[k])
|
||||
const json_document a = json_document::parse(texts[i]);
|
||||
const json_document b = json_document::parse(texts[k]);
|
||||
const json ja = json::parse(texts[i]);
|
||||
const json jb = json::parse(texts[k]);
|
||||
CHECK((a.root() == b.root()) == (ja == jb));
|
||||
CHECK((a.root() != b.root()) == (ja != jb));
|
||||
CHECK((a.root() == jb) == (ja == jb));
|
||||
CHECK((jb == a.root()) == (ja == jb));
|
||||
CHECK((a.root() != jb) == (ja != jb));
|
||||
CHECK((jb != a.root()) == (ja != jb));
|
||||
|
||||
// ordered_json compares members in order
|
||||
const ordered_json_document oa = ordered_json_document::parse(texts[i]);
|
||||
const ordered_json_document ob = ordered_json_document::parse(texts[k]);
|
||||
const ordered_json oja = ordered_json::parse(texts[i]);
|
||||
const ordered_json ojb = ordered_json::parse(texts[k]);
|
||||
CHECK((oa.root() == ob.root()) == (oja == ojb));
|
||||
CHECK((oa.root() == ojb) == (oja == ojb));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("numbers, duplicate keys, member order")
|
||||
{
|
||||
const auto same = [](const char* x, const char* y)
|
||||
{
|
||||
const json_document dx = json_document::parse(x);
|
||||
const json_document dy = json_document::parse(y);
|
||||
return dx.root() == dy.root();
|
||||
};
|
||||
CHECK(same("1", "1.0"));
|
||||
CHECK(same("[1, -1, 2.5]", "[1.0, -1.0, 25e-1]"));
|
||||
CHECK(!same("1", "1.5"));
|
||||
CHECK(same("18446744073709551615", "18446744073709551615"));
|
||||
CHECK(same(R"({"a": 1, "a": 2})", R"({"a": 2})"));
|
||||
CHECK(!same(R"({"a": 1, "a": 2})", R"({"a": 1})"));
|
||||
CHECK(same(R"({"a": 1, "b": 2})", R"({"b": 2, "a": 1})"));
|
||||
CHECK(!same(R"({"a": 1})", R"({"a": 1, "b": 2})"));
|
||||
CHECK(!same("[1, 2]", "[2, 1]"));
|
||||
CHECK(!same("\"a\"", "\"b\""));
|
||||
CHECK(same("\"\\u00e9\"", "\"\xc3\xa9\""));
|
||||
CHECK(!same("null", "false"));
|
||||
CHECK(!same("[]", "{}"));
|
||||
const ordered_json_document dup = ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})");
|
||||
const ordered_json_document last = ordered_json_document::parse(R"({"a": 3, "b": 2})");
|
||||
CHECK(dup.root() == last.root());
|
||||
// (== compares as parse() resolves duplicates, a lookup finds the first)
|
||||
CHECK(dup.root()["a"].materialize() == 1);
|
||||
CHECK(dup.root()["a"] != last.root()["a"]);
|
||||
const ordered_json_document ab = ordered_json_document::parse(R"({"a": 1, "b": 2})");
|
||||
const ordered_json_document ba = ordered_json_document::parse(R"({"b": 2, "a": 1})");
|
||||
CHECK(ab.root() != ba.root());
|
||||
|
||||
// discarded values compare as basic_json's do
|
||||
const json discarded(json::value_t::discarded);
|
||||
CHECK((json_view() == json_view()) == (discarded == discarded)); // NOLINT(readability-container-size-empty): operator== is tested
|
||||
CHECK((json_view() == discarded) == (discarded == discarded)); // NOLINT(readability-container-size-empty)
|
||||
const json_document null_document = json_document::parse("null");
|
||||
CHECK(!(json_view() == null_document.root())); // NOLINT(readability-container-size-empty)
|
||||
CHECK(!(null_document.root() == discarded));
|
||||
}
|
||||
|
||||
SECTION("deep nesting")
|
||||
{
|
||||
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
|
||||
const json_document a = json_document::parse(deep);
|
||||
const json_document b = json_document::parse(deep);
|
||||
CHECK(a.root() == b.root());
|
||||
CHECK(a.root() == json::parse(deep));
|
||||
const std::string other = std::string(100000, '[') + "1" + std::string(100000, ']');
|
||||
const json_document c = json_document::parse(other);
|
||||
CHECK(a.root() != c.root());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view large objects")
|
||||
{
|
||||
// objects with 128 members or more are looked up with a hash index
|
||||
for (const std::size_t members :
|
||||
{
|
||||
127u, 128u, 129u, 10000u
|
||||
})
|
||||
{
|
||||
CAPTURE(members)
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\\n" : "") + "\":" + std::to_string(i);
|
||||
}
|
||||
text += R"(,"":"empty key","k1":"a duplicate of an earlier key"})";
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
const json j = json::parse(text);
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
const std::string key = std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\n" : "");
|
||||
CHECK(v[key].get<std::size_t>() == i);
|
||||
CHECK(v.contains(key));
|
||||
CHECK(v.find(key).key() == key);
|
||||
CHECK(v.at(key).get<std::size_t>() == i);
|
||||
CHECK(!v.contains(key + "x"));
|
||||
}
|
||||
CHECK(v[""].get_string() == "empty key");
|
||||
CHECK(v["k1"].get<int>() == 1); // the first of duplicate keys, as for small objects
|
||||
CHECK(!v.contains("missing"));
|
||||
CHECK_THROWS_WITH_AS(v.at("missing"), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&);
|
||||
CHECK(v == j);
|
||||
CHECK(v.materialize() == j);
|
||||
}
|
||||
|
||||
SECTION("duplicate keys: the first member wins, with and without a table")
|
||||
{
|
||||
// an object of `total` members: the keys "k0".."k<n-1>" in order, then
|
||||
// three keys repeated twice more (one copy in the middle, one at the
|
||||
// end), and two keys repeated once; the value of a member is its
|
||||
// position, so that the first member of a key can be told apart
|
||||
struct member
|
||||
{
|
||||
std::string key;
|
||||
std::size_t position;
|
||||
};
|
||||
const auto make_members = [](std::size_t total)
|
||||
{
|
||||
std::vector<std::string> keys;
|
||||
for (std::size_t i = 0; i + 8 < total; ++i)
|
||||
{
|
||||
keys.push_back("k" + std::to_string(i));
|
||||
}
|
||||
const std::size_t n = keys.size();
|
||||
const std::array<std::size_t, 3> triple = {{3, 17, n - 1}};
|
||||
const std::array<std::size_t, 2> twice = {{5, n / 2}};
|
||||
std::vector<std::string> ordered = keys;
|
||||
for (const std::size_t i : triple)
|
||||
{
|
||||
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 2), keys[i]);
|
||||
}
|
||||
for (const std::size_t i : twice)
|
||||
{
|
||||
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 3), keys[i]);
|
||||
}
|
||||
for (const std::size_t i : triple)
|
||||
{
|
||||
ordered.push_back(keys[i]);
|
||||
}
|
||||
std::vector<member> result;
|
||||
result.reserve(ordered.size());
|
||||
for (std::size_t i = 0; i < ordered.size(); ++i)
|
||||
{
|
||||
result.push_back({ordered[i], i});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// 100 and 127 members: no table; 128 members and more: a table
|
||||
for (const std::size_t total :
|
||||
{
|
||||
100u, 127u, 128u, 200u, 5000u
|
||||
})
|
||||
{
|
||||
CAPTURE(total)
|
||||
const std::vector<member> members = make_members(total);
|
||||
REQUIRE(members.size() >= total);
|
||||
REQUIRE(members.size() >= 100);
|
||||
std::string text = "{";
|
||||
std::map<std::string, std::size_t> first;
|
||||
std::map<std::string, std::size_t> last;
|
||||
std::map<std::string, std::size_t> occurrences;
|
||||
for (const member& m : members)
|
||||
{
|
||||
text += (text.size() > 1 ? ",\"" : "\"") + m.key + "\":" + std::to_string(m.position);
|
||||
first.insert({m.key, m.position});
|
||||
last[m.key] = m.position;
|
||||
++occurrences[m.key];
|
||||
}
|
||||
text += '}';
|
||||
REQUIRE(first.size() < members.size());
|
||||
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
const json j = json::parse(text);
|
||||
CHECK(v.size() == members.size()); // every occurrence is visited
|
||||
const json m = v.materialize();
|
||||
for (const auto& entry : first)
|
||||
{
|
||||
CAPTURE(entry.first)
|
||||
const std::size_t expected = entry.second;
|
||||
CHECK(v[entry.first].get<std::size_t>() == expected);
|
||||
CHECK(v.at(entry.first).get<std::size_t>() == expected);
|
||||
CHECK(v.find(entry.first).value().get<std::size_t>() == expected);
|
||||
CHECK(v.value(entry.first, std::size_t{0}) == expected);
|
||||
CHECK(v.contains(entry.first));
|
||||
CHECK(v.count(entry.first) == occurrences[entry.first]);
|
||||
const std::string pointer = "/" + entry.first;
|
||||
CHECK(v[json::json_pointer(pointer)].get<std::size_t>() == expected);
|
||||
CHECK(v.at(json::json_pointer(pointer)).get<std::size_t>() == expected);
|
||||
CHECK(v.value(json::json_pointer(pointer), std::size_t{0}) == expected);
|
||||
CHECK(v.contains(json::json_pointer(pointer)));
|
||||
// materialize() and parse() keep the last value instead
|
||||
CHECK(j[entry.first].get<std::size_t>() == last.at(entry.first));
|
||||
CHECK(m[entry.first].get<std::size_t>() == last.at(entry.first));
|
||||
}
|
||||
CHECK(!v.contains("k"));
|
||||
CHECK(v["missing"].is_discarded());
|
||||
CHECK(v.materialize() == j);
|
||||
CHECK(v == j);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("colliding keys")
|
||||
{
|
||||
// the hash is not seeded, so keys that all land in one place must not
|
||||
// make the table build quadratic: such an object gets no table and is
|
||||
// searched linearly
|
||||
constexpr std::size_t members = 300;
|
||||
constexpr std::size_t table_size = 1024; // the table size for 300 members: the next power of two >= 600
|
||||
std::vector<std::string> colliding;
|
||||
std::vector<std::string> spread;
|
||||
for (std::uint64_t counter = 0; colliding.size() < members || spread.size() < members; ++counter)
|
||||
{
|
||||
std::string key(8, 'a');
|
||||
std::uint64_t x = counter;
|
||||
for (std::size_t i = 0; i < 8; ++i, x /= 26)
|
||||
{
|
||||
key[i] = static_cast<char>('a' + (x % 26));
|
||||
}
|
||||
const bool lands_in_slot_zero = (nlohmann::detail::view::key_hash(key.data(), key.size()) & (table_size - 1)) == 0;
|
||||
if (lands_in_slot_zero && colliding.size() < members)
|
||||
{
|
||||
colliding.push_back(key);
|
||||
}
|
||||
else if (!lands_in_slot_zero && spread.size() < members)
|
||||
{
|
||||
spread.push_back(key);
|
||||
}
|
||||
}
|
||||
|
||||
const auto make_text = [](const std::vector<std::string>& keys)
|
||||
{
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i % 10);
|
||||
}
|
||||
return text + "}";
|
||||
};
|
||||
const std::string colliding_text = make_text(colliding);
|
||||
const std::string spread_text = make_text(spread);
|
||||
|
||||
json_document with_collisions = json_document::parse(colliding_text);
|
||||
json_document without_collisions = json_document::parse(spread_text);
|
||||
for (const auto* pair :
|
||||
{
|
||||
&colliding, &spread
|
||||
})
|
||||
{
|
||||
const json_view v = (pair == &colliding ? with_collisions : without_collisions).root();
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
CAPTURE(i)
|
||||
CHECK(v[(*pair)[i]].get<std::size_t>() == i % 10);
|
||||
CHECK(v.at((*pair)[i]).get<std::size_t>() == i % 10);
|
||||
CHECK(v.find((*pair)[i]).key() == (*pair)[i]);
|
||||
CHECK(!v.contains((*pair)[i] + "x"));
|
||||
}
|
||||
CHECK(!v.contains("missing"));
|
||||
}
|
||||
CHECK(with_collisions.root() == json::parse(colliding_text));
|
||||
|
||||
// only the object with the spread keys got a table (both texts have the
|
||||
// same length, so the tables are the only difference)
|
||||
with_collisions.shrink_to_fit();
|
||||
without_collisions.shrink_to_fit();
|
||||
CHECK(without_collisions.memory_usage() >= with_collisions.memory_usage() + (table_size * sizeof(std::uint32_t)));
|
||||
}
|
||||
|
||||
SECTION("keys that differ in their last bytes are spread over the table")
|
||||
{
|
||||
// The hash must spread the usual families of keys (numbered names, hex
|
||||
// strings, dates, short names): a key farther than index_max_displacement
|
||||
// from its home slot would leave the object without a table. The keys
|
||||
// are placed with linear probing, as the table does.
|
||||
const auto max_displacement = [](const std::vector<std::string>& keys)
|
||||
{
|
||||
std::size_t slots = 16;
|
||||
while (slots < 2 * keys.size())
|
||||
{
|
||||
slots *= 2;
|
||||
}
|
||||
std::vector<bool> used(slots, false);
|
||||
std::size_t farthest = 0;
|
||||
for (const std::string& key : keys)
|
||||
{
|
||||
std::size_t i = nlohmann::detail::conditional_static_cast<std::size_t>(nlohmann::detail::view::key_hash(key.data(), key.size())) & (slots - 1);
|
||||
std::size_t distance = 0;
|
||||
while (used[i])
|
||||
{
|
||||
i = (i + 1) & (slots - 1);
|
||||
++distance;
|
||||
}
|
||||
used[i] = true;
|
||||
farthest = (std::max)(farthest, distance);
|
||||
}
|
||||
return farthest;
|
||||
};
|
||||
// the keys of a printf-like pattern with one %d, %0Nd or %0Nx, formatted with a stream
|
||||
// (a format string that is not a literal is a Flawfinder error)
|
||||
const auto numbered = [](const char* format, int count, int step)
|
||||
{
|
||||
const std::string pattern(format);
|
||||
const std::size_t percent = pattern.find('%');
|
||||
std::size_t conversion = percent + 1;
|
||||
std::size_t width = 0;
|
||||
while (pattern[conversion] >= '0' && pattern[conversion] <= '9')
|
||||
{
|
||||
width = (width * 10) + static_cast<std::size_t>(pattern[conversion] - '0');
|
||||
++conversion;
|
||||
}
|
||||
const bool hex = pattern[conversion] == 'x';
|
||||
std::vector<std::string> keys;
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
std::ostringstream key;
|
||||
key << pattern.substr(0, percent) << std::setfill('0') << std::setw(static_cast<int>(width))
|
||||
<< (hex ? std::hex : std::dec) << (i * step) << pattern.substr(conversion + 1);
|
||||
keys.push_back(key.str());
|
||||
}
|
||||
return keys;
|
||||
};
|
||||
for (const char* format :
|
||||
{"key%d", "%d", "item_%04d", "a%05d", "%03dxyz", "id%d", "user_%d", "%d_suffix", "0x%04x", "A%07d", "12345678%d", "1234567%d", "123456789%d",
|
||||
"configuration_parameter_value_%04d_extra", "2026-01-%02dT00:00:00Z"
|
||||
})
|
||||
{
|
||||
CAPTURE(format)
|
||||
CHECK(max_displacement(numbered(format, std::string(format).find("%02d") != std::string::npos ? 60 : 1000, 1)) < nlohmann::detail::view::index_max_displacement); // NOLINT(abseil-string-find-str-contains)
|
||||
CHECK(max_displacement(numbered(format, 600, 7)) < nlohmann::detail::view::index_max_displacement);
|
||||
}
|
||||
std::mt19937_64 rng(7); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
for (const std::size_t length :
|
||||
{
|
||||
std::size_t{1}, std::size_t{2}, std::size_t{3}, std::size_t{5}, std::size_t{7}, std::size_t{8}, std::size_t{9}, std::size_t{12}, std::size_t{16}, std::size_t{17}, std::size_t{33}
|
||||
})
|
||||
{
|
||||
CAPTURE(length)
|
||||
std::set<std::string> distinct;
|
||||
// (there are 26 and 676 keys of one and two letters)
|
||||
std::size_t wanted = 700;
|
||||
if (length == 1)
|
||||
{
|
||||
wanted = 26;
|
||||
}
|
||||
else if (length == 2)
|
||||
{
|
||||
wanted = 500;
|
||||
}
|
||||
while (distinct.size() < wanted)
|
||||
{
|
||||
std::string key(length, 'a');
|
||||
for (char& c : key)
|
||||
{
|
||||
c = static_cast<char>('a' + (rng() % 26));
|
||||
}
|
||||
distinct.insert(key);
|
||||
}
|
||||
CHECK(max_displacement(std::vector<std::string>(distinct.begin(), distinct.end())) < nlohmann::detail::view::index_max_displacement);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("keys of every length that differ in one byte")
|
||||
{
|
||||
// In a large object, a key is found by comparing its bytes with those of the member the
|
||||
// table points to: up to 16 bytes as two overlapping words, longer ones with memcmp.
|
||||
// Keys of every length from 0 to 40, and keys that differ from them in one byte.
|
||||
std::mt19937_64 rng(2026); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
std::vector<std::string> keys;
|
||||
std::set<std::string> distinct;
|
||||
for (std::size_t length = 0; length <= 40; ++length)
|
||||
{
|
||||
for (int copy = 0; copy < 5; ++copy)
|
||||
{
|
||||
std::string key(length, 'a');
|
||||
for (char& c : key)
|
||||
{
|
||||
c = static_cast<char>('a' + (rng() % 26));
|
||||
}
|
||||
if (distinct.insert(key).second)
|
||||
{
|
||||
keys.push_back(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i);
|
||||
}
|
||||
text += "}";
|
||||
REQUIRE(keys.size() >= 128); // (a table is built for 128 members)
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
CAPTURE(keys[i])
|
||||
CHECK(v[keys[i]].get<std::size_t>() == i);
|
||||
CHECK(v.at(keys[i]).get<std::size_t>() == i);
|
||||
CHECK(v.contains(keys[i]));
|
||||
for (std::size_t at = 0; at < keys[i].size(); ++at)
|
||||
{
|
||||
std::string other = keys[i];
|
||||
other[at] = other[at] == 'z' ? 'y' : 'z';
|
||||
CAPTURE(other)
|
||||
CHECK(v.contains(other) == (distinct.count(other) != 0));
|
||||
}
|
||||
CHECK(v.contains(keys[i] + "a") == (distinct.count(keys[i] + "a") != 0));
|
||||
if (!keys[i].empty())
|
||||
{
|
||||
CHECK(v.contains(keys[i].substr(1)) == (distinct.count(keys[i].substr(1)) != 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("shrink_to_fit releases the tables' spare capacity")
|
||||
{
|
||||
const auto make_text = [](int objects, int members)
|
||||
{
|
||||
std::string text = "[";
|
||||
for (int object = 0; object < objects; ++object)
|
||||
{
|
||||
text += object != 0 ? ",{" : "{";
|
||||
for (int i = 0, n = members + object; i < n; ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\":" + std::to_string(i);
|
||||
}
|
||||
text += '}';
|
||||
}
|
||||
return text + "]";
|
||||
};
|
||||
const std::string small_text = make_text(5, 150);
|
||||
const std::string big_text = make_text(40, 400);
|
||||
|
||||
// reading a big text, and then a small one, leaves the spare capacity
|
||||
// of the big one: shrink_to_fit() brings the document to the size of
|
||||
// one parsed from the small text alone
|
||||
json_document d = json_document::parse(big_text);
|
||||
const std::size_t big = d.memory_usage();
|
||||
d.read(small_text);
|
||||
CHECK(d.memory_usage() >= big);
|
||||
d.shrink_to_fit();
|
||||
json_document fresh = json_document::parse(small_text);
|
||||
fresh.shrink_to_fit();
|
||||
CHECK(d.memory_usage() == fresh.memory_usage());
|
||||
CHECK(d.memory_usage() < big / 2);
|
||||
CHECK(d.root() == json::parse(small_text));
|
||||
for (int object = 0; object < 5; ++object)
|
||||
{
|
||||
const json_view v = d.root()[static_cast<std::size_t>(object)];
|
||||
for (int i = 0, n = 150 + object; i < n; ++i)
|
||||
{
|
||||
CHECK(v[std::to_string(i)].get<int>() == i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("nested, reused, and in arrays")
|
||||
{
|
||||
std::string inner = "{";
|
||||
for (int i = 0; i < 300; ++i)
|
||||
{
|
||||
inner += (i != 0 ? ",\"m" : "\"m") + std::to_string(i) + "\":" + std::to_string(i);
|
||||
}
|
||||
inner += '}';
|
||||
const std::string text = "[" + inner + ",{\"x\":" + inner + "}," + inner + "]";
|
||||
json_document d = json_document::parse(text);
|
||||
CHECK(d.root()[0]["m299"].get<int>() == 299);
|
||||
CHECK(d.root()[1]["x"]["m150"].get<int>() == 150);
|
||||
CHECK(d.root()[2]["m0"].get<int>() == 0);
|
||||
const std::size_t with_index = d.memory_usage();
|
||||
d.read(std::string("{\"small\": 1}"));
|
||||
CHECK(d.root()["small"].get<int>() == 1);
|
||||
d.read(text);
|
||||
CHECK(d.root()[2]["m7"].get<int>() == 7);
|
||||
CHECK(d.memory_usage() >= with_index / 2);
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ using nlohmann::json;
|
||||
#include <cstdint>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -249,51 +248,6 @@ TEST_CASE_TEMPLATE("serialization for extreme integer values", T, int32_t, uint3
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("integers of every digit count are written like std::to_string")
|
||||
{
|
||||
// dump_integer() writes up to 16 digits in two groups of eight and 17 to
|
||||
// 20 digits as one to four digits and two groups of eight
|
||||
std::vector<std::uint64_t> values;
|
||||
std::uint64_t power = 1;
|
||||
for (int digits = 1; digits <= 20; ++digits)
|
||||
{
|
||||
values.push_back(power); // 1 followed by zeros
|
||||
values.push_back(power + 1);
|
||||
values.push_back((power * 2) - 1);
|
||||
if (digits < 20)
|
||||
{
|
||||
values.push_back((power * 10) - 1); // all 9s
|
||||
values.push_back((power * 10) - power); // 9 followed by zeros
|
||||
power *= 10;
|
||||
}
|
||||
}
|
||||
values.push_back((std::numeric_limits<std::uint64_t>::max)());
|
||||
values.push_back(static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()));
|
||||
values.push_back(static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()) + 1);
|
||||
std::uint64_t state = 0x9E3779B97F4A7C15u;
|
||||
for (int i = 0; i < 20000; ++i)
|
||||
{
|
||||
state = (state * 6364136223846793005u) + 1442695040888963407u;
|
||||
values.push_back(state >> (state % 61)); // every bit length
|
||||
}
|
||||
|
||||
for (const auto v : values)
|
||||
{
|
||||
CAPTURE(v)
|
||||
CHECK(json(v).dump() == std::to_string(v));
|
||||
if (v <= static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()))
|
||||
{
|
||||
const auto s = static_cast<std::int64_t>(v);
|
||||
CHECK(json(s).dump() == std::to_string(s));
|
||||
CHECK(json(-s).dump() == std::to_string(-s));
|
||||
}
|
||||
}
|
||||
CHECK(json((std::numeric_limits<std::int64_t>::min)()).dump() == "-9223372036854775808");
|
||||
|
||||
// in a container, so the digits are followed by other output
|
||||
CHECK(json::array({std::uint64_t{18446744073709551615u}, std::int64_t{-1234567890123456789}, 1}).dump() == "[18446744073709551615,-1234567890123456789,1]");
|
||||
}
|
||||
|
||||
TEST_CASE("dump with binary values")
|
||||
{
|
||||
auto binary = json::binary({1, 2, 3, 4});
|
||||
|
||||
Reference in new issue
Block a user