Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27bb251d17 | ||
|
|
2301f01284 | ||
|
|
056ba3b7c1 | ||
|
|
d7b9472173 | ||
|
|
04a5accfb9 | ||
|
|
ddc34f546a | ||
|
|
f022e3c59c | ||
|
|
9742edc294 | ||
|
|
517444b504 | ||
|
|
fb515ddc34 | ||
|
|
edf8d17601 | ||
|
|
aecbef3949 | ||
|
|
6c3ab966d3 | ||
|
|
5337d841d4 | ||
|
|
162eda2574 | ||
|
|
1a018577cc | ||
|
|
76001c6b4c | ||
|
|
ec6986c918 | ||
|
|
3fd3e0fdaa | ||
|
|
9c349b1d7c | ||
|
|
367963f6bb | ||
|
|
4fe670b5f4 | ||
|
|
755bb16d7a |
No files matched your search
@@ -154,15 +154,6 @@ and each page's title (H1) and declaration by
|
||||
[`docs/docset/generate_docset.py`](https://github.com/nlohmann/json/blob/develop/docs/docset/generate_docset.py);
|
||||
`make build` reports API pages that cannot be classified.
|
||||
|
||||
A pull request that changes only the documentation, Markdown files, GitHub metadata, or license files skips the
|
||||
compiler workflows (Ubuntu, macOS, Windows); the documentation and license checks still run. See
|
||||
[`tools/check_path_filters`](https://github.com/nlohmann/json/tree/develop/tools/check_path_filters) for the list of
|
||||
skipped files and what to do when a compiler job starts reading one of them.
|
||||
|
||||
Most compiler jobs run only once a maintainer adds the label "🚀 ready to merge" to a pull request; until then, a
|
||||
smaller set of jobs runs on every push. See
|
||||
[How CI runs](https://json.nlohmann.me/community/quality_assurance/#how-ci-runs) for the sets and when they run.
|
||||
|
||||
### Amalgamate the source code
|
||||
|
||||
The single-header files
|
||||
|
||||
@@ -7,25 +7,6 @@ on:
|
||||
- master
|
||||
- release/*
|
||||
pull_request:
|
||||
# Skip pull requests that change only files no job here reads. The list is checked by
|
||||
# tools/check_path_filters/check_path_filters.py (repository_checks.yml); change both together.
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '!**.md'
|
||||
- '!.github/*'
|
||||
- '!.github/ISSUE_TEMPLATE/**'
|
||||
- '!.github/external_ci/**'
|
||||
- '!.github/workflows/**'
|
||||
- '!.reuse/**'
|
||||
- '!LICENSES/**'
|
||||
- '!CITATION.cff'
|
||||
- '!REUSE.toml'
|
||||
# read by tools/check_build_options in the ci_meson_install job (ubuntu.yml)
|
||||
- 'docs/mkdocs/docs/integration/package_managers.md'
|
||||
# built by the swiftpm job (macos.yml)
|
||||
- 'docs/mkdocs/docs/integration/swift/**'
|
||||
- '.github/workflows/macos.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
name: Repository checks
|
||||
|
||||
# Checks that concern files the compiler workflows (ubuntu.yml, macos.yml,
|
||||
# windows.yml) skip with their path filter: the documentation, the license
|
||||
# information, and the CI configuration itself. This workflow has no path filter.
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- master
|
||||
- release/*
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check_ci_configuration:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check the path filters of the compiler workflows
|
||||
run: python3 tools/check_path_filters/check_path_filters.py
|
||||
- name: Check the job sets of the compiler workflows and their documentation
|
||||
run: |
|
||||
python3 -m venv venv
|
||||
venv/bin/pip install --quiet -r tools/ci_jobs/requirements.txt
|
||||
venv/bin/python tools/ci_jobs/ci_jobs.py
|
||||
|
||||
ci_reuse_compliance:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Get latest CMake and ninja
|
||||
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
|
||||
- name: Run CMake
|
||||
run: cmake -S . -B build -DJSON_CI=On
|
||||
- name: Build
|
||||
run: cmake --build build --target ci_reuse_compliance
|
||||
|
||||
ci_test_documentation:
|
||||
runs-on: ubuntu-26.04
|
||||
strategy:
|
||||
matrix:
|
||||
target: [ci_test_examples, ci_test_build_documentation, ci_test_documentation_mermaid]
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
# the git-revision-date-localized plugin needs the history; a shallow clone makes the strict build fail
|
||||
fetch-depth: ${{ matrix.target == 'ci_test_build_documentation' && '0' || '1' }}
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
if: matrix.target == 'ci_test_documentation_mermaid'
|
||||
with:
|
||||
node-version: 24
|
||||
- name: Run CMake
|
||||
run: cmake -S . -B build -DJSON_CI=On
|
||||
- name: Build
|
||||
run: cmake --build build --target ${{ matrix.target }}
|
||||
+262
-422
@@ -7,62 +7,17 @@ on:
|
||||
- master
|
||||
- release/*
|
||||
pull_request:
|
||||
# labeled: the "🚀 ready to merge" label starts the full set (see below)
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
# Skip pull requests that change only files no job here reads. The list is checked by
|
||||
# tools/check_path_filters/check_path_filters.py (repository_checks.yml); change both together.
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '!**.md'
|
||||
- '!.github/*'
|
||||
- '!.github/ISSUE_TEMPLATE/**'
|
||||
- '!.github/external_ci/**'
|
||||
- '!.github/workflows/**'
|
||||
- '!.reuse/**'
|
||||
- '!LICENSES/**'
|
||||
- '!CITATION.cff'
|
||||
- '!REUSE.toml'
|
||||
# read by tools/check_build_options in the ci_meson_install job (ubuntu.yml)
|
||||
- 'docs/mkdocs/docs/integration/package_managers.md'
|
||||
# built by the swiftpm job (macos.yml)
|
||||
- 'docs/mkdocs/docs/integration/swift/**'
|
||||
- '.github/workflows/ubuntu.yml'
|
||||
schedule:
|
||||
# the nightly set (see below), on develop
|
||||
- cron: '0 2 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# A run triggered by any other label gets a group of its own, so it does not
|
||||
# cancel the run of the last push; its jobs are skipped.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'schedule' && 'nightly' || github.ref || github.run_id }}${{ github.event.action == 'labeled' && github.event.label.name != '🚀 ready to merge' && format('-{0}', github.run_id) || '' }}
|
||||
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# The jobs are split into three sets (see "How CI runs" in
|
||||
# docs/mkdocs/docs/community/quality_assurance.md):
|
||||
#
|
||||
# - PR set: every pull request push and every push; chosen because these jobs
|
||||
# caught 92 % of the failing commits in Sep/Oct 2026.
|
||||
# - full set: after the PR set passed, on pushes and on pull requests with the
|
||||
# "🚀 ready to merge" label; every remaining compiler, version and standard.
|
||||
# - nightly set: slow tools and rare compilers, nightly on develop, on pushes
|
||||
# to master and release/*, and when started by hand.
|
||||
#
|
||||
# A matrix whose entries belong to different sets is split into one job per
|
||||
# set; the jobs share their steps through YAML anchors. tools/ci_jobs checks
|
||||
# that every job belongs to exactly one set.
|
||||
|
||||
jobs:
|
||||
###########################################################################
|
||||
# PR set
|
||||
###########################################################################
|
||||
|
||||
ci_test_gcc:
|
||||
if: &pr_set ${{ github.event_name != 'schedule' && (github.event.action != 'labeled' || github.event.label.name == '🚀 ready to merge') }}
|
||||
runs-on: ubuntu-latest
|
||||
container: gcc:latest
|
||||
steps:
|
||||
@@ -77,7 +32,6 @@ jobs:
|
||||
run: cmake --build build --target ci_test_gcc
|
||||
|
||||
ci_meson_install:
|
||||
if: *pr_set
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -122,342 +76,7 @@ jobs:
|
||||
cmake -S tests/cmake_import/project -B build-import-split -DCMAKE_PREFIX_PATH=${{ github.workspace }}/install-split
|
||||
cmake --build build-import-split
|
||||
|
||||
ci_static_analysis_ubuntu:
|
||||
if: *pr_set
|
||||
runs-on: ubuntu-26.04
|
||||
strategy:
|
||||
matrix:
|
||||
target: [ci_cpplint]
|
||||
steps: &static_analysis_ubuntu_steps
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Install Valgrind
|
||||
if: matrix.target == 'ci_test_valgrind'
|
||||
run: sudo apt-get update ; sudo apt-get install -y valgrind
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Get latest CMake and ninja
|
||||
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
|
||||
- name: Run CMake
|
||||
run: cmake -S . -B build -DJSON_CI=On
|
||||
- name: Build
|
||||
run: cmake --build build --target ${{ matrix.target }}
|
||||
|
||||
ci_static_analysis_clang:
|
||||
if: *pr_set
|
||||
runs-on: ubuntu-latest
|
||||
container: silkeh/clang:22
|
||||
strategy:
|
||||
matrix:
|
||||
target: [ci_test_clang, ci_clang_tidy, ci_test_clang_sanitizer, ci_single_binaries]
|
||||
steps: &static_analysis_clang_steps
|
||||
- name: Install git, clang-tools, iwyu (ci_single_binaries), and unzip
|
||||
run: apt-get update ; apt-get install -y git clang-tools iwyu unzip
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Get latest CMake and ninja
|
||||
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
|
||||
- name: Run CMake
|
||||
run: cmake -S . -B build -DJSON_CI=On
|
||||
- name: Build
|
||||
run: cmake --build build --target ${{ matrix.target }}
|
||||
|
||||
ci_cmake_options:
|
||||
if: *pr_set
|
||||
runs-on: ubuntu-latest
|
||||
container: ubuntu:24.04
|
||||
strategy:
|
||||
matrix:
|
||||
target: [ci_test_diagnostics, ci_test_diagnostic_positions, ci_test_noexceptions, ci_test_noglobaludls]
|
||||
steps: &cmake_options_steps
|
||||
- name: Install build-essential
|
||||
run: apt-get update ; apt-get install -y build-essential unzip wget git
|
||||
# the CMake 3.5.0 release binary that ci_cmake_flags downloads links
|
||||
# against libidn.so.11, which Ubuntu 24.04 no longer ships
|
||||
- name: Install libidn11 for CMake 3.5.0
|
||||
if: matrix.target == 'ci_cmake_flags'
|
||||
run: |
|
||||
wget -q http://archive.ubuntu.com/ubuntu/pool/main/libi/libidn/libidn11_1.33-2.2ubuntu2_amd64.deb
|
||||
dpkg -i libidn11_1.33-2.2ubuntu2_amd64.deb
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Get latest CMake and ninja
|
||||
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
|
||||
- name: Run CMake
|
||||
run: cmake -S . -B build -DJSON_CI=On
|
||||
- name: Build
|
||||
run: cmake --build build --target ${{ matrix.target }}
|
||||
|
||||
ci_test_compilers_gcc_old:
|
||||
if: *pr_set
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
compiler: ['4.8']
|
||||
# official gcc:4.8/4.9/5/6 images fail to check out code (too old for
|
||||
# actions/checkout); install the old compilers on top of official ubuntu:20.04
|
||||
# instead, mirroring what the (now retired) custom json-ci image did.
|
||||
container: ubuntu:20.04
|
||||
steps: &compilers_gcc_old_steps
|
||||
- name: Install g++-${{ matrix.compiler }}
|
||||
run: |
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends software-properties-common ca-certificates gnupg make git
|
||||
# add-apt-repository resolves the PPA through the Launchpad API,
|
||||
# which intermittently times out or fails the team lookup (the plain
|
||||
# "deb ..." sources below never hit Launchpad and never flake).
|
||||
# Retry with backoff so a transient Launchpad blip does not fail CI.
|
||||
for attempt in 1 2 3 4 5; do
|
||||
add-apt-repository -y ppa:ubuntu-toolchain-r/test && break
|
||||
echo "::warning::add-apt-repository ppa:ubuntu-toolchain-r/test failed (attempt ${attempt}/5); retrying"
|
||||
sleep $((attempt * 10))
|
||||
done
|
||||
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ bionic main"
|
||||
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ bionic universe"
|
||||
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial main"
|
||||
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial universe"
|
||||
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial-updates main"
|
||||
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial-updates universe"
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends g++-${{ matrix.compiler }}
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Get latest CMake and ninja
|
||||
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
|
||||
- name: Run CMake
|
||||
run: CXX=g++-${{ matrix.compiler }} cmake -S . -B build -DJSON_CI=On
|
||||
- name: Build
|
||||
run: cmake --build build --target ci_test_compiler_g++-${{ matrix.compiler }}
|
||||
|
||||
ci_test_compilers_clang:
|
||||
if: *pr_set
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
compiler: ['3.4']
|
||||
container: silkeh/clang:${{ matrix.compiler }}
|
||||
steps: &compilers_clang_steps
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Get latest CMake and ninja
|
||||
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
|
||||
- name: Disable filesystem support (std::filesystem is unusable) for clang 7 / 8 / 9 / 10
|
||||
run: echo "JSON_FORCED_GLOBAL_COMPILE_OPTIONS=-DJSON_HAS_FILESYSTEM=0;-DJSON_HAS_EXPERIMENTAL_FILESYSTEM=0" >> "$GITHUB_ENV"
|
||||
if: ${{ matrix.compiler == '7' || matrix.compiler == '8' || matrix.compiler == '9' || matrix.compiler == '10' }}
|
||||
- name: Run CMake
|
||||
run: cmake -S . -B build -DJSON_CI=On
|
||||
- name: Build
|
||||
run: cmake --build build --target ci_test_compiler_default
|
||||
|
||||
ci_test_standards_gcc:
|
||||
if: *pr_set
|
||||
runs-on: ubuntu-latest
|
||||
container: gcc:latest
|
||||
strategy:
|
||||
matrix:
|
||||
standard: [26]
|
||||
steps: &standards_gcc_steps
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Get latest CMake and ninja
|
||||
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
|
||||
- name: Run CMake
|
||||
run: cmake -S . -B build -DJSON_CI=On
|
||||
- name: Build
|
||||
run: cmake --build build --target ci_test_gcc_cxx${{ matrix.standard }}
|
||||
|
||||
ci_test_standards_clang:
|
||||
if: *pr_set
|
||||
runs-on: ubuntu-latest
|
||||
container: silkeh/clang:latest
|
||||
strategy:
|
||||
matrix:
|
||||
standard: [26]
|
||||
stdlib: [libcxx]
|
||||
steps: &standards_clang_steps
|
||||
- name: Install git and unzip
|
||||
run: apt-get update ; apt-get install -y git unzip
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Get latest CMake and ninja
|
||||
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
|
||||
- name: Run CMake
|
||||
run: cmake -S . -B build -DJSON_CI=On
|
||||
- name: Build with libc++
|
||||
run: cmake --build build --target ci_test_clang_libcxx_cxx${{ matrix.standard }}
|
||||
if: ${{ matrix.stdlib == 'libcxx' }}
|
||||
- name: Build with libstdc++
|
||||
run: cmake --build build --target ci_test_clang_cxx${{ matrix.standard }}
|
||||
if: ${{ matrix.stdlib == 'libstdcxx' }}
|
||||
|
||||
ci_emscripten:
|
||||
if: *pr_set
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Install emscripten
|
||||
uses: mymindstorm/setup-emsdk@4528d102f7230f0e7b276855c01ea1159be0e984 # v16
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Get latest CMake and ninja
|
||||
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
|
||||
- name: Run CMake
|
||||
run: cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake -GNinja
|
||||
- name: Build
|
||||
run: cmake --build build
|
||||
|
||||
###########################################################################
|
||||
# full set: starts after the PR set passed
|
||||
###########################################################################
|
||||
|
||||
ci_static_analysis_ubuntu_full:
|
||||
needs: &pr_set_jobs [ci_test_gcc, ci_meson_install, ci_static_analysis_ubuntu, ci_static_analysis_clang, ci_cmake_options, ci_test_compilers_gcc_old, ci_test_compilers_clang, ci_test_standards_gcc, ci_test_standards_clang, ci_emscripten]
|
||||
if: &full_set ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, '🚀 ready to merge') }}
|
||||
runs-on: ubuntu-26.04
|
||||
strategy:
|
||||
matrix:
|
||||
target: [ci_test_single_header, ci_cppcheck, ci_reproducible_tests, ci_non_git_tests, ci_offline_testdata]
|
||||
steps: *static_analysis_ubuntu_steps
|
||||
|
||||
ci_static_analysis_clang_full:
|
||||
needs: *pr_set_jobs
|
||||
if: *full_set
|
||||
runs-on: ubuntu-latest
|
||||
container: silkeh/clang:22
|
||||
strategy:
|
||||
matrix:
|
||||
target: [ci_clang_analyze]
|
||||
steps: *static_analysis_clang_steps
|
||||
|
||||
ci_cmake_options_full:
|
||||
needs: *pr_set_jobs
|
||||
if: *full_set
|
||||
runs-on: ubuntu-latest
|
||||
container: ubuntu:24.04
|
||||
strategy:
|
||||
matrix:
|
||||
target: [ci_cmake_flags, ci_test_noimplicitconversions, ci_test_legacycomparison, ci_test_disableenumserialization, ci_test_disabletuplereferenceconversion, ci_test_skiplibraryversioncheck, ci_test_simdutf, ci_test_strict_nul_handling, ci_test_delete_deprecated_functions, ci_test_no_thread_local]
|
||||
steps: *cmake_options_steps
|
||||
|
||||
ci_test_compilers_gcc_old_full:
|
||||
needs: *pr_set_jobs
|
||||
if: *full_set
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
compiler: ['4.9', '5', '6']
|
||||
container: ubuntu:20.04
|
||||
steps: *compilers_gcc_old_steps
|
||||
|
||||
ci_test_compilers_gcc_full:
|
||||
needs: *pr_set_jobs
|
||||
if: *full_set
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
# older GCC docker images (4, 5, 6) fail to check out code
|
||||
compiler: ['7', '8', '9', '10', '11', '12', '13', '14', '15', '16', 'latest']
|
||||
container: gcc:${{ matrix.compiler }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Get latest CMake and ninja
|
||||
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
|
||||
- name: Run CMake
|
||||
run: cmake -S . -B build -DJSON_CI=On
|
||||
- name: Build
|
||||
run: cmake --build build --target ci_test_compiler_default
|
||||
|
||||
ci_test_compilers_clang_full:
|
||||
needs: *pr_set_jobs
|
||||
if: *full_set
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
compiler: ['3.5', '3.6', '3.7', '3.8', '3.9', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15-bullseye', '16', '17', '18', '19', '20', '21', '22', 'latest']
|
||||
container: silkeh/clang:${{ matrix.compiler }}
|
||||
steps: *compilers_clang_steps
|
||||
|
||||
ci_test_standards_gcc_full:
|
||||
needs: *pr_set_jobs
|
||||
if: *full_set
|
||||
runs-on: ubuntu-latest
|
||||
container: gcc:latest
|
||||
strategy:
|
||||
matrix:
|
||||
standard: [11, 14, 17, 20, 23]
|
||||
steps: *standards_gcc_steps
|
||||
|
||||
ci_test_standards_clang_full:
|
||||
needs: *pr_set_jobs
|
||||
if: *full_set
|
||||
runs-on: ubuntu-latest
|
||||
container: silkeh/clang:latest
|
||||
strategy:
|
||||
matrix:
|
||||
standard: [11, 14, 17, 20, 23, 26]
|
||||
stdlib: [libcxx, libstdcxx]
|
||||
exclude:
|
||||
# in the PR set
|
||||
- standard: 26
|
||||
stdlib: libcxx
|
||||
steps: *standards_clang_steps
|
||||
|
||||
ci_module_cpp20:
|
||||
needs: *pr_set_jobs
|
||||
if: *full_set
|
||||
strategy:
|
||||
matrix:
|
||||
container: ['gcc:latest', 'silkeh/clang:latest']
|
||||
runs-on: ubuntu-latest
|
||||
container: ${{ matrix.container }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
# The module test uses `import std;`, which needs CMake's experimental
|
||||
# import-std support. Its opt-in token is CMake-version-specific, so pin
|
||||
# CMake to the version whose token is set in tests/module_cpp20/CMakeLists.txt.
|
||||
- name: Get pinned CMake and ninja
|
||||
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
|
||||
with:
|
||||
cmakeVersion: 4.3.4
|
||||
# Clang: the std library module is provided by libc++ (the image's libstdc++
|
||||
# ships none), and the image's libc++ module manifest has a broken relative
|
||||
# path — repoint it at the real module sources.
|
||||
- name: Use libc++ and fix its module manifest path (Clang)
|
||||
if: matrix.container == 'silkeh/clang:latest'
|
||||
run: |
|
||||
echo "CXXFLAGS=-stdlib=libc++" >> "$GITHUB_ENV"
|
||||
mkdir -p /usr/lib/share && ln -sf /usr/lib/llvm-*/share/libc++ /usr/lib/share/libc++
|
||||
- name: Run CMake
|
||||
run: cmake -S . -B build -DJSON_CI=On
|
||||
- name: Build
|
||||
run: cmake --build build --target ci_module_cpp20
|
||||
|
||||
###########################################################################
|
||||
# nightly set
|
||||
###########################################################################
|
||||
|
||||
ci_infer:
|
||||
if: &nightly_set ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref != 'refs/heads/develop') }}
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
@@ -486,16 +105,76 @@ jobs:
|
||||
path: ${{ github.workspace }}/build/build_infer/infer-out/report.txt
|
||||
if-no-files-found: ignore
|
||||
|
||||
ci_static_analysis_ubuntu_nightly:
|
||||
if: *nightly_set
|
||||
ci_static_analysis_ubuntu:
|
||||
runs-on: ubuntu-26.04
|
||||
strategy:
|
||||
matrix:
|
||||
target: [ci_test_valgrind]
|
||||
steps: *static_analysis_ubuntu_steps
|
||||
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@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Install Valgrind
|
||||
if: matrix.target == 'ci_test_valgrind'
|
||||
run: sudo apt-get update ; sudo apt-get install -y valgrind
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Get latest CMake and ninja
|
||||
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
|
||||
- name: Run CMake
|
||||
run: cmake -S . -B build -DJSON_CI=On
|
||||
- name: Build
|
||||
run: cmake --build build --target ${{ matrix.target }}
|
||||
|
||||
ci_static_analysis_clang:
|
||||
runs-on: ubuntu-latest
|
||||
container: silkeh/clang:22
|
||||
strategy:
|
||||
matrix:
|
||||
target: [ci_test_clang, ci_clang_tidy, ci_test_clang_sanitizer, ci_clang_analyze, ci_single_binaries]
|
||||
steps:
|
||||
- name: Install git, clang-tools, iwyu (ci_single_binaries), and unzip
|
||||
run: apt-get update ; apt-get install -y git clang-tools iwyu unzip
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Get latest CMake and ninja
|
||||
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
|
||||
- name: Run CMake
|
||||
run: cmake -S . -B build -DJSON_CI=On
|
||||
- name: Build
|
||||
run: cmake --build build --target ${{ matrix.target }}
|
||||
|
||||
ci_cmake_options:
|
||||
runs-on: ubuntu-latest
|
||||
container: ubuntu:24.04
|
||||
strategy:
|
||||
matrix:
|
||||
target: [ci_cmake_flags, ci_test_diagnostics, ci_test_diagnostic_positions, ci_test_noexceptions, ci_test_noimplicitconversions, ci_test_legacycomparison, ci_test_noglobaludls, ci_test_disableenumserialization, ci_test_disabletuplereferenceconversion, ci_test_skiplibraryversioncheck, ci_test_simdutf, ci_test_strict_nul_handling, ci_test_delete_deprecated_functions, ci_test_no_thread_local]
|
||||
steps:
|
||||
- name: Install build-essential
|
||||
run: apt-get update ; apt-get install -y build-essential unzip wget git
|
||||
# the CMake 3.5.0 release binary that ci_cmake_flags downloads links
|
||||
# against libidn.so.11, which Ubuntu 24.04 no longer ships
|
||||
- name: Install libidn11 for CMake 3.5.0
|
||||
if: matrix.target == 'ci_cmake_flags'
|
||||
run: |
|
||||
wget -q http://archive.ubuntu.com/ubuntu/pool/main/libi/libidn/libidn11_1.33-2.2ubuntu2_amd64.deb
|
||||
dpkg -i libidn11_1.33-2.2ubuntu2_amd64.deb
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Get latest CMake and ninja
|
||||
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
|
||||
- name: Run CMake
|
||||
run: cmake -S . -B build -DJSON_CI=On
|
||||
- name: Build
|
||||
run: cmake --build build --target ${{ matrix.target }}
|
||||
|
||||
ci_test_coverage:
|
||||
if: *nightly_set
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
@@ -529,8 +208,128 @@ jobs:
|
||||
path-to-lcov: ${{ github.workspace }}/build/json.info.filtered.noexcept
|
||||
fail-on-error: false
|
||||
|
||||
ci_test_compilers_gcc_old:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
compiler: ['4.8', '4.9', '5', '6']
|
||||
# official gcc:4.8/4.9/5/6 images fail to check out code (too old for
|
||||
# actions/checkout); install the old compilers on top of official ubuntu:20.04
|
||||
# instead, mirroring what the (now retired) custom json-ci image did.
|
||||
container: ubuntu:20.04
|
||||
steps:
|
||||
- name: Install g++-${{ matrix.compiler }}
|
||||
run: |
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends software-properties-common ca-certificates gnupg make git
|
||||
# add-apt-repository resolves the PPA through the Launchpad API,
|
||||
# which intermittently times out or fails the team lookup (the plain
|
||||
# "deb ..." sources below never hit Launchpad and never flake).
|
||||
# Retry with backoff so a transient Launchpad blip does not fail CI.
|
||||
for attempt in 1 2 3 4 5; do
|
||||
add-apt-repository -y ppa:ubuntu-toolchain-r/test && break
|
||||
echo "::warning::add-apt-repository ppa:ubuntu-toolchain-r/test failed (attempt ${attempt}/5); retrying"
|
||||
sleep $((attempt * 10))
|
||||
done
|
||||
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ bionic main"
|
||||
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ bionic universe"
|
||||
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial main"
|
||||
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial universe"
|
||||
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial-updates main"
|
||||
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial-updates universe"
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends g++-${{ matrix.compiler }}
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Get latest CMake and ninja
|
||||
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
|
||||
- name: Run CMake
|
||||
run: CXX=g++-${{ matrix.compiler }} cmake -S . -B build -DJSON_CI=On
|
||||
- name: Build
|
||||
run: cmake --build build --target ci_test_compiler_g++-${{ matrix.compiler }}
|
||||
|
||||
ci_test_compilers_gcc:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
# older GCC docker images (4, 5, 6) fail to check out code
|
||||
compiler: ['7', '8', '9', '10', '11', '12', '13', '14', '15', '16', 'latest']
|
||||
container: gcc:${{ matrix.compiler }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Get latest CMake and ninja
|
||||
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
|
||||
- name: Run CMake
|
||||
run: cmake -S . -B build -DJSON_CI=On
|
||||
- name: Build
|
||||
run: cmake --build build --target ci_test_compiler_default
|
||||
|
||||
ci_test_compilers_clang:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
compiler: ['3.4', '3.5', '3.6', '3.7', '3.8', '3.9', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15-bullseye', '16', '17', '18', '19', '20', '21', '22', 'latest']
|
||||
container: silkeh/clang:${{ matrix.compiler }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Get latest CMake and ninja
|
||||
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
|
||||
- name: Disable filesystem support (std::filesystem is unusable) for clang 7 / 8 / 9 / 10
|
||||
run: echo "JSON_FORCED_GLOBAL_COMPILE_OPTIONS=-DJSON_HAS_FILESYSTEM=0;-DJSON_HAS_EXPERIMENTAL_FILESYSTEM=0" >> "$GITHUB_ENV"
|
||||
if: ${{ matrix.compiler == '7' || matrix.compiler == '8' || matrix.compiler == '9' || matrix.compiler == '10' }}
|
||||
- name: Run CMake
|
||||
run: cmake -S . -B build -DJSON_CI=On
|
||||
- name: Build
|
||||
run: cmake --build build --target ci_test_compiler_default
|
||||
|
||||
ci_test_standards_gcc:
|
||||
runs-on: ubuntu-latest
|
||||
container: gcc:latest
|
||||
strategy:
|
||||
matrix:
|
||||
standard: [11, 14, 17, 20, 23, 26]
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Get latest CMake and ninja
|
||||
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
|
||||
- name: Run CMake
|
||||
run: cmake -S . -B build -DJSON_CI=On
|
||||
- name: Build
|
||||
run: cmake --build build --target ci_test_gcc_cxx${{ matrix.standard }}
|
||||
|
||||
ci_test_standards_clang:
|
||||
runs-on: ubuntu-latest
|
||||
container: silkeh/clang:latest
|
||||
strategy:
|
||||
matrix:
|
||||
standard: [11, 14, 17, 20, 23, 26]
|
||||
stdlib: [libcxx, libstdcxx]
|
||||
steps:
|
||||
- name: Install git and unzip
|
||||
run: apt-get update ; apt-get install -y git unzip
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Get latest CMake and ninja
|
||||
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
|
||||
- name: Run CMake
|
||||
run: cmake -S . -B build -DJSON_CI=On
|
||||
- name: Build with libc++
|
||||
run: cmake --build build --target ci_test_clang_libcxx_cxx${{ matrix.standard }}
|
||||
if: ${{ matrix.stdlib == 'libcxx' }}
|
||||
- name: Build with libstdc++
|
||||
run: cmake --build build --target ci_test_clang_cxx${{ matrix.standard }}
|
||||
if: ${{ matrix.stdlib == 'libstdcxx' }}
|
||||
|
||||
ci_cuda_example:
|
||||
if: *nightly_set
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -553,8 +352,37 @@ jobs:
|
||||
- name: Build
|
||||
run: cmake --build build --target ci_cuda_example
|
||||
|
||||
ci_module_cpp20:
|
||||
strategy:
|
||||
matrix:
|
||||
container: ['gcc:latest', 'silkeh/clang:latest']
|
||||
runs-on: ubuntu-latest
|
||||
container: ${{ matrix.container }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
# The module test uses `import std;`, which needs CMake's experimental
|
||||
# import-std support. Its opt-in token is CMake-version-specific, so pin
|
||||
# CMake to the version whose token is set in tests/module_cpp20/CMakeLists.txt.
|
||||
- name: Get pinned CMake and ninja
|
||||
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
|
||||
with:
|
||||
cmakeVersion: 4.3.4
|
||||
# Clang: the std library module is provided by libc++ (the image's libstdc++
|
||||
# ships none), and the image's libc++ module manifest has a broken relative
|
||||
# path — repoint it at the real module sources.
|
||||
- name: Use libc++ and fix its module manifest path (Clang)
|
||||
if: matrix.container == 'silkeh/clang:latest'
|
||||
run: |
|
||||
echo "CXXFLAGS=-stdlib=libc++" >> "$GITHUB_ENV"
|
||||
mkdir -p /usr/lib/share && ln -sf /usr/lib/llvm-*/share/libc++ /usr/lib/share/libc++
|
||||
- name: Run CMake
|
||||
run: cmake -S . -B build -DJSON_CI=On
|
||||
- name: Build
|
||||
run: cmake --build build --target ci_module_cpp20
|
||||
|
||||
ci_icpc:
|
||||
if: *nightly_set
|
||||
runs-on: ubuntu-latest
|
||||
# Intel discontinued the classic icc/icpc compiler in oneAPI 2024.0; this is
|
||||
# Intel's own last officially published image that still includes it.
|
||||
@@ -574,7 +402,6 @@ jobs:
|
||||
run: cmake --build build --target ci_icpc
|
||||
|
||||
ci_icpx:
|
||||
if: *nightly_set
|
||||
runs-on: ubuntu-latest
|
||||
container: intel/oneapi-hpckit:latest
|
||||
steps:
|
||||
@@ -589,7 +416,6 @@ jobs:
|
||||
run: cmake --build build --target ci_icpx
|
||||
|
||||
ci_nvhpc:
|
||||
if: *nightly_set
|
||||
runs-on: ubuntu-latest
|
||||
container: nvcr.io/nvidia/nvhpc:25.5-devel-cuda12.9-ubuntu22.04
|
||||
steps:
|
||||
@@ -603,10 +429,54 @@ jobs:
|
||||
- name: Build
|
||||
run: cmake --build build --target ci_nvhpc
|
||||
|
||||
ci_emscripten:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Install emscripten
|
||||
uses: mymindstorm/setup-emsdk@4528d102f7230f0e7b276855c01ea1159be0e984 # v16
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Get latest CMake and ninja
|
||||
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
|
||||
- name: Run CMake
|
||||
run: cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake -GNinja
|
||||
- name: Build
|
||||
run: cmake --build build
|
||||
|
||||
ci_test_documentation:
|
||||
runs-on: ubuntu-26.04
|
||||
strategy:
|
||||
matrix:
|
||||
target: [ci_test_examples, ci_test_build_documentation, ci_test_documentation_mermaid]
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
# the git-revision-date-localized plugin needs the history; a shallow clone makes the strict build fail
|
||||
fetch-depth: ${{ matrix.target == 'ci_test_build_documentation' && '0' || '1' }}
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
if: matrix.target == 'ci_test_documentation_mermaid'
|
||||
with:
|
||||
node-version: 24
|
||||
- name: Run CMake
|
||||
run: cmake -S . -B build -DJSON_CI=On
|
||||
- name: Build
|
||||
run: cmake --build build --target ${{ matrix.target }}
|
||||
|
||||
# Native Linux ARM64 runner with the latest GCC. The ubuntu-24.04-arm label is
|
||||
# only available for public repositories.
|
||||
ci_test_arm64:
|
||||
if: *nightly_set
|
||||
runs-on: ubuntu-24.04-arm
|
||||
container: gcc:latest
|
||||
steps:
|
||||
@@ -621,33 +491,3 @@ jobs:
|
||||
run: cmake -S . -B build -DJSON_CI=On
|
||||
- name: Build
|
||||
run: cmake --build build --target ci_test_compiler_default
|
||||
|
||||
# A failure of a scheduled run opens an issue, or comments on the open one.
|
||||
nightly_report:
|
||||
needs: [ci_infer, ci_static_analysis_ubuntu_nightly, ci_test_coverage, ci_cuda_example, ci_icpc, ci_icpx, ci_nvhpc, ci_test_arm64]
|
||||
if: ${{ always() && github.event_name == 'schedule' && contains(needs.*.result, 'failure') }}
|
||||
runs-on: ubuntu-26.04
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Open or update the nightly failure issue
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
WORKFLOW: ${{ github.workflow }}
|
||||
SHA: ${{ github.sha }}
|
||||
run: |
|
||||
title="Nightly CI failure (${WORKFLOW})"
|
||||
body="The nightly run of ${WORKFLOW} on ${SHA:0:10} failed: ${RUN_URL}"
|
||||
number=$(gh issue list --state open --search "in:title \"${title}\"" --json number,title --jq "map(select(.title == \"${title}\")) | .[0].number // empty")
|
||||
if [ -n "$number" ]; then
|
||||
gh issue comment "$number" --body "$body"
|
||||
else
|
||||
gh issue create --title "$title" --body "$body"
|
||||
fi
|
||||
@@ -7,25 +7,6 @@ on:
|
||||
- master
|
||||
- release/*
|
||||
pull_request:
|
||||
# Skip pull requests that change only files no job here reads. The list is checked by
|
||||
# tools/check_path_filters/check_path_filters.py (repository_checks.yml); change both together.
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '!**.md'
|
||||
- '!.github/*'
|
||||
- '!.github/ISSUE_TEMPLATE/**'
|
||||
- '!.github/external_ci/**'
|
||||
- '!.github/workflows/**'
|
||||
- '!.reuse/**'
|
||||
- '!LICENSES/**'
|
||||
- '!CITATION.cff'
|
||||
- '!REUSE.toml'
|
||||
# read by tools/check_build_options in the ci_meson_install job (ubuntu.yml)
|
||||
- 'docs/mkdocs/docs/integration/package_managers.md'
|
||||
# built by the swiftpm job (macos.yml)
|
||||
- 'docs/mkdocs/docs/integration/swift/**'
|
||||
- '.github/workflows/windows.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
+3
-3
@@ -348,20 +348,20 @@ add_custom_target(ci_test_no_thread_local
|
||||
|
||||
add_custom_target(ci_test_coverage
|
||||
COMMAND CXX=g++ ${CMAKE_COMMAND}
|
||||
-DCMAKE_BUILD_TYPE=Debug -GNinja -DCMAKE_CXX_FLAGS="--coverage;-fprofile-arcs;-ftest-coverage"
|
||||
-DCMAKE_BUILD_TYPE=Debug -GNinja -DCMAKE_CXX_FLAGS="--coverage;-fprofile-arcs;-ftest-coverage;-fvar-tracking-assignments"
|
||||
-DJSON_BuildTests=ON
|
||||
-S${PROJECT_SOURCE_DIR} -B${PROJECT_BINARY_DIR}/build_coverage
|
||||
COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_coverage
|
||||
COMMAND cd ${PROJECT_BINARY_DIR}/build_coverage && ${CMAKE_CTEST_COMMAND} --parallel ${N} --output-on-failure
|
||||
|
||||
COMMAND CXX=g++ ${CMAKE_COMMAND}
|
||||
-DCMAKE_BUILD_TYPE=Debug -GNinja -DCMAKE_CXX_FLAGS="-m32;--coverage;-fprofile-arcs;-ftest-coverage"
|
||||
-DCMAKE_BUILD_TYPE=Debug -GNinja -DCMAKE_CXX_FLAGS="-m32;--coverage;-fprofile-arcs;-ftest-coverage;-fvar-tracking-assignments"
|
||||
-DJSON_BuildTests=ON -DJSON_32bitTest=ONLY
|
||||
-S${PROJECT_SOURCE_DIR} -B${PROJECT_BINARY_DIR}/build_coverage32
|
||||
COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_coverage32
|
||||
COMMAND cd ${PROJECT_BINARY_DIR}/build_coverage32 && ${CMAKE_CTEST_COMMAND} --parallel ${N} --output-on-failure
|
||||
|
||||
COMMAND ${LCOV_TOOL} --directory . --capture --output-file json.info --rc branch_coverage=1 --rc geninfo_unexecuted_blocks=1 --ignore-errors mismatch --ignore-errors unused --ignore-errors inconsistent
|
||||
COMMAND ${LCOV_TOOL} --directory . --capture --output-file json.info --rc branch_coverage=1 --ignore-errors mismatch --ignore-errors unused --ignore-errors inconsistent
|
||||
COMMAND ${LCOV_TOOL} -e json.info ${SRC_FILES} --output-file json.info.filtered --rc branch_coverage=1 --ignore-errors unused --ignore-errors inconsistent
|
||||
COMMAND ${CMAKE_SOURCE_DIR}/tests/thirdparty/imapdl/filterbr.py json.info.filtered > json.info.filtered.noexcept
|
||||
COMMAND ${GENHTML_TOOL} --title "JSON for Modern C++" --legend --demangle-cpp --output-directory html --show-details --branch-coverage --ignore-errors inconsistent json.info.filtered.noexcept
|
||||
|
||||
@@ -13,9 +13,15 @@ function never throws an exception for invalid input, and the returned `#!cpp bo
|
||||
is returned.
|
||||
|
||||
The result is the same as that of `BasicJsonType::accept()` by design; the difference is how the check is made. This
|
||||
function runs the document's index builder (and discards the index), while `BasicJsonType::accept()` runs the SAX
|
||||
parser with a handler that ignores every event. Use this one if you go on to parse the same input with
|
||||
[`parse()`](parse.md) anyway, so that both use the same code; either is fine for a plain yes/no answer.
|
||||
function runs the document's builder, which checks the text as [`parse()`](parse.md) does, while
|
||||
`BasicJsonType::accept()` runs the SAX parser with a handler that ignores every event. Use this one if you go on to
|
||||
parse the same input with [`parse()`](parse.md) anyway, so that both use the same code; either is fine for a plain
|
||||
yes/no answer.
|
||||
|
||||
When both `ignore_comments` and `ignore_trailing_commas` are `#!cpp false` (the default), the builder writes no index.
|
||||
With either option set, it builds the index as [`parse()`](parse.md) does and discards it: the result is the same, only
|
||||
the time differs. (Neither option needs the index; the variant that skips it exists only for the default options to
|
||||
keep the code size down.)
|
||||
|
||||
## Template parameters
|
||||
|
||||
|
||||
@@ -68,9 +68,10 @@ Strong guarantee: there are no changes to the view or the document it refers to.
|
||||
- Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if `ptr` cannot be resolved
|
||||
because a reference token is used on a primitive value.
|
||||
|
||||
None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no
|
||||
`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with
|
||||
`JSON_DIAGNOSTICS` enabled.
|
||||
As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) these exceptions name the JSON
|
||||
Pointer path of the value within the document, and with
|
||||
[`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) its bytes in the source text (when
|
||||
[`materialize()`](materialize.md) sets them).
|
||||
|
||||
## Complexity
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ Strong guarantee: there are no changes to the view or the document it refers to.
|
||||
Throws [`invalid_iterator.214`](../../home/exceptions.md#jsonexceptioninvalid_iterator214) if the view is
|
||||
[null](is_null.md) or [discarded](is_discarded.md), or if it is an empty array or object.
|
||||
|
||||
This exception does not carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no
|
||||
`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with
|
||||
`JSON_DIAGNOSTICS` enabled.
|
||||
As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the exception names the JSON Pointer
|
||||
path of the value within the document, and with [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md)
|
||||
its bytes in the source text (when [`materialize()`](materialize.md) sets them).
|
||||
|
||||
## Complexity
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ Strong guarantee: there are no changes to the view or the document it refers to.
|
||||
Throws [`invalid_iterator.214`](../../home/exceptions.md#jsonexceptioninvalid_iterator214) if the view is
|
||||
[null](is_null.md) or [discarded](is_discarded.md), or if it is an empty array or object.
|
||||
|
||||
This exception does not carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no
|
||||
`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with
|
||||
`JSON_DIAGNOSTICS` enabled.
|
||||
As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the exception names the JSON Pointer
|
||||
path of the value within the document, and with [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md)
|
||||
its bytes in the source text (when [`materialize()`](materialize.md) sets them).
|
||||
|
||||
## Complexity
|
||||
|
||||
|
||||
@@ -80,14 +80,13 @@ Strong guarantee: there are no changes to the view or the document it refers to.
|
||||
- For every other `T`: whatever [`materialize().get<T>()`](../basic_json/get.md) throws — typically `type_error.302`,
|
||||
or whatever a user-provided `from_json()` throws.
|
||||
|
||||
None of the exceptions thrown directly by this function or by a `from_json()` that reads the view (the first four
|
||||
bullets above) carry a
|
||||
[`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no `BasicJsonType` value to point at. An
|
||||
exception thrown while converting through `materialize()` (the last bullet) is different: it is thrown by a real
|
||||
`BasicJsonType` value, so it **does** carry a `JSON_DIAGNOSTICS` path if `BasicJsonType` was built with it enabled.
|
||||
That path is relative to the root of the view's subtree, because `materialize()` builds only the subtree (with parent
|
||||
pointers): it is empty if the failing value is the subtree's root itself, and it never is the path within the whole
|
||||
document.
|
||||
The exceptions thrown directly by this function or by a `from_json()` that reads the view (the first four bullets
|
||||
above) carry the context of those of `BasicJsonType`: with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the
|
||||
JSON Pointer path of the value within the document, and with
|
||||
[`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) its bytes in the source text. An exception
|
||||
thrown while converting through `materialize()` (the last bullet) is thrown by the materialized `BasicJsonType` value
|
||||
instead: its path is relative to the root of the view's subtree, because `materialize()` builds only the subtree
|
||||
(with parent pointers), so it is empty if the failing value is the subtree's root itself.
|
||||
|
||||
## Complexity
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ Strong guarantee: there are no changes to the view or the document it refers to.
|
||||
Throws [`type_error.302`](../../home/exceptions.md#jsonexceptiontype_error302) if the value is not a string; example:
|
||||
`"type must be string, but is array"`.
|
||||
|
||||
This exception does not carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no
|
||||
`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with
|
||||
`JSON_DIAGNOSTICS` enabled.
|
||||
As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the exception names the JSON Pointer
|
||||
path of the value within the document, and with [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md)
|
||||
its bytes in the source text (when [`materialize()`](materialize.md) sets them).
|
||||
|
||||
## Complexity
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ Strong guarantee: there are no changes to the view or the document it refers to.
|
||||
Throws [`type_error.302`](../../home/exceptions.md#jsonexceptiontype_error302) if the value is not a number; example:
|
||||
`"type must be number, but is string"`.
|
||||
|
||||
This exception does not carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no
|
||||
`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with
|
||||
`JSON_DIAGNOSTICS` enabled.
|
||||
As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the exception names the JSON Pointer
|
||||
path of the value within the document, and with [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md)
|
||||
its bytes in the source text (when [`materialize()`](materialize.md) sets them).
|
||||
|
||||
## Complexity
|
||||
|
||||
|
||||
@@ -73,9 +73,10 @@ Strong guarantee: there are no changes to the view or the document it refers to.
|
||||
- [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) if an array index in `ptr` is not a
|
||||
number.
|
||||
|
||||
None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no
|
||||
`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with
|
||||
`JSON_DIAGNOSTICS` enabled.
|
||||
As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) these exceptions name the JSON
|
||||
Pointer path of the value within the document, and with
|
||||
[`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) its bytes in the source text (when
|
||||
[`materialize()`](materialize.md) sets them).
|
||||
|
||||
## Complexity
|
||||
|
||||
|
||||
@@ -61,9 +61,10 @@ Strong guarantee: there are no changes to the view or the document it refers to.
|
||||
`default_value` instead of throwing, exactly as [`BasicJsonType::value`](../basic_json/value.md) catches
|
||||
`out_of_range` and returns `default_value`.
|
||||
|
||||
None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no
|
||||
`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with
|
||||
`JSON_DIAGNOSTICS` enabled.
|
||||
As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) these exceptions name the JSON
|
||||
Pointer path of the value within the document, and with
|
||||
[`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) its bytes in the source text (when
|
||||
[`materialize()`](materialize.md) sets them).
|
||||
|
||||
## Complexity
|
||||
|
||||
|
||||
@@ -13,72 +13,6 @@ describe each of them.
|
||||
|
||||
<p class="nj-caption" markdown>The checks grouped by the stage at which they run, from push or pull request through the merge into develop to a release. The badge on each card says whether the check runs on every push or PR, on a schedule, or continuously.</p>
|
||||
|
||||
## How CI runs
|
||||
|
||||
Not every job runs for every change. The compiler jobs on Ubuntu are split into three sets, so a pull request gets fast
|
||||
feedback and the long jobs run when they matter:
|
||||
|
||||
| Set | When it runs | What it contains |
|
||||
|-------------|-------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| PR set | every push to a pull request, and every push to `develop`, `master`, and `release/*` | GCC and Clang with all warnings, Clang-Tidy, sanitizers, IWYU, cpplint, the oldest and newest GCC and Clang, common CMake options, Meson, Emscripten; with the checks that always run, they would have caught at least 92 % of the failing commits of September and October 2026 |
|
||||
| full set | after the PR set passed: on pull requests with the label :rocket: ready to merge, and on every push | every other compiler, version, C++ standard, and CMake option, the Clang Static Analyzer, Cppcheck, and the C++20 module tests |
|
||||
| nightly set | every night on `develop`, on every push to `master` and `release/*`, and when started by hand | Infer, Valgrind, coverage, the Intel and NVIDIA compilers, CUDA, and ARM64; a failure opens an issue |
|
||||
|
||||
The jobs on macOS and Windows, the documentation and license checks, and the amalgamation check run on every push and
|
||||
pull request. A pull request that changes only the documentation, Markdown files, GitHub metadata, or license files
|
||||
skips the compiler workflows (see [`tools/check_path_filters`](https://github.com/nlohmann/json/tree/develop/tools/check_path_filters)).
|
||||
|
||||
A maintainer adds the label :rocket: ready to merge when a pull request is ready; the full set then runs, and again on
|
||||
every later push to that pull request. A pull request is merged only when the full set passed.
|
||||
|
||||
??? abstract "Jobs of the compiler workflows and their sets"
|
||||
|
||||
The table is generated by [`tools/ci_jobs`](https://github.com/nlohmann/json/tree/develop/tools/ci_jobs), which
|
||||
also checks in CI that it is up to date and that every job belongs to exactly one set.
|
||||
|
||||
<!-- ci_jobs: begin -->
|
||||
| Set | Workflow | Job | Configurations |
|
||||
|-----|----------|-----|----------------|
|
||||
| PR set | ubuntu.yml | `ci_test_gcc` | - |
|
||||
| PR set | ubuntu.yml | `ci_meson_install` | - |
|
||||
| PR set | ubuntu.yml | `ci_static_analysis_ubuntu` | ci_cpplint |
|
||||
| PR set | ubuntu.yml | `ci_static_analysis_clang` | ci_test_clang; ci_clang_tidy; ci_test_clang_sanitizer; ci_single_binaries |
|
||||
| PR set | ubuntu.yml | `ci_cmake_options` | ci_test_diagnostics; ci_test_diagnostic_positions; ci_test_noexceptions; ci_test_noglobaludls |
|
||||
| PR set | ubuntu.yml | `ci_test_compilers_gcc_old` | 4.8 |
|
||||
| PR set | ubuntu.yml | `ci_test_compilers_clang` | 3.4 |
|
||||
| PR set | ubuntu.yml | `ci_test_standards_gcc` | 26 |
|
||||
| PR set | ubuntu.yml | `ci_test_standards_clang` | 26, libcxx |
|
||||
| PR set | ubuntu.yml | `ci_emscripten` | - |
|
||||
| full set | ubuntu.yml | `ci_static_analysis_ubuntu_full` | ci_test_single_header; ci_cppcheck; ci_reproducible_tests; ci_non_git_tests; ci_offline_testdata |
|
||||
| full set | ubuntu.yml | `ci_static_analysis_clang_full` | ci_clang_analyze |
|
||||
| full set | ubuntu.yml | `ci_cmake_options_full` | ci_cmake_flags; ci_test_noimplicitconversions; ci_test_legacycomparison; ci_test_disableenumserialization; ci_test_disabletuplereferenceconversion; ci_test_skiplibraryversioncheck; ci_test_simdutf; ci_test_strict_nul_handling; ci_test_delete_deprecated_functions; ci_test_no_thread_local |
|
||||
| full set | ubuntu.yml | `ci_test_compilers_gcc_old_full` | 4.9; 5; 6 |
|
||||
| full set | ubuntu.yml | `ci_test_compilers_gcc_full` | 7; 8; 9; 10; 11; 12; 13; 14; 15; 16; latest |
|
||||
| full set | ubuntu.yml | `ci_test_compilers_clang_full` | 3.5; 3.6; 3.7; 3.8; 3.9; 4; 5; 6; 7; 8; 9; 10; 11; 12; 13; 14; 15-bullseye; 16; 17; 18; 19; 20; 21; 22; latest |
|
||||
| full set | ubuntu.yml | `ci_test_standards_gcc_full` | 11; 14; 17; 20; 23 |
|
||||
| full set | ubuntu.yml | `ci_test_standards_clang_full` | 11, libcxx; 11, libstdcxx; 14, libcxx; 14, libstdcxx; 17, libcxx; 17, libstdcxx; 20, libcxx; 20, libstdcxx; 23, libcxx; 23, libstdcxx; 26, libstdcxx |
|
||||
| full set | ubuntu.yml | `ci_module_cpp20` | gcc:latest; silkeh/clang:latest |
|
||||
| nightly set | ubuntu.yml | `ci_infer` | - |
|
||||
| nightly set | ubuntu.yml | `ci_static_analysis_ubuntu_nightly` | ci_test_valgrind |
|
||||
| nightly set | ubuntu.yml | `ci_test_coverage` | - |
|
||||
| nightly set | ubuntu.yml | `ci_cuda_example` | 11.8.0; 12.1.1; 12.6.3 |
|
||||
| nightly set | ubuntu.yml | `ci_icpc` | - |
|
||||
| nightly set | ubuntu.yml | `ci_icpx` | - |
|
||||
| nightly set | ubuntu.yml | `ci_nvhpc` | - |
|
||||
| nightly set | ubuntu.yml | `ci_test_arm64` | - |
|
||||
| every push and PR | macos.yml | `macos-15` | 16.0; 16.1; 16.2; 16.3; 16.4; 26.0.1; 26.1.1; 26.2; 26.3 |
|
||||
| every push and PR | macos.yml | `macos-26` | 26.4.1; 26.5; 26.6 |
|
||||
| every push and PR | macos.yml | `xcode_standards` | 11; 14; 17; 20; 23; 26 |
|
||||
| every push and PR | macos.yml | `swiftpm` | - |
|
||||
| every push and PR | windows.yml | `mingw` | x64; x86 |
|
||||
| every push and PR | windows.yml | `msvc` | Debug, Win32, default; Debug, Win32, latest; Debug, x64, default; Debug, x64, latest; Release, Win32, default; Release, Win32, latest; Release, x64, default; Release, x64, latest |
|
||||
| every push and PR | windows.yml | `msvc-vs2026` | Debug, Win32, default; Debug, Win32, latest; Debug, x64, default; Debug, x64, latest; Release, Win32, default; Release, Win32, latest; Release, x64, default; Release, x64, latest |
|
||||
| every push and PR | windows.yml | `msvc-arm64` | Debug; Release |
|
||||
| every push and PR | windows.yml | `clang` | 11.0.1; 12.0.1; 13.0.1; 14.0.6; 15.0.7; 16.0.6; 18.1.8; 19.1.7; 20.1.8 |
|
||||
| every push and PR | windows.yml | `clang-cl` | Win32; x64 |
|
||||
| every push and PR | windows.yml | `ci_module_cpp20` | - |
|
||||
<!-- ci_jobs: end -->
|
||||
|
||||
## C++ language compliance and compiler compatibility
|
||||
|
||||
!!! success "Requirement: Compiler support"
|
||||
|
||||
@@ -134,9 +134,21 @@ document: `#!cpp auto v = json_document::parse(text).root();` does not compile.
|
||||
- **Macros.** [`JSON_STRICT_NUL_HANDLING`](../api/macros/json_strict_nul_handling.md) and
|
||||
[`JSON_NOEXCEPTION`](../api/macros/json_noexception.md)/[`JSON_THROW_USER`](../api/macros/json_throw_user.md)
|
||||
behave the same way they do for `<nlohmann/json.hpp>`. These are the macros that change what the parser accepts or
|
||||
how it reports errors. The others either do not apply ([`JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md)
|
||||
needs parent pointers in `basic_json` values) or only affect the values `materialize()` produces, through
|
||||
`BasicJsonType`.
|
||||
how it reports errors. Two more macros add context to the exceptions of views, as they do for those of
|
||||
`#!cpp basic_json`:
|
||||
|
||||
- With [`JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md), an exception names the JSON Pointer path of the
|
||||
value within the document.
|
||||
- With [`JSON_DIAGNOSTIC_POSITIONS`](../api/macros/json_diagnostic_positions.md), an exception names the bytes of
|
||||
the value in the source text.
|
||||
|
||||
A view has neither parent pointers nor stored positions, so both are found from the document's index when the
|
||||
exception is thrown. This takes time linear in the size of the document. One exception has neither:
|
||||
`key()` of an iterator over an array or a primitive value
|
||||
([`invalid_iterator.207`](../home/exceptions.md#jsonexceptioninvalid_iterator207)), because a view's iterator does
|
||||
not know the value it iterates over.
|
||||
|
||||
The other macros only affect the values `materialize()` produces, through `BasicJsonType`.
|
||||
|
||||
## What is different
|
||||
|
||||
@@ -167,10 +179,6 @@ document: `#!cpp auto v = json_document::parse(text).root();` does not compile.
|
||||
[`at`](../api/basic_json_view/at.md) throws for every missing value.
|
||||
- **Duplicate keys: lookups find the first member, `parse()` keeps the last.** See [Duplicate keys](#duplicate-keys)
|
||||
below.
|
||||
- **No [`JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md) path.** Exceptions thrown by `basic_json_view`'s own
|
||||
element access and lookup functions never carry the JSON Pointer path `JSON_DIAGNOSTICS` would otherwise add: the
|
||||
view has no `basic_json` value to point at, so the exception is created without one, regardless of how
|
||||
`BasicJsonType` was built.
|
||||
- **Ordering comparisons are not provided** by `basic_json_view` — there is no `#!cpp operator<`.
|
||||
[`operator==`](../api/basic_json_view/operator_eq.md) and [`operator!=`](../api/basic_json_view/operator_ne.md) are
|
||||
provided, though: two views, or a view and a `BasicJsonType` value, compare equal exactly when
|
||||
@@ -253,6 +261,34 @@ Two conversions never copy at all:
|
||||
|
||||
Both results are only valid as long as the view — and, for a string with no escapes, the borrowed source text — is.
|
||||
|
||||
### `string_view` members
|
||||
|
||||
A struct whose member is a `#!cpp std::string_view` (C++17) can be decoded from a view with the
|
||||
`NLOHMANN_DEFINE_TYPE_*` macros, and the member is not copied either: for a string without escape sequences it points
|
||||
into the document's source text, and for a string with escape sequences into the document's buffer of decoded strings.
|
||||
The member is valid as long as the document is alive and, for a document that
|
||||
[borrows its source text](#ownership-and-lifetime) (as [`parse()`](../api/basic_json_document/parse.md) does for
|
||||
an lvalue `#!cpp std::string`, a `#!cpp std::string_view`, or a C string), as long as that text is.
|
||||
|
||||
```cpp
|
||||
struct record
|
||||
{
|
||||
std::string_view name;
|
||||
int count;
|
||||
NLOHMANN_DEFINE_TYPE_INTRUSIVE(record, name, count)
|
||||
};
|
||||
|
||||
const std::string text = R"({"name": "widget", "count": 3})";
|
||||
const json_document doc = json_document::parse(text); // borrows text
|
||||
const record r = doc.root().get<record>(); // r.name points into text, valid while text and doc are
|
||||
```
|
||||
|
||||
!!! warning "Decoding into a `string_view` from a `json` value dangles at once"
|
||||
|
||||
The same struct also compiles with `#!cpp json::parse(text).get<record>()`, but there `name` points into the
|
||||
temporary `json` value, which is destroyed at the end of the statement. Use a named
|
||||
[`json_document`](../api/json_document.md), as above, to decode such a struct.
|
||||
|
||||
## Writing generic code
|
||||
|
||||
A view's type names its `BasicJsonType` and its [`view_mode`](../api/view_mode.md):
|
||||
@@ -329,14 +365,10 @@ Its body may only use what views and `basic_json` have in common: the type check
|
||||
a view does not have; such a type is converted through `materialize()`. A `from_json()` for `json` only is
|
||||
converted through `materialize()` as well.
|
||||
|
||||
Two things differ when a `from_json()` reads the view:
|
||||
|
||||
- **Duplicate keys.** Members looked up by name (`at()`, `value()`, ... and thus the macros) find the **first** member
|
||||
with a repeated key, see [Duplicate keys](#duplicate-keys); `#!cpp materialize().get<T>()` would see the last one.
|
||||
Containers built from an object, such as `#!cpp std::map`, keep the **last** value either way, as
|
||||
[`parse()`](../api/basic_json/parse.md) does.
|
||||
- **[`JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md).** Exceptions thrown by the view carry no path to the
|
||||
value.
|
||||
One thing differs when a `from_json()` reads the view: members looked up by name (`at()`, `value()`, ... and thus
|
||||
the macros) find the **first** member with a repeated key, see [Duplicate keys](#duplicate-keys);
|
||||
`#!cpp materialize().get<T>()` would see the last one. Containers built from an object, such as `#!cpp std::map`,
|
||||
keep the **last** value either way, as [`parse()`](../api/basic_json/parse.md) does.
|
||||
|
||||
`to_json()` is unaffected: it writes `basic_json` values, never views.
|
||||
|
||||
|
||||
@@ -10,6 +10,13 @@ at its root, a [Natvis](https://learn.microsoft.com/en-us/visualstudio/debugger/
|
||||
file that gives `json`/`ordered_json` values a friendly, key/value debugger view instead of showing raw internal
|
||||
fields, when debugging with the MSVC debug engine (`cppvsdbg`) in Visual Studio or VS Code.
|
||||
|
||||
The file also has visualizers for `json_document` and `json_view` (and their `ordered_json`/editable variants). They
|
||||
show the value of the document or view, and its elements as an expandable list. Because a document is only a flat index
|
||||
into the source text, the strings of the direct elements are shown, but the elements of nested arrays and objects are
|
||||
shown as index nodes (their kind, size, and numbers; strings only by length). Arrays and objects that have been edited
|
||||
in a `json_editable_document` are not expanded. The visualizers read the internal layout of the MSVC STL (`std::array`,
|
||||
`std::unique_ptr`).
|
||||
|
||||
The checked-in visualizers cover namespace variants with zero, one, or two ABI tags. Configurations with three or more
|
||||
ABI tags are not covered and may show raw internal fields instead. To generate a Natvis file for such a configuration:
|
||||
|
||||
@@ -29,7 +36,7 @@ ABI tags are not covered and may show raw internal fields instead. To generate a
|
||||
|
||||
4. Use the generated file instead of `nlohmann_json.natvis`.
|
||||
|
||||
`--abi-tags` can be repeated for several configurations, and `--all` generates every combination (about 60 MB for 12
|
||||
`--abi-tags` can be repeated for several configurations, and `--all` generates every combination (about 60 MB for 11
|
||||
ABI tags, doubling with each further tag). See
|
||||
[`tools/generate_natvis`](https://github.com/nlohmann/json/tree/develop/tools/generate_natvis) for all options.
|
||||
|
||||
|
||||
@@ -1,31 +1,26 @@
|
||||
<svg class="nj-fig nj-exceptions-qa nj-exceptions-qa--wide" viewBox="0 0 760 862" role="img" aria-labelledby="nj-qa-title-w nj-qa-desc-w" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg class="nj-fig nj-exceptions-qa nj-exceptions-qa--wide" viewBox="0 0 760 808" role="img" aria-labelledby="nj-qa-title-w nj-qa-desc-w" xmlns="http://www.w3.org/2000/svg">
|
||||
<title id="nj-qa-title-w">Quality assurance pipeline</title>
|
||||
<desc id="nj-qa-desc-w">A push or pull request starts these checks; the badges say when each runs: Build matrix (every push / PR, before merge): 50+ compilers: G​CC, Clang, M​SVC, AppleClang, Intel, NVIDIA, ... C++11, 14, 17, 20, 23, and 26 libc++ and libstdc++ warnings: Clang -Weverything, G​CC 300+ warnings a subset on every push and PR, all before merge; Intel, NVIDIA, C​UDA, and ARM64 nightly; Unit tests (doctest) (every push / PR): every public A​PI function every exception id and message all Unicode code points single header and split headers custom number, string, array, and object types; Coverage (nightly): unit tests cover all lines of the code base; Static analysis (every push / PR, scheduled): Clang-Tidy, Cppcheck, Infer Clang Static Analyzer (52 checkers), I​WYU, Codacy CodeQL, Semgrep, Flawfinder (also scheduled); Infer nightly; Dynamic analysis (every push / PR, nightly): runtime assertions Valgrind (Memcheck), nightly address and undefined behavior sanitizers, integer overflow, nullability violations; Fuzzing (every PR, continuous): CIFuzz on every pull request OSS-Fuzz: billions of inputs every crash fixed with a reproducing unit test; Style (every push / PR): Artistic Style (astyle), enforced in the C​I cpplint with 61 rules; Docs and examples (every push / PR, scheduled): every documentation example is tested; changed output fails site built with warnings as errors; links checked regularly (scheduled); Integration, supply chain (every push / PR, scheduled): CMake 3.5, 3.31.6, and 4.0.0 Meson, Swift Package Manager dependency review on every PR Dependabot daily, OpenSSF Scorecard, R​EUSE. When they pass, the change is merged into develop; a release is made only when no vulnerability alert is open.</desc>
|
||||
<desc id="nj-qa-desc-w">Every push or pull request starts these checks in parallel: Build matrix (every push / PR): 50+ compilers: G​CC, Clang, M​SVC, AppleClang, Intel, NVIDIA, ... C++11, 14, 17, 20, 23, and 26 libc++ and libstdc++ warnings: Clang -Weverything, G​CC 300+ warnings; Unit tests (doctest) (every push / PR): every public A​PI function every exception id and message all Unicode code points single header and split headers custom number, string, array, and object types; Coverage (every push / PR): unit tests cover all lines of the code base; Static analysis (every push / PR, scheduled): Clang-Tidy, Cppcheck, Infer Clang Static Analyzer (52 checkers), I​WYU, Codacy CodeQL, Semgrep, Flawfinder (also scheduled); Dynamic analysis (every push / PR): runtime assertions Valgrind (Memcheck) address and undefined behavior sanitizers, integer overflow, nullability violations; Fuzzing (every PR, continuous): CIFuzz on every pull request OSS-Fuzz: billions of inputs every crash fixed with a reproducing unit test; Style (every push / PR): Artistic Style (astyle), enforced in the C​I cpplint with 61 rules; Docs and examples (every push / PR, scheduled): every documentation example is tested; changed output fails site built with warnings as errors; links checked regularly (scheduled); Integration, supply chain (every push / PR, scheduled): CMake 3.5, 3.31.6, and 4.0.0 Meson, Swift Package Manager dependency review on every PR Dependabot daily, OpenSSF Scorecard, R​EUSE. When they pass, the change is merged into develop; a release is made only when no vulnerability alert is open.</desc>
|
||||
<defs><marker id="nj-qa-arrow-w" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path class="fg-head" d="M0 1 L10 5 L0 9 z"/></marker></defs>
|
||||
<rect class="nj-exceptions-groupbox" x="12" y="70" width="736" height="648" rx="12"/>
|
||||
<rect class="nj-exceptions-groupbox" x="12" y="70" width="736" height="594" rx="12"/>
|
||||
<rect class="nj-exceptions-stage" x="265" y="8" width="230" height="34" rx="17"/>
|
||||
<text class="nj-exceptions-stagetext" x="380" y="30">push or pull request</text>
|
||||
<line class="nj-exceptions-edge" x1="380" y1="42" x2="380" y2="68" marker-end="url(#nj-qa-arrow-w)"/>
|
||||
<text class="nj-exceptions-group" x="26" y="94">checks, run in parallel on GitHub Actions and AppVeyor</text>
|
||||
<g class="nj-exceptions-card">
|
||||
<rect class="nj-exceptions-box" x="26" y="110" width="228" height="238" rx="8"/>
|
||||
<rect class="nj-exceptions-box" x="26" y="110" width="228" height="184" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="40" y="135">Build matrix</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="146" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="160">every push / PR</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="156.5" y="146" width="91.6" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="202.3" y="160">before merge</text>
|
||||
<text class="nj-exceptions-line" x="40" y="188">50+ compilers: G​CC, Clang, M​SVC,</text>
|
||||
<text class="nj-exceptions-line" x="40" y="206">AppleClang, Intel, NVIDIA, ...</text>
|
||||
<text class="nj-exceptions-line" x="40" y="224">C++11, 14, 17, 20, 23, and 26</text>
|
||||
<text class="nj-exceptions-line" x="40" y="242">libc++ and libstdc++</text>
|
||||
<text class="nj-exceptions-line" x="40" y="260">warnings: Clang -Weverything,</text>
|
||||
<text class="nj-exceptions-line" x="40" y="278">G​CC 300+ warnings</text>
|
||||
<text class="nj-exceptions-line" x="40" y="296">a subset on every push and PR,</text>
|
||||
<text class="nj-exceptions-line" x="40" y="314">all before merge; Intel, NVIDIA,</text>
|
||||
<text class="nj-exceptions-line" x="40" y="332">C​UDA, and ARM64 nightly</text>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<rect class="nj-exceptions-box" x="266" y="110" width="228" height="238" rx="8"/>
|
||||
<rect class="nj-exceptions-box" x="266" y="110" width="228" height="184" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="280" y="135">Unit tests (doctest)</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="280" y="146" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="335.25" y="160">every push / PR</text>
|
||||
@@ -37,218 +32,209 @@
|
||||
<text class="nj-exceptions-line" x="280" y="278">and object types</text>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<rect class="nj-exceptions-box" x="506" y="110" width="228" height="238" rx="8"/>
|
||||
<rect class="nj-exceptions-box" x="506" y="110" width="228" height="184" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="520" y="135">Coverage</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="520" y="146" width="60.1" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="550.05" y="160">nightly</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="520" y="146" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="575.25" y="160">every push / PR</text>
|
||||
<text class="nj-exceptions-line" x="520" y="188">unit tests cover all lines</text>
|
||||
<text class="nj-exceptions-line" x="520" y="206">of the code base</text>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<rect class="nj-exceptions-box" x="26" y="360" width="228" height="166" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="40" y="385">Static analysis</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="396" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="410">every push / PR</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="396" width="72.7" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="410">scheduled</text>
|
||||
<text class="nj-exceptions-line" x="40" y="438">Clang-Tidy, Cppcheck, Infer</text>
|
||||
<text class="nj-exceptions-line" x="40" y="456">Clang Static Analyzer (52</text>
|
||||
<text class="nj-exceptions-line" x="40" y="474">checkers), I​WYU, Codacy</text>
|
||||
<text class="nj-exceptions-line" x="40" y="492">CodeQL, Semgrep, Flawfinder</text>
|
||||
<text class="nj-exceptions-line" x="40" y="510">(also scheduled); Infer nightly</text>
|
||||
<rect class="nj-exceptions-box" x="26" y="306" width="228" height="166" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="40" y="331">Static analysis</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="342" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="356">every push / PR</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="342" width="72.7" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="356">scheduled</text>
|
||||
<text class="nj-exceptions-line" x="40" y="384">Clang-Tidy, Cppcheck, Infer</text>
|
||||
<text class="nj-exceptions-line" x="40" y="402">Clang Static Analyzer (52</text>
|
||||
<text class="nj-exceptions-line" x="40" y="420">checkers), I​WYU, Codacy</text>
|
||||
<text class="nj-exceptions-line" x="40" y="438">CodeQL, Semgrep, Flawfinder</text>
|
||||
<text class="nj-exceptions-line" x="40" y="456">(also scheduled)</text>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<rect class="nj-exceptions-box" x="266" y="360" width="228" height="166" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="280" y="385">Dynamic analysis</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="280" y="396" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="335.25" y="410">every push / PR</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="396.5" y="396" width="60.1" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="426.55" y="410">nightly</text>
|
||||
<text class="nj-exceptions-line" x="280" y="438">runtime assertions</text>
|
||||
<text class="nj-exceptions-line" x="280" y="456">Valgrind (Memcheck), nightly</text>
|
||||
<text class="nj-exceptions-line" x="280" y="474">address and undefined behavior</text>
|
||||
<text class="nj-exceptions-line" x="280" y="492">sanitizers, integer overflow,</text>
|
||||
<text class="nj-exceptions-line" x="280" y="510">nullability violations</text>
|
||||
<rect class="nj-exceptions-box" x="266" y="306" width="228" height="166" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="280" y="331">Dynamic analysis</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="280" y="342" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="335.25" y="356">every push / PR</text>
|
||||
<text class="nj-exceptions-line" x="280" y="384">runtime assertions</text>
|
||||
<text class="nj-exceptions-line" x="280" y="402">Valgrind (Memcheck)</text>
|
||||
<text class="nj-exceptions-line" x="280" y="420">address and undefined behavior</text>
|
||||
<text class="nj-exceptions-line" x="280" y="438">sanitizers, integer overflow,</text>
|
||||
<text class="nj-exceptions-line" x="280" y="456">nullability violations</text>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<rect class="nj-exceptions-box" x="506" y="360" width="228" height="166" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="520" y="385">Fuzzing</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="520" y="396" width="66.4" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="553.2" y="410">every PR</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--cont" x="592.4" y="396" width="79" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--cont" x="631.9" y="410">continuous</text>
|
||||
<text class="nj-exceptions-line" x="520" y="438">CIFuzz on every pull request</text>
|
||||
<text class="nj-exceptions-line" x="520" y="456">OSS-Fuzz: billions of inputs</text>
|
||||
<text class="nj-exceptions-line" x="520" y="474">every crash fixed with a</text>
|
||||
<text class="nj-exceptions-line" x="520" y="492">reproducing unit test</text>
|
||||
<rect class="nj-exceptions-box" x="506" y="306" width="228" height="166" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="520" y="331">Fuzzing</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="520" y="342" width="66.4" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="553.2" y="356">every PR</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--cont" x="592.4" y="342" width="79" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--cont" x="631.9" y="356">continuous</text>
|
||||
<text class="nj-exceptions-line" x="520" y="384">CIFuzz on every pull request</text>
|
||||
<text class="nj-exceptions-line" x="520" y="402">OSS-Fuzz: billions of inputs</text>
|
||||
<text class="nj-exceptions-line" x="520" y="420">every crash fixed with a</text>
|
||||
<text class="nj-exceptions-line" x="520" y="438">reproducing unit test</text>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<rect class="nj-exceptions-box" x="26" y="538" width="228" height="166" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="40" y="563">Style</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="574" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="588">every push / PR</text>
|
||||
<text class="nj-exceptions-line" x="40" y="616">Artistic Style (astyle),</text>
|
||||
<text class="nj-exceptions-line" x="40" y="634">enforced in the C​I</text>
|
||||
<text class="nj-exceptions-line" x="40" y="652">cpplint with 61 rules</text>
|
||||
<rect class="nj-exceptions-box" x="26" y="484" width="228" height="166" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="40" y="509">Style</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="520" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="534">every push / PR</text>
|
||||
<text class="nj-exceptions-line" x="40" y="562">Artistic Style (astyle),</text>
|
||||
<text class="nj-exceptions-line" x="40" y="580">enforced in the C​I</text>
|
||||
<text class="nj-exceptions-line" x="40" y="598">cpplint with 61 rules</text>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<rect class="nj-exceptions-box" x="266" y="538" width="228" height="166" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="280" y="563">Docs and examples</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="280" y="574" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="335.25" y="588">every push / PR</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="396.5" y="574" width="72.7" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="432.85" y="588">scheduled</text>
|
||||
<text class="nj-exceptions-line" x="280" y="616">every documentation example is</text>
|
||||
<text class="nj-exceptions-line" x="280" y="634">tested; changed output fails</text>
|
||||
<text class="nj-exceptions-line" x="280" y="652">site built with warnings</text>
|
||||
<text class="nj-exceptions-line" x="280" y="670">as errors; links checked</text>
|
||||
<text class="nj-exceptions-line" x="280" y="688">regularly (scheduled)</text>
|
||||
<rect class="nj-exceptions-box" x="266" y="484" width="228" height="166" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="280" y="509">Docs and examples</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="280" y="520" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="335.25" y="534">every push / PR</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="396.5" y="520" width="72.7" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="432.85" y="534">scheduled</text>
|
||||
<text class="nj-exceptions-line" x="280" y="562">every documentation example is</text>
|
||||
<text class="nj-exceptions-line" x="280" y="580">tested; changed output fails</text>
|
||||
<text class="nj-exceptions-line" x="280" y="598">site built with warnings</text>
|
||||
<text class="nj-exceptions-line" x="280" y="616">as errors; links checked</text>
|
||||
<text class="nj-exceptions-line" x="280" y="634">regularly (scheduled)</text>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<rect class="nj-exceptions-box" x="506" y="538" width="228" height="166" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="520" y="563">Integration, supply chain</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="520" y="574" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="575.25" y="588">every push / PR</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="636.5" y="574" width="72.7" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="672.85" y="588">scheduled</text>
|
||||
<text class="nj-exceptions-line" x="520" y="616">CMake 3.5, 3.31.6, and 4.0.0</text>
|
||||
<text class="nj-exceptions-line" x="520" y="634">Meson, Swift Package Manager</text>
|
||||
<text class="nj-exceptions-line" x="520" y="652">dependency review on every PR</text>
|
||||
<text class="nj-exceptions-line" x="520" y="670">Dependabot daily, OpenSSF</text>
|
||||
<text class="nj-exceptions-line" x="520" y="688">Scorecard, R​EUSE</text>
|
||||
<rect class="nj-exceptions-box" x="506" y="484" width="228" height="166" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="520" y="509">Integration, supply chain</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="520" y="520" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="575.25" y="534">every push / PR</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="636.5" y="520" width="72.7" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="672.85" y="534">scheduled</text>
|
||||
<text class="nj-exceptions-line" x="520" y="562">CMake 3.5, 3.31.6, and 4.0.0</text>
|
||||
<text class="nj-exceptions-line" x="520" y="580">Meson, Swift Package Manager</text>
|
||||
<text class="nj-exceptions-line" x="520" y="598">dependency review on every PR</text>
|
||||
<text class="nj-exceptions-line" x="520" y="616">Dependabot daily, OpenSSF</text>
|
||||
<text class="nj-exceptions-line" x="520" y="634">Scorecard, R​EUSE</text>
|
||||
</g>
|
||||
<line class="nj-exceptions-edge" x1="380" y1="718" x2="380" y2="744" marker-end="url(#nj-qa-arrow-w)"/>
|
||||
<rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="265" y="746" width="230" height="34" rx="17"/>
|
||||
<text class="nj-exceptions-stagetext" x="380" y="768">merge into develop</text>
|
||||
<line class="nj-exceptions-edge" x1="380" y1="780" x2="380" y2="806" marker-end="url(#nj-qa-arrow-w)"/>
|
||||
<rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="230" y="808" width="300" height="46" rx="23"/>
|
||||
<text class="nj-exceptions-stagetext" x="380" y="830">release</text>
|
||||
<text class="nj-exceptions-stagesub" x="380" y="846">no open vulnerability alert</text>
|
||||
<line class="nj-exceptions-edge" x1="380" y1="664" x2="380" y2="690" marker-end="url(#nj-qa-arrow-w)"/>
|
||||
<rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="265" y="692" width="230" height="34" rx="17"/>
|
||||
<text class="nj-exceptions-stagetext" x="380" y="714">merge into develop</text>
|
||||
<line class="nj-exceptions-edge" x1="380" y1="726" x2="380" y2="752" marker-end="url(#nj-qa-arrow-w)"/>
|
||||
<rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="230" y="754" width="300" height="46" rx="23"/>
|
||||
<text class="nj-exceptions-stagetext" x="380" y="776">release</text>
|
||||
<text class="nj-exceptions-stagesub" x="380" y="792">no open vulnerability alert</text>
|
||||
</svg>
|
||||
<svg class="nj-fig nj-exceptions-qa nj-exceptions-qa--narrow" viewBox="0 0 360 1840" role="img" aria-labelledby="nj-qa-title-n nj-qa-desc-n" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg class="nj-fig nj-exceptions-qa nj-exceptions-qa--narrow" viewBox="0 0 360 1786" role="img" aria-labelledby="nj-qa-title-n nj-qa-desc-n" xmlns="http://www.w3.org/2000/svg">
|
||||
<title id="nj-qa-title-n">Quality assurance pipeline</title>
|
||||
<desc id="nj-qa-desc-n">A push or pull request starts these checks; the badges say when each runs: Build matrix (every push / PR, before merge): 50+ compilers: G​CC, Clang, M​SVC, AppleClang, Intel, NVIDIA, ... C++11, 14, 17, 20, 23, and 26 libc++ and libstdc++ warnings: Clang -Weverything, G​CC 300+ warnings a subset on every push and PR, all before merge; Intel, NVIDIA, C​UDA, and ARM64 nightly; Unit tests (doctest) (every push / PR): every public A​PI function every exception id and message all Unicode code points single header and split headers custom number, string, array, and object types; Coverage (nightly): unit tests cover all lines of the code base; Static analysis (every push / PR, scheduled): Clang-Tidy, Cppcheck, Infer Clang Static Analyzer (52 checkers), I​WYU, Codacy CodeQL, Semgrep, Flawfinder (also scheduled); Infer nightly; Dynamic analysis (every push / PR, nightly): runtime assertions Valgrind (Memcheck), nightly address and undefined behavior sanitizers, integer overflow, nullability violations; Fuzzing (every PR, continuous): CIFuzz on every pull request OSS-Fuzz: billions of inputs every crash fixed with a reproducing unit test; Style (every push / PR): Artistic Style (astyle), enforced in the C​I cpplint with 61 rules; Docs and examples (every push / PR, scheduled): every documentation example is tested; changed output fails site built with warnings as errors; links checked regularly (scheduled); Integration, supply chain (every push / PR, scheduled): CMake 3.5, 3.31.6, and 4.0.0 Meson, Swift Package Manager dependency review on every PR Dependabot daily, OpenSSF Scorecard, R​EUSE. When they pass, the change is merged into develop; a release is made only when no vulnerability alert is open.</desc>
|
||||
<desc id="nj-qa-desc-n">Every push or pull request starts these checks in parallel: Build matrix (every push / PR): 50+ compilers: G​CC, Clang, M​SVC, AppleClang, Intel, NVIDIA, ... C++11, 14, 17, 20, 23, and 26 libc++ and libstdc++ warnings: Clang -Weverything, G​CC 300+ warnings; Unit tests (doctest) (every push / PR): every public A​PI function every exception id and message all Unicode code points single header and split headers custom number, string, array, and object types; Coverage (every push / PR): unit tests cover all lines of the code base; Static analysis (every push / PR, scheduled): Clang-Tidy, Cppcheck, Infer Clang Static Analyzer (52 checkers), I​WYU, Codacy CodeQL, Semgrep, Flawfinder (also scheduled); Dynamic analysis (every push / PR): runtime assertions Valgrind (Memcheck) address and undefined behavior sanitizers, integer overflow, nullability violations; Fuzzing (every PR, continuous): CIFuzz on every pull request OSS-Fuzz: billions of inputs every crash fixed with a reproducing unit test; Style (every push / PR): Artistic Style (astyle), enforced in the C​I cpplint with 61 rules; Docs and examples (every push / PR, scheduled): every documentation example is tested; changed output fails site built with warnings as errors; links checked regularly (scheduled); Integration, supply chain (every push / PR, scheduled): CMake 3.5, 3.31.6, and 4.0.0 Meson, Swift Package Manager dependency review on every PR Dependabot daily, OpenSSF Scorecard, R​EUSE. When they pass, the change is merged into develop; a release is made only when no vulnerability alert is open.</desc>
|
||||
<defs><marker id="nj-qa-arrow-n" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path class="fg-head" d="M0 1 L10 5 L0 9 z"/></marker></defs>
|
||||
<rect class="nj-exceptions-groupbox" x="12" y="70" width="336" height="1626" rx="12"/>
|
||||
<rect class="nj-exceptions-groupbox" x="12" y="70" width="336" height="1572" rx="12"/>
|
||||
<rect class="nj-exceptions-stage" x="65" y="8" width="230" height="34" rx="17"/>
|
||||
<text class="nj-exceptions-stagetext" x="180" y="30">push or pull request</text>
|
||||
<line class="nj-exceptions-edge" x1="180" y1="42" x2="180" y2="68" marker-end="url(#nj-qa-arrow-n)"/>
|
||||
<text class="nj-exceptions-group" x="26" y="94">checks, run in parallel</text>
|
||||
<g class="nj-exceptions-card">
|
||||
<rect class="nj-exceptions-box" x="26" y="110" width="308" height="238" rx="8"/>
|
||||
<rect class="nj-exceptions-box" x="26" y="110" width="308" height="184" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="40" y="135">Build matrix</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="146" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="160">every push / PR</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="156.5" y="146" width="91.6" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="202.3" y="160">before merge</text>
|
||||
<text class="nj-exceptions-line" x="40" y="188">50+ compilers: G​CC, Clang, M​SVC,</text>
|
||||
<text class="nj-exceptions-line" x="40" y="206">AppleClang, Intel, NVIDIA, ...</text>
|
||||
<text class="nj-exceptions-line" x="40" y="224">C++11, 14, 17, 20, 23, and 26</text>
|
||||
<text class="nj-exceptions-line" x="40" y="242">libc++ and libstdc++</text>
|
||||
<text class="nj-exceptions-line" x="40" y="260">warnings: Clang -Weverything,</text>
|
||||
<text class="nj-exceptions-line" x="40" y="278">G​CC 300+ warnings</text>
|
||||
<text class="nj-exceptions-line" x="40" y="296">a subset on every push and PR,</text>
|
||||
<text class="nj-exceptions-line" x="40" y="314">all before merge; Intel, NVIDIA,</text>
|
||||
<text class="nj-exceptions-line" x="40" y="332">C​UDA, and ARM64 nightly</text>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<rect class="nj-exceptions-box" x="26" y="360" width="308" height="184" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="40" y="385">Unit tests (doctest)</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="396" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="410">every push / PR</text>
|
||||
<text class="nj-exceptions-line" x="40" y="438">every public A​PI function</text>
|
||||
<text class="nj-exceptions-line" x="40" y="456">every exception id and message</text>
|
||||
<text class="nj-exceptions-line" x="40" y="474">all Unicode code points</text>
|
||||
<text class="nj-exceptions-line" x="40" y="492">single header and split headers</text>
|
||||
<text class="nj-exceptions-line" x="40" y="510">custom number, string, array,</text>
|
||||
<text class="nj-exceptions-line" x="40" y="528">and object types</text>
|
||||
<rect class="nj-exceptions-box" x="26" y="306" width="308" height="184" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="40" y="331">Unit tests (doctest)</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="342" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="356">every push / PR</text>
|
||||
<text class="nj-exceptions-line" x="40" y="384">every public A​PI function</text>
|
||||
<text class="nj-exceptions-line" x="40" y="402">every exception id and message</text>
|
||||
<text class="nj-exceptions-line" x="40" y="420">all Unicode code points</text>
|
||||
<text class="nj-exceptions-line" x="40" y="438">single header and split headers</text>
|
||||
<text class="nj-exceptions-line" x="40" y="456">custom number, string, array,</text>
|
||||
<text class="nj-exceptions-line" x="40" y="474">and object types</text>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<rect class="nj-exceptions-box" x="26" y="556" width="308" height="112" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="40" y="581">Coverage</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="40" y="592" width="60.1" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="70.05" y="606">nightly</text>
|
||||
<text class="nj-exceptions-line" x="40" y="634">unit tests cover all lines</text>
|
||||
<text class="nj-exceptions-line" x="40" y="652">of the code base</text>
|
||||
<rect class="nj-exceptions-box" x="26" y="502" width="308" height="112" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="40" y="527">Coverage</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="538" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="552">every push / PR</text>
|
||||
<text class="nj-exceptions-line" x="40" y="580">unit tests cover all lines</text>
|
||||
<text class="nj-exceptions-line" x="40" y="598">of the code base</text>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<rect class="nj-exceptions-box" x="26" y="680" width="308" height="166" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="40" y="705">Static analysis</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="716" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="730">every push / PR</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="716" width="72.7" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="730">scheduled</text>
|
||||
<text class="nj-exceptions-line" x="40" y="758">Clang-Tidy, Cppcheck, Infer</text>
|
||||
<text class="nj-exceptions-line" x="40" y="776">Clang Static Analyzer (52</text>
|
||||
<text class="nj-exceptions-line" x="40" y="794">checkers), I​WYU, Codacy</text>
|
||||
<text class="nj-exceptions-line" x="40" y="812">CodeQL, Semgrep, Flawfinder</text>
|
||||
<text class="nj-exceptions-line" x="40" y="830">(also scheduled); Infer nightly</text>
|
||||
<rect class="nj-exceptions-box" x="26" y="626" width="308" height="166" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="40" y="651">Static analysis</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="662" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="676">every push / PR</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="662" width="72.7" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="676">scheduled</text>
|
||||
<text class="nj-exceptions-line" x="40" y="704">Clang-Tidy, Cppcheck, Infer</text>
|
||||
<text class="nj-exceptions-line" x="40" y="722">Clang Static Analyzer (52</text>
|
||||
<text class="nj-exceptions-line" x="40" y="740">checkers), I​WYU, Codacy</text>
|
||||
<text class="nj-exceptions-line" x="40" y="758">CodeQL, Semgrep, Flawfinder</text>
|
||||
<text class="nj-exceptions-line" x="40" y="776">(also scheduled)</text>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<rect class="nj-exceptions-box" x="26" y="858" width="308" height="166" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="40" y="883">Dynamic analysis</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="894" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="908">every push / PR</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="894" width="60.1" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="186.55" y="908">nightly</text>
|
||||
<text class="nj-exceptions-line" x="40" y="936">runtime assertions</text>
|
||||
<text class="nj-exceptions-line" x="40" y="954">Valgrind (Memcheck), nightly</text>
|
||||
<text class="nj-exceptions-line" x="40" y="972">address and undefined behavior</text>
|
||||
<text class="nj-exceptions-line" x="40" y="990">sanitizers, integer overflow,</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1008">nullability violations</text>
|
||||
<rect class="nj-exceptions-box" x="26" y="804" width="308" height="166" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="40" y="829">Dynamic analysis</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="840" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="854">every push / PR</text>
|
||||
<text class="nj-exceptions-line" x="40" y="882">runtime assertions</text>
|
||||
<text class="nj-exceptions-line" x="40" y="900">Valgrind (Memcheck)</text>
|
||||
<text class="nj-exceptions-line" x="40" y="918">address and undefined behavior</text>
|
||||
<text class="nj-exceptions-line" x="40" y="936">sanitizers, integer overflow,</text>
|
||||
<text class="nj-exceptions-line" x="40" y="954">nullability violations</text>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<rect class="nj-exceptions-box" x="26" y="1036" width="308" height="148" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="40" y="1061">Fuzzing</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1072" width="66.4" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="73.2" y="1086">every PR</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--cont" x="112.4" y="1072" width="79" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--cont" x="151.9" y="1086">continuous</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1114">CIFuzz on every pull request</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1132">OSS-Fuzz: billions of inputs</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1150">every crash fixed with a</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1168">reproducing unit test</text>
|
||||
<rect class="nj-exceptions-box" x="26" y="982" width="308" height="148" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="40" y="1007">Fuzzing</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1018" width="66.4" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="73.2" y="1032">every PR</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--cont" x="112.4" y="1018" width="79" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--cont" x="151.9" y="1032">continuous</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1060">CIFuzz on every pull request</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1078">OSS-Fuzz: billions of inputs</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1096">every crash fixed with a</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1114">reproducing unit test</text>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<rect class="nj-exceptions-box" x="26" y="1196" width="308" height="130" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="40" y="1221">Style</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1232" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="1246">every push / PR</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1274">Artistic Style (astyle),</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1292">enforced in the C​I</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1310">cpplint with 61 rules</text>
|
||||
<rect class="nj-exceptions-box" x="26" y="1142" width="308" height="130" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="40" y="1167">Style</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1178" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="1192">every push / PR</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1220">Artistic Style (astyle),</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1238">enforced in the C​I</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1256">cpplint with 61 rules</text>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<rect class="nj-exceptions-box" x="26" y="1338" width="308" height="166" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="40" y="1363">Docs and examples</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1374" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="1388">every push / PR</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="1374" width="72.7" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="1388">scheduled</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1416">every documentation example is</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1434">tested; changed output fails</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1452">site built with warnings</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1470">as errors; links checked</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1488">regularly (scheduled)</text>
|
||||
<rect class="nj-exceptions-box" x="26" y="1284" width="308" height="166" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="40" y="1309">Docs and examples</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1320" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="1334">every push / PR</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="1320" width="72.7" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="1334">scheduled</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1362">every documentation example is</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1380">tested; changed output fails</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1398">site built with warnings</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1416">as errors; links checked</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1434">regularly (scheduled)</text>
|
||||
</g>
|
||||
<g class="nj-exceptions-card">
|
||||
<rect class="nj-exceptions-box" x="26" y="1516" width="308" height="166" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="40" y="1541">Integration, supply chain</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1552" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="1566">every push / PR</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="1552" width="72.7" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="1566">scheduled</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1594">CMake 3.5, 3.31.6, and 4.0.0</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1612">Meson, Swift Package Manager</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1630">dependency review on every PR</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1648">Dependabot daily, OpenSSF</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1666">Scorecard, R​EUSE</text>
|
||||
<rect class="nj-exceptions-box" x="26" y="1462" width="308" height="166" rx="8"/>
|
||||
<text class="nj-exceptions-ctitle" x="40" y="1487">Integration, supply chain</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1498" width="110.5" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="1512">every push / PR</text>
|
||||
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="1498" width="72.7" height="20" rx="10"/>
|
||||
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="1512">scheduled</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1540">CMake 3.5, 3.31.6, and 4.0.0</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1558">Meson, Swift Package Manager</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1576">dependency review on every PR</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1594">Dependabot daily, OpenSSF</text>
|
||||
<text class="nj-exceptions-line" x="40" y="1612">Scorecard, R​EUSE</text>
|
||||
</g>
|
||||
<line class="nj-exceptions-edge" x1="180" y1="1696" x2="180" y2="1722" marker-end="url(#nj-qa-arrow-n)"/>
|
||||
<rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="65" y="1724" width="230" height="34" rx="17"/>
|
||||
<text class="nj-exceptions-stagetext" x="180" y="1746">merge into develop</text>
|
||||
<line class="nj-exceptions-edge" x1="180" y1="1758" x2="180" y2="1784" marker-end="url(#nj-qa-arrow-n)"/>
|
||||
<rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="30" y="1786" width="300" height="46" rx="23"/>
|
||||
<text class="nj-exceptions-stagetext" x="180" y="1808">release</text>
|
||||
<text class="nj-exceptions-stagesub" x="180" y="1824">no open vulnerability alert</text>
|
||||
<line class="nj-exceptions-edge" x1="180" y1="1642" x2="180" y2="1668" marker-end="url(#nj-qa-arrow-n)"/>
|
||||
<rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="65" y="1670" width="230" height="34" rx="17"/>
|
||||
<text class="nj-exceptions-stagetext" x="180" y="1692">merge into develop</text>
|
||||
<line class="nj-exceptions-edge" x1="180" y1="1704" x2="180" y2="1730" marker-end="url(#nj-qa-arrow-n)"/>
|
||||
<rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="30" y="1732" width="300" height="46" rx="23"/>
|
||||
<text class="nj-exceptions-stagetext" x="180" y="1754">release</text>
|
||||
<text class="nj-exceptions-stagesub" x="180" y="1770">no open vulnerability alert</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 22 KiB |
@@ -22,24 +22,19 @@ P = "nj-exceptions-"
|
||||
TRIGGERS = {
|
||||
"push": ("every push / PR", "push"),
|
||||
"pr": ("every PR", "push"),
|
||||
"merge": ("before merge", "push"),
|
||||
"nightly": ("nightly", "sched"),
|
||||
"sched": ("scheduled", "sched"),
|
||||
"cont": ("continuous", "cont"),
|
||||
}
|
||||
|
||||
# title, triggers, lines (at most 31 characters, so they fit a card of the three-column drawing)
|
||||
LANES = [
|
||||
("Build matrix", ["push", "merge"], [
|
||||
("Build matrix", ["push"], [
|
||||
"50+ compilers: GCC, Clang, MSVC,",
|
||||
"AppleClang, Intel, NVIDIA, ...",
|
||||
"C++11, 14, 17, 20, 23, and 26",
|
||||
"libc++ and libstdc++",
|
||||
"warnings: Clang -Weverything,",
|
||||
"GCC 300+ warnings",
|
||||
"a subset on every push and PR,",
|
||||
"all before merge; Intel, NVIDIA,",
|
||||
"CUDA, and ARM64 nightly",
|
||||
]),
|
||||
("Unit tests (doctest)", ["push"], [
|
||||
"every public API function",
|
||||
@@ -49,7 +44,7 @@ LANES = [
|
||||
"custom number, string, array,",
|
||||
"and object types",
|
||||
]),
|
||||
("Coverage", ["nightly"], [
|
||||
("Coverage", ["push"], [
|
||||
"unit tests cover all lines",
|
||||
"of the code base",
|
||||
]),
|
||||
@@ -58,11 +53,11 @@ LANES = [
|
||||
"Clang Static Analyzer (52",
|
||||
"checkers), IWYU, Codacy",
|
||||
"CodeQL, Semgrep, Flawfinder",
|
||||
"(also scheduled); Infer nightly",
|
||||
"(also scheduled)",
|
||||
]),
|
||||
("Dynamic analysis", ["push", "nightly"], [
|
||||
("Dynamic analysis", ["push"], [
|
||||
"runtime assertions",
|
||||
"Valgrind (Memcheck), nightly",
|
||||
"Valgrind (Memcheck)",
|
||||
"address and undefined behavior",
|
||||
"sanitizers, integer overflow,",
|
||||
"nullability violations",
|
||||
@@ -183,7 +178,7 @@ def drawing(W, cols, suffix):
|
||||
defs = ('<defs><marker id="%s" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">'
|
||||
'<path class="fg-head" d="M0 1 L10 5 L0 9 z"/></marker></defs>' % mid)
|
||||
tid, did = "nj-qa-title-" + suffix, "nj-qa-desc-" + suffix
|
||||
desc = ("A push or pull request starts these checks; the badges say when each runs: " +
|
||||
desc = ("Every push or pull request starts these checks in parallel: " +
|
||||
"; ".join("%s (%s): %s" % (t, ", ".join(TRIGGERS[k][0] for k in trig), " ".join(lines)) for t, trig, lines in LANES) +
|
||||
". When they pass, the change is merged into develop; a release is made only when no vulnerability alert is open.")
|
||||
out = ['<svg class="nj-fig %sqa %sqa--%s" viewBox="0 0 %d %d" role="img" aria-labelledby="%s %s" xmlns="http://www.w3.org/2000/svg">'
|
||||
|
||||
@@ -91,7 +91,7 @@ const typename BasicJsonType::binary_t& binary_value(const BasicJsonType& j)
|
||||
}
|
||||
|
||||
template<typename View, enable_if_t<is_json_view<View>::value, int> = 0>
|
||||
typename View::basic_json_type::binary_t binary_value(const View& /*unused*/)
|
||||
typename View::basic_json_type::binary_t binary_value(const View& /*unused*/) // LCOV_EXCL_LINE
|
||||
{
|
||||
return {}; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
@@ -431,6 +431,28 @@ std::nullptr_t exception_context(const View& /*unused*/) noexcept
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
namespace view
|
||||
{
|
||||
// the path and bytes of the value of a view (defined in <nlohmann/detail/view/errors.hpp>)
|
||||
template<typename View>
|
||||
std::string diagnostics_of(const View& v);
|
||||
} // namespace view
|
||||
|
||||
// what goes in front of the message of an exception about j: nothing for a
|
||||
// basic_json value (exception_context() passes the value itself), the context
|
||||
// basic_json would add (JSON_DIAGNOSTICS, JSON_DIAGNOSTIC_POSITIONS) for a view
|
||||
template<typename BasicJsonType, enable_if_t<is_basic_json<BasicJsonType>::value, int> = 0>
|
||||
const char* exception_prefix(const BasicJsonType& /*unused*/) noexcept
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
template<typename View, enable_if_t<is_json_view<View>::value, int> = 0>
|
||||
std::string exception_prefix(const View& v)
|
||||
{
|
||||
return view::diagnostics_of(v);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief throws because @a j does not have the type a conversion expects
|
||||
@param[in] expected the expected type(s), e.g. "array" or "binary or array"
|
||||
@@ -442,7 +464,7 @@ JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_type_must_be(const cha
|
||||
{
|
||||
static_cast<void>(expected); // unused when JSON_NOEXCEPTION is defined
|
||||
static_cast<void>(j);
|
||||
JSON_THROW(type_error::create(exception_id::type_mismatch, concat("type must be ", expected, ", but is ", j.type_name()), exception_context(j)));
|
||||
JSON_THROW(type_error::create(exception_id::type_mismatch, concat(exception_prefix(j), "type must be ", expected, ", but is ", j.type_name()), exception_context(j)));
|
||||
}
|
||||
|
||||
/*!
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <bit> //byteswap
|
||||
#endif
|
||||
|
||||
#include <nlohmann/detail/bit_ops.hpp>
|
||||
#include <nlohmann/detail/exceptions.hpp>
|
||||
#include <nlohmann/detail/input/input_adapters.hpp>
|
||||
#include <nlohmann/detail/input/json_sax.hpp>
|
||||
@@ -465,15 +466,17 @@ class binary_reader
|
||||
}
|
||||
|
||||
const std::size_t element_type_parse_position = chars_read;
|
||||
// an array's elements are named "0", "1", ... in the wire format,
|
||||
// and those names are not passed on: they are skipped unless
|
||||
// error_handler_t::strict must reject ill-formed UTF-8 in them
|
||||
const bool is_object = container_stack.back().is_object;
|
||||
key.clear();
|
||||
if (JSON_HEDLEY_UNLIKELY(!get_bson_cstr(key)))
|
||||
if (JSON_HEDLEY_UNLIKELY(!get_bson_cstr(key, is_object || error_handler == error_handler_t::strict)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// an array's elements are named "0", "1", ... in the wire format,
|
||||
// and those names are not passed on
|
||||
if (container_stack.back().is_object && !sax->key(key))
|
||||
if (is_object && !sax->key(key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -489,14 +492,18 @@ class binary_reader
|
||||
@brief Parses a C-style string from the BSON input.
|
||||
@param[in,out] result A reference to the string variable where the read
|
||||
string is to be stored.
|
||||
@param[in] store whether the characters are needed; if not (the names of
|
||||
an array's elements), they are skipped and @a result may
|
||||
be left unchanged, and they are not checked against
|
||||
@ref error_handler
|
||||
@return `true` if the \\x00-byte indicating the end of the string was
|
||||
encountered before the EOF; false` indicates an unexpected EOF.
|
||||
*/
|
||||
bool get_bson_cstr(string_t& result)
|
||||
bool get_bson_cstr(string_t& result, const bool store = true)
|
||||
{
|
||||
if (get_bson_cstr_bulk(result, std::integral_constant<bool, bulk_scan> {}))
|
||||
if (get_bson_cstr_bulk(result, store, std::integral_constant<bool, bulk_scan> {}))
|
||||
{
|
||||
return check_string_utf8(result, "key");
|
||||
return !store || check_string_utf8(result, "key");
|
||||
}
|
||||
|
||||
auto out = std::back_inserter(result);
|
||||
@@ -509,7 +516,7 @@ class binary_reader
|
||||
}
|
||||
if (current == 0x00)
|
||||
{
|
||||
return check_string_utf8(result, "key");
|
||||
return !store || check_string_utf8(result, "key");
|
||||
}
|
||||
*out++ = static_cast<typename string_t::value_type>(current);
|
||||
}
|
||||
@@ -519,10 +526,11 @@ class binary_reader
|
||||
@brief read a C-style string from contiguous input in one step
|
||||
|
||||
@param[in,out] result the string to append to
|
||||
@param[in] store whether to append the characters to @a result
|
||||
@return whether the string was read; if the input has no \\x00-byte, nothing
|
||||
is read, and @ref get_bson_cstr reports the end of the input
|
||||
*/
|
||||
bool get_bson_cstr_bulk(string_t& result, std::true_type /*bulk*/)
|
||||
bool get_bson_cstr_bulk(string_t& result, const bool store, std::true_type /*bulk*/)
|
||||
{
|
||||
const std::size_t remaining = ia.bulk_remaining();
|
||||
if (remaining == 0)
|
||||
@@ -530,18 +538,15 @@ class binary_reader
|
||||
return false;
|
||||
}
|
||||
const auto* const data = reinterpret_cast<const unsigned char*>(ia.bulk_data());
|
||||
// a plain loop rather than std::memchr: most keys are short (array
|
||||
// indices are keys, too), and the call would cost more than it saves
|
||||
std::size_t length = 0;
|
||||
while (length < remaining && data[length] != 0x00)
|
||||
{
|
||||
++length;
|
||||
}
|
||||
const std::size_t length = find_zero_byte(data, remaining);
|
||||
if (length == remaining)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
result.append(reinterpret_cast<const typename string_t::value_type*>(data), length);
|
||||
if (store)
|
||||
{
|
||||
result.append(reinterpret_cast<const typename string_t::value_type*>(data), length);
|
||||
}
|
||||
// consume the string and its \x00-byte, as the byte-wise path does
|
||||
ia.bulk_skip(length + 1);
|
||||
chars_read += length + 1;
|
||||
@@ -550,7 +555,7 @@ class binary_reader
|
||||
}
|
||||
|
||||
/// input that is not contiguous: C-style strings are read byte by byte
|
||||
bool get_bson_cstr_bulk(string_t& /*result*/, std::false_type /*bulk*/) const noexcept
|
||||
bool get_bson_cstr_bulk(string_t& /*result*/, const bool /*store*/, std::false_type /*bulk*/) const noexcept
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -741,11 +746,13 @@ class binary_reader
|
||||
bool get_cbor_negative_integer()
|
||||
{
|
||||
NumberType number{};
|
||||
if (JSON_HEDLEY_UNLIKELY(!get_number(number)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return get_number(number) && emit_cbor_negative_integer(number);
|
||||
}
|
||||
|
||||
/// pass the CBOR negative integer -1 - @a number to the SAX parser
|
||||
template<typename NumberType>
|
||||
bool emit_cbor_negative_integer(const NumberType number)
|
||||
{
|
||||
// the value is -1 - number, which fits into number_integer_t
|
||||
// whenever number does; the outer cast undoes the integral promotion
|
||||
// for number_integer_t types narrower than int
|
||||
@@ -970,7 +977,7 @@ class binary_reader
|
||||
case 0x95:
|
||||
case 0x96:
|
||||
case 0x97:
|
||||
return enter_array(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x1Fu));
|
||||
return enter_cbor_array(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x1Fu));
|
||||
|
||||
case 0x98: // array (one-byte uint8_t for n follows)
|
||||
case 0x99: // array (two-byte uint16_t for n follow)
|
||||
@@ -979,7 +986,7 @@ class binary_reader
|
||||
{
|
||||
std::uint64_t len{};
|
||||
std::size_t size{};
|
||||
return get_cbor_argument(len) && get_cbor_container_size(len, size, "array") && enter_array(size);
|
||||
return get_cbor_argument(len) && get_cbor_container_size(len, size, "array") && enter_cbor_array(size);
|
||||
}
|
||||
|
||||
case 0x9F: // array (indefinite length)
|
||||
@@ -1133,16 +1140,10 @@ class binary_reader
|
||||
return get_half_float(false);
|
||||
|
||||
case 0xFA: // Single-Precision Float (four-byte IEEE 754)
|
||||
{
|
||||
float number{};
|
||||
return get_number(number) && emit_float(number);
|
||||
}
|
||||
return get_cbor_float<float>();
|
||||
|
||||
case 0xFB: // Double-Precision Float (eight-byte IEEE 754)
|
||||
{
|
||||
double number{};
|
||||
return get_number(number) && emit_float(number);
|
||||
}
|
||||
return get_cbor_float<double>();
|
||||
|
||||
default: // anything else (0xFF is handled inside the other types)
|
||||
{
|
||||
@@ -1151,6 +1152,14 @@ class binary_reader
|
||||
}
|
||||
}
|
||||
|
||||
/// read a single- or double-precision float
|
||||
template<typename NumberType>
|
||||
bool get_cbor_float()
|
||||
{
|
||||
NumberType number{};
|
||||
return get_number(number) && emit_float(number);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief reports a nested indefinite-length CBOR string or byte array
|
||||
@param[in] type_name name of the rejected string type
|
||||
@@ -1676,6 +1685,155 @@ class binary_reader
|
||||
return true;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief read a big-endian number of type @a NumberType at @a data
|
||||
*/
|
||||
template<typename NumberType>
|
||||
NumberType load_big_endian(const unsigned char* data) const noexcept
|
||||
{
|
||||
NumberType result{};
|
||||
std::memcpy(&result, data, sizeof(NumberType));
|
||||
if (is_little_endian)
|
||||
{
|
||||
byte_swap(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief read the integers, floats, booleans, and nulls that follow in a
|
||||
definite-length array, without the per-element bookkeeping of
|
||||
@ref parse_cbor_internal
|
||||
|
||||
Stops at the end of the array, at any other kind of value, and when fewer
|
||||
than 9 bytes (the longest of these values) are left in the input, leaving
|
||||
those to @ref parse_cbor_value, which reads them the same way.
|
||||
|
||||
Out of line, so that @ref parse_cbor_value stays small enough to be inlined.
|
||||
|
||||
The array is the one on top of @ref container_stack; its count of
|
||||
elements not yet read is updated.
|
||||
|
||||
@return whether the SAX parser accepted the values
|
||||
*/
|
||||
JSON_HEDLEY_NEVER_INLINE
|
||||
bool parse_cbor_scalar_run(std::true_type /*bulk*/)
|
||||
{
|
||||
// nothing pushes onto the stack while the run reads scalars
|
||||
std::size_t& remaining = container_stack.back().remaining;
|
||||
while (remaining != 0 && ia.bulk_remaining() >= 9)
|
||||
{
|
||||
const auto* const data = reinterpret_cast<const unsigned char*>(ia.bulk_data());
|
||||
const unsigned char head = data[0];
|
||||
std::size_t size = 0;
|
||||
bool accepted = false;
|
||||
switch (head)
|
||||
{
|
||||
case 0x18:
|
||||
size = 2;
|
||||
accepted = emit_unsigned(data[1]);
|
||||
break;
|
||||
case 0x19:
|
||||
size = 3;
|
||||
accepted = emit_unsigned(load_big_endian<std::uint16_t>(data + 1));
|
||||
break;
|
||||
case 0x1A:
|
||||
size = 5;
|
||||
accepted = emit_unsigned(load_big_endian<std::uint32_t>(data + 1));
|
||||
break;
|
||||
case 0x1B:
|
||||
size = 9;
|
||||
accepted = emit_unsigned(load_big_endian<std::uint64_t>(data + 1));
|
||||
break;
|
||||
case 0x38:
|
||||
size = 2;
|
||||
accepted = emit_cbor_negative_integer(data[1]);
|
||||
break;
|
||||
case 0x39:
|
||||
size = 3;
|
||||
accepted = emit_cbor_negative_integer(load_big_endian<std::uint16_t>(data + 1));
|
||||
break;
|
||||
case 0x3A:
|
||||
size = 5;
|
||||
accepted = emit_cbor_negative_integer(load_big_endian<std::uint32_t>(data + 1));
|
||||
break;
|
||||
case 0x3B:
|
||||
size = 9;
|
||||
accepted = emit_cbor_negative_integer(load_big_endian<std::uint64_t>(data + 1));
|
||||
break;
|
||||
case 0xF4:
|
||||
size = 1;
|
||||
accepted = sax->boolean(false);
|
||||
break;
|
||||
case 0xF5:
|
||||
size = 1;
|
||||
accepted = sax->boolean(true);
|
||||
break;
|
||||
case 0xF6:
|
||||
size = 1;
|
||||
accepted = sax->null();
|
||||
break;
|
||||
case 0xFA:
|
||||
size = 5;
|
||||
accepted = emit_float(load_big_endian<float>(data + 1));
|
||||
break;
|
||||
case 0xFB:
|
||||
size = 9;
|
||||
accepted = emit_float(load_big_endian<double>(data + 1));
|
||||
break;
|
||||
default:
|
||||
if (head <= 0x17)
|
||||
{
|
||||
size = 1;
|
||||
accepted = sax->number_unsigned(static_cast<number_unsigned_t>(head));
|
||||
}
|
||||
else if (head >= 0x20 && head <= 0x37)
|
||||
{
|
||||
size = 1;
|
||||
accepted = sax->number_integer(static_cast<std::int8_t>(0x20 - 1 - head));
|
||||
}
|
||||
else
|
||||
{
|
||||
// any other value is read by parse_cbor_value
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// as if read by get() and get_number(): current is the head byte
|
||||
ia.bulk_skip(size);
|
||||
chars_read += size;
|
||||
current = head;
|
||||
--remaining;
|
||||
if (JSON_HEDLEY_UNLIKELY(!accepted))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief open a definite-length array and read the scalars it begins with
|
||||
(see @ref parse_cbor_scalar_run)
|
||||
|
||||
Only the start of an array is tried, which covers arrays of numbers
|
||||
(e.g. coordinates) but costs arrays of other values only one call, not one
|
||||
per element.
|
||||
|
||||
@param[in] len the number of elements
|
||||
@return whether the SAX parser accepted the array and the values read
|
||||
*/
|
||||
bool enter_cbor_array(const std::size_t len)
|
||||
{
|
||||
return enter_array(len) && (len == 0 || parse_cbor_scalar_run(std::integral_constant<bool, bulk_scan> {}));
|
||||
}
|
||||
|
||||
/// input that is not contiguous: every value is read by parse_cbor_value
|
||||
bool parse_cbor_scalar_run(std::false_type /*bulk*/) const noexcept
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief read a CBOR value and everything nested inside it
|
||||
|
||||
@@ -1705,6 +1863,7 @@ class binary_reader
|
||||
// pop_back() below, which destroys the container_stack element
|
||||
// it would otherwise alias
|
||||
const container_frame top = container_stack.back();
|
||||
const bool is_object = top.is_object;
|
||||
bool at_end = false;
|
||||
|
||||
if (top.remaining != npos)
|
||||
@@ -1716,7 +1875,7 @@ class binary_reader
|
||||
{
|
||||
// claim the element about to be read
|
||||
--container_stack.back().remaining;
|
||||
if (top.is_object)
|
||||
if (is_object)
|
||||
{
|
||||
get();
|
||||
}
|
||||
@@ -1729,7 +1888,7 @@ class binary_reader
|
||||
// Testing for it consumes a byte, which is the first byte
|
||||
// of the next element when it is not one.
|
||||
at_end = (get() == 0xFF);
|
||||
fetch = top.is_object;
|
||||
fetch = is_object;
|
||||
}
|
||||
|
||||
if (at_end)
|
||||
@@ -1746,7 +1905,7 @@ class binary_reader
|
||||
continue;
|
||||
}
|
||||
|
||||
if (top.is_object)
|
||||
if (is_object)
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(!skip_cbor_key_tags(tag_handler)))
|
||||
{
|
||||
@@ -1971,7 +2130,7 @@ class binary_reader
|
||||
case 0x9D:
|
||||
case 0x9E:
|
||||
case 0x9F:
|
||||
return enter_array(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x0Fu));
|
||||
return enter_msgpack_array(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x0Fu));
|
||||
|
||||
// fixstr
|
||||
case 0xA0:
|
||||
@@ -2102,13 +2261,13 @@ class binary_reader
|
||||
case 0xDC: // array 16
|
||||
{
|
||||
std::uint16_t len{};
|
||||
return get_number(len) && enter_array(static_cast<std::size_t>(len));
|
||||
return get_number(len) && enter_msgpack_array(static_cast<std::size_t>(len));
|
||||
}
|
||||
|
||||
case 0xDD: // array 32
|
||||
{
|
||||
std::uint32_t len{};
|
||||
return get_number(len) && enter_array(conditional_static_cast<std::size_t>(len));
|
||||
return get_number(len) && enter_msgpack_array(conditional_static_cast<std::size_t>(len));
|
||||
}
|
||||
|
||||
case 0xDE: // map 16
|
||||
@@ -2450,6 +2609,137 @@ class binary_reader
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief read the integers, floats, booleans, and nils that follow in a
|
||||
MessagePack array, without the per-element bookkeeping of
|
||||
@ref parse_msgpack_internal
|
||||
|
||||
Stops at the end of the array, at any other kind of value, and when fewer
|
||||
than 9 bytes (the longest of these values) are left in the input, leaving
|
||||
those to @ref parse_msgpack_value, which reads them the same way.
|
||||
|
||||
Out of line, so that @ref parse_msgpack_value stays small enough to be inlined.
|
||||
|
||||
The array is the one on top of @ref container_stack; its count of
|
||||
elements not yet read is updated.
|
||||
|
||||
@return whether the SAX parser accepted the values
|
||||
*/
|
||||
JSON_HEDLEY_NEVER_INLINE
|
||||
bool parse_msgpack_scalar_run(std::true_type /*bulk*/)
|
||||
{
|
||||
// nothing pushes onto the stack while the run reads scalars
|
||||
std::size_t& remaining = container_stack.back().remaining;
|
||||
while (remaining != 0 && ia.bulk_remaining() >= 9)
|
||||
{
|
||||
const auto* const data = reinterpret_cast<const unsigned char*>(ia.bulk_data());
|
||||
const unsigned char head = data[0];
|
||||
std::size_t size = 0;
|
||||
bool accepted = false;
|
||||
switch (head)
|
||||
{
|
||||
case 0xC0: // nil
|
||||
size = 1;
|
||||
accepted = sax->null();
|
||||
break;
|
||||
case 0xC2: // false
|
||||
size = 1;
|
||||
accepted = sax->boolean(false);
|
||||
break;
|
||||
case 0xC3: // true
|
||||
size = 1;
|
||||
accepted = sax->boolean(true);
|
||||
break;
|
||||
case 0xCA: // float 32
|
||||
size = 5;
|
||||
accepted = emit_float(load_big_endian<float>(data + 1));
|
||||
break;
|
||||
case 0xCB: // float 64
|
||||
size = 9;
|
||||
accepted = emit_float(load_big_endian<double>(data + 1));
|
||||
break;
|
||||
case 0xCC: // uint 8
|
||||
size = 2;
|
||||
accepted = emit_unsigned(data[1]);
|
||||
break;
|
||||
case 0xCD: // uint 16
|
||||
size = 3;
|
||||
accepted = emit_unsigned(load_big_endian<std::uint16_t>(data + 1));
|
||||
break;
|
||||
case 0xCE: // uint 32
|
||||
size = 5;
|
||||
accepted = emit_unsigned(load_big_endian<std::uint32_t>(data + 1));
|
||||
break;
|
||||
case 0xCF: // uint 64
|
||||
size = 9;
|
||||
accepted = emit_unsigned(load_big_endian<std::uint64_t>(data + 1));
|
||||
break;
|
||||
case 0xD0: // int 8
|
||||
size = 2;
|
||||
accepted = emit_signed(static_cast<std::int8_t>(data[1]));
|
||||
break;
|
||||
case 0xD1: // int 16
|
||||
size = 3;
|
||||
accepted = emit_signed(load_big_endian<std::int16_t>(data + 1));
|
||||
break;
|
||||
case 0xD2: // int 32
|
||||
size = 5;
|
||||
accepted = emit_signed(load_big_endian<std::int32_t>(data + 1));
|
||||
break;
|
||||
case 0xD3: // int 64
|
||||
size = 9;
|
||||
accepted = emit_signed(load_big_endian<std::int64_t>(data + 1));
|
||||
break;
|
||||
default:
|
||||
if (head <= 0x7F)
|
||||
{
|
||||
// positive fixint
|
||||
size = 1;
|
||||
accepted = sax->number_unsigned(static_cast<number_unsigned_t>(head));
|
||||
}
|
||||
else if (head >= 0xE0)
|
||||
{
|
||||
// negative fixint
|
||||
size = 1;
|
||||
accepted = sax->number_integer(static_cast<std::int8_t>(head));
|
||||
}
|
||||
else
|
||||
{
|
||||
// any other value is read by parse_msgpack_value
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// as if read by get() and get_number(): current is the last byte
|
||||
ia.bulk_skip(size);
|
||||
chars_read += size;
|
||||
current = data[size - 1];
|
||||
--remaining;
|
||||
if (JSON_HEDLEY_UNLIKELY(!accepted))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief open a MessagePack array and read the scalars it begins with (see
|
||||
@ref parse_msgpack_scalar_run)
|
||||
@param[in] len the number of elements
|
||||
@return whether the SAX parser accepted the array and the values read
|
||||
*/
|
||||
bool enter_msgpack_array(const std::size_t len)
|
||||
{
|
||||
return enter_array(len) && (len == 0 || parse_msgpack_scalar_run(std::integral_constant<bool, bulk_scan> {}));
|
||||
}
|
||||
|
||||
/// input that is not contiguous: every value is read by parse_msgpack_value
|
||||
bool parse_msgpack_scalar_run(std::false_type /*bulk*/) const noexcept
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief read a MessagePack value and everything nested inside it
|
||||
|
||||
@@ -3255,6 +3545,107 @@ class binary_reader
|
||||
return unexpected_byte("invalid byte", "value");
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief read the elements of an optimized array of numbers (type marker
|
||||
@a type) that are in the input, in one loop, without the
|
||||
per-element bookkeeping of @ref parse_ubjson_internal
|
||||
|
||||
Values are emitted as @ref get_ubjson_value emits them. Types other than
|
||||
the fixed-size integers and floats are left to @ref get_ubjson_value, as
|
||||
are the elements the input ends before.
|
||||
|
||||
@param[in,out] remaining the number of elements not yet read
|
||||
@param[in] type the type marker of the elements
|
||||
@return whether the SAX parser accepted the values
|
||||
*/
|
||||
JSON_HEDLEY_NEVER_INLINE
|
||||
bool parse_ubjson_typed_run(std::size_t& remaining, const char_int_type type, std::true_type /*bulk*/)
|
||||
{
|
||||
const bool bjdata = input_format == input_format_t::bjdata;
|
||||
switch (type)
|
||||
{
|
||||
case 'U':
|
||||
return parse_ubjson_typed_run<std::uint8_t>(remaining);
|
||||
case 'i':
|
||||
return parse_ubjson_typed_run<std::int8_t>(remaining);
|
||||
case 'I':
|
||||
return parse_ubjson_typed_run<std::int16_t>(remaining);
|
||||
case 'l':
|
||||
return parse_ubjson_typed_run<std::int32_t>(remaining);
|
||||
case 'L':
|
||||
return parse_ubjson_typed_run<std::int64_t>(remaining);
|
||||
case 'u':
|
||||
return !bjdata || parse_ubjson_typed_run<std::uint16_t>(remaining);
|
||||
case 'm':
|
||||
return !bjdata || parse_ubjson_typed_run<std::uint32_t>(remaining);
|
||||
case 'M':
|
||||
return !bjdata || parse_ubjson_typed_run<std::uint64_t>(remaining);
|
||||
case 'd':
|
||||
return parse_ubjson_typed_run<float>(remaining);
|
||||
case 'D':
|
||||
return parse_ubjson_typed_run<double>(remaining);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// input that is not contiguous: every element is read by get_ubjson_value
|
||||
bool parse_ubjson_typed_run(std::size_t& /*remaining*/, const char_int_type /*type*/, std::false_type /*bulk*/) const noexcept
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
template<typename NumberType>
|
||||
bool parse_ubjson_typed_run(std::size_t& remaining)
|
||||
{
|
||||
constexpr std::size_t size = sizeof(NumberType);
|
||||
const std::size_t count = (std::min)(remaining, ia.bulk_remaining() / size);
|
||||
if (count == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
const auto* const data = reinterpret_cast<const unsigned char*>(ia.bulk_data());
|
||||
// UBJSON stores numbers big-endian, BJData little-endian
|
||||
const bool swap = is_little_endian != (input_format == input_format_t::bjdata);
|
||||
std::size_t read = 0;
|
||||
bool accepted = true;
|
||||
while (read < count && accepted)
|
||||
{
|
||||
NumberType number{};
|
||||
std::memcpy(&number, data + (read * size), size);
|
||||
if (swap)
|
||||
{
|
||||
byte_swap(number);
|
||||
}
|
||||
++read;
|
||||
accepted = emit_ubjson_number(number);
|
||||
}
|
||||
// as if read by get_number(): current is the last byte read
|
||||
ia.bulk_skip(read * size);
|
||||
chars_read += read * size;
|
||||
current = data[(read * size) - 1];
|
||||
remaining -= read;
|
||||
return accepted;
|
||||
}
|
||||
|
||||
template<typename NumberType, enable_if_t<std::is_floating_point<NumberType>::value, int> = 0>
|
||||
bool emit_ubjson_number(const NumberType number)
|
||||
{
|
||||
return emit_float(number);
|
||||
}
|
||||
|
||||
template < typename NumberType, enable_if_t < std::is_integral<NumberType>::value&& std::is_signed<NumberType>::value, int > = 0 >
|
||||
bool emit_ubjson_number(const NumberType number)
|
||||
{
|
||||
return emit_signed(number);
|
||||
}
|
||||
|
||||
template < typename NumberType, enable_if_t < std::is_integral<NumberType>::value&& !std::is_signed<NumberType>::value, int > = 0 >
|
||||
bool emit_ubjson_number(const NumberType number)
|
||||
{
|
||||
return emit_unsigned(number);
|
||||
}
|
||||
|
||||
/*!
|
||||
@return whether array creation completed
|
||||
*/
|
||||
@@ -3290,7 +3681,15 @@ class binary_reader
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < size_and_type.first; ++i)
|
||||
// the numbers that are in the input, in one loop; the rest (if
|
||||
// the input ends early) one by one below
|
||||
std::size_t remaining = size_and_type.first;
|
||||
if (!nd_is_char && JSON_HEDLEY_UNLIKELY(!parse_ubjson_typed_run(remaining, size_and_type.second, std::integral_constant<bool, bulk_scan> {})))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t i = size_and_type.first - remaining; i < size_and_type.first; ++i)
|
||||
{
|
||||
if (nd_is_char)
|
||||
{
|
||||
@@ -3350,7 +3749,8 @@ class binary_reader
|
||||
container_stack.back().remaining = 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
return size_and_type.second == 0
|
||||
|| parse_ubjson_typed_run(container_stack.back().remaining, size_and_type.second, std::integral_constant<bool, bulk_scan> {});
|
||||
}
|
||||
|
||||
return enter_array(detail::unknown_size());
|
||||
@@ -3691,13 +4091,13 @@ class binary_reader
|
||||
return sax->null();
|
||||
|
||||
case 0xFB:
|
||||
return sax->number_float(static_cast<number_float_t>(-1.0), "");
|
||||
return sax->number_float(static_cast<number_float_t>(-1.0), no_source);
|
||||
|
||||
case 0xFC:
|
||||
return sax->number_float(static_cast<number_float_t>(0.0), "");
|
||||
return sax->number_float(static_cast<number_float_t>(0.0), no_source);
|
||||
|
||||
case 0xFD:
|
||||
return sax->number_float(static_cast<number_float_t>(1.0), "");
|
||||
return sax->number_float(static_cast<number_float_t>(1.0), no_source);
|
||||
|
||||
case 0xFF: // empty string
|
||||
{
|
||||
@@ -4020,7 +4420,12 @@ class binary_reader
|
||||
`char_traits<char_type>::eof()` in that case.
|
||||
|
||||
@return character read from the input
|
||||
|
||||
@note always inlined: GCC otherwise stops inlining the stream adapter's
|
||||
get_character() into parse_cbor_internal once that function grows
|
||||
(+0.45 % instructions for CBOR from a std::istringstream with GCC 13)
|
||||
*/
|
||||
JSON_HEDLEY_ALWAYS_INLINE
|
||||
char_int_type get()
|
||||
{
|
||||
++chars_read;
|
||||
@@ -4066,6 +4471,27 @@ class binary_reader
|
||||
return current;
|
||||
}
|
||||
|
||||
/// reverse the byte order of a double with a single byteswap(): GCC does
|
||||
/// not merge the byte-wise swap of the generic version into one
|
||||
static void byte_swap(double& number) noexcept
|
||||
{
|
||||
static_assert(sizeof(double) == sizeof(std::uint64_t), "double must have 64 bits");
|
||||
std::uint64_t bits{};
|
||||
std::memcpy(&bits, &number, sizeof(bits));
|
||||
bits = byteswap(bits);
|
||||
std::memcpy(&number, &bits, sizeof(bits));
|
||||
}
|
||||
|
||||
/// reverse the byte order of a float with a single byteswap()
|
||||
static void byte_swap(float& number) noexcept
|
||||
{
|
||||
static_assert(sizeof(float) == sizeof(std::uint32_t), "float must have 32 bits");
|
||||
std::uint32_t bits{};
|
||||
std::memcpy(&bits, &number, sizeof(bits));
|
||||
bits = byteswap(bits);
|
||||
std::memcpy(&number, &bits, sizeof(bits));
|
||||
}
|
||||
|
||||
template<class NumberType>
|
||||
static void byte_swap(NumberType& number)
|
||||
{
|
||||
@@ -4201,7 +4627,7 @@ class binary_reader
|
||||
{
|
||||
return out_of_range_error(exception_id::number_overflow, "number overflow", "value");
|
||||
}
|
||||
return sax->number_float(result, "");
|
||||
return sax->number_float(result, no_source);
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -4261,7 +4687,7 @@ class binary_reader
|
||||
}();
|
||||
return sax->number_float((half & 0x8000u) != 0
|
||||
? static_cast<number_float_t>(-val)
|
||||
: static_cast<number_float_t>(val), "");
|
||||
: static_cast<number_float_t>(val), no_source);
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -4293,6 +4719,19 @@ class binary_reader
|
||||
return get_bytes(len, "string", result);
|
||||
}
|
||||
|
||||
/// whether @a s is valid UTF-8: ASCII eight bytes at a time, the rest
|
||||
/// sequence by sequence (as the binary writers check strings)
|
||||
static bool is_valid_utf8_string(const string_t& s, std::true_type /*is_byte*/) noexcept
|
||||
{
|
||||
return valid_utf8_string(reinterpret_cast<const unsigned char*>(s.data()), s.size());
|
||||
}
|
||||
|
||||
/// whether @a s, of a character type wider than a byte, is valid UTF-8
|
||||
static bool is_valid_utf8_string(const string_t& s, std::false_type /*is_byte*/) noexcept
|
||||
{
|
||||
return is_valid_utf8(s);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief validate a decoded text string (value or object key) against @ref error_handler
|
||||
|
||||
@@ -4312,7 +4751,16 @@ class binary_reader
|
||||
*/
|
||||
bool check_string_utf8(string_t& result, const char* context)
|
||||
{
|
||||
if (error_handler == error_handler_t::keep || is_valid_utf8(result))
|
||||
return error_handler == error_handler_t::keep || check_string_utf8_handler(result, context);
|
||||
}
|
||||
|
||||
/// @ref check_string_utf8 for an error handler other than @ref
|
||||
/// error_handler_t::keep; out of line, so that reading strings without a
|
||||
/// check does not pay for the code of the check
|
||||
JSON_HEDLEY_NEVER_INLINE
|
||||
bool check_string_utf8_handler(string_t& result, const char* context)
|
||||
{
|
||||
if (is_valid_utf8_string(result, std::integral_constant<bool, sizeof(typename string_t::value_type) == 1>()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -4586,6 +5034,10 @@ class binary_reader
|
||||
/// the SAX parser
|
||||
json_sax_t* sax = nullptr;
|
||||
|
||||
/// the (empty) source text of every number_float event: binary formats
|
||||
/// have none, and a string_t built for each float would cost more
|
||||
const string_t no_source{};
|
||||
|
||||
/// the containers that have been opened and not closed yet; see @ref container_frame
|
||||
std::vector<container_frame> container_stack{};
|
||||
|
||||
|
||||
@@ -572,7 +572,7 @@ class input_stream_adapter
|
||||
}
|
||||
else
|
||||
{
|
||||
current_in_get_area = true;
|
||||
current_in_get_area = true; // LCOV_EXCL_LINE (called only when the get area is exhausted)
|
||||
}
|
||||
#endif
|
||||
return res;
|
||||
|
||||
@@ -999,7 +999,7 @@ class json_sax_dom_parser : public json_sax_dom_error_state
|
||||
ordered_data.members[f.ordered.dest_member].second = std::move(value);
|
||||
}
|
||||
|
||||
static void place_member(const frame& /*f*/, BasicJsonType&& /*value*/, std::false_type /*indexed*/) noexcept {}
|
||||
static void place_member(const frame& /*f*/, BasicJsonType&& /*value*/, std::false_type /*indexed*/) noexcept {} // LCOV_EXCL_LINE
|
||||
|
||||
/// store the members of a new object directly in it
|
||||
bool start_stored_object(std::size_t len)
|
||||
|
||||
@@ -209,8 +209,13 @@ constexpr bool input_adapter_bulk_window(std::false_type /*detected*/)
|
||||
@brief lexical analysis
|
||||
|
||||
This class organizes the lexical analysis during JSON deserialization.
|
||||
|
||||
DiscardValues: the caller only needs the token types and never looks at the values of number and string tokens (only
|
||||
accept(), which parses through json_sax_acceptor). A template parameter rather than a member, so that the lexer of
|
||||
parse() is compiled without the paths of accept(): with both string scans in one scan(), parse() was up to 2 % slower
|
||||
on Apple M1.
|
||||
*/
|
||||
template<typename BasicJsonType, typename InputAdapterType>
|
||||
template<typename BasicJsonType, typename InputAdapterType, bool DiscardValues = false>
|
||||
class lexer : public lexer_base<BasicJsonType>
|
||||
{
|
||||
using number_integer_t = typename BasicJsonType::number_integer_t;
|
||||
@@ -268,10 +273,9 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
public:
|
||||
using token_type = typename lexer_base<BasicJsonType>::token_type;
|
||||
|
||||
explicit lexer(InputAdapterType&& adapter, bool ignore_comments_ = false, bool discard_number_values_ = false) noexcept
|
||||
explicit lexer(InputAdapterType&& adapter, bool ignore_comments_ = false) noexcept
|
||||
: ia(std::move(adapter))
|
||||
, ignore_comments(ignore_comments_)
|
||||
, discard_number_values(discard_number_values_)
|
||||
{
|
||||
// the token string of an error before the first token is everything
|
||||
// read so far
|
||||
@@ -388,6 +392,8 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
checks if it is inside the range. If a violation was detected, set up an
|
||||
error message and return false. Otherwise, return true.
|
||||
|
||||
@param[in] keep whether the bytes are added to token_buffer (false for
|
||||
accept(), see scan())
|
||||
@param[in] ranges list of integers; interpreted as list of pairs of
|
||||
inclusive lower and upper bound, respectively
|
||||
|
||||
@@ -396,17 +402,18 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
|
||||
@return true if and only if no range violation was detected
|
||||
*/
|
||||
bool next_byte_in_range(std::initializer_list<char_int_type> ranges)
|
||||
template<bool Keep>
|
||||
bool next_byte_in_range(std::integral_constant<bool, Keep> keep, std::initializer_list<char_int_type> ranges)
|
||||
{
|
||||
JSON_ASSERT(ranges.size() == 2 || ranges.size() == 4 || ranges.size() == 6);
|
||||
add(current);
|
||||
add_string_char(keep, current);
|
||||
|
||||
for (auto range = ranges.begin(); range != ranges.end(); ++range)
|
||||
{
|
||||
get();
|
||||
if (JSON_HEDLEY_LIKELY(*range <= current && current <= *(++range))) // NOLINT(bugprone-inc-dec-in-conditions)
|
||||
{
|
||||
add(current);
|
||||
add_string_char(keep, current);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -419,20 +426,21 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
}
|
||||
|
||||
/// contiguous input: bulk-append the run of ordinary characters and complete
|
||||
/// well-formed UTF-8 sequences starting at the current read position, then
|
||||
/// well-formed UTF-8 sequences starting at the current read position (only
|
||||
/// skip it if @a keep is false), then
|
||||
/// read the first byte that needs individual handling (the closing quote, an
|
||||
/// escape, a control character, or an ill-formed UTF-8 byte) like get().
|
||||
/// That byte is taken from the buffer that was just scanned. Through get(),
|
||||
/// Apple Clang merged this read with the other get() calls of the scan
|
||||
/// function, which load the read position again right after it was stored
|
||||
/// (3% of json::accept() on jeopardy.json on Apple M1).
|
||||
char_int_type get_after_string_run(std::true_type /*bulk*/)
|
||||
template<bool Keep>
|
||||
char_int_type get_after_string_run(std::true_type /*bulk*/, std::integral_constant<bool, Keep> keep)
|
||||
{
|
||||
// a pending unget must be consumed through the normal path first
|
||||
if (next_unget)
|
||||
{
|
||||
return get();
|
||||
}
|
||||
// scan_string() is entered from scan_after_whitespace(), whose current
|
||||
// was read by skip_whitespace() (its first get() clears a pending
|
||||
// unget), and nothing in scan_string() calls unget()
|
||||
JSON_ASSERT(!next_unget);
|
||||
const std::size_t remaining = ia.bulk_remaining();
|
||||
if (remaining == 0)
|
||||
{
|
||||
@@ -444,7 +452,7 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
const std::size_t pos = lexer_string_bulk_run(data, remaining);
|
||||
if (pos != 0)
|
||||
{
|
||||
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos);
|
||||
append_string_run(keep, data, pos);
|
||||
bulk_consume(reinterpret_cast<const char*>(data), pos);
|
||||
}
|
||||
if (pos == remaining)
|
||||
@@ -488,7 +496,8 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
}
|
||||
|
||||
/// streaming input: no bulk fast path
|
||||
char_int_type get_after_string_run(std::false_type /*bulk*/)
|
||||
template<bool Keep>
|
||||
char_int_type get_after_string_run(std::false_type /*bulk*/, std::integral_constant<bool, Keep> /*keep*/)
|
||||
{
|
||||
return get();
|
||||
}
|
||||
@@ -502,13 +511,18 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
may contain \0 bytes), and token_buffer.size() is the number of bytes in the
|
||||
string.
|
||||
|
||||
@param[in] keep whether the string is copied into token_buffer; if false
|
||||
(accept(), see scan()), it is only checked, with the same
|
||||
errors, and token_buffer stays empty
|
||||
|
||||
@return token_type::value_string if string could be successfully scanned,
|
||||
token_type::parse_error otherwise
|
||||
|
||||
@note In case of errors, variable error_message contains a textual
|
||||
description.
|
||||
*/
|
||||
token_type scan_string()
|
||||
template<bool Keep>
|
||||
token_type scan_string(std::integral_constant<bool, Keep> keep)
|
||||
{
|
||||
// reset token_buffer (ignore opening quote)
|
||||
reset();
|
||||
@@ -520,7 +534,7 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
{
|
||||
// bulk-consume ordinary characters from contiguous input, then
|
||||
// get the next character, the one that ends the run
|
||||
switch (get_after_string_run(std::integral_constant<bool, bulk_scan>()))
|
||||
switch (get_after_string_run(std::integral_constant<bool, bulk_scan>(), keep))
|
||||
{
|
||||
// end of file while parsing the string
|
||||
case char_traits<char_type>::eof():
|
||||
@@ -542,35 +556,35 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
{
|
||||
// quotation mark
|
||||
case '\"':
|
||||
add('\"');
|
||||
add_string_char(keep, '\"');
|
||||
break;
|
||||
// reverse solidus
|
||||
case '\\':
|
||||
add('\\');
|
||||
add_string_char(keep, '\\');
|
||||
break;
|
||||
// solidus
|
||||
case '/':
|
||||
add('/');
|
||||
add_string_char(keep, '/');
|
||||
break;
|
||||
// backspace
|
||||
case 'b':
|
||||
add('\b');
|
||||
add_string_char(keep, '\b');
|
||||
break;
|
||||
// form feed
|
||||
case 'f':
|
||||
add('\f');
|
||||
add_string_char(keep, '\f');
|
||||
break;
|
||||
// line feed
|
||||
case 'n':
|
||||
add('\n');
|
||||
add_string_char(keep, '\n');
|
||||
break;
|
||||
// carriage return
|
||||
case 'r':
|
||||
add('\r');
|
||||
add_string_char(keep, '\r');
|
||||
break;
|
||||
// tab
|
||||
case 't':
|
||||
add('\t');
|
||||
add_string_char(keep, '\t');
|
||||
break;
|
||||
|
||||
// unicode escapes
|
||||
@@ -638,10 +652,7 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
JSON_ASSERT(0x00 <= codepoint && codepoint <= 0x10FFFF);
|
||||
|
||||
// translate codepoint into bytes
|
||||
encode_utf8(static_cast<std::uint32_t>(codepoint), [this](std::uint32_t byte)
|
||||
{
|
||||
add(static_cast<char_int_type>(byte));
|
||||
});
|
||||
add_codepoint(keep, codepoint);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -824,7 +835,7 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
case 0x7E:
|
||||
case 0x7F:
|
||||
{
|
||||
add(current);
|
||||
add_string_char(keep, current);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -860,7 +871,7 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
case 0xDE:
|
||||
case 0xDF:
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(!next_byte_in_range({0x80, 0xBF})))
|
||||
if (JSON_HEDLEY_UNLIKELY(!next_byte_in_range(keep, {0x80, 0xBF})))
|
||||
{
|
||||
return token_type::parse_error;
|
||||
}
|
||||
@@ -870,7 +881,7 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
// U+0800..U+0FFF: bytes E0 A0..BF 80..BF
|
||||
case 0xE0:
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0xA0, 0xBF, 0x80, 0xBF}))))
|
||||
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0xA0, 0xBF, 0x80, 0xBF}))))
|
||||
{
|
||||
return token_type::parse_error;
|
||||
}
|
||||
@@ -894,7 +905,7 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
case 0xEE:
|
||||
case 0xEF:
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0xBF, 0x80, 0xBF}))))
|
||||
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0x80, 0xBF, 0x80, 0xBF}))))
|
||||
{
|
||||
return token_type::parse_error;
|
||||
}
|
||||
@@ -904,7 +915,7 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
// U+D000..U+D7FF: bytes ED 80..9F 80..BF
|
||||
case 0xED:
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0x9F, 0x80, 0xBF}))))
|
||||
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0x80, 0x9F, 0x80, 0xBF}))))
|
||||
{
|
||||
return token_type::parse_error;
|
||||
}
|
||||
@@ -914,7 +925,7 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
// U+10000..U+3FFFF F0 90..BF 80..BF 80..BF
|
||||
case 0xF0:
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x90, 0xBF, 0x80, 0xBF, 0x80, 0xBF}))))
|
||||
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0x90, 0xBF, 0x80, 0xBF, 0x80, 0xBF}))))
|
||||
{
|
||||
return token_type::parse_error;
|
||||
}
|
||||
@@ -926,7 +937,7 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
case 0xF2:
|
||||
case 0xF3:
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0xBF, 0x80, 0xBF, 0x80, 0xBF}))))
|
||||
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0x80, 0xBF, 0x80, 0xBF, 0x80, 0xBF}))))
|
||||
{
|
||||
return token_type::parse_error;
|
||||
}
|
||||
@@ -936,7 +947,7 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
// U+100000..U+10FFFF F4 80..8F 80..BF 80..BF
|
||||
case 0xF4:
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0x8F, 0x80, 0xBF, 0x80, 0xBF}))))
|
||||
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0x80, 0x8F, 0x80, 0xBF, 0x80, 0xBF}))))
|
||||
{
|
||||
return token_type::parse_error;
|
||||
}
|
||||
@@ -1427,7 +1438,7 @@ scan_number_done:
|
||||
token_type convert_number(token_type number_type, std::size_t mantissa_end)
|
||||
{
|
||||
// accept() only needs to know whether the input is valid, so it sets
|
||||
// discard_number_values (see json.hpp), and an integer token whose
|
||||
// discard_values (see json.hpp), and an integer token whose
|
||||
// digit count shows that it fits is reported without calling
|
||||
// convert_integer(). A number with up to 18 digits always fits into
|
||||
// both std::uint64_t and std::int64_t (18 nines is about 1e18, below
|
||||
@@ -1442,7 +1453,7 @@ scan_number_done:
|
||||
// json_sax_acceptor, whose number callbacks discard their argument and
|
||||
// return true, and the parser rejects neither integers nor finite
|
||||
// floats. value_unsigned/value_integer are left unset here, so a caller
|
||||
// that reads the converted value must not set discard_number_values.
|
||||
// that reads the converted value must not set discard_values.
|
||||
//
|
||||
// On contiguous input, scan_number_bulk_contiguous() converts integer
|
||||
// tokens itself and does not pass them to this function, unless
|
||||
@@ -1450,7 +1461,7 @@ scan_number_done:
|
||||
// reached for input without bulk access (e.g. streams), with
|
||||
// JSON_DIAGNOSTIC_POSITIONS, or when scan_number_bulk_contiguous()
|
||||
// falls back to scan_number().
|
||||
if (discard_number_values)
|
||||
if (discard_values())
|
||||
{
|
||||
constexpr std::size_t safe_digit_count = 18;
|
||||
if (number_type == token_type::value_unsigned && token_buffer.size() <= safe_digit_count)
|
||||
@@ -1504,11 +1515,9 @@ scan_number_done:
|
||||
*/
|
||||
token_type scan_number_bulk_contiguous()
|
||||
{
|
||||
// a pending unget offsets the buffer position from current; fall back
|
||||
if (next_unget)
|
||||
{
|
||||
return token_type::uninitialized;
|
||||
}
|
||||
// called right after scan_after_whitespace() read current (see
|
||||
// get_after_string_run()), so no unget is pending
|
||||
JSON_ASSERT(!next_unget);
|
||||
const std::size_t rem = ia.bulk_remaining();
|
||||
if (rem == 0)
|
||||
{
|
||||
@@ -1529,11 +1538,7 @@ scan_number_done:
|
||||
if (data[0] == '-')
|
||||
{
|
||||
number_type = token_type::value_integer;
|
||||
i = 1;
|
||||
if (i >= avail)
|
||||
{
|
||||
return token_type::uninitialized;
|
||||
}
|
||||
i = 1; // avail >= 2: rem == 0 returned above
|
||||
}
|
||||
const std::size_t integer_begin = i;
|
||||
if (data[i] == '0')
|
||||
@@ -1701,7 +1706,7 @@ scan_number_done:
|
||||
// w < 10^19, so a value below 10^max_exponent10 cannot overflow, and a
|
||||
// value that underflows is a finite number as well. Only a token that
|
||||
// may overflow has to be converted to find out.
|
||||
if (JSON_HEDLEY_UNLIKELY(discard_number_values && s.exponent + 19 <= std::numeric_limits<number_float_t>::max_exponent10))
|
||||
if (JSON_HEDLEY_UNLIKELY(discard_values() && s.exponent + 19 <= std::numeric_limits<number_float_t>::max_exponent10))
|
||||
{
|
||||
value_float = 0;
|
||||
return true;
|
||||
@@ -2033,6 +2038,34 @@ scan_number_done:
|
||||
token_buffer.push_back(static_cast<typename string_t::value_type>(c));
|
||||
}
|
||||
|
||||
/// add a character of a string token; accept() checks the string but does
|
||||
/// not keep its value (@a keep is false)
|
||||
void add_string_char(std::true_type /*keep*/, char_int_type c)
|
||||
{
|
||||
token_buffer.push_back(static_cast<typename string_t::value_type>(c));
|
||||
}
|
||||
|
||||
static void add_string_char(std::false_type /*keep*/, char_int_type /*c*/) noexcept {}
|
||||
|
||||
/// add the UTF-8 bytes of a code point from a \\u escape to a string token
|
||||
void add_codepoint(std::true_type /*keep*/, int codepoint)
|
||||
{
|
||||
encode_utf8(static_cast<std::uint32_t>(codepoint), [this](std::uint32_t byte)
|
||||
{
|
||||
add(static_cast<char_int_type>(byte));
|
||||
});
|
||||
}
|
||||
|
||||
static void add_codepoint(std::false_type /*keep*/, int /*codepoint*/) noexcept {}
|
||||
|
||||
/// append a run of ordinary string characters (see get_after_string_run())
|
||||
void append_string_run(std::true_type /*keep*/, const unsigned char* data, std::size_t n)
|
||||
{
|
||||
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), n);
|
||||
}
|
||||
|
||||
static void append_string_run(std::false_type /*keep*/, const unsigned char* /*data*/, std::size_t /*n*/) noexcept {}
|
||||
|
||||
public:
|
||||
/////////////////////
|
||||
// value getters
|
||||
@@ -2427,7 +2460,9 @@ scan_number_done:
|
||||
|
||||
// string
|
||||
case '\"':
|
||||
return scan_string();
|
||||
// accept() (DiscardValues) checks strings without copying them
|
||||
// into token_buffer
|
||||
return scan_string(keep_strings {});
|
||||
|
||||
// number
|
||||
case '-':
|
||||
@@ -2512,11 +2547,19 @@ scan_number_done:
|
||||
std::size_t decimal_point_position = std::string::npos;
|
||||
|
||||
/// whether the caller only needs the token types and never looks at the
|
||||
/// converted numeric values; set only by accept(), which parses through
|
||||
/// json_sax_acceptor. When set, convert_number() skips converting integer
|
||||
/// tokens whose digit count guarantees that they fit into 64 bits (see
|
||||
/// there)
|
||||
const bool discard_number_values = false;
|
||||
/// values of number and string tokens; DiscardValues, set only by accept(), which parses
|
||||
/// through json_sax_acceptor. When set, convert_number() skips converting
|
||||
/// integer tokens whose digit count guarantees that they fit into 64 bits
|
||||
/// (see there), floats are converted only when they may overflow (see
|
||||
/// convert_accumulated_float()), and strings are checked without being
|
||||
/// copied into token_buffer (see scan())
|
||||
static bool discard_values() noexcept
|
||||
{
|
||||
return DiscardValues;
|
||||
}
|
||||
|
||||
/// the tag of scan_string(): whether string tokens are kept
|
||||
using keep_strings = typename std::conditional<DiscardValues, std::false_type, std::true_type>::type;
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
@@ -72,14 +72,14 @@ This class implements a parser for JSON text. Nested arrays and objects are trac
|
||||
stack instead of recursion, so deeply nested input does not exhaust the call stack, and what is read
|
||||
is reported as SAX events.
|
||||
*/
|
||||
template<typename BasicJsonType, typename InputAdapterType>
|
||||
template<typename BasicJsonType, typename InputAdapterType, bool DiscardValues = false>
|
||||
class parser
|
||||
{
|
||||
using number_integer_t = typename BasicJsonType::number_integer_t;
|
||||
using number_unsigned_t = typename BasicJsonType::number_unsigned_t;
|
||||
using number_float_t = typename BasicJsonType::number_float_t;
|
||||
using string_t = typename BasicJsonType::string_t;
|
||||
using lexer_t = lexer<BasicJsonType, InputAdapterType>;
|
||||
using lexer_t = lexer<BasicJsonType, InputAdapterType, DiscardValues>;
|
||||
using token_type = typename lexer_t::token_type;
|
||||
|
||||
public:
|
||||
@@ -88,10 +88,9 @@ class parser
|
||||
parser_callback_t<BasicJsonType> cb = nullptr,
|
||||
const bool allow_exceptions_ = true,
|
||||
const bool ignore_comments = false,
|
||||
const bool ignore_trailing_commas_ = false,
|
||||
const bool discard_number_values_ = false)
|
||||
const bool ignore_trailing_commas_ = false)
|
||||
: callback(std::move(cb))
|
||||
, m_lexer(std::move(adapter), ignore_comments, discard_number_values_)
|
||||
, m_lexer(std::move(adapter), ignore_comments)
|
||||
, allow_exceptions(allow_exceptions_)
|
||||
, ignore_trailing_commas(ignore_trailing_commas_)
|
||||
{
|
||||
@@ -599,7 +598,7 @@ class parser
|
||||
}
|
||||
|
||||
return error_msg;
|
||||
}
|
||||
} // LCOV_EXCL_LINE (exception cleanup only)
|
||||
|
||||
private:
|
||||
/// callback function
|
||||
|
||||
@@ -322,6 +322,78 @@ JSON_SCAN_SSSE3_TARGET JSON_HEDLEY_NEVER_INLINE inline bool vector_utf8_run(cons
|
||||
block_out = block;
|
||||
return false;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief skip 16-byte blocks of well-formed UTF-8 at p (a character boundary)
|
||||
|
||||
Like vector_utf8_run, but quotes, backslashes, and control characters are
|
||||
ordinary characters. Returns the start of the first block in which the check
|
||||
found an error (which may belong to a sequence begun in the block before) or
|
||||
where fewer than 16 bytes are left; a sequence may be cut at that point, so the
|
||||
caller resumes at its start, one sequence at a time.
|
||||
*/
|
||||
JSON_SCAN_SSSE3_TARGET JSON_HEDLEY_NEVER_INLINE inline const unsigned char* vector_utf8_valid_blocks(const unsigned char* block, const unsigned char* e) noexcept
|
||||
{
|
||||
using lookup = utf8_lookup4<>;
|
||||
#if JSON_SCAN_NEON
|
||||
const uint8x16_t t1h = vld1q_u8(lookup::byte_1_high.data());
|
||||
const uint8x16_t t1l = vld1q_u8(lookup::byte_1_low.data());
|
||||
const uint8x16_t t2h = vld1q_u8(lookup::byte_2_high.data());
|
||||
uint8x16_t prev = vdupq_n_u8(0);
|
||||
while (e - block >= 16)
|
||||
{
|
||||
const uint8x16_t in = vld1q_u8(block);
|
||||
if (vmaxvq_u8(vorrq_u8(in, prev)) < 0x80)
|
||||
{
|
||||
// ASCII here, and no sequence from the block before to finish
|
||||
prev = in;
|
||||
block += 16;
|
||||
continue;
|
||||
}
|
||||
const uint8x16_t prev1 = vextq_u8(prev, in, 15);
|
||||
const uint8x16_t sc = vandq_u8(vandq_u8(vqtbl1q_u8(t1h, vshrq_n_u8(prev1, 4)), vqtbl1q_u8(t1l, vandq_u8(prev1, vdupq_n_u8(0x0F)))), vqtbl1q_u8(t2h, vshrq_n_u8(in, 4)));
|
||||
const uint8x16_t must23 = vorrq_u8(vqsubq_u8(vextq_u8(prev, in, 14), vdupq_n_u8(0xE0 - 0x80)), vqsubq_u8(vextq_u8(prev, in, 13), vdupq_n_u8(0xF0 - 0x80)));
|
||||
const uint8x16_t err = veorq_u8(vandq_u8(must23, vdupq_n_u8(0x80)), sc);
|
||||
if (vmaxvq_u8(err) != 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
prev = in;
|
||||
block += 16;
|
||||
}
|
||||
#else
|
||||
const __m128i t1h = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(lookup::byte_1_high.data()))); // NOLINT(bugprone-casting-through-void)
|
||||
const __m128i t1l = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(lookup::byte_1_low.data()))); // NOLINT(bugprone-casting-through-void)
|
||||
const __m128i t2h = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(lookup::byte_2_high.data()))); // NOLINT(bugprone-casting-through-void)
|
||||
const __m128i nibble = _mm_set1_epi8(0x0F);
|
||||
const __m128i zero = _mm_setzero_si128();
|
||||
__m128i prev = zero;
|
||||
while (e - block >= 16)
|
||||
{
|
||||
const __m128i in = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(block))); // NOLINT(bugprone-casting-through-void)
|
||||
if (_mm_movemask_epi8(_mm_or_si128(in, prev)) == 0)
|
||||
{
|
||||
prev = in;
|
||||
block += 16;
|
||||
continue;
|
||||
}
|
||||
const __m128i prev1 = _mm_alignr_epi8(in, prev, 15);
|
||||
const __m128i sc = _mm_and_si128(_mm_and_si128(_mm_shuffle_epi8(t1h, _mm_and_si128(_mm_srli_epi16(prev1, 4), nibble)),
|
||||
_mm_shuffle_epi8(t1l, _mm_and_si128(prev1, nibble))),
|
||||
_mm_shuffle_epi8(t2h, _mm_and_si128(_mm_srli_epi16(in, 4), nibble)));
|
||||
const __m128i must23 = _mm_or_si128(_mm_subs_epu8(_mm_alignr_epi8(in, prev, 14), _mm_set1_epi8(0xE0 - 0x80)),
|
||||
_mm_subs_epu8(_mm_alignr_epi8(in, prev, 13), _mm_set1_epi8(0xF0 - 0x80)));
|
||||
const __m128i err = _mm_xor_si128(_mm_and_si128(must23, _mm_set1_epi8(static_cast<char>(-128))), sc);
|
||||
if (_mm_movemask_epi8(_mm_cmpeq_epi8(err, zero)) != 0xFFFF)
|
||||
{
|
||||
break;
|
||||
}
|
||||
prev = in;
|
||||
block += 16;
|
||||
}
|
||||
#endif
|
||||
return block;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace detail
|
||||
|
||||
@@ -90,6 +90,34 @@ JSON_HEDLEY_ALWAYS_INLINE std::size_t find_string_special(const unsigned char* d
|
||||
return n;
|
||||
}
|
||||
|
||||
// return the index of the first 0x00 byte in [data, data+n), or n if there is
|
||||
// none; scans 8 bytes at a time (BSON's C-style strings)
|
||||
JSON_HEDLEY_ALWAYS_INLINE std::size_t find_zero_byte(const unsigned char* data, std::size_t n) noexcept
|
||||
{
|
||||
constexpr std::uint64_t ones = 0x0101010101010101ull;
|
||||
constexpr std::uint64_t high = 0x8080808080808080ull;
|
||||
std::size_t i = 0;
|
||||
for (; i + 8 <= n; i += 8)
|
||||
{
|
||||
const std::uint64_t v = read_eight_bytes(data + i);
|
||||
const std::uint64_t zero = (v - ones) & ~v & high;
|
||||
if (zero != 0)
|
||||
{
|
||||
// the lowest flagged byte is the first zero byte (see
|
||||
// find_string_special())
|
||||
return i + (static_cast<std::size_t>(count_trailing_zeros(zero)) / 8);
|
||||
}
|
||||
}
|
||||
for (; i < n; ++i)
|
||||
{
|
||||
if (data[i] == 0x00)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// classify a byte as one the serializer must NOT copy verbatim when
|
||||
// ensure_ascii is requested: the closing quote, an escape, a control character
|
||||
// (< 0x20), DEL (0x7F), or any non-ASCII byte (>= 0x80). Everything else -
|
||||
@@ -236,6 +264,64 @@ inline std::size_t valid_utf8_prefix(const unsigned char* data, std::size_t n) n
|
||||
return pos;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief whether [data, data+n) is valid UTF-8, for checking whole strings
|
||||
|
||||
The same answer as valid_utf8_prefix(data, n) == n. Short ASCII strings, the
|
||||
most common keys and values, are decided in one test, and long strings are
|
||||
checked 16 bytes at a time where the vector check is available.
|
||||
*/
|
||||
inline bool valid_utf8_string(const unsigned char* data, std::size_t n) noexcept
|
||||
{
|
||||
constexpr std::uint64_t high = 0x8080808080808080ull;
|
||||
if (n <= 16)
|
||||
{
|
||||
// two overlapping words cover 8 to 16 bytes, one partial word fewer
|
||||
std::uint64_t first = 0;
|
||||
std::uint64_t last = 0;
|
||||
if (n >= 8)
|
||||
{
|
||||
std::memcpy(&first, data, sizeof(first));
|
||||
std::memcpy(&last, data + n - 8, sizeof(last));
|
||||
}
|
||||
else if (n != 0)
|
||||
{
|
||||
std::memcpy(&first, data, n);
|
||||
}
|
||||
if (((first | last) & high) == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#if JSON_SCAN_VECTOR_UTF8
|
||||
else if (n >= 32
|
||||
#if JSON_SCAN_SSSE3_DISPATCH
|
||||
&& cpu_has_ssse3()
|
||||
#endif
|
||||
)
|
||||
{
|
||||
const unsigned char* block = vector_utf8_valid_blocks(data, data + n);
|
||||
// the rest from the start of a sequence that may be cut at block
|
||||
for (int i = 1; i <= 3 && block - i >= data; ++i)
|
||||
{
|
||||
const unsigned char c = block[-i];
|
||||
if (c < 0x80)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (c >= 0xC0)
|
||||
{
|
||||
block -= i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const auto checked = static_cast<std::size_t>(block - data);
|
||||
return checked + valid_utf8_prefix(block, n - checked) == n;
|
||||
}
|
||||
#endif
|
||||
return valid_utf8_prefix(data, n) == n;
|
||||
}
|
||||
|
||||
#if JSON_SCAN_VECTOR_UTF8
|
||||
/// the end of scan_string_vector from block, where the vector loop stopped
|
||||
/// (ill-formed UTF-8, or fewer than 16 bytes left): one byte or sequence at a
|
||||
|
||||
@@ -304,7 +304,7 @@ class json_pointer
|
||||
result.reference_tokens.insert(result.reference_tokens.end(), lhs.reference_tokens.begin(), lhs.reference_tokens.end());
|
||||
result.reference_tokens.insert(result.reference_tokens.end(), rhs.reference_tokens.begin(), rhs.reference_tokens.end());
|
||||
return result;
|
||||
}
|
||||
} // LCOV_EXCL_LINE (exception cleanup only)
|
||||
|
||||
/// @brief create a new JSON pointer by appending the unescaped token at the end of the JSON pointer
|
||||
/// @sa https://json.nlohmann.me/api/json_pointer/operator_slash/
|
||||
@@ -315,7 +315,7 @@ class json_pointer
|
||||
result.reference_tokens.insert(result.reference_tokens.end(), lhs.reference_tokens.begin(), lhs.reference_tokens.end());
|
||||
result.reference_tokens.push_back(std::move(token));
|
||||
return result;
|
||||
}
|
||||
} // LCOV_EXCL_LINE (exception cleanup only)
|
||||
|
||||
/// @brief create a new JSON pointer by appending the array-index-token at the end of the JSON pointer
|
||||
/// @sa https://json.nlohmann.me/api/json_pointer/operator_slash/
|
||||
@@ -2180,7 +2180,7 @@ class json_pointer
|
||||
{
|
||||
// create an entry in the array
|
||||
current = ¤t->operator[](array_index<BasicJsonType>(token));
|
||||
break;
|
||||
break; // LCOV_EXCL_LINE (needs an array index with 10 or more digits: more than 1e9 elements)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -2217,7 +2217,7 @@ class json_pointer
|
||||
json_pointer<string_t> result;
|
||||
result.reference_tokens = reference_tokens;
|
||||
return result;
|
||||
}
|
||||
} // LCOV_EXCL_LINE (exception cleanup only)
|
||||
|
||||
json_pointer<string_t> convert()&&
|
||||
{
|
||||
|
||||
@@ -347,7 +347,7 @@
|
||||
return ej_pair.second == j; \
|
||||
}); \
|
||||
if (it != std::end(m)) e = it->first; \
|
||||
else ::nlohmann::detail::templated_json_throw<nlohmann::detail::out_of_range>(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range, nlohmann::detail::concat("enum value out of range for " #ENUM_TYPE ": ", nlohmann::detail::from_json_dump(j)), nlohmann::detail::exception_context(j))); \
|
||||
else ::nlohmann::detail::templated_json_throw<nlohmann::detail::out_of_range>(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range, nlohmann::detail::concat(nlohmann::detail::exception_prefix(j), "enum value out of range for " #ENUM_TYPE ": ", nlohmann::detail::from_json_dump(j)), nlohmann::detail::exception_context(j))); \
|
||||
}
|
||||
|
||||
// Ugly macros to avoid uglier copy-paste when specializing basic_json. They
|
||||
|
||||
@@ -833,7 +833,7 @@ class binary_writer
|
||||
while (n != 0);
|
||||
std::reverse(result.begin(), result.end());
|
||||
return result;
|
||||
}
|
||||
} // LCOV_EXCL_LINE (exception cleanup only)
|
||||
|
||||
/*!
|
||||
@brief the decimal digits of the integer @a n, with a leading '-' if it
|
||||
@@ -860,7 +860,7 @@ class binary_writer
|
||||
}
|
||||
std::reverse(result.begin(), result.end());
|
||||
return result;
|
||||
}
|
||||
} // LCOV_EXCL_LINE (exception cleanup only)
|
||||
|
||||
/*!
|
||||
@brief throws because the integer @a j is too large for @a format
|
||||
@@ -1792,7 +1792,7 @@ class binary_writer
|
||||
std::memcpy(out, &n, sizeof(NumberType));
|
||||
if (!is_little_endian)
|
||||
{
|
||||
std::reverse(out, out + sizeof(NumberType));
|
||||
std::reverse(out, out + sizeof(NumberType)); // LCOV_EXCL_LINE (big-endian hosts only)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1921,7 +1921,7 @@ class binary_writer
|
||||
const string_t& value = *j.m_data.m_value.string;
|
||||
if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::int32_t>(value.size() + 1ul)))
|
||||
{
|
||||
return false;
|
||||
return false; // LCOV_EXCL_LINE (a string of 2 GiB or more)
|
||||
}
|
||||
out[0] = to_char_type(0x02);
|
||||
store_little_endian<std::int32_t>(p, static_cast<std::int32_t>(value.size() + 1ul));
|
||||
@@ -2088,7 +2088,8 @@ class binary_writer
|
||||
/// arrays: the first index with one more digit; only maintained by
|
||||
/// @ref calc_bson_sizes
|
||||
std::uint64_t index_boundary = 10;
|
||||
/// @ref calc_bson_sizes only: where its size goes in the table
|
||||
/// @ref calc_bson_sizes: where its size goes in the table; @ref
|
||||
/// write_bson_document_single_pass: the position of its size field
|
||||
std::size_t size_slot;
|
||||
/// @ref calc_bson_sizes only: the size of its entries seen so far
|
||||
std::size_t entries_size = 0;
|
||||
@@ -2339,6 +2340,199 @@ class binary_writer
|
||||
}
|
||||
}
|
||||
|
||||
template<typename SinkType>
|
||||
using sink_written_t = decltype(std::declval<const SinkType&>().written());
|
||||
|
||||
/// whether every integer fits the 64 bits BSON has, so that writing an
|
||||
/// array of numbers needs no range check
|
||||
static constexpr bool bson_integers_fit_64_bits =
|
||||
std::numeric_limits<typename BasicJsonType::number_integer_t>::is_specialized
|
||||
&& std::numeric_limits<typename BasicJsonType::number_integer_t>::digits <= std::numeric_limits<std::int64_t>::digits
|
||||
&& std::numeric_limits<typename BasicJsonType::number_unsigned_t>::is_specialized
|
||||
&& std::numeric_limits<typename BasicJsonType::number_unsigned_t>::digits <= std::numeric_limits<std::uint64_t>::digits;
|
||||
|
||||
/*!
|
||||
@brief whether BSON documents can be written in a single pass: the sink
|
||||
can overwrite what it has written (output_vector_sink), and
|
||||
@ref bson_integers_fit_64_bits
|
||||
*/
|
||||
using bson_single_pass_sink = conjunction<is_detected<sink_written_t, OutputSinkType>, std::integral_constant<bool, bson_integers_fit_64_bits>>;
|
||||
|
||||
/// write a placeholder for the size of an object or array whose entries
|
||||
/// follow; @return its position, for @ref close_bson_container
|
||||
std::size_t write_bson_size_placeholder()
|
||||
{
|
||||
const std::size_t pos = oa.written();
|
||||
// zero in any byte order
|
||||
const std::array<CharType, sizeof(std::int32_t)> zero{};
|
||||
oa.write_characters(zero.data(), zero.size());
|
||||
return pos;
|
||||
}
|
||||
|
||||
/// end the object or array whose size is at @a pos and write its size
|
||||
/// there: everything from that field to the terminating null byte
|
||||
/// @return false (and the size is not written) if it does not fit
|
||||
bool close_bson_container(const std::size_t pos)
|
||||
{
|
||||
oa.write_character(to_char_type(0x00));
|
||||
const std::size_t size = oa.written() - pos;
|
||||
if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::int32_t>(size)))
|
||||
{
|
||||
return false; // LCOV_EXCL_LINE (a document of 2 GiB or more)
|
||||
}
|
||||
const auto le_size = static_cast<std::uint32_t>(size);
|
||||
oa.overwrite(pos, is_little_endian ? le_size : byteswap(le_size));
|
||||
return true;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief check the value @a j of a BSON entry as @ref calc_bson_value_size
|
||||
does, for @ref write_bson_document_single_pass
|
||||
@return at least the size of the value
|
||||
A number, boolean, or null needs no check there (its integers have at most
|
||||
64 bits) and has at most 8 characters; a discarded value is rejected with
|
||||
the same exception when it is written.
|
||||
*/
|
||||
std::size_t check_bson_value(const BasicJsonType& j)
|
||||
{
|
||||
return (j.is_string() || j.is_binary()) ? calc_bson_value_size(j) : 8;
|
||||
}
|
||||
|
||||
/// whether an entry of @a entry_size characters keeps the document that
|
||||
/// begins at @a start, closed by @a open_containers null bytes, within
|
||||
/// the 32-bit size of BSON
|
||||
bool bson_entry_fits(const std::size_t start, const std::size_t entry_size, const std::size_t open_containers) const noexcept
|
||||
{
|
||||
const auto limit = static_cast<std::size_t>((std::numeric_limits<std::int32_t>::max)());
|
||||
const std::size_t written = oa.written() - start;
|
||||
return written <= limit && entry_size <= limit - written && open_containers <= limit - written - entry_size;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief Serializes the JSON object @a document as a BSON document in a
|
||||
single pass, writing the size of each object and array once it is
|
||||
complete
|
||||
|
||||
Checks every key and value as @ref calc_bson_sizes does, in the same
|
||||
order, so the same exception is thrown; the output written up to then is
|
||||
discarded with the vector it was written to.
|
||||
|
||||
@return false as soon as the document or a string, binary value, object,
|
||||
or array in it would be larger than the 32-bit BSON sizes allow;
|
||||
nothing large has been written then, and the caller writes the
|
||||
document in two passes, which reports that error
|
||||
*/
|
||||
template<typename SinkType = OutputSinkType, typename std::enable_if<is_detected<sink_written_t, SinkType>::value, int>::type = 0>
|
||||
bool write_bson_document_single_pass(const BasicJsonType& document)
|
||||
{
|
||||
const std::size_t start = oa.written();
|
||||
// the object or array whose entries are being written (with the
|
||||
// position of its size), and the ones it is in
|
||||
bson_frame current(&document, write_bson_size_placeholder());
|
||||
std::vector<bson_frame> parents;
|
||||
|
||||
while (true)
|
||||
{
|
||||
const BasicJsonType* nested = nullptr;
|
||||
std::size_t nested_size_pos = 0;
|
||||
if (current.value->is_object())
|
||||
{
|
||||
const auto& object = *current.value->m_data.m_value.object;
|
||||
while (nested == nullptr && current.member != object.cend())
|
||||
{
|
||||
const auto& el = *current.member;
|
||||
++current.member;
|
||||
const std::size_t header_size = calc_bson_entry_header_size(el.first, el.second);
|
||||
const std::size_t value_size = el.second.is_structured() ? 0 : check_bson_value(el.second);
|
||||
if (JSON_HEDLEY_UNLIKELY(!bson_entry_fits(start, header_size + value_size, parents.size() + 1)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (el.second.is_structured())
|
||||
{
|
||||
write_bson_entry_header(el.first, el.second.is_object() ? 0x03 : 0x04);
|
||||
nested_size_pos = write_bson_size_placeholder();
|
||||
nested = &el.second;
|
||||
}
|
||||
else
|
||||
{
|
||||
write_bson_value(el.first, el.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto& array = *current.value->m_data.m_value.array;
|
||||
while (nested == nullptr && current.index < array.size())
|
||||
{
|
||||
if (is_bson_fixed_size(array[current.index]))
|
||||
{
|
||||
write_bson_array_fixed_entries(current);
|
||||
if (current.index == array.size())
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// a string, binary value, object or array
|
||||
const BasicJsonType& el = array[current.index];
|
||||
const string_t index_name(current.index_name.data(), current.index_name_size);
|
||||
current.next_index();
|
||||
const std::size_t value_size = el.is_structured() ? 0 : check_bson_value(el);
|
||||
if (JSON_HEDLEY_UNLIKELY(!bson_entry_fits(start, 2 + index_name.size() + value_size, parents.size() + 1)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (el.is_structured())
|
||||
{
|
||||
write_bson_entry_header(index_name, el.is_object() ? 0x03 : 0x04);
|
||||
nested_size_pos = write_bson_size_placeholder();
|
||||
nested = ⪙
|
||||
}
|
||||
else
|
||||
{
|
||||
write_bson_value(index_name, el);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nested != nullptr)
|
||||
{
|
||||
if (nested->is_array())
|
||||
{
|
||||
// an array of booleans, numbers and null is written without a frame on the stack
|
||||
bson_frame leaf(nested, nested_size_pos);
|
||||
write_bson_array_fixed_entries(leaf);
|
||||
if (leaf.index == nested->m_data.m_value.array->size())
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(!close_bson_container(nested_size_pos)))
|
||||
{
|
||||
return false; // LCOV_EXCL_LINE (a document of 2 GiB or more)
|
||||
}
|
||||
continue;
|
||||
}
|
||||
parents.push_back(std::move(current));
|
||||
current = std::move(leaf);
|
||||
continue;
|
||||
}
|
||||
parents.push_back(std::move(current));
|
||||
current = bson_frame(nested, nested_size_pos);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (JSON_HEDLEY_UNLIKELY(!close_bson_container(current.size_slot)))
|
||||
{
|
||||
return false; // LCOV_EXCL_LINE (a document of 2 GiB or more)
|
||||
}
|
||||
if (parents.empty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
current = std::move(parents.back());
|
||||
parents.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief Serializes the JSON object @a document as a BSON document
|
||||
|
||||
@@ -2350,6 +2544,39 @@ class binary_writer
|
||||
@pre document.type() == value_t::object
|
||||
*/
|
||||
void write_bson_document(const BasicJsonType& document)
|
||||
{
|
||||
write_bson_document(document, bson_single_pass_sink());
|
||||
}
|
||||
|
||||
/// the sink can overwrite: one pass, unless the error handler replaces or
|
||||
/// drops ill-formed UTF-8, which changes the sizes the checks compute
|
||||
void write_bson_document(const BasicJsonType& document, std::true_type /*single_pass*/)
|
||||
{
|
||||
const std::size_t start = oa.written();
|
||||
if ((error_handler == error_handler_t::keep || error_handler == error_handler_t::strict)
|
||||
&& write_bson_document_single_pass(document))
|
||||
{
|
||||
return;
|
||||
}
|
||||
// a length does not fit: start over, so that it is reported as the
|
||||
// two passes report it, before anything is written
|
||||
oa.rewind(start);
|
||||
write_bson_document_two_pass(document);
|
||||
}
|
||||
|
||||
void write_bson_document(const BasicJsonType& document, std::false_type /*single_pass*/)
|
||||
{
|
||||
write_bson_document_two_pass(document);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief Serializes the JSON object @a document as a BSON document: the
|
||||
sizes of all objects and arrays first, then the entries
|
||||
|
||||
@param[in] document the JSON object to serialize
|
||||
@pre document.type() == value_t::object
|
||||
*/
|
||||
void write_bson_document_two_pass(const BasicJsonType& document)
|
||||
{
|
||||
std::vector<std::size_t> nested_sizes;
|
||||
const std::size_t document_size = calc_bson_sizes(document, nested_sizes);
|
||||
@@ -2552,10 +2779,10 @@ class binary_writer
|
||||
|
||||
Selected through masks rather than branches, as the type and the sign of
|
||||
the values in an array of mixed signs alternate unpredictably (non-negative
|
||||
integers are number_unsigned after parsing). Up to 64 bits, the sign is bit
|
||||
63 of the result (a narrower integer is sign-extended); computing it with
|
||||
masks keeps GCC from branching on the type. A wider integer has to be
|
||||
compared.
|
||||
integers are number_unsigned after parsing). The sign is bit 63 of the
|
||||
result (a narrower integer is sign-extended); computing it with masks keeps
|
||||
GCC from branching on the type. Only called if @ref integers_fit_64_bits();
|
||||
a wider number type is written with range checks on the whole value.
|
||||
*/
|
||||
static std::uint64_t integer_bits(const BasicJsonType& j, std::uint64_t& sign_mask) noexcept
|
||||
{
|
||||
@@ -2563,9 +2790,7 @@ class binary_writer
|
||||
const std::uint64_t raw = is_signed
|
||||
? static_cast<std::uint64_t>(j.m_data.m_value.number_integer)
|
||||
: static_cast<std::uint64_t>(j.m_data.m_value.number_unsigned);
|
||||
sign_mask = sizeof(typename BasicJsonType::number_integer_t) <= sizeof(std::uint64_t)
|
||||
? (std::uint64_t{0} - static_cast<std::uint64_t>(is_signed)) & (std::uint64_t{0} - (raw >> 63u))
|
||||
: std::uint64_t{0} - static_cast<std::uint64_t>(is_signed && j.m_data.m_value.number_integer < 0);
|
||||
sign_mask = (std::uint64_t{0} - static_cast<std::uint64_t>(is_signed)) & (std::uint64_t{0} - (raw >> 63u));
|
||||
return raw;
|
||||
}
|
||||
|
||||
@@ -3080,6 +3305,25 @@ class binary_writer
|
||||
}
|
||||
}
|
||||
|
||||
/// write the head and the characters of @a value, at most
|
||||
/// max_text_in_reservation of them, in one reservation
|
||||
JSON_HEDLEY_ALWAYS_INLINE void write_cbor_string_reserved(const string_t& value)
|
||||
{
|
||||
CharType* const out = oa.reserve(value.size() + 2);
|
||||
CharType* p = out;
|
||||
if (value.size() <= 0x17)
|
||||
{
|
||||
*p++ = to_char_type(static_cast<std::uint8_t>(0x60 + value.size()));
|
||||
}
|
||||
else
|
||||
{
|
||||
*p++ = to_char_type(0x78);
|
||||
*p++ = to_char_type(static_cast<std::uint8_t>(value.size()));
|
||||
}
|
||||
copy_characters(p, reinterpret_cast<const CharType*>(value.data()), value.size());
|
||||
oa.commit(static_cast<std::size_t>(p - out) + value.size());
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief write a CBOR text string
|
||||
|
||||
@@ -3101,19 +3345,7 @@ class binary_writer
|
||||
// the string is written as it is: head and characters in one reservation
|
||||
if (JSON_HEDLEY_LIKELY(value.size() <= max_text_in_reservation))
|
||||
{
|
||||
CharType* const out = oa.reserve(value.size() + 2);
|
||||
CharType* p = out;
|
||||
if (value.size() <= 0x17)
|
||||
{
|
||||
*p++ = to_char_type(static_cast<std::uint8_t>(0x60 + value.size()));
|
||||
}
|
||||
else
|
||||
{
|
||||
*p++ = to_char_type(0x78);
|
||||
*p++ = to_char_type(static_cast<std::uint8_t>(value.size()));
|
||||
}
|
||||
copy_characters(p, reinterpret_cast<const CharType*>(value.data()), value.size());
|
||||
oa.commit(static_cast<std::size_t>(p - out) + value.size());
|
||||
write_cbor_string_reserved(value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -3127,6 +3359,13 @@ class binary_writer
|
||||
JSON_HEDLEY_NEVER_INLINE
|
||||
void write_cbor_checked_string(const string_t& value, const BasicJsonType& context)
|
||||
{
|
||||
if (error_handler == error_handler_t::strict && value.size() <= max_text_in_reservation)
|
||||
{
|
||||
check_utf8(value, context);
|
||||
write_cbor_string_reserved(value);
|
||||
return;
|
||||
}
|
||||
|
||||
string_t storage;
|
||||
const string_t& sanitized = sanitize_utf8_for_write(value, context, storage);
|
||||
|
||||
@@ -3141,6 +3380,27 @@ class binary_writer
|
||||
// MsgPack //
|
||||
/////////////
|
||||
|
||||
/// write the head and the characters of @a value, at most
|
||||
/// max_text_in_reservation of them, in one reservation
|
||||
JSON_HEDLEY_ALWAYS_INLINE void write_msgpack_string_reserved(const string_t& value)
|
||||
{
|
||||
CharType* const out = oa.reserve(value.size() + 2);
|
||||
CharType* p = out;
|
||||
if (value.size() <= 31)
|
||||
{
|
||||
// fixstr
|
||||
*p++ = to_char_type(static_cast<std::uint8_t>(0xA0 | value.size()));
|
||||
}
|
||||
else
|
||||
{
|
||||
// str 8
|
||||
*p++ = to_char_type(0xD9);
|
||||
*p++ = to_char_type(static_cast<std::uint8_t>(value.size()));
|
||||
}
|
||||
copy_characters(p, reinterpret_cast<const CharType*>(value.data()), value.size());
|
||||
oa.commit(static_cast<std::size_t>(p - out) + value.size());
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief write a MessagePack str
|
||||
|
||||
@@ -3158,21 +3418,7 @@ class binary_writer
|
||||
// the string is written as it is: head and characters in one reservation
|
||||
if (JSON_HEDLEY_LIKELY(value.size() <= max_text_in_reservation))
|
||||
{
|
||||
CharType* const out = oa.reserve(value.size() + 2);
|
||||
CharType* p = out;
|
||||
if (value.size() <= 31)
|
||||
{
|
||||
// fixstr
|
||||
*p++ = to_char_type(static_cast<std::uint8_t>(0xA0 | value.size()));
|
||||
}
|
||||
else
|
||||
{
|
||||
// str 8
|
||||
*p++ = to_char_type(0xD9);
|
||||
*p++ = to_char_type(static_cast<std::uint8_t>(value.size()));
|
||||
}
|
||||
copy_characters(p, reinterpret_cast<const CharType*>(value.data()), value.size());
|
||||
oa.commit(static_cast<std::size_t>(p - out) + value.size());
|
||||
write_msgpack_string_reserved(value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -3185,6 +3431,13 @@ class binary_writer
|
||||
JSON_HEDLEY_NEVER_INLINE
|
||||
void write_msgpack_checked_string(const string_t& value, const BasicJsonType& context)
|
||||
{
|
||||
if (error_handler == error_handler_t::strict && value.size() <= max_text_in_reservation)
|
||||
{
|
||||
check_utf8(value, context);
|
||||
write_msgpack_string_reserved(value);
|
||||
return;
|
||||
}
|
||||
|
||||
string_t storage;
|
||||
const string_t& sanitized = sanitize_utf8_for_write(value, context, storage);
|
||||
|
||||
@@ -3904,57 +4157,34 @@ class binary_writer
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief validate (dry_run) or write one BJData ND-array element of integer dtype @a T
|
||||
@return whether @a el's value is in range of @a T; always true when @a dry_run is false
|
||||
*/
|
||||
template<typename T>
|
||||
bool write_bjdata_ndarray_element(const BasicJsonType& el, const bool dry_run)
|
||||
{
|
||||
if (dry_run)
|
||||
{
|
||||
return bjdata_ndarray_value_in_range<T>(el);
|
||||
}
|
||||
using storage_type = typename std::conditional<std::is_unsigned<T>::value, std::uint64_t, std::int64_t>::type;
|
||||
write_number(static_cast<T>(el.template get<storage_type>()), true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief validate (dry_run) or write one BJData ND-array element of dtype 'd' (single precision)
|
||||
@brief whether the BJData ND-array element @a el can be written with dtype 'd' (single precision)
|
||||
@return whether @a el's value survives narrowing to float and back without any change
|
||||
(so the ND-array round-trips exactly), or is infinite or NaN; always true when
|
||||
@a dry_run is false
|
||||
(so the ND-array round-trips exactly), or is infinite or NaN
|
||||
*/
|
||||
bool write_bjdata_ndarray_float_element(const BasicJsonType& el, const bool dry_run)
|
||||
static bool bjdata_ndarray_float_in_range(const BasicJsonType& el) noexcept
|
||||
{
|
||||
const auto dval = static_cast<double>(el.m_data.m_value.number_float);
|
||||
if (dry_run)
|
||||
{
|
||||
#ifdef __GNUC__
|
||||
JSON_HEDLEY_DIAGNOSTIC_PUSH
|
||||
JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal")
|
||||
JSON_HEDLEY_DIAGNOSTIC_PUSH
|
||||
JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal")
|
||||
#endif
|
||||
// a value that would be rounded (rather than exactly represented) by the
|
||||
// narrowing to float is treated like an out-of-range integer element; this
|
||||
// is the same criterion write_compact_float() uses for CBOR/MessagePack
|
||||
const bool in_range = std::isnan(dval) ||
|
||||
(dval >= static_cast<double>(std::numeric_limits<float>::lowest()) &&
|
||||
dval <= static_cast<double>((std::numeric_limits<float>::max)()) &&
|
||||
static_cast<double>(static_cast<float>(dval)) == dval) ||
|
||||
std::isinf(dval);
|
||||
// a value that would be rounded (rather than exactly represented) by the
|
||||
// narrowing to float is treated like an out-of-range integer element; this
|
||||
// is the same criterion write_compact_float() uses for CBOR/MessagePack
|
||||
return std::isnan(dval) ||
|
||||
(dval >= static_cast<double>(std::numeric_limits<float>::lowest()) &&
|
||||
dval <= static_cast<double>((std::numeric_limits<float>::max)()) &&
|
||||
static_cast<double>(static_cast<float>(dval)) == dval) ||
|
||||
std::isinf(dval);
|
||||
#ifdef __GNUC__
|
||||
JSON_HEDLEY_DIAGNOSTIC_POP
|
||||
JSON_HEDLEY_DIAGNOSTIC_POP
|
||||
#endif
|
||||
return in_range;
|
||||
}
|
||||
write_number(static_cast<float>(dval), true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief write the elements of an ND-array as the number kind @a dtype names
|
||||
(see @ref write_bjdata_ndarray), after they have been found to be
|
||||
in range by a dry run of @ref write_bjdata_ndarray_elements
|
||||
in range by @ref bjdata_ndarray_elements_in_range
|
||||
|
||||
The elements are encoded in blocks into space reserved from the sink.
|
||||
*/
|
||||
@@ -4036,20 +4266,13 @@ class binary_writer
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief validate or write every element of a BJData ND-array's `_ArrayData_`
|
||||
@brief whether every element of a BJData ND-array's `_ArrayData_` is in range for its dtype
|
||||
@param[in] array_data the `_ArrayData_` array
|
||||
@param[in] dtype the ND-array dtype marker, as returned by bjdata_ndarray_type_marker()
|
||||
@param[in] dry_run true to only range-check each element, false to write it
|
||||
@return whether every element is in range for @a dtype (always true when @a dry_run is false)
|
||||
@return whether every element is in range for @a dtype
|
||||
*/
|
||||
bool write_bjdata_ndarray_elements(const BasicJsonType& array_data, const CharType dtype, const bool dry_run)
|
||||
static bool bjdata_ndarray_elements_in_range(const BasicJsonType& array_data, const CharType dtype)
|
||||
{
|
||||
if (!dry_run)
|
||||
{
|
||||
write_bjdata_ndarray_values(array_data, dtype);
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const auto& el : array_data)
|
||||
{
|
||||
bool ok = true;
|
||||
@@ -4057,43 +4280,39 @@ class binary_writer
|
||||
{
|
||||
case 'U':
|
||||
case 'B':
|
||||
ok = write_bjdata_ndarray_element<std::uint8_t>(el, dry_run);
|
||||
ok = bjdata_ndarray_value_in_range<std::uint8_t>(el);
|
||||
break;
|
||||
case 'C':
|
||||
// BJData limits a char to 0..127 (the intersection of the int8 and uint8 ranges)
|
||||
ok = bjdata_ndarray_value_in_range<std::int8_t>(el) && bjdata_ndarray_value_in_range<std::uint8_t>(el);
|
||||
break;
|
||||
case 'i':
|
||||
ok = write_bjdata_ndarray_element<std::int8_t>(el, dry_run);
|
||||
ok = bjdata_ndarray_value_in_range<std::int8_t>(el);
|
||||
break;
|
||||
case 'u':
|
||||
ok = write_bjdata_ndarray_element<std::uint16_t>(el, dry_run);
|
||||
ok = bjdata_ndarray_value_in_range<std::uint16_t>(el);
|
||||
break;
|
||||
case 'I':
|
||||
ok = write_bjdata_ndarray_element<std::int16_t>(el, dry_run);
|
||||
ok = bjdata_ndarray_value_in_range<std::int16_t>(el);
|
||||
break;
|
||||
case 'm':
|
||||
ok = write_bjdata_ndarray_element<std::uint32_t>(el, dry_run);
|
||||
ok = bjdata_ndarray_value_in_range<std::uint32_t>(el);
|
||||
break;
|
||||
case 'l':
|
||||
ok = write_bjdata_ndarray_element<std::int32_t>(el, dry_run);
|
||||
ok = bjdata_ndarray_value_in_range<std::int32_t>(el);
|
||||
break;
|
||||
case 'M':
|
||||
ok = write_bjdata_ndarray_element<std::uint64_t>(el, dry_run);
|
||||
ok = bjdata_ndarray_value_in_range<std::uint64_t>(el);
|
||||
break;
|
||||
case 'L':
|
||||
ok = write_bjdata_ndarray_element<std::int64_t>(el, dry_run);
|
||||
ok = bjdata_ndarray_value_in_range<std::int64_t>(el);
|
||||
break;
|
||||
case 'd':
|
||||
ok = write_bjdata_ndarray_float_element(el, dry_run);
|
||||
ok = bjdata_ndarray_float_in_range(el);
|
||||
break;
|
||||
case 'D':
|
||||
default:
|
||||
// 'D' (double) already spans the full range of number_float_t
|
||||
if (!dry_run)
|
||||
{
|
||||
write_number(el.template get<double>(), true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!ok)
|
||||
@@ -4180,7 +4399,7 @@ class binary_writer
|
||||
const auto dim = el.template get<std::uint64_t>();
|
||||
if (!value_in_range_of<std::size_t>(dim))
|
||||
{
|
||||
return true;
|
||||
return true; // LCOV_EXCL_LINE (size_t is narrower than 64 bits)
|
||||
}
|
||||
const auto dim_size = static_cast<std::size_t>(dim);
|
||||
|
||||
@@ -4230,7 +4449,7 @@ class binary_writer
|
||||
// wrap (integers) or overflow to infinity (the "single" precision
|
||||
// float) instead of being reported, so such an object falls back to
|
||||
// a plain object encoding as well
|
||||
if (!write_bjdata_ndarray_elements(array_data, dtype, true))
|
||||
if (!bjdata_ndarray_elements_in_range(array_data, dtype))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -4242,7 +4461,7 @@ class binary_writer
|
||||
|
||||
write_ubjson(array_size, use_count, use_type, true, true, bjdata_version);
|
||||
|
||||
write_bjdata_ndarray_elements(array_data, dtype, false);
|
||||
write_bjdata_ndarray_values(array_data, dtype);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -4568,8 +4787,8 @@ class binary_writer
|
||||
run are encoded into space reserved from the sink, as in
|
||||
@ref write_array_elements; an integer that BON8 cannot write (an unsigned
|
||||
one above the int64 range, or one of a number type wider than 64 bits) throws
|
||||
from write_bon8_value, like any other value that is not a number. Kept out
|
||||
of line, like @ref write_cbor_array.
|
||||
out_of_range.407, as write_bon8_value does. Kept out of line, like
|
||||
@ref write_cbor_array.
|
||||
*/
|
||||
template<typename ArrayType>
|
||||
JSON_HEDLEY_NEVER_INLINE
|
||||
@@ -4619,10 +4838,10 @@ class binary_writer
|
||||
string_open = false;
|
||||
}
|
||||
|
||||
if (unencodable)
|
||||
if (JSON_HEDLEY_UNLIKELY(unencodable))
|
||||
{
|
||||
write_bon8_value(*it, string_open, depth + 1);
|
||||
++it;
|
||||
// an integer beyond the int64 range, as write_bon8_value would report it
|
||||
throw_integer_out_of_range(*it, input_format_t::bon8, "int64");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4657,14 +4876,13 @@ class binary_writer
|
||||
break;
|
||||
|
||||
case value_t::number_integer:
|
||||
if (JSON_HEDLEY_LIKELY(integer_fits<std::int64_t>(value.m_data.m_value.number_integer)))
|
||||
if (JSON_HEDLEY_UNLIKELY(!integer_fits<std::int64_t>(value.m_data.m_value.number_integer)))
|
||||
{
|
||||
write_bon8_integer(static_cast<std::int64_t>(value.m_data.m_value.number_integer));
|
||||
string_open = false;
|
||||
break;
|
||||
// beyond 64 bits (number types wider than that)
|
||||
throw_integer_out_of_range(value, input_format_t::bon8, "int64");
|
||||
}
|
||||
// beyond 64 bits (number types wider than that): throws
|
||||
write_bon8_value(value, string_open, depth + 1);
|
||||
write_bon8_integer(static_cast<std::int64_t>(value.m_data.m_value.number_integer));
|
||||
string_open = false;
|
||||
break;
|
||||
|
||||
case value_t::number_float:
|
||||
@@ -4673,14 +4891,13 @@ class binary_writer
|
||||
break;
|
||||
|
||||
case value_t::number_unsigned:
|
||||
if (JSON_HEDLEY_LIKELY(integer_fits_max<std::int64_t>(value.m_data.m_value.number_unsigned)))
|
||||
if (JSON_HEDLEY_UNLIKELY(!integer_fits_max<std::int64_t>(value.m_data.m_value.number_unsigned)))
|
||||
{
|
||||
write_bon8_integer(static_cast<std::int64_t>(value.m_data.m_value.number_unsigned));
|
||||
string_open = false;
|
||||
break;
|
||||
// too large for BON8
|
||||
throw_integer_out_of_range(value, input_format_t::bon8, "int64");
|
||||
}
|
||||
// too large for BON8: throws
|
||||
write_bon8_value(value, string_open, depth + 1);
|
||||
write_bon8_integer(static_cast<std::int64_t>(value.m_data.m_value.number_unsigned));
|
||||
string_open = false;
|
||||
break;
|
||||
|
||||
case value_t::object:
|
||||
@@ -4794,10 +5011,9 @@ class binary_writer
|
||||
static void check_utf8(const string_t& s, const BasicJsonType& context)
|
||||
{
|
||||
const auto* data = reinterpret_cast<const unsigned char*>(s.data());
|
||||
const std::size_t valid = valid_utf8_prefix(data, s.size());
|
||||
if (JSON_HEDLEY_UNLIKELY(valid != s.size()))
|
||||
if (JSON_HEDLEY_UNLIKELY(!valid_utf8_string(data, s.size())))
|
||||
{
|
||||
throw_invalid_utf8(data, valid, context);
|
||||
throw_invalid_utf8(data, valid_utf8_prefix(data, s.size()), context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <algorithm> // max
|
||||
#include <array> // array
|
||||
#include <cstddef> // size_t
|
||||
#include <cstdint> // uint32_t
|
||||
#include <cstring> // memcpy
|
||||
#include <memory> // shared_ptr, make_shared
|
||||
#include <string> // basic_string
|
||||
@@ -227,6 +228,31 @@ class output_vector_sink
|
||||
void flush() noexcept
|
||||
{}
|
||||
|
||||
/// the number of characters written so far, the position of the next one
|
||||
std::size_t written() const noexcept
|
||||
{
|
||||
return position();
|
||||
}
|
||||
|
||||
/// @brief replace the 4 bytes written before at position @a pos with
|
||||
/// those of @a n, in the byte order of the machine
|
||||
///
|
||||
/// Lets a writer put a length in front of data it has written since, such
|
||||
/// as the size of a BSON document, without computing it in advance.
|
||||
void overwrite(const std::size_t pos, const std::uint32_t n) noexcept
|
||||
{
|
||||
static_assert(sizeof(CharType) == 1, "overwrite() writes bytes");
|
||||
JSON_ASSERT(pos + sizeof(n) <= position());
|
||||
std::memcpy(v->data() + pos, &n, sizeof(n));
|
||||
}
|
||||
|
||||
/// drop the characters written from position @a pos on
|
||||
void rewind(const std::size_t pos) noexcept
|
||||
{
|
||||
JSON_ASSERT(pos <= position());
|
||||
cursor = v->data() + pos;
|
||||
}
|
||||
|
||||
private:
|
||||
/// number of characters of *v written so far
|
||||
std::size_t position() const noexcept
|
||||
|
||||
@@ -1117,7 +1117,7 @@ class serializer
|
||||
// character
|
||||
if (EnsureAscii)
|
||||
{
|
||||
JSON_ASSERT(codepoint >= 0x7F);
|
||||
JSON_ASSERT(codepoint == 0x7F);
|
||||
if (error_handler == error_handler_t::keep)
|
||||
{
|
||||
// this character was buffered as raw bytes below
|
||||
@@ -1128,21 +1128,13 @@ class serializer
|
||||
// character instead
|
||||
bytes = bytes_after_last_accept;
|
||||
}
|
||||
if (codepoint <= 0xFFFF)
|
||||
{
|
||||
write_u_escape(bytes, static_cast<std::uint16_t>(codepoint));
|
||||
}
|
||||
else
|
||||
{
|
||||
write_u_escape(bytes, static_cast<std::uint16_t>(0xD7C0u + (codepoint >> 10u)));
|
||||
write_u_escape(bytes, static_cast<std::uint16_t>(0xDC00u + (codepoint & 0x3FFu)));
|
||||
}
|
||||
write_u_escape(bytes, static_cast<std::uint16_t>(codepoint));
|
||||
}
|
||||
else
|
||||
{
|
||||
// copy byte to buffer (all previous bytes of the
|
||||
// sequence have been copied in the default case below)
|
||||
string_buffer[bytes++] = s[i];
|
||||
string_buffer[bytes++] = s[i]; // LCOV_EXCL_LINE (well-formed sequences are copied in runs)
|
||||
}
|
||||
|
||||
reserve_string_buffer(bytes);
|
||||
@@ -1977,6 +1969,23 @@ 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'
|
||||
@@ -2122,6 +2131,23 @@ class serializer
|
||||
}
|
||||
}
|
||||
|
||||
/// whether every number_magnitude_t fits 64 bits
|
||||
static constexpr bool magnitude_has_at_most_64_bits = std::numeric_limits<number_magnitude_t>::is_specialized
|
||||
&& std::numeric_limits<number_magnitude_t>::digits <= 64;
|
||||
|
||||
/// whether @a x fits 64 bits: always if number_magnitude_t has at most
|
||||
/// 64 bits (selected by type, as the comparison would be reported as
|
||||
/// always true), else compared
|
||||
static bool magnitude_fits_64_bits(const number_magnitude_t& /*unused*/, std::true_type /*unused*/) noexcept
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool magnitude_fits_64_bits(const number_magnitude_t& x, std::false_type /*unused*/) noexcept
|
||||
{
|
||||
return x <= 18446744073709551615u;
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper function for dump_integer
|
||||
*
|
||||
|
||||
@@ -139,7 +139,7 @@ inline OutStringType concat(Args && ... args)
|
||||
str.reserve(concat_length(args...));
|
||||
concat_into(str, std::forward<Args>(args)...);
|
||||
return str;
|
||||
}
|
||||
} // LCOV_EXCL_LINE (exception cleanup only)
|
||||
|
||||
} // namespace detail
|
||||
NLOHMANN_JSON_NAMESPACE_END
|
||||
@@ -83,10 +83,6 @@ template<typename StringType>
|
||||
inline void unescape(StringType& s)
|
||||
{
|
||||
auto pos = s.find_first_of('~', 0);
|
||||
if (pos == StringType::npos)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StringType result;
|
||||
result.reserve(s.size());
|
||||
|
||||
@@ -36,7 +36,7 @@ StringType to_string(std::size_t value)
|
||||
StringType result;
|
||||
int_to_string(result, value);
|
||||
return result;
|
||||
}
|
||||
} // LCOV_EXCL_LINE (exception cleanup only)
|
||||
|
||||
/// @return a byte as two uppercase hexadecimal digits
|
||||
inline std::string hex_byte(const std::uint8_t byte)
|
||||
@@ -46,7 +46,7 @@ inline std::string hex_byte(const std::uint8_t byte)
|
||||
result[0] = nibble_to_hex[byte / 16];
|
||||
result[1] = nibble_to_hex[byte % 16];
|
||||
return result;
|
||||
}
|
||||
} // LCOV_EXCL_LINE (exception cleanup only)
|
||||
|
||||
/*!
|
||||
@brief write a UTF-16 code unit as four lowercase hexadecimal digits
|
||||
@@ -390,7 +390,7 @@ inline StringType sanitize_utf8(const StringType& s, const error_handler_t error
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
} // LCOV_EXCL_LINE (exception cleanup only)
|
||||
|
||||
} // namespace detail
|
||||
NLOHMANN_JSON_NAMESPACE_END
|
||||
@@ -69,7 +69,8 @@ struct parse_failure
|
||||
};
|
||||
|
||||
/// FloatType: the number_float_t of the document, whose overflow parse() rejects
|
||||
template<typename FloatType, bool Comments, bool TrailingCommas, bool NulIsEnd, bool Sentinel>
|
||||
/// Store: false for accept(): the grammar, number, string and UTF-8 checks run as for parse(), but no node is written
|
||||
template<typename FloatType, bool Comments, bool TrailingCommas, bool NulIsEnd, bool Sentinel, bool Store>
|
||||
class builder
|
||||
{
|
||||
public:
|
||||
@@ -245,11 +246,87 @@ class builder
|
||||
return p;
|
||||
}
|
||||
|
||||
/// the string from p, the first backslash, control character, ill-formed UTF-8 byte, or the end, checked and not
|
||||
/// decoded (accept() stores nothing, so the arena is not written): the checks, errors and error positions of
|
||||
/// slow_string(), for everything up to the closing quote
|
||||
NLOHMANN_VIEW_NOINLINE decoded check_string(const unsigned char* p)
|
||||
{
|
||||
const unsigned char* const end = m_end;
|
||||
for (;;)
|
||||
{
|
||||
if (p == end)
|
||||
{
|
||||
return failed(error_code::string_missing_quote, p);
|
||||
}
|
||||
const unsigned char c = *p;
|
||||
if (c == '"')
|
||||
{
|
||||
++p;
|
||||
return decoded{p, 0, 0};
|
||||
}
|
||||
if (c != '\\')
|
||||
{
|
||||
return failed(c < 0x20 ? error_code::string_control_character : error_code::string_utf8, p);
|
||||
}
|
||||
++p;
|
||||
if (p == end)
|
||||
{
|
||||
return failed(error_code::string_missing_quote, p);
|
||||
}
|
||||
const unsigned char d = *p++;
|
||||
if (d == 'u')
|
||||
{
|
||||
const int cp = hex4(p); // (moves p past the four digits)
|
||||
if (NLOHMANN_VIEW_UNLIKELY(cp < 0))
|
||||
{
|
||||
return failed(error_code::string_unicode_hex, p);
|
||||
}
|
||||
if (NLOHMANN_VIEW_UNLIKELY((cp & 0xF800) == 0xD800)) // a surrogate
|
||||
{
|
||||
if (cp >= 0xDC00)
|
||||
{
|
||||
return failed(error_code::string_surrogate_low, p);
|
||||
}
|
||||
if (end - p < 2 || p[0] != '\\' || p[1] != 'u')
|
||||
{
|
||||
return failed(error_code::string_surrogate_high, p);
|
||||
}
|
||||
p += 2;
|
||||
const int lo = hex4(p); // (moves p past the four digits)
|
||||
if (lo < 0)
|
||||
{
|
||||
return failed(error_code::string_unicode_hex, p);
|
||||
}
|
||||
if (lo < 0xDC00 || lo > 0xDFFF)
|
||||
{
|
||||
return failed(error_code::string_surrogate_high, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!(d == '"' || d == '\\' || d == '/' || d == 'b' || d == 'f' || d == 'n' || d == 'r' || d == 't'))
|
||||
{
|
||||
--p;
|
||||
return failed(error_code::string_escape, p);
|
||||
}
|
||||
if (p != end && *p == '\\')
|
||||
{
|
||||
continue; // (escapes in a row: no run of text between them)
|
||||
}
|
||||
p = scan_string_run(p, end); // the text up to the next backslash, quote, control character, or ill-formed UTF-8
|
||||
}
|
||||
}
|
||||
|
||||
/// s points to the start of the string, p to a backslash, a control
|
||||
/// character, an ill-formed UTF-8 byte, or the end: decode the string into
|
||||
/// the arena
|
||||
NLOHMANN_VIEW_NOINLINE decoded slow_string(const unsigned char* s, const unsigned char* p)
|
||||
{
|
||||
if (!enabled(Store))
|
||||
{
|
||||
// (accept(): the string is checked, not decoded)
|
||||
static_cast<void>(s);
|
||||
return check_string(p);
|
||||
}
|
||||
// single-character escapes; 0: invalid (and 'u', handled separately)
|
||||
static const std::array<char, 128> simple_escape =
|
||||
{
|
||||
@@ -343,6 +420,21 @@ class builder
|
||||
}
|
||||
arena_ensure(w, w_end, 4);
|
||||
w = put_utf8(w, cp);
|
||||
// A chain of unicode escapes ("\u3042\u3044\u3046", as Python's json.dumps writes non-ASCII text) is
|
||||
// decoded here, without going round the loop above for each of them. Anything else (a surrogate, a
|
||||
// bad digit, fewer than six bytes) leaves p at the backslash: the loop above handles it, and reports
|
||||
// the same error at the same position as it would without this loop.
|
||||
while (end - p >= 6 && p[0] == '\\' && p[1] == 'u')
|
||||
{
|
||||
const int next = hex_codepoint(p + 2);
|
||||
if (next < 0 || (next & 0xF800) == 0xD800)
|
||||
{
|
||||
break;
|
||||
}
|
||||
p += 6;
|
||||
arena_ensure(w, w_end, 4);
|
||||
w = put_utf8(w, next);
|
||||
}
|
||||
}
|
||||
else if (NLOHMANN_VIEW_LIKELY(d < 128 && simple_escape[d] != 0))
|
||||
{
|
||||
@@ -610,10 +702,13 @@ run_found:
|
||||
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE bool run()
|
||||
{
|
||||
cold.m_doc.reserve(estimate_nodes(reinterpret_cast<const char*>(b), static_cast<std::size_t>(e - b)));
|
||||
base = cold.m_doc.nodes;
|
||||
out = base;
|
||||
cap = base + cold.m_doc.node_capacity;
|
||||
if (enabled(Store))
|
||||
{
|
||||
cold.m_doc.reserve(estimate_nodes(reinterpret_cast<const char*>(b), static_cast<std::size_t>(e - b)));
|
||||
base = cold.m_doc.nodes;
|
||||
out = base;
|
||||
cap = base + cold.m_doc.node_capacity;
|
||||
}
|
||||
|
||||
if (e - p >= 3 && p[0] == 0xEF && p[1] == 0xBB && p[2] == 0xBF)
|
||||
{
|
||||
@@ -797,7 +892,10 @@ close_object:
|
||||
// an array pays nothing for this)
|
||||
if (NLOHMANN_VIEW_UNLIKELY(cur_count >= document_data::index_min_members))
|
||||
{
|
||||
cold.note_large_object(cur_idx);
|
||||
if (enabled(Store))
|
||||
{
|
||||
cold.note_large_object(cur_idx);
|
||||
}
|
||||
}
|
||||
|
||||
close_container:
|
||||
@@ -957,6 +1055,12 @@ indent_done:
|
||||
/// an integer's value); two stores on little-endian targets
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE node* push_node(node_kind k, std::uint8_t flags, std::uint16_t extra, std::size_t off, std::uint64_t second)
|
||||
{
|
||||
if (!enabled(Store))
|
||||
{
|
||||
// accept(): nothing is stored, so what computes the arguments is not run either. base and out stay
|
||||
// null: their difference, which open() takes, is 0.
|
||||
return out;
|
||||
}
|
||||
if (NLOHMANN_VIEW_UNLIKELY(out == cap))
|
||||
{
|
||||
const auto n = static_cast<std::size_t>(out - base);
|
||||
@@ -1015,9 +1119,12 @@ indent_done:
|
||||
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE void close()
|
||||
{
|
||||
node& n = base[cur_idx];
|
||||
n.len = cur_count;
|
||||
n.next = static_cast<std::uint32_t>(out - base) - cur_idx;
|
||||
if (enabled(Store))
|
||||
{
|
||||
node& n = base[cur_idx];
|
||||
n.len = cur_count;
|
||||
n.next = static_cast<std::uint32_t>(out - base) - cur_idx;
|
||||
}
|
||||
if (--depth != 0)
|
||||
{
|
||||
if (NLOHMANN_VIEW_LIKELY(depth <= 64))
|
||||
@@ -1082,7 +1189,7 @@ indent_done:
|
||||
{
|
||||
++p;
|
||||
const unsigned char* const f0 = p;
|
||||
p = skip_digits(p, e);
|
||||
p = skip_fraction_digits(p, e);
|
||||
if (NLOHMANN_VIEW_UNLIKELY(p == f0))
|
||||
{
|
||||
return fail(error_code::number_after_dot);
|
||||
@@ -1192,34 +1299,44 @@ indent_done:
|
||||
};
|
||||
|
||||
/// run the builder with compile-time options
|
||||
template<typename FloatType, bool Comments, bool TrailingCommas, bool NulIsEnd>
|
||||
template<typename FloatType, bool Comments, bool TrailingCommas, bool NulIsEnd, bool Store>
|
||||
inline bool build_with(document_data& d, const char* src, std::size_t size, bool sentinel, parse_failure& failure)
|
||||
{
|
||||
if (sentinel)
|
||||
{
|
||||
builder<FloatType, Comments, TrailingCommas, NulIsEnd, true> bld(d, src, size);
|
||||
builder<FloatType, Comments, TrailingCommas, NulIsEnd, true, Store> bld(d, src, size);
|
||||
const bool ok = bld.run();
|
||||
failure = bld.failure();
|
||||
return ok;
|
||||
}
|
||||
builder<FloatType, Comments, TrailingCommas, NulIsEnd, false> bld(d, src, size);
|
||||
builder<FloatType, Comments, TrailingCommas, NulIsEnd, false, Store> bld(d, src, size);
|
||||
const bool ok = bld.run();
|
||||
failure = bld.failure();
|
||||
return ok;
|
||||
}
|
||||
|
||||
/// sentinel: src[size] is readable and 0 (e.g. std::string); FloatType: the
|
||||
/// number_float_t of the document
|
||||
/// number_float_t of the document; validate_only: accept(), only the result is
|
||||
/// used
|
||||
template<typename FloatType, bool NulIsEnd>
|
||||
inline bool build(document_data& d, const char* src, std::size_t size, bool comments, bool trailing_commas, bool sentinel, parse_failure& failure)
|
||||
inline bool build(document_data& d, const char* src, std::size_t size, bool comments, bool trailing_commas, bool sentinel, bool validate_only,
|
||||
parse_failure& failure)
|
||||
{
|
||||
if (validate_only && !comments && !trailing_commas)
|
||||
{
|
||||
// accept() of plain JSON: nothing is stored. The other option sets
|
||||
// could skip the stores just as well; they keep the builder that
|
||||
// stores only so that there are two more instantiations of the parse
|
||||
// loop, not eight.
|
||||
return build_with<FloatType, false, false, NulIsEnd, false>(d, src, size, sentinel, failure);
|
||||
}
|
||||
if (comments)
|
||||
{
|
||||
return trailing_commas ? build_with<FloatType, true, true, NulIsEnd>(d, src, size, sentinel, failure)
|
||||
: build_with<FloatType, true, false, NulIsEnd>(d, src, size, sentinel, failure);
|
||||
return trailing_commas ? build_with<FloatType, true, true, NulIsEnd, true>(d, src, size, sentinel, failure)
|
||||
: build_with<FloatType, true, false, NulIsEnd, true>(d, src, size, sentinel, failure);
|
||||
}
|
||||
return trailing_commas ? build_with<FloatType, false, true, NulIsEnd>(d, src, size, sentinel, failure)
|
||||
: build_with<FloatType, false, false, NulIsEnd>(d, src, size, sentinel, failure);
|
||||
return trailing_commas ? build_with<FloatType, false, true, NulIsEnd, true>(d, src, size, sentinel, failure)
|
||||
: build_with<FloatType, false, false, NulIsEnd, true>(d, src, size, sentinel, failure);
|
||||
}
|
||||
|
||||
} // namespace view
|
||||
|
||||
@@ -428,28 +428,19 @@ class view_side
|
||||
return string_view_t(m_view.m_doc->str(*m_view.m_node), m_view.m_node->len);
|
||||
}
|
||||
|
||||
/// a number, boolean, or null as a basic_json value (no allocation)
|
||||
/// a number as a basic_json value (no allocation); only called for numbers
|
||||
BasicJsonType scalar() const
|
||||
{
|
||||
switch (m_view.type())
|
||||
const value_t t = m_view.type();
|
||||
if (t == value_t::number_integer)
|
||||
{
|
||||
case value_t::number_integer:
|
||||
return BasicJsonType(m_view.template get<typename BasicJsonType::number_integer_t>());
|
||||
case value_t::number_unsigned:
|
||||
return BasicJsonType(m_view.template get<typename BasicJsonType::number_unsigned_t>());
|
||||
case value_t::number_float:
|
||||
return BasicJsonType(m_view.template get<typename BasicJsonType::number_float_t>());
|
||||
case value_t::boolean:
|
||||
return BasicJsonType(m_view.template get<bool>());
|
||||
case value_t::null:
|
||||
case value_t::object:
|
||||
case value_t::array:
|
||||
case value_t::string:
|
||||
case value_t::binary:
|
||||
case value_t::discarded:
|
||||
default:
|
||||
return BasicJsonType(nullptr);
|
||||
return BasicJsonType(m_view.template get<typename BasicJsonType::number_integer_t>());
|
||||
}
|
||||
if (t == value_t::number_unsigned)
|
||||
{
|
||||
return BasicJsonType(m_view.template get<typename BasicJsonType::number_unsigned_t>());
|
||||
}
|
||||
return BasicJsonType(m_view.template get<typename BasicJsonType::number_float_t>());
|
||||
}
|
||||
|
||||
/// start at the first element of this array
|
||||
|
||||
@@ -96,11 +96,11 @@ NLOHMANN_VIEW_NOINLINE inline void check_utf8_scan(const unsigned char* p, std::
|
||||
}
|
||||
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
|
||||
break; // ill-formed UTF-8: reported below
|
||||
}
|
||||
++q; // a quote, backslash, or control character
|
||||
}
|
||||
check_utf8_bytewise(p, n, static_cast<std::size_t>(q - p));
|
||||
}
|
||||
|
||||
inline void check_utf8(const char* s, std::size_t n)
|
||||
@@ -157,7 +157,7 @@ class editor
|
||||
node* const o = own(object);
|
||||
if (o->kind != node_kind::object && o->kind != node_kind::null)
|
||||
{
|
||||
throw_type_error(exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", object.type_name());
|
||||
throw_type_error(site_of(object), exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", object.type_name());
|
||||
}
|
||||
check_utf8(key.data(), key.size());
|
||||
const encoded e = encode(std::forward<V>(value));
|
||||
@@ -208,9 +208,9 @@ class editor
|
||||
node* const a = own(array);
|
||||
if (a->kind != node_kind::array)
|
||||
{
|
||||
throw_type_error(exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", array.type_name());
|
||||
throw_type_error(site_of(array), exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", array.type_name());
|
||||
}
|
||||
check_index(idx, a->len);
|
||||
check_index(a, idx, a->len);
|
||||
const encoded e = encode(std::forward<V>(value));
|
||||
node* const slot = const_cast<node*>(nav::value(element_at<true>(m_doc, a, idx))); // NOLINT(cppcoreguidelines-pro-type-const-cast)
|
||||
assign(slot, e, a, true);
|
||||
@@ -225,7 +225,7 @@ class editor
|
||||
node* const a = own(array);
|
||||
if (a->kind != node_kind::array && a->kind != node_kind::null)
|
||||
{
|
||||
throw_type_error(exception_id::push_back_wrong_type, "cannot use push_back() with ", array.type_name());
|
||||
throw_type_error(site_of(array), exception_id::push_back_wrong_type, "cannot use push_back() with ", array.type_name());
|
||||
}
|
||||
const encoded e = encode(std::forward<V>(value));
|
||||
if (a->kind == node_kind::null)
|
||||
@@ -249,9 +249,9 @@ class editor
|
||||
node* const a = own(array);
|
||||
if (a->kind != node_kind::array)
|
||||
{
|
||||
throw_type_error(exception_id::insert_wrong_type, "cannot use insert() with ", array.type_name());
|
||||
throw_type_error(site_of(array), exception_id::insert_wrong_type, "cannot use insert() with ", array.type_name());
|
||||
}
|
||||
check_index(idx, a->len + 1);
|
||||
check_index(a, idx, a->len + 1);
|
||||
const encoded e = encode(std::forward<V>(value));
|
||||
node* const slot = new_slot(e);
|
||||
node* const h = block_of(m_doc, a, 1);
|
||||
@@ -269,7 +269,7 @@ class editor
|
||||
node* const o = own(object);
|
||||
if (o->kind != node_kind::object)
|
||||
{
|
||||
throw_type_error(exception_id::erase_wrong_type, "cannot use erase() with ", object.type_name());
|
||||
throw_type_error(site_of(object), exception_id::erase_wrong_type, "cannot use erase() with ", object.type_name());
|
||||
}
|
||||
for (const node* k = nav::first(m_doc, o), *end = nav::end(m_doc, o); k != end; k = document_data::after(k + 1))
|
||||
{
|
||||
@@ -287,9 +287,9 @@ class editor
|
||||
node* const a = own(array);
|
||||
if (a->kind != node_kind::array)
|
||||
{
|
||||
throw_type_error(exception_id::erase_wrong_type, "cannot use erase() with ", array.type_name());
|
||||
throw_type_error(site_of(array), exception_id::erase_wrong_type, "cannot use erase() with ", array.type_name());
|
||||
}
|
||||
check_index(idx, a->len);
|
||||
check_index(a, idx, a->len);
|
||||
node* const h = block_of(m_doc, a, 0);
|
||||
std::memmove(h + 1 + idx, h + 2 + idx, (h->next - 2 - idx) * sizeof(node));
|
||||
--h->next;
|
||||
@@ -316,11 +316,11 @@ class editor
|
||||
return const_cast<node*>(v.m_node); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to this document
|
||||
}
|
||||
|
||||
static void check_index(std::size_t idx, std::size_t limit)
|
||||
void check_index(const node* array, std::size_t idx, std::size_t limit) const
|
||||
{
|
||||
if (idx >= limit)
|
||||
{
|
||||
throw_array_index_out_of_range(idx);
|
||||
throw_array_index_out_of_range(error_site(&m_doc, array), idx);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@ inline node* find_parent(const document_data& d, const node* target)
|
||||
}
|
||||
if (target == lo)
|
||||
{
|
||||
return nullptr;
|
||||
return nullptr; // LCOV_EXCL_LINE (the root, or a new value's root, which is linked)
|
||||
}
|
||||
for (;;)
|
||||
{
|
||||
|
||||
@@ -10,11 +10,15 @@
|
||||
|
||||
#include <algorithm> // min
|
||||
#include <cstddef> // size_t
|
||||
#include <string> // string
|
||||
#include <string> // string, to_string
|
||||
#include <vector> // vector
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <nlohmann/detail/view/builder.hpp>
|
||||
#include <nlohmann/detail/view/document_data.hpp>
|
||||
#include <nlohmann/detail/view/macro_scope.hpp>
|
||||
#include <nlohmann/detail/view/materialize.hpp>
|
||||
#include <nlohmann/detail/view/node.hpp>
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
namespace detail
|
||||
@@ -22,8 +26,293 @@ namespace detail
|
||||
namespace view
|
||||
{
|
||||
|
||||
/*!
|
||||
@brief the value an exception is about
|
||||
|
||||
basic_json passes the value to the exception, which then names its path
|
||||
(JSON_DIAGNOSTICS) and its bytes in the source (JSON_DIAGNOSTIC_POSITIONS). A
|
||||
view's value has no parent pointer and no stored positions: both are found
|
||||
from the document and the node, when the exception is thrown (so only the
|
||||
error path pays for them). Without the two macros, a site is empty.
|
||||
*/
|
||||
class error_site
|
||||
{
|
||||
public:
|
||||
error_site() noexcept = default;
|
||||
|
||||
#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS
|
||||
error_site(const document_data* d, const node* n) noexcept
|
||||
: m_doc(d), m_node(n)
|
||||
{}
|
||||
|
||||
const document_data* doc() const noexcept
|
||||
{
|
||||
return m_doc;
|
||||
}
|
||||
const node* value() const noexcept
|
||||
{
|
||||
return m_node;
|
||||
}
|
||||
|
||||
private:
|
||||
const document_data* m_doc = nullptr;
|
||||
const node* m_node = nullptr;
|
||||
#else
|
||||
error_site(const document_data* /*d*/, const node* /*n*/) noexcept {}
|
||||
#endif
|
||||
};
|
||||
|
||||
/// the site of a view's value (the views make this a friend)
|
||||
struct view_access
|
||||
{
|
||||
template<typename View>
|
||||
static error_site site(const View& v) noexcept
|
||||
{
|
||||
return {v.m_doc, v.m_node};
|
||||
}
|
||||
};
|
||||
|
||||
template<typename View>
|
||||
error_site site_of(const View& v) noexcept
|
||||
{
|
||||
static_assert(is_json_view<View>::value, "the site of a value is only defined for views");
|
||||
return view_access::site(v);
|
||||
}
|
||||
|
||||
#if JSON_DIAGNOSTICS
|
||||
/*!
|
||||
@brief the JSON pointer tokens from the root of document @a d to node @a target
|
||||
|
||||
Nodes have no parent index, so the path is found by descending from the root:
|
||||
before the first edit, only into the subtree that contains the target (a
|
||||
container spans the next `next` nodes); after edits, elements can live in
|
||||
other sequences, so every container is searched. Iterative (the nesting depth
|
||||
is limited by memory only). False if the target is not found (then tokens is
|
||||
unspecified).
|
||||
*/
|
||||
template<bool Editable>
|
||||
bool value_path(const document_data& d, const node* target, std::vector<std::string>& tokens)
|
||||
{
|
||||
using nav = navigation<Editable>;
|
||||
struct frame
|
||||
{
|
||||
const node* pos;
|
||||
const node* end;
|
||||
std::size_t index;
|
||||
bool object;
|
||||
};
|
||||
const node* const root = d.nodes;
|
||||
if (target == root)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (!is_container(*root))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::vector<frame> stack;
|
||||
stack.push_back(frame{nav::first(d, root), nav::end(d, root), 0, root->kind == node_kind::object});
|
||||
// (tokens[i] leads from stack[i] to stack[i + 1], or to the target)
|
||||
for (;;)
|
||||
{
|
||||
// a copy: pop_back() and push_back() below change the stack
|
||||
const frame f = stack.back();
|
||||
if (f.pos == f.end)
|
||||
{
|
||||
stack.pop_back();
|
||||
if (stack.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
tokens.pop_back();
|
||||
continue;
|
||||
}
|
||||
const node* entry = f.pos;
|
||||
std::string token;
|
||||
if (f.object)
|
||||
{
|
||||
// (as basic_json: the key up to a null byte)
|
||||
token.assign(d.str(*entry), entry->len);
|
||||
const std::size_t nul = token.find('\0');
|
||||
if (nul != std::string::npos)
|
||||
{
|
||||
token.resize(nul);
|
||||
}
|
||||
++entry;
|
||||
}
|
||||
else
|
||||
{
|
||||
token = std::to_string(f.index);
|
||||
}
|
||||
const node* const child = nav::value(entry);
|
||||
if (child == target)
|
||||
{
|
||||
tokens.push_back(token);
|
||||
return true;
|
||||
}
|
||||
stack.back().pos = document_data::after(entry);
|
||||
++stack.back().index;
|
||||
if (is_container(*child) && (nav::editable() || (target > child && target < child + child->next)))
|
||||
{
|
||||
tokens.push_back(token);
|
||||
stack.push_back(frame{nav::first(d, child), nav::end(d, child), 0, child->kind == node_kind::object});
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if JSON_DIAGNOSTIC_POSITIONS
|
||||
/*!
|
||||
@brief the bytes [start, end) of node @a n in the source, as materialize() sets them
|
||||
|
||||
Scalars as position_source::set_scalar; an array or object from its opening
|
||||
bracket to its closing bracket, which is found by following the chain of last
|
||||
elements to a scalar (or an empty container) and scanning from there to the
|
||||
closing brackets. None (npos) for values written by edits, and for containers
|
||||
whose last element has none, as with materialize().
|
||||
*/
|
||||
template<bool Editable>
|
||||
void value_range(const document_data& d, const node* n, std::size_t& start, std::size_t& end)
|
||||
{
|
||||
using nav = navigation<Editable>;
|
||||
position_source p;
|
||||
start = std::string::npos;
|
||||
end = std::string::npos;
|
||||
if (!is_container(*n))
|
||||
{
|
||||
p.set_scalar(d, *n);
|
||||
start = p.start;
|
||||
end = p.end;
|
||||
return;
|
||||
}
|
||||
if ((n->flags & node_flags::is_new) != 0 || n->off >= d.size)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// the open brackets: n, then the last element as long as it is a non-empty container
|
||||
std::size_t closing = 0;
|
||||
std::size_t pos = std::string::npos;
|
||||
for (const node* c = n;;)
|
||||
{
|
||||
if (!is_container(*c))
|
||||
{
|
||||
p.set_scalar(d, *c);
|
||||
pos = p.end;
|
||||
break;
|
||||
}
|
||||
if ((c->flags & node_flags::is_new) != 0 || c->off >= d.size)
|
||||
{
|
||||
return;
|
||||
}
|
||||
++closing;
|
||||
const node* const first = nav::first(d, c);
|
||||
const node* const last_end = nav::end(d, c);
|
||||
if (first == last_end)
|
||||
{
|
||||
pos = std::size_t{c->off} + 1;
|
||||
break;
|
||||
}
|
||||
const std::size_t value_offset = c->kind == node_kind::object ? 1 : 0;
|
||||
const node* last = first;
|
||||
for (const node* e = document_data::after(first + value_offset); e != last_end; e = document_data::after(e + value_offset))
|
||||
{
|
||||
last = e;
|
||||
}
|
||||
c = nav::value(last + value_offset);
|
||||
}
|
||||
for (; closing != 0 && pos != std::string::npos; --closing)
|
||||
{
|
||||
pos = position_source::container_end(d.base[0], d.size, pos);
|
||||
}
|
||||
if (pos != std::string::npos)
|
||||
{
|
||||
start = n->off;
|
||||
end = pos;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// the context basic_json puts in front of the message of an exception about
|
||||
/// a value: "(/path) " with JSON_DIAGNOSTICS (none for the root), then
|
||||
/// "(bytes a-b) " with JSON_DIAGNOSTIC_POSITIONS (with JSON_DIAGNOSTICS, only
|
||||
/// if there is a path)
|
||||
///
|
||||
/// This and the throw helpers that take an error_site are templates, so that
|
||||
/// the path and range code is instantiated only where an exception can be
|
||||
/// thrown: instantiated in every translation unit (as a non-template inline
|
||||
/// function makes it), it changed GCC's early inlining of unrelated view
|
||||
/// functions, so that GCC 13 no longer inlined a recursive walk over a view
|
||||
/// into itself (2.7 times the instructions).
|
||||
#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS
|
||||
template<typename Unused = void>
|
||||
std::string diagnostics(const error_site site)
|
||||
{
|
||||
if (site.doc() == nullptr || site.value() == nullptr)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
const document_data& d = *site.doc();
|
||||
const bool edited = d.edits != nullptr;
|
||||
std::string context;
|
||||
#if JSON_DIAGNOSTICS
|
||||
std::vector<std::string> tokens;
|
||||
const bool found = edited ? value_path<true>(d, site.value(), tokens) : value_path<false>(d, site.value(), tokens);
|
||||
if (!found || tokens.empty())
|
||||
{
|
||||
// context is still empty; returning it keeps the copy elided (-Wnrvo)
|
||||
return context;
|
||||
}
|
||||
std::string path;
|
||||
for (const std::string& token : tokens)
|
||||
{
|
||||
path.push_back('/');
|
||||
path += detail::escape(token);
|
||||
}
|
||||
context = concat('(', path, ") ");
|
||||
#endif
|
||||
#if JSON_DIAGNOSTIC_POSITIONS
|
||||
std::size_t start = 0;
|
||||
std::size_t end = 0;
|
||||
if (edited)
|
||||
{
|
||||
value_range<true>(d, site.value(), start, end);
|
||||
}
|
||||
else
|
||||
{
|
||||
value_range<false>(d, site.value(), start, end);
|
||||
}
|
||||
if (start != std::string::npos && end != std::string::npos)
|
||||
{
|
||||
context += concat("(bytes ", std::to_string(start), "-", std::to_string(end), ") ");
|
||||
}
|
||||
#endif
|
||||
return context;
|
||||
}
|
||||
#else
|
||||
template<typename Unused = void>
|
||||
std::string diagnostics(const error_site /*site*/)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
#endif
|
||||
|
||||
/// the context of an exception about view @a v (see exception_prefix())
|
||||
template<typename View>
|
||||
std::string diagnostics_of(const View& v)
|
||||
{
|
||||
return diagnostics<>(site_of(v));
|
||||
}
|
||||
|
||||
// Exceptions are thrown out of line, so that the accessors that may throw stay
|
||||
// small enough to be inlined.
|
||||
// small enough to be inlined. The overloads with an error_site put its context
|
||||
// in front of the message.
|
||||
//
|
||||
// The overloads without one are for exceptions that are not about a value of a
|
||||
// document: limits and invalid input of the editing and image functions, a
|
||||
// discarded view (it has no node), a view of another document (its path would
|
||||
// be one in that document), and those that basic_json creates without a
|
||||
// context too (the errors of json_pointer, invalid UTF-8). The one exception
|
||||
// about a value that has no site is key(), see view_iterator::key().
|
||||
|
||||
[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_type_error(exception_id id, const char* prefix, const char* type)
|
||||
{
|
||||
@@ -45,6 +334,24 @@ namespace view
|
||||
NLOHMANN_VIEW_THROW(invalid_iterator::create(id, msg, nullptr));
|
||||
}
|
||||
|
||||
template<typename Unused = void>
|
||||
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_type_error(const error_site site, exception_id id, const char* prefix, const char* type)
|
||||
{
|
||||
NLOHMANN_VIEW_THROW(type_error::create(id, concat(diagnostics<>(site), prefix, type), nullptr));
|
||||
}
|
||||
|
||||
template<typename Unused = void>
|
||||
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_out_of_range(const error_site site, exception_id id, const std::string& msg)
|
||||
{
|
||||
NLOHMANN_VIEW_THROW(out_of_range::create(id, concat(diagnostics<>(site), msg), nullptr));
|
||||
}
|
||||
|
||||
template<typename Unused = void>
|
||||
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_invalid_iterator(const error_site site, exception_id id, const char* msg)
|
||||
{
|
||||
NLOHMANN_VIEW_THROW(invalid_iterator::create(id, concat(diagnostics<>(site), msg), nullptr));
|
||||
}
|
||||
|
||||
/// a parse error without a position (as those of json_pointer)
|
||||
[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_parse_error(exception_id id, const std::string& msg)
|
||||
{
|
||||
@@ -53,22 +360,23 @@ namespace view
|
||||
|
||||
/// out_of_range.401: the array index @a idx is not below the size
|
||||
template<typename Index>
|
||||
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_out_of_range(const Index idx)
|
||||
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_out_of_range(const error_site site, const Index idx)
|
||||
{
|
||||
throw_out_of_range(exception_id::array_index_out_of_range, concat("array index ", std::to_string(idx), " is out of range"));
|
||||
throw_out_of_range(site, exception_id::array_index_out_of_range, concat("array index ", std::to_string(idx), " is out of range"));
|
||||
}
|
||||
|
||||
/// out_of_range.402: the JSON Pointer token '-' of an array of @a size elements
|
||||
[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_array_index_past_end(std::size_t size)
|
||||
template<typename Unused = void>
|
||||
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_past_end(const error_site site, std::size_t size)
|
||||
{
|
||||
throw_out_of_range(exception_id::pointer_past_the_end_index, concat("array index '-' (", std::to_string(size), ") is out of range"));
|
||||
throw_out_of_range(site, exception_id::pointer_past_the_end_index, concat("array index '-' (", std::to_string(size), ") is out of range"));
|
||||
}
|
||||
|
||||
/// out_of_range.403: the object has no member @a key
|
||||
template<typename Key>
|
||||
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_key_not_found(const Key& key)
|
||||
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_key_not_found(const error_site site, const Key& key)
|
||||
{
|
||||
throw_out_of_range(exception_id::key_not_found, concat("key '", key, "' not found"));
|
||||
throw_out_of_range(site, exception_id::key_not_found, concat("key '", key, "' not found"));
|
||||
}
|
||||
|
||||
/// SAX handler of the error replay: accepts every event without storing it,
|
||||
@@ -112,10 +420,12 @@ struct throwing_sax
|
||||
return true;
|
||||
}
|
||||
|
||||
// LCOV_EXCL_START (throw_parse_failure parses JSON only)
|
||||
bool binary(binary_t& /*unused*/)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// LCOV_EXCL_STOP
|
||||
|
||||
bool start_object(std::size_t /*unused*/)
|
||||
{
|
||||
|
||||
@@ -105,7 +105,10 @@ class view_iterator
|
||||
}
|
||||
|
||||
/// the key of the current object member; throws invalid_iterator.207 for
|
||||
/// other iterators, like basic_json's iterators
|
||||
/// other iterators, like basic_json's iterators (but without the context
|
||||
/// JSON_DIAGNOSTICS and JSON_DIAGNOSTIC_POSITIONS add: the iterator does not
|
||||
/// know the value it iterates over, and keeping it would make iterators
|
||||
/// larger, which keeps GCC from inlining recursive walks)
|
||||
string_view_t key() const
|
||||
{
|
||||
if (NLOHMANN_VIEW_UNLIKELY(m_value_offset == 0))
|
||||
|
||||
@@ -40,6 +40,15 @@
|
||||
#define NLOHMANN_VIEW_NOINLINE
|
||||
#endif
|
||||
|
||||
// GCC: a function compiled as written, without the clones in which interprocedural
|
||||
// constant propagation drops an argument that every caller passes as the same constant
|
||||
// (calling such a clone changes the code GCC generates for the parse loop that calls it)
|
||||
#if defined(__GNUC__) && !defined(__clang__)
|
||||
#define NLOHMANN_VIEW_NOCLONE __attribute__((noclone))
|
||||
#else
|
||||
#define NLOHMANN_VIEW_NOCLONE
|
||||
#endif
|
||||
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
#define NLOHMANN_VIEW_NODISCARD __attribute__((warn_unused_result))
|
||||
#elif defined(_MSC_VER)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#undef NLOHMANN_VIEW_UNLIKELY
|
||||
#undef NLOHMANN_VIEW_ALWAYS_INLINE
|
||||
#undef NLOHMANN_VIEW_NOINLINE
|
||||
#undef NLOHMANN_VIEW_NOCLONE
|
||||
#undef NLOHMANN_VIEW_NODISCARD
|
||||
#undef NLOHMANN_VIEW_THROW
|
||||
#undef NLOHMANN_VIEW_LITTLE_ENDIAN
|
||||
|
||||
@@ -65,14 +65,9 @@ struct position_source
|
||||
return token{end - start};
|
||||
}
|
||||
|
||||
/// the range [first, last), or none if either is npos
|
||||
/// the range [first, last)
|
||||
void set(std::size_t first, std::size_t last) noexcept
|
||||
{
|
||||
if (first == std::string::npos || last == std::string::npos)
|
||||
{
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
start = first;
|
||||
end = last;
|
||||
}
|
||||
@@ -191,11 +186,11 @@ struct position_source
|
||||
first = n.off;
|
||||
last = n.off + 4u;
|
||||
break;
|
||||
case node_kind::object:
|
||||
case node_kind::object: // LCOV_EXCL_LINE
|
||||
case node_kind::array:
|
||||
case node_kind::link:
|
||||
default:
|
||||
break;
|
||||
break; // LCOV_EXCL_LINE
|
||||
}
|
||||
// (a crafted image can point past the text)
|
||||
if (last == std::string::npos || last > d.size)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -44,6 +44,7 @@ index_status array_index(const StringType& s, std::size_t& idx) noexcept
|
||||
}
|
||||
|
||||
/// throws the exception json_pointer::array_index throws for this status
|
||||
/// (without a context, as json_pointer::array_index)
|
||||
template<typename StringType>
|
||||
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_error(index_status status, const StringType& s)
|
||||
{
|
||||
@@ -83,7 +84,7 @@ View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode)
|
||||
{
|
||||
if (mode == pointer_mode::checked)
|
||||
{
|
||||
throw_key_not_found(token);
|
||||
throw_key_not_found(site_of(cur), token);
|
||||
}
|
||||
return View();
|
||||
}
|
||||
@@ -95,7 +96,7 @@ View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode)
|
||||
{
|
||||
if (throwing)
|
||||
{
|
||||
throw_array_index_past_end(cur.size());
|
||||
throw_array_index_past_end(site_of(cur), cur.size());
|
||||
}
|
||||
return View();
|
||||
}
|
||||
@@ -114,7 +115,7 @@ View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode)
|
||||
{
|
||||
if (mode == pointer_mode::checked)
|
||||
{
|
||||
throw_array_index_out_of_range(idx);
|
||||
throw_array_index_out_of_range(site_of(cur), idx);
|
||||
}
|
||||
return View();
|
||||
}
|
||||
@@ -124,7 +125,7 @@ View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode)
|
||||
{
|
||||
if (throwing)
|
||||
{
|
||||
throw_out_of_range(exception_id::pointer_unresolved, concat("unresolved reference token '", token, "'"));
|
||||
throw_out_of_range(site_of(cur), exception_id::pointer_unresolved, concat("unresolved reference token '", token, "'"));
|
||||
}
|
||||
return View();
|
||||
}
|
||||
|
||||
@@ -198,6 +198,57 @@ inline const unsigned char* find_block_comment_end(const unsigned char* p, const
|
||||
return p;
|
||||
}
|
||||
|
||||
/// The digits of a fraction, whose number varies from one number to the next: skip_digits() with 16 bytes compared at
|
||||
/// once and the end of the run taken from a bit mask (SSE2), instead of a compare and a branch for each digit. That
|
||||
/// is about a third of the instructions for a fraction of 12 digits, and no branch that is mispredicted when the
|
||||
/// lengths vary. Where there is no SSE2, skip_digits() is used.
|
||||
#if NLOHMANN_VIEW_SSE2
|
||||
// The last bytes of the input, one at a time. GCC unrolls this loop in every inlined copy of the function below, which
|
||||
// makes the parse loop 4 KiB larger and slows down documents that have hardly any fractions; out of line, it does
|
||||
// not. Clang does better with the loop inline (a call in the parse loop costs it more than the unrolling).
|
||||
#if defined(__GNUC__) && !defined(__clang__)
|
||||
inline NLOHMANN_VIEW_NOINLINE const unsigned char* skip_digits_tail(const unsigned char* p, const unsigned char* e) noexcept
|
||||
{
|
||||
while (p != e && is_digit(*p))
|
||||
{
|
||||
++p;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
#else
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* skip_digits_tail(const unsigned char* p, const unsigned char* e) noexcept
|
||||
{
|
||||
while (p != e && is_digit(*p))
|
||||
{
|
||||
++p;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
#endif
|
||||
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* skip_fraction_digits(const unsigned char* p, const unsigned char* e) noexcept
|
||||
{
|
||||
while (e - p >= 16)
|
||||
{
|
||||
const __m128i in = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(p))); // NOLINT(bugprone-casting-through-void)
|
||||
// a digit is above '/' and below ':' (signed compares: a byte with the top bit set is negative, and neither)
|
||||
const __m128i digits = _mm_and_si128(_mm_cmpgt_epi8(in, _mm_set1_epi8('/')), _mm_cmplt_epi8(in, _mm_set1_epi8(':')));
|
||||
const auto not_digit = static_cast<std::uint64_t>(~static_cast<unsigned>(_mm_movemask_epi8(digits)) & 0xFFFFu);
|
||||
if (not_digit != 0)
|
||||
{
|
||||
return p + count_trailing_zeros(not_digit);
|
||||
}
|
||||
p += 16;
|
||||
}
|
||||
return skip_digits_tail(p, e);
|
||||
}
|
||||
#else
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* skip_fraction_digits(const unsigned char* p, const unsigned char* e) noexcept
|
||||
{
|
||||
return skip_digits(p, e);
|
||||
}
|
||||
#endif
|
||||
|
||||
/// powers of ten up to 10^19 as integers
|
||||
inline std::uint64_t int_pow10(unsigned k) noexcept
|
||||
{
|
||||
|
||||
@@ -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,23 +1138,261 @@ class view_serializer
|
||||
}
|
||||
}
|
||||
|
||||
/// (float_batch) the mark of digits whose rounding carried beyond the
|
||||
/// token's last four (see mark())
|
||||
static constexpr std::uint64_t carried() noexcept
|
||||
{
|
||||
return std::uint64_t{1} << 63u;
|
||||
}
|
||||
|
||||
/// floats of 16 or 17 digits ahead of the walk, converted together (see
|
||||
/// convert_ahead()), with their digits as mark() keeps them
|
||||
struct float_batch
|
||||
{
|
||||
static constexpr std::size_t capacity() noexcept
|
||||
{
|
||||
return 256;
|
||||
}
|
||||
/// (on the heap, allocated by the first conversion: a frame of more
|
||||
/// than a page made every dump of a small document slower)
|
||||
struct entries
|
||||
{
|
||||
std::array<const node*, 256> at; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<std::uint64_t, 256> digits; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
};
|
||||
std::unique_ptr<entries> storage{};
|
||||
const node** at = nullptr;
|
||||
std::uint64_t* digits = nullptr;
|
||||
std::size_t count = 0;
|
||||
std::size_t next = 0;
|
||||
const node* scanned = nullptr; ///< where the last scan of the node array ended
|
||||
};
|
||||
|
||||
/// (doubles) the float at n as dump() writes it, at w (64 bytes of room):
|
||||
/// from the batch if it holds n
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE char* write_double_ahead(char* w, const char* src, const node* n, float_batch& batch)
|
||||
{
|
||||
#if NLOHMANN_VIEW_VECTOR && NLOHMANN_VIEW_LITTLE_ENDIAN
|
||||
// (digits 0: not written by the batch; carried(): rounding carried
|
||||
// beyond the last four digits)
|
||||
if (batch.next != batch.count && batch.at[batch.next] == n && batch.digits[batch.next] - 1 < carried() - 1)
|
||||
{
|
||||
return write_token_digits(w, src, *n, batch.digits[batch.next++]);
|
||||
}
|
||||
return write_double_beside(w, n, batch);
|
||||
#else
|
||||
static_cast<void>(src);
|
||||
static_cast<void>(batch);
|
||||
return write_double_at(w, *n);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if NLOHMANN_VIEW_VECTOR && NLOHMANN_VIEW_LITTLE_ENDIAN
|
||||
/// write_double_ahead() for a float that is not the batch's next one (out
|
||||
/// of line: the walk is faster with less code in its loop)
|
||||
NLOHMANN_VIEW_NOINLINE char* write_double_beside(char* w, const node* n, float_batch& batch)
|
||||
{
|
||||
// (an editable document's walk can skip nodes of the array)
|
||||
const std::less<const node*> before{}; // NOLINT(modernize-use-transparent-functors) C++11 has no std::less<>
|
||||
while (batch.next != batch.count && before(batch.at[batch.next], n))
|
||||
{
|
||||
++batch.next;
|
||||
}
|
||||
if ((batch.next != batch.count && batch.at[batch.next] == n) || convert_ahead(n, batch))
|
||||
{
|
||||
const std::uint64_t digits = batch.digits[batch.next++];
|
||||
if ((digits & carried()) != 0)
|
||||
{
|
||||
return write_carried_digits(w, *n, digits & ~carried());
|
||||
}
|
||||
if (digits != 0)
|
||||
{
|
||||
return write_token_digits(w, m_doc.src, *n, digits);
|
||||
}
|
||||
}
|
||||
return write_double_at(w, *n);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief convert the floats of 16 or 17 digits among the next nodes of the
|
||||
array at once; whether n is the first of them
|
||||
|
||||
Such tokens (like the coordinates of GeoJSON, written with 17 digits) are
|
||||
written as the token with its last four digits replaced
|
||||
(write_token_digits()). Each step (the digits, locate_token(),
|
||||
token_digits_1/2()) is a long chain of dependent operations; one loop per
|
||||
step over many floats lets the processor overlap the floats, which it does
|
||||
not across the steps of one float (on Apple M1, one loop for the digits and
|
||||
locate_token() made the dump of canada.json 12 % slower). Each node of the array
|
||||
is scanned once; a node that the walk does not reach in order (in an
|
||||
editable document) is written without the batch.
|
||||
*/
|
||||
NLOHMANN_VIEW_NOINLINE bool convert_ahead(const node* n, float_batch& batch)
|
||||
{
|
||||
const std::less<const node*> before{}; // NOLINT(modernize-use-transparent-functors) C++11 has no std::less<>
|
||||
const node* const last = m_doc.nodes + m_doc.node_count;
|
||||
if (before(n, batch.scanned) || before(n, m_doc.nodes) || !before(n, last))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const char* const src = m_doc.src;
|
||||
const char* const src_end = src + m_doc.size;
|
||||
const node* const stop = last - n > 1024 ? n + 1024 : last;
|
||||
batch.count = 0;
|
||||
batch.next = 0;
|
||||
// the digits of each token (and its last four from the text)
|
||||
std::array<std::uint64_t, 256> digits; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<int, 256> exponent; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<std::uint32_t, 256> last_four; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
const node* p = n;
|
||||
if (!batch.storage)
|
||||
{
|
||||
batch.storage.reset(new typename float_batch::entries); // NOLINT(cppcoreguidelines-owning-memory,modernize-make-unique): C++11
|
||||
batch.at = batch.storage->at.data();
|
||||
batch.digits = batch.storage->digits.data();
|
||||
}
|
||||
for (; p != stop && batch.count != float_batch::capacity(); ++p)
|
||||
{
|
||||
// a token of the source: 16 or 17 digits, at least 8 after the
|
||||
// point, no exponent, 32 bytes to read
|
||||
const unsigned frac_digits = p->extra >> 8u;
|
||||
const unsigned int_digits = p->extra & 0xFFu;
|
||||
const unsigned count = int_digits + frac_digits;
|
||||
if (p->kind == node_kind::number_float && (p->flags & node_flags::storage) == 0
|
||||
&& count - 16u <= 1u && frac_digits >= 8 && src_end - (src + p->off) >= 33)
|
||||
{
|
||||
const auto* const token = reinterpret_cast<const unsigned char*>(src + p->off); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
const unsigned sign = token[0] == '-' ? 1 : 0;
|
||||
const unsigned char* const first = token + sign;
|
||||
const std::uint64_t w = sixteen_digits(first, int_digits);
|
||||
// (the 17th digit follows the point; a token with an exponent
|
||||
// gets digits outside the range of locate_token(), and is not
|
||||
// written by the batch. A token 0.xxx with 16 significant
|
||||
// digits is: its double is written as "0." and its digits too)
|
||||
const std::uint64_t w17 = (w * 10) + static_cast<std::uint64_t>(first[17] - '0');
|
||||
const std::size_t i = batch.count++;
|
||||
batch.at[i] = p;
|
||||
if (p->len != sign + count + 1)
|
||||
{
|
||||
digits[i] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
digits[i] = count == 17 ? w17 : w;
|
||||
}
|
||||
exponent[i] = -static_cast<int>(frac_digits);
|
||||
// (byte 0: the last digit, byte 1: the last two, bits 16-29: the last four)
|
||||
std::uint32_t four = 0;
|
||||
std::memcpy(&four, token + p->len - 4, 4);
|
||||
four -= 0x30303030u;
|
||||
const std::uint32_t pairs = ((four & 0x0F0F0F0Fu) * 2561u) >> 8u; // (byte 0, 2: the pairs)
|
||||
last_four[i] = (four >> 24u) | ((pairs >> 16u & 0xFFu) << 8u) | ((((pairs & 0x00FF00FFu) * 6553601u) >> 16u) << 16u);
|
||||
}
|
||||
}
|
||||
batch.scanned = p;
|
||||
// the position of each double, and the floats of either kind
|
||||
std::array<std::uint64_t, 256> offset; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<std::uint64_t, 256> radius; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<std::uint8_t, 256> kind_1; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<std::uint8_t, 256> kind_2; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::size_t count_1 = 0;
|
||||
std::size_t count_2 = 0;
|
||||
for (std::size_t i = 0; i != batch.count; ++i)
|
||||
{
|
||||
const std::uint64_t w = digits[i];
|
||||
// (the range excludes the values of bytes that are not digits, only
|
||||
// in a damaged image loaded with image_check::bounds)
|
||||
const int g = w - 1000000000000000u < 99000000000000000u ? locate_token(w, exponent[i], offset[i], radius[i]) : 3;
|
||||
batch.digits[i] = mask_if(g <= 0) & ((last_four[i] >> 16u) + 1);
|
||||
kind_1[count_1] = static_cast<std::uint8_t>(i);
|
||||
count_1 += static_cast<std::size_t>(g == 1);
|
||||
kind_2[count_2] = static_cast<std::uint8_t>(i);
|
||||
count_2 += static_cast<std::size_t>(g == 2);
|
||||
}
|
||||
// the digits to write, in one loop per kind (see mark())
|
||||
for (std::size_t k = 0; k != count_1; ++k)
|
||||
{
|
||||
const std::size_t i = kind_1[k];
|
||||
const std::uint64_t s = token_digits_1(digits[i], offset[i], radius[i], last_four[i] & 0xFFu);
|
||||
batch.digits[i] = mark(digits[i], s, last_four[i] >> 16u);
|
||||
}
|
||||
for (std::size_t k = 0; k != count_2; ++k)
|
||||
{
|
||||
const std::size_t i = kind_2[k];
|
||||
const std::uint64_t s = token_digits_2(digits[i], offset[i], radius[i], last_four[i] & 0xFFu, (last_four[i] >> 8u) & 0xFFu);
|
||||
batch.digits[i] = mark(digits[i], s, last_four[i] >> 16u);
|
||||
}
|
||||
return batch.count != 0 && batch.at[0] == n;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief a float token of 16 or 17 digits as dump() writes it, at w (64 bytes
|
||||
of room), from the digits of its double (see locate_token()) whose
|
||||
rounding carried beyond the token's last four digits
|
||||
|
||||
(like 43.42027299999999 -> 43.420273): the digits go to the writer of
|
||||
Zmij's results as its 15 or 16 leading digits and one more, which omits
|
||||
the trailing zeros.
|
||||
*/
|
||||
NLOHMANN_VIEW_NOINLINE char* write_carried_digits(char* w, const node& n, std::uint64_t digits) const noexcept
|
||||
{
|
||||
const int exponent = -(n.extra >> 8u);
|
||||
const std::uint64_t leading = digits / 10;
|
||||
const auto last = static_cast<unsigned char>(digits - (leading * 10));
|
||||
const bool seventeen = digits >= 10000000000000000u;
|
||||
*w = '-';
|
||||
w += m_doc.src[n.off] == '-' ? 1 : 0;
|
||||
return ::nlohmann::detail::dtoa_impl::write_shortest(w, seventeen ? ::nlohmann::detail::zmij::shortest_decimal{leading, exponent, last, last != 0}
|
||||
: ::nlohmann::detail::zmij::shortest_decimal{digits, exponent - 1, 0, false});
|
||||
}
|
||||
|
||||
/// the digits s of a token with the digits w and the last four digits
|
||||
/// r4, as the batch keeps them: the new last four digits + 1 (only those
|
||||
/// are written over the token's), or s marked if it differs from w beyond
|
||||
/// them (0 stays for a float the batch does not write)
|
||||
static std::uint64_t mark(std::uint64_t w, std::uint64_t s, std::uint64_t r4) noexcept
|
||||
{
|
||||
const std::uint64_t last = s - (w - r4);
|
||||
return last < 10000 ? last + 1 : s | carried();
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief a float token of 16 or 17 digits as dump() writes it, at w (64 bytes
|
||||
of room): the token with its last four digits replaced by digits - 1 (see
|
||||
mark(); the others are the token's), and without trailing zeros after the
|
||||
point, but one
|
||||
*/
|
||||
char* write_token_digits(char* w, const char* src, const node& n, std::uint64_t digits)
|
||||
{
|
||||
const char* const token = src + n.off;
|
||||
std::memcpy(w, token, 32);
|
||||
char* const end = w + n.len;
|
||||
const std::uint32_t last = four_digit_text(static_cast<std::uint32_t>(digits - 1));
|
||||
std::memcpy(end - 4, &last, 4);
|
||||
// (the token's text before the new digits, from the source: reading
|
||||
// the output back would wait for the stores)
|
||||
std::uint64_t before = 0;
|
||||
std::memcpy(&before, token + n.len - 12, 8);
|
||||
const std::uint32_t last_zeros = last ^ 0x30303030u;
|
||||
const std::uint64_t before_zeros = before ^ 0x3030303030303030u;
|
||||
if (NLOHMANN_VIEW_UNLIKELY(last_zeros == 0 && before_zeros == 0))
|
||||
{
|
||||
return write_double_at(w, n); // (twelve or more trailing zeros)
|
||||
}
|
||||
const unsigned zeros = last_zeros != 0 ? (static_cast<unsigned>(count_leading_zeros(last_zeros)) / 8) - 4
|
||||
: 4 + (static_cast<unsigned>(count_leading_zeros(before_zeros)) / 8);
|
||||
const unsigned frac_digits = n.extra >> 8u;
|
||||
return end - (zeros < frac_digits ? zeros : frac_digits - 1);
|
||||
}
|
||||
#endif
|
||||
|
||||
/// a float node as dump() writes it
|
||||
void write_float_node(const node& n)
|
||||
{
|
||||
write_float_node(n, fast_double {});
|
||||
}
|
||||
|
||||
void write_float_node(const node& n, std::false_type /*other*/)
|
||||
{
|
||||
// (only for types other than double: doubles go through write_double_ahead())
|
||||
write_float(float_value<number_float_t>(m_doc, n));
|
||||
}
|
||||
|
||||
void write_float_node(const node& n, std::true_type /*double*/)
|
||||
{
|
||||
m_out.reserve(64);
|
||||
m_out.set_cursor(write_double_at(m_out.cursor(), n));
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief (doubles) the float at n as dump() writes it, at w (64 bytes of room)
|
||||
|
||||
@@ -1095,7 +1487,7 @@ class view_serializer
|
||||
{
|
||||
const string_t dumped = BasicJsonType(string_t(reinterpret_cast<const char*>(s), n)).dump(); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
static_cast<void>(dumped);
|
||||
}
|
||||
} // LCOV_EXCL_LINE (dump() throws: the callers pass only ill-formed UTF-8)
|
||||
|
||||
template<bool EnsureAscii>
|
||||
void write_codepoint(std::uint32_t codepoint, const unsigned char* bytes, std::size_t len)
|
||||
@@ -1128,15 +1520,7 @@ class view_serializer
|
||||
}
|
||||
if (codepoint <= 0x1F || (EnsureAscii && codepoint >= 0x7F))
|
||||
{
|
||||
if (codepoint <= 0xFFFF)
|
||||
{
|
||||
write_u_escape(codepoint);
|
||||
}
|
||||
else
|
||||
{
|
||||
write_u_escape(0xD7C0u + (codepoint >> 10u));
|
||||
write_u_escape(0xDC00u + (codepoint & 0x3FFu));
|
||||
}
|
||||
write_u_escape(codepoint); // (a single byte: the only caller passes s[i])
|
||||
return;
|
||||
}
|
||||
m_out.put(reinterpret_cast<const char*>(bytes), len); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) LCOV_EXCL_LINE (printable characters are copied in runs)
|
||||
|
||||
@@ -289,9 +289,12 @@ compares, and the UTF-8 check covers the bytes up to it. Returns where the
|
||||
string scan stops, like scan_string_run: before ill-formed UTF-8 and for the
|
||||
last bytes of the input, the bytes are checked one sequence at a time. Out of
|
||||
line, so that no constants of the check occupy registers in the parse loop.
|
||||
On x86-64, it is compiled for SSSE3 (see cpu_has_ssse3()).
|
||||
On x86-64, it is compiled for SSSE3 (see cpu_has_ssse3()). Not cloned: with
|
||||
the calls of accept()'s builder, GCC 16 would clone it without plain (always
|
||||
string_plain()), and calling the clone made json_document::parse() run 0.7-1.2 %
|
||||
more instructions.
|
||||
*/
|
||||
NLOHMANN_VIEW_SSSE3_TARGET NLOHMANN_VIEW_NOINLINE inline const unsigned char* scan_string_vector(const unsigned char* p, const unsigned char* e, const std::uint8_t* plain) noexcept
|
||||
NLOHMANN_VIEW_SSSE3_TARGET NLOHMANN_VIEW_NOINLINE NLOHMANN_VIEW_NOCLONE inline const unsigned char* scan_string_vector(const unsigned char* p, const unsigned char* e, const std::uint8_t* plain) noexcept
|
||||
{
|
||||
using lookup = utf8_lookup4<>;
|
||||
const unsigned char* block = p;
|
||||
|
||||
@@ -60,7 +60,7 @@ NLOHMANN_VIEW_ALWAYS_INLINE T arithmetic_value(const document_data& d, const nod
|
||||
case value_t::binary:
|
||||
case value_t::discarded:
|
||||
default:
|
||||
throw_type_error(exception_id::type_mismatch, "type must be number, but is ", value_type_name(static_cast<value_t>(n.kind)));
|
||||
throw_type_error(error_site(&d, &n), exception_id::type_mismatch, "type must be number, but is ", value_type_name(static_cast<value_t>(n.kind)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ std::vector<U, A> vector_value(const View& v)
|
||||
{
|
||||
if (NLOHMANN_VIEW_UNLIKELY(!v.is_array()))
|
||||
{
|
||||
throw_type_error(exception_id::type_mismatch, "type must be array, but is ", v.type_name());
|
||||
throw_type_error(site_of(v), exception_id::type_mismatch, "type must be array, but is ", v.type_name());
|
||||
}
|
||||
std::vector<U, A> r;
|
||||
r.reserve(v.size());
|
||||
@@ -88,7 +88,7 @@ Map map_value(const View& v)
|
||||
{
|
||||
if (NLOHMANN_VIEW_UNLIKELY(!v.is_object()))
|
||||
{
|
||||
throw_type_error(exception_id::type_mismatch, "type must be object, but is ", v.type_name());
|
||||
throw_type_error(site_of(v), exception_id::type_mismatch, "type must be object, but is ", v.type_name());
|
||||
}
|
||||
Map r;
|
||||
for (auto it = v.begin(); it != v.end(); ++it)
|
||||
|
||||
+19
-18
@@ -150,7 +150,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
// can be restored when json_pointer backwards compatibility is removed
|
||||
// friend ::nlohmann::json_pointer<StringType>;
|
||||
|
||||
template<typename BasicJsonType, typename InputType>
|
||||
template<typename BasicJsonType, typename InputType, bool DiscardValues>
|
||||
friend class ::nlohmann::detail::parser;
|
||||
friend ::nlohmann::detail::serializer<basic_json>;
|
||||
template<typename BasicJsonType>
|
||||
@@ -183,12 +183,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
detail::parser_callback_t<basic_json>cb = nullptr,
|
||||
const bool allow_exceptions = true,
|
||||
const bool ignore_comments = false,
|
||||
const bool ignore_trailing_commas = false,
|
||||
const bool discard_number_values = false
|
||||
const bool ignore_trailing_commas = false
|
||||
)
|
||||
{
|
||||
return ::nlohmann::detail::parser<basic_json, InputAdapterType>(std::move(adapter),
|
||||
std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas, discard_number_values);
|
||||
std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas);
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -401,7 +400,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
result["compiler"] = {{"family", "clang"}, {"version", __clang_version__}};
|
||||
#elif defined(__GNUC__) || defined(__GNUG__)
|
||||
result["compiler"] = {{"family", "gcc"}, {"version", detail::concat<string_t>(
|
||||
detail::to_string<string_t>(static_cast<std::size_t>(__GNUC__)), '.',
|
||||
detail::to_string<string_t>(static_cast<std::size_t>(__GNUC__)), '.', // LCOV_EXCL_LINE
|
||||
detail::to_string<string_t>(static_cast<std::size_t>(__GNUC_MINOR__)), '.',
|
||||
detail::to_string<string_t>(static_cast<std::size_t>(__GNUC_PATCHLEVEL__)))
|
||||
}
|
||||
@@ -673,14 +672,14 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
case value_t::binary:
|
||||
binary = create<binary_t>();
|
||||
break;
|
||||
case value_t::null:
|
||||
case value_t::null: // LCOV_EXCL_LINE
|
||||
case value_t::boolean:
|
||||
case value_t::number_integer:
|
||||
case value_t::number_unsigned:
|
||||
case value_t::number_float:
|
||||
case value_t::discarded:
|
||||
default:
|
||||
break;
|
||||
break; // LCOV_EXCL_LINE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1035,14 +1034,14 @@ public:
|
||||
destroy_container(t);
|
||||
break;
|
||||
|
||||
case value_t::null:
|
||||
case value_t::null: // LCOV_EXCL_LINE
|
||||
case value_t::boolean:
|
||||
case value_t::number_integer:
|
||||
case value_t::number_unsigned:
|
||||
case value_t::number_float:
|
||||
case value_t::discarded:
|
||||
default:
|
||||
break;
|
||||
break; // LCOV_EXCL_LINE
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1215,9 +1214,9 @@ public:
|
||||
return is_array() ? array_capacity() : object_capacity();
|
||||
}
|
||||
#else
|
||||
static constexpr std::size_t container_capacity() noexcept
|
||||
static constexpr std::size_t container_capacity() noexcept // LCOV_EXCL_LINE
|
||||
{
|
||||
return detail::unknown_size();
|
||||
return detail::unknown_size(); // LCOV_EXCL_LINE
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1403,7 +1402,7 @@ public:
|
||||
/// into this switch is worth a few percent when copying a value made
|
||||
/// mostly of numbers
|
||||
JSON_HEDLEY_ALWAYS_INLINE
|
||||
static void copy_leaf_value(const basic_json& src, basic_json& dst)
|
||||
static void copy_leaf_value(const basic_json& src, basic_json& dst) // LCOV_EXCL_LINE
|
||||
{
|
||||
switch (src.m_data.m_type)
|
||||
{
|
||||
@@ -3117,7 +3116,9 @@ public:
|
||||
const bool ignore_comments,
|
||||
const bool ignore_trailing_commas)
|
||||
{
|
||||
return parser(std::forward<InputAdapterType>(adapter), nullptr, false, ignore_comments, ignore_trailing_commas, true).accept(true);
|
||||
// the parser and lexer that discard the values of tokens (see lexer)
|
||||
return ::nlohmann::detail::parser<basic_json, typename std::decay<InputAdapterType>::type, true>(std::forward<InputAdapterType>(adapter),
|
||||
nullptr, false, ignore_comments, ignore_trailing_commas).accept(true);
|
||||
}
|
||||
|
||||
/// generate SAX events from the input behind an already created adapter
|
||||
@@ -5553,7 +5554,7 @@ public:
|
||||
/// their inlined code at each call site small. Binary values are
|
||||
/// rare and left to equal_general().
|
||||
JSON_HEDLEY_ALWAYS_INLINE
|
||||
static int equal_leaf(const_reference lhs, const_reference rhs) noexcept
|
||||
static int equal_leaf(const_reference lhs, const_reference rhs) noexcept // LCOV_EXCL_LINE
|
||||
{
|
||||
#ifdef __GNUC__
|
||||
JSON_HEDLEY_DIAGNOSTIC_PUSH
|
||||
@@ -6862,7 +6863,7 @@ public:
|
||||
basic_json result(value_t::object);
|
||||
json_pointer::flatten("", *this, result);
|
||||
return result;
|
||||
}
|
||||
} // LCOV_EXCL_LINE (exception cleanup only)
|
||||
|
||||
/// @brief unflatten a previously flattened JSON value
|
||||
/// @sa https://json.nlohmann.me/api/basic_json/unflatten/
|
||||
@@ -7575,7 +7576,7 @@ public:
|
||||
result.append(path.data(), path.size());
|
||||
diff_append_key(result, key);
|
||||
return result;
|
||||
}
|
||||
} // LCOV_EXCL_LINE (exception cleanup only)
|
||||
|
||||
/// @brief the path of the element @a index of the value at @a path
|
||||
static string_t diff_index_path(const string_t& path, const std::size_t index)
|
||||
@@ -7585,7 +7586,7 @@ public:
|
||||
result.append(path.data(), path.size());
|
||||
diff_append_index(result, index);
|
||||
return result;
|
||||
}
|
||||
} // LCOV_EXCL_LINE (exception cleanup only)
|
||||
|
||||
/// @brief append the "remove" operations for the elements of array
|
||||
/// @a source from @a index on, and the "add" operations for the
|
||||
@@ -7745,7 +7746,7 @@ public:
|
||||
// operations
|
||||
if (source == target)
|
||||
{
|
||||
return false;
|
||||
return false; // LCOV_EXCL_LINE (equal objects with the same members in the same order return early)
|
||||
}
|
||||
|
||||
// slow path: the common keys are in a different relative
|
||||
|
||||
@@ -285,7 +285,7 @@ class basic_json_view
|
||||
{
|
||||
return {};
|
||||
}
|
||||
detail::view::throw_type_error(detail::exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", type_name());
|
||||
detail::view::throw_type_error(site(), detail::exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", type_name());
|
||||
}
|
||||
return lookup(key);
|
||||
}
|
||||
@@ -311,7 +311,7 @@ class basic_json_view
|
||||
{
|
||||
return {};
|
||||
}
|
||||
detail::view::throw_type_error(detail::exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", type_name());
|
||||
detail::view::throw_type_error(site(), detail::exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", type_name());
|
||||
}
|
||||
return idx < m_node->len ? element(idx) : basic_json_view();
|
||||
}
|
||||
@@ -344,12 +344,12 @@ class basic_json_view
|
||||
{
|
||||
if (NLOHMANN_VIEW_UNLIKELY(!is_object()))
|
||||
{
|
||||
detail::view::throw_type_error(detail::exception_id::at_wrong_type, "cannot use at() with ", type_name());
|
||||
detail::view::throw_type_error(site(), detail::exception_id::at_wrong_type, "cannot use at() with ", type_name());
|
||||
}
|
||||
const basic_json_view r = lookup(key);
|
||||
if (NLOHMANN_VIEW_UNLIKELY(r.is_discarded()))
|
||||
{
|
||||
detail::view::throw_key_not_found(std::string(key.data(), key.size()));
|
||||
detail::view::throw_key_not_found(site(), std::string(key.data(), key.size()));
|
||||
}
|
||||
return r;
|
||||
}
|
||||
@@ -370,11 +370,11 @@ class basic_json_view
|
||||
{
|
||||
if (NLOHMANN_VIEW_UNLIKELY(!is_array()))
|
||||
{
|
||||
detail::view::throw_type_error(detail::exception_id::at_wrong_type, "cannot use at() with ", type_name());
|
||||
detail::view::throw_type_error(site(), detail::exception_id::at_wrong_type, "cannot use at() with ", type_name());
|
||||
}
|
||||
if (NLOHMANN_VIEW_UNLIKELY(idx >= m_node->len))
|
||||
{
|
||||
detail::view::throw_array_index_out_of_range(idx);
|
||||
detail::view::throw_array_index_out_of_range(site(), idx);
|
||||
}
|
||||
return element(idx);
|
||||
}
|
||||
@@ -402,7 +402,7 @@ class basic_json_view
|
||||
{
|
||||
if (NLOHMANN_VIEW_UNLIKELY(!is_object()))
|
||||
{
|
||||
detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name());
|
||||
detail::view::throw_type_error(site(), detail::exception_id::value_wrong_type, "cannot use value() with ", type_name());
|
||||
}
|
||||
const basic_json_view r = lookup(key);
|
||||
return r.is_discarded() ? default_value : r.template get<T>();
|
||||
@@ -412,7 +412,7 @@ class basic_json_view
|
||||
{
|
||||
if (NLOHMANN_VIEW_UNLIKELY(!is_object()))
|
||||
{
|
||||
detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name());
|
||||
detail::view::throw_type_error(site(), detail::exception_id::value_wrong_type, "cannot use value() with ", type_name());
|
||||
}
|
||||
// (the default is made only if there is no such member)
|
||||
const basic_json_view r = lookup(key);
|
||||
@@ -427,7 +427,7 @@ class basic_json_view
|
||||
{
|
||||
if (NLOHMANN_VIEW_UNLIKELY(!is_structured()))
|
||||
{
|
||||
detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name());
|
||||
detail::view::throw_type_error(site(), detail::exception_id::value_wrong_type, "cannot use value() with ", type_name());
|
||||
}
|
||||
const basic_json_view r = detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::value);
|
||||
return r.is_discarded() ? default_value : r.template get<T>();
|
||||
@@ -437,7 +437,7 @@ class basic_json_view
|
||||
{
|
||||
if (NLOHMANN_VIEW_UNLIKELY(!is_structured()))
|
||||
{
|
||||
detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name());
|
||||
detail::view::throw_type_error(site(), detail::exception_id::value_wrong_type, "cannot use value() with ", type_name());
|
||||
}
|
||||
const basic_json_view r = detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::value);
|
||||
return r.is_discarded() ? string_t(default_value) : r.template get<string_t>();
|
||||
@@ -450,7 +450,7 @@ class basic_json_view
|
||||
const iterator it = begin();
|
||||
if (NLOHMANN_VIEW_UNLIKELY(it == end()))
|
||||
{
|
||||
detail::view::throw_invalid_iterator(detail::exception_id::iterator_value_unavailable, "cannot get value");
|
||||
detail::view::throw_invalid_iterator(site(), detail::exception_id::iterator_value_unavailable, "cannot get value");
|
||||
}
|
||||
return *it;
|
||||
}
|
||||
@@ -606,7 +606,7 @@ class basic_json_view
|
||||
{
|
||||
if (NLOHMANN_VIEW_UNLIKELY(!is_string()))
|
||||
{
|
||||
detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be string, but is ", type_name());
|
||||
detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be string, but is ", type_name());
|
||||
}
|
||||
return {m_doc->str(*m_node), m_node->len};
|
||||
}
|
||||
@@ -618,7 +618,7 @@ class basic_json_view
|
||||
{
|
||||
if (NLOHMANN_VIEW_UNLIKELY(!is_number()))
|
||||
{
|
||||
detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be number, but is ", type_name());
|
||||
detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be number, but is ", type_name());
|
||||
}
|
||||
return {m_doc->str(*m_node), detail::view::number_length(*m_node)};
|
||||
}
|
||||
@@ -764,15 +764,19 @@ class basic_json_view
|
||||
template<typename, view_mode> friend class basic_json_view;
|
||||
template<typename, typename> friend class detail::view::editor;
|
||||
template<typename, typename> friend class detail::view::view_side;
|
||||
friend struct detail::view::view_access;
|
||||
friend iterator;
|
||||
|
||||
/// whether the view is one of an editable document
|
||||
static constexpr bool is_editable = Editable;
|
||||
|
||||
basic_json_view(const document_data* d, const node* n) noexcept
|
||||
: m_doc(d), m_node(n)
|
||||
{}
|
||||
|
||||
/// this value as the context of an exception
|
||||
detail::view::error_site site() const noexcept
|
||||
{
|
||||
return {m_doc, m_node};
|
||||
}
|
||||
|
||||
using json_side_t = detail::view::json_side<BasicJsonType, string_view_t>;
|
||||
|
||||
static detail::view::view_side<BasicJsonType, basic_json_view> side(const basic_json_view& v) noexcept
|
||||
@@ -879,7 +883,7 @@ class basic_json_view
|
||||
{
|
||||
if (NLOHMANN_VIEW_UNLIKELY(!is_boolean()))
|
||||
{
|
||||
detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be boolean, but is ", type_name());
|
||||
detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be boolean, but is ", type_name());
|
||||
}
|
||||
return (m_node->flags & detail::view::node_flags::is_true) != 0;
|
||||
}
|
||||
@@ -889,7 +893,7 @@ class basic_json_view
|
||||
{
|
||||
if (NLOHMANN_VIEW_UNLIKELY(m_node == nullptr))
|
||||
{
|
||||
detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be number, but is ", type_name());
|
||||
detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be number, but is ", type_name());
|
||||
}
|
||||
return detail::view::arithmetic_value<T, BasicJsonType>(*m_doc, *m_node);
|
||||
}
|
||||
@@ -898,7 +902,7 @@ class basic_json_view
|
||||
{
|
||||
if (NLOHMANN_VIEW_UNLIKELY(!is_null()))
|
||||
{
|
||||
detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be null, but is ", type_name());
|
||||
detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be null, but is ", type_name());
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
@@ -1063,7 +1067,7 @@ class basic_json_document
|
||||
const bool ignore_trailing_commas = false)
|
||||
{
|
||||
basic_json_document d;
|
||||
d.read_range(first, last, allow_exceptions, ignore_comments, ignore_trailing_commas);
|
||||
d.read_range(first, last, error_mode(allow_exceptions), ignore_comments, ignore_trailing_commas);
|
||||
return d;
|
||||
}
|
||||
|
||||
@@ -1078,7 +1082,7 @@ class basic_json_document
|
||||
basic_json_document d;
|
||||
std::string buf;
|
||||
append_input(buf, std::forward<InputType>(input));
|
||||
d.build_owned(std::move(buf), allow_exceptions, ignore_comments, ignore_trailing_commas);
|
||||
d.build_owned(std::move(buf), error_mode(allow_exceptions), ignore_comments, ignore_trailing_commas);
|
||||
return d;
|
||||
}
|
||||
|
||||
@@ -1091,7 +1095,8 @@ class basic_json_document
|
||||
static bool accept(InputType&& input, const bool ignore_comments = false, const bool ignore_trailing_commas = false)
|
||||
{
|
||||
basic_json_document d;
|
||||
d.read(std::forward<InputType>(input), false, ignore_comments, ignore_trailing_commas);
|
||||
d.read_kind(std::forward<InputType>(input), on_error::validate, ignore_comments, ignore_trailing_commas,
|
||||
std::integral_constant<detail::view::input_kind, detail::view::classify_input<InputType>::value> {});
|
||||
return !d.is_discarded();
|
||||
}
|
||||
|
||||
@@ -1107,7 +1112,7 @@ class basic_json_document
|
||||
const bool ignore_comments = false,
|
||||
const bool ignore_trailing_commas = false)
|
||||
{
|
||||
read_kind(std::forward<InputType>(input), allow_exceptions, ignore_comments, ignore_trailing_commas,
|
||||
read_kind(std::forward<InputType>(input), error_mode(allow_exceptions), ignore_comments, ignore_trailing_commas,
|
||||
std::integral_constant<detail::view::input_kind, detail::view::classify_input<InputType>::value> {});
|
||||
}
|
||||
|
||||
@@ -1312,7 +1317,7 @@ class basic_json_document
|
||||
template<typename I, typename V, typename std::enable_if<detail::view::is_non_bool_integral<I>::value, int>::type = 0>
|
||||
view_type set(view_type array, I idx, V && value)
|
||||
{
|
||||
return editor().set(array, index(idx), std::forward<V>(value));
|
||||
return editor().set(array, index(array, idx), std::forward<V>(value));
|
||||
}
|
||||
|
||||
/// set the value at a JSON pointer: its parent must exist; an object
|
||||
@@ -1349,7 +1354,7 @@ class basic_json_document
|
||||
const std::size_t idx = pointer_index(token);
|
||||
if (idx >= 0xFFFFFFFFu)
|
||||
{
|
||||
detail::view::throw_array_index_out_of_range(idx);
|
||||
detail::view::throw_array_index_out_of_range(detail::view::site_of(parent), idx);
|
||||
}
|
||||
for (std::size_t i = 0; i < idx; ++i)
|
||||
{
|
||||
@@ -1383,7 +1388,7 @@ class basic_json_document
|
||||
template<typename I, typename V, typename std::enable_if<detail::view::is_non_bool_integral<I>::value, int>::type = 0>
|
||||
view_type insert(view_type array, I idx, V && value)
|
||||
{
|
||||
return editor().insert(array, index(idx), std::forward<V>(value));
|
||||
return editor().insert(array, index(array, idx), std::forward<V>(value));
|
||||
}
|
||||
|
||||
/// remove all members with this key; returns their number
|
||||
@@ -1396,7 +1401,7 @@ class basic_json_document
|
||||
template<typename I, typename std::enable_if<detail::view::is_non_bool_integral<I>::value, int>::type = 0>
|
||||
void erase(view_type array, I idx)
|
||||
{
|
||||
editor().erase(array, index(idx));
|
||||
editor().erase(array, index(array, idx));
|
||||
}
|
||||
|
||||
/// remove the value at a JSON pointer; returns the number of removed
|
||||
@@ -1430,25 +1435,25 @@ class basic_json_document
|
||||
return detail::view::editor<BasicJsonType, view_type>(*m_data);
|
||||
}
|
||||
|
||||
/// an index (out_of_range.401 if negative)
|
||||
/// an index into @a array (out_of_range.401 if negative)
|
||||
template<typename I>
|
||||
static std::size_t index(I idx)
|
||||
static std::size_t index(const view_type& array, I idx)
|
||||
{
|
||||
return index(idx, std::is_signed<I> {});
|
||||
return index(array, idx, std::is_signed<I> {});
|
||||
}
|
||||
|
||||
template<typename I>
|
||||
static std::size_t index(I idx, std::true_type /*signed*/)
|
||||
static std::size_t index(const view_type& array, I idx, std::true_type /*signed*/)
|
||||
{
|
||||
if (idx < 0)
|
||||
{
|
||||
detail::view::throw_array_index_out_of_range(idx);
|
||||
detail::view::throw_array_index_out_of_range(detail::view::site_of(array), idx);
|
||||
}
|
||||
return static_cast<std::size_t>(idx);
|
||||
}
|
||||
|
||||
template<typename I>
|
||||
static std::size_t index(I idx, std::false_type /*unsigned*/)
|
||||
static std::size_t index(const view_type& /*array*/, I idx, std::false_type /*unsigned*/)
|
||||
{
|
||||
return static_cast<std::size_t>(idx);
|
||||
}
|
||||
@@ -1466,6 +1471,22 @@ class basic_json_document
|
||||
return idx;
|
||||
}
|
||||
|
||||
/// what a failed parse does
|
||||
enum class on_error : std::uint8_t
|
||||
{
|
||||
discard, ///< the document is discarded
|
||||
raise, ///< parse_error is thrown
|
||||
validate ///< accept(): the document is discarded, and no node is stored while parsing
|
||||
};
|
||||
|
||||
static on_error error_mode(bool allow_exceptions) noexcept
|
||||
{
|
||||
return allow_exceptions ? on_error::raise : on_error::discard;
|
||||
}
|
||||
|
||||
/// NulIsEnd of the builder: a null byte ends the input unless strict_nul_handling
|
||||
static constexpr bool nul_is_end = !detail::abi_config::strict_nul_handling;
|
||||
|
||||
/// create the storage (sized for the input) on first use
|
||||
void ensure_data(const char* src, std::size_t size)
|
||||
{
|
||||
@@ -1477,7 +1498,7 @@ class basic_json_document
|
||||
|
||||
/// parse a buffer the document takes ownership of (it replaces the
|
||||
/// previous owned copy)
|
||||
void build_owned(std::string&& buf, bool allow_exceptions, bool comments, bool trailing_commas)
|
||||
void build_owned(std::string&& buf, on_error allow_exceptions, bool comments, bool trailing_commas)
|
||||
{
|
||||
ensure_data(buf.data(), buf.size());
|
||||
m_data->owned = std::move(buf);
|
||||
@@ -1487,7 +1508,7 @@ class basic_json_document
|
||||
/// parse a copy of the input; the copy is appended to the cleared owned
|
||||
/// buffer, so that its capacity is reused
|
||||
template<typename T>
|
||||
void build_copy(T&& input, bool allow_exceptions, bool comments, bool trailing_commas)
|
||||
void build_copy(T&& input, on_error allow_exceptions, bool comments, bool trailing_commas)
|
||||
{
|
||||
if (!m_data)
|
||||
{
|
||||
@@ -1508,7 +1529,7 @@ class basic_json_document
|
||||
}
|
||||
|
||||
/// sentinel: src[size] is readable and 0 (std::string, C strings)
|
||||
void build(const char* src, std::size_t size, bool allow_exceptions, bool comments, bool trailing_commas, bool owned, bool sentinel)
|
||||
void build(const char* src, std::size_t size, on_error allow_exceptions, bool comments, bool trailing_commas, bool owned, bool sentinel)
|
||||
{
|
||||
ensure_data(src, size);
|
||||
document_data& d = *m_data;
|
||||
@@ -1535,7 +1556,8 @@ class basic_json_document
|
||||
}
|
||||
else
|
||||
{
|
||||
ok = detail::view::build < typename BasicJsonType::number_float_t, !detail::abi_config::strict_nul_handling > (d, src, size, comments, trailing_commas, sentinel, failure);
|
||||
ok = detail::view::build<typename BasicJsonType::number_float_t, nul_is_end>(d, src, size, comments, trailing_commas, sentinel,
|
||||
allow_exceptions == on_error::validate, failure);
|
||||
}
|
||||
if (NLOHMANN_VIEW_LIKELY(ok))
|
||||
{
|
||||
@@ -1548,7 +1570,7 @@ class basic_json_document
|
||||
return;
|
||||
}
|
||||
std::vector<std::uint32_t>().swap(d.large_objects);
|
||||
if (allow_exceptions)
|
||||
if (allow_exceptions == on_error::raise)
|
||||
{
|
||||
detail::view::throw_parse_failure<BasicJsonType>(failure, src, size, comments, trailing_commas);
|
||||
}
|
||||
@@ -1556,13 +1578,13 @@ class basic_json_document
|
||||
|
||||
// --- input dispatch (see detail::view::input_kind) ---
|
||||
|
||||
void read_kind(std::string&& s, bool ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::move_string> /*unused*/)
|
||||
void read_kind(std::string&& s, on_error ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::move_string> /*unused*/)
|
||||
{
|
||||
build_owned(std::move(s), ae, c, tc);
|
||||
}
|
||||
|
||||
template<typename CharT>
|
||||
void read_kind(CharT* s, bool ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::c_string> /*unused*/)
|
||||
void read_kind(CharT* s, on_error ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::c_string> /*unused*/)
|
||||
{
|
||||
if (s == nullptr)
|
||||
{
|
||||
@@ -1576,7 +1598,7 @@ class basic_json_document
|
||||
}
|
||||
|
||||
template<typename Array>
|
||||
void read_kind(Array& a, bool ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::char_array> /*unused*/)
|
||||
void read_kind(Array& a, on_error ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::char_array> /*unused*/)
|
||||
{
|
||||
using CharT = typename std::remove_cv<typename std::remove_extent<Array>::type>::type;
|
||||
const std::size_t n = std::extent<Array>::value;
|
||||
@@ -1587,7 +1609,7 @@ class basic_json_document
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void read_kind(const T& s, bool ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::borrow_range> /*unused*/)
|
||||
void read_kind(const T& s, on_error ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::borrow_range> /*unused*/)
|
||||
{
|
||||
// std::basic_string guarantees data()[size()] == 0: use it as sentinel
|
||||
const auto size = static_cast<std::size_t>(s.size());
|
||||
@@ -1596,13 +1618,13 @@ class basic_json_document
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void read_kind(const T& s, bool ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::copy_range> /*unused*/)
|
||||
void read_kind(const T& s, on_error ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::copy_range> /*unused*/)
|
||||
{
|
||||
build_copy(s, ae, c, tc);
|
||||
}
|
||||
|
||||
template<typename CharT>
|
||||
void read_kind(CharT* s, bool ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::wide_c_string> /*unused*/)
|
||||
void read_kind(CharT* s, on_error ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::wide_c_string> /*unused*/)
|
||||
{
|
||||
if (s == nullptr)
|
||||
{
|
||||
@@ -1614,13 +1636,13 @@ class basic_json_document
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void read_kind(T&& input, bool ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::adapter> /*unused*/)
|
||||
void read_kind(T&& input, on_error ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::adapter> /*unused*/)
|
||||
{
|
||||
build_copy(std::forward<T>(input), ae, c, tc);
|
||||
}
|
||||
|
||||
template<typename IteratorType>
|
||||
void read_range(IteratorType first, IteratorType last, bool ae, bool c, bool tc)
|
||||
void read_range(IteratorType first, IteratorType last, on_error ae, bool c, bool tc)
|
||||
{
|
||||
using value_type = typename std::remove_cv<typename std::iterator_traits<IteratorType>::value_type>::type;
|
||||
// borrow the range where the library's input adapter scans it as one
|
||||
@@ -1631,14 +1653,14 @@ class basic_json_document
|
||||
}
|
||||
|
||||
template<typename IteratorType>
|
||||
void read_range_impl(IteratorType first, IteratorType last, bool ae, bool c, bool tc, std::true_type /*contiguous bytes*/)
|
||||
void read_range_impl(IteratorType first, IteratorType last, on_error ae, bool c, bool tc, std::true_type /*contiguous bytes*/)
|
||||
{
|
||||
const auto size = static_cast<std::size_t>(std::distance(first, last));
|
||||
build(size == 0 ? "" : reinterpret_cast<const char*>(&*first), size, ae, c, tc, false, size == 0); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
}
|
||||
|
||||
template<typename IteratorType>
|
||||
void read_range_impl(IteratorType first, IteratorType last, bool ae, bool c, bool tc, std::false_type /*other*/)
|
||||
void read_range_impl(IteratorType first, IteratorType last, on_error ae, bool c, bool tc, std::false_type /*other*/)
|
||||
{
|
||||
build_copy(detail::view::make_adapter_input(detail::input_adapter(first, last)), ae, c, tc);
|
||||
}
|
||||
@@ -1683,9 +1705,11 @@ class basic_json_document
|
||||
{
|
||||
// (as before: the library's adapter decides what a null stream yields)
|
||||
detail::view::append_adapter(buf, detail::input_adapter(file));
|
||||
return;
|
||||
}
|
||||
detail::view::append_file(buf, file);
|
||||
else
|
||||
{
|
||||
detail::view::append_file(buf, file);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
Generated
+11880
File diff suppressed because it is too large.
Load diff
Generated
+1444
-504
File diff suppressed because it is too large.
Load diff
Generated
+1649
-601
File diff suppressed because it is too large.
Load diff
@@ -15,6 +15,7 @@
|
||||
#include <nlohmann/json_view.hpp>
|
||||
|
||||
#include <algorithm> // any_of
|
||||
#include <cstddef> // size_t
|
||||
#include <functional> // function
|
||||
#include <random> // mt19937
|
||||
#include <string> // string
|
||||
@@ -88,6 +89,27 @@ inline bool has_duplicate_keys(const nlohmann::ordered_json_view& v)
|
||||
});
|
||||
}
|
||||
|
||||
// the message of an exception about the value at path (a JSON pointer, "" for
|
||||
// the root) whose source bytes are [start, end): the context basic_json adds
|
||||
// with JSON_DIAGNOSTICS and JSON_DIAGNOSTIC_POSITIONS, if enabled, then what
|
||||
inline std::string message(const std::string& id, const std::string& path, std::size_t start, std::size_t end, const std::string& what)
|
||||
{
|
||||
#if JSON_DIAGNOSTIC_POSITIONS
|
||||
const std::string positions = "(bytes " + std::to_string(start) + "-" + std::to_string(end) + ") ";
|
||||
#else
|
||||
const std::string positions;
|
||||
static_cast<void>(start);
|
||||
static_cast<void>(end);
|
||||
#endif
|
||||
#if JSON_DIAGNOSTICS
|
||||
const std::string context = path.empty() ? "" : "(" + path + ") " + positions;
|
||||
#else
|
||||
const std::string& context = positions;
|
||||
static_cast<void>(path);
|
||||
#endif
|
||||
return "[json.exception." + id + "] " + context + what;
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// the exception a call throws, or "" if it throws none
|
||||
inline std::string exception_of_call(const std::function<void()>& f)
|
||||
|
||||
@@ -14,7 +14,9 @@ using nlohmann::json;
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -767,3 +769,69 @@ TEST_CASE("a write that throws leaves the same bytes in every kind of output")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
namespace
|
||||
{
|
||||
|
||||
// an adapter whose every write fails
|
||||
class failing_adapter : public nlohmann::detail::output_adapter_protocol<char>
|
||||
{
|
||||
public:
|
||||
[[noreturn]] void write_character(char /*c*/) override
|
||||
{
|
||||
throw std::runtime_error("write failed");
|
||||
}
|
||||
|
||||
[[noreturn]] void write_characters(const char* /*s*/, std::size_t /*length*/) override
|
||||
{
|
||||
throw std::runtime_error("write failed");
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
TEST_CASE("output adapters")
|
||||
{
|
||||
SECTION("writing a single character")
|
||||
{
|
||||
// the writers pass whole buffers on, but an adapter takes single
|
||||
// characters as well
|
||||
std::vector<char> as_vector;
|
||||
nlohmann::detail::output_vector_adapter<char> vector_adapter(as_vector);
|
||||
vector_adapter.write_character('a');
|
||||
vector_adapter.write_character('b');
|
||||
CHECK(as_vector == std::vector<char>({'a', 'b'}));
|
||||
|
||||
std::ostringstream as_stream;
|
||||
nlohmann::detail::output_stream_adapter<char> stream_adapter(as_stream);
|
||||
stream_adapter.write_character('c');
|
||||
stream_adapter.write_character('d');
|
||||
CHECK(as_stream.str() == "cd");
|
||||
|
||||
std::string as_string;
|
||||
nlohmann::detail::output_string_adapter<char> string_adapter(as_string);
|
||||
string_adapter.write_character('e');
|
||||
string_adapter.write_character('f');
|
||||
CHECK(as_string == "ef");
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("a sink that is destroyed with characters left over")
|
||||
{
|
||||
// the destructor passes the pending characters on, as a write that
|
||||
// threw leaves them there; the adapter failing then must not escape
|
||||
{
|
||||
nlohmann::detail::output_adapter_sink<char> sink(std::make_shared<failing_adapter>());
|
||||
sink.write_character('x');
|
||||
}
|
||||
|
||||
// while flush() reports the failure
|
||||
nlohmann::detail::output_adapter_sink<char> sink(std::make_shared<failing_adapter>());
|
||||
sink.write_character('x');
|
||||
CHECK_THROWS_AS(sink.flush(), std::runtime_error&);
|
||||
// (the character counts as written, so there is nothing left for the destructor)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -26,11 +26,14 @@ using nlohmann::ordered_json;
|
||||
#include <limits>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <list>
|
||||
#include <set>
|
||||
#include "make_test_data_available.hpp"
|
||||
#include "test_utils.hpp"
|
||||
#include "sax_countdown.hpp"
|
||||
#include "sax_event_loggers.hpp"
|
||||
using utils::SaxCountdown;
|
||||
using utils::SaxEventLogger;
|
||||
|
||||
|
||||
// trait_test_arg and the "value_in_range_of trait" TEST_CASE_TEMPLATE_DEFINE
|
||||
@@ -2309,6 +2312,23 @@ TEST_CASE("BJData")
|
||||
CHECK_FALSE(json::sax_parse(v, &scp, json::input_format_t::bjdata));
|
||||
}
|
||||
|
||||
SECTION("every event of an ndarray of chars")
|
||||
{
|
||||
// the elements of an ndarray of 'C' are read one by one
|
||||
std::vector<uint8_t> const v = {'[', '$', 'C', '#', '[', '$', 'U', '#', 'U', 2, 2, 2, 'a', 'b', 'c', 'd'};
|
||||
// start_object, key, string, key, start_array, 2 numbers, end_array,
|
||||
// key, start_array, 4 chars, end_array, end_object
|
||||
constexpr int events = 16;
|
||||
for (int allowed = 0; allowed < events; ++allowed)
|
||||
{
|
||||
CAPTURE(allowed)
|
||||
SaxCountdown scp(allowed);
|
||||
CHECK_FALSE(json::sax_parse(v, &scp, json::input_format_t::bjdata));
|
||||
}
|
||||
SaxCountdown scp(events);
|
||||
CHECK(json::sax_parse(v, &scp, json::input_format_t::bjdata));
|
||||
}
|
||||
|
||||
SECTION("start_array() in ndarray _ArrayData_")
|
||||
{
|
||||
std::vector<uint8_t> const v = {'[', '$', 'U', '#', '[', 'i', 2, 'i', 3, ']', 6, 5, 4, 3, 2, 1};
|
||||
@@ -3092,6 +3112,15 @@ TEST_CASE("BJData")
|
||||
json _;
|
||||
CHECK_THROWS_WITH(_ = json::from_bjdata(v), "[json.exception.parse_error.113] parse error at byte 2: syntax error while parsing BJData char: byte after 'C' must be in range 0x00..0x7F; last byte: 0x82");
|
||||
}
|
||||
|
||||
SECTION("ndarray of chars cut off by the end of the input")
|
||||
{
|
||||
// 2x2 chars announced, three present
|
||||
std::vector<uint8_t> const v = {'[', '$', 'C', '#', '[', '$', 'U', '#', 'U', 2, 2, 2, 'a', 'b', 'c'};
|
||||
json _;
|
||||
CHECK_THROWS_WITH_AS(_ = json::from_bjdata(v), "[json.exception.parse_error.110] parse error at byte 16: syntax error while parsing BJData char: unexpected end of input", json::parse_error&);
|
||||
CHECK(json::from_bjdata(v, true, false).is_discarded());
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("byte")
|
||||
@@ -4746,3 +4775,77 @@ TEST_CASE("BJData optimized container type marker is validated for count 0")
|
||||
CHECK(json::from_bjdata(vb) == json::binary({}));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("BJData optimized arrays from contiguous and non-contiguous input")
|
||||
{
|
||||
// contiguous input reads the numbers of an array with a type marker in
|
||||
// one loop, other input number by number; both must give the same events,
|
||||
// value, or error for the complete document and for every truncation of
|
||||
// it, and stop at every event a SAX handler rejects
|
||||
json document =
|
||||
{
|
||||
{"u8", {0, 200, 255}},
|
||||
{"i8", {-5, 100, -128}},
|
||||
{"i16", {-30000, 1000, 30000}},
|
||||
{"i32", {-2000000000, 5, 70000}},
|
||||
{"i64", {-9000000000000000000LL, 1, 5000000000LL}},
|
||||
{"f64", {1.5, -0.1, 2.5}},
|
||||
{"pairs", {{1.25, -3.75}, {1.25, -3.75}, json::array()}},
|
||||
{"mixed", {1, "a", 2.5, nullptr}},
|
||||
{"u16", {40000, 1, 65535}},
|
||||
{"u32", {4000000000U, 1, 70000}},
|
||||
{"u64", {18446744073709551615ULL, 1, 5000000000ULL}},
|
||||
{"nd", {{"_ArrayType_", "double"}, {"_ArraySize_", {2, 2}}, {"_ArrayData_", {1.5, -2.5, 3.5, -4.5}}}},
|
||||
{"nd8", {{"_ArrayType_", "uint8"}, {"_ArraySize_", {3}}, {"_ArrayData_", {1, 2, 3}}}},
|
||||
};
|
||||
const std::vector<std::uint8_t> encoded = json::to_bjdata(document, true, true);
|
||||
CHECK(json::from_bjdata(encoded) == document);
|
||||
|
||||
for (std::size_t length = 0; length <= encoded.size(); ++length)
|
||||
{
|
||||
CAPTURE(length)
|
||||
const std::vector<std::uint8_t> contiguous(encoded.begin(), encoded.begin() + static_cast<std::ptrdiff_t>(length));
|
||||
const std::list<std::uint8_t> bytewise(contiguous.begin(), contiguous.end());
|
||||
|
||||
CHECK(json::from_bjdata(contiguous, true, false).dump() == json::from_bjdata(bytewise.begin(), bytewise.end(), true, false).dump());
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
std::string contiguous_error;
|
||||
std::string bytewise_error;
|
||||
try
|
||||
{
|
||||
json _;
|
||||
_ = json::from_bjdata(contiguous);
|
||||
}
|
||||
catch (const json::exception& e)
|
||||
{
|
||||
contiguous_error = e.what();
|
||||
}
|
||||
try
|
||||
{
|
||||
json _;
|
||||
_ = json::from_bjdata(bytewise.begin(), bytewise.end());
|
||||
}
|
||||
catch (const json::exception& e)
|
||||
{
|
||||
bytewise_error = e.what();
|
||||
}
|
||||
CHECK(contiguous_error == bytewise_error);
|
||||
#endif
|
||||
|
||||
SaxEventLogger from_contiguous;
|
||||
SaxEventLogger from_bytewise;
|
||||
CHECK(json::sax_parse(contiguous, &from_contiguous, json::input_format_t::bjdata)
|
||||
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::bjdata));
|
||||
CHECK(from_contiguous.events == from_bytewise.events);
|
||||
}
|
||||
|
||||
for (int events = 0; events <= 80; ++events)
|
||||
{
|
||||
CAPTURE(events)
|
||||
const std::list<std::uint8_t> bytewise(encoded.begin(), encoded.end());
|
||||
SaxCountdown from_contiguous(events);
|
||||
SaxCountdown from_bytewise(events);
|
||||
CHECK(json::sax_parse(encoded, &from_contiguous, json::input_format_t::bjdata)
|
||||
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::bjdata));
|
||||
}
|
||||
}
|
||||
@@ -329,6 +329,9 @@ TEST_CASE("BON8")
|
||||
check_bon8({false, nullptr, true}, {0x83, 0xF8, 0xFA, 0xF9});
|
||||
check_bon8({false, nullptr, true, 1.0}, {0x84, 0xF8, 0xFA, 0xF9, 0xFD});
|
||||
check_bon8({false, nullptr, true, 1.0, json::array(), 0.0}, {0x85, 0xF8, 0xFA, 0xF9, 0xFD, 0x80, 0xFC, 0xFE});
|
||||
// an array that starts with a number: -1, 0, and 1 have bytes of their own
|
||||
check_bon8({-1.0, 0.0, 1.0}, {0x83, 0xFB, 0xFC, 0xFD});
|
||||
check_bon8({1.0, -1.0, -0.0}, {0x83, 0xFD, 0xFB, 0x8E, 0x80, 0x00, 0x00, 0x00});
|
||||
check_bon8({{{1}}}, {0x81, 0x81, 0x81, 0x91});
|
||||
check_bon8({{{"foo"}}}, {0x81, 0x81, 0x81, 'f', 'o', 'o', 0xFF});
|
||||
check_bon8({{{""}}}, {0x81, 0x81, 0x81, 0xFF});
|
||||
|
||||
+261
-2
@@ -19,6 +19,7 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
@@ -80,8 +81,20 @@ class huge_string_t : public std::string
|
||||
return result;
|
||||
}
|
||||
|
||||
// returns a copy of @a s whose size() pretends to be @a n
|
||||
static huge_string_t as_size(const std::string& s, const size_type n)
|
||||
{
|
||||
huge_string_t result(s);
|
||||
result.fake_size = n;
|
||||
return result;
|
||||
}
|
||||
|
||||
size_type size() const noexcept
|
||||
{
|
||||
if (fake_size != 0)
|
||||
{
|
||||
return fake_size;
|
||||
}
|
||||
if (pretend_huge)
|
||||
{
|
||||
// one byte more than the BSON length field can represent
|
||||
@@ -92,6 +105,7 @@ class huge_string_t : public std::string
|
||||
|
||||
private:
|
||||
bool pretend_huge = false;
|
||||
size_type fake_size = 0;
|
||||
};
|
||||
|
||||
using huge_string_json = nlohmann::json::with_string_t<huge_string_t>;
|
||||
@@ -266,6 +280,17 @@ TEST_CASE("BSON")
|
||||
CHECK_THROWS_WITH_AS(huge_string_json::to_bson(j), "[json.exception.out_of_range.412] BSON length 2147483661 exceeds maximum of 2147483647", huge_string_json::out_of_range&);
|
||||
}
|
||||
|
||||
SECTION("an array element that no longer fits the document")
|
||||
{
|
||||
// the string fits the length field, but not the document that
|
||||
// holds it; nothing of it is written
|
||||
const auto limit = static_cast<std::string::size_type>((std::numeric_limits<std::int32_t>::max)());
|
||||
huge_string_json j;
|
||||
j["a"] = huge_string_json::array({huge_string_t::as_size("value", limit - 8)});
|
||||
|
||||
CHECK_THROWS_AS(huge_string_json::to_bson(j), huge_string_json::out_of_range&);
|
||||
}
|
||||
|
||||
SECTION("document")
|
||||
{
|
||||
// an oversized string nested one level deep makes the
|
||||
@@ -897,6 +922,38 @@ TEST_CASE("BSON")
|
||||
CHECK_THROWS_AS(_ = json::from_bson(input), json::parse_error&);
|
||||
}
|
||||
|
||||
SECTION("binary member with subtype 0x02 (old binary) with a truncated inner length")
|
||||
{
|
||||
// the inner length field is cut off by the end of the input
|
||||
std::vector<std::uint8_t> const input =
|
||||
{
|
||||
0x0F, 0x00, 0x00, 0x00, // size (little endian)
|
||||
0x05, 'a', 0x00, // entry: binary
|
||||
0x08, 0x00, 0x00, 0x00, // size of binary (little endian)
|
||||
0x02, // "old binary" subtype
|
||||
0x01 // first byte of the inner length
|
||||
};
|
||||
|
||||
json _;
|
||||
CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.110] parse error at byte 14: syntax error while parsing BSON number: unexpected end of input", json::parse_error&);
|
||||
CHECK(json::from_bson(input, true, false).is_discarded());
|
||||
}
|
||||
|
||||
SECTION("string member that is cut off by the end of the input")
|
||||
{
|
||||
std::vector<std::uint8_t> const input =
|
||||
{
|
||||
0x0F, 0x00, 0x00, 0x00, // size (little endian)
|
||||
0x02, 'a', 0x00, // entry: string
|
||||
0x0A, 0x00, 0x00, 0x00, // size of the string (little endian)
|
||||
'b', 'c' // the string ends early
|
||||
};
|
||||
|
||||
json _;
|
||||
CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.110] parse error at byte 14: syntax error while parsing BSON string: unexpected end of input", json::parse_error&);
|
||||
CHECK(json::from_bson(input, true, false).is_discarded());
|
||||
}
|
||||
|
||||
SECTION("Some more complex document")
|
||||
{
|
||||
json const j =
|
||||
@@ -1164,7 +1221,13 @@ TEST_CASE("BSON keys from contiguous and stream input")
|
||||
// contiguous input reads a key up to its \x00-byte in one step, a stream
|
||||
// reads it byte by byte; both must give the same value or error for the
|
||||
// complete document and for every truncation of it
|
||||
const json j = {{"", true}, {"k", {1, 2, 3}}, {std::string(40, 'x'), {{"nested key", "value"}}}};
|
||||
json j = {{"", true}, {"k", {1, 2, 3}}, {std::string(40, 'x'), {{"nested key", "value"}}}};
|
||||
// keys and array element names ("0" to "11") that end at every position of
|
||||
// an 8-byte word, which contiguous input scans at a time
|
||||
for (std::size_t length = 1; length <= 17; ++length)
|
||||
{
|
||||
j[std::string(length, static_cast<char>('a' + length))] = json::array({0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, {11}});
|
||||
}
|
||||
const std::vector<std::uint8_t> bson = json::to_bson(j);
|
||||
CHECK(json::from_bson(bson) == j);
|
||||
|
||||
@@ -1184,7 +1247,7 @@ TEST_CASE("BSON keys from contiguous and stream input")
|
||||
}
|
||||
try
|
||||
{
|
||||
std::istringstream stream(std::string(input.begin(), input.end()));
|
||||
std::istringstream stream(std::string(reinterpret_cast<const char*>(input.data()), input.size()));
|
||||
from_stream = json::from_bson(stream).dump();
|
||||
}
|
||||
catch (const json::parse_error& e)
|
||||
@@ -1194,6 +1257,33 @@ TEST_CASE("BSON keys from contiguous and stream input")
|
||||
CHECK(from_vector == from_stream);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("BSON array element names")
|
||||
{
|
||||
// an array's element names are not part of the value, so they are not
|
||||
// passed on; error_handler_t::strict still rejects ill-formed UTF-8 in them
|
||||
const std::vector<std::uint8_t> bson =
|
||||
{
|
||||
0x1D, 0x00, 0x00, 0x00, // size (little endian)
|
||||
0x04, 'a', 0x00, // entry: array "a"
|
||||
0x15, 0x00, 0x00, 0x00, // size (little endian)
|
||||
0x08, 0xFF, 0x00, 0x01, // entry: boolean named "\xFF", true
|
||||
0x08, '1', '2', '3', '4', '5', '6', '7', '8', '9', 0x00, 0x00, // entry: boolean named "123456789", false
|
||||
0x00, // end of the array
|
||||
0x00 // end of the document
|
||||
};
|
||||
const json expected = {{"a", {true, false}}};
|
||||
|
||||
CHECK(json::from_bson(bson) == expected);
|
||||
CHECK(json::from_bson(bson, true, true, json::error_handler_t::replace) == expected);
|
||||
CHECK(json::from_bson(bson, true, true, json::error_handler_t::ignore) == expected);
|
||||
|
||||
json _;
|
||||
CHECK_THROWS_WITH_AS(_ = json::from_bson(bson, true, true, json::error_handler_t::strict), "[json.exception.parse_error.113] parse error at byte 14: syntax error while parsing BSON key: invalid string: ill-formed UTF-8 byte", json::parse_error&);
|
||||
|
||||
std::istringstream stream(std::string(reinterpret_cast<const char*>(bson.data()), bson.size()));
|
||||
CHECK(json::from_bson(stream) == expected);
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST_CASE("Negative size of binary value")
|
||||
@@ -2102,6 +2192,25 @@ value = depth % 2 == 0 ? json{{"a", std::move(value)}, {"b", {1, "x"}}} :
|
||||
CHECK(output.empty());
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("U+0000 in a long key is found at any position")
|
||||
{
|
||||
// the key is searched eight characters at a time
|
||||
for (const std::string& key :
|
||||
{
|
||||
std::string("abc\0defghijkl", 12), std::string("abcdefgh\0ijkl", 13), std::string("abcdefghijklmnop\0", 17), std::string("\0abcdefghijkl", 13),
|
||||
std::string("abcdefghijklmno\0", 16)
|
||||
})
|
||||
{
|
||||
CAPTURE(key)
|
||||
const json value = json::object({{key, 1}});
|
||||
std::vector<std::uint8_t> output;
|
||||
CHECK_THROWS_AS(json::to_bson(value, output), json::out_of_range&);
|
||||
CHECK(output.empty());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
SECTION("a binary subtype that doesn't fit a byte is rejected before anything is written (#5675)")
|
||||
{
|
||||
// the offending value is nested, so this also covers that the check
|
||||
@@ -2267,3 +2376,153 @@ TEST_CASE("BSON large strings and binaries (chunked reader)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("BSON written in one pass and in two passes")
|
||||
{
|
||||
// to_bson() into a returned vector writes each size once the object or
|
||||
// array is complete; through an output adapter, all sizes are computed
|
||||
// first. Both must produce the same bytes and throw the same exceptions.
|
||||
const auto two_pass = [](const json & j, const json::error_handler_t handler)
|
||||
{
|
||||
std::vector<std::uint8_t> result;
|
||||
json::to_bson(j, result, handler);
|
||||
return result;
|
||||
};
|
||||
|
||||
const auto check_same = [&](const json & j, const json::error_handler_t handler)
|
||||
{
|
||||
CAPTURE(j.dump(-1, ' ', true, json::error_handler_t::replace))
|
||||
CAPTURE(static_cast<int>(handler))
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
std::string one_pass_error;
|
||||
std::string two_pass_error;
|
||||
std::vector<std::uint8_t> one;
|
||||
std::vector<std::uint8_t> two;
|
||||
try
|
||||
{
|
||||
one = json::to_bson(j, handler);
|
||||
}
|
||||
catch (const json::exception& e)
|
||||
{
|
||||
one_pass_error = e.what();
|
||||
}
|
||||
try
|
||||
{
|
||||
two = two_pass(j, handler);
|
||||
}
|
||||
catch (const json::exception& e)
|
||||
{
|
||||
two_pass_error = e.what();
|
||||
}
|
||||
CHECK(one_pass_error == two_pass_error);
|
||||
CHECK(one == two);
|
||||
#else
|
||||
CHECK(json::to_bson(j, handler) == two_pass(j, handler));
|
||||
#endif
|
||||
};
|
||||
|
||||
const std::array<json::error_handler_t, 4> handlers = {{
|
||||
json::error_handler_t::keep, json::error_handler_t::strict,
|
||||
json::error_handler_t::replace, json::error_handler_t::ignore
|
||||
}
|
||||
};
|
||||
|
||||
SECTION("corpus")
|
||||
{
|
||||
for (const auto& value : utils::round_trip_corpus::values())
|
||||
{
|
||||
const json document = value.is_object() ? value : json{{"value", value}};
|
||||
for (const auto handler : handlers)
|
||||
{
|
||||
check_same(document, handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("nesting and arrays of every length class")
|
||||
{
|
||||
json document = json::object();
|
||||
document["empty object"] = json::object();
|
||||
document["empty array"] = json::array();
|
||||
for (const std::size_t n :
|
||||
{
|
||||
1u, 9u, 10u, 11u, 99u, 100u, 101u, 1000u
|
||||
})
|
||||
{
|
||||
json numbers = json::array();
|
||||
json mixed = json::array();
|
||||
for (std::size_t i = 0; i < n; ++i)
|
||||
{
|
||||
numbers.push_back(i % 3 == 0 ? json(static_cast<double>(i) / 3) : json(static_cast<std::int64_t>(i) * 1000003));
|
||||
switch (i % 4)
|
||||
{
|
||||
case 0:
|
||||
mixed.push_back(json("s" + std::to_string(i)));
|
||||
break;
|
||||
case 1:
|
||||
mixed.push_back(json::array({i, json::object({{"k", i}})}));
|
||||
break;
|
||||
case 2:
|
||||
mixed.push_back(json(nullptr));
|
||||
break;
|
||||
default:
|
||||
mixed.push_back(json(i % 2 == 0));
|
||||
break;
|
||||
}
|
||||
}
|
||||
document["numbers " + std::to_string(n)] = numbers;
|
||||
document["mixed " + std::to_string(n)] = mixed;
|
||||
document["nested " + std::to_string(n)] = json::object({{"a", numbers}, {"b", json::array({mixed, json::object()})}});
|
||||
}
|
||||
document["binary"] = json::binary({1, 2, 3}, 0x80);
|
||||
document["binary old"] = json::binary({1, 2, 3}, 0x02);
|
||||
for (const auto handler : handlers)
|
||||
{
|
||||
check_same(document, handler);
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// every entry here throws, which aborts without exceptions
|
||||
SECTION("errors at every position")
|
||||
{
|
||||
// an entry that cannot be written, at each place among valid ones
|
||||
const std::string bad_utf8 = "\xC3\x28";
|
||||
const std::array<json, 7> bad_entries =
|
||||
{
|
||||
{
|
||||
json::object({{std::string("a\0b", 3), 1}}),
|
||||
json::object({{"s", bad_utf8}}),
|
||||
json::object({{bad_utf8, 1}}),
|
||||
json::array({1, 2, bad_utf8}),
|
||||
json::object({{"b", json::binary({1}, 0x100)}}),
|
||||
json::object({{"d", json(json::value_t::discarded)}}),
|
||||
json::array({json(json::value_t::discarded)}),
|
||||
}
|
||||
};
|
||||
for (const auto& bad : bad_entries)
|
||||
{
|
||||
for (std::size_t pos = 0; pos < 4; ++pos)
|
||||
{
|
||||
json document = json::object();
|
||||
json list = json::array();
|
||||
for (std::size_t i = 0; i < 4; ++i)
|
||||
{
|
||||
if (i == pos)
|
||||
{
|
||||
document["x" + std::to_string(i)] = bad;
|
||||
list.push_back(bad);
|
||||
}
|
||||
document["n" + std::to_string(i)] = json::array({i, "str", json::object({{"k", i}})});
|
||||
list.push_back(i);
|
||||
}
|
||||
document["list"] = list;
|
||||
for (const auto handler : handlers)
|
||||
{
|
||||
check_same(document, handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -29,7 +29,9 @@ using nlohmann::json;
|
||||
#include "round_trip_corpus.hpp"
|
||||
#include "test_utils.hpp"
|
||||
#include "sax_countdown.hpp"
|
||||
#include "sax_event_loggers.hpp"
|
||||
using utils::SaxCountdown;
|
||||
using utils::SaxEventLogger;
|
||||
|
||||
|
||||
TEST_CASE("CBOR")
|
||||
@@ -1807,6 +1809,30 @@ TEST_CASE("CBOR")
|
||||
CHECK(json::from_cbor(std::vector<uint8_t>({0xa1, 0xff, 0x01}), true, false).is_discarded());
|
||||
}
|
||||
|
||||
SECTION("tag in front of a key that is cut off by the end of the input")
|
||||
{
|
||||
// 0xD8: the tag number is in the next byte, which is missing
|
||||
const std::vector<std::uint8_t> input = {0xA1, 0xD8};
|
||||
json _;
|
||||
for (const auto handler :
|
||||
{
|
||||
json::cbor_tag_handler_t::ignore, json::cbor_tag_handler_t::store
|
||||
})
|
||||
{
|
||||
CHECK_THROWS_WITH_AS(_ = json::from_cbor(input, true, true, handler), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing CBOR number: unexpected end of input", json::parse_error&);
|
||||
CHECK(json::from_cbor(input, true, false, handler).is_discarded());
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("chunk of an indefinite-length string that is cut off by the end of the input")
|
||||
{
|
||||
// the second byte announces a chunk of three bytes, but two follow
|
||||
const std::vector<std::uint8_t> input = {0x7F, 0x63, 'a', 'b'};
|
||||
json _;
|
||||
CHECK_THROWS_WITH_AS(_ = json::from_cbor(input), "[json.exception.parse_error.110] parse error at byte 5: syntax error while parsing CBOR string: unexpected end of input", json::parse_error&);
|
||||
CHECK(json::from_cbor(input, true, false).is_discarded());
|
||||
}
|
||||
|
||||
SECTION("tag in front of a key")
|
||||
{
|
||||
const auto ignore = json::cbor_tag_handler_t::ignore;
|
||||
@@ -2446,6 +2472,78 @@ TEST_CASE("CBOR SAX parsing stops at every event")
|
||||
CHECK(count_events(std::vector<std::uint8_t>({0xBF, 0x61, 'a', 0x9F, 0x01, 0xFF, 0xFF})) == 6);
|
||||
}
|
||||
|
||||
TEST_CASE("CBOR arrays of scalars from contiguous and non-contiguous input")
|
||||
{
|
||||
// contiguous input reads the integers, floats, booleans, and nulls an
|
||||
// array begins with in one loop, other input value by value; both must
|
||||
// give the same events, value, or error for the complete document and for
|
||||
// every truncation of it, and stop at every event a SAX handler rejects
|
||||
const std::vector<std::uint8_t> cbor =
|
||||
{
|
||||
0xA3, // map(3)
|
||||
0x61, 'a', // "a"
|
||||
0x98, 0x16, // array(22)
|
||||
0x00, 0x17, 0x18, 0xFF, 0x19, 0x01, 0x00, // 0, 23, 255, 256
|
||||
0x1A, 0x00, 0x01, 0x00, 0x00, // 65536
|
||||
0x1B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 18446744073709551615
|
||||
0x20, 0x37, 0x38, 0xFF, 0x39, 0x01, 0x00, // -1, -24, -256, -257
|
||||
0x3A, 0x00, 0x01, 0x00, 0x00, // -65537
|
||||
0x3B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // -18446744073709551616 (a float)
|
||||
0x3B, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // -9223372036854775808
|
||||
0xF4, 0xF5, 0xF6, // false, true, null
|
||||
0xFA, 0x3F, 0xC0, 0x00, 0x00, // 1.5 (float32)
|
||||
0xFB, 0xBF, 0xB9, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9A, // -0.1
|
||||
0xF9, 0x3C, 0x00, // 1.0 (float16, ends the loop)
|
||||
0x01, 0x61, 'a', // 1, "a"
|
||||
0xFB, 0x40, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 2.5
|
||||
0x61, 'b', // "b"
|
||||
0x83, // array(3)
|
||||
0x82, 0xFB, 0x3F, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // [1.25,
|
||||
0xFB, 0xC0, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // -3.75]
|
||||
0x82, 0xFB, 0x3F, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // [1.25,
|
||||
0xFB, 0xC0, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // -3.75]
|
||||
0x80, // []
|
||||
0x61, 'c', // "c"
|
||||
0x84, 0xA0, 0x01, 0xC1, 0x02, 0x81, 0x03 // [{}, 1, tag(1) 2, [3]]
|
||||
};
|
||||
const auto tags = json::cbor_tag_handler_t::ignore;
|
||||
|
||||
const json j = json::from_cbor(cbor, true, true, tags);
|
||||
CHECK(j["a"].size() == 22);
|
||||
CHECK(j["a"][5] == 18446744073709551615u);
|
||||
CHECK(j["a"][11].is_number_float());
|
||||
CHECK(j["a"][11] == -18446744073709551616.0);
|
||||
CHECK(j["a"][12] == (std::numeric_limits<std::int64_t>::min)());
|
||||
CHECK(j["a"][17] == -0.1);
|
||||
CHECK(j["b"] == json::parse("[[1.25, -3.75], [1.25, -3.75], []]"));
|
||||
CHECK(j["c"] == json::parse("[{}, 1, 2, [3]]"));
|
||||
|
||||
for (std::size_t length = 0; length <= cbor.size(); ++length)
|
||||
{
|
||||
CAPTURE(length)
|
||||
const std::vector<std::uint8_t> contiguous(cbor.begin(), cbor.begin() + static_cast<std::ptrdiff_t>(length));
|
||||
const std::list<std::uint8_t> bytewise(contiguous.begin(), contiguous.end());
|
||||
|
||||
CHECK(json::from_cbor(contiguous, true, false, tags).dump() == json::from_cbor(bytewise.begin(), bytewise.end(), true, false, tags).dump());
|
||||
|
||||
SaxEventLogger from_contiguous;
|
||||
SaxEventLogger from_bytewise;
|
||||
CHECK(json::sax_parse(contiguous, &from_contiguous, json::input_format_t::cbor, true, false, false, tags)
|
||||
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::cbor, true, false, false, tags));
|
||||
CHECK(from_contiguous.events == from_bytewise.events);
|
||||
}
|
||||
|
||||
for (int events = 0; events <= 60; ++events)
|
||||
{
|
||||
CAPTURE(events)
|
||||
const std::list<std::uint8_t> bytewise(cbor.begin(), cbor.end());
|
||||
SaxCountdown from_contiguous(events);
|
||||
SaxCountdown from_bytewise(events);
|
||||
CHECK(json::sax_parse(cbor, &from_contiguous, json::input_format_t::cbor, true, false, false, tags)
|
||||
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::cbor, true, false, false, tags));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("CBOR indefinite-length strings do not recurse per chunk")
|
||||
{
|
||||
// Reading an indefinite-length string or byte array used to call itself
|
||||
|
||||
@@ -2092,6 +2092,12 @@ TEST_CASE("UTF-8 validators accept the same sequences")
|
||||
}
|
||||
};
|
||||
|
||||
// an ASCII byte is a sequence of its own
|
||||
for (unsigned lead = 0; lead < 0x80; ++lead)
|
||||
{
|
||||
CHECK(nlohmann::detail::utf8_shape(static_cast<std::uint8_t>(lead)).length == 1);
|
||||
}
|
||||
|
||||
for (unsigned lead = 0x80; lead <= 0xFF; ++lead)
|
||||
{
|
||||
const auto shape = nlohmann::detail::utf8_shape(static_cast<std::uint8_t>(lead));
|
||||
@@ -2157,3 +2163,72 @@ TEST_CASE("UTF-8 validators accept the same sequences")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("float_bigint")
|
||||
{
|
||||
// the big integer of the exact comparison of a float token with the
|
||||
// midpoint between two floats (digit_comparison())
|
||||
using nlohmann::detail::float_bigint;
|
||||
|
||||
SECTION("zero")
|
||||
{
|
||||
// shifting zero keeps it zero, whatever the distance
|
||||
float_bigint zero(0);
|
||||
zero.shift_left(0);
|
||||
zero.shift_left(5);
|
||||
zero.shift_left(64);
|
||||
CHECK(zero.compare(float_bigint(0)) == 0);
|
||||
CHECK(zero.compare(float_bigint(1)) == -1);
|
||||
CHECK(float_bigint(1).compare(zero) == 1);
|
||||
}
|
||||
|
||||
SECTION("compare numbers of different lengths")
|
||||
{
|
||||
// the number with more 32-bit limbs is the greater one
|
||||
const float_bigint small(0xFFFFFFFFu);
|
||||
const float_bigint large(std::uint64_t{1} << 32u);
|
||||
CHECK(small.compare(large) == -1);
|
||||
CHECK(large.compare(small) == 1);
|
||||
CHECK(large.compare(large) == 0);
|
||||
|
||||
// a shift can add a limb
|
||||
float_bigint shifted(1);
|
||||
shifted.shift_left(32);
|
||||
CHECK(shifted.compare(large) == 0);
|
||||
shifted.shift_left(1);
|
||||
CHECK(shifted.compare(large) == 1);
|
||||
CHECK(large.compare(shifted) == -1);
|
||||
}
|
||||
|
||||
SECTION("compare numbers of the same length")
|
||||
{
|
||||
CHECK(float_bigint(2).compare(float_bigint(3)) == -1);
|
||||
CHECK(float_bigint(3).compare(float_bigint(2)) == 1);
|
||||
CHECK(float_bigint(3).compare(float_bigint(3)) == 0);
|
||||
CHECK(float_bigint((std::uint64_t{3} << 32u) | 1u).compare(float_bigint((std::uint64_t{3} << 32u) | 2u)) == -1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("serialization of short strings")
|
||||
{
|
||||
// the serializer takes strings of up to 32 characters that need no
|
||||
// escaping by a table lookup of its own where the scan is not vectorized
|
||||
// (this file is built that way, too, with JSON_NO_SIMD)
|
||||
for (const std::string& s :
|
||||
{
|
||||
std::string(), std::string("a"), std::string("hello"), std::string("0123456789abcdef"), std::string("0123456789abcdef0123456789abcdef"),
|
||||
std::string("0123456789abcdef0123456789abcdef0")
|
||||
})
|
||||
{
|
||||
CAPTURE(s)
|
||||
const json j = s;
|
||||
CHECK(j.dump() == "\"" + s + "\"");
|
||||
CHECK(j.dump(-1, ' ', true) == "\"" + s + "\"");
|
||||
CHECK(json({{s, s}}).dump() == "{\"" + s + "\":\"" + s + "\"}");
|
||||
}
|
||||
|
||||
// a character that needs an escape
|
||||
CHECK(json("a\"b").dump() == "\"a\\\"b\"");
|
||||
CHECK(json("caf\xC3\xA9").dump() == "\"caf\xC3\xA9\"");
|
||||
CHECK(json("caf\xC3\xA9").dump(-1, ' ', true) == "\"caf\\u00e9\"");
|
||||
}
|
||||
@@ -2982,6 +2982,67 @@ TEST_CASE("accept() agrees with parse() on floats around the overflow limit")
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("accept() agrees with parse() on strings, which it checks without copying them")
|
||||
{
|
||||
// accept() scans strings and object keys with a separate instantiation of
|
||||
// the string scanner that does not copy them into the token buffer; it
|
||||
// must accept and reject exactly what parse() does
|
||||
const std::vector<std::string> pieces =
|
||||
{
|
||||
"", "abc", R"(\"\\\/\b\f\n\r\t)", R"(\u0041\u00e4\u20AC)", "\\ud83d\\ude00", "\\uD83D\\uDE00",
|
||||
"\\ud800", "\\udc00", "\\ud800\\u0041", "\\ud800x", "\\udbff\\udfff", "\\u12", "\\u12g4", "\\x", "\\",
|
||||
"\xc3\xa4", "\xe2\x82\xac", "\xf0\x9f\x98\x80", "\xf4\x8f\xbf\xbf", "\xc0\xaf", "\xed\xa0\x80", "\xf4\x90\x80\x80",
|
||||
"\xff", "\xe2\x82", "\xc3", "\x01", "\x1f", "\x7f", "\n", std::string(1, '\0')
|
||||
};
|
||||
|
||||
for (std::size_t run = 0; run <= 70; ++run)
|
||||
{
|
||||
for (const std::string& piece : pieces)
|
||||
{
|
||||
// a run of ordinary characters puts the piece at every offset of the bulk scanners
|
||||
const std::string str = std::string(run, 'a') + piece;
|
||||
std::string opened = "\"";
|
||||
opened += str;
|
||||
std::string closed = opened;
|
||||
closed += '"';
|
||||
std::vector<std::string> texts;
|
||||
texts.push_back(closed);
|
||||
texts.emplace_back("[");
|
||||
texts.back() += closed;
|
||||
texts.back() += ']';
|
||||
texts.emplace_back("{");
|
||||
texts.back() += closed;
|
||||
texts.back() += ":1}";
|
||||
texts.emplace_back("{\"k\":");
|
||||
texts.back() += closed;
|
||||
texts.back() += ',';
|
||||
texts.back() += closed;
|
||||
texts.back() += ":[";
|
||||
texts.back() += closed;
|
||||
texts.back() += "]}";
|
||||
texts.emplace_back("[");
|
||||
texts.back() += opened;
|
||||
for (const std::string& text : texts)
|
||||
{
|
||||
CAPTURE(text)
|
||||
for (int options = 0; options < 4; ++options)
|
||||
{
|
||||
const bool ignore_comments = (options & 1) != 0;
|
||||
const bool ignore_trailing_commas = (options & 2) != 0;
|
||||
const bool parses = !json::parse(text, nullptr, false, ignore_comments, ignore_trailing_commas).is_discarded();
|
||||
CHECK(json::accept(text, ignore_comments, ignore_trailing_commas) == parses);
|
||||
|
||||
std::istringstream stream(text);
|
||||
CHECK(json::accept(stream, ignore_comments, ignore_trailing_commas) == parses);
|
||||
|
||||
const std::list<char> chars(text.begin(), text.end());
|
||||
CHECK(json::accept(chars.begin(), chars.end(), ignore_comments, ignore_trailing_commas) == parses);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// a DOM parser that reports parse errors without throwing, as in the sax_no_exception example
|
||||
|
||||
@@ -210,3 +210,23 @@ TEST_CASE("regression test #5476 - array type without reserve()")
|
||||
CHECK(json::from_cbor(truncated, true, false).is_discarded());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("array type with std::deque nested beyond the recursion limit")
|
||||
{
|
||||
// the binary writers finish a value nested deeper than
|
||||
// recursion_depth_limit() with an explicit stack of frames; with the
|
||||
// iterators of std::deque, these frames are copied and moved through
|
||||
// their own special members
|
||||
deque_json j = 1;
|
||||
for (int i = 0; i < 200; ++i)
|
||||
{
|
||||
deque_json outer = deque_json::array();
|
||||
outer.push_back(std::move(j));
|
||||
j = std::move(outer);
|
||||
}
|
||||
|
||||
// 200 arrays of one element each (0x81) around the integer 1
|
||||
std::vector<std::uint8_t> expected(200, 0x81);
|
||||
expected.push_back(0x01);
|
||||
CHECK(deque_json::to_cbor(j) == expected);
|
||||
}
|
||||
@@ -49,3 +49,24 @@ TEST_CASE("custom object key types: MessagePack (json_full)")
|
||||
{
|
||||
custom_key_test::test_msgpack<json_full>();
|
||||
}
|
||||
|
||||
TEST_CASE("custom object key types: patch (json_full)")
|
||||
{
|
||||
// the members of a patch are found by lookup, as their keys are not std::string
|
||||
const json_full j = json_full::parse(R"({"a":1,"b":[1,2],"c":{"d":2}})");
|
||||
const json_full patch = json_full::parse(R"([
|
||||
{"op": "add", "path": "/e", "value": 5},
|
||||
{"op": "remove", "path": "/a"},
|
||||
{"op": "replace", "path": "/c/d", "value": 3},
|
||||
{"op": "test", "path": "/c/d", "value": 3},
|
||||
{"op": "move", "from": "/e", "path": "/f"},
|
||||
{"op": "copy", "from": "/f", "path": "/g"},
|
||||
{"op": "add", "path": "/b/-", "value": 7}
|
||||
])");
|
||||
|
||||
CHECK(j.patch(patch).dump() == R"({"b":[1,2,7],"c":{"d":3},"f":5,"g":5})");
|
||||
|
||||
// a patch with a missing member is rejected
|
||||
CHECK_THROWS_AS(j.patch(json_full::parse(R"([{"op": "add", "path": "/e"}])")), json_full::parse_error&);
|
||||
CHECK_THROWS_AS(j.patch(json_full::parse(R"([{"op": "test", "path": "/a", "value": 2}])")), json_full::other_error&);
|
||||
}
|
||||
@@ -337,6 +337,52 @@ TEST_CASE("custom number types: integers beyond 64 bits")
|
||||
CHECK(wide_json::to_bson(fit_members) == nlohmann::json::to_bson(fit_members64));
|
||||
}
|
||||
|
||||
SECTION("MessagePack integers of every width")
|
||||
{
|
||||
// an integer of a number type wider than 64 bits is written with the
|
||||
// smallest MessagePack integer that holds it, as for 64-bit types
|
||||
const wide_json widths = wide_json::array(
|
||||
{
|
||||
make_int(-33), make_int(-100), make_int(-128), make_int(-129), make_int(-32768), make_int(-32769), make_int(-2147483648LL), make_int(-2147483649LL),
|
||||
make_int(128), make_int(255), make_int(256), make_int(300), make_int(65535), make_int(65536), make_int(70000), make_int(4294967295LL), make_int(4294967296LL),
|
||||
make_uint(128), make_uint(255), make_uint(256), make_uint(300), make_uint(65535), make_uint(65536), make_uint(70000), make_uint(4294967295ULL), make_uint(4294967296ULL)
|
||||
});
|
||||
const nlohmann::json widths64 = nlohmann::json::array(
|
||||
{
|
||||
-33, -100, -128, -129, -32768, -32769, -2147483648LL, -2147483649LL,
|
||||
128, 255, 256, 300, 65535, 65536, 70000, 4294967295LL, 4294967296LL,
|
||||
128u, 255u, 256u, 300u, 65535u, 65536u, 70000u, 4294967295ULL, 4294967296ULL
|
||||
});
|
||||
CHECK(wide_json::to_msgpack(widths) == nlohmann::json::to_msgpack(widths64));
|
||||
|
||||
// as single values, too
|
||||
CHECK(wide_json::to_msgpack(make_int(-100)) == std::vector<std::uint8_t>({0xD0, 0x9C}));
|
||||
CHECK(wide_json::to_msgpack(make_int(300)) == std::vector<std::uint8_t>({0xCD, 0x01, 0x2C}));
|
||||
CHECK(wide_json::to_msgpack(make_int(70000)) == std::vector<std::uint8_t>({0xCE, 0x00, 0x01, 0x11, 0x70}));
|
||||
CHECK(wide_json::to_msgpack(make_uint(300)) == std::vector<std::uint8_t>({0xCD, 0x01, 0x2C}));
|
||||
CHECK(wide_json::to_msgpack(make_uint(70000)) == std::vector<std::uint8_t>({0xCE, 0x00, 0x01, 0x11, 0x70}));
|
||||
}
|
||||
|
||||
SECTION("dump")
|
||||
{
|
||||
// beyond 64 bits, the digits are counted before they are written
|
||||
CHECK(make_int(0).dump() == "0");
|
||||
CHECK(make_int(two_64).dump() == "18446744073709551616");
|
||||
CHECK(make_int(-two_64).dump() == "-18446744073709551616");
|
||||
CHECK(make_int(two_100).dump() == "1267650600228229401496703205376");
|
||||
CHECK(make_int(-two_100).dump() == "-1267650600228229401496703205376");
|
||||
// powers of ten, where the count of digits ends on a single digit or two
|
||||
const int128 ten_20 = static_cast<int128>(100000000000ULL) * static_cast<int128>(1000000000ULL);
|
||||
CHECK(make_int(ten_20).dump() == "100000000000000000000");
|
||||
CHECK(make_int(-ten_20).dump() == "-100000000000000000000");
|
||||
CHECK(make_int(ten_20 * 10).dump() == "1000000000000000000000");
|
||||
CHECK(make_int(ten_20 * 100).dump() == "10000000000000000000000");
|
||||
CHECK(make_int(ten_20 * 1000).dump() == "100000000000000000000000");
|
||||
CHECK(make_uint(static_cast<uint128>(ten_20) * 10u).dump() == "1000000000000000000000");
|
||||
CHECK(make_uint((std::numeric_limits<uint128>::max)()).dump() == "340282366920938463463374607431768211455");
|
||||
CHECK(make_int((std::numeric_limits<int128>::min)()).dump() == "-170141183460469231731687303715884105728");
|
||||
}
|
||||
|
||||
SECTION("BJData ND-array")
|
||||
{
|
||||
// an ND-array element beyond 64 bits does not fit any dtype, so the
|
||||
@@ -417,3 +463,23 @@ TEST_CASE("custom number types")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TEST_CASE("custom number types: 32-bit integers in the binary formats")
|
||||
{
|
||||
// integers narrower than 64 bits are widened (a signed one with its sign)
|
||||
// before the CBOR, MessagePack, and UBJSON writers pick their encoding
|
||||
using json32 = nlohmann::json::with_integers_t<std::int32_t, std::uint32_t>;
|
||||
json32 values = json32::array();
|
||||
values.push_back(std::uint32_t{0});
|
||||
values.push_back(std::uint32_t{200});
|
||||
values.push_back(std::uint32_t{4294967295u});
|
||||
values.push_back(std::int32_t{-1});
|
||||
values.push_back(std::int32_t{-2147483647 - 1});
|
||||
REQUIRE(values[2].is_number_unsigned());
|
||||
REQUIRE(values[4].is_number_integer());
|
||||
|
||||
const nlohmann::json values64 = {0u, 200u, 4294967295u, -1, -2147483648LL};
|
||||
CHECK(json32::to_cbor(values) == nlohmann::json::to_cbor(values64));
|
||||
CHECK(json32::to_msgpack(values) == nlohmann::json::to_msgpack(values64));
|
||||
CHECK(json32::to_ubjson(values) == nlohmann::json::to_ubjson(values64));
|
||||
}
|
||||
@@ -11,8 +11,13 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "custom_object_key_type.hpp"
|
||||
|
||||
@@ -125,3 +130,161 @@ TEST_CASE("MessagePack supports custom object key types nested deeper than the r
|
||||
CHECK(encoded == nlohmann::json::to_msgpack(expected));
|
||||
CHECK(nlohmann::json::from_msgpack(encoded) == expected);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// A key type that cannot be constructed from a std::string, so that a value
|
||||
// with such keys cannot be converted from a nlohmann::json by the range
|
||||
// constructor of its object type: the serializer converts the object instead.
|
||||
class strict_key
|
||||
{
|
||||
public:
|
||||
strict_key(const char* value)
|
||||
: m_value(value)
|
||||
{}
|
||||
|
||||
strict_key(const std::string& /*value*/) = delete;
|
||||
|
||||
#if JSON_DIAGNOSTICS
|
||||
// Required by JSON_DIAGNOSTICS, which reads object keys through data()
|
||||
// when building the path of an exception.
|
||||
const char* data() const noexcept
|
||||
{
|
||||
return m_value.data();
|
||||
}
|
||||
#endif
|
||||
|
||||
friend bool operator<(const strict_key& lhs, const strict_key& rhs)
|
||||
{
|
||||
return lhs.m_value < rhs.m_value;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string m_value{};
|
||||
};
|
||||
|
||||
strict_key make_strict_key(const std::string& key)
|
||||
{
|
||||
return strict_key(key.c_str());
|
||||
}
|
||||
|
||||
template<class Key, class Value, class Compare, class Allocator>
|
||||
class strict_object
|
||||
: public std::map <
|
||||
strict_key,
|
||||
Value,
|
||||
std::less<strict_key>, // NOLINT(modernize-use-transparent-functors)
|
||||
typename std::allocator_traits<Allocator>::template rebind_alloc <
|
||||
std::pair<const strict_key, Value >>>
|
||||
{
|
||||
public:
|
||||
strict_object() = default;
|
||||
};
|
||||
|
||||
template<typename T, typename = void>
|
||||
struct strict_serializer : nlohmann::adl_serializer<T> {};
|
||||
|
||||
using strict_json = nlohmann::basic_json < strict_object, std::vector, std::string, bool, std::int64_t,
|
||||
std::uint64_t, double, std::allocator, strict_serializer, std::vector<std::uint8_t >>;
|
||||
|
||||
// the members of an object are converted one by one, their keys by make_strict_key()
|
||||
template<>
|
||||
struct strict_serializer<nlohmann::json::object_t, void>
|
||||
{
|
||||
static void to_json(strict_json& j, const nlohmann::json::object_t& object)
|
||||
{
|
||||
strict_json::object_t converted;
|
||||
for (const auto& member : object)
|
||||
{
|
||||
converted.emplace(make_strict_key(member.first), strict_json(member.second));
|
||||
}
|
||||
j = strict_json(std::move(converted));
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("converting a value with keys that cannot be made from a std::string nested deeper than the recursion depth limit")
|
||||
{
|
||||
static_assert(!std::is_constructible<strict_key, const std::string&>::value, "the keys cannot be made from a std::string");
|
||||
|
||||
// below detail::recursion_depth_limit(), objects and arrays are converted
|
||||
// by the serializers instead of the range constructors
|
||||
const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 10;
|
||||
|
||||
nlohmann::json value = 1;
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
if (i % 2 == 0)
|
||||
{
|
||||
value = nlohmann::json::object({{"a", std::move(value)}, {"b", true}});
|
||||
}
|
||||
else
|
||||
{
|
||||
value = nlohmann::json::array({std::move(value), "sibling"});
|
||||
}
|
||||
}
|
||||
|
||||
const strict_json converted = value;
|
||||
|
||||
// at() needs to make a std::string of a key for its error message
|
||||
const auto member = [](const strict_json & object, const char* name) -> const strict_json &
|
||||
{
|
||||
const auto it = object.find(strict_key(name));
|
||||
REQUIRE(it != object.end());
|
||||
return *it;
|
||||
};
|
||||
|
||||
// walk down the nesting levels without recursion
|
||||
const strict_json* current = &converted;
|
||||
for (std::size_t i = depth; i > 0; --i)
|
||||
{
|
||||
if (i % 2 == 1)
|
||||
{
|
||||
REQUIRE(current->is_object());
|
||||
CHECK(current->size() == 2);
|
||||
CHECK(member(*current, "b").get<bool>());
|
||||
current = &member(*current, "a");
|
||||
}
|
||||
else
|
||||
{
|
||||
REQUIRE(current->is_array());
|
||||
CHECK(current->size() == 2);
|
||||
CHECK(current->at(1).get<std::string>() == "sibling");
|
||||
current = ¤t->at(0);
|
||||
}
|
||||
}
|
||||
CHECK(current->get<int>() == 1);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// shorter keys come first
|
||||
struct shortlex_less
|
||||
{
|
||||
bool operator()(const std::string& lhs, const std::string& rhs) const
|
||||
{
|
||||
return lhs.size() != rhs.size() ? lhs.size() < rhs.size() : lhs < rhs;
|
||||
}
|
||||
};
|
||||
|
||||
template<class Key, class Value, class Compare, class Allocator>
|
||||
using shortlex_map = std::map<Key, Value, shortlex_less, Allocator>;
|
||||
|
||||
using shortlex_json = nlohmann::json::with_object_t<shortlex_map>;
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("unflatten with an object type that does not sort the keys alphabetically")
|
||||
{
|
||||
// the keys of /a come before and after one of /b: unlike in an object
|
||||
// sorted alphabetically, the values below a prefix are not found one
|
||||
// after the other, so the value of /a must be looked up again
|
||||
shortlex_json flat = shortlex_json::object();
|
||||
flat["/a/p"] = 1;
|
||||
flat["/b/qq"] = 2;
|
||||
flat["/a/pqr"] = 3;
|
||||
REQUIRE(flat.begin().key() == "/a/p");
|
||||
REQUIRE((++flat.begin()).key() == "/b/qq");
|
||||
|
||||
CHECK(flat.unflatten().dump() == R"({"a":{"p":1,"pqr":3},"b":{"qq":2}})");
|
||||
CHECK(flat.unflatten().flatten() == flat);
|
||||
}
|
||||
@@ -1195,6 +1195,28 @@ TEST_CASE("deserialization")
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED
|
||||
TEST_CASE("deserialization from a pointer and a length (deprecated)")
|
||||
{
|
||||
// the unit test build disables -Wdeprecated-declarations (see
|
||||
// cmake/clang_flags.cmake)
|
||||
const std::string text = R"({"foo": [1, 2, 3]})";
|
||||
const std::string invalid = R"({"foo": )";
|
||||
|
||||
SECTION("parse")
|
||||
{
|
||||
const json j = json::parse({text.data(), text.size()});
|
||||
CHECK(j == json({{"foo", {1, 2, 3}}}));
|
||||
}
|
||||
|
||||
SECTION("accept")
|
||||
{
|
||||
CHECK(json::accept({text.data(), text.size()}));
|
||||
CHECK_FALSE(json::accept({invalid.data(), invalid.size()}));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// select the types to test - char8_t is only available since C++20 if and only
|
||||
// if __cpp_char8_t is defined.
|
||||
#define TYPE_LIST(...) __VA_ARGS__
|
||||
|
||||
@@ -868,3 +868,23 @@ TEST_CASE("Parent pointers stay consistent for object types not known to keep th
|
||||
obj["1"].push_back(42);
|
||||
CHECK_THROWS_WITH_AS(obj["1"].at(5), "[json.exception.out_of_range.401] (/1) array index 5 is out of range", relocating_json::out_of_range&);
|
||||
}
|
||||
|
||||
TEST_CASE("patch_inplace() with a patch that is stored inside the patched value")
|
||||
{
|
||||
// the patch is modified-while-iterated if it is not copied first: it is a
|
||||
// descendant of the value it is applied to, which only the parent pointers
|
||||
// (JSON_DIAGNOSTICS) reveal
|
||||
json j =
|
||||
{
|
||||
{"patch", {{{"op", "add"}, {"path", "/x"}, {"value", 1}}, {{"op", "remove"}, {"path", "/patch/0"}}}},
|
||||
{"y", 2}
|
||||
};
|
||||
|
||||
j.patch_inplace(j["patch"]);
|
||||
|
||||
CHECK(j.size() == 3);
|
||||
CHECK(j["x"] == 1);
|
||||
CHECK(j["y"] == 2);
|
||||
CHECK(j["patch"].size() == 1);
|
||||
CHECK(j["patch"][0]["op"] == "remove");
|
||||
}
|
||||
@@ -403,6 +403,7 @@ TEST_CASE("guard page")
|
||||
CHECK(nlohmann::detail::find_string_special(p, n) <= n);
|
||||
CHECK(nlohmann::detail::find_ascii_copyable_run(p, n) <= n);
|
||||
CHECK(nlohmann::detail::valid_utf8_prefix(p, n) <= n);
|
||||
CHECK(nlohmann::detail::valid_utf8_string(p, n) == (nlohmann::detail::valid_utf8_prefix(p, n) == n));
|
||||
#if defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17)
|
||||
CHECK(nlohmann::detail::find_string_delimiter(p, n) <= n);
|
||||
#endif
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
#include <iterator>
|
||||
|
||||
#if JSON_HAS_RANGES
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
@@ -665,6 +667,23 @@ TEST_CASE("iterators 2")
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("reverse iterators of a const value")
|
||||
{
|
||||
const json j_array = {1, 2, 3};
|
||||
const json j_object = {{"one", 1}, {"two", 2}};
|
||||
|
||||
// rbegin() and rend() of a const value are crbegin() and crend()
|
||||
CHECK(j_array.rbegin() == j_array.crbegin());
|
||||
CHECK(j_array.rend() == j_array.crend());
|
||||
CHECK(*j_array.rbegin() == 3);
|
||||
CHECK(std::distance(j_array.rbegin(), j_array.rend()) == 3);
|
||||
|
||||
CHECK(j_object.rbegin() == j_object.crbegin());
|
||||
CHECK(j_object.rend() == j_object.crend());
|
||||
CHECK(j_object.rbegin().key() == "two");
|
||||
CHECK(std::distance(j_object.rbegin(), j_object.rend()) == 2);
|
||||
}
|
||||
|
||||
SECTION("reverse iterator arithmetic")
|
||||
{
|
||||
json j_object = {{"one", 1}, {"two", 2}, {"three", 3}};
|
||||
|
||||
@@ -14,9 +14,11 @@ using nlohmann::json;
|
||||
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
|
||||
#endif
|
||||
|
||||
#include <cstddef>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include "make_test_data_available.hpp"
|
||||
|
||||
@@ -2453,3 +2455,99 @@ TEST_CASE("JSON patch - diff behavior documented in the API reference")
|
||||
CHECK(json::diff(json(nullptr), json::array()) == R"([{"op": "replace", "path": "", "value": []}])"_json);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("JSON patch - operations that are left to json_pointer")
|
||||
{
|
||||
// the usual operations on a plain pointer are applied without building a
|
||||
// json_pointer; the following are not plain, and some are errors
|
||||
SECTION("a path without a slash")
|
||||
{
|
||||
const json j = {{"a", 1}};
|
||||
|
||||
CHECK_THROWS_AS(j.patch(R"([{"op": "add", "path": "abc", "value": 1}])"_json), json::parse_error&);
|
||||
CHECK_THROWS_AS(j.patch(R"([{"op": "remove", "path": "abc"}])"_json), json::parse_error&);
|
||||
}
|
||||
|
||||
SECTION("remove with an escaped reference token")
|
||||
{
|
||||
const json j = {{"a/b", 1}, {"c~d", 2}, {"e", 3}};
|
||||
|
||||
CHECK(j.patch(R"([{"op": "remove", "path": "/a~1b"}])"_json) == json({{"c~d", 2}, {"e", 3}}));
|
||||
CHECK(j.patch(R"([{"op": "remove", "path": "/c~0d"}])"_json) == json({{"a/b", 1}, {"e", 3}}));
|
||||
CHECK_THROWS_AS(j.patch(R"([{"op": "remove", "path": "/missing~1key"}])"_json), json::out_of_range&);
|
||||
}
|
||||
|
||||
SECTION("a path through a primitive value")
|
||||
{
|
||||
const json j = {{"a", 1}};
|
||||
|
||||
CHECK_THROWS_AS(j.patch(R"([{"op": "test", "path": "/a/b", "value": 1}])"_json), json::other_error&);
|
||||
CHECK_THROWS_AS(j.patch(R"([{"op": "replace", "path": "/a/b", "value": 1}])"_json), json::out_of_range&);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("JSON patch - diff of objects with many members")
|
||||
{
|
||||
// more members than diff() keeps without allocating
|
||||
for (const std::size_t members :
|
||||
{
|
||||
16u, 17u, 40u
|
||||
})
|
||||
{
|
||||
CAPTURE(members)
|
||||
|
||||
json source = json::object();
|
||||
json target = json::object();
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
const std::string key = "key" + std::to_string(i);
|
||||
source[key] = i;
|
||||
// every third member differs
|
||||
target[key] = (i % 3 == 0) ? i + 1 : i;
|
||||
}
|
||||
// the same keys, a removed and an added one
|
||||
source["removed"] = true;
|
||||
target["added"] = false;
|
||||
|
||||
const json patch = json::diff(source, target);
|
||||
CHECK(source.patch(patch) == target);
|
||||
CHECK(patch.size() == ((members + 2) / 3) + 2);
|
||||
|
||||
const nlohmann::ordered_json ordered_source = source;
|
||||
const nlohmann::ordered_json ordered_target = target;
|
||||
CHECK(ordered_source.patch(nlohmann::ordered_json::diff(ordered_source, ordered_target)) == ordered_target);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("JSON patch - diff of leaf values below the bound of the recursive descent")
|
||||
{
|
||||
// the values at the innermost level are diffed without recursion: equal
|
||||
// leaves yield nothing, leaves of different types and different leaves
|
||||
// are replaced
|
||||
const std::size_t depth = 200;
|
||||
|
||||
json source = {1, "text", 3, {{"x", 1}, {"y", 2}}, true, nullptr};
|
||||
json target = {1, 2, 4, {{"x", 1}, {"y", 3}}, true, nullptr};
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
source = json::array({std::move(source)});
|
||||
target = json::array({std::move(target)});
|
||||
}
|
||||
|
||||
const json patch = json::diff(source, target);
|
||||
CHECK(patch.size() == 3);
|
||||
CHECK(source.patch(patch) == target);
|
||||
|
||||
// the same with objects as the outer levels
|
||||
json object_source = {{"equal", 1}, {"type", "text"}, {"value", 3}};
|
||||
json object_target = {{"equal", 1}, {"type", 2}, {"value", 4}};
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
object_source = json::object({{"a", std::move(object_source)}});
|
||||
object_target = json::object({{"a", std::move(object_target)}});
|
||||
}
|
||||
|
||||
const json object_patch = json::diff(object_source, object_target);
|
||||
CHECK(object_patch.size() == 2);
|
||||
CHECK(object_source.patch(object_patch) == object_target);
|
||||
}
|
||||
@@ -106,6 +106,12 @@ TEST_CASE("JSON pointers")
|
||||
CHECK_THROWS_WITH_AS(p.pop_back(),
|
||||
"[json.exception.out_of_range.405] JSON pointer has no parent", json::out_of_range&);
|
||||
|
||||
// the top of a pointer is its first reference token
|
||||
const json::json_pointer nonempty("/foo/bar/0");
|
||||
CHECK(nonempty.top() == json::json_pointer("/foo"));
|
||||
CHECK(nonempty.top().to_string() == "/foo");
|
||||
CHECK(json::json_pointer("/a~1b").top() == json::json_pointer("/a~1b"));
|
||||
|
||||
SECTION("array index error")
|
||||
{
|
||||
json v = {1, 2, 3, 4};
|
||||
@@ -697,6 +703,16 @@ TEST_CASE("JSON pointers")
|
||||
CHECK_THROWS_WITH_AS(j_error.unflatten(),
|
||||
"[json.exception.type_error.313] invalid value to unflatten", json::type_error&);
|
||||
|
||||
// error for an invalid escape sequence in a key
|
||||
CHECK_THROWS_WITH_AS(json({{"/a~2", 1}}).unflatten(),
|
||||
"[json.exception.parse_error.108] parse error: escape character '~' must be followed with '0' or '1'", json::parse_error&);
|
||||
CHECK_THROWS_WITH_AS(json({{"/a/b", 1}, {"/c~", 1}}).unflatten(),
|
||||
"[json.exception.parse_error.108] parse error: escape character '~' must be followed with '0' or '1'", json::parse_error&);
|
||||
|
||||
// error for an element of an array that is also a container
|
||||
CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/0/x", 2}}).unflatten(),
|
||||
"[json.exception.type_error.313] invalid value to unflatten", json::type_error&);
|
||||
|
||||
// explicit roundtrip check
|
||||
CHECK(j.flatten().unflatten() == j);
|
||||
|
||||
|
||||
@@ -31,9 +31,11 @@ using json_view_test::has_duplicate_keys;
|
||||
#include <ios>
|
||||
#include <istream>
|
||||
#include <iterator>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <new>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -98,6 +100,37 @@ template<typename View>
|
||||
using bool_conversion_t = decltype(static_cast<bool>(std::declval<View>()));
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// an allocator that never has room
|
||||
template<typename T>
|
||||
struct failing_allocator
|
||||
{
|
||||
using value_type = T;
|
||||
|
||||
failing_allocator() = default;
|
||||
|
||||
template<typename U>
|
||||
explicit failing_allocator(const failing_allocator<U>& /*other*/) noexcept {}
|
||||
|
||||
T* allocate(std::size_t /*n*/) // NOLINT(readability-convert-member-functions-to-static)
|
||||
{
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
|
||||
void deallocate(T* /*p*/, std::size_t /*n*/) noexcept {} // NOLINT(readability-convert-member-functions-to-static)
|
||||
|
||||
template<typename U>
|
||||
bool operator==(const failing_allocator<U>& /*other*/) const noexcept
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
template<typename U>
|
||||
bool operator!=(const failing_allocator<U>& /*other*/) const noexcept
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// the exception parse() throws for a text, or "" if it accepts it
|
||||
std::string parse_exception(const std::string& text, bool comments = false, bool trailing_commas = false)
|
||||
{
|
||||
@@ -225,7 +258,7 @@ TEST_CASE("json_view")
|
||||
#if JSON_DIAGNOSTICS
|
||||
// the parents are set, so errors name the path
|
||||
const json m = materialized<json_document>(R"({"a":{"b":[1]}})");
|
||||
CHECK_THROWS_WITH_AS(m.at("a").at("b").at(0).at("x"), "[json.exception.type_error.304] (/a/b/0) cannot use at() with number", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(m.at("a").at("b").at(0).at("x"), json_view_test::message("type_error.304", "/a/b/0", 11, 12, "cannot use at() with number").c_str(), json::type_error&);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -235,7 +268,9 @@ TEST_CASE("json_view")
|
||||
{
|
||||
"", " ", "[", "]", "{", "[1,]", "{\"a\":1,}", "[1 2]", "{\"a\" 1}", "{1:2}", "tru", "nul", "fals", "truex", "-", "01", "1.", ".5", "1e",
|
||||
"\"", "\"abc", "\"\\x\"", "\"\\u12\"", "\"\\ud800\"", "\"\\udc00\"", "\"\x01\"", "\"\xff\"", "\"\xc3\"", "[1]x", "/", "/*", "[\n 1,\n x\n]", // NOLINT(modernize-raw-string-literal)
|
||||
"1e400", "-1e400", "[1.7976931348623159e308]", "{\"a\":\n{\"b\": [1, 2,\n 3 x]}}"
|
||||
"1e400", "-1e400", "[1.7976931348623159e308]", "{\"a\":\n{\"b\": [1, 2,\n 3 x]}}",
|
||||
// (an error after every kind of value: the error is found by replaying the parser)
|
||||
"[null, true, -1, 1, 1.5, \"s\", {\"a\": 1} 2]", "{\"a\": null, \"b\": -1, \"c\": 1.5, \"d\": \"s\", \"e\": {}, 1}"
|
||||
})
|
||||
{
|
||||
CAPTURE(text)
|
||||
@@ -368,6 +403,10 @@ TEST_CASE("json_view")
|
||||
CHECK(from_stream.root().materialize() == expected);
|
||||
const std::list<char> list(text.begin(), text.end());
|
||||
CHECK(json_document::parse(list.begin(), list.end()).owns_source());
|
||||
// (an input that is not valid)
|
||||
const std::list<char> bad_list{'[', '1', ','};
|
||||
json_document from_bad_list;
|
||||
CHECK_THROWS_AS(from_bad_list = json_document::parse(bad_list.begin(), bad_list.end()), json::parse_error&);
|
||||
CHECK(materialized<json_document>(list.begin(), list.end()) == expected);
|
||||
|
||||
// iterator pairs: pointers are borrowed, and so are contiguous library
|
||||
@@ -750,19 +789,6 @@ std::string exception_of(F f)
|
||||
return "";
|
||||
}
|
||||
|
||||
// the exception a call on a basic_json value throws, without the byte range of
|
||||
// the value (JSON_DIAGNOSTIC_POSITIONS), which the exceptions of views lack
|
||||
template<typename F>
|
||||
std::string json_exception_of(F f)
|
||||
{
|
||||
std::string what = exception_of(f);
|
||||
const std::size_t at = what.find("] (bytes ");
|
||||
if (at != std::string::npos)
|
||||
{
|
||||
what.erase(at + 2, what.find(") ", at) - at);
|
||||
}
|
||||
return what;
|
||||
}
|
||||
#endif
|
||||
|
||||
// compares a view with the ordered_json value materialize() gives for it:
|
||||
@@ -971,24 +997,25 @@ TEST_CASE("json_view element access and iteration")
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
if (!j.is_object())
|
||||
{
|
||||
CHECK(exception_of([&] { static_cast<void>(v["a"]); }) == json_exception_of([&] { static_cast<void>(j["a"]); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v["a"]); }) == exception_of([&] { static_cast<void>(j["a"]); }));
|
||||
}
|
||||
if (!j.is_array())
|
||||
{
|
||||
CHECK(exception_of([&] { static_cast<void>(v[0]); }) == json_exception_of([&] { static_cast<void>(j[0]); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v[0]); }) == exception_of([&] { static_cast<void>(j[0]); }));
|
||||
}
|
||||
CHECK(exception_of([&] { static_cast<void>(v.at("a")); }) == json_exception_of([&] { static_cast<void>(j.at("a")); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.at("missing")); }) == json_exception_of([&] { static_cast<void>(j.at("missing")); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.at(0)); }) == json_exception_of([&] { static_cast<void>(j.at(0)); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.at(5)); }) == json_exception_of([&] { static_cast<void>(j.at(5)); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.at("a")); }) == exception_of([&] { static_cast<void>(j.at("a")); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.at("missing")); }) == exception_of([&] { static_cast<void>(j.at("missing")); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.at(0)); }) == exception_of([&] { static_cast<void>(j.at(0)); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.at(5)); }) == exception_of([&] { static_cast<void>(j.at(5)); }));
|
||||
if (!(j.is_object() && j.empty())) // (key() of an end iterator)
|
||||
{
|
||||
CHECK(exception_of([&] { static_cast<void>(v.begin().key()); }) == json_exception_of([&] { static_cast<void>(j.begin().key()); }));
|
||||
// (without the context of basic_json: view iterators do not know the value they iterate over)
|
||||
CHECK(exception_of([&] { static_cast<void>(v.begin().key()); }) == (j.is_object() ? "" : "[json.exception.invalid_iterator.207] cannot use key() for non-object iterators"));
|
||||
}
|
||||
if (!j.empty() || j.is_null())
|
||||
{
|
||||
CHECK(exception_of([&] { static_cast<void>(v.front()); }) == json_exception_of([&] { static_cast<void>(j.front()); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.back()); }) == json_exception_of([&] { static_cast<void>(j.back()); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.front()); }) == exception_of([&] { static_cast<void>(j.front()); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.back()); }) == exception_of([&] { static_cast<void>(j.back()); }));
|
||||
}
|
||||
#endif
|
||||
CHECK(v.contains("a") == j.contains("a"));
|
||||
@@ -1000,8 +1027,8 @@ TEST_CASE("json_view element access and iteration")
|
||||
const json_document d = json_document::parse(R"({"a":[]})");
|
||||
CHECK(d.root()["b"].is_discarded());
|
||||
CHECK(d.root()["a"][0].is_discarded());
|
||||
CHECK_THROWS_WITH_AS(d.root()["a"].front(), "[json.exception.invalid_iterator.214] cannot get value", json::invalid_iterator&);
|
||||
CHECK_THROWS_WITH_AS(d.root()["a"].back(), "[json.exception.invalid_iterator.214] cannot get value", json::invalid_iterator&);
|
||||
CHECK_THROWS_WITH_AS(d.root()["a"].front(), json_view_test::message("invalid_iterator.214", "/a", 5, 7, "cannot get value").c_str(), json::invalid_iterator&);
|
||||
CHECK_THROWS_WITH_AS(d.root()["a"].back(), json_view_test::message("invalid_iterator.214", "/a", 5, 7, "cannot get value").c_str(), json::invalid_iterator&);
|
||||
const json_view invalid{};
|
||||
CHECK(invalid.begin() == invalid.end());
|
||||
CHECK(std::string(invalid.type_name()) == "discarded");
|
||||
@@ -1032,10 +1059,10 @@ TEST_CASE("json_view element access and iteration")
|
||||
CHECK(v[json_view::json_pointer("/a/b/5")][0].is_discarded());
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// type errors on values that are not discarded stay
|
||||
CHECK_THROWS_WITH_AS(v[0], "[json.exception.type_error.305] cannot use operator[] with a numeric argument with object", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(v["a"]["b"]["c"], "[json.exception.type_error.305] cannot use operator[] with a string argument with array", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(v["s"]["c"], "[json.exception.type_error.305] cannot use operator[] with a string argument with string", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(v["s"][0], "[json.exception.type_error.305] cannot use operator[] with a numeric argument with string", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(v[0], json_view_test::message("type_error.305", "", 0, 29, "cannot use operator[] with a numeric argument with object").c_str(), json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(v["a"]["b"]["c"], json_view_test::message("type_error.305", "/a/b", 10, 17, "cannot use operator[] with a string argument with array").c_str(), json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(v["s"]["c"], json_view_test::message("type_error.305", "/s", 23, 28, "cannot use operator[] with a string argument with string").c_str(), json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(v["s"][0], json_view_test::message("type_error.305", "/s", 23, 28, "cannot use operator[] with a numeric argument with string").c_str(), json::type_error&);
|
||||
CHECK_THROWS_AS(v["a"]["b"][0]["c"], json::type_error&);
|
||||
CHECK_THROWS_AS(v["s"][json_view::json_pointer("/x")], json::out_of_range&);
|
||||
// at() keeps throwing on a discarded view
|
||||
@@ -1110,9 +1137,9 @@ TEST_CASE("json_view element access and iteration")
|
||||
CHECK(v[(std::numeric_limits<std::size_t>::max)()].is_discarded());
|
||||
CHECK(v[std::numeric_limits<int>::max()].is_discarded());
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
CHECK_THROWS_WITH_AS(v.at(3), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(v.at(3u), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(v.at(std::int64_t(3)), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(v.at(3), json_view_test::message("out_of_range.401", "", 0, 10, "array index 3 is out of range").c_str(), json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(v.at(3u), json_view_test::message("out_of_range.401", "", 0, 10, "array index 3 is out of range").c_str(), json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(v.at(std::int64_t(3)), json_view_test::message("out_of_range.401", "", 0, 10, "array index 3 is out of range").c_str(), json::out_of_range&);
|
||||
CHECK_THROWS_AS(v.at(-1), json::out_of_range&);
|
||||
CHECK_THROWS_AS(v.at(-1L), json::out_of_range&);
|
||||
CHECK_THROWS_AS(v.at(std::int64_t(-1)), json::out_of_range&);
|
||||
@@ -1246,26 +1273,6 @@ TEST_CASE("json_view element access and iteration")
|
||||
|
||||
namespace
|
||||
{
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// an exception message without the context that basic_json adds with
|
||||
// JSON_DIAGNOSTICS ("(/path) ") and JSON_DIAGNOSTIC_POSITIONS ("(bytes 1-2) ");
|
||||
// the view's exceptions have no such context
|
||||
std::string without_path(std::string msg)
|
||||
{
|
||||
for (const char* prefix :
|
||||
{"] (/", "] (bytes "
|
||||
})
|
||||
{
|
||||
const std::size_t open = msg.find(prefix);
|
||||
if (open != std::string::npos)
|
||||
{
|
||||
msg.erase(open + 2, msg.find(") ", open) + 2 - (open + 2));
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
#endif
|
||||
|
||||
// the bits of a float, to compare values bit for bit
|
||||
std::uint64_t bits(double x)
|
||||
{
|
||||
@@ -1336,18 +1343,18 @@ void check_values(const ordered_json_view& v, const ordered_json& j, const std::
|
||||
// conversions to the wrong type throw what basic_json throws
|
||||
if (!j.is_number())
|
||||
{
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<int>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<int>()); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<int>()); }) == exception_of([&] { static_cast<void>(j.get<int>()); }));
|
||||
}
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<bool>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<bool>()); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<std::string>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::string>()); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<std::nullptr_t>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::nullptr_t>()); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<bool>()); }) == exception_of([&] { static_cast<void>(j.get<bool>()); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<std::string>()); }) == exception_of([&] { static_cast<void>(j.get<std::string>()); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<std::nullptr_t>()); }) == exception_of([&] { static_cast<void>(j.get<std::nullptr_t>()); }));
|
||||
if (!j.is_array())
|
||||
{
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<std::vector<int>>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::vector<int>>()); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<std::vector<int>>()); }) == exception_of([&] { static_cast<void>(j.get<std::vector<int>>()); }));
|
||||
}
|
||||
if (!j.is_object())
|
||||
{
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<std::map<std::string, int>>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::map<std::string, int>>()); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.get<std::map<std::string, int>>()); }) == exception_of([&] { static_cast<void>(j.get<std::map<std::string, int>>()); }));
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1490,8 +1497,8 @@ TEST_CASE("json_view values")
|
||||
CHECK(v[3].number_token() == "123456789012345678901234567890");
|
||||
CHECK(v[4].number_token() == "-12");
|
||||
CHECK(v[5].number_token() == "7");
|
||||
CHECK_THROWS_WITH_AS(v[6].number_token(), "[json.exception.type_error.302] type must be number, but is string", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(v.get_string(), "[json.exception.type_error.302] type must be string, but is array", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(v[6].number_token(), json_view_test::message("type_error.302", "/6", 56, 59, "type must be number, but is string").c_str(), json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(v.get_string(), json_view_test::message("type_error.302", "", 0, 60, "type must be string, but is array").c_str(), json::type_error&);
|
||||
}
|
||||
|
||||
SECTION("conversions")
|
||||
@@ -1543,6 +1550,17 @@ TEST_CASE("json_view values")
|
||||
const json_document dup = json_document::parse(R"({"a":1,"a":2})");
|
||||
CHECK((dup.root().get<std::map<std::string, int>>() == std::map<std::string, int> {{"a", 2}}));
|
||||
|
||||
// a map of views from something that is not an object
|
||||
using view_map = std::map<std::string, json_view>;
|
||||
const json_document not_an_object = json_document::parse("[1,2]");
|
||||
CHECK_THROWS_WITH_AS(not_an_object.root().get<view_map>(), json_view_test::message("type_error.302", "", 0, 5, "type must be object, but is array").c_str(), json::type_error&);
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// (and one that cannot allocate its nodes)
|
||||
using failing_map = std::map<std::string, json_view, std::less<std::string>, failing_allocator<std::pair<const std::string, json_view>>>;
|
||||
const json_document one_member = json_document::parse(R"({"a":1})");
|
||||
CHECK_THROWS_AS(one_member.root().get<failing_map>(), std::bad_alloc&);
|
||||
#endif
|
||||
|
||||
const json_view invalid{};
|
||||
CHECK_THROWS_WITH_AS(invalid.get<int>(), "[json.exception.type_error.302] type must be number, but is discarded", json::type_error&);
|
||||
CHECK(invalid.get<json>().is_discarded());
|
||||
@@ -1565,15 +1583,15 @@ TEST_CASE("json_view values")
|
||||
CHECK(v["o"]["x"].value(json::json_pointer("/1"), 0) == j["o"]["x"].value(json::json_pointer("/1"), 0));
|
||||
CHECK(v["o"]["x"].value(json::json_pointer("/7"), 3) == j["o"]["x"].value(json::json_pointer("/7"), 3));
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
CHECK(exception_of([&] { static_cast<void>(v["o"]["x"].value("k", 0)); }) == without_path(exception_of([&] { static_cast<void>(j["o"]["x"].value("k", 0)); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.value("s", 0)); }) == without_path(exception_of([&] { static_cast<void>(j.value("s", 0)); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v["n"].value("x", 0)); }) == without_path(exception_of([&] { static_cast<void>(j["n"].value("x", 0)); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v["n"].value(json::json_pointer("/x"), 0)); }) == without_path(exception_of([&] { static_cast<void>(j["n"].value(json::json_pointer("/x"), 0)); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v["o"]["x"].value("k", 0)); }) == exception_of([&] { static_cast<void>(j["o"]["x"].value("k", 0)); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.value("s", 0)); }) == exception_of([&] { static_cast<void>(j.value("s", 0)); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v["n"].value("x", 0)); }) == exception_of([&] { static_cast<void>(j["n"].value("x", 0)); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v["n"].value(json::json_pointer("/x"), 0)); }) == exception_of([&] { static_cast<void>(j["n"].value(json::json_pointer("/x"), 0)); }));
|
||||
// string defaults: the member is not a string, or the value is neither an object nor an array
|
||||
CHECK(exception_of([&] { static_cast<void>(v.value("n", "default")); }) == without_path(exception_of([&] { static_cast<void>(j.value("n", "default")); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v["n"].value("x", "default")); }) == without_path(exception_of([&] { static_cast<void>(j["n"].value("x", "default")); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.value(json::json_pointer("/n"), "default")); }) == without_path(exception_of([&] { static_cast<void>(j.value(json::json_pointer("/n"), "default")); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v["s"].value(json::json_pointer("/x"), "default")); }) == without_path(exception_of([&] { static_cast<void>(j["s"].value(json::json_pointer("/x"), "default")); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.value("n", "default")); }) == exception_of([&] { static_cast<void>(j.value("n", "default")); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v["n"].value("x", "default")); }) == exception_of([&] { static_cast<void>(j["n"].value("x", "default")); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.value(json::json_pointer("/n"), "default")); }) == exception_of([&] { static_cast<void>(j.value(json::json_pointer("/n"), "default")); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v["s"].value(json::json_pointer("/x"), "default")); }) == exception_of([&] { static_cast<void>(j["s"].value(json::json_pointer("/x"), "default")); }));
|
||||
#endif
|
||||
// (defaults and members longer than the short string optimization)
|
||||
CHECK(v.value("s", "a default longer than fifteen characters") == "text");
|
||||
@@ -1632,7 +1650,7 @@ TEST_CASE("json_view JSON pointers")
|
||||
{
|
||||
CAPTURE(pointer)
|
||||
const json::json_pointer p(pointer);
|
||||
const std::string at_error = without_path(exception_of([&] { static_cast<void>(j.at(p)); }));
|
||||
const std::string at_error = exception_of([&] { static_cast<void>(j.at(p)); });
|
||||
CHECK(exception_of([&] { static_cast<void>(v.at(p)); }) == at_error);
|
||||
if (at_error.empty())
|
||||
{
|
||||
@@ -1646,7 +1664,7 @@ TEST_CASE("json_view JSON pointers")
|
||||
}
|
||||
else
|
||||
{
|
||||
CHECK(exception_of([&] { static_cast<void>(v[p]); }) == without_path(exception_of([&] { static_cast<void>(j[p]); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v[p]); }) == exception_of([&] { static_cast<void>(j[p]); }));
|
||||
}
|
||||
// (basic_json::contains() throws out_of_range.404 for an empty
|
||||
// array index token, although it is not meant to throw; the view
|
||||
@@ -1657,7 +1675,7 @@ TEST_CASE("json_view JSON pointers")
|
||||
static_cast<void>(found);
|
||||
});
|
||||
CHECK(v.contains(p) == (contains_error.empty() && j.contains(p)));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.value(p, 5)); }) == without_path(exception_of([&] { static_cast<void>(j.value(p, 5)); })));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.value(p, 5)); }) == exception_of([&] { static_cast<void>(j.value(p, 5)); }));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <nlohmann/detail/view/builder.hpp>
|
||||
#include <nlohmann/detail/view/materialize.hpp>
|
||||
#include <nlohmann/detail/view/string_ref.hpp>
|
||||
#include <nlohmann/json_view.hpp> // json_document::accept
|
||||
#else
|
||||
#include <nlohmann/json_view.hpp> // the single header contains the internal headers
|
||||
#endif
|
||||
@@ -59,7 +60,8 @@ built build(const std::string& text, bool comments, bool trailing_commas, bool s
|
||||
r.copy.assign(text.begin(), text.end());
|
||||
src = r.copy.data();
|
||||
}
|
||||
r.ok = nlohmann::detail::view::build < FloatType, !nlohmann::detail::abi_config::strict_nul_handling > (*r.data, src, text.size(), comments, trailing_commas, sentinel, r.failure);
|
||||
constexpr bool nul_is_end = !nlohmann::detail::abi_config::strict_nul_handling;
|
||||
r.ok = nlohmann::detail::view::build<FloatType, nul_is_end>(*r.data, src, text.size(), comments, trailing_commas, sentinel, false, r.failure);
|
||||
r.data->src = src;
|
||||
r.data->base[0] = src;
|
||||
r.data->base[1] = r.data->arena.data();
|
||||
@@ -172,6 +174,22 @@ void check_string(const std::string& content)
|
||||
}
|
||||
}
|
||||
|
||||
// accept() stores no nodes: it must accept and reject what json::accept does, for all options and for inputs with
|
||||
// and without a terminating NUL
|
||||
void check_accept(const std::string& text)
|
||||
{
|
||||
CAPTURE(text)
|
||||
const std::vector<char> bytes(text.begin(), text.end());
|
||||
for (int options = 0; options < 4; ++options)
|
||||
{
|
||||
const bool comments = (options & 1) != 0;
|
||||
const bool trailing_commas = (options & 2) != 0;
|
||||
const bool accepted = json::accept(text, comments, trailing_commas);
|
||||
CHECK(nlohmann::json_document::accept(text, comments, trailing_commas) == accepted);
|
||||
CHECK(nlohmann::json_document::accept(bytes, comments, trailing_commas) == accepted);
|
||||
}
|
||||
}
|
||||
|
||||
// a small deterministic generator of documents
|
||||
struct generator
|
||||
{
|
||||
@@ -423,6 +441,133 @@ TEST_CASE("json_view builder")
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("accept() without an index")
|
||||
{
|
||||
for (const char* text :
|
||||
{
|
||||
"", " ", "\n", "null", "nul", "true", "[", "]", "[]", "{}", "[1,2,3]", "[1,2,3", "[1,2,3,]", "{\"a\":1,}", "{\"a\"}", "{\"a\":}",
|
||||
"[1] x", "[1][2]", "\xEF\xBB\xBF[1]", "[\"a\\", R"(["\u12"])", R"(["\ud800"])", "[\"\x01\"]", "[\"\xff\"]", "[01]", "[1.]",
|
||||
"[.5]", "[-]", "[1e]", "// c\n[1]", "/* c */ {\"a\" /* x */ : 1}", "/* c", "[1] // c", "1e400", "-1e400", "123456789012345678901234567890",
|
||||
R"({"a":1,"a":2})", "[[[]]]", R"("\u00e4\n\ud83d\ude00")"
|
||||
})
|
||||
{
|
||||
check_accept(text);
|
||||
}
|
||||
// a null byte inside the text, and after it
|
||||
check_accept(std::string("[1,\0]", 5));
|
||||
check_accept(std::string("[1]\0", 4));
|
||||
check_accept(std::string("\0", 1));
|
||||
|
||||
// generated documents and damaged copies, as for the builder above
|
||||
generator g;
|
||||
for (int i = 0; i < 2000; ++i)
|
||||
{
|
||||
std::string text;
|
||||
g.value(text, 0);
|
||||
check_accept(text);
|
||||
std::string damaged = text;
|
||||
const auto at = static_cast<std::size_t>(g.r(static_cast<int>(damaged.size())));
|
||||
static const char replacements[] = {'x', '"', '\\', ',', ':', ']', '}', '[', '{', '1', '-', '.', 'e', '\0', '\n', '/'}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
|
||||
damaged[at] = replacements[g.r(16)];
|
||||
check_accept(damaged);
|
||||
check_accept(text.substr(0, at));
|
||||
}
|
||||
|
||||
// nesting beyond the 64 levels that are kept on the builder's own stack, closed and not closed
|
||||
for (const std::size_t depth :
|
||||
{
|
||||
63u, 64u, 65u, 66u, 200u, 1000u
|
||||
})
|
||||
{
|
||||
check_accept(std::string(depth, '[') + std::string(depth, ']'));
|
||||
check_accept(std::string(depth, '[') + std::string(depth - 1, ']'));
|
||||
std::string objects;
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
objects += "{\"k\":";
|
||||
}
|
||||
objects += "1" + std::string(depth, '}');
|
||||
check_accept(objects);
|
||||
check_accept(objects.substr(0, objects.size() - 1));
|
||||
}
|
||||
|
||||
// objects with enough members to get a hash index when they are indexed (and duplicate keys)
|
||||
std::string large = "{";
|
||||
for (int i = 0; i < 300; ++i)
|
||||
{
|
||||
large += (i == 0 ? "\"k" : ",\"k") + std::to_string(i % 200) + "\":" + std::to_string(i);
|
||||
}
|
||||
check_accept(large + "}");
|
||||
check_accept(large);
|
||||
check_accept("[" + large + "},{" + large.substr(1) + "}]");
|
||||
|
||||
// the overflow check uses the floating-point type of the document
|
||||
using float_json = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
|
||||
for (const char* text :
|
||||
{
|
||||
"1e39", "-1e39", "3.4028235e38", "3.4028236e38", "[1.5e38, 3.5e38]", R"({"a": 1e-50, "b": 1e39})", "340282356779733661637539395458142568448"
|
||||
})
|
||||
{
|
||||
CAPTURE(text)
|
||||
CHECK(nlohmann::basic_json_document<float_json>::accept(text) == float_json::accept(text));
|
||||
}
|
||||
|
||||
// strings are checked and not decoded: runs of text of every length from 0 to 40 between valid and invalid
|
||||
// escapes, UTF-8 sequences and control characters, in values and in keys, cut near the end of the text
|
||||
const std::array<const char*, 9> valid = {{"\\n", "\\\"", "\\\\", "\\/", "\\t", "\\u00e4", "\\u20ac", "\\ud83d\\ude00", "\\u0000"}};
|
||||
const std::array<const char*, 12> invalid = {{"\\x", "\\ud800", "\\udc00", "\\u12G4", "\\u12", "\\ud83dx", "\\ud83d\\u0041", "\\ud83d\\ud83d", "\\", "\x01", "\x1f", "\x7f"}};
|
||||
const std::array<const char*, 6> utf8 = {{"\xc3\xa4", "\xe2\x82\xac", "\xf0\x9f\x98\x80", "\xc3", "\xff", "\x80"}};
|
||||
std::mt19937 rng(2029); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
|
||||
for (std::size_t run = 0; run <= 40; ++run)
|
||||
{
|
||||
for (int i = 0; i < 30; ++i)
|
||||
{
|
||||
std::string content = std::string(rng() % 20, 'a');
|
||||
const auto pieces = 1 + (rng() % 6);
|
||||
for (unsigned k = 0; k < pieces; ++k)
|
||||
{
|
||||
content += valid[rng() % valid.size()];
|
||||
content += std::string(k == 0 ? run : rng() % 41, static_cast<char>('b' + (rng() % 20)));
|
||||
switch (rng() % 8)
|
||||
{
|
||||
case 0:
|
||||
content += utf8[rng() % 3];
|
||||
break;
|
||||
case 1:
|
||||
content += utf8[rng() % utf8.size()];
|
||||
break;
|
||||
case 2:
|
||||
content += invalid[rng() % invalid.size()];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const std::string& text :
|
||||
{
|
||||
"[\"" + content + "\"]", "{\"" + content + "\":1}", "[\"" + content, "[\"" + content.substr(0, content.size() - (rng() % 4)) + "\"]"
|
||||
})
|
||||
{
|
||||
check_accept(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("escapes in a row")
|
||||
{
|
||||
for (const char* content :
|
||||
{
|
||||
"\\n", R"(\n\t\\)", R"(\u00e9\u00e9\u4e2d)", "\\u00e9\\u00e", "\\u00e9\\", "\\n\\q", R"(\n\ud83d\ude00\n)",
|
||||
R"(\ud83d\ude00\ud83d)", R"(\n\n\n abc \n\n)", "\\n\x01", "\\n\xC3\x28", "\\n\\"
|
||||
})
|
||||
{
|
||||
check_accept(std::string("[\"") + content + "\"]");
|
||||
check_accept(std::string("[\"") + content);
|
||||
check_accept(std::string("{\"") + content + "\":1}");
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("test files")
|
||||
{
|
||||
for (const char* name :
|
||||
@@ -523,6 +668,54 @@ TEST_CASE("json_view builder: strings across vector blocks")
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view builder: fraction digits")
|
||||
{
|
||||
// The digits of a fraction are skipped 16 bytes at a time where there are
|
||||
// that many left (SSE2), and one at a time after that. Fractions of every
|
||||
// length from 0 to 40 digits, followed by every kind of byte that ends a
|
||||
// number or lies next to the digits in ASCII ('/' and ':'), as values, as
|
||||
// keys' neighbours, and at the very end of the text (a vector without a
|
||||
// terminating NUL). The length of the number is part of the value that
|
||||
// the test compares with json::parse().
|
||||
const std::string followers = ",]}/:.eE+-x \n\t\"09";
|
||||
std::mt19937 rng(2028); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
|
||||
for (std::size_t digits = 0; digits <= 40; ++digits)
|
||||
{
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
std::string fraction;
|
||||
for (std::size_t k = 0; k < digits; ++k)
|
||||
{
|
||||
fraction += static_cast<char>('0' + (rng() % 10));
|
||||
}
|
||||
const std::string number = std::string(rng() % 2 == 0 ? "-" : "") + std::to_string(rng() % 100000) + "." + fraction;
|
||||
check_same(number);
|
||||
check_same("[" + number + "]");
|
||||
std::string pair;
|
||||
pair.reserve(number.size() * 2 + 3);
|
||||
pair += '[';
|
||||
pair += number;
|
||||
pair += ',';
|
||||
pair += number;
|
||||
pair += ']';
|
||||
check_same(pair);
|
||||
check_same("{\"a\":" + number + "}");
|
||||
for (const char follower : followers)
|
||||
{
|
||||
check_same(number + follower);
|
||||
check_same("[" + number + follower + "]");
|
||||
check_same("[" + number + follower + std::string(rng() % 20, '1'));
|
||||
}
|
||||
// everything near the end of the text: the number cut in every place
|
||||
for (std::size_t cut = 1; cut <= 18 && cut <= number.size(); ++cut)
|
||||
{
|
||||
check_same(number.substr(0, number.size() - cut));
|
||||
check_same("[" + number.substr(0, number.size() - cut));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view builder: escapes between runs of text")
|
||||
{
|
||||
// The text between escapes is copied 16 bytes at a time where there are
|
||||
@@ -575,6 +768,59 @@ TEST_CASE("json_view builder: escapes between runs of text")
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view builder: chains of unicode escapes")
|
||||
{
|
||||
// Consecutive \uXXXX escapes are decoded in a loop of their own. Chains of
|
||||
// every length from 1 to 40 of escapes that give one, two, and three
|
||||
// bytes of UTF-8, in upper and lower case, followed by what ends a chain:
|
||||
// the end of the string, text, another escape, UTF-8, and everything that
|
||||
// is not a valid escape (a bad digit at every position of the last
|
||||
// escape, a lone surrogate, a surrogate pair, a cut in every place).
|
||||
const std::array<const char*, 8> chain = {{"\\u0041", "\\u00e4", "\\u00E4", "\\u20ac", "\\u3042", "\\uFFFF", "\\u0000", "\\u07ff"}};
|
||||
const std::array<const char*, 12> ends = {{"", "abc", "\\n", "\\u00e9", "\\ud83d\\ude00", "\\ud83d", "\\ude00", "\\ud83d\\u0041", "\\u12G4", "\\u12", "\xc3\xa4", "\\"}};
|
||||
std::mt19937 rng(2027); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
|
||||
for (std::size_t length = 1; length <= 40; ++length)
|
||||
{
|
||||
for (int i = 0; i < 20; ++i)
|
||||
{
|
||||
std::string content = std::string(rng() % 20, 'a');
|
||||
for (std::size_t k = 0; k < length; ++k)
|
||||
{
|
||||
content += chain[rng() % chain.size()];
|
||||
}
|
||||
const std::string whole = content;
|
||||
for (const char* end : ends)
|
||||
{
|
||||
check_string(whole + end);
|
||||
check_string(whole + end + std::string(rng() % 19, 'z'));
|
||||
}
|
||||
// a bad digit at every position of every escape near the end of the chain
|
||||
for (std::size_t back = 1; back <= 6 && back <= length; ++back)
|
||||
{
|
||||
for (std::size_t digit = 2; digit < 6; ++digit)
|
||||
{
|
||||
std::string broken = whole;
|
||||
broken[broken.size() - (6 * back) + digit] = "gG\"\x01 z"[rng() % 6];
|
||||
check_string(broken);
|
||||
}
|
||||
}
|
||||
// the text cut in every place of the last three escapes
|
||||
for (std::size_t cut = 0; cut <= 18 && cut <= whole.size(); ++cut)
|
||||
{
|
||||
check_string(whole.substr(0, whole.size() - cut));
|
||||
}
|
||||
}
|
||||
}
|
||||
// a chain longer than the first growth of the arena, in a key and in a value
|
||||
std::string long_chain;
|
||||
for (int i = 0; i < 3000; ++i)
|
||||
{
|
||||
long_chain += chain[static_cast<std::size_t>(i) % chain.size()];
|
||||
}
|
||||
check_string(long_chain);
|
||||
check_string(long_chain + "\\ud83d\\ude00" + long_chain);
|
||||
}
|
||||
|
||||
TEST_CASE("json_view node integer bits")
|
||||
{
|
||||
using nlohmann::detail::view::integer_bits;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json_view.hpp>
|
||||
using nlohmann::json;
|
||||
using nlohmann::json_document;
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
// These tests were split off unit-json_view_compare.cpp, whose object file got
|
||||
// too large for the MinGW assembler ("file too big").
|
||||
|
||||
namespace
|
||||
{
|
||||
// an object type that does not enumerate its members in ascending order of keys
|
||||
template<typename Key, typename Value, typename /*Compare*/, typename Allocator>
|
||||
using descending_object = std::map<Key, Value, std::greater<Key>, Allocator>;
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("json_view comparison of differing values")
|
||||
{
|
||||
SECTION("values that differ in their first bytes, or are written differently")
|
||||
{
|
||||
const auto same = [](const char* x, const char* y)
|
||||
{
|
||||
const json_document dx = json_document::parse(x);
|
||||
const json_document dy = json_document::parse(y);
|
||||
const bool equal = dx.root() == dy.root();
|
||||
CHECK(equal == (json::parse(x) == json::parse(y)));
|
||||
CHECK(equal == (dx.root() == json::parse(y)));
|
||||
const nlohmann::json_editable_document ex = nlohmann::json_editable_document::parse(x);
|
||||
CHECK(equal == (ex.root() == dy.root()));
|
||||
return equal;
|
||||
};
|
||||
// booleans
|
||||
CHECK(!same("true", "false"));
|
||||
CHECK(!same("[true]", "[false]"));
|
||||
CHECK(!same(R"({"a":false,"b":[1]})", R"({"a":true,"b":[1]})"));
|
||||
CHECK(same(R"({"a":true,"b":[1]})", R"({"a":true,"b":[1]})"));
|
||||
// strings and keys of equal length that differ in the first eight bytes (they are compared in words)
|
||||
CHECK(!same(R"("abcdefghijklmnopqrstuvwxyz")", R"("Xbcdefghijklmnopqrstuvwxyz")"));
|
||||
CHECK(!same(R"(["abcdefghijklmnopq",1])", R"(["abcdefghXjklmnopq",1])"));
|
||||
CHECK(!same(R"({"abcdefghijklmnopq":1})", R"({"Xbcdefghijklmnopq":1})"));
|
||||
CHECK(!same(R"({"abcdefghijklmnopq":[1]})", R"({"abcdefghijklmnopX":[1]})"));
|
||||
// floats of one layout and different tokens are the same value
|
||||
CHECK(same("[1.5e0, 2]", "[1.5E0, 2]"));
|
||||
CHECK(same("[1.5e0, 2]", "[1.5e+0, 2]"));
|
||||
CHECK(same("[1.50, 2]", "[1.5, 2]"));
|
||||
CHECK(!same("[1.5e0, 2]", "[1.6e0, 2]"));
|
||||
CHECK(!same("[1.5e0, 2]", "[1.5E1, 2]"));
|
||||
CHECK(same(R"({"a":1.25e0,"b":[3]})", R"({"a":1.25E0,"b":[3]})"));
|
||||
}
|
||||
|
||||
SECTION("a basic_json whose members are not sorted")
|
||||
{
|
||||
// (an object type that is not an ordered map, and does not keep its members sorted either)
|
||||
using descending_json = nlohmann::basic_json<descending_object>;
|
||||
using descending_document = nlohmann::basic_json_document<descending_json>;
|
||||
std::string text = "{";
|
||||
for (char key = 'a'; key <= 'p'; ++key)
|
||||
{
|
||||
text += (key != 'a' ? ",\"" : "\"") + std::string(1, key) + "\":[" + std::to_string(key) + "]";
|
||||
}
|
||||
text += "}";
|
||||
const descending_json j = descending_json::parse(text);
|
||||
const descending_document d = descending_document::parse(text);
|
||||
CHECK(d.root() == j);
|
||||
CHECK(j == d.root());
|
||||
descending_json other = j;
|
||||
other["k"] = 0;
|
||||
CHECK(d.root() != other);
|
||||
CHECK(other != d.root());
|
||||
}
|
||||
}
|
||||
+112
-672
@@ -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")
|
||||
{
|
||||
@@ -101,6 +101,10 @@ TEST_CASE("json_view dump")
|
||||
CHECK(d.root().dump(0, ' ', false, json_view::number_format::source) == "[\n1.50,\n1E2,\n-0,\n-0.0,\n123456789012345678901234567890,\n18446744073709551615,\n-9223372036854775808,\n0.1,\n1e-7,\n5e-324\n]");
|
||||
CHECK(d.root().dump(-1, ' ', true, json_view::number_format::source) == "[1.50,1E2,-0,-0.0,123456789012345678901234567890,18446744073709551615,-9223372036854775808,0.1,1e-7,5e-324]");
|
||||
|
||||
CHECK(d.root().dump(0, ' ', true, json_view::number_format::source) == d.root().dump(0, ' ', false, json_view::number_format::source));
|
||||
const json_document escaped = json_document::parse("[\"\xC3\xA9\", 1.50, {\"\xF0\x9F\x98\x80\": 1E2}]");
|
||||
CHECK(escaped.root().dump(1, ' ', true, json_view::number_format::source) == "[\n \"\\u00e9\",\n 1.50,\n {\n \"\\ud83d\\ude00\": 1E2\n }\n]");
|
||||
|
||||
// float tokens of up to 17 significant digits in every spelling: those
|
||||
// of at most 15 digits are written from their digits, the others
|
||||
// through the conversion; both as dump() writes them
|
||||
@@ -150,6 +154,111 @@ TEST_CASE("json_view dump")
|
||||
CHECK(many_doc.root().dump() == json::parse(many_tokens).dump());
|
||||
}
|
||||
|
||||
// tokens of 16 and 17 digits without an exponent (like GeoJSON
|
||||
// coordinates), which dump() converts in batches: doubles exactly
|
||||
// halfway between two candidates for the shortest digits, random
|
||||
// digits, and digits near a shorter decimal (...99999997,
|
||||
// ...00000003), whose rounding carries; in pairs and among other
|
||||
// values, so that the batches are interrupted; the last token near the
|
||||
// end of the text
|
||||
{
|
||||
std::mt19937_64 tokens(5627); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
const auto draw = [&tokens](std::size_t n)
|
||||
{
|
||||
const std::uint64_t r = tokens() % n;
|
||||
return static_cast<std::size_t>(r);
|
||||
};
|
||||
std::string coordinates = "[754570942.33984373,257769445.56835938,858106901.94921875,97944964.876953125,"
|
||||
"[0.5,1e-7],[1.5,-0.0,2.5],[1E2,12.5,0.1,3.25,4.5,5.75,6.125,7.0,8.5],[],[1.5],[1,2.5],[2.5,\"x\"],[[1.5],2.5],";
|
||||
for (int i = 0; i < 20000; ++i)
|
||||
{
|
||||
const std::size_t length = 16 + draw(2);
|
||||
std::string digits(1, static_cast<char>('1' + draw(9)));
|
||||
const std::size_t kept = draw(3) == 0 ? length : 2 + draw(length - 3);
|
||||
for (std::size_t k = 1; k < kept; ++k)
|
||||
{
|
||||
digits += static_cast<char>('0' + draw(10));
|
||||
}
|
||||
digits += std::string(length - kept, draw(2) == 0 ? '9' : '0');
|
||||
digits.back() = static_cast<char>('0' + draw(10));
|
||||
const std::size_t point = 1 + draw(9);
|
||||
const std::string token = (draw(2) == 0 ? "-" : "") + digits.substr(0, point) + "." + digits.substr(point);
|
||||
if (i != 0)
|
||||
{
|
||||
coordinates += ',';
|
||||
}
|
||||
if (draw(8) == 0)
|
||||
{
|
||||
coordinates += R"({"x":)";
|
||||
coordinates += token;
|
||||
coordinates += R"(,"s":"a"})";
|
||||
}
|
||||
else
|
||||
{
|
||||
coordinates += '[';
|
||||
coordinates += token;
|
||||
coordinates += ',';
|
||||
coordinates += token;
|
||||
coordinates += ']';
|
||||
}
|
||||
}
|
||||
coordinates += ",-65.613616999999977]";
|
||||
const json_document parsed = json_document::parse(coordinates);
|
||||
const ordered_json j = ordered_json::parse(coordinates);
|
||||
CHECK(parsed.root().dump() == j.dump());
|
||||
CHECK(parsed.root().dump(1) == j.dump(1));
|
||||
CHECK(parsed.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(parsed.root().back().get<double>() == j.back().get<double>());
|
||||
CHECK(parsed.root().back().dump() == "-65.61361699999998");
|
||||
std::size_t i = 0;
|
||||
for (const json_view v : parsed.root())
|
||||
{
|
||||
const ordered_json& e = j[i++];
|
||||
CHECK(v.dump() == e.dump());
|
||||
if (v.is_array() && !v.empty() && v[0].is_number())
|
||||
{
|
||||
CHECK(v[0].get<double>() == e[0].get<double>());
|
||||
}
|
||||
}
|
||||
|
||||
// an editable document: edits move some arrays, so that the walk
|
||||
// leaves the order of the parsed nodes
|
||||
ordered_json_editable_document editable = ordered_json_editable_document::parse(coordinates);
|
||||
ordered_json expected = j;
|
||||
for (std::size_t k = 0; k < 20000; k += 97)
|
||||
{
|
||||
if (expected[k].is_array())
|
||||
{
|
||||
editable.push_back(editable.root()[k], ordered_json(1.5));
|
||||
expected[k].push_back(1.5);
|
||||
}
|
||||
}
|
||||
CHECK(editable.root().dump() == expected.dump());
|
||||
}
|
||||
|
||||
// tokens of 16 and 17 digits with few digits after the point, and with a value of 0, which are
|
||||
// converted by the general algorithm; padded, as a token near the end of the text is not read in this way
|
||||
{
|
||||
const std::string padding(40, ' ');
|
||||
for (const char* token :
|
||||
{
|
||||
"123456789012.3456", "-123456789012.3456", "1234567890123.456", "12345678901234.56", "123456789012345.6", "1234567890123456.0",
|
||||
"0.0000000000000000", "-0.0000000000000000", "0.000000000000000", "1234567890123.4567", "99999999999999.999"
|
||||
})
|
||||
{
|
||||
CAPTURE(token)
|
||||
const std::string padded = std::string("[") + token + padding + "]";
|
||||
const json_document padded_document = json_document::parse(padded);
|
||||
const json padded_json = json::parse(padded);
|
||||
CHECK(padded_document.root()[0].get<double>() == padded_json[0].get<double>());
|
||||
CHECK(padded_document.root().dump() == padded_json.dump());
|
||||
// a float document converts the tokens as well
|
||||
using json_float = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
|
||||
const nlohmann::basic_json_document<json_float> float_doc = nlohmann::basic_json_document<json_float>::parse(padded);
|
||||
CHECK(float_doc.root()[0].get<float>() == json_float::parse(padded)[0].get<float>());
|
||||
}
|
||||
}
|
||||
|
||||
// random doubles, written as parse() and dump() would
|
||||
std::mt19937_64 rng(1170); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
std::string many = "[";
|
||||
@@ -171,673 +280,4 @@ TEST_CASE("json_view dump")
|
||||
const nlohmann::basic_json_document<json_float> float_document = nlohmann::basic_json_document<json_float>::parse("[0.1, 1.5e10, 3.4028235e38]");
|
||||
CHECK(float_document.root().dump() == json_float::parse("[0.1, 1.5e10, 3.4028235e38]").dump());
|
||||
}
|
||||
|
||||
SECTION("members in document order, all of them")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"b": 1, "a": 2, "b": 3})");
|
||||
CHECK(d.root().dump() == R"({"b":1,"a":2,"b":3})");
|
||||
CHECK(d.root().dump(1) == "{\n \"b\": 1,\n \"a\": 2,\n \"b\": 3\n}");
|
||||
// dump() writes all members, though the lookup finds the first
|
||||
CHECK(d.root()["b"].dump() == "1");
|
||||
}
|
||||
|
||||
SECTION("deep nesting")
|
||||
{
|
||||
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
|
||||
const json_document deep_document = json_document::parse(deep);
|
||||
CHECK(deep_document.root().dump() == deep);
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii: a character that needs an escape at each position of a string")
|
||||
{
|
||||
// string lengths around the width of the vector steps, in a string value, a key, and a value near the end of the text
|
||||
for (const char* const special :
|
||||
{
|
||||
"\xC3\xA9", "\xE4\xB8\xAD", "\xF0\x9F\x98\x80", "\x7F", "\\n", "\\u0001", "\\\"", "\\\\"
|
||||
})
|
||||
{
|
||||
for (std::size_t length = 0; length <= 40; ++length)
|
||||
{
|
||||
for (std::size_t position = 0; position <= length; ++position)
|
||||
{
|
||||
std::string content(length, 'a');
|
||||
content.insert(position, special);
|
||||
const std::array<std::string, 3> texts =
|
||||
{
|
||||
{
|
||||
"\"" + content + "\"",
|
||||
"[\"" + content + "\"]",
|
||||
"{\"" + content + "\":1}",
|
||||
}
|
||||
};
|
||||
for (const std::string& text : texts)
|
||||
{
|
||||
CAPTURE(text)
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii: every character of the Basic Multilingual Plane and some beyond")
|
||||
{
|
||||
// (the two- and three-byte sequences, including the ranges that need a check: E0, ED, EF BF BF)
|
||||
std::string text = "[";
|
||||
std::size_t in_chunk = 0;
|
||||
const auto append = [&](std::uint32_t cp)
|
||||
{
|
||||
if (cp < 0x80)
|
||||
{
|
||||
if (cp < 0x20 || cp == '"' || cp == '\\')
|
||||
{
|
||||
return;
|
||||
}
|
||||
text += static_cast<char>(cp);
|
||||
}
|
||||
else if (cp < 0x800)
|
||||
{
|
||||
text += static_cast<char>(0xC0 | (cp >> 6));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
else if (cp < 0x10000)
|
||||
{
|
||||
text += static_cast<char>(0xE0 | (cp >> 12));
|
||||
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
else
|
||||
{
|
||||
text += static_cast<char>(0xF0 | (cp >> 18));
|
||||
text += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
||||
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
};
|
||||
for (std::uint32_t cp = 0x20; cp < 0x10400; ++cp)
|
||||
{
|
||||
if (cp >= 0xD800 && cp < 0xE000)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (in_chunk == 0)
|
||||
{
|
||||
text += text.size() > 1 ? ",\"" : "\"";
|
||||
}
|
||||
append(cp);
|
||||
if (++in_chunk == 37)
|
||||
{
|
||||
text += "\"";
|
||||
in_chunk = 0;
|
||||
}
|
||||
}
|
||||
text += in_chunk != 0 ? "\"]" : "]";
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
CHECK(d.root().dump(2) == j.dump(2));
|
||||
}
|
||||
|
||||
SECTION("deep nesting, indented")
|
||||
{
|
||||
// indentations of up to and beyond the width of the writer's fixed-size copy
|
||||
for (const int depth :
|
||||
{
|
||||
1, 5, 20, 70, 300
|
||||
})
|
||||
{
|
||||
std::string text;
|
||||
for (int i = 0; i < depth; ++i)
|
||||
{
|
||||
text += i % 2 == 0 ? "[1," : R"({"k":2,"\u00e9":)";
|
||||
}
|
||||
text += "[]";
|
||||
for (int i = depth - 1; i >= 0; --i)
|
||||
{
|
||||
text += i % 2 == 0 ? "]" : "}";
|
||||
}
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
for (const int indent :
|
||||
{
|
||||
0, 1, 2, 3, 8
|
||||
})
|
||||
{
|
||||
CHECK(d.root().dump(indent) == j.dump(indent));
|
||||
CHECK(d.root().dump(indent, '\t') == j.dump(indent, '\t'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("the output buffer of a small value is small")
|
||||
{
|
||||
// an escaped key after the value: its node lies in the arena, so the
|
||||
// source extent of the value cannot be read from the next node
|
||||
const std::string big(100000, 'a');
|
||||
const std::string text = R"({"small":1,"list":[1,2,3],"k\n":")" + big + R"("})";
|
||||
const json_document d = json_document::parse(text);
|
||||
const auto small = d.root()["small"].dump();
|
||||
CHECK(small == "1");
|
||||
CHECK(small.capacity() < 4096);
|
||||
const auto list = d.root()["list"].dump();
|
||||
CHECK(list == "[1,2,3]");
|
||||
CHECK(list.capacity() < 4096);
|
||||
CHECK(d.root()["list"].dump(2).capacity() < 4096);
|
||||
|
||||
// the whole document and the large value are unaffected
|
||||
CHECK(d.root().dump() == ordered_json::parse(text).dump());
|
||||
CHECK(d.root()["k\n"].dump() == "\"" + big + "\"");
|
||||
}
|
||||
|
||||
SECTION("output that outgrows the estimate")
|
||||
{
|
||||
// ensure_ascii writes six bytes for each two-byte character
|
||||
std::string chars;
|
||||
for (int i = 0; i < 5000; ++i)
|
||||
{
|
||||
chars += "\xC3\xA9";
|
||||
}
|
||||
const json_document d = json_document::parse(R"({"a":")" + chars + R"(","k\n":1})");
|
||||
const json expected = json::parse("\"" + chars + "\"");
|
||||
CHECK(d.root()["a"].dump(-1, ' ', true) == expected.dump(-1, ' ', true));
|
||||
CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6));
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii with strings of the source, decoded strings, and edited strings")
|
||||
{
|
||||
// plain ASCII, non-ASCII, escaped (decoded), and mixed strings, as values and as keys;
|
||||
// the second text has a DEL, which ensure_ascii writes as an escape
|
||||
for (const char* const del :
|
||||
{"", "\x7F"
|
||||
})
|
||||
{
|
||||
const std::string text = std::string("{\"plain\":\"abc\",\"caf\xC3\xA9\":\"\xC3\xA9\xE2\x82\xAC\xF0\x9F\x98\x80\",")
|
||||
+ "\"esc\\n\\\"\":\"a\\tb\\u00e4\\ud83d\\ude00\",\"del\":\"a" + del + "b\",\"\":\"\"," // NOLINT(modernize-raw-string-literal)
|
||||
+ "\"list\":[\"x\",\"\xC3\xBC\",\"\\\\\",\"long ASCII string with more than thirty-two bytes in it\","
|
||||
+ "\"long string with more than thirty-two bytes and one non-ASCII byte \xC3\xB6\"],"
|
||||
+ "\"n\":[1,2.5,-0,true,null]}";
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(4, ' ', true) == j.dump(4, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
CHECK(d.root()["list"].dump(-1, ' ', true) == j["list"].dump(-1, ' ', true));
|
||||
CHECK(d.root()["del"].dump(-1, ' ', true) == j["del"].dump(-1, ' ', true));
|
||||
|
||||
// strings that an edit wrote (the source strings that they replace were plain)
|
||||
ordered_json_editable_document e = ordered_json_editable_document::parse(text);
|
||||
ordered_json k = j;
|
||||
e.set(e.root()["plain"], std::string("\xC3\xA4gypten"));
|
||||
k["plain"] = "\xC3\xA4gypten";
|
||||
e.set(e.root()["list"][0], std::string("ascii after edit"));
|
||||
k["list"][0] = "ascii after edit";
|
||||
e.set(e.root()["list"][1], std::string("\xC3\xA4"));
|
||||
k["list"][1] = "\xC3\xA4";
|
||||
e.set(e.root()["del"], std::string("tab\there"));
|
||||
k["del"] = "tab\there";
|
||||
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
|
||||
CHECK(e.root().dump(2, ' ', true) == k.dump(2, ' ', true));
|
||||
CHECK(e.root().dump() == k.dump());
|
||||
|
||||
// strings that were edited from or to ASCII text
|
||||
e.set(e.root()["list"][2], std::string("\xC3\xA4"));
|
||||
k["list"][2] = "\xC3\xA4";
|
||||
e.set(e.root()["list"][4], std::string("plain again"));
|
||||
k["list"][4] = "plain again";
|
||||
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii with DEL or a non-ASCII character at every position")
|
||||
{
|
||||
// the check whether a string of the source has no byte >= 0x7F reads
|
||||
// eight bytes at a time and then the rest one by one
|
||||
for (std::size_t length = 1; length <= 20; ++length)
|
||||
{
|
||||
for (std::size_t pos = 0; pos < length; ++pos)
|
||||
{
|
||||
for (const char* const c :
|
||||
{
|
||||
"\x7F", "\x7E", "\xC3\xA4", "\xC3\xBF" // NOLINT(modernize-raw-string-literal)
|
||||
})
|
||||
{
|
||||
const std::string s = std::string(pos, 'a') + c + std::string(length - pos - 1, 'b');
|
||||
std::string text = "[\"";
|
||||
text.append(s).append("\",{\"").append(s).append("\":1}]");
|
||||
const json_document d = json_document::parse(text);
|
||||
const json j = json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(1, ' ', true) == j.dump(1, ' ', true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("streams and discarded views")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"a": [1, 2]})");
|
||||
std::ostringstream compact;
|
||||
compact << d.root();
|
||||
CHECK(compact.str() == R"({"a":[1,2]})");
|
||||
std::ostringstream pretty;
|
||||
pretty << std::setw(2) << std::setfill('.') << d.root() << d.root()["a"];
|
||||
CHECK(pretty.str() == "{\n..\"a\": [\n....1,\n....2\n..]\n}[1,2]");
|
||||
CHECK(json_view().dump() == json(json::value_t::discarded).dump());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view large objects")
|
||||
{
|
||||
// objects with 128 members or more are looked up with a hash index
|
||||
for (const std::size_t members :
|
||||
{
|
||||
127u, 128u, 129u, 10000u
|
||||
})
|
||||
{
|
||||
CAPTURE(members)
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\\n" : "") + "\":" + std::to_string(i);
|
||||
}
|
||||
text += R"(,"":"empty key","k1":"a duplicate of an earlier key"})";
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
const json j = json::parse(text);
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
const std::string key = std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\n" : "");
|
||||
CHECK(v[key].get<std::size_t>() == i);
|
||||
CHECK(v.contains(key));
|
||||
CHECK(v.find(key).key() == key);
|
||||
CHECK(v.at(key).get<std::size_t>() == i);
|
||||
CHECK(!v.contains(key + "x"));
|
||||
}
|
||||
CHECK(v[""].get_string() == "empty key");
|
||||
CHECK(v["k1"].get<int>() == 1); // the first of duplicate keys, as for small objects
|
||||
CHECK(!v.contains("missing"));
|
||||
CHECK_THROWS_WITH_AS(v.at("missing"), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&);
|
||||
CHECK(v == j);
|
||||
CHECK(v.materialize() == j);
|
||||
}
|
||||
|
||||
SECTION("duplicate keys: the first member wins, with and without a table")
|
||||
{
|
||||
// an object of `total` members: the keys "k0".."k<n-1>" in order, then
|
||||
// three keys repeated twice more (one copy in the middle, one at the
|
||||
// end), and two keys repeated once; the value of a member is its
|
||||
// position, so that the first member of a key can be told apart
|
||||
struct member
|
||||
{
|
||||
std::string key;
|
||||
std::size_t position;
|
||||
};
|
||||
const auto make_members = [](std::size_t total)
|
||||
{
|
||||
std::vector<std::string> keys;
|
||||
for (std::size_t i = 0; i + 8 < total; ++i)
|
||||
{
|
||||
keys.push_back("k" + std::to_string(i));
|
||||
}
|
||||
const std::size_t n = keys.size();
|
||||
const std::array<std::size_t, 3> triple = {{3, 17, n - 1}};
|
||||
const std::array<std::size_t, 2> twice = {{5, n / 2}};
|
||||
std::vector<std::string> ordered = keys;
|
||||
for (const std::size_t i : triple)
|
||||
{
|
||||
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 2), keys[i]);
|
||||
}
|
||||
for (const std::size_t i : twice)
|
||||
{
|
||||
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 3), keys[i]);
|
||||
}
|
||||
for (const std::size_t i : triple)
|
||||
{
|
||||
ordered.push_back(keys[i]);
|
||||
}
|
||||
std::vector<member> result;
|
||||
result.reserve(ordered.size());
|
||||
for (std::size_t i = 0; i < ordered.size(); ++i)
|
||||
{
|
||||
result.push_back({ordered[i], i});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// 100 and 127 members: no table; 128 members and more: a table
|
||||
for (const std::size_t total :
|
||||
{
|
||||
100u, 127u, 128u, 200u, 5000u
|
||||
})
|
||||
{
|
||||
CAPTURE(total)
|
||||
const std::vector<member> members = make_members(total);
|
||||
REQUIRE(members.size() >= total);
|
||||
REQUIRE(members.size() >= 100);
|
||||
std::string text = "{";
|
||||
std::map<std::string, std::size_t> first;
|
||||
std::map<std::string, std::size_t> last;
|
||||
std::map<std::string, std::size_t> occurrences;
|
||||
for (const member& m : members)
|
||||
{
|
||||
text += (text.size() > 1 ? ",\"" : "\"") + m.key + "\":" + std::to_string(m.position);
|
||||
first.insert({m.key, m.position});
|
||||
last[m.key] = m.position;
|
||||
++occurrences[m.key];
|
||||
}
|
||||
text += '}';
|
||||
REQUIRE(first.size() < members.size());
|
||||
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
const json j = json::parse(text);
|
||||
CHECK(v.size() == members.size()); // every occurrence is visited
|
||||
const json m = v.materialize();
|
||||
for (const auto& entry : first)
|
||||
{
|
||||
CAPTURE(entry.first)
|
||||
const std::size_t expected = entry.second;
|
||||
CHECK(v[entry.first].get<std::size_t>() == expected);
|
||||
CHECK(v.at(entry.first).get<std::size_t>() == expected);
|
||||
CHECK(v.find(entry.first).value().get<std::size_t>() == expected);
|
||||
CHECK(v.value(entry.first, std::size_t{0}) == expected);
|
||||
CHECK(v.contains(entry.first));
|
||||
CHECK(v.count(entry.first) == occurrences[entry.first]);
|
||||
const std::string pointer = "/" + entry.first;
|
||||
CHECK(v[json::json_pointer(pointer)].get<std::size_t>() == expected);
|
||||
CHECK(v.at(json::json_pointer(pointer)).get<std::size_t>() == expected);
|
||||
CHECK(v.value(json::json_pointer(pointer), std::size_t{0}) == expected);
|
||||
CHECK(v.contains(json::json_pointer(pointer)));
|
||||
// materialize() and parse() keep the last value instead
|
||||
CHECK(j[entry.first].get<std::size_t>() == last.at(entry.first));
|
||||
CHECK(m[entry.first].get<std::size_t>() == last.at(entry.first));
|
||||
}
|
||||
CHECK(!v.contains("k"));
|
||||
CHECK(v["missing"].is_discarded());
|
||||
CHECK(v.materialize() == j);
|
||||
CHECK(v == j);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("colliding keys")
|
||||
{
|
||||
// the hash is not seeded, so keys that all land in one place must not
|
||||
// make the table build quadratic: such an object gets no table and is
|
||||
// searched linearly
|
||||
constexpr std::size_t members = 300;
|
||||
constexpr std::size_t table_size = 1024; // the table size for 300 members: the next power of two >= 600
|
||||
std::vector<std::string> colliding;
|
||||
std::vector<std::string> spread;
|
||||
for (std::uint64_t counter = 0; colliding.size() < members || spread.size() < members; ++counter)
|
||||
{
|
||||
std::string key(8, 'a');
|
||||
std::uint64_t x = counter;
|
||||
for (std::size_t i = 0; i < 8; ++i, x /= 26)
|
||||
{
|
||||
key[i] = static_cast<char>('a' + (x % 26));
|
||||
}
|
||||
const bool lands_in_slot_zero = (nlohmann::detail::view::key_hash(key.data(), key.size()) & (table_size - 1)) == 0;
|
||||
if (lands_in_slot_zero && colliding.size() < members)
|
||||
{
|
||||
colliding.push_back(key);
|
||||
}
|
||||
else if (!lands_in_slot_zero && spread.size() < members)
|
||||
{
|
||||
spread.push_back(key);
|
||||
}
|
||||
}
|
||||
|
||||
const auto make_text = [](const std::vector<std::string>& keys)
|
||||
{
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i % 10);
|
||||
}
|
||||
return text + "}";
|
||||
};
|
||||
const std::string colliding_text = make_text(colliding);
|
||||
const std::string spread_text = make_text(spread);
|
||||
|
||||
json_document with_collisions = json_document::parse(colliding_text);
|
||||
json_document without_collisions = json_document::parse(spread_text);
|
||||
for (const auto* pair :
|
||||
{
|
||||
&colliding, &spread
|
||||
})
|
||||
{
|
||||
const json_view v = (pair == &colliding ? with_collisions : without_collisions).root();
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
CAPTURE(i)
|
||||
CHECK(v[(*pair)[i]].get<std::size_t>() == i % 10);
|
||||
CHECK(v.at((*pair)[i]).get<std::size_t>() == i % 10);
|
||||
CHECK(v.find((*pair)[i]).key() == (*pair)[i]);
|
||||
CHECK(!v.contains((*pair)[i] + "x"));
|
||||
}
|
||||
CHECK(!v.contains("missing"));
|
||||
}
|
||||
CHECK(with_collisions.root() == json::parse(colliding_text));
|
||||
|
||||
// only the object with the spread keys got a table (both texts have the
|
||||
// same length, so the tables are the only difference)
|
||||
with_collisions.shrink_to_fit();
|
||||
without_collisions.shrink_to_fit();
|
||||
CHECK(without_collisions.memory_usage() >= with_collisions.memory_usage() + (table_size * sizeof(std::uint32_t)));
|
||||
}
|
||||
|
||||
SECTION("keys that differ in their last bytes are spread over the table")
|
||||
{
|
||||
// The hash must spread the usual families of keys (numbered names, hex
|
||||
// strings, dates, short names): a key farther than index_max_displacement
|
||||
// from its home slot would leave the object without a table. The keys
|
||||
// are placed with linear probing, as the table does.
|
||||
const auto max_displacement = [](const std::vector<std::string>& keys)
|
||||
{
|
||||
std::size_t slots = 16;
|
||||
while (slots < 2 * keys.size())
|
||||
{
|
||||
slots *= 2;
|
||||
}
|
||||
std::vector<bool> used(slots, false);
|
||||
std::size_t farthest = 0;
|
||||
for (const std::string& key : keys)
|
||||
{
|
||||
std::size_t i = nlohmann::detail::conditional_static_cast<std::size_t>(nlohmann::detail::view::key_hash(key.data(), key.size())) & (slots - 1);
|
||||
std::size_t distance = 0;
|
||||
while (used[i])
|
||||
{
|
||||
i = (i + 1) & (slots - 1);
|
||||
++distance;
|
||||
}
|
||||
used[i] = true;
|
||||
farthest = (std::max)(farthest, distance);
|
||||
}
|
||||
return farthest;
|
||||
};
|
||||
// the keys of a printf-like pattern with one %d, %0Nd or %0Nx, formatted with a stream
|
||||
// (a format string that is not a literal is a Flawfinder error)
|
||||
const auto numbered = [](const char* format, int count, int step)
|
||||
{
|
||||
const std::string pattern(format);
|
||||
const std::size_t percent = pattern.find('%');
|
||||
std::size_t conversion = percent + 1;
|
||||
std::size_t width = 0;
|
||||
while (pattern[conversion] >= '0' && pattern[conversion] <= '9')
|
||||
{
|
||||
width = (width * 10) + static_cast<std::size_t>(pattern[conversion] - '0');
|
||||
++conversion;
|
||||
}
|
||||
const bool hex = pattern[conversion] == 'x';
|
||||
std::vector<std::string> keys;
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
std::ostringstream key;
|
||||
key << pattern.substr(0, percent) << std::setfill('0') << std::setw(static_cast<int>(width))
|
||||
<< (hex ? std::hex : std::dec) << (i * step) << pattern.substr(conversion + 1);
|
||||
keys.push_back(key.str());
|
||||
}
|
||||
return keys;
|
||||
};
|
||||
for (const char* format :
|
||||
{"key%d", "%d", "item_%04d", "a%05d", "%03dxyz", "id%d", "user_%d", "%d_suffix", "0x%04x", "A%07d", "12345678%d", "1234567%d", "123456789%d",
|
||||
"configuration_parameter_value_%04d_extra", "2026-01-%02dT00:00:00Z"
|
||||
})
|
||||
{
|
||||
CAPTURE(format)
|
||||
CHECK(max_displacement(numbered(format, std::string(format).find("%02d") != std::string::npos ? 60 : 1000, 1)) < nlohmann::detail::view::index_max_displacement); // NOLINT(abseil-string-find-str-contains)
|
||||
CHECK(max_displacement(numbered(format, 600, 7)) < nlohmann::detail::view::index_max_displacement);
|
||||
}
|
||||
std::mt19937_64 rng(7); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
for (const std::size_t length :
|
||||
{
|
||||
std::size_t{1}, std::size_t{2}, std::size_t{3}, std::size_t{5}, std::size_t{7}, std::size_t{8}, std::size_t{9}, std::size_t{12}, std::size_t{16}, std::size_t{17}, std::size_t{33}
|
||||
})
|
||||
{
|
||||
CAPTURE(length)
|
||||
std::set<std::string> distinct;
|
||||
// (there are 26 and 676 keys of one and two letters)
|
||||
std::size_t wanted = 700;
|
||||
if (length == 1)
|
||||
{
|
||||
wanted = 26;
|
||||
}
|
||||
else if (length == 2)
|
||||
{
|
||||
wanted = 500;
|
||||
}
|
||||
while (distinct.size() < wanted)
|
||||
{
|
||||
std::string key(length, 'a');
|
||||
for (char& c : key)
|
||||
{
|
||||
c = static_cast<char>('a' + (rng() % 26));
|
||||
}
|
||||
distinct.insert(key);
|
||||
}
|
||||
CHECK(max_displacement(std::vector<std::string>(distinct.begin(), distinct.end())) < nlohmann::detail::view::index_max_displacement);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("keys of every length that differ in one byte")
|
||||
{
|
||||
// In a large object, a key is found by comparing its bytes with those of the member the
|
||||
// table points to: up to 16 bytes as two overlapping words, longer ones with memcmp.
|
||||
// Keys of every length from 0 to 40, and keys that differ from them in one byte.
|
||||
std::mt19937_64 rng(2026); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
std::vector<std::string> keys;
|
||||
std::set<std::string> distinct;
|
||||
for (std::size_t length = 0; length <= 40; ++length)
|
||||
{
|
||||
for (int copy = 0; copy < 5; ++copy)
|
||||
{
|
||||
std::string key(length, 'a');
|
||||
for (char& c : key)
|
||||
{
|
||||
c = static_cast<char>('a' + (rng() % 26));
|
||||
}
|
||||
if (distinct.insert(key).second)
|
||||
{
|
||||
keys.push_back(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i);
|
||||
}
|
||||
text += "}";
|
||||
REQUIRE(keys.size() >= 128); // (a table is built for 128 members)
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
CAPTURE(keys[i])
|
||||
CHECK(v[keys[i]].get<std::size_t>() == i);
|
||||
CHECK(v.at(keys[i]).get<std::size_t>() == i);
|
||||
CHECK(v.contains(keys[i]));
|
||||
for (std::size_t at = 0; at < keys[i].size(); ++at)
|
||||
{
|
||||
std::string other = keys[i];
|
||||
other[at] = other[at] == 'z' ? 'y' : 'z';
|
||||
CAPTURE(other)
|
||||
CHECK(v.contains(other) == (distinct.count(other) != 0));
|
||||
}
|
||||
CHECK(v.contains(keys[i] + "a") == (distinct.count(keys[i] + "a") != 0));
|
||||
if (!keys[i].empty())
|
||||
{
|
||||
CHECK(v.contains(keys[i].substr(1)) == (distinct.count(keys[i].substr(1)) != 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("shrink_to_fit releases the tables' spare capacity")
|
||||
{
|
||||
const auto make_text = [](int objects, int members)
|
||||
{
|
||||
std::string text = "[";
|
||||
for (int object = 0; object < objects; ++object)
|
||||
{
|
||||
text += object != 0 ? ",{" : "{";
|
||||
for (int i = 0, n = members + object; i < n; ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\":" + std::to_string(i);
|
||||
}
|
||||
text += '}';
|
||||
}
|
||||
return text + "]";
|
||||
};
|
||||
const std::string small_text = make_text(5, 150);
|
||||
const std::string big_text = make_text(40, 400);
|
||||
|
||||
// reading a big text, and then a small one, leaves the spare capacity
|
||||
// of the big one: shrink_to_fit() brings the document to the size of
|
||||
// one parsed from the small text alone
|
||||
json_document d = json_document::parse(big_text);
|
||||
const std::size_t big = d.memory_usage();
|
||||
d.read(small_text);
|
||||
CHECK(d.memory_usage() >= big);
|
||||
d.shrink_to_fit();
|
||||
json_document fresh = json_document::parse(small_text);
|
||||
fresh.shrink_to_fit();
|
||||
CHECK(d.memory_usage() == fresh.memory_usage());
|
||||
CHECK(d.memory_usage() < big / 2);
|
||||
CHECK(d.root() == json::parse(small_text));
|
||||
for (int object = 0; object < 5; ++object)
|
||||
{
|
||||
const json_view v = d.root()[static_cast<std::size_t>(object)];
|
||||
for (int i = 0, n = 150 + object; i < n; ++i)
|
||||
{
|
||||
CHECK(v[std::to_string(i)].get<int>() == i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("nested, reused, and in arrays")
|
||||
{
|
||||
std::string inner = "{";
|
||||
for (int i = 0; i < 300; ++i)
|
||||
{
|
||||
inner += (i != 0 ? ",\"m" : "\"m") + std::to_string(i) + "\":" + std::to_string(i);
|
||||
}
|
||||
inner += '}';
|
||||
const std::string text = "[" + inner + ",{\"x\":" + inner + "}," + inner + "]";
|
||||
json_document d = json_document::parse(text);
|
||||
CHECK(d.root()[0]["m299"].get<int>() == 299);
|
||||
CHECK(d.root()[1]["x"]["m150"].get<int>() == 150);
|
||||
CHECK(d.root()[2]["m0"].get<int>() == 0);
|
||||
const std::size_t with_index = d.memory_usage();
|
||||
d.read(std::string("{\"small\": 1}"));
|
||||
CHECK(d.root()["small"].get<int>() == 1);
|
||||
d.read(text);
|
||||
CHECK(d.root()[2]["m7"].get<int>() == 7);
|
||||
CHECK(d.memory_usage() >= with_index / 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json_view.hpp>
|
||||
using nlohmann::json;
|
||||
using nlohmann::ordered_json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::ordered_json_editable_document;
|
||||
using nlohmann::json_view;
|
||||
using nlohmann::ordered_json_document;
|
||||
using nlohmann::ordered_json_view;
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <map>
|
||||
#include <random>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// These tests were split off unit-json_view_dump.cpp, whose object file got too large for the MinGW
|
||||
// linker (see json_view_test_helpers.hpp). Like that file, this one is also built
|
||||
// with C++17, as it mentions JSON_HAS_CPP_17.
|
||||
|
||||
TEST_CASE("json_view dump: escaping, nesting, and output buffers")
|
||||
{
|
||||
SECTION("members in document order, all of them")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"b": 1, "a": 2, "b": 3})");
|
||||
CHECK(d.root().dump() == R"({"b":1,"a":2,"b":3})");
|
||||
CHECK(d.root().dump(1) == "{\n \"b\": 1,\n \"a\": 2,\n \"b\": 3\n}");
|
||||
// dump() writes all members, though the lookup finds the first
|
||||
CHECK(d.root()["b"].dump() == "1");
|
||||
}
|
||||
|
||||
SECTION("deep nesting")
|
||||
{
|
||||
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
|
||||
const json_document deep_document = json_document::parse(deep);
|
||||
CHECK(deep_document.root().dump() == deep);
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii: a character that needs an escape at each position of a string")
|
||||
{
|
||||
// string lengths around the width of the vector steps, in a string value, a key, and a value near the end of the text
|
||||
for (const char* const special :
|
||||
{
|
||||
"\xC3\xA9", "\xE4\xB8\xAD", "\xF0\x9F\x98\x80", "\x7F", "\\n", "\\u0001", "\\\"", "\\\\"
|
||||
})
|
||||
{
|
||||
for (std::size_t length = 0; length <= 40; ++length)
|
||||
{
|
||||
for (std::size_t position = 0; position <= length; ++position)
|
||||
{
|
||||
std::string content(length, 'a');
|
||||
content.insert(position, special);
|
||||
const std::array<std::string, 3> texts =
|
||||
{
|
||||
{
|
||||
"\"" + content + "\"",
|
||||
"[\"" + content + "\"]",
|
||||
"{\"" + content + "\":1}",
|
||||
}
|
||||
};
|
||||
for (const std::string& text : texts)
|
||||
{
|
||||
CAPTURE(text)
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii: every character of the Basic Multilingual Plane and some beyond")
|
||||
{
|
||||
// (the two- and three-byte sequences, including the ranges that need a check: E0, ED, EF BF BF)
|
||||
std::string text = "[";
|
||||
std::size_t in_chunk = 0;
|
||||
const auto append = [&](std::uint32_t cp)
|
||||
{
|
||||
if (cp < 0x80)
|
||||
{
|
||||
if (cp < 0x20 || cp == '"' || cp == '\\')
|
||||
{
|
||||
return;
|
||||
}
|
||||
text += static_cast<char>(cp);
|
||||
}
|
||||
else if (cp < 0x800)
|
||||
{
|
||||
text += static_cast<char>(0xC0 | (cp >> 6));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
else if (cp < 0x10000)
|
||||
{
|
||||
text += static_cast<char>(0xE0 | (cp >> 12));
|
||||
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
else
|
||||
{
|
||||
text += static_cast<char>(0xF0 | (cp >> 18));
|
||||
text += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
||||
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
};
|
||||
for (std::uint32_t cp = 0x20; cp < 0x10400; ++cp)
|
||||
{
|
||||
if (cp >= 0xD800 && cp < 0xE000)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (in_chunk == 0)
|
||||
{
|
||||
text += text.size() > 1 ? ",\"" : "\"";
|
||||
}
|
||||
append(cp);
|
||||
if (++in_chunk == 37)
|
||||
{
|
||||
text += "\"";
|
||||
in_chunk = 0;
|
||||
}
|
||||
}
|
||||
text += in_chunk != 0 ? "\"]" : "]";
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
CHECK(d.root().dump(2) == j.dump(2));
|
||||
}
|
||||
|
||||
SECTION("deep nesting, indented")
|
||||
{
|
||||
// indentations of up to and beyond the width of the writer's fixed-size copy
|
||||
for (const int depth :
|
||||
{
|
||||
1, 5, 20, 70, 300
|
||||
})
|
||||
{
|
||||
std::string text;
|
||||
for (int i = 0; i < depth; ++i)
|
||||
{
|
||||
text += i % 2 == 0 ? "[1," : R"({"k":2,"\u00e9":)";
|
||||
}
|
||||
text += "[]";
|
||||
for (int i = depth - 1; i >= 0; --i)
|
||||
{
|
||||
text += i % 2 == 0 ? "]" : "}";
|
||||
}
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
for (const int indent :
|
||||
{
|
||||
0, 1, 2, 3, 8
|
||||
})
|
||||
{
|
||||
CHECK(d.root().dump(indent) == j.dump(indent));
|
||||
CHECK(d.root().dump(indent, '\t') == j.dump(indent, '\t'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("the output buffer of a small value is small")
|
||||
{
|
||||
// an escaped key after the value: its node lies in the arena, so the
|
||||
// source extent of the value cannot be read from the next node
|
||||
const std::string big(100000, 'a');
|
||||
const std::string text = R"({"small":1,"list":[1,2,3],"k\n":")" + big + R"("})";
|
||||
const json_document d = json_document::parse(text);
|
||||
const auto small = d.root()["small"].dump();
|
||||
CHECK(small == "1");
|
||||
CHECK(small.capacity() < 4096);
|
||||
const auto list = d.root()["list"].dump();
|
||||
CHECK(list == "[1,2,3]");
|
||||
CHECK(list.capacity() < 4096);
|
||||
CHECK(d.root()["list"].dump(2).capacity() < 4096);
|
||||
|
||||
// the whole document and the large value are unaffected
|
||||
CHECK(d.root().dump() == ordered_json::parse(text).dump());
|
||||
CHECK(d.root()["k\n"].dump() == "\"" + big + "\"");
|
||||
}
|
||||
|
||||
SECTION("output that outgrows the estimate")
|
||||
{
|
||||
// ensure_ascii writes six bytes for each two-byte character
|
||||
std::string chars;
|
||||
for (int i = 0; i < 5000; ++i)
|
||||
{
|
||||
chars += "\xC3\xA9";
|
||||
}
|
||||
const json_document d = json_document::parse(R"({"a":")" + chars + R"(","k\n":1})");
|
||||
const json expected = json::parse("\"" + chars + "\"");
|
||||
CHECK(d.root()["a"].dump(-1, ' ', true) == expected.dump(-1, ' ', true));
|
||||
CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6));
|
||||
}
|
||||
|
||||
SECTION("the estimate for a value that is followed by many decoded strings")
|
||||
{
|
||||
// (the end of the value in the source is not known when the nodes after it have decoded strings)
|
||||
std::string text = "[7";
|
||||
for (int i = 0; i < 20; ++i)
|
||||
{
|
||||
text += R"(,"\n")";
|
||||
}
|
||||
text += ",8]";
|
||||
const json_document d = json_document::parse(text);
|
||||
CHECK(d.root()[0].dump() == "7");
|
||||
CHECK(d.root()[1].dump() == R"("\n")");
|
||||
CHECK(d.root().dump() == json::parse(text).dump());
|
||||
// the same with a container
|
||||
const json_document nested = json_document::parse(R"([[7],)" + text.substr(3));
|
||||
CHECK(nested.root()[0].dump() == "[7]");
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii with strings of the source, decoded strings, and edited strings")
|
||||
{
|
||||
// plain ASCII, non-ASCII, escaped (decoded), and mixed strings, as values and as keys;
|
||||
// the second text has a DEL, which ensure_ascii writes as an escape
|
||||
for (const char* const del :
|
||||
{"", "\x7F"
|
||||
})
|
||||
{
|
||||
const std::string text = std::string("{\"plain\":\"abc\",\"caf\xC3\xA9\":\"\xC3\xA9\xE2\x82\xAC\xF0\x9F\x98\x80\",")
|
||||
+ "\"esc\\n\\\"\":\"a\\tb\\u00e4\\ud83d\\ude00\",\"del\":\"a" + del + "b\",\"\":\"\"," // NOLINT(modernize-raw-string-literal)
|
||||
+ "\"list\":[\"x\",\"\xC3\xBC\",\"\\\\\",\"long ASCII string with more than thirty-two bytes in it\","
|
||||
+ "\"long string with more than thirty-two bytes and one non-ASCII byte \xC3\xB6\"],"
|
||||
+ "\"n\":[1,2.5,-0,true,null]}";
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(4, ' ', true) == j.dump(4, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
CHECK(d.root()["list"].dump(-1, ' ', true) == j["list"].dump(-1, ' ', true));
|
||||
CHECK(d.root()["del"].dump(-1, ' ', true) == j["del"].dump(-1, ' ', true));
|
||||
|
||||
// strings that an edit wrote (the source strings that they replace were plain)
|
||||
ordered_json_editable_document e = ordered_json_editable_document::parse(text);
|
||||
ordered_json k = j;
|
||||
e.set(e.root()["plain"], std::string("\xC3\xA4gypten"));
|
||||
k["plain"] = "\xC3\xA4gypten";
|
||||
e.set(e.root()["list"][0], std::string("ascii after edit"));
|
||||
k["list"][0] = "ascii after edit";
|
||||
e.set(e.root()["list"][1], std::string("\xC3\xA4"));
|
||||
k["list"][1] = "\xC3\xA4";
|
||||
e.set(e.root()["del"], std::string("tab\there"));
|
||||
k["del"] = "tab\there";
|
||||
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
|
||||
CHECK(e.root().dump(2, ' ', true) == k.dump(2, ' ', true));
|
||||
CHECK(e.root().dump() == k.dump());
|
||||
|
||||
// strings that were edited from or to ASCII text
|
||||
e.set(e.root()["list"][2], std::string("\xC3\xA4"));
|
||||
k["list"][2] = "\xC3\xA4";
|
||||
e.set(e.root()["list"][4], std::string("plain again"));
|
||||
k["list"][4] = "plain again";
|
||||
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("the length of the run that ensure_ascii copies")
|
||||
{
|
||||
// (dump() only asks for strings with a byte to escape, so a run that ends the string is checked here)
|
||||
for (const std::size_t length :
|
||||
{
|
||||
std::size_t{0}, std::size_t{1}, std::size_t{15}, std::size_t{16}, std::size_t{17}, std::size_t{31}, std::size_t{32}, std::size_t{48}, std::size_t{50}
|
||||
})
|
||||
{
|
||||
CAPTURE(length)
|
||||
// a run of printable characters, followed by a byte that is not copied and more text
|
||||
std::string text(length, 'a');
|
||||
text += "\"abcdefghijklmnopqrstuvwxyz";
|
||||
const auto* const bytes = reinterpret_cast<const unsigned char*>(text.data()); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
const auto* const end = bytes + text.size();
|
||||
CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length, end) == length);
|
||||
CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length + 1, end) == length);
|
||||
CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length + 5, end) == length);
|
||||
// the end of the buffer right after the run
|
||||
CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length, bytes + length) == length);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii with DEL or a non-ASCII character at every position")
|
||||
{
|
||||
// the check whether a string of the source has no byte >= 0x7F reads
|
||||
// eight bytes at a time and then the rest one by one
|
||||
for (std::size_t length = 1; length <= 20; ++length)
|
||||
{
|
||||
for (std::size_t pos = 0; pos < length; ++pos)
|
||||
{
|
||||
for (const char* const c :
|
||||
{
|
||||
"\x7F", "\x7E", "\xC3\xA4", "\xC3\xBF" // NOLINT(modernize-raw-string-literal)
|
||||
})
|
||||
{
|
||||
const std::string s = std::string(pos, 'a') + c + std::string(length - pos - 1, 'b');
|
||||
std::string text = "[\"";
|
||||
text.append(s).append("\",{\"").append(s).append("\":1}]");
|
||||
const json_document d = json_document::parse(text);
|
||||
const json j = json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(1, ' ', true) == j.dump(1, ' ', true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("streams and discarded views")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"a": [1, 2]})");
|
||||
std::ostringstream compact;
|
||||
compact << d.root();
|
||||
CHECK(compact.str() == R"({"a":[1,2]})");
|
||||
std::ostringstream pretty;
|
||||
pretty << std::setw(2) << std::setfill('.') << d.root() << d.root()["a"];
|
||||
CHECK(pretty.str() == "{\n..\"a\": [\n....1,\n....2\n..]\n}[1,2]");
|
||||
CHECK(json_view().dump() == json(json::value_t::discarded).dump());
|
||||
}
|
||||
}
|
||||
@@ -532,21 +532,25 @@ TEST_CASE("json_view edits: errors")
|
||||
|
||||
CHECK_THROWS_WITH_AS(d.set(other_editable.root(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&);
|
||||
CHECK_THROWS_WITH_AS(d.set(json_editable_view(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root["a"], "k", 1), "[json.exception.type_error.305] cannot use operator[] with a string argument with array", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root["o"], 0, 1), "[json.exception.type_error.305] cannot use operator[] with a numeric argument with object", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root["a"], 2, 1), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root["a"], -1, 1), "[json.exception.out_of_range.401] array index -1 is out of range", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.push_back(root["o"], 1), "[json.exception.type_error.308] cannot use push_back() with object", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.insert(root["n"], 0, 1), "[json.exception.type_error.309] cannot use insert() with number", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.insert(root["a"], 3, 1), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.erase(root["n"], "k"), "[json.exception.type_error.307] cannot use erase() with number", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.erase(root["o"], 0), "[json.exception.type_error.307] cannot use erase() with object", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.erase(root["a"], 2), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root["a"], "k", 1), json_view_test::message("type_error.305", "/a", 21, 27, "cannot use operator[] with a string argument with array").c_str(), json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root["o"], 0, 1), json_view_test::message("type_error.305", "/o", 6, 14, "cannot use operator[] with a numeric argument with object").c_str(), json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root["a"], 2, 1), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index 2 is out of range").c_str(), json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root["a"], -1, 1), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index -1 is out of range").c_str(), json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.push_back(root["o"], 1), json_view_test::message("type_error.308", "/o", 6, 14, "cannot use push_back() with object").c_str(), json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.insert(root["n"], 0, 1), json_view_test::message("type_error.309", "/n", 34, 35, "cannot use insert() with number").c_str(), json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.insert(root["a"], 3, 1), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index 3 is out of range").c_str(), json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.erase(root["n"], "k"), json_view_test::message("type_error.307", "/n", 34, 35, "cannot use erase() with number").c_str(), json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.erase(root["o"], 0), json_view_test::message("type_error.307", "/o", 6, 14, "cannot use erase() with object").c_str(), json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.erase(root["a"], 2), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index 2 is out of range").c_str(), json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("")), "[json.exception.out_of_range.405] JSON pointer has no parent", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("/missing/x")), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("/missing/x")), json_view_test::message("out_of_range.403", "", 0, 47, "key 'missing' not found").c_str(), json::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(d.set(json::json_pointer("/a/01"), 1), "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root, json_editable_view()), "[json.exception.type_error.302] type must be a value, but is discarded", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root, json::binary({1, 2})), "[json.exception.type_error.319] cannot store a binary value in a json_document", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(d.set(root, json(json::value_t::discarded)), "[json.exception.type_error.302] type must be a value, but is discarded", json::type_error&);
|
||||
// (an index that does not fit the document, in a null: basic_json would pad it with 4 Gi nulls)
|
||||
CHECK_THROWS_AS(d.set(json::json_pointer("/z/4294967295"), 1), json::out_of_range&);
|
||||
CHECK(root["z"].is_null());
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// invalid UTF-8 is rejected when it enters the document, with the error
|
||||
@@ -572,6 +576,19 @@ TEST_CASE("json_view edits: errors")
|
||||
static_cast<void>(other);
|
||||
}
|
||||
|
||||
TEST_CASE("json_view edits: a container that was added is replaced by a scalar")
|
||||
{
|
||||
// the view of an element that push_back() added is that of the new value itself, not of
|
||||
// the slot that links to it: replacing it by a scalar has to find the container it is in
|
||||
json_editable_document d = json_editable_document::parse(R"({"a": [1]})");
|
||||
d.push_back(d.root()["a"], json::array({2, 3}));
|
||||
d.push_back(d.root()["a"], json::object({{"k", json::array({4, 5})}}));
|
||||
d.set(d.root()["a"][1], 6);
|
||||
d.set(d.root()["a"][2]["k"], "s");
|
||||
CHECK(d.root().dump() == R"({"a":[1,6,{"k":"s"}]})");
|
||||
CHECK(d.root().materialize() == json::parse(R"({"a":[1,6,{"k":"s"}]})"));
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
TEST_CASE("json_view edits: the size of a text arena")
|
||||
{
|
||||
|
||||
@@ -39,6 +39,7 @@ using nlohmann::ordered_json_document;
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#endif
|
||||
|
||||
// NLOHMANN_JSON_SERIALIZE_ENUM uses a static std::pair; same() compares floating-point values on purpose
|
||||
@@ -276,6 +277,17 @@ TEST_CASE("json_view from_json")
|
||||
CHECK(true);
|
||||
}
|
||||
|
||||
SECTION("a map with enum keys from a view that is not an object")
|
||||
{
|
||||
// an object is read with the keys as the enum's names; any other
|
||||
// value is read as an array of pairs, like a map of any other key type
|
||||
const json_document d = json_document::parse(R"({"pairs": [["red", 1], ["green", 2]], "number": 1})");
|
||||
const json j = d.root().materialize();
|
||||
const json_view v = d.root();
|
||||
CHECK(same<std::map<color, int>>(v, j, "pairs"));
|
||||
CHECK((v["pairs"].get<std::map<color, int>>() == std::map<color, int> {{color::red, 1}, {color::green, 2}}));
|
||||
}
|
||||
|
||||
SECTION("std types: the same values as from the materialized value")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"ints": [3, 1, 2, 1], "strings": ["b", "a", "b"], "pairs": [[1, "x"], [2, "y"]],
|
||||
@@ -337,8 +349,7 @@ TEST_CASE("json_view from_json")
|
||||
CHECK(d.root().get<std::unordered_map<std::string, int>>().at("a") == 3);
|
||||
}
|
||||
|
||||
// (the exceptions of views have neither the path nor the byte range of the value)
|
||||
#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("std types: the same errors as from the materialized value")
|
||||
{
|
||||
const json_document d = json_document::parse(R"([[1, "x"], {"a": 1}, "text", [1], 5, null])");
|
||||
@@ -403,10 +414,10 @@ TEST_CASE("json_view from_json")
|
||||
CHECK(d.root()[4].get<strict_color>() == strict_color::red);
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
const json j = d.root().materialize();
|
||||
CHECK(exception_of_call([&] { static_cast<void>(d.root()[2].get<strict_color>()); }) == "[json.exception.out_of_range.410] enum value out of range for strict_color: \"purple\"");
|
||||
#if !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS
|
||||
CHECK(exception_of_call([&] { static_cast<void>(d.root()[2].get<strict_color>()); }) == exception_of_call([&] { static_cast<void>(j[2].get<strict_color>()); }));
|
||||
CHECK(exception_of_call([&] { static_cast<void>(d.root()[2].get<strict_color>()); }) == "[json.exception.out_of_range.410] enum value out of range for strict_color: \"purple\"");
|
||||
#endif
|
||||
CHECK(exception_of_call([&] { static_cast<void>(d.root()[2].get<strict_color>()); }) == exception_of_call([&] { static_cast<void>(j[2].get<strict_color>()); }));
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -443,6 +454,30 @@ TEST_CASE("json_view from_json")
|
||||
CHECK(d.root().materialize().get<point>().x == 3);
|
||||
}
|
||||
|
||||
SECTION("escaped keys: members are found by their decoded names")
|
||||
{
|
||||
// keys are decoded before they are compared, as by parse(); a lookup
|
||||
// that compares the raw bytes of the source would miss these members
|
||||
const json_document d = json_document::parse(R"([{"\u0078": 1, "y": 2}, {"V\u0061lue": 3, "Te\u0078t": "t"}, {"\u0062": "escaped"}, {"x": 1, "\u0078": 3, "y": 2}])");
|
||||
const json j = d.root().materialize();
|
||||
CHECK(d.root()[0].get<point>().x == 1);
|
||||
CHECK(d.root()[0].get<point>().y == 2);
|
||||
CHECK((d.root()[0].get<point>() == j[0].get<point>()));
|
||||
CHECK(d.root()[1].get<with_names>().value == 3);
|
||||
CHECK(d.root()[1].get<with_names>().text == "t");
|
||||
CHECK(d.root()[2].get<with_defaults>().b == "escaped");
|
||||
CHECK(d.root()[2].get<with_defaults>().a == 1);
|
||||
// a duplicate key written once plain and once escaped: still the first member
|
||||
CHECK(d.root()[3].get<point>().x == 1);
|
||||
CHECK(j[3].get<point>().x == 3);
|
||||
#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS
|
||||
// an escaped backslash is part of the decoded key: "x\\" is not "x"
|
||||
const json_document e = json_document::parse(R"({"x\\": 1, "y": 2})");
|
||||
CHECK(exception_of_call([&] { static_cast<void>(e.root().get<point>()); }) == exception_of_call([&] { static_cast<void>(e.root().materialize().get<point>()); }));
|
||||
CHECK(exception_of_call([&] { static_cast<void>(e.root().get<point>()); }) == "[json.exception.out_of_range.403] key 'x' not found");
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("editable documents and ordered_json")
|
||||
{
|
||||
json_editable_document e = json_editable_document::parse(R"({"x": 1, "y": 2})");
|
||||
@@ -452,11 +487,9 @@ TEST_CASE("json_view from_json")
|
||||
CHECK(o.root().get<shape>().center.y == 2);
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("errors are those of the same conversion of a json value")
|
||||
{
|
||||
// (with JSON_DIAGNOSTICS, those of json name the path of the value,
|
||||
// and with JSON_DIAGNOSTIC_POSITIONS its bytes)
|
||||
const json_document d = json_document::parse(R"([{"x": 1}, {"x": "1", "y": 2}, [1, 2], {"id": "7", "c": "red"}])");
|
||||
const json j = d.root().materialize();
|
||||
for (std::size_t i = 0; i < 3; ++i)
|
||||
@@ -469,5 +502,70 @@ TEST_CASE("json_view from_json")
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
namespace view_from_json
|
||||
{
|
||||
// a string_view member is not copied: it points into the source text or, for a
|
||||
// string with escape sequences, into the buffer of decoded strings of the document
|
||||
struct named_view
|
||||
{
|
||||
// initialized for GCC -Weffc++
|
||||
// NOLINTNEXTLINE(readability-redundant-member-init)
|
||||
std::string_view name{};
|
||||
int n = 0;
|
||||
NLOHMANN_DEFINE_TYPE_INTRUSIVE(named_view, name, n)
|
||||
};
|
||||
} // namespace view_from_json
|
||||
|
||||
TEST_CASE("json_view from_json: string_view members borrow from the document")
|
||||
{
|
||||
using view_from_json::named_view;
|
||||
|
||||
// whether all of s is inside the bytes of text
|
||||
const auto within = [](std::string_view s, std::string_view text)
|
||||
{
|
||||
const auto first = reinterpret_cast<std::uintptr_t>(text.data());
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(s.data());
|
||||
return begin >= first && begin + s.size() <= first + text.size();
|
||||
};
|
||||
|
||||
SECTION("a string without escapes points into the source text")
|
||||
{
|
||||
const std::string text = R"({"name": "hello", "n": 1})";
|
||||
const json_document d = json_document::parse(text);
|
||||
REQUIRE_FALSE(d.owns_source());
|
||||
const auto r = d.root().get<named_view>();
|
||||
CHECK(r.name == "hello");
|
||||
CHECK(r.n == 1);
|
||||
CHECK(within(r.name, text));
|
||||
CHECK(within(r.name, d.source()));
|
||||
}
|
||||
|
||||
SECTION("a string with escapes points into the document, not into the source text")
|
||||
{
|
||||
const std::string text = R"({"name": "a\nb", "n": 2})";
|
||||
json_document d = json_document::parse(text);
|
||||
REQUIRE_FALSE(d.owns_source());
|
||||
const auto r = d.root().get<named_view>();
|
||||
CHECK(r.name == "a\nb");
|
||||
CHECK(r.n == 2);
|
||||
CHECK_FALSE(within(r.name, text));
|
||||
// valid as long as the document is alive, even if the document is moved
|
||||
const json_document moved = std::move(d);
|
||||
CHECK(r.name == "a\nb");
|
||||
CHECK(moved.root().get<named_view>().name == r.name);
|
||||
}
|
||||
|
||||
SECTION("a document that owns its text keeps the members readable while it is alive")
|
||||
{
|
||||
const json_document d = json_document::parse(std::string(R"({"name": "owned", "n": 3})"));
|
||||
REQUIRE(d.owns_source());
|
||||
const auto r = d.root().get<named_view>();
|
||||
CHECK(r.name == "owned");
|
||||
CHECK(within(r.name, d.source()));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
DOCTEST_CLANG_SUPPRESS_WARNING_POP
|
||||
DOCTEST_GCC_SUPPRESS_WARNING_POP
|
||||
@@ -819,6 +819,13 @@ TEST_CASE("json_view images: errors")
|
||||
static_cast<void>(d);
|
||||
}) == prefix + "too short");
|
||||
|
||||
// an image that is moved into the document is checked as well
|
||||
CHECK(exception_of([&]
|
||||
{
|
||||
const json_document d = json_document::load(std::vector<std::uint8_t>(10, 0));
|
||||
static_cast<void>(d);
|
||||
}) == prefix + "too short");
|
||||
|
||||
std::vector<std::uint8_t> bad = image;
|
||||
bad[0] = 'X';
|
||||
CHECK(load_result(bad, image_check::full) == prefix + "unknown format");
|
||||
|
||||
@@ -0,0 +1,600 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json_view.hpp>
|
||||
using nlohmann::json;
|
||||
using nlohmann::ordered_json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::ordered_json_editable_document;
|
||||
using nlohmann::json_view;
|
||||
using nlohmann::ordered_json_document;
|
||||
using nlohmann::ordered_json_view;
|
||||
|
||||
#include "json_view_test_helpers.hpp"
|
||||
using json_view_test::generator;
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <map>
|
||||
#include <random>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// These tests were split off unit-json_view_dump.cpp, whose object file got too large for the MinGW
|
||||
// linker (see json_view_test_helpers.hpp). Like that file, this one is also built
|
||||
// with C++17, as it mentions JSON_HAS_CPP_17.
|
||||
|
||||
TEST_CASE("json_view comparison")
|
||||
{
|
||||
SECTION("equality of the values parse() produces")
|
||||
{
|
||||
generator g;
|
||||
std::vector<std::string> texts;
|
||||
for (int i = 0; i < 600; ++i)
|
||||
{
|
||||
std::string text;
|
||||
g.value(text, 0);
|
||||
texts.push_back(text);
|
||||
// the same value written differently: sorted keys, canonical numbers
|
||||
texts.push_back(json::parse(text).dump(1));
|
||||
}
|
||||
for (std::size_t i = 0; i + 2 < texts.size(); ++i)
|
||||
{
|
||||
for (std::size_t k = i; k < i + 3; ++k)
|
||||
{
|
||||
CAPTURE(texts[i])
|
||||
CAPTURE(texts[k])
|
||||
const json_document a = json_document::parse(texts[i]);
|
||||
const json_document b = json_document::parse(texts[k]);
|
||||
const json ja = json::parse(texts[i]);
|
||||
const json jb = json::parse(texts[k]);
|
||||
CHECK((a.root() == b.root()) == (ja == jb));
|
||||
CHECK((a.root() != b.root()) == (ja != jb));
|
||||
CHECK((a.root() == jb) == (ja == jb));
|
||||
CHECK((jb == a.root()) == (ja == jb));
|
||||
CHECK((a.root() != jb) == (ja != jb));
|
||||
CHECK((jb != a.root()) == (ja != jb));
|
||||
|
||||
// ordered_json compares members in order
|
||||
const ordered_json_document oa = ordered_json_document::parse(texts[i]);
|
||||
const ordered_json_document ob = ordered_json_document::parse(texts[k]);
|
||||
const ordered_json oja = ordered_json::parse(texts[i]);
|
||||
const ordered_json ojb = ordered_json::parse(texts[k]);
|
||||
CHECK((oa.root() == ob.root()) == (oja == ojb));
|
||||
CHECK((oa.root() == ojb) == (oja == ojb));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("numbers, duplicate keys, member order")
|
||||
{
|
||||
const auto same = [](const char* x, const char* y)
|
||||
{
|
||||
const json_document dx = json_document::parse(x);
|
||||
const json_document dy = json_document::parse(y);
|
||||
return dx.root() == dy.root();
|
||||
};
|
||||
CHECK(same("1", "1.0"));
|
||||
CHECK(same("[1, -1, 2.5]", "[1.0, -1.0, 25e-1]"));
|
||||
CHECK(!same("1", "1.5"));
|
||||
CHECK(same("18446744073709551615", "18446744073709551615"));
|
||||
CHECK(same(R"({"a": 1, "a": 2})", R"({"a": 2})"));
|
||||
CHECK(!same(R"({"a": 1, "a": 2})", R"({"a": 1})"));
|
||||
CHECK(same(R"({"a": 1, "b": 2})", R"({"b": 2, "a": 1})"));
|
||||
CHECK(!same(R"({"a": 1})", R"({"a": 1, "b": 2})"));
|
||||
CHECK(!same("[1, 2]", "[2, 1]"));
|
||||
CHECK(!same("\"a\"", "\"b\""));
|
||||
CHECK(same("\"\\u00e9\"", "\"\xc3\xa9\""));
|
||||
CHECK(!same("null", "false"));
|
||||
CHECK(!same("[]", "{}"));
|
||||
const ordered_json_document dup = ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})");
|
||||
const ordered_json_document last = ordered_json_document::parse(R"({"a": 3, "b": 2})");
|
||||
CHECK(dup.root() == last.root());
|
||||
// (== compares as parse() resolves duplicates, a lookup finds the first)
|
||||
CHECK(dup.root()["a"].materialize() == 1);
|
||||
CHECK(dup.root()["a"] != last.root()["a"]);
|
||||
const ordered_json_document ab = ordered_json_document::parse(R"({"a": 1, "b": 2})");
|
||||
const ordered_json_document ba = ordered_json_document::parse(R"({"b": 2, "a": 1})");
|
||||
CHECK(ab.root() != ba.root());
|
||||
|
||||
// discarded values compare as basic_json's do
|
||||
const json discarded(json::value_t::discarded);
|
||||
CHECK((json_view() == json_view()) == (discarded == discarded)); // NOLINT(readability-container-size-empty): operator== is tested
|
||||
CHECK((json_view() == discarded) == (discarded == discarded)); // NOLINT(readability-container-size-empty)
|
||||
const json_document null_document = json_document::parse("null");
|
||||
CHECK(!(json_view() == null_document.root())); // NOLINT(readability-container-size-empty)
|
||||
CHECK(!(null_document.root() == discarded));
|
||||
}
|
||||
|
||||
SECTION("deep nesting")
|
||||
{
|
||||
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
|
||||
const json_document a = json_document::parse(deep);
|
||||
const json_document b = json_document::parse(deep);
|
||||
CHECK(a.root() == b.root());
|
||||
CHECK(a.root() == json::parse(deep));
|
||||
const std::string other = std::string(100000, '[') + "1" + std::string(100000, ']');
|
||||
const json_document c = json_document::parse(other);
|
||||
CHECK(a.root() != c.root());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view large objects")
|
||||
{
|
||||
// objects with 128 members or more are looked up with a hash index
|
||||
for (const std::size_t members :
|
||||
{
|
||||
127u, 128u, 129u, 10000u
|
||||
})
|
||||
{
|
||||
CAPTURE(members)
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\\n" : "") + "\":" + std::to_string(i);
|
||||
}
|
||||
text += R"(,"":"empty key","k1":"a duplicate of an earlier key"})";
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
const json j = json::parse(text);
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
const std::string key = std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\n" : "");
|
||||
CHECK(v[key].get<std::size_t>() == i);
|
||||
CHECK(v.contains(key));
|
||||
CHECK(v.find(key).key() == key);
|
||||
CHECK(v.at(key).get<std::size_t>() == i);
|
||||
CHECK(!v.contains(key + "x"));
|
||||
}
|
||||
CHECK(v[""].get_string() == "empty key");
|
||||
CHECK(v["k1"].get<int>() == 1); // the first of duplicate keys, as for small objects
|
||||
CHECK(!v.contains("missing"));
|
||||
CHECK_THROWS_WITH_AS(v.at("missing"), json_view_test::message("out_of_range.403", "", 0, text.size(), "key 'missing' not found").c_str(), json::out_of_range&);
|
||||
CHECK(v == j);
|
||||
CHECK(v.materialize() == j);
|
||||
}
|
||||
|
||||
SECTION("duplicate keys: the first member wins, with and without a table")
|
||||
{
|
||||
// an object of `total` members: the keys "k0".."k<n-1>" in order, then
|
||||
// three keys repeated twice more (one copy in the middle, one at the
|
||||
// end), and two keys repeated once; the value of a member is its
|
||||
// position, so that the first member of a key can be told apart
|
||||
struct member
|
||||
{
|
||||
std::string key;
|
||||
std::size_t position;
|
||||
};
|
||||
const auto make_members = [](std::size_t total)
|
||||
{
|
||||
std::vector<std::string> keys;
|
||||
for (std::size_t i = 0; i + 8 < total; ++i)
|
||||
{
|
||||
keys.push_back("k" + std::to_string(i));
|
||||
}
|
||||
const std::size_t n = keys.size();
|
||||
const std::array<std::size_t, 3> triple = {{3, 17, n - 1}};
|
||||
const std::array<std::size_t, 2> twice = {{5, n / 2}};
|
||||
std::vector<std::string> ordered = keys;
|
||||
for (const std::size_t i : triple)
|
||||
{
|
||||
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 2), keys[i]);
|
||||
}
|
||||
for (const std::size_t i : twice)
|
||||
{
|
||||
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 3), keys[i]);
|
||||
}
|
||||
for (const std::size_t i : triple)
|
||||
{
|
||||
ordered.push_back(keys[i]);
|
||||
}
|
||||
std::vector<member> result;
|
||||
result.reserve(ordered.size());
|
||||
for (std::size_t i = 0; i < ordered.size(); ++i)
|
||||
{
|
||||
result.push_back({ordered[i], i});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// 100 and 127 members: no table; 128 members and more: a table
|
||||
for (const std::size_t total :
|
||||
{
|
||||
100u, 127u, 128u, 200u, 5000u
|
||||
})
|
||||
{
|
||||
CAPTURE(total)
|
||||
const std::vector<member> members = make_members(total);
|
||||
REQUIRE(members.size() >= total);
|
||||
REQUIRE(members.size() >= 100);
|
||||
std::string text = "{";
|
||||
std::map<std::string, std::size_t> first;
|
||||
std::map<std::string, std::size_t> last;
|
||||
std::map<std::string, std::size_t> occurrences;
|
||||
for (const member& m : members)
|
||||
{
|
||||
text += (text.size() > 1 ? ",\"" : "\"") + m.key + "\":" + std::to_string(m.position);
|
||||
first.insert({m.key, m.position});
|
||||
last[m.key] = m.position;
|
||||
++occurrences[m.key];
|
||||
}
|
||||
text += '}';
|
||||
REQUIRE(first.size() < members.size());
|
||||
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
const json j = json::parse(text);
|
||||
CHECK(v.size() == members.size()); // every occurrence is visited
|
||||
const json m = v.materialize();
|
||||
for (const auto& entry : first)
|
||||
{
|
||||
CAPTURE(entry.first)
|
||||
const std::size_t expected = entry.second;
|
||||
CHECK(v[entry.first].get<std::size_t>() == expected);
|
||||
CHECK(v.at(entry.first).get<std::size_t>() == expected);
|
||||
CHECK(v.find(entry.first).value().get<std::size_t>() == expected);
|
||||
CHECK(v.value(entry.first, std::size_t{0}) == expected);
|
||||
CHECK(v.contains(entry.first));
|
||||
CHECK(v.count(entry.first) == occurrences[entry.first]);
|
||||
const std::string pointer = "/" + entry.first;
|
||||
CHECK(v[json::json_pointer(pointer)].get<std::size_t>() == expected);
|
||||
CHECK(v.at(json::json_pointer(pointer)).get<std::size_t>() == expected);
|
||||
CHECK(v.value(json::json_pointer(pointer), std::size_t{0}) == expected);
|
||||
CHECK(v.contains(json::json_pointer(pointer)));
|
||||
// materialize() and parse() keep the last value instead
|
||||
CHECK(j[entry.first].get<std::size_t>() == last.at(entry.first));
|
||||
CHECK(m[entry.first].get<std::size_t>() == last.at(entry.first));
|
||||
}
|
||||
CHECK(!v.contains("k"));
|
||||
CHECK(v["missing"].is_discarded());
|
||||
CHECK(v.materialize() == j);
|
||||
CHECK(v == j);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("colliding keys")
|
||||
{
|
||||
// the hash is not seeded, so keys that all land in one place must not
|
||||
// make the table build quadratic: such an object gets no table and is
|
||||
// searched linearly
|
||||
constexpr std::size_t members = 300;
|
||||
constexpr std::size_t table_size = 1024; // the table size for 300 members: the next power of two >= 600
|
||||
std::vector<std::string> colliding;
|
||||
std::vector<std::string> spread;
|
||||
for (std::uint64_t counter = 0; colliding.size() < members || spread.size() < members; ++counter)
|
||||
{
|
||||
std::string key(8, 'a');
|
||||
std::uint64_t x = counter;
|
||||
for (std::size_t i = 0; i < 8; ++i, x /= 26)
|
||||
{
|
||||
key[i] = static_cast<char>('a' + (x % 26));
|
||||
}
|
||||
const bool lands_in_slot_zero = (nlohmann::detail::view::key_hash(key.data(), key.size()) & (table_size - 1)) == 0;
|
||||
if (lands_in_slot_zero && colliding.size() < members)
|
||||
{
|
||||
colliding.push_back(key);
|
||||
}
|
||||
else if (!lands_in_slot_zero && spread.size() < members)
|
||||
{
|
||||
spread.push_back(key);
|
||||
}
|
||||
}
|
||||
|
||||
const auto make_text = [](const std::vector<std::string>& keys)
|
||||
{
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i % 10);
|
||||
}
|
||||
return text + "}";
|
||||
};
|
||||
const std::string colliding_text = make_text(colliding);
|
||||
const std::string spread_text = make_text(spread);
|
||||
|
||||
json_document with_collisions = json_document::parse(colliding_text);
|
||||
json_document without_collisions = json_document::parse(spread_text);
|
||||
for (const auto* pair :
|
||||
{
|
||||
&colliding, &spread
|
||||
})
|
||||
{
|
||||
const json_view v = (pair == &colliding ? with_collisions : without_collisions).root();
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
CAPTURE(i)
|
||||
CHECK(v[(*pair)[i]].get<std::size_t>() == i % 10);
|
||||
CHECK(v.at((*pair)[i]).get<std::size_t>() == i % 10);
|
||||
CHECK(v.find((*pair)[i]).key() == (*pair)[i]);
|
||||
CHECK(!v.contains((*pair)[i] + "x"));
|
||||
}
|
||||
CHECK(!v.contains("missing"));
|
||||
}
|
||||
CHECK(with_collisions.root() == json::parse(colliding_text));
|
||||
|
||||
// only the object with the spread keys got a table (both texts have the
|
||||
// same length, so the tables are the only difference)
|
||||
with_collisions.shrink_to_fit();
|
||||
without_collisions.shrink_to_fit();
|
||||
CHECK(without_collisions.memory_usage() >= with_collisions.memory_usage() + (table_size * sizeof(std::uint32_t)));
|
||||
}
|
||||
|
||||
SECTION("a lookup of a missing key stops at the end of the probe window")
|
||||
{
|
||||
// Every slot of the first half of the table holds a key, each at its home slot, so
|
||||
// that the table is built; a key that is not in the object but whose home slot is
|
||||
// the first slot finds a full window of slots with other keys.
|
||||
constexpr std::size_t members = 128;
|
||||
constexpr std::size_t table_size = 256; // the table size for 128 members: the next power of two >= 256
|
||||
for (const std::size_t prefix_length :
|
||||
{
|
||||
std::size_t{0}, std::size_t{20}
|
||||
})
|
||||
{
|
||||
CAPTURE(prefix_length)
|
||||
std::vector<std::string> keys(members);
|
||||
std::vector<bool> taken(members, false);
|
||||
std::string missing;
|
||||
std::size_t found = 0;
|
||||
for (std::uint64_t counter = 0; found < members || missing.empty(); ++counter)
|
||||
{
|
||||
std::string key(prefix_length, 'p');
|
||||
std::uint64_t x = counter;
|
||||
for (std::size_t i = 0; i < 8; ++i, x /= 26)
|
||||
{
|
||||
key += static_cast<char>('a' + (x % 26));
|
||||
}
|
||||
const std::size_t slot = nlohmann::detail::conditional_static_cast<std::size_t>(nlohmann::detail::view::key_hash(key.data(), key.size())) & (table_size - 1);
|
||||
if (slot < members && !taken[slot])
|
||||
{
|
||||
taken[slot] = true;
|
||||
keys[slot] = key;
|
||||
++found;
|
||||
}
|
||||
else if (slot == 0 && missing.empty())
|
||||
{
|
||||
missing = key;
|
||||
}
|
||||
}
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i);
|
||||
}
|
||||
text += "}";
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
for (std::size_t i = 0; i < members; ++i)
|
||||
{
|
||||
CAPTURE(i)
|
||||
CHECK(v[keys[i]].get<std::size_t>() == i);
|
||||
}
|
||||
CHECK(!v.contains(missing));
|
||||
CHECK(v.find(missing) == v.end());
|
||||
CHECK(v.count(missing) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("keys that differ in their last bytes are spread over the table")
|
||||
{
|
||||
// The hash must spread the usual families of keys (numbered names, hex
|
||||
// strings, dates, short names): a key farther than index_max_displacement
|
||||
// from its home slot would leave the object without a table. The keys
|
||||
// are placed with linear probing, as the table does.
|
||||
const auto max_displacement = [](const std::vector<std::string>& keys)
|
||||
{
|
||||
std::size_t slots = 16;
|
||||
while (slots < 2 * keys.size())
|
||||
{
|
||||
slots *= 2;
|
||||
}
|
||||
std::vector<bool> used(slots, false);
|
||||
std::size_t farthest = 0;
|
||||
for (const std::string& key : keys)
|
||||
{
|
||||
std::size_t i = nlohmann::detail::conditional_static_cast<std::size_t>(nlohmann::detail::view::key_hash(key.data(), key.size())) & (slots - 1);
|
||||
std::size_t distance = 0;
|
||||
while (used[i])
|
||||
{
|
||||
i = (i + 1) & (slots - 1);
|
||||
++distance;
|
||||
}
|
||||
used[i] = true;
|
||||
farthest = (std::max)(farthest, distance);
|
||||
}
|
||||
return farthest;
|
||||
};
|
||||
// the keys of a printf-like pattern with one %d, %0Nd or %0Nx, formatted with a stream
|
||||
// (a format string that is not a literal is a Flawfinder error)
|
||||
const auto numbered = [](const char* format, int count, int step)
|
||||
{
|
||||
const std::string pattern(format);
|
||||
const std::size_t percent = pattern.find('%');
|
||||
std::size_t conversion = percent + 1;
|
||||
std::size_t width = 0;
|
||||
while (pattern[conversion] >= '0' && pattern[conversion] <= '9')
|
||||
{
|
||||
width = (width * 10) + static_cast<std::size_t>(pattern[conversion] - '0');
|
||||
++conversion;
|
||||
}
|
||||
const bool hex = pattern[conversion] == 'x';
|
||||
std::vector<std::string> keys;
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
std::ostringstream key;
|
||||
key << pattern.substr(0, percent) << std::setfill('0') << std::setw(static_cast<int>(width))
|
||||
<< (hex ? std::hex : std::dec) << (i * step) << pattern.substr(conversion + 1);
|
||||
keys.push_back(key.str());
|
||||
}
|
||||
return keys;
|
||||
};
|
||||
for (const char* format :
|
||||
{"key%d", "%d", "item_%04d", "a%05d", "%03dxyz", "id%d", "user_%d", "%d_suffix", "0x%04x", "A%07d", "12345678%d", "1234567%d", "123456789%d",
|
||||
"configuration_parameter_value_%04d_extra", "2026-01-%02dT00:00:00Z"
|
||||
})
|
||||
{
|
||||
CAPTURE(format)
|
||||
CHECK(max_displacement(numbered(format, std::string(format).find("%02d") != std::string::npos ? 60 : 1000, 1)) < nlohmann::detail::view::index_max_displacement); // NOLINT(abseil-string-find-str-contains)
|
||||
CHECK(max_displacement(numbered(format, 600, 7)) < nlohmann::detail::view::index_max_displacement);
|
||||
}
|
||||
std::mt19937_64 rng(7); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
for (const std::size_t length :
|
||||
{
|
||||
std::size_t{1}, std::size_t{2}, std::size_t{3}, std::size_t{5}, std::size_t{7}, std::size_t{8}, std::size_t{9}, std::size_t{12}, std::size_t{16}, std::size_t{17}, std::size_t{33}
|
||||
})
|
||||
{
|
||||
CAPTURE(length)
|
||||
std::set<std::string> distinct;
|
||||
// (there are 26 and 676 keys of one and two letters)
|
||||
std::size_t wanted = 700;
|
||||
if (length == 1)
|
||||
{
|
||||
wanted = 26;
|
||||
}
|
||||
else if (length == 2)
|
||||
{
|
||||
wanted = 500;
|
||||
}
|
||||
while (distinct.size() < wanted)
|
||||
{
|
||||
std::string key(length, 'a');
|
||||
for (char& c : key)
|
||||
{
|
||||
c = static_cast<char>('a' + (rng() % 26));
|
||||
}
|
||||
distinct.insert(key);
|
||||
}
|
||||
CHECK(max_displacement(std::vector<std::string>(distinct.begin(), distinct.end())) < nlohmann::detail::view::index_max_displacement);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("keys of every length that differ in one byte")
|
||||
{
|
||||
// In a large object, a key is found by comparing its bytes with those of the member the
|
||||
// table points to: up to 16 bytes as two overlapping words, longer ones with memcmp.
|
||||
// Keys of every length from 0 to 40, and keys that differ from them in one byte.
|
||||
std::mt19937_64 rng(2026); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
std::vector<std::string> keys;
|
||||
std::set<std::string> distinct;
|
||||
for (std::size_t length = 0; length <= 40; ++length)
|
||||
{
|
||||
for (int copy = 0; copy < 5; ++copy)
|
||||
{
|
||||
std::string key(length, 'a');
|
||||
for (char& c : key)
|
||||
{
|
||||
c = static_cast<char>('a' + (rng() % 26));
|
||||
}
|
||||
if (distinct.insert(key).second)
|
||||
{
|
||||
keys.push_back(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::string text = "{";
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i);
|
||||
}
|
||||
text += "}";
|
||||
REQUIRE(keys.size() >= 128); // (a table is built for 128 members)
|
||||
const json_document d = json_document::parse(text);
|
||||
const json_view v = d.root();
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
CAPTURE(keys[i])
|
||||
CHECK(v[keys[i]].get<std::size_t>() == i);
|
||||
CHECK(v.at(keys[i]).get<std::size_t>() == i);
|
||||
CHECK(v.contains(keys[i]));
|
||||
for (std::size_t at = 0; at < keys[i].size(); ++at)
|
||||
{
|
||||
std::string other = keys[i];
|
||||
other[at] = other[at] == 'z' ? 'y' : 'z';
|
||||
CAPTURE(other)
|
||||
CHECK(v.contains(other) == (distinct.count(other) != 0));
|
||||
}
|
||||
CHECK(v.contains(keys[i] + "a") == (distinct.count(keys[i] + "a") != 0));
|
||||
if (!keys[i].empty())
|
||||
{
|
||||
CHECK(v.contains(keys[i].substr(1)) == (distinct.count(keys[i].substr(1)) != 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("shrink_to_fit releases the tables' spare capacity")
|
||||
{
|
||||
const auto make_text = [](int objects, int members)
|
||||
{
|
||||
std::string text = "[";
|
||||
for (int object = 0; object < objects; ++object)
|
||||
{
|
||||
text += object != 0 ? ",{" : "{";
|
||||
for (int i = 0, n = members + object; i < n; ++i)
|
||||
{
|
||||
text += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\":" + std::to_string(i);
|
||||
}
|
||||
text += '}';
|
||||
}
|
||||
return text + "]";
|
||||
};
|
||||
const std::string small_text = make_text(5, 150);
|
||||
const std::string big_text = make_text(40, 400);
|
||||
|
||||
// reading a big text, and then a small one, leaves the spare capacity
|
||||
// of the big one: shrink_to_fit() brings the document to the size of
|
||||
// one parsed from the small text alone
|
||||
json_document d = json_document::parse(big_text);
|
||||
const std::size_t big = d.memory_usage();
|
||||
d.read(small_text);
|
||||
CHECK(d.memory_usage() >= big);
|
||||
d.shrink_to_fit();
|
||||
json_document fresh = json_document::parse(small_text);
|
||||
fresh.shrink_to_fit();
|
||||
CHECK(d.memory_usage() == fresh.memory_usage());
|
||||
CHECK(d.memory_usage() < big / 2);
|
||||
CHECK(d.root() == json::parse(small_text));
|
||||
for (int object = 0; object < 5; ++object)
|
||||
{
|
||||
const json_view v = d.root()[static_cast<std::size_t>(object)];
|
||||
for (int i = 0, n = 150 + object; i < n; ++i)
|
||||
{
|
||||
CHECK(v[std::to_string(i)].get<int>() == i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("nested, reused, and in arrays")
|
||||
{
|
||||
std::string inner = "{";
|
||||
for (int i = 0; i < 300; ++i)
|
||||
{
|
||||
inner += (i != 0 ? ",\"m" : "\"m") + std::to_string(i) + "\":" + std::to_string(i);
|
||||
}
|
||||
inner += '}';
|
||||
const std::string text = "[" + inner + ",{\"x\":" + inner + "}," + inner + "]";
|
||||
json_document d = json_document::parse(text);
|
||||
CHECK(d.root()[0]["m299"].get<int>() == 299);
|
||||
CHECK(d.root()[1]["x"]["m150"].get<int>() == 150);
|
||||
CHECK(d.root()[2]["m0"].get<int>() == 0);
|
||||
const std::size_t with_index = d.memory_usage();
|
||||
d.read(std::string("{\"small\": 1}"));
|
||||
CHECK(d.root()["small"].get<int>() == 1);
|
||||
d.read(text);
|
||||
CHECK(d.root()[2]["m7"].get<int>() == 7);
|
||||
CHECK(d.memory_usage() >= with_index / 2);
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@
|
||||
#endif
|
||||
|
||||
// the view's own macros do not leak
|
||||
#if defined(NLOHMANN_VIEW_LIKELY) || defined(NLOHMANN_VIEW_UNLIKELY) || defined(NLOHMANN_VIEW_ALWAYS_INLINE) || defined(NLOHMANN_VIEW_NOINLINE) \
|
||||
#if defined(NLOHMANN_VIEW_LIKELY) || defined(NLOHMANN_VIEW_UNLIKELY) || defined(NLOHMANN_VIEW_ALWAYS_INLINE) || defined(NLOHMANN_VIEW_NOINLINE) || defined(NLOHMANN_VIEW_NOCLONE) \
|
||||
|| defined(NLOHMANN_VIEW_NODISCARD) || defined(NLOHMANN_VIEW_THROW) || defined(NLOHMANN_VIEW_HAS_CPP_17) || defined(NLOHMANN_VIEW_LITTLE_ENDIAN) \
|
||||
|| defined(NLOHMANN_VIEW_REPEAT16)
|
||||
#error "json_view.hpp leaks a macro"
|
||||
|
||||
@@ -20,6 +20,7 @@ using nlohmann::ordered_json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::json_editable_document;
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
@@ -81,6 +82,112 @@ std::string range(const json& j)
|
||||
{
|
||||
return j.start_pos() == npos && j.end_pos() == npos ? "none" : std::to_string(j.start_pos()) + "-" + std::to_string(j.end_pos());
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
using json_view_test::exception_of_call;
|
||||
|
||||
// the exceptions about every value of v are those about the value at the same
|
||||
// place in j, a value of the whole document from parse() or materialize() (so
|
||||
// that its parents and positions are set); members whose key repeats are
|
||||
// skipped (the view finds the first one, j holds the last)
|
||||
template<typename View>
|
||||
void check_exceptions(const View& v, const json& j)
|
||||
{
|
||||
CAPTURE(j.dump())
|
||||
if (!v.is_number())
|
||||
{
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v.template get<int>()); }) == exception_of_call([&] { static_cast<void>(j.get<int>()); }));
|
||||
}
|
||||
if (!v.is_string())
|
||||
{
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v.template get<std::string>()); }) == exception_of_call([&] { static_cast<void>(j.get<std::string>()); }));
|
||||
}
|
||||
if (!v.is_array())
|
||||
{
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v.template get<std::vector<int>>()); }) == exception_of_call([&] { static_cast<void>(j.get<std::vector<int>>()); }));
|
||||
}
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v.at("missing")); }) == exception_of_call([&] { static_cast<void>(j.at("missing")); }));
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v.at(1000)); }) == exception_of_call([&] { static_cast<void>(j.at(1000)); }));
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v.at(json::json_pointer("/missing"))); }) == exception_of_call([&] { static_cast<void>(j.at(json::json_pointer("/missing"))); }));
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v.at(json::json_pointer("/-"))); }) == exception_of_call([&] { static_cast<void>(j.at(json::json_pointer("/-"))); }));
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v.value("k", 0)); }) == exception_of_call([&] { static_cast<void>(j.value("k", 0)); }));
|
||||
if (!v.is_object())
|
||||
{
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v["k"]); }) == exception_of_call([&] { static_cast<void>(j["k"]); }));
|
||||
// (without the context of basic_json: view iterators do not know the value they iterate over)
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v.begin().key()); }) == "[json.exception.invalid_iterator.207] cannot use key() for non-object iterators");
|
||||
}
|
||||
if (!v.is_array())
|
||||
{
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v[0]); }) == exception_of_call([&] { static_cast<void>(j[0]); }));
|
||||
}
|
||||
if (v.is_null())
|
||||
{
|
||||
CHECK(exception_of_call([&] { static_cast<void>(v.front()); }) == exception_of_call([&] { static_cast<void>(j.front()); }));
|
||||
}
|
||||
if (v.is_object())
|
||||
{
|
||||
for (auto it = v.begin(); it != v.end(); ++it)
|
||||
{
|
||||
const std::string key(it.key().data(), it.key().size());
|
||||
if (v.count(key) == 1)
|
||||
{
|
||||
check_exceptions(it.value(), j.at(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (v.is_array())
|
||||
{
|
||||
for (std::size_t i = 0; i < v.size(); ++i)
|
||||
{
|
||||
check_exceptions(v[i], j[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// as check_exceptions, for edits that fail (they change neither d nor j)
|
||||
void check_edit_exceptions(json_editable_document& d, const json_editable_document::view_type& v, json& j)
|
||||
{
|
||||
CAPTURE(j.dump())
|
||||
if (!v.is_object() && !v.is_null())
|
||||
{
|
||||
CHECK(exception_of_call([&] { d.set(v, "k", 1); }) == exception_of_call([&] { j["k"] = 1; }));
|
||||
CHECK(exception_of_call([&] { d.erase(v, "k"); }) == exception_of_call([&] { j.erase("k"); }));
|
||||
}
|
||||
if (!v.is_array() && !v.is_null())
|
||||
{
|
||||
CHECK(exception_of_call([&] { d.set(v, 0, 1); }) == exception_of_call([&] { j[0] = 1; }));
|
||||
CHECK(exception_of_call([&] { d.push_back(v, 1); }) == exception_of_call([&] { j.push_back(1); }));
|
||||
}
|
||||
if (!v.is_array())
|
||||
{
|
||||
CHECK(exception_of_call([&] { d.insert(v, 0, 1); }) == exception_of_call([&] { j.insert(j.cbegin(), 1); }));
|
||||
CHECK(exception_of_call([&] { d.erase(v, 0); }) == exception_of_call([&] { j.erase(0); }));
|
||||
}
|
||||
else
|
||||
{
|
||||
CHECK(exception_of_call([&] { d.erase(v, 1000); }) == exception_of_call([&] { j.erase(1000); }));
|
||||
}
|
||||
if (v.is_object())
|
||||
{
|
||||
for (auto it = v.begin(); it != v.end(); ++it)
|
||||
{
|
||||
const std::string key(it.key().data(), it.key().size());
|
||||
if (v.count(key) == 1)
|
||||
{
|
||||
check_edit_exceptions(d, it.value(), j.at(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (v.is_array())
|
||||
{
|
||||
for (std::size_t i = 0; i < v.size(); ++i)
|
||||
{
|
||||
check_edit_exceptions(d, v[i], j[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("json_view materialize() source positions")
|
||||
@@ -321,6 +428,31 @@ TEST_CASE("json_view materialize() source positions")
|
||||
CHECK(same_positions(m[std::string("a") + '\n'][2], j[std::string("a") + '\n'][2]));
|
||||
}
|
||||
|
||||
SECTION("a damaged text: the end of a container is not found")
|
||||
{
|
||||
// (the text of an image is not checked with image_check::none)
|
||||
const std::string array_text = "[1,2]";
|
||||
const std::vector<std::uint8_t> original = json_document::parse(array_text).save();
|
||||
const auto at = std::search(original.begin(), original.end(), array_text.begin(), array_text.end());
|
||||
REQUIRE(at != original.end());
|
||||
const auto closing = static_cast<std::size_t>(at - original.begin()) + array_text.size() - 1;
|
||||
// not a closing bracket, a slash that starts no comment, and the end of the text
|
||||
for (const char replacement :
|
||||
{
|
||||
'x', '/', ' '
|
||||
})
|
||||
{
|
||||
CAPTURE(replacement)
|
||||
std::vector<std::uint8_t> damaged = original;
|
||||
damaged[closing] = static_cast<std::uint8_t>(replacement);
|
||||
const json_document d = json_document::load(damaged, json_document::image_check::none);
|
||||
const json m = d.root().materialize();
|
||||
CHECK(range(m) == "none");
|
||||
CHECK(range(m[0]) == "1-2");
|
||||
CHECK(range(m[1]) == "3-4");
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("a decoded string's position must be within the text")
|
||||
{
|
||||
@@ -345,3 +477,167 @@ TEST_CASE("json_view materialize() source positions")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
TEST_CASE("json_view exceptions name the path and the bytes of the value")
|
||||
{
|
||||
SECTION("as those of basic_json")
|
||||
{
|
||||
const std::vector<std::string> texts =
|
||||
{
|
||||
"null", "true", "42", "\"s\"", "[]", "{}", "[1, [2, [3, []]]]", R"({"a": {"b": [1, {"c": null}]}, "d": "e"})",
|
||||
R"({"a/b": {"~": [1]}, "\u0041\n": {"x": "\t"}, "": [{}]})",
|
||||
R"({"a\u0000b": [1, "x"], "c": {"d\u0000": 2}})", // (the path of a key ends at a null byte, as in basic_json)
|
||||
R"([1 /* ] */, {"a": [] // }
|
||||
, "b": {} } , ])",
|
||||
"\n\t[\r\n 1 ,\n {\n \"a\" : [ ]\n }\n]\n",
|
||||
};
|
||||
for (const auto& text : texts)
|
||||
{
|
||||
CAPTURE(text)
|
||||
const json j = json::parse(text, nullptr, true, true, true);
|
||||
const json_document d = json_document::parse(text, true, true, true);
|
||||
check_exceptions(d.root(), j);
|
||||
json_editable_document e = json_editable_document::parse(text, true, true, true);
|
||||
check_exceptions(e.root(), j);
|
||||
json m = e.root().materialize();
|
||||
check_edit_exceptions(e, e.root(), m);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("generated documents")
|
||||
{
|
||||
json_view_test::generator gen;
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
std::string text;
|
||||
gen.value(text, 0);
|
||||
CAPTURE(text)
|
||||
const json_document d = json_document::parse(text);
|
||||
check_exceptions(d.root(), json::parse(text));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("edited documents: as those of materialize()")
|
||||
{
|
||||
const std::string text = R"({"a": [1, 2], "b": {"c": "\n", "d": true}, "e": [3, [4]], "f": "x"})";
|
||||
json_editable_document d = json_editable_document::parse(text);
|
||||
d.set(d.root()["f"], 5);
|
||||
d.push_back(d.root()["a"], json::parse(R"({"n": [null]})"));
|
||||
d.erase(d.root()["e"], 0);
|
||||
d.set(d.root()["b"], "c2", d.root()["e"]);
|
||||
d.insert(d.root()["e"], 0, "new");
|
||||
json m = d.root().materialize();
|
||||
check_exceptions(d.root(), m);
|
||||
check_edit_exceptions(d, d.root(), m);
|
||||
// (a value inside a new container: a path, but no bytes)
|
||||
std::string expected = "[json.exception.type_error.302] ";
|
||||
#if JSON_DIAGNOSTICS
|
||||
expected += "(/a/2/n/0) ";
|
||||
#endif
|
||||
expected += "type must be number, but is null";
|
||||
CHECK(exception_of_call([&] { static_cast<void>(d.root()["a"][2]["n"][0].get<int>()); }) == expected);
|
||||
}
|
||||
|
||||
SECTION("views of values that are no longer part of the document, and of no value")
|
||||
{
|
||||
const std::string expected_string = "[json.exception.type_error.302] type must be number, but is string";
|
||||
static_cast<void>(expected_string);
|
||||
// (a value that is not found has no path, and with it no bytes)
|
||||
{
|
||||
json_editable_document d = json_editable_document::parse(R"(["x", ["y"]])");
|
||||
const auto removed = d.root()[0];
|
||||
d.erase(d.root(), 0);
|
||||
#if JSON_DIAGNOSTICS
|
||||
CHECK(exception_of_call([&] { static_cast<void>(removed.get<int>()); }) == expected_string);
|
||||
#endif
|
||||
// (the root is a scalar now: the value cannot be in it)
|
||||
d.set(d.root(), 5);
|
||||
#if JSON_DIAGNOSTICS
|
||||
CHECK(exception_of_call([&] { static_cast<void>(removed.get<int>()); }) == expected_string);
|
||||
#endif
|
||||
CHECK(d.root().get<int>() == 5);
|
||||
}
|
||||
{
|
||||
json_editable_document d = json_editable_document::parse(R"(["x"])");
|
||||
const auto removed = d.root()[0];
|
||||
d.set(d.root(), 5);
|
||||
#if JSON_DIAGNOSTICS
|
||||
CHECK(exception_of_call([&] { static_cast<void>(removed.get<int>()); }) == expected_string);
|
||||
#endif
|
||||
}
|
||||
// a view of no value has no context
|
||||
CHECK(exception_of_call([&] { static_cast<void>(nlohmann::json_view().get<int>()); }) == "[json.exception.type_error.302] type must be number, but is discarded");
|
||||
}
|
||||
|
||||
SECTION("an array whose last element was replaced by a new container has no bytes")
|
||||
{
|
||||
json_editable_document d = json_editable_document::parse(R"({"a": [1, [2]], "b": 1})");
|
||||
d.set(d.root()["a"][1], json::array({3}));
|
||||
std::string expected = "[json.exception.type_error.302] ";
|
||||
#if JSON_DIAGNOSTICS
|
||||
expected += "(/a) ";
|
||||
#endif
|
||||
expected += "type must be number, but is array";
|
||||
CHECK(exception_of_call([&] { static_cast<void>(d.root()["a"].get<int>()); }) == expected);
|
||||
}
|
||||
|
||||
SECTION("deep documents with siblings after every nested container")
|
||||
{
|
||||
// the search for the path pushes and pops one frame per level (the
|
||||
// stack reallocates while it grows, and shrinks again at every
|
||||
// sibling): run under a sanitizer, a reference to a popped or moved
|
||||
// frame would show here
|
||||
std::string text = "7";
|
||||
for (int level = 0; level < 40; ++level)
|
||||
{
|
||||
text = R"([[1, [2]], )" + text + R"(, {"k": [3, {"l": [4]}]}, [5, [6, [7]]]])";
|
||||
}
|
||||
CAPTURE(text)
|
||||
const json j = json::parse(text);
|
||||
const json_document d = json_document::parse(text);
|
||||
check_exceptions(d.root(), j);
|
||||
json_editable_document e = json_editable_document::parse(text);
|
||||
e.set(e.root()[0][0], 9);
|
||||
check_exceptions(e.root(), e.root().materialize());
|
||||
}
|
||||
|
||||
SECTION("examples")
|
||||
{
|
||||
const std::string text = R"({"people": [{"name": "a", "age": 1}, {"name": "c", "age": "x"}], "o": {"k": 1}})";
|
||||
const json_document d = json_document::parse(text);
|
||||
std::string expected_age = "[json.exception.type_error.302] ";
|
||||
#if JSON_DIAGNOSTICS
|
||||
expected_age += "(/people/1/age) ";
|
||||
#endif
|
||||
expected_age += "(bytes 58-61) type must be number, but is string";
|
||||
CHECK(exception_of_call([&] { static_cast<void>(d.root()["people"][1]["age"].get<int>()); }) == expected_age);
|
||||
|
||||
std::string expected_zz = "[json.exception.out_of_range.403] ";
|
||||
#if JSON_DIAGNOSTICS
|
||||
expected_zz += "(/people/0) ";
|
||||
#endif
|
||||
expected_zz += "(bytes 12-35) key 'zz' not found";
|
||||
CHECK(exception_of_call([&] { static_cast<void>(d.root().at(json::json_pointer("/people/0/zz"))); }) == expected_zz);
|
||||
|
||||
// the root has no path, and with JSON_DIAGNOSTICS then no bytes either (as basic_json)
|
||||
std::string expected_missing = "[json.exception.out_of_range.403] ";
|
||||
#if !JSON_DIAGNOSTICS
|
||||
expected_missing += "(bytes 0-79) ";
|
||||
#endif
|
||||
expected_missing += "key 'missing' not found";
|
||||
CHECK(exception_of_call([&] { static_cast<void>(d.root().at("missing")); }) == expected_missing);
|
||||
}
|
||||
|
||||
SECTION("duplicate keys: the value the view finds")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"x": "1", "x": "22"})");
|
||||
std::string expected = "[json.exception.type_error.302] ";
|
||||
#if JSON_DIAGNOSTICS
|
||||
expected += "(/x) ";
|
||||
#endif
|
||||
expected += "(bytes 6-9) type must be number, but is string";
|
||||
CHECK(exception_of_call([&] { static_cast<void>(d.root()["x"].get<int>()); }) == expected);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -12,7 +12,10 @@
|
||||
using nlohmann::json;
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
TEST_CASE("tests on very large JSONs")
|
||||
{
|
||||
@@ -140,6 +143,56 @@ TEST_CASE("tests on deeply nested JSONs")
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("leaf values of every type below the bound of the recursive descent")
|
||||
{
|
||||
// the elements and members of the containers below the bound are
|
||||
// copied one by one, whatever their type
|
||||
const json leaves = json::array(
|
||||
{
|
||||
nullptr, true, false, -1, std::uint64_t{1}, 1.5, "text", json::binary({1, 2, 3})
|
||||
});
|
||||
const json members =
|
||||
{
|
||||
{"null", nullptr}, {"boolean", true}, {"integer", -1}, {"unsigned", std::uint64_t{1}},
|
||||
{"float", 1.5}, {"string", "text"}, {"binary", json::binary({1, 2, 3})}
|
||||
};
|
||||
|
||||
for (const bool objects :
|
||||
{
|
||||
false, true
|
||||
})
|
||||
{
|
||||
CAPTURE(objects)
|
||||
|
||||
json j = objects ? members : leaves;
|
||||
for (std::size_t i = 0; i < 300; ++i)
|
||||
{
|
||||
json wrapper = objects ? json::object() : json::array();
|
||||
if (objects)
|
||||
{
|
||||
wrapper["a"] = std::move(j);
|
||||
}
|
||||
else
|
||||
{
|
||||
wrapper.push_back(std::move(j));
|
||||
}
|
||||
j = std::move(wrapper);
|
||||
}
|
||||
|
||||
const json copy(j); // NOLINT(performance-unnecessary-copy-initialization): the copy is what is tested
|
||||
|
||||
const json* current = ©
|
||||
for (std::size_t i = 0; i < 300; ++i)
|
||||
{
|
||||
current = objects ? ¤t->at("a") : ¤t->front();
|
||||
}
|
||||
|
||||
// the innermost value is shallow: it is safe to compare
|
||||
CHECK(*current == (objects ? members : leaves));
|
||||
CHECK(current->size() == (objects ? members.size() : leaves.size()));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("a value that is deep in one place only")
|
||||
{
|
||||
json j = json::object();
|
||||
|
||||
@@ -495,3 +495,206 @@ TEST_CASE("locale changes during a single dump() (#5709 item 3)")
|
||||
// fix #5597 made on the parser side
|
||||
CHECK(buf.data == "[\"" + padding + "\",1234.5]");
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// a token whose first data() call switches LC_NUMERIC, so that the locale
|
||||
// changes between convert_float_locale_aware() looking up the decimal point
|
||||
// and strtod reading the token (the race of #5198, made deterministic)
|
||||
struct locale_switching_token
|
||||
{
|
||||
using value_type = char;
|
||||
|
||||
locale_switching_token(std::string text_, const char* switch_to_)
|
||||
: text(std::move(text_)), switch_to(switch_to_)
|
||||
{}
|
||||
|
||||
std::size_t size() const
|
||||
{
|
||||
return text.size();
|
||||
}
|
||||
|
||||
char& operator[](std::size_t i)
|
||||
{
|
||||
return text[i];
|
||||
}
|
||||
|
||||
char* data()
|
||||
{
|
||||
if (!switched)
|
||||
{
|
||||
switched = std::setlocale(LC_NUMERIC, switch_to) != nullptr;
|
||||
}
|
||||
return &text[0];
|
||||
}
|
||||
|
||||
std::string text;
|
||||
const char* switch_to;
|
||||
bool switched = false;
|
||||
};
|
||||
|
||||
// set LC_NUMERIC to the first of @a names that is usable and has a decimal
|
||||
// point of @a min_size bytes at least (and at most @a max_size)
|
||||
bool use_decimal_point_locale(const std::vector<const char*>& names, std::size_t min_size, std::size_t max_size, const char*& used)
|
||||
{
|
||||
for (const char* name : names)
|
||||
{
|
||||
if (std::setlocale(LC_NUMERIC, name) == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::size_t size = std::string(std::localeconv()->decimal_point).size();
|
||||
if (size >= min_size && size <= max_size)
|
||||
{
|
||||
used = name;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// convert a token with convert_float_locale_aware(); the token must be given
|
||||
// back as it was
|
||||
template<typename FloatType>
|
||||
FloatType convert_locale_aware(const std::string& text)
|
||||
{
|
||||
std::string token = text;
|
||||
const std::size_t dot = token.find('.');
|
||||
FloatType value{};
|
||||
nlohmann::detail::convert_float_locale_aware(token, dot, value);
|
||||
CHECK(token == text);
|
||||
return value;
|
||||
}
|
||||
|
||||
template<typename FloatType>
|
||||
void check_locale_aware_conversions()
|
||||
{
|
||||
CHECK(convert_locale_aware<FloatType>("3.5") == FloatType(3.5));
|
||||
CHECK(convert_locale_aware<FloatType>("-0.25") == FloatType(-0.25));
|
||||
CHECK(convert_locale_aware<FloatType>("1.25e2") == FloatType(125));
|
||||
// no decimal point at all
|
||||
CHECK(convert_locale_aware<FloatType>("12e1") == FloatType(120));
|
||||
CHECK(convert_locale_aware<FloatType>("7") == FloatType(7));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("convert_float_locale_aware")
|
||||
{
|
||||
const std::vector<const char*> single_byte_names = {"de_DE.UTF-8", "de_DE", "fr_FR.UTF-8", "fr_FR", "ru_RU.UTF-8"};
|
||||
const std::vector<const char*> multi_byte_names = {"ar_EG.UTF-8", "ar_SA.UTF-8", "fa_IR.UTF-8", "ps_AF.UTF-8", "ar_EG", "fa_IR"};
|
||||
const char* used = nullptr;
|
||||
|
||||
SECTION("decimal point '.'")
|
||||
{
|
||||
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
||||
check_locale_aware_conversions<float>();
|
||||
check_locale_aware_conversions<double>();
|
||||
check_locale_aware_conversions<long double>();
|
||||
}
|
||||
|
||||
SECTION("single-byte decimal point other than '.'")
|
||||
{
|
||||
if (use_decimal_point_locale(single_byte_names, 1, 1, used) && std::localeconv()->decimal_point[0] != '.')
|
||||
{
|
||||
CAPTURE(used)
|
||||
check_locale_aware_conversions<float>();
|
||||
check_locale_aware_conversions<double>();
|
||||
check_locale_aware_conversions<long double>();
|
||||
}
|
||||
else
|
||||
{
|
||||
MESSAGE("no locale with a single-byte decimal point other than '.' is usable");
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("multi-byte decimal point")
|
||||
{
|
||||
if (use_decimal_point_locale(multi_byte_names, 2, 8, used))
|
||||
{
|
||||
CAPTURE(used)
|
||||
check_locale_aware_conversions<float>();
|
||||
check_locale_aware_conversions<double>();
|
||||
check_locale_aware_conversions<long double>();
|
||||
}
|
||||
else
|
||||
{
|
||||
MESSAGE("no locale with a multi-byte decimal point is usable");
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("a token that is not converted completely")
|
||||
{
|
||||
// the locale does not change, so there is nothing to retry: the
|
||||
// value up to the garbage is kept
|
||||
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
||||
std::string token = "1.5x";
|
||||
double value = 0;
|
||||
nlohmann::detail::convert_float_locale_aware(token, token.find('.'), value);
|
||||
CHECK(value == 1.5);
|
||||
CHECK(token == "1.5x");
|
||||
}
|
||||
|
||||
SECTION("the locale changes before the conversion")
|
||||
{
|
||||
if (!use_decimal_point_locale(single_byte_names, 1, 1, used) || std::localeconv()->decimal_point[0] == '.')
|
||||
{
|
||||
MESSAGE("no locale with a single-byte decimal point other than '.' is usable");
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::string comma_locale = used;
|
||||
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
||||
|
||||
// "C" -> comma locale: strtod stops at the '.', so the conversion is repeated
|
||||
{
|
||||
locale_switching_token token("1.5", comma_locale.c_str());
|
||||
double value = 0;
|
||||
nlohmann::detail::convert_float_locale_aware(token, 1, value);
|
||||
if (token.switched)
|
||||
{
|
||||
CHECK(value == 1.5);
|
||||
CHECK(token.text == "1.5");
|
||||
}
|
||||
}
|
||||
|
||||
// the same for the other floating-point types
|
||||
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
||||
{
|
||||
locale_switching_token token("2.25", comma_locale.c_str());
|
||||
float value = 0;
|
||||
nlohmann::detail::convert_float_locale_aware(token, 1, value);
|
||||
if (token.switched)
|
||||
{
|
||||
CHECK(value == 2.25f);
|
||||
}
|
||||
}
|
||||
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
||||
{
|
||||
locale_switching_token token("2.25", comma_locale.c_str());
|
||||
long double value = 0;
|
||||
nlohmann::detail::convert_float_locale_aware(token, 1, value);
|
||||
if (token.switched)
|
||||
{
|
||||
CHECK(value == 2.25L);
|
||||
}
|
||||
}
|
||||
|
||||
// comma locale -> multi-byte decimal point
|
||||
const char* multi_byte = nullptr;
|
||||
if (use_decimal_point_locale(multi_byte_names, 2, 8, multi_byte))
|
||||
{
|
||||
REQUIRE(std::setlocale(LC_NUMERIC, comma_locale.c_str()) != nullptr);
|
||||
locale_switching_token token("1.5", multi_byte);
|
||||
double value = 0;
|
||||
nlohmann::detail::convert_float_locale_aware(token, 1, value);
|
||||
if (token.switched)
|
||||
{
|
||||
CHECK(value == 1.5);
|
||||
CHECK(token.text == "1.5");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CHECK(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ using nlohmann::json;
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Test suite for verifying MessagePack handling with std::byte input
|
||||
@@ -96,4 +97,33 @@ TEST_CASE("MessagePack with std::byte")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// JSON text (not MessagePack) as std::byte input: the error position is
|
||||
// computed from the bytes consumed so far
|
||||
TEST_CASE("JSON text with std::byte")
|
||||
{
|
||||
const auto to_bytes = [](const std::string & text)
|
||||
{
|
||||
std::vector<std::byte> bytes;
|
||||
for (const char c : text)
|
||||
{
|
||||
bytes.push_back(static_cast<std::byte>(c));
|
||||
}
|
||||
return bytes;
|
||||
};
|
||||
|
||||
SECTION("valid text")
|
||||
{
|
||||
const json j = json::parse(to_bytes("[1,\n2]"));
|
||||
CHECK(j == json({1, 2}));
|
||||
CHECK(json::accept(to_bytes("[1,\n2]")));
|
||||
}
|
||||
|
||||
SECTION("invalid text")
|
||||
{
|
||||
CHECK_FALSE(json::accept(to_bytes("[1,\n2,")));
|
||||
CHECK_THROWS_WITH_AS(json::parse(to_bytes("[1,\n2,")),
|
||||
"[json.exception.parse_error.101] parse error at line 2, column 3: syntax error while parsing value - unexpected end of input; expected '[', '{', or a literal", json::parse_error&);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -27,12 +27,15 @@ using nlohmann::json;
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <list>
|
||||
#include <set>
|
||||
#include "make_test_data_available.hpp"
|
||||
#include "round_trip_corpus.hpp"
|
||||
#include "test_utils.hpp"
|
||||
#include "sax_countdown.hpp"
|
||||
#include "sax_event_loggers.hpp"
|
||||
using utils::SaxCountdown;
|
||||
using utils::SaxEventLogger;
|
||||
|
||||
|
||||
TEST_CASE("MessagePack")
|
||||
@@ -2212,3 +2215,99 @@ TEST_CASE("MessagePack large strings and binaries (chunked reader)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("MessagePack arrays of scalars from contiguous and non-contiguous input")
|
||||
{
|
||||
// contiguous input reads the integers, floats, booleans, and nils an
|
||||
// array begins with in one loop, other input value by value; both must
|
||||
// give the same events, value, or error for the complete document and for
|
||||
// every truncation of it, and stop at every event a SAX handler rejects
|
||||
const std::vector<std::uint8_t> msgpack =
|
||||
{
|
||||
0x83, // map(3)
|
||||
0xA1, 'a', // "a"
|
||||
0xDC, 0x00, 0x16, // array 16 (22)
|
||||
0x00, 0x7F, 0xCC, 0xFF, 0xCD, 0x01, 0x00, // 0, 127, 255, 256
|
||||
0xCE, 0x00, 0x01, 0x00, 0x00, // 65536
|
||||
0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 18446744073709551615
|
||||
0xFF, 0xE0, 0xD0, 0x80, 0xD1, 0xFF, 0x00, // -1, -32, -128, -256
|
||||
0xD2, 0xFF, 0xFE, 0xFF, 0xFF, // -65537
|
||||
0xD3, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // -9223372036854775808
|
||||
0xC2, 0xC3, 0xC0, // false, true, nil
|
||||
0xCA, 0x3F, 0xC0, 0x00, 0x00, // 1.5 (float 32)
|
||||
0xCB, 0xBF, 0xB9, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9A, // -0.1
|
||||
0xA1, 'x', // "x" (ends the loop)
|
||||
0x01, // 1
|
||||
0xCB, 0x40, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 2.5
|
||||
0xD9, 0x01, 'y', // "y" (str 8)
|
||||
0xC0, // nil
|
||||
0xA1, 'b', // "b"
|
||||
0x93, // fixarray(3)
|
||||
0x92, 0xCB, 0x3F, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // [1.25,
|
||||
0xCB, 0xC0, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // -3.75]
|
||||
0x92, 0xCB, 0x3F, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // [1.25,
|
||||
0xCB, 0xC0, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // -3.75]
|
||||
0x90, // []
|
||||
0xA1, 'c', // "c"
|
||||
0x94, 0x80, 0x01, 0xC4, 0x01, 0x05, 0x91, 0x03 // [{}, 1, bin 8, [3]]
|
||||
};
|
||||
|
||||
const json j = json::from_msgpack(msgpack);
|
||||
CHECK(j["a"].size() == 22);
|
||||
CHECK(j["a"][5] == 18446744073709551615u);
|
||||
CHECK(j["a"][7] == -32);
|
||||
CHECK(j["a"][11] == (std::numeric_limits<std::int64_t>::min)());
|
||||
CHECK(j["a"][15] == 1.5);
|
||||
CHECK(j["a"][16] == -0.1);
|
||||
CHECK(j["a"][17] == "x");
|
||||
CHECK(j["b"] == json::parse("[[1.25, -3.75], [1.25, -3.75], []]"));
|
||||
CHECK(j["c"][2].is_binary());
|
||||
|
||||
for (std::size_t length = 0; length <= msgpack.size(); ++length)
|
||||
{
|
||||
CAPTURE(length)
|
||||
const std::vector<std::uint8_t> contiguous(msgpack.begin(), msgpack.begin() + static_cast<std::ptrdiff_t>(length));
|
||||
const std::list<std::uint8_t> bytewise(contiguous.begin(), contiguous.end());
|
||||
|
||||
CHECK(json::from_msgpack(contiguous, true, false).dump() == json::from_msgpack(bytewise.begin(), bytewise.end(), true, false).dump());
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
std::string contiguous_error;
|
||||
std::string bytewise_error;
|
||||
try
|
||||
{
|
||||
json _;
|
||||
_ = json::from_msgpack(contiguous);
|
||||
}
|
||||
catch (const json::parse_error& e)
|
||||
{
|
||||
contiguous_error = e.what();
|
||||
}
|
||||
try
|
||||
{
|
||||
json _;
|
||||
_ = json::from_msgpack(bytewise.begin(), bytewise.end());
|
||||
}
|
||||
catch (const json::parse_error& e)
|
||||
{
|
||||
bytewise_error = e.what();
|
||||
}
|
||||
CHECK(contiguous_error == bytewise_error);
|
||||
#endif
|
||||
|
||||
SaxEventLogger from_contiguous;
|
||||
SaxEventLogger from_bytewise;
|
||||
CHECK(json::sax_parse(contiguous, &from_contiguous, json::input_format_t::msgpack)
|
||||
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::msgpack));
|
||||
CHECK(from_contiguous.events == from_bytewise.events);
|
||||
}
|
||||
|
||||
for (int events = 0; events <= 60; ++events)
|
||||
{
|
||||
CAPTURE(events)
|
||||
const std::list<std::uint8_t> bytewise(msgpack.begin(), msgpack.end());
|
||||
SaxCountdown from_contiguous(events);
|
||||
SaxCountdown from_bytewise(events);
|
||||
CHECK(json::sax_parse(msgpack, &from_contiguous, json::input_format_t::msgpack)
|
||||
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::msgpack));
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ using nlohmann::ordered_json;
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
TEST_CASE("ordered_json")
|
||||
{
|
||||
@@ -515,3 +516,88 @@ TEST_CASE("parsing random trees of arrays and objects")
|
||||
CHECK(json::parse(text).dump() == expected.dump());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("object_key_index")
|
||||
{
|
||||
// the hash table of the DOM parser finds the position of a key in an
|
||||
// ordered_json object; the parser creates it for exactly 64 elements, but
|
||||
// it is correct for any number of elements it is given
|
||||
using nlohmann::detail::object_key_index;
|
||||
const std::size_t not_found = object_key_index::npos;
|
||||
|
||||
for (const std::size_t initial :
|
||||
{
|
||||
std::size_t{0}, std::size_t{10}, std::size_t{64}, std::size_t{65}, std::size_t{300}
|
||||
})
|
||||
{
|
||||
CAPTURE(initial)
|
||||
|
||||
// the hashes are not distinct on purpose: elements with the same
|
||||
// hash are told apart by the caller
|
||||
const auto hash_of = [](std::size_t position) -> std::size_t
|
||||
{
|
||||
// truncated to 32 bits where std::size_t is that small
|
||||
return static_cast<std::size_t>((position % 50) * 0x9E3779B97F4A7C15ull);
|
||||
};
|
||||
|
||||
std::vector<std::size_t> hashes;
|
||||
for (std::size_t i = 0; i < initial; ++i)
|
||||
{
|
||||
hashes.push_back(hash_of(i));
|
||||
}
|
||||
object_key_index index(std::move(hashes));
|
||||
|
||||
// the elements that are there
|
||||
for (std::size_t i = 0; i < initial; ++i)
|
||||
{
|
||||
CHECK(index.find(hash_of(i), [i](std::size_t position)
|
||||
{
|
||||
return position == i;
|
||||
}) == i);
|
||||
}
|
||||
CHECK(index.find(hash_of(1), [](std::size_t /*position*/)
|
||||
{
|
||||
return false;
|
||||
}) == not_found);
|
||||
|
||||
// elements that are added later, until the table grows
|
||||
for (std::size_t i = initial; i < initial + 600; ++i)
|
||||
{
|
||||
index.add(hash_of(i));
|
||||
CHECK(index.find(hash_of(i), [i](std::size_t position)
|
||||
{
|
||||
return position == i;
|
||||
}) == i);
|
||||
}
|
||||
CHECK(index.find(hash_of(initial), [initial](std::size_t position)
|
||||
{
|
||||
return position == initial;
|
||||
}) == initial);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("JSON pointers on ordered_json")
|
||||
{
|
||||
using pointer = ordered_json::json_pointer;
|
||||
|
||||
SECTION("operator[] inserts a missing key at the end")
|
||||
{
|
||||
ordered_json j = {{"z", 1}, {"x", {{"y", 2}}}};
|
||||
|
||||
j[pointer("/a")] = 3;
|
||||
j[pointer("/x/b")] = 4;
|
||||
|
||||
CHECK(j == ordered_json({{"z", 1}, {"x", {{"y", 2}, {"b", 4}}}, {"a", 3}}));
|
||||
CHECK(j.dump() == R"({"z":1,"x":{"y":2,"b":4},"a":3})");
|
||||
}
|
||||
|
||||
SECTION("unflatten of a flattened object whose keys are not sorted")
|
||||
{
|
||||
// the prefixes /a and /c denote arrays; a key of /a follows one of /c
|
||||
const ordered_json flat = {{"/a/0/p", 1}, {"/c/0/x", 2}, {"/a/0/q", 3}, {"/a/1/p", 4}};
|
||||
const ordered_json expected = {{"a", {{{"p", 1}, {"q", 3}}, {{"p", 4}}}}, {"c", {{{"x", 2}}}}};
|
||||
|
||||
CHECK(flat.unflatten() == expected);
|
||||
CHECK(expected.flatten().unflatten() == expected);
|
||||
}
|
||||
}
|
||||
@@ -23,11 +23,13 @@ using nlohmann::json;
|
||||
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
|
||||
#endif
|
||||
|
||||
#include <array>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <list>
|
||||
#include <limits>
|
||||
#include <cstdio>
|
||||
#include <utility>
|
||||
#include "make_test_data_available.hpp"
|
||||
#include "test_utils.hpp"
|
||||
|
||||
@@ -1519,6 +1521,76 @@ TEST_CASE("regression tests 1")
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("operator[](json_pointer) continues with the member that emplace() found")
|
||||
{
|
||||
// see the section above: the lookup does not find "k", but emplace()
|
||||
// finds "x" and the pointer continues with the member of "x". Here
|
||||
// the member is a container, so the pointer can go on below it. Only
|
||||
// the outermost object may be a copy (with restarted timestamps): the
|
||||
// value of "x" is moved in, so that its own lookups work.
|
||||
const my_json orig = my_json::parse(R"({"x":null})");
|
||||
const auto make = [&orig]()
|
||||
{
|
||||
my_json::object_t top = orig.get_ref<const my_json::object_t&>();
|
||||
top.at("x") = my_json::parse(R"({"a":{"b":1},"n":null,"arr":[10,20],"objs":[{"m":5},{"m":6}]})");
|
||||
return my_json(std::move(top));
|
||||
};
|
||||
|
||||
SECTION("existing members")
|
||||
{
|
||||
// emplace() registers the key "k" in the object, so that every
|
||||
// pointer needs a new object to reach the member of "x" again
|
||||
const std::array<std::pair<const char*, const char*>, 4> pointers = {{{"/k/a/b", "1"}, {"/k/arr/1", "20"}, {"/k/n", "null"}, {"/k/objs/1/m", "6"}}};
|
||||
for (const auto& pointer : pointers)
|
||||
{
|
||||
CAPTURE(pointer.first)
|
||||
my_json j = make();
|
||||
CHECK(j[my_json::json_pointer(pointer.first)].dump() == pointer.second);
|
||||
CHECK(j.size() == 1);
|
||||
CHECK(j["x"].size() == 4);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("a missing member of an object")
|
||||
{
|
||||
my_json j = make();
|
||||
#ifdef JSON_NO_THREAD_LOCAL
|
||||
// (set by the user, or by the library for clang with MinGW)
|
||||
// the objects below the outermost one are not copied by their
|
||||
// object_t here, but member by member, which keeps their
|
||||
// timestamps correct: "c" is not mistaken for "b" and is inserted
|
||||
CHECK(j[my_json::json_pointer("/k/a/c/d")].is_null());
|
||||
CHECK(j["x"]["a"].dump() == R"({"b":1,"c":{"d":null}})");
|
||||
#else
|
||||
// the timestamp of "c" is that of "b" again, so that emplace()
|
||||
// finds the member "b", a number, which has no member "d"
|
||||
CHECK_THROWS_AS(j[my_json::json_pointer("/k/a/c/d")], my_json::out_of_range&);
|
||||
CHECK(j["x"]["a"].dump() == R"({"b":1})");
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("a null value is converted to an object")
|
||||
{
|
||||
my_json j = make();
|
||||
j[my_json::json_pointer("/k/n/c")] = 3;
|
||||
CHECK(j["x"]["n"].dump() == R"({"c":3})");
|
||||
}
|
||||
|
||||
SECTION("the end of an array")
|
||||
{
|
||||
my_json j = make();
|
||||
j[my_json::json_pointer("/k/arr/-")] = 30;
|
||||
CHECK(j["x"]["arr"].dump() == "[10,20,30]");
|
||||
}
|
||||
|
||||
SECTION("an index past the end of an array")
|
||||
{
|
||||
my_json j = make();
|
||||
j[my_json::json_pointer("/k/arr/3")] = 40;
|
||||
CHECK(j["x"]["arr"].dump() == "[10,20,null,40]");
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("issue #977 - Assigning between different json types")
|
||||
{
|
||||
foo_json lj = ns::foo{3};
|
||||
|
||||
@@ -441,6 +441,10 @@ TEST_CASE("regression tests 2")
|
||||
|
||||
float_json j2 = {1000.0, 2000.0, 3000.0};
|
||||
CHECK(float_json::from_ubjson(float_json::to_ubjson(j2, true, true)) == j2);
|
||||
|
||||
// half-precision floats of both signs are converted to the float type
|
||||
CHECK(float_json::from_cbor(std::vector<std::uint8_t> {0xF9, 0xC0, 0x00}) == -2.0f);
|
||||
CHECK(float_json::from_cbor(std::vector<std::uint8_t> {0xF9, 0x3C, 0x00}) == 1.0f);
|
||||
}
|
||||
|
||||
SECTION("issue #1045 - Using STL algorithms with JSON containers with expected results?")
|
||||
|
||||
@@ -23,6 +23,7 @@ using nlohmann::json;
|
||||
#include <cstdint>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -248,6 +249,51 @@ TEST_CASE_TEMPLATE("serialization for extreme integer values", T, int32_t, uint3
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("integers of every digit count are written like std::to_string")
|
||||
{
|
||||
// dump_integer() writes up to 16 digits in two groups of eight and 17 to
|
||||
// 20 digits as one to four digits and two groups of eight
|
||||
std::vector<std::uint64_t> values;
|
||||
std::uint64_t power = 1;
|
||||
for (int digits = 1; digits <= 20; ++digits)
|
||||
{
|
||||
values.push_back(power); // 1 followed by zeros
|
||||
values.push_back(power + 1);
|
||||
values.push_back((power * 2) - 1);
|
||||
if (digits < 20)
|
||||
{
|
||||
values.push_back((power * 10) - 1); // all 9s
|
||||
values.push_back((power * 10) - power); // 9 followed by zeros
|
||||
power *= 10;
|
||||
}
|
||||
}
|
||||
values.push_back((std::numeric_limits<std::uint64_t>::max)());
|
||||
values.push_back(static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()));
|
||||
values.push_back(static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()) + 1);
|
||||
std::uint64_t state = 0x9E3779B97F4A7C15u;
|
||||
for (int i = 0; i < 20000; ++i)
|
||||
{
|
||||
state = (state * 6364136223846793005u) + 1442695040888963407u;
|
||||
values.push_back(state >> (state % 61)); // every bit length
|
||||
}
|
||||
|
||||
for (const auto v : values)
|
||||
{
|
||||
CAPTURE(v)
|
||||
CHECK(json(v).dump() == std::to_string(v));
|
||||
if (v <= static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()))
|
||||
{
|
||||
const auto s = static_cast<std::int64_t>(v);
|
||||
CHECK(json(s).dump() == std::to_string(s));
|
||||
CHECK(json(-s).dump() == std::to_string(-s));
|
||||
}
|
||||
}
|
||||
CHECK(json((std::numeric_limits<std::int64_t>::min)()).dump() == "-9223372036854775808");
|
||||
|
||||
// in a container, so the digits are followed by other output
|
||||
CHECK(json::array({std::uint64_t{18446744073709551615u}, std::int64_t{-1234567890123456789}, 1}).dump() == "[18446744073709551615,-1234567890123456789,1]");
|
||||
}
|
||||
|
||||
TEST_CASE("dump with binary values")
|
||||
{
|
||||
auto binary = json::binary({1, 2, 3, 4});
|
||||
|
||||
@@ -19,11 +19,13 @@
|
||||
using nlohmann::json;
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <istream>
|
||||
#include <limits>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <streambuf>
|
||||
@@ -93,6 +95,33 @@ class chunked_streambuf : public std::streambuf
|
||||
std::size_t m_unbuffered;
|
||||
};
|
||||
|
||||
#if TEST_STREAM_BULK_SCAN
|
||||
// A streambuf that claims a get area of several gigabytes, of which only the
|
||||
// first bytes exist. It is only used to move the read position of the adapter,
|
||||
// never to read beyond them, so that skipping more than gbump() can bump at
|
||||
// once can be tested without allocating that much.
|
||||
class oversized_get_area_streambuf : public std::streambuf
|
||||
{
|
||||
public:
|
||||
explicit oversized_get_area_streambuf(std::size_t claimed_size)
|
||||
{
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
|
||||
setg(m_data.data(), m_data.data(), m_data.data() + claimed_size);
|
||||
}
|
||||
|
||||
// Give the get area back its real size, so that the adapter may read the
|
||||
// character at the read position when it is destroyed (with
|
||||
// JSON_PRECISE_STREAM_POSITION, it consumes the last character returned).
|
||||
void restore_get_area()
|
||||
{
|
||||
setg(m_data.data(), m_data.data(), m_data.data() + m_data.size());
|
||||
}
|
||||
|
||||
private:
|
||||
std::array<char, 8> m_data{{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}};
|
||||
};
|
||||
#endif
|
||||
|
||||
// how a test reads from the stream
|
||||
enum class read_mode
|
||||
{
|
||||
@@ -433,6 +462,35 @@ TEST_CASE("parsing from a std::istream scans the get area in bulk")
|
||||
ia.copy_token(token, false);
|
||||
CHECK(token.empty());
|
||||
}
|
||||
|
||||
SECTION("skipping more characters than gbump() takes at once")
|
||||
{
|
||||
// gbump() takes an int, but a get area can be larger
|
||||
const std::size_t claimed = sizeof(std::size_t) < 8 ? 0 : std::size_t{3} << 30u;
|
||||
oversized_get_area_streambuf buf(claimed);
|
||||
if (claimed == 0 || static_cast<std::size_t>(buf.in_avail()) != claimed)
|
||||
{
|
||||
// a 32-bit size_t, or a standard library whose streambuf stores
|
||||
// the size of the get area in an int (MSVC)
|
||||
MESSAGE("a get area larger than an int is not supported");
|
||||
}
|
||||
else
|
||||
{
|
||||
std::istream is(&buf);
|
||||
nlohmann::detail::input_stream_adapter ia(is);
|
||||
CHECK(ia.get_character() == 'a');
|
||||
CHECK(ia.bulk_remaining() == claimed - 1);
|
||||
|
||||
const std::size_t skipped = static_cast<std::size_t>((std::numeric_limits<int>::max)()) + 10;
|
||||
ia.bulk_skip(skipped);
|
||||
CHECK(ia.bulk_remaining() == claimed - 1 - skipped);
|
||||
|
||||
ia.bulk_skip(ia.bulk_remaining());
|
||||
CHECK(ia.bulk_remaining() == 0);
|
||||
|
||||
buf.restore_get_area();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
SECTION("SAX parsing from a stream")
|
||||
|
||||
@@ -928,3 +928,49 @@ TEST_CASE("short decimals")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("decimals of up to 17 digits")
|
||||
{
|
||||
// write_decimal() lays out digits * 10^exp as format_buffer() does; to_chars()
|
||||
// calls it for the doubles whose digits write_shortest() does not convert at
|
||||
// once, and the digits may be as many as 17
|
||||
const auto written = [](std::uint64_t digits, int exp) -> std::string
|
||||
{
|
||||
std::array<char, 64> buf{}; // (up to 41 bytes are written)
|
||||
char* const end = nlohmann::detail::dtoa_impl::write_decimal(buf.data(), digits, exp);
|
||||
return {buf.data(), end};
|
||||
};
|
||||
|
||||
SECTION("fixed notation")
|
||||
{
|
||||
CHECK(written(1, 0) == "1.0");
|
||||
CHECK(written(1234, -2) == "12.34");
|
||||
CHECK(written(123, 2) == "12300.0");
|
||||
CHECK(written(50000000000u, -1) == "5000000000.0");
|
||||
CHECK(written(100000000u, -8) == "1.0");
|
||||
CHECK(written(12345678u, -12) == "1.2345678e-05");
|
||||
CHECK(written(1234, 11) == "123400000000000.0");
|
||||
// the point moves left of the first digit
|
||||
CHECK(written(5, -3) == "0.005");
|
||||
CHECK(written(12345678901234567u, -20) == "0.00012345678901234567");
|
||||
}
|
||||
|
||||
SECTION("exponent notation")
|
||||
{
|
||||
CHECK(written(5, -5) == "5e-05");
|
||||
CHECK(written(1234, 12) == "1.234e+15");
|
||||
CHECK(written(5, -324) == "5e-324");
|
||||
}
|
||||
|
||||
SECTION("17 digits")
|
||||
{
|
||||
CHECK(written(12345678901234567u, -16) == "1.2345678901234567");
|
||||
CHECK(written(12345678901234567u, -19) == "0.0012345678901234567");
|
||||
CHECK(written(12345678901234567u, 5) == "1.2345678901234567e+21");
|
||||
CHECK(written(99999999999999999u, -17) == "0.99999999999999999");
|
||||
CHECK(written(10000000000000001u, -16) == "1.0000000000000001");
|
||||
// trailing zeros are not written
|
||||
CHECK(written(10000000000000000u, -17) == "0.1");
|
||||
CHECK(written(12000000000000000u, -15) == "12.0");
|
||||
}
|
||||
}
|
||||
@@ -21,12 +21,15 @@ using nlohmann::json;
|
||||
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <list>
|
||||
#include <set>
|
||||
#include "make_test_data_available.hpp"
|
||||
#include "round_trip_corpus.hpp"
|
||||
#include "test_utils.hpp"
|
||||
#include "sax_countdown.hpp"
|
||||
#include "sax_event_loggers.hpp"
|
||||
using utils::SaxCountdown;
|
||||
using utils::SaxEventLogger;
|
||||
|
||||
|
||||
TEST_CASE("UBJSON")
|
||||
@@ -1670,6 +1673,31 @@ TEST_CASE("UBJSON")
|
||||
return true;
|
||||
});
|
||||
CHECK_THROWS_AS(scp2.start_array((std::numeric_limits<std::size_t>::max)() - 1), json::out_of_range&);
|
||||
|
||||
// without exceptions, the error is reported to the caller
|
||||
json l;
|
||||
nlohmann::detail::json_sax_dom_callback_parser<json, decltype(nlohmann::detail::input_adapter(v_ubjson))> scp3(l, [](int /*unused*/, json::parse_event_t /*unused*/, const json& /*unused*/) noexcept
|
||||
{
|
||||
return true;
|
||||
}, false);
|
||||
CHECK_FALSE(scp3.start_array((std::numeric_limits<std::size_t>::max)() - 1));
|
||||
CHECK(scp3.is_errored());
|
||||
}
|
||||
|
||||
SECTION("array with a known size, read without exceptions")
|
||||
{
|
||||
std::vector<uint8_t> const v_ubjson = {'[', '#', 'i', 2, 'i', 1, 'i', 2};
|
||||
json j;
|
||||
nlohmann::detail::json_sax_dom_parser<json, decltype(nlohmann::detail::input_adapter(v_ubjson))> sax(j, false);
|
||||
CHECK(json::sax_parse(v_ubjson, &sax, json::input_format_t::ubjson));
|
||||
CHECK(j == json({1, 2}));
|
||||
|
||||
// the readers reject a size this large before they announce
|
||||
// it, so it can only reach start_array() directly
|
||||
json k;
|
||||
nlohmann::detail::json_sax_dom_parser<json, decltype(nlohmann::detail::input_adapter(v_ubjson))> sax2(k, false);
|
||||
CHECK_FALSE(sax2.start_array((std::numeric_limits<std::size_t>::max)() - 1));
|
||||
CHECK(sax2.is_errored());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3400,3 +3428,72 @@ TEST_CASE("UBJSON optimized container type marker is validated for count 0")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("UBJSON optimized arrays from contiguous and non-contiguous input")
|
||||
{
|
||||
// contiguous input reads the numbers of an array with a type marker in
|
||||
// one loop, other input number by number; both must give the same events,
|
||||
// value, or error for the complete document and for every truncation of
|
||||
// it, and stop at every event a SAX handler rejects
|
||||
json document =
|
||||
{
|
||||
{"u8", {0, 200, 255}},
|
||||
{"i8", {-5, 100, -128}},
|
||||
{"i16", {-30000, 1000, 30000}},
|
||||
{"i32", {-2000000000, 5, 70000}},
|
||||
{"i64", {-9000000000000000000LL, 1, 5000000000LL}},
|
||||
{"f64", {1.5, -0.1, 2.5}},
|
||||
{"pairs", {{1.25, -3.75}, {1.25, -3.75}, json::array()}},
|
||||
{"mixed", {1, "a", 2.5, nullptr}},
|
||||
};
|
||||
const std::vector<std::uint8_t> encoded = json::to_ubjson(document, true, true);
|
||||
CHECK(json::from_ubjson(encoded) == document);
|
||||
|
||||
for (std::size_t length = 0; length <= encoded.size(); ++length)
|
||||
{
|
||||
CAPTURE(length)
|
||||
const std::vector<std::uint8_t> contiguous(encoded.begin(), encoded.begin() + static_cast<std::ptrdiff_t>(length));
|
||||
const std::list<std::uint8_t> bytewise(contiguous.begin(), contiguous.end());
|
||||
|
||||
CHECK(json::from_ubjson(contiguous, true, false).dump() == json::from_ubjson(bytewise.begin(), bytewise.end(), true, false).dump());
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
std::string contiguous_error;
|
||||
std::string bytewise_error;
|
||||
try
|
||||
{
|
||||
json _;
|
||||
_ = json::from_ubjson(contiguous);
|
||||
}
|
||||
catch (const json::exception& e)
|
||||
{
|
||||
contiguous_error = e.what();
|
||||
}
|
||||
try
|
||||
{
|
||||
json _;
|
||||
_ = json::from_ubjson(bytewise.begin(), bytewise.end());
|
||||
}
|
||||
catch (const json::exception& e)
|
||||
{
|
||||
bytewise_error = e.what();
|
||||
}
|
||||
CHECK(contiguous_error == bytewise_error);
|
||||
#endif
|
||||
|
||||
SaxEventLogger from_contiguous;
|
||||
SaxEventLogger from_bytewise;
|
||||
CHECK(json::sax_parse(contiguous, &from_contiguous, json::input_format_t::ubjson)
|
||||
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::ubjson));
|
||||
CHECK(from_contiguous.events == from_bytewise.events);
|
||||
}
|
||||
|
||||
for (int events = 0; events <= 80; ++events)
|
||||
{
|
||||
CAPTURE(events)
|
||||
const std::list<std::uint8_t> bytewise(encoded.begin(), encoded.end());
|
||||
SaxCountdown from_contiguous(events);
|
||||
SaxCountdown from_bytewise(events);
|
||||
CHECK(json::sax_parse(encoded, &from_contiguous, json::input_format_t::ubjson)
|
||||
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::ubjson));
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ using nlohmann::json;
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include "make_test_data_available.hpp"
|
||||
#include "test_utils.hpp"
|
||||
@@ -1631,3 +1632,40 @@ TEST_CASE("Unicode RFC 3629")
|
||||
}
|
||||
|
||||
DOCTEST_CLANG_SUPPRESS_WARNING_POP
|
||||
|
||||
TEST_CASE("UTF-8 check of whole strings")
|
||||
{
|
||||
// valid_utf8_string() decides short strings with a word test and long ones
|
||||
// 16 bytes at a time where a vector check is available; it must agree with
|
||||
// the byte-wise check for every mix of ASCII, sequences of each length,
|
||||
// ill-formed and truncated sequences, at every length and offset
|
||||
const char* const pieces[] = // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
|
||||
{
|
||||
"a", "\x7F", "\xC3\xA9", "\xE2\x82\xAC", "\xE0\xA0\x80", "\xEF\xBF\xBF", "\xF0\x9F\x98\x80", "\xF4\x8F\xBF\xBF",
|
||||
"\x80", "\xC0\xAF", "\xC3", "\xE2\x82", "\xED\xA0\x80", "\xF0\x9F", "\xF4\x90\x80\x80", "\xFF"
|
||||
};
|
||||
// std::mt19937's sequence is fixed by the standard; no distribution is used
|
||||
std::mt19937 rng(5); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
std::size_t valid = 0;
|
||||
for (int i = 0; i < 100000; ++i)
|
||||
{
|
||||
std::string s;
|
||||
const std::size_t length = rng() % 100;
|
||||
const auto ascii = static_cast<std::uint32_t>(rng() % 101);
|
||||
while (s.size() < length)
|
||||
{
|
||||
// mostly well-formed pieces (the first eight), so that long valid
|
||||
// strings are common
|
||||
s += (rng() % 100 < ascii) ? std::string("x") : std::string(pieces[rng() % (rng() % 8 == 0 ? 16 : 8)]);
|
||||
}
|
||||
const auto* data = reinterpret_cast<const unsigned char*>(s.data());
|
||||
const bool expected = nlohmann::detail::is_valid_utf8(s);
|
||||
valid += expected ? 1 : 0;
|
||||
CAPTURE(s)
|
||||
CHECK(nlohmann::detail::valid_utf8_string(data, s.size()) == expected);
|
||||
CHECK((nlohmann::detail::valid_utf8_prefix(data, s.size()) == s.size()) == expected);
|
||||
}
|
||||
// both outcomes are common enough to be tested
|
||||
CHECK(valid > 20000);
|
||||
CHECK(valid < 80000);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
# check_path_filters
|
||||
|
||||
The compiler workflows [`ubuntu.yml`](../../.github/workflows/ubuntu.yml),
|
||||
[`macos.yml`](../../.github/workflows/macos.yml), and [`windows.yml`](../../.github/workflows/windows.yml) skip pull
|
||||
requests that change only files no compiler job reads: the documentation, Markdown files, GitHub metadata, other
|
||||
workflows, and the license files. Pushes to `develop`, `master`, and `release/*` always run them. The documentation,
|
||||
license, and path-filter checks run in [`repository_checks.yml`](../../.github/workflows/repository_checks.yml), which
|
||||
has no path filter.
|
||||
|
||||
The filter starts with `'**'` and excludes these files explicitly, so a new file runs the compiler workflows unless it
|
||||
is excluded on purpose. [`check_path_filters.py`](check_path_filters.py) holds the filter (`PATHS`) and checks that
|
||||
|
||||
- each of the three workflows has exactly this filter, with its own workflow file included,
|
||||
- the CI targets that read excluded files (`UNFILTERED_TARGETS`) do not run in these workflows, and
|
||||
- no file the filter keeps (sources, tests, build files, tools, workflows) mentions an excluded file or directory
|
||||
outside a comment line, unless the mention is listed in `KNOWN_REFERENCES` with a reason.
|
||||
|
||||
When the check reports a mention, decide whether a compiler job reads that file:
|
||||
|
||||
- If it does, include the file in `PATHS` after the exclusions (as `package_managers.md` and the Swift example are) and
|
||||
copy the new filter into the three workflows.
|
||||
- If it does not, add the mention to `KNOWN_REFERENCES` with the reason.
|
||||
|
||||
Run the check with
|
||||
|
||||
```shell
|
||||
python3 tools/check_path_filters/check_path_filters.py
|
||||
```
|
||||
|
||||
To see which workflows a pull request would run, pass the changed files:
|
||||
|
||||
```shell
|
||||
python3 tools/check_path_filters/check_path_filters.py --changed $(git diff --name-only origin/develop)
|
||||
```
|
||||
@@ -1,273 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check the path filters of the compiler workflows.
|
||||
|
||||
ubuntu.yml, macos.yml and windows.yml skip pull requests that change only files
|
||||
no compiler job reads (documentation, Markdown files, GitHub metadata, license
|
||||
files). The filter starts with '**' and excludes these files explicitly, so a
|
||||
new file runs the workflows unless it is excluded on purpose.
|
||||
|
||||
The script checks that
|
||||
|
||||
- each of the three workflows has exactly the filter defined in PATHS below,
|
||||
with its own workflow file included, and
|
||||
- no tracked file that the filter keeps (sources, tests, build files, tools,
|
||||
workflows) mentions a file the filter excludes, unless the mention is listed
|
||||
in KNOWN_REFERENCES with the reason why it does not matter.
|
||||
|
||||
A mention found this way must either be added to PATHS as an included path
|
||||
(the file is read by a compiler job) or to KNOWN_REFERENCES (it is not).
|
||||
|
||||
With --changed FILE..., the script prints instead which workflows a pull
|
||||
request changing these files would run.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import subprocess # nosec B404
|
||||
import sys
|
||||
|
||||
REPO_ROOT = os.path.normpath(os.path.join(sys.path[0], '..', '..'))
|
||||
SELF = 'tools/check_path_filters/check_path_filters.py'
|
||||
|
||||
WORKFLOWS = ['.github/workflows/ubuntu.yml', '.github/workflows/macos.yml', '.github/workflows/windows.yml']
|
||||
|
||||
# The pull_request paths filter of the workflows above; '{workflow}' is replaced
|
||||
# with the workflow's own file. GitHub applies the patterns in order: a file
|
||||
# matching a later pattern is included ('pattern') or excluded ('!pattern').
|
||||
PATHS = [
|
||||
'**',
|
||||
'!docs/**',
|
||||
'!**.md',
|
||||
'!.github/*',
|
||||
'!.github/ISSUE_TEMPLATE/**',
|
||||
'!.github/external_ci/**',
|
||||
'!.github/workflows/**',
|
||||
'!.reuse/**',
|
||||
'!LICENSES/**',
|
||||
'!CITATION.cff',
|
||||
'!REUSE.toml',
|
||||
# read by tools/check_build_options in the ci_meson_install job (ubuntu.yml)
|
||||
'docs/mkdocs/docs/integration/package_managers.md',
|
||||
# built by the swiftpm job (macos.yml)
|
||||
'docs/mkdocs/docs/integration/swift/**',
|
||||
'{workflow}',
|
||||
]
|
||||
|
||||
# Mentions of excluded files that no job of the three workflows depends on:
|
||||
# (file that mentions it, the mention, reason)
|
||||
KNOWN_REFERENCES = [
|
||||
('cmake/ci.cmake', 'docs/', 'ci_test_examples; runs in repository_checks.yml, which has no path filter'),
|
||||
('cmake/ci.cmake', 'docs/mkdocs/', 'ci_test_build_documentation and ci_test_documentation_mermaid; they run in '
|
||||
'repository_checks.yml, which has no path filter'),
|
||||
('cmake/ci.cmake', 'docs/mkdocs/docs/examples/', 'indentation check of ci_test_amalgamation; the same check runs in '
|
||||
'check_amalgamation.yml, which has no path filter'),
|
||||
('Makefile', 'docs/mkdocs/docs/examples/', 'make pretty; runs in check_amalgamation.yml, which has no path filter'),
|
||||
('Makefile', 'ChangeLog.md', 'make ChangeLog.md writes the file; no CI job runs it'),
|
||||
('Makefile', 'tests/fuzzing.md', 'help text'),
|
||||
('Makefile', 'tools/generate_natvis/README.md', 'help text'),
|
||||
('tests/src/make_test_data_available.hpp', 'README.md', 'a file of the external test data repository'),
|
||||
('tools/check_build_options/check_build_options.py', 'docs/', 'builds the path of package_managers.md, which '
|
||||
'PATHS includes'),
|
||||
('tools/ci_jobs/ci_jobs.py', 'docs/mkdocs/docs/community/quality_assurance.md',
|
||||
'checks the job table; runs in repository_checks.yml, which has no path filter'),
|
||||
('tools/ci_jobs/ci_jobs.py', '.github/workflows/repository_checks.yml', 'read by --compare, a manual check'),
|
||||
('tools/ci_jobs/ci_jobs.py', '.github/workflows/check_amalgamation.yml', 'read by --compare, a manual check'),
|
||||
]
|
||||
|
||||
|
||||
# CI targets that read excluded files; they must run in a workflow without a path filter
|
||||
UNFILTERED_TARGETS = ['ci_test_examples', 'ci_test_build_documentation', 'ci_test_documentation_mermaid',
|
||||
'ci_test_amalgamation', 'ci_reuse_compliance']
|
||||
|
||||
|
||||
def git_files(root):
|
||||
# fixed command, no user input; includes new files not yet added, so a local run
|
||||
# sees what CI sees after the commit
|
||||
out = subprocess.check_output(['git', 'ls-files', '-z', '--cached', '--others', '--exclude-standard'], cwd=root) # nosec B603, B607
|
||||
return [f for f in out.decode('utf-8').split('\0') if f]
|
||||
|
||||
|
||||
def glob_to_regex(pattern):
|
||||
"""Translate a GitHub Actions path pattern to a regular expression."""
|
||||
regex = ''
|
||||
i = 0
|
||||
while i < len(pattern):
|
||||
if pattern.startswith('**', i):
|
||||
regex += '.*'
|
||||
i += 2
|
||||
elif pattern[i] == '*':
|
||||
regex += '[^/]*'
|
||||
i += 1
|
||||
elif pattern[i] == '?':
|
||||
regex += '[^/]'
|
||||
i += 1
|
||||
else:
|
||||
regex += re.escape(pattern[i])
|
||||
i += 1
|
||||
return re.compile(regex + r'\Z')
|
||||
|
||||
|
||||
def compile_filter(paths, workflow):
|
||||
return [(p.startswith('!'), glob_to_regex(p.lstrip('!').replace('{workflow}', workflow))) for p in paths]
|
||||
|
||||
|
||||
def included(compiled, path):
|
||||
result = False
|
||||
for negated, regex in compiled:
|
||||
if regex.match(path):
|
||||
result = not negated
|
||||
return result
|
||||
|
||||
|
||||
def read_filter(root, workflow):
|
||||
"""Return the pull_request paths list of a workflow file."""
|
||||
with open(os.path.join(root, workflow), encoding='utf-8') as f:
|
||||
lines = f.read().splitlines()
|
||||
paths = None
|
||||
in_pull_request = False
|
||||
for line in lines:
|
||||
if re.match(r'^ pull_request:', line):
|
||||
in_pull_request = True
|
||||
continue
|
||||
if in_pull_request:
|
||||
if re.match(r'^ \S', line) or re.match(r'^\S', line):
|
||||
break
|
||||
if re.match(r'^ paths:\s*$', line):
|
||||
paths = []
|
||||
continue
|
||||
m = re.match(r"^ - '(.*)'\s*(#.*)?$", line)
|
||||
if paths is not None and m:
|
||||
paths.append(m.group(1))
|
||||
elif paths is not None and line.strip() and not line.strip().startswith('#'):
|
||||
break
|
||||
return paths
|
||||
|
||||
|
||||
TOKEN = re.compile(r'(["\'/]?)([\w.-]+(?:/[\w.-]+)*)')
|
||||
|
||||
|
||||
COMMENT_LINE = re.compile(r'^[ \t]*(//|#|/?\*|--).*$', re.MULTILINE)
|
||||
|
||||
|
||||
class Mentions:
|
||||
"""Finds the words and paths in a text that name an excluded file or directory."""
|
||||
|
||||
def __init__(self, files, excluded, is_excluded):
|
||||
self.files = set(files)
|
||||
self.excluded = excluded
|
||||
self.dirs = {'/'.join(f.split('/')[:n]) for f in files for n in range(1, f.count('/') + 1)}
|
||||
# an excluded directory: a new file in it would be excluded
|
||||
self.excluded_dirs = {d for d in self.dirs if is_excluded(d + '/new_file')}
|
||||
# base names that only excluded files have, e.g. 'ChangeLog.md' but not 'CMakeLists.txt'
|
||||
self.names = {os.path.basename(f) for f in excluded} - {os.path.basename(f) for f in files if f not in excluded}
|
||||
|
||||
def find(self, text):
|
||||
"""Yield (offset, mention) for every word or path in text that names an excluded file or directory."""
|
||||
for m in TOKEN.finditer(text):
|
||||
parts = m.group(2).rstrip('/').split('/')
|
||||
# a single word names a directory only in a path context, e.g. 'docs' in os.path.join('docs', ...)
|
||||
path_context = len(parts) > 1 or m.group(1)
|
||||
for i in range(len(parts)):
|
||||
path = '/'.join(parts[i:])
|
||||
if path in self.files:
|
||||
if path in self.excluded:
|
||||
yield m.start(2), path
|
||||
break
|
||||
if path in self.dirs and path_context:
|
||||
if path in self.excluded_dirs:
|
||||
yield m.start(2), path + '/'
|
||||
break
|
||||
else:
|
||||
if parts[-1] in self.names and '.' in parts[-1]:
|
||||
yield m.start(2), parts[-1]
|
||||
|
||||
|
||||
def known(path, mention):
|
||||
return any(path == file and mention == known_mention for file, known_mention, _ in KNOWN_REFERENCES)
|
||||
|
||||
|
||||
def check_workflows(root):
|
||||
"""Return the errors in the filters of the workflows and in the targets they run."""
|
||||
errors = []
|
||||
for workflow in WORKFLOWS:
|
||||
expected = [p.replace('{workflow}', workflow) for p in PATHS]
|
||||
actual = read_filter(root, workflow)
|
||||
if actual != expected:
|
||||
errors.append('{}: the pull_request paths filter differs from tools/check_path_filters/check_path_filters.py\n'
|
||||
' expected: {}\n actual: {}'.format(workflow, expected, actual))
|
||||
with open(os.path.join(root, workflow), encoding='utf-8') as f:
|
||||
text = f.read()
|
||||
errors += ['{}: runs {}, which reads excluded files; run it in repository_checks.yml'.format(workflow, target)
|
||||
for target in UNFILTERED_TARGETS if re.search(r'\b' + target + r'\b', text)]
|
||||
return errors
|
||||
|
||||
|
||||
def scanned_text(root, path):
|
||||
"""Return the text of a kept file without comment lines and without the filter itself, or None to skip it."""
|
||||
# third-party code and Markdown files read nothing; this script lists the excluded files
|
||||
if path.startswith('tests/thirdparty/') or path.endswith('.md') or path == SELF:
|
||||
return None
|
||||
try:
|
||||
with open(os.path.join(root, path), encoding='utf-8') as f:
|
||||
# a mention in a comment line reads nothing; keep the line numbers
|
||||
text = COMMENT_LINE.sub('', f.read())
|
||||
except (UnicodeDecodeError, OSError):
|
||||
return None
|
||||
if path in WORKFLOWS:
|
||||
# the filter itself lists the excluded files
|
||||
for p in PATHS:
|
||||
text = text.replace(" - '{}'".format(p.replace('{workflow}', path)), '')
|
||||
return text
|
||||
|
||||
|
||||
def check_mentions(root, files, excluded, finder):
|
||||
"""Return an error for every mention of an excluded file in a kept file that KNOWN_REFERENCES does not list."""
|
||||
errors = []
|
||||
for path in sorted(set(files) - excluded):
|
||||
text = scanned_text(root, path)
|
||||
reported = set()
|
||||
for offset, mention in finder.find(text or ''):
|
||||
if mention not in reported and not known(path, mention):
|
||||
reported.add(mention)
|
||||
line = text.count('\n', 0, offset) + 1
|
||||
errors.append('{}:{}: mentions excluded {}; include it in PATHS if a compiler job reads it, '
|
||||
'or add it to KNOWN_REFERENCES'.format(path, line, mention))
|
||||
return errors
|
||||
|
||||
|
||||
def check(root):
|
||||
files = git_files(root)
|
||||
union = [compile_filter(PATHS, w) for w in WORKFLOWS]
|
||||
|
||||
def is_excluded(path):
|
||||
return not any(included(c, path) for c in union)
|
||||
|
||||
excluded = {f for f in files if is_excluded(f)}
|
||||
errors = check_workflows(root) + check_mentions(root, files, excluded, Mentions(files, excluded, is_excluded))
|
||||
for e in errors:
|
||||
print(e)
|
||||
if not errors:
|
||||
print('path filters OK: {} of {} tracked files skip the compiler workflows'.format(len(excluded), len(files)))
|
||||
return 1 if errors else 0
|
||||
|
||||
|
||||
def explain(changed):
|
||||
for workflow in WORKFLOWS:
|
||||
compiled = compile_filter(PATHS, workflow)
|
||||
hits = [f for f in changed if included(compiled, f)]
|
||||
print('{}: {}'.format(workflow, 'runs ({})'.format(', '.join(hits[:3]) + (' ...' if len(hits) > 3 else ''))
|
||||
if hits else 'skipped'))
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
parser.add_argument('--root', default=REPO_ROOT, help='repository root')
|
||||
parser.add_argument('--changed', nargs='+', metavar='FILE', help='show which workflows these changed files run')
|
||||
args = parser.parse_args()
|
||||
return explain(args.changed) if args.changed else check(args.root)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Loaded 100 of 106 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user