mirror of
https://github.com/nlohmann/json.git
synced 2026-11-12 15:10:47 +00:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e1d35eda9 | ||
|
|
4b538d80f6 | ||
|
|
d20c4dcd89 | ||
|
|
ea03d601af | ||
|
|
e2cb15c479 | ||
|
|
c451fac1bc | ||
|
|
28f71914b5 | ||
|
|
0b91f9ddd8 | ||
|
|
0115bb47b3 | ||
|
|
ea8a05484b | ||
|
|
2aecd86b37 | ||
|
|
5eccbbd2bc | ||
|
|
66ee5d1b84 | ||
|
|
7a4e6b7a5d | ||
|
|
46bead6e13 | ||
|
|
ae57b09bc6 | ||
|
|
fd37b04813 | ||
|
|
54f6b3ef06 | ||
|
|
091c10d4a5 | ||
|
|
2120927a7a | ||
|
|
282079ba82 |
No files matched your search
@@ -28,7 +28,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -38,14 +38,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
with:
|
||||
languages: c-cpp
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
uses: github/codeql-action/autobuild@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -48,6 +48,6 @@ jobs:
|
||||
output: 'flawfinder_results.sarif'
|
||||
|
||||
- name: Upload analysis results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
with:
|
||||
sarif_file: ${{github.workspace}}/flawfinder_results.sarif
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ 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:
|
||||
@@ -30,37 +28,23 @@ on:
|
||||
- '.github/workflows/macos.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
# A run triggered by any other label gets a group of its own, so it does not
|
||||
# cancel the run of the last push; its jobs are skipped.
|
||||
concurrency:
|
||||
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
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
||||
# The jobs are split into a PR set and a full set like in ubuntu.yml (see "How
|
||||
# CI runs" in docs/mkdocs/docs/community/quality_assurance.md): the PR set runs
|
||||
# on every push; the full set after the PR set passed, on pushes and on pull
|
||||
# requests with the "🚀 ready to merge" label. tools/ci_jobs checks that every
|
||||
# job belongs to exactly one set.
|
||||
|
||||
jobs:
|
||||
###########################################################################
|
||||
# PR set
|
||||
###########################################################################
|
||||
|
||||
macos-15:
|
||||
if: &pr_set ${{ github.event_name != 'schedule' && (github.event.action != 'labeled' || github.event.label.name == '🚀 ready to merge') }}
|
||||
runs-on: macos-15 # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md
|
||||
strategy:
|
||||
matrix:
|
||||
xcode: ['16.0', '16.4', '26.3']
|
||||
xcode: ['16.0', '16.1', '16.2', '16.3', '16.4', '26.0.1', '26.1.1', '26.2', '26.3']
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
|
||||
|
||||
steps: &xcode_steps
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
@@ -72,11 +56,10 @@ jobs:
|
||||
run: cd build ; ctest -j 10 --output-on-failure
|
||||
|
||||
macos-26:
|
||||
if: *pr_set
|
||||
runs-on: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md
|
||||
strategy:
|
||||
matrix:
|
||||
xcode: ['26.6']
|
||||
xcode: ['26.4.1', '26.5', '26.6']
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
|
||||
|
||||
@@ -92,13 +75,12 @@ jobs:
|
||||
run: cd build ; ctest -j 10 --output-on-failure
|
||||
|
||||
xcode_standards:
|
||||
if: *pr_set
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
standard: [14]
|
||||
standard: [11, 14, 17, 20, 23, 26]
|
||||
|
||||
steps: &xcode_standards_steps
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
@@ -110,7 +92,6 @@ jobs:
|
||||
run: cd build ; ctest -j 10 --output-on-failure
|
||||
|
||||
swiftpm:
|
||||
if: *pr_set
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -144,42 +125,3 @@ jobs:
|
||||
EOF
|
||||
cd /tmp/json-swiftpm-consumer
|
||||
swift build
|
||||
|
||||
|
||||
###########################################################################
|
||||
# full set: starts after the PR set passed
|
||||
###########################################################################
|
||||
|
||||
macos-15_full:
|
||||
needs: &pr_set_jobs [macos-15, macos-26, xcode_standards, swiftpm]
|
||||
if: &full_set ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, '🚀 ready to merge') }}
|
||||
runs-on: macos-15 # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md
|
||||
strategy:
|
||||
matrix:
|
||||
xcode: ['16.1', '16.2', '16.3', '26.0.1', '26.1.1', '26.2']
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
|
||||
|
||||
steps: *xcode_steps
|
||||
|
||||
macos-26_full:
|
||||
needs: *pr_set_jobs
|
||||
if: *full_set
|
||||
runs-on: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md
|
||||
strategy:
|
||||
matrix:
|
||||
xcode: ['26.4.1', '26.5']
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
|
||||
|
||||
steps: *xcode_steps
|
||||
|
||||
xcode_standards_full:
|
||||
needs: *pr_set_jobs
|
||||
if: *full_set
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
standard: [11, 17, 20, 23, 26]
|
||||
|
||||
steps: *xcode_standards_steps
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
target: [ci_test_examples, ci_test_build_documentation, ci_test_documentation_mermaid]
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -80,6 +80,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
|
||||
# Upload SARIF file generated in previous step
|
||||
- name: Upload SARIF file
|
||||
uses: github/codeql-action/upload-sarif@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
with:
|
||||
sarif_file: semgrep.sarif
|
||||
if: always()
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ jobs:
|
||||
target: [ci_cpplint]
|
||||
steps: &static_analysis_ubuntu_steps
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -306,7 +306,7 @@ jobs:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -461,7 +461,7 @@ jobs:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -499,7 +499,7 @@ jobs:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -631,7 +631,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
+35
-198
@@ -7,8 +7,6 @@ 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:
|
||||
@@ -28,46 +26,28 @@ on:
|
||||
# built by the swiftpm job (macos.yml)
|
||||
- 'docs/mkdocs/docs/integration/swift/**'
|
||||
- '.github/workflows/windows.yml'
|
||||
schedule:
|
||||
# the nightly set (see below), on develop
|
||||
- cron: '0 2 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
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 like in ubuntu.yml (see "How CI runs" in
|
||||
# docs/mkdocs/docs/community/quality_assurance.md): the PR set runs on every
|
||||
# push; the full set after the PR set passed, on pushes and on pull requests
|
||||
# with the "🚀 ready to merge" label; the nightly set nightly on develop, on
|
||||
# pushes to master and release/*, and when started by hand. tools/ci_jobs
|
||||
# checks that every job belongs to exactly one set.
|
||||
|
||||
jobs:
|
||||
###########################################################################
|
||||
# PR set
|
||||
###########################################################################
|
||||
|
||||
mingw:
|
||||
if: &pr_set ${{ github.event_name != 'schedule' && (github.event.action != 'labeled' || github.event.label.name == '🚀 ready to merge') }}
|
||||
runs-on: windows-2022
|
||||
strategy:
|
||||
matrix:
|
||||
architecture: [x86]
|
||||
architecture: [x64, x86]
|
||||
|
||||
steps: &mingw_steps
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up MinGW
|
||||
uses: egor-tensin/setup-mingw@41b837e47d7f85214629d255b9c4bc3fcbe9fd63 # v3.0
|
||||
uses: egor-tensin/setup-mingw@d54f5ef0ce585bd3fbb1f8a71b5b254f59dbf931 # v3.0
|
||||
with:
|
||||
platform: ${{ matrix.architecture }}
|
||||
version: 12.2.0 # https://github.com/egor-tensin/setup-mingw/issues/14
|
||||
@@ -79,26 +59,15 @@ jobs:
|
||||
run: cd build ; ctest -j 10 -C Debug --output-on-failure
|
||||
|
||||
msvc:
|
||||
if: *pr_set
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- build_type: Debug
|
||||
architecture: x64
|
||||
std_version: default
|
||||
- build_type: Release
|
||||
architecture: x64
|
||||
std_version: default
|
||||
- build_type: Debug
|
||||
architecture: Win32
|
||||
std_version: default
|
||||
- build_type: Release
|
||||
architecture: Win32
|
||||
std_version: latest
|
||||
build_type: [Debug, Release]
|
||||
architecture: [Win32, x64]
|
||||
std_version: [default, latest]
|
||||
|
||||
runs-on: windows-2022
|
||||
|
||||
steps: &msvc_steps
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
@@ -127,20 +96,15 @@ jobs:
|
||||
# Visual Studio 2026 (v145 toolset) on the windows-2025 image. The "Visual Studio
|
||||
# 18 2026" generator requires CMake 4.2+, so a recent CMake is fetched explicitly.
|
||||
msvc-vs2026:
|
||||
if: *pr_set
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- build_type: Debug
|
||||
architecture: x64
|
||||
std_version: latest
|
||||
- build_type: Release
|
||||
architecture: Win32
|
||||
std_version: default
|
||||
build_type: [Debug, Release]
|
||||
architecture: [Win32, x64]
|
||||
std_version: [default, latest]
|
||||
|
||||
runs-on: windows-2025
|
||||
|
||||
steps: &msvc_vs2026_steps
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
@@ -172,14 +136,13 @@ jobs:
|
||||
# Native ARM64 Windows runner with the MSVC ARM64 toolset. The windows-11-arm
|
||||
# label is only available for public repositories.
|
||||
msvc-arm64:
|
||||
if: *pr_set
|
||||
strategy:
|
||||
matrix:
|
||||
build_type: [Debug]
|
||||
build_type: [Debug, Release]
|
||||
|
||||
runs-on: windows-11-arm
|
||||
|
||||
steps: &msvc_arm64_steps
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
@@ -197,20 +160,19 @@ jobs:
|
||||
run: cd build ; ctest -j 10 -C ${{ matrix.build_type }} --output-on-failure
|
||||
|
||||
clang:
|
||||
if: *pr_set
|
||||
runs-on: windows-2022
|
||||
strategy:
|
||||
matrix:
|
||||
version: [20.1.8]
|
||||
version: [11.0.1, 12.0.1, 13.0.1, 14.0.6, 15.0.7, 16.0.6, 18.1.8, 19.1.7, 20.1.8]
|
||||
|
||||
steps: &clang_steps
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install Clang
|
||||
run: curl -fsSL -o LLVM${{ matrix.version }}.exe https://github.com/llvm/llvm-project/releases/download/llvmorg-${{ matrix.version }}/LLVM-${{ matrix.version }}-win64.exe ; 7z x LLVM${{ matrix.version }}.exe -y -o"C:/Program Files/LLVM"
|
||||
- name: Set up MinGW
|
||||
uses: egor-tensin/setup-mingw@41b837e47d7f85214629d255b9c4bc3fcbe9fd63 # v3.0
|
||||
uses: egor-tensin/setup-mingw@d54f5ef0ce585bd3fbb1f8a71b5b254f59dbf931 # v3.0
|
||||
with:
|
||||
platform: x64
|
||||
version: 12.2.0 # https://github.com/egor-tensin/setup-mingw/issues/14
|
||||
@@ -246,8 +208,24 @@ jobs:
|
||||
- name: Test
|
||||
run: cd build ; ctest -j 10 -C Debug --output-on-failure
|
||||
|
||||
clang-cl:
|
||||
runs-on: windows-2022
|
||||
strategy:
|
||||
matrix:
|
||||
architecture: [Win32, x64]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run CMake
|
||||
run: cmake -S . -B build -G "Visual Studio 17 2022" -A ${{ matrix.architecture }} -T ClangCL -DJSON_BuildTests=On
|
||||
- name: Build
|
||||
run: cmake --build build --config Debug --parallel 10
|
||||
- name: Test
|
||||
run: cd build ; ctest -j 10 -C Debug --output-on-failure
|
||||
|
||||
ci_module_cpp20:
|
||||
if: *pr_set
|
||||
runs-on: windows-2022
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -266,145 +244,4 @@ jobs:
|
||||
run: cmake -S . -B build -G "Visual Studio 17 2022" -DJSON_CI=ON -DCMAKE_CXX_FLAGS="/permissive- /std:c++latest /utf-8 /EHsc /W4 /WX"
|
||||
- name: Build
|
||||
run: cmake --build build --config Debug --target ci_module_cpp20
|
||||
|
||||
|
||||
|
||||
###########################################################################
|
||||
# full set: starts after the PR set passed
|
||||
###########################################################################
|
||||
|
||||
msvc_full:
|
||||
needs: &pr_set_jobs [mingw, msvc, msvc-vs2026, msvc-arm64, clang, ci_module_cpp20]
|
||||
if: &full_set ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, '🚀 ready to merge') }}
|
||||
strategy:
|
||||
matrix:
|
||||
build_type: [Debug, Release]
|
||||
architecture: [Win32, x64]
|
||||
std_version: [default, latest]
|
||||
exclude:
|
||||
# in the PR set
|
||||
- build_type: Debug
|
||||
architecture: x64
|
||||
std_version: default
|
||||
- build_type: Release
|
||||
architecture: x64
|
||||
std_version: default
|
||||
- build_type: Debug
|
||||
architecture: Win32
|
||||
std_version: default
|
||||
- build_type: Release
|
||||
architecture: Win32
|
||||
std_version: latest
|
||||
|
||||
runs-on: windows-2022
|
||||
|
||||
steps: *msvc_steps
|
||||
|
||||
# Visual Studio 2026 (v145 toolset) on the windows-2025 image. The "Visual Studio
|
||||
# 18 2026" generator requires CMake 4.2+, so a recent CMake is fetched explicitly.
|
||||
msvc-vs2026_full:
|
||||
needs: *pr_set_jobs
|
||||
if: *full_set
|
||||
strategy:
|
||||
matrix:
|
||||
build_type: [Debug, Release]
|
||||
architecture: [Win32, x64]
|
||||
std_version: [default, latest]
|
||||
exclude:
|
||||
# in the PR set
|
||||
- build_type: Debug
|
||||
architecture: x64
|
||||
std_version: latest
|
||||
- build_type: Release
|
||||
architecture: Win32
|
||||
std_version: default
|
||||
|
||||
runs-on: windows-2025
|
||||
|
||||
steps: *msvc_vs2026_steps
|
||||
|
||||
# Native ARM64 Windows runner with the MSVC ARM64 toolset. The windows-11-arm
|
||||
# label is only available for public repositories.
|
||||
msvc-arm64_full:
|
||||
needs: *pr_set_jobs
|
||||
if: *full_set
|
||||
strategy:
|
||||
matrix:
|
||||
build_type: [Release]
|
||||
|
||||
runs-on: windows-11-arm
|
||||
|
||||
steps: *msvc_arm64_steps
|
||||
|
||||
clang_full:
|
||||
needs: *pr_set_jobs
|
||||
if: *full_set
|
||||
runs-on: windows-2022
|
||||
strategy:
|
||||
matrix:
|
||||
version: [11.0.1, 12.0.1, 13.0.1, 14.0.6, 15.0.7, 16.0.6, 18.1.8, 19.1.7]
|
||||
|
||||
steps: *clang_steps
|
||||
|
||||
clang-cl_full:
|
||||
needs: *pr_set_jobs
|
||||
if: *full_set
|
||||
runs-on: windows-2022
|
||||
strategy:
|
||||
matrix:
|
||||
architecture: [Win32, x64]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run CMake
|
||||
run: cmake -S . -B build -G "Visual Studio 17 2022" -A ${{ matrix.architecture }} -T ClangCL -DJSON_BuildTests=On
|
||||
- name: Build
|
||||
run: cmake --build build --config Debug --parallel 10
|
||||
- name: Test
|
||||
run: cd build ; ctest -j 10 -C Debug --output-on-failure
|
||||
|
||||
|
||||
###########################################################################
|
||||
# nightly set
|
||||
###########################################################################
|
||||
|
||||
mingw_nightly:
|
||||
if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref != 'refs/heads/develop') }}
|
||||
runs-on: windows-2022
|
||||
strategy:
|
||||
matrix:
|
||||
architecture: [x64]
|
||||
|
||||
steps: *mingw_steps
|
||||
|
||||
# A failure of a scheduled run opens an issue, or comments on the open one.
|
||||
nightly_report:
|
||||
needs: [mingw_nightly]
|
||||
if: ${{ always() && github.event_name == 'schedule' && contains(needs.*.result, 'failure') }}
|
||||
runs-on: ubuntu-26.04
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Open or update the nightly failure issue
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
WORKFLOW: ${{ github.workflow }}
|
||||
SHA: ${{ github.sha }}
|
||||
run: |
|
||||
title="Nightly CI failure (${WORKFLOW})"
|
||||
body="The nightly run of ${WORKFLOW} on ${SHA:0:10} failed: ${RUN_URL}"
|
||||
number=$(gh issue list --state open --search "in:title \"${title}\"" --json number,title --jq "map(select(.title == \"${title}\")) | .[0].number // empty")
|
||||
if [ -n "$number" ]; then
|
||||
gh issue comment "$number" --body "$body"
|
||||
else
|
||||
gh issue create --title "$title" --body "$body"
|
||||
fi
|
||||
|
||||
@@ -1408,9 +1408,9 @@ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR I
|
||||
- The class contains a port of the shortest double-to-decimal conversion of [Żmij](https://github.com/vitaut/zmij) by Victor Zverovich, including the conversion of the digits to text by Xiang JunBo and the SIMD instruction sequence of Dougall Johnson, which is licensed under the [MIT License](https://opensource.org/licenses/MIT) (see above). Copyright © 2025 [Victor Zverovich](https://github.com/vitaut)
|
||||
- The class contains a copy of [Hedley](https://nemequ.github.io/hedley/) from Evan Nemerson which is licensed as [CC0-1.0](https://creativecommons.org/publicdomain/zero/1.0/).
|
||||
- The class contains parts of [Google Abseil](https://github.com/abseil/abseil-cpp) which is licensed under the [Apache 2.0 License](https://opensource.org/licenses/Apache-2.0).
|
||||
- The class contains an adapted version of the Eisel-Lemire algorithm, its table of powers of five, and its digit comparison for long numbers from [fast_float](https://github.com/fastfloat/fast_float) by Daniel Lemire and contributors, which is available under the [MIT License](https://opensource.org/licenses/MIT) (used here), the Apache 2.0 License, and the Boost Software License. Copyright © 2021 The fast_float authors
|
||||
- The class contains an adapted version of the Eisel-Lemire algorithm, its table of powers of five, its digit comparison for long numbers, and its parsing of eight digits at a time from [fast_float](https://github.com/fastfloat/fast_float) by Daniel Lemire and contributors, which is available under the [MIT License](https://opensource.org/licenses/MIT) (used here), the Apache 2.0 License, and the Boost Software License. Copyright © 2021 The fast_float authors
|
||||
- The view's parser (`<nlohmann/json_view.hpp>`) contains techniques and code adapted from [yyjson](https://github.com/ibireme/yyjson) by YaoYuan, which is licensed under the [MIT License](https://opensource.org/licenses/MIT) (see above): table-driven decoding of `\u` escapes and fixed-offset unrolled checks.
|
||||
- The view's parser (`<nlohmann/json_view.hpp>`) validates non-ASCII strings with the vector UTF-8 check of [simdjson](https://github.com/simdjson/simdjson) by Daniel Lemire, Geoff Langdale, John Keiser, and contributors (its "lookup4" algorithm and tables, after J. Keiser and D. Lemire, "Validating UTF-8 In Less Than One Instruction Per Byte", 2021), which is available under the [MIT License](https://opensource.org/licenses/MIT) (used here) and the Apache 2.0 License. Copyright © 2018-2025 The simdjson authors
|
||||
- The class's lexer and the view's parser (`<nlohmann/json_view.hpp>`) validate non-ASCII strings with the vector UTF-8 check of [simdjson](https://github.com/simdjson/simdjson) by Daniel Lemire, Geoff Langdale, John Keiser, and contributors (its "lookup4" algorithm and tables, after J. Keiser and D. Lemire, "Validating UTF-8 In Less Than One Instruction Per Byte", 2021), which is available under the [MIT License](https://opensource.org/licenses/MIT) (used here) and the Apache 2.0 License. Copyright © 2018-2025 The simdjson authors
|
||||
|
||||
<img align="right" src="https://git.fsfe.org/reuse/reuse-ci/raw/branch/master/reuse-horizontal.png" alt="REUSE Software">
|
||||
|
||||
|
||||
@@ -63,6 +63,15 @@ called:
|
||||
Whether the JSON value which called the function during parsing should be kept (`#!cpp true`) or not (`#!cpp false`). In
|
||||
the latter case, it is skipped completely, or replaced by `null` if it is the top-level value.
|
||||
|
||||
## Notes
|
||||
|
||||
For a `parse_event_t::key` event, `parsed` holds the decoded object name, with the same guarantees as the `val`
|
||||
parameter of [`json_sax::key`](../json_sax/key.md#notes): all escape sequences are decoded and `\uXXXX` escapes are
|
||||
stored as UTF-8, so different spellings of the same name yield identical strings; the name is validated completely
|
||||
before the callback is called, so a name with an invalid escape sequence or ill-formed UTF-8 causes a parse error and
|
||||
no `key` event; no Unicode normalization or case conversion is applied; and a name that occurs several times in the
|
||||
same object yields one `key` event per occurrence.
|
||||
|
||||
## Examples
|
||||
|
||||
??? example "Example: skip an object key while parsing"
|
||||
|
||||
@@ -69,6 +69,19 @@ Duplicate member names inside an operation object are resolved by the parser bef
|
||||
is kept, see [`object_t`](object_t.md#behavior)), so `#!json {"op": "add", "op": "remove", "path": "/a"}` is a
|
||||
`remove` operation.
|
||||
|
||||
The JSON patch may be the JSON value itself (`#!cpp j.patch_inplace(j)`): the operations are then read from a copy of
|
||||
the patch made before the first operation is applied, so the result is the same as that of
|
||||
`#!cpp j = j.patch(j)`, and the exception safety is the same as above.
|
||||
|
||||
If [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) is enabled, the same holds for a JSON patch stored inside the
|
||||
JSON value (e.g., `#!cpp j.patch_inplace(j["patch"])`): it is detected via the parent pointers.
|
||||
|
||||
!!! danger "Undefined behavior"
|
||||
|
||||
Without [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md), the JSON patch must not be a value stored inside the
|
||||
JSON value (e.g., `#!cpp j.patch_inplace(j["patch"])`), as the operations may change or destroy it while it is
|
||||
read. Pass a copy (`#!cpp j.patch_inplace(json(j["patch"]))`) or use [`patch`](patch.md) instead.
|
||||
|
||||
## Examples
|
||||
|
||||
??? example "Example: apply a JSON patch in place"
|
||||
@@ -122,3 +135,5 @@ is kept, see [`object_t`](object_t.md#behavior)), so `#!json {"op": "add", "op":
|
||||
- Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) instead of
|
||||
[`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) for a one-character array index that is
|
||||
not a digit (e.g., `/x`) in version 3.13.0, as it already did for longer ones.
|
||||
- Applying a JSON value as patch to itself (or, with `JSON_DIAGNOSTICS`, to a value stored inside it) no longer results
|
||||
in undefined behavior in version 3.13.0.
|
||||
@@ -32,9 +32,24 @@ uses, so the result matches it exactly — including, for an object, that a repe
|
||||
last value. The replay is iterative, so it is not limited by the call stack the way a naive recursive conversion
|
||||
would be; the JSON nesting depth is limited only by available memory, as for `BasicJsonType::parse()` itself.
|
||||
|
||||
Unlike parsing with [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) enabled, the values
|
||||
produced by `materialize()` do not carry source positions. The index has the start offset of every value, but not the
|
||||
end of arrays, objects, and escaped strings, which the positions also need.
|
||||
With [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) enabled, the values get the source
|
||||
positions [`BasicJsonType::parse()`](../basic_json/parse.md) would give them, also for the subtree of a view (the
|
||||
positions are those in the document's whole source text), and exceptions about them name the same bytes. For values of
|
||||
an [editable document](../json_editable_document.md), positions are all-or-nothing, and [`start_pos()`](../basic_json/start_pos.md)
|
||||
and [`end_pos()`](../basic_json/end_pos.md) return `std::string::npos` for
|
||||
|
||||
- a value written by an edit (a copy of an array or object of the document is also new; copies of its scalars keep
|
||||
their positions),
|
||||
- an array or object whose elements were changed (its elements keep theirs), and
|
||||
- an array or object whose last element has no positions (its end is found from that element).
|
||||
|
||||
This differs from a `basic_json` value, whose positions are not updated when it is changed: they stay as they were
|
||||
parsed, even if they no longer match the value (see [`start_pos()`](../basic_json/start_pos.md)).
|
||||
|
||||
Documents [loaded](../basic_json_document/load.md) from an image have the positions of the saved document. Its strings
|
||||
with escapes have positions only if the document was parsed with `JSON_DIAGNOSTIC_POSITIONS`: the position of their
|
||||
opening quote is recorded while parsing, and `save()` and `load()` keep it whether or not they are compiled with the
|
||||
macro.
|
||||
|
||||
Calling `materialize()` on the same view repeatedly builds a new, independent `BasicJsonType` value each time; it
|
||||
never caches the result.
|
||||
@@ -62,8 +77,7 @@ never caches the result.
|
||||
|
||||
- [`root`](../basic_json_document/root.md) - the view of a document's root value
|
||||
- [`BasicJsonType::parse`](../basic_json/parse.md) - build a `BasicJsonType` value directly from a JSON text
|
||||
- [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) - source positions on parsed values (not
|
||||
produced by `materialize()`)
|
||||
- [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) - source positions on parsed values
|
||||
- [Zero-copy JSON views](../../features/json_view.md#getting-values-out-without-copying) - when to build a basic_json value from a view
|
||||
|
||||
## Version history
|
||||
|
||||
@@ -18,7 +18,8 @@ json_pointer& operator/=(std::size_t array_idx)
|
||||
## Parameters
|
||||
|
||||
`ptr` (in)
|
||||
: JSON pointer to append
|
||||
: JSON pointer to append; may be the JSON pointer itself (`#!cpp p /= p`), which then appends a copy of its
|
||||
reference tokens
|
||||
|
||||
`token` (in)
|
||||
: reference token to append
|
||||
@@ -69,6 +70,7 @@ json_pointer& operator/=(std::size_t array_idx)
|
||||
|
||||
## Version history
|
||||
|
||||
1. Added in version 3.6.0.
|
||||
1. Added in version 3.6.0. Appending a JSON pointer to itself no longer results in undefined behavior in version
|
||||
3.13.0.
|
||||
2. Added in version 3.6.0. Changed type of `token` to `string_t` in version 3.11.0.
|
||||
3. Added in version 3.6.0.
|
||||
@@ -19,6 +19,23 @@ Whether parsing should proceed.
|
||||
|
||||
It is safe to move the passed object key value.
|
||||
|
||||
When parsing JSON text, `val` is the decoded object name, not its spelling in the input:
|
||||
|
||||
- All escape sequences (`\"`, `\\`, `\/`, `\b`, `\f`, `\n`, `\r`, `\t`, `\uXXXX`, and surrogate pairs) are
|
||||
decoded, and `\uXXXX` escapes are stored as UTF-8. Different spellings of the same name, such as `#!json "/"`
|
||||
and `#!json "\/"`, or `#!json "\u00e9"` and `#!json "\u00E9"`, yield identical strings. An escaped `\u0000` yields
|
||||
an embedded NUL byte.
|
||||
- The name is scanned and validated completely before `key` is called. If it contains an invalid escape sequence, an
|
||||
unpaired surrogate, a control character, or ill-formed UTF-8, the parse fails with a
|
||||
[`parse_error`](../../home/exceptions.md#parse-errors), and `key` is not called for it.
|
||||
- No Unicode normalization or case conversion is applied: names that differ in any byte are different strings.
|
||||
- `key` is called before the following `:` is read, so a parse error may still follow for the same member.
|
||||
- A name that occurs several times in the same object is passed once per occurrence.
|
||||
|
||||
For the binary formats, object keys contain no escape sequences. For CBOR, MessagePack, UBJSON, and BSON, `val` holds
|
||||
the bytes stored in the input, and [`sax_parse`](../basic_json/sax_parse.md) passes them unchanged, without checking them
|
||||
for valid UTF-8. BON8 encodes characters itself, so an ill-formed name is a parse error and `key` is not called for it.
|
||||
|
||||
## Examples
|
||||
|
||||
??? example
|
||||
@@ -45,3 +62,7 @@ It is safe to move the passed object key value.
|
||||
## Version history
|
||||
|
||||
- Added in version 3.2.0.
|
||||
- The behavior described in the notes above (decoded names, validation before `key` is called, no normalization, `key`
|
||||
before the `:` check, one call per duplicate, bytes of binary formats passed unchanged) is the same in all versions
|
||||
from 3.2.0 on, and for the `key` event of a [parser callback](../basic_json/parser_callback_t.md) from 3.0.0 on. BSON
|
||||
is only supported from version 3.4.0, and BON8 only from version 3.13.0.
|
||||
@@ -113,6 +113,7 @@ When the macro is not defined, the library will define it to its default value.
|
||||
|
||||
- [:simple-cmake: JSON_Diagnostic_Positions](../../integration/cmake.md#json_diagnostic_positions) - CMake option to control the macro
|
||||
- [`JSON_DIAGNOSTICS`](json_diagnostics.md) - macro to control extended diagnostics
|
||||
- [`materialize()`](../basic_json_view/materialize.md) - values of a JSON view, with the same positions
|
||||
- [Parsing and Exceptions](../../features/parsing/parse_exceptions.md) - handling parse errors and locating them in the input
|
||||
|
||||
## Version history
|
||||
|
||||
@@ -15,17 +15,17 @@ describe each of them.
|
||||
|
||||
## How CI runs
|
||||
|
||||
Not every job runs for every change. The compiler jobs on Ubuntu, macOS, and Windows are split into three sets, so a
|
||||
pull request gets fast feedback and the long jobs run when they matter:
|
||||
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, four Visual Studio 2022 and two Visual Studio 2026 configurations, MinGW, Clang on Windows, ARM64 Windows, five Xcode versions, Swift Package Manager; with the checks that always run, they would have caught at least 92 % of the failing commits of September and October 2026 |
|
||||
| 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, ARM64 Linux, and 64-bit MinGW; a failure opens an issue |
|
||||
| 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 documentation and license checks and the amalgamation check run on every push and pull request; AppVeyor (Visual
|
||||
Studio 2015, 2017, and 2019) has its own schedule. A pull request that changes only the documentation, Markdown files, GitHub metadata, or license files
|
||||
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
|
||||
@@ -39,10 +39,6 @@ every later push to that pull request. A pull request is merged only when the fu
|
||||
<!-- ci_jobs: begin -->
|
||||
| Set | Workflow | Job | Configurations |
|
||||
|-----|----------|-----|----------------|
|
||||
| PR set | macos.yml | `macos-15` | 16.0; 16.4; 26.3 |
|
||||
| PR set | macos.yml | `macos-26` | 26.6 |
|
||||
| PR set | macos.yml | `xcode_standards` | 14 |
|
||||
| PR set | macos.yml | `swiftpm` | - |
|
||||
| PR set | ubuntu.yml | `ci_test_gcc` | - |
|
||||
| PR set | ubuntu.yml | `ci_meson_install` | - |
|
||||
| PR set | ubuntu.yml | `ci_static_analysis_ubuntu` | ci_cpplint |
|
||||
@@ -53,15 +49,6 @@ every later push to that pull request. A pull request is merged only when the fu
|
||||
| PR set | ubuntu.yml | `ci_test_standards_gcc` | 26 |
|
||||
| PR set | ubuntu.yml | `ci_test_standards_clang` | 26, libcxx |
|
||||
| PR set | ubuntu.yml | `ci_emscripten` | - |
|
||||
| PR set | windows.yml | `mingw` | x86 |
|
||||
| PR set | windows.yml | `msvc` | Debug, x64, default; Release, x64, default; Debug, Win32, default; Release, Win32, latest |
|
||||
| PR set | windows.yml | `msvc-vs2026` | Debug, x64, latest; Release, Win32, default |
|
||||
| PR set | windows.yml | `msvc-arm64` | Debug |
|
||||
| PR set | windows.yml | `clang` | 20.1.8 |
|
||||
| PR set | windows.yml | `ci_module_cpp20` | - |
|
||||
| full set | macos.yml | `macos-15_full` | 16.1; 16.2; 16.3; 26.0.1; 26.1.1; 26.2 |
|
||||
| full set | macos.yml | `macos-26_full` | 26.4.1; 26.5 |
|
||||
| full set | macos.yml | `xcode_standards_full` | 11; 17; 20; 23; 26 |
|
||||
| full set | ubuntu.yml | `ci_static_analysis_ubuntu_full` | ci_test_single_header; ci_cppcheck; ci_reproducible_tests; ci_non_git_tests; ci_offline_testdata |
|
||||
| full set | ubuntu.yml | `ci_static_analysis_clang_full` | ci_clang_analyze |
|
||||
| full set | ubuntu.yml | `ci_cmake_options_full` | ci_cmake_flags; ci_test_noimplicitconversions; ci_test_legacycomparison; ci_test_disableenumserialization; ci_test_disabletuplereferenceconversion; ci_test_skiplibraryversioncheck; ci_test_simdutf; ci_test_strict_nul_handling; ci_test_delete_deprecated_functions; ci_test_no_thread_local |
|
||||
@@ -71,11 +58,6 @@ every later push to that pull request. A pull request is merged only when the fu
|
||||
| full set | ubuntu.yml | `ci_test_standards_gcc_full` | 11; 14; 17; 20; 23 |
|
||||
| full set | ubuntu.yml | `ci_test_standards_clang_full` | 11, libcxx; 11, libstdcxx; 14, libcxx; 14, libstdcxx; 17, libcxx; 17, libstdcxx; 20, libcxx; 20, libstdcxx; 23, libcxx; 23, libstdcxx; 26, libstdcxx |
|
||||
| full set | ubuntu.yml | `ci_module_cpp20` | gcc:latest; silkeh/clang:latest |
|
||||
| full set | windows.yml | `msvc_full` | Debug, Win32, latest; Debug, x64, latest; Release, Win32, default; Release, x64, latest |
|
||||
| full set | windows.yml | `msvc-vs2026_full` | Debug, Win32, default; Debug, Win32, latest; Debug, x64, default; Release, Win32, latest; Release, x64, default; Release, x64, latest |
|
||||
| full set | windows.yml | `msvc-arm64_full` | Release |
|
||||
| full set | windows.yml | `clang_full` | 11.0.1; 12.0.1; 13.0.1; 14.0.6; 15.0.7; 16.0.6; 18.1.8; 19.1.7 |
|
||||
| full set | windows.yml | `clang-cl_full` | Win32; x64 |
|
||||
| nightly set | ubuntu.yml | `ci_infer` | - |
|
||||
| nightly set | ubuntu.yml | `ci_static_analysis_ubuntu_nightly` | ci_test_valgrind |
|
||||
| nightly set | ubuntu.yml | `ci_test_coverage` | - |
|
||||
@@ -84,7 +66,17 @@ every later push to that pull request. A pull request is merged only when the fu
|
||||
| nightly set | ubuntu.yml | `ci_icpx` | - |
|
||||
| nightly set | ubuntu.yml | `ci_nvhpc` | - |
|
||||
| nightly set | ubuntu.yml | `ci_test_arm64` | - |
|
||||
| nightly set | windows.yml | `mingw_nightly` | x64 |
|
||||
| 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
|
||||
|
||||
@@ -149,10 +149,12 @@ document: `#!cpp auto v = json_document::parse(text).root();` does not compile.
|
||||
end must still fit.
|
||||
- **A stream is always read to its end.** There is no partial/streaming read of an `#!cpp std::istream`: the stream
|
||||
is read to its end into a buffer the document owns, and parsing starts after that.
|
||||
- **No source positions on `materialize()`.** Even with
|
||||
- **Fewer source positions after edits.** With
|
||||
[`JSON_DIAGNOSTIC_POSITIONS`](../api/macros/json_diagnostic_positions.md) enabled,
|
||||
[`materialize()`](../api/basic_json_view/materialize.md) does not set them. The index has the start offset of every
|
||||
value, but not the end of arrays, objects, and escaped strings, which the positions also need.
|
||||
[`materialize()`](../api/basic_json_view/materialize.md) sets the same positions as `#!cpp basic_json::parse()`.
|
||||
In an [editable document](../api/json_editable_document.md), a value written by an edit has none, and neither has
|
||||
an array or object whose elements changed or whose last element has none; a `basic_json` value keeps its positions
|
||||
when it is changed. See [`materialize()`](../api/basic_json_view/materialize.md#notes).
|
||||
- **Objects iterate in document order.** [`begin()`](../api/basic_json_view/begin.md)/
|
||||
[`end()`](../api/basic_json_view/end.md) and [`items()`](../api/basic_json_view/items.md) visit an object's
|
||||
members in the order they appear in the source text. `basic_json`'s default `object_t` is a `std::map`, which
|
||||
|
||||
@@ -64,6 +64,9 @@ bool parse_error(std::size_t position, const std::string& last_token, const json
|
||||
|
||||
The return value of each function determines whether parsing should proceed.
|
||||
|
||||
Object keys are passed to `key` decoded and validated: escape sequences are already resolved, so different spellings of
|
||||
the same name yield the same string. See [`key`](../../api/json_sax/key.md#notes) for the exact guarantees.
|
||||
|
||||
## Stepping through the events
|
||||
|
||||
The events for a small input, step by step, and what two different handlers do with them. Press *Play* or step with *Next*:
|
||||
|
||||
@@ -203,7 +203,7 @@ packet-beta
|
||||
| 2-3 | `extra` | `uint16_t` | numbers: the number of integer digits (low byte) and fraction digits (high byte), 255 for more; objects: the number of their key hash index (1-based), or 0; otherwise 0 |
|
||||
| 4-7 | `off` | `uint32_t` | where the value starts: the first byte after a string's opening quote (or its position in the buffer of decoded strings), the first byte of a number or literal, the bracket of an array or object |
|
||||
| 8-11 | `len` | `uint32_t` | strings: the length after decoding; floats and literals: the length of the token; arrays and objects: the number of elements |
|
||||
| 12-15 | `next` | `uint32_t` | arrays and objects: the number of nodes of the subtree, including the node itself |
|
||||
| 12-15 | `next` | `uint32_t` | arrays and objects: the number of nodes of the subtree, including the node itself; strings with escapes: the position of the opening quote in the source plus one if the document was parsed with [`JSON_DIAGNOSTIC_POSITIONS`](../api/macros/json_diagnostic_positions.md), or 0 if it was not; all other values: 0 |
|
||||
|
||||
<p class="nj-table-caption" markdown>Fields of a node, by their byte offset within the 16-byte node. Integers use bytes 8-15 differently, see below.</p>
|
||||
|
||||
@@ -218,6 +218,12 @@ packet-beta
|
||||
step from element to element this way and skip whole subtrees in constant time.
|
||||
- **Offsets** are 32 bits wide, so a document is limited to 4294967279 bytes, 4 GiB minus 16 bytes (a margin below
|
||||
2^32 for positions one scanner step past the end of the text; `out_of_range.416`).
|
||||
- **Source positions** ([`JSON_DIAGNOSTIC_POSITIONS`](../api/macros/json_diagnostic_positions.md)) are derived by
|
||||
[`materialize()`](../api/basic_json_view/materialize.md) and cost almost nothing while parsing: the only work is
|
||||
storing the offset of the opening quote of a string with escapes in its `next`, which is otherwise unused. A
|
||||
scalar's range follows from `off` and its length, a string with escapes is scanned from its opening quote (`next`) to the closing one,
|
||||
and the end of an array or object is found by scanning from the end of its last element over whitespace, comments,
|
||||
and a trailing comma. `materialize()` hands these positions to the SAX builder of `parse()` in place of its lexer.
|
||||
- **Large objects** (128 members or more) get a hash index for their keys after parsing
|
||||
([`detail/view/object_index.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/view/object_index.hpp)):
|
||||
an open-addressing table whose slots hold the distance from the object's node to a key's node, so that a lookup does
|
||||
@@ -277,7 +283,7 @@ Images ([`save`](../api/basic_json_document/save.md) and [`load`](../api/basic_j
|
||||
the nodes as they are: a 64-byte header (the magic bytes `NJVI`, a format version, the sizes, and reserved bytes that
|
||||
must be zero), the nodes, the text, and the decoded strings. An edited document is first written in document order, as
|
||||
the parser would have written it (without links), and the numbers of the hash indexes are cleared, since `load`
|
||||
rebuilds the indexes. So a change of the node layout is a change of the image format: it must raise `image_version`,
|
||||
rebuilds the indexes. The flag *new* is kept, as these values have no position in the text. So a change of the node layout is a change of the image format: it must raise `image_version`,
|
||||
and `load` then rejects images of other versions (`parse_error.116`) instead of misreading them.
|
||||
|
||||
## Input adapters
|
||||
|
||||
@@ -22,8 +22,8 @@ The class contains a port of the shortest double-to-decimal conversion of [Żmij
|
||||
|
||||
The class contains a copy of [Hedley](https://nemequ.github.io/hedley/) from Evan Nemerson which is licensed as [CC0-1.0](https://creativecommons.org/publicdomain/zero/1.0/).
|
||||
|
||||
The class contains an adapted version of the Eisel-Lemire algorithm, its table of powers of five, and its digit comparison for long numbers from [fast_float](https://github.com/fastfloat/fast_float) by Daniel Lemire and contributors, which is available under the [MIT License](https://opensource.org/licenses/MIT) (used here), the Apache 2.0 License, and the Boost Software License. Copyright © 2021 The fast_float authors
|
||||
The class contains an adapted version of the Eisel-Lemire algorithm, its table of powers of five, its digit comparison for long numbers, and its parsing of eight digits at a time from [fast_float](https://github.com/fastfloat/fast_float) by Daniel Lemire and contributors, which is available under the [MIT License](https://opensource.org/licenses/MIT) (used here), the Apache 2.0 License, and the Boost Software License. Copyright © 2021 The fast_float authors
|
||||
|
||||
The view's parser (`<nlohmann/json_view.hpp>`) contains techniques and code adapted from [yyjson](https://github.com/ibireme/yyjson) by YaoYuan, which is licensed under the [MIT License](https://opensource.org/licenses/MIT) (see above): table-driven decoding of `\u` escapes and fixed-offset unrolled checks.
|
||||
|
||||
The view's parser (`<nlohmann/json_view.hpp>`) validates non-ASCII strings with the vector UTF-8 check of [simdjson](https://github.com/simdjson/simdjson) by Daniel Lemire, Geoff Langdale, John Keiser, and contributors (its "lookup4" algorithm and tables, after J. Keiser and D. Lemire, "Validating UTF-8 In Less Than One Instruction Per Byte", 2021), which is available under the [MIT License](https://opensource.org/licenses/MIT) (used here) and the Apache 2.0 License. Copyright © 2018-2025 The simdjson authors
|
||||
The class's lexer and the view's parser (`<nlohmann/json_view.hpp>`) validate non-ASCII strings with the vector UTF-8 check of [simdjson](https://github.com/simdjson/simdjson) by Daniel Lemire, Geoff Langdale, John Keiser, and contributors (its "lookup4" algorithm and tables, after J. Keiser and D. Lemire, "Validating UTF-8 In Less Than One Instruction Per Byte", 2021), which is available under the [MIT License](https://opensource.org/licenses/MIT) (used here) and the Apache 2.0 License. Copyright © 2018-2025 The simdjson authors
|
||||
@@ -19,6 +19,11 @@
|
||||
#include <type_traits> // conditional
|
||||
|
||||
|
||||
// The shortest-digit conversion of doubles (see zmij.hpp) and the writing of
|
||||
// its digits, including the conversion of the digits to text by Xiang JunBo
|
||||
// and the SIMD instruction sequence by Dougall Johnson, are ported from Zmij
|
||||
// by Victor Zverovich (https://github.com/vitaut/zmij, MIT license).
|
||||
//
|
||||
// SSE2 (every x86-64 CPU) and NEON (every 64-bit Arm CPU) convert the 16
|
||||
// digits of a double at once (unless JSON_NO_SIMD selects the portable code)
|
||||
#if !defined(JSON_NO_SIMD) && (defined(__x86_64__) || (defined(_M_X64) && !defined(_M_ARM64EC)))
|
||||
|
||||
@@ -230,7 +230,10 @@ struct diagnostic_positions
|
||||
{
|
||||
// Lexer has read the first character of the container, so
|
||||
// subtract 1 from the position to get the correct start position.
|
||||
v.start_position = lexer->get_chars_read_total() - 1;
|
||||
// (npos: a value without a source position, see materialize() of
|
||||
// json_view)
|
||||
const std::size_t read = lexer->get_chars_read_total();
|
||||
v.start_position = read == std::string::npos ? std::string::npos : read - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,6 +248,12 @@ struct diagnostic_positions
|
||||
{
|
||||
// Lexer's position is past the closing brace or bracket, so set that as the end position.
|
||||
v.end_position = lexer->get_chars_read_total();
|
||||
if (v.end_position == std::string::npos)
|
||||
{
|
||||
// a range has both ends or none (exception::diagnostics()
|
||||
// prints only complete ones)
|
||||
v.start_position = std::string::npos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,6 +270,12 @@ struct diagnostic_positions
|
||||
// The start position will be set below based on the length of the string representation
|
||||
// of the value.
|
||||
v.end_position = lexer->get_chars_read_total();
|
||||
if (v.end_position == std::string::npos)
|
||||
{
|
||||
// a value without a source position
|
||||
v.start_position = std::string::npos;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (v.type())
|
||||
{
|
||||
@@ -473,8 +488,12 @@ read up to it.
|
||||
@tparam Collect whether to collect the values of containers of unknown size on
|
||||
a stack (only for a caller that does not use the value after a
|
||||
parse error)
|
||||
@tparam LexerType the type of the lexer that reports the source positions
|
||||
(JSON_DIAGNOSTIC_POSITIONS); only diagnostic_positions uses
|
||||
it, so any type with its three functions works (as
|
||||
materialize() of json_view uses it)
|
||||
*/
|
||||
template<typename BasicJsonType, typename InputAdapterType, bool Collect = false>
|
||||
template<typename BasicJsonType, typename InputAdapterType, bool Collect = false, typename LexerType = lexer<BasicJsonType, InputAdapterType>>
|
||||
class json_sax_dom_parser : public json_sax_dom_error_state
|
||||
{
|
||||
public:
|
||||
@@ -483,7 +502,7 @@ class json_sax_dom_parser : public json_sax_dom_error_state
|
||||
using number_float_t = typename BasicJsonType::number_float_t;
|
||||
using string_t = typename BasicJsonType::string_t;
|
||||
using binary_t = typename BasicJsonType::binary_t;
|
||||
using lexer_t = lexer<BasicJsonType, InputAdapterType>;
|
||||
using lexer_t = LexerType;
|
||||
|
||||
/*!
|
||||
@param[in,out] r reference to a JSON value that is manipulated while
|
||||
|
||||
@@ -44,6 +44,13 @@
|
||||
// use std::from_chars or std::strtold. They are free functions so the lexer
|
||||
// stays focused on scanning (see lexer::convert_number()) and so that other
|
||||
// parsers of JSON text can convert tokens exactly like it does.
|
||||
//
|
||||
// The Eisel-Lemire conversion (eisel_lemire()), the digit comparison for the
|
||||
// cases it cannot decide, and the eight-digits-at-a-time helpers
|
||||
// (is_eight_digits(), parse_eight_digits()) are adapted from fast_float
|
||||
// (https://github.com/fastfloat/fast_float, Daniel Lemire and contributors,
|
||||
// available under the MIT, Apache 2.0, and Boost Software licenses; used here
|
||||
// under the MIT license). The table of powers of five is in pow5_table.hpp.
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
namespace detail
|
||||
|
||||
@@ -56,8 +56,8 @@ using parser_callback_t =
|
||||
template<typename SAX>
|
||||
struct sax_ignores_number_text : std::false_type {};
|
||||
|
||||
template<typename BasicJsonType, typename InputAdapterType, bool Collect>
|
||||
struct sax_ignores_number_text<json_sax_dom_parser<BasicJsonType, InputAdapterType, Collect>> : std::true_type {};
|
||||
template<typename BasicJsonType, typename InputAdapterType, bool Collect, typename LexerType>
|
||||
struct sax_ignores_number_text<json_sax_dom_parser<BasicJsonType, InputAdapterType, Collect, LexerType>> : std::true_type {};
|
||||
|
||||
template<typename BasicJsonType, typename InputAdapterType>
|
||||
struct sax_ignores_number_text<json_sax_dom_callback_parser<BasicJsonType, InputAdapterType>> : std::true_type {};
|
||||
|
||||
@@ -30,6 +30,13 @@
|
||||
// NLOHMANN_JSON_NO_TARGET_ATTRIBUTE (for compilers that ignore the target
|
||||
// attribute and then reject the SSSE3 intrinsics) keeps the check portable
|
||||
// without disabling the SSE2 code.
|
||||
//
|
||||
// The vector UTF-8 check (utf8_lookup4) is adapted from simdjson
|
||||
// (https://github.com/simdjson/simdjson, Daniel Lemire, Geoff Langdale, John
|
||||
// Keiser, and contributors, available under the Apache 2.0 and MIT licenses;
|
||||
// used here under the MIT license): the "lookup4" algorithm and tables of
|
||||
// J. Keiser and D. Lemire, "Validating UTF-8 In Less Than One Instruction Per
|
||||
// Byte" (2021).
|
||||
#if !defined(JSON_NO_SIMD) && defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) && defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
#include <arm_neon.h>
|
||||
#define JSON_SCAN_NEON 1
|
||||
|
||||
@@ -266,6 +266,13 @@ class json_pointer
|
||||
/// @sa https://json.nlohmann.me/api/json_pointer/operator_slasheq/
|
||||
json_pointer& operator/=(const json_pointer& ptr)
|
||||
{
|
||||
// insert() must not be given iterators into the vector it inserts
|
||||
// into: append a copy when a pointer is appended to itself
|
||||
if (JSON_HEDLEY_UNLIKELY(&ptr == this))
|
||||
{
|
||||
return *this /= json_pointer(ptr);
|
||||
}
|
||||
|
||||
reference_tokens.insert(reference_tokens.end(),
|
||||
ptr.reference_tokens.begin(),
|
||||
ptr.reference_tokens.end());
|
||||
|
||||
@@ -173,31 +173,16 @@ class builder
|
||||
}
|
||||
if (p[1] == '/')
|
||||
{
|
||||
p += 2;
|
||||
// (as in parse(), a null byte is the end of the input, so it is
|
||||
// left for the caller to see)
|
||||
while (p != m_end && *p != '\n' && *p != '\r' && !(NulIsEnd && *p == 0))
|
||||
{
|
||||
++p;
|
||||
}
|
||||
return p;
|
||||
return skip_line_comment<NulIsEnd>(p + 2, m_end);
|
||||
}
|
||||
if (p[1] == '*')
|
||||
{
|
||||
p += 2;
|
||||
for (;;)
|
||||
p = find_block_comment_end<NulIsEnd>(p + 2, m_end);
|
||||
if (p == m_end || *p != '*')
|
||||
{
|
||||
if (p == m_end || (NulIsEnd && *p == 0))
|
||||
{
|
||||
return fail_at(error_code::comment_unterminated, p);
|
||||
}
|
||||
if (*p == '*' && p + 1 != m_end && p[1] == '/')
|
||||
{
|
||||
p += 2;
|
||||
return p;
|
||||
}
|
||||
++p;
|
||||
return fail_at(error_code::comment_unterminated, p);
|
||||
}
|
||||
return p + 2;
|
||||
}
|
||||
++p;
|
||||
return fail_at(error_code::comment_start, p);
|
||||
@@ -1193,7 +1178,14 @@ indent_done:
|
||||
return false;
|
||||
}
|
||||
p = r.p;
|
||||
push_node(node_kind::string, node_flags::escaped, 0, r.start, r.len);
|
||||
#if JSON_DIAGNOSTIC_POSITIONS
|
||||
// the decoded string is in the arena: next keeps the source offset
|
||||
// of the opening quote plus one (0: unknown) for materialize()
|
||||
const std::uint64_t second = static_cast<std::uint64_t>(r.len) | (static_cast<std::uint64_t>(s - b) << 32);
|
||||
#else
|
||||
const std::uint64_t second = r.len;
|
||||
#endif
|
||||
push_node(node_kind::string, node_flags::escaped, 0, r.start, second);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -26,6 +26,7 @@
|
||||
#include <nlohmann/detail/view/lookup.hpp>
|
||||
#include <nlohmann/detail/view/macro_scope.hpp>
|
||||
#include <nlohmann/detail/view/node.hpp>
|
||||
#include <nlohmann/detail/view/scan.hpp>
|
||||
#include <nlohmann/detail/view/view_mode.hpp>
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
@@ -46,10 +47,9 @@ struct first_true<false, Conditions...> : std::integral_constant < int, 1 + firs
|
||||
/// (type_error.316 with the same message), so that an editable document
|
||||
/// only holds valid UTF-8: the error is at the first byte that no
|
||||
/// well-formed sequence can continue with (Unicode, Table 3-7).
|
||||
inline void check_utf8(const char* s, std::size_t n)
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE void check_utf8_bytewise(const unsigned char* p, std::size_t n, std::size_t i)
|
||||
{
|
||||
const auto* const p = reinterpret_cast<const unsigned char*>(s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
for (std::size_t i = 0; i < n;)
|
||||
while (i < n)
|
||||
{
|
||||
const unsigned char c = p[i];
|
||||
if (c < 0x80)
|
||||
@@ -79,6 +79,43 @@ inline void check_utf8(const char* s, std::size_t n)
|
||||
}
|
||||
}
|
||||
|
||||
/// The long-string path of check_utf8(): skips well-formed runs with the
|
||||
/// parser's string scan (which stops at a quote, a backslash, or a control
|
||||
/// character, all legal here, and at ill-formed UTF-8); the byte-wise check
|
||||
/// runs only from where the scan found ill-formed UTF-8, to report it.
|
||||
NLOHMANN_VIEW_NOINLINE inline void check_utf8_scan(const unsigned char* p, std::size_t n)
|
||||
{
|
||||
const unsigned char* const e = p + n;
|
||||
const unsigned char* q = p;
|
||||
for (;;)
|
||||
{
|
||||
q = scan_string_run<true>(q, e);
|
||||
if (q == e)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (*q >= 0x80)
|
||||
{
|
||||
check_utf8_bytewise(p, n, static_cast<std::size_t>(q - p));
|
||||
return; // not reached: the scan stopped at ill-formed UTF-8
|
||||
}
|
||||
++q; // a quote, backslash, or control character
|
||||
}
|
||||
}
|
||||
|
||||
inline void check_utf8(const char* s, std::size_t n)
|
||||
{
|
||||
const auto* const p = reinterpret_cast<const unsigned char*>(s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
if (NLOHMANN_VIEW_UNLIKELY(n >= 32))
|
||||
{
|
||||
check_utf8_scan(p, n);
|
||||
return;
|
||||
}
|
||||
// short strings: the vector scan costs more than it saves; (the loop is
|
||||
// inlined with a constant start, so this path is the plain byte-wise check)
|
||||
check_utf8_bytewise(p, n, 0);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief the edits of an editable basic_json_document
|
||||
|
||||
|
||||
@@ -80,7 +80,8 @@ constexpr std::uint64_t image_limit = 0xFFFFFFF0u;
|
||||
/// Copy the current structure of an edited document into nodes in document
|
||||
/// order, as the parser would have written them. Text written by edits is
|
||||
/// appended to text_tail (number tokens) and arena_tail (strings); floats that
|
||||
/// are not finite become null, as dump() writes them.
|
||||
/// are not finite become null, as dump() writes them. Nodes keep the flag
|
||||
/// is_new: they have no source position (JSON_DIAGNOSTIC_POSITIONS).
|
||||
inline void compact_nodes(const document_data& d, std::size_t arena_size, std::vector<node>& out, std::string& text_tail, std::string& arena_tail)
|
||||
{
|
||||
struct frame
|
||||
@@ -103,16 +104,18 @@ inline void compact_nodes(const document_data& d, std::size_t arena_size, std::v
|
||||
arena_tail.append(d.str(s), s.len);
|
||||
r.flags = node_flags::escaped;
|
||||
}
|
||||
r.flags = static_cast<std::uint8_t>(r.flags | (s.flags & node_flags::is_new));
|
||||
return r;
|
||||
};
|
||||
const auto copy_node = [&](const node * v)
|
||||
{
|
||||
node r = *v;
|
||||
const auto is_new = static_cast<std::uint8_t>(v->flags & node_flags::is_new);
|
||||
switch (static_cast<value_t>(v->kind))
|
||||
{
|
||||
case value_t::object:
|
||||
case value_t::array:
|
||||
r.flags = 0;
|
||||
r.flags = is_new;
|
||||
r.extra = 0;
|
||||
r.off = (v->flags & (node_flags::moved | node_flags::is_new)) != 0 ? 0 : v->off;
|
||||
r.len = 0; // counted below
|
||||
@@ -129,7 +132,7 @@ inline void compact_nodes(const document_data& d, std::size_t arena_size, std::v
|
||||
r.off = static_cast<std::uint32_t>(d.size + text_tail.size());
|
||||
text_tail.append(d.str(*v), number_length(*v));
|
||||
}
|
||||
r.flags = 0;
|
||||
r.flags = is_new;
|
||||
break;
|
||||
case value_t::number_float:
|
||||
if ((v->flags & node_flags::storage) == node_flags::edited)
|
||||
@@ -139,22 +142,23 @@ inline void compact_nodes(const document_data& d, std::size_t arena_size, std::v
|
||||
{
|
||||
r = node{}; // nan and infinity: null, as dump() writes them
|
||||
r.kind = node_kind::null;
|
||||
r.flags = is_new;
|
||||
break;
|
||||
}
|
||||
r.off = static_cast<std::uint32_t>(d.size + text_tail.size());
|
||||
text_tail.append(t, v->len);
|
||||
r.extra = 0xFFFFu; // the digit layout is not recorded
|
||||
}
|
||||
r.flags = 0;
|
||||
r.flags = is_new;
|
||||
break;
|
||||
case value_t::boolean:
|
||||
r.flags = static_cast<std::uint8_t>(v->flags & node_flags::is_true);
|
||||
r.flags = static_cast<std::uint8_t>(v->flags & (node_flags::is_true | node_flags::is_new));
|
||||
break;
|
||||
case value_t::null:
|
||||
case value_t::binary:
|
||||
case value_t::discarded:
|
||||
default:
|
||||
r.flags = 0;
|
||||
r.flags = is_new;
|
||||
break;
|
||||
}
|
||||
out.push_back(r);
|
||||
@@ -254,6 +258,81 @@ struct number_token
|
||||
bool is_float; ///< a fraction or an exponent
|
||||
};
|
||||
|
||||
/// advance over ASCII digits, eight bytes at a time
|
||||
inline const unsigned char* digit_run(const unsigned char* p, const unsigned char* e) noexcept
|
||||
{
|
||||
constexpr std::uint64_t ones = 0x0101010101010101ull;
|
||||
while (e - p >= 8)
|
||||
{
|
||||
// (the digits become 0..9; a byte above 9, or with bit 7 set, is no digit. A carry
|
||||
// out of a byte with bit 7 set only affects the bytes above, after the first one
|
||||
// that count_trailing_zeros finds.)
|
||||
const std::uint64_t x = read_eight_bytes(p) ^ (ones * 0x30u);
|
||||
const std::uint64_t not_digit = ((x + (ones * 0x76u)) | x) & (ones * 0x80u);
|
||||
if (not_digit != 0)
|
||||
{
|
||||
return p + (static_cast<std::size_t>(count_trailing_zeros(not_digit)) / 8);
|
||||
}
|
||||
p += 8;
|
||||
}
|
||||
while (p != e && is_digit(*p))
|
||||
{
|
||||
++p;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/// whether the n bytes at p are ASCII digits (n > 0)
|
||||
inline bool all_digits(const unsigned char* p, std::size_t n) noexcept
|
||||
{
|
||||
const auto digits = [](const unsigned char* q)
|
||||
{
|
||||
constexpr std::uint64_t ones = 0x0101010101010101ull;
|
||||
const std::uint64_t x = read_eight_bytes(q) ^ (ones * 0x30u);
|
||||
return (((x + (ones * 0x76u)) | x) & (ones * 0x80u)) == 0;
|
||||
};
|
||||
if (n >= 8)
|
||||
{
|
||||
for (std::size_t i = 0; i + 8 < n; i += 8)
|
||||
{
|
||||
if (!digits(p + i))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return digits(p + n - 8); // (overlapping the bytes before: they are digits)
|
||||
}
|
||||
for (std::size_t i = 0; i < n; ++i)
|
||||
{
|
||||
if (!is_digit(p[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Whether a float token has the shape that the parser records for most of
|
||||
/// them: [-]digits.digits, with the numbers of digits of the node's layout
|
||||
/// (extra). Then the token is a number of that layout, and the general scan
|
||||
/// that follows is not needed.
|
||||
inline bool plain_float_token(const unsigned char* s, std::size_t len, std::uint16_t extra) noexcept
|
||||
{
|
||||
const std::size_t int_digits = extra & 0xFFu;
|
||||
const std::size_t frac_digits = extra >> 8u;
|
||||
if (int_digits == 0 || int_digits >= 255 || frac_digits == 0 || frac_digits >= 255 || len == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const std::size_t negative = *s == '-' ? 1 : 0;
|
||||
if (len != negative + int_digits + 1 + frac_digits)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const unsigned char* const p = s + negative;
|
||||
return !(int_digits > 1 && *p == '0') && p[int_digits] == '.' && all_digits(p, int_digits) && all_digits(p + int_digits + 1, frac_digits);
|
||||
}
|
||||
|
||||
/// whether [s, s + len) is a JSON number (the grammar the parser accepts)
|
||||
inline bool scan_number_token(const unsigned char* s, std::size_t len, number_token& t)
|
||||
{
|
||||
@@ -272,11 +351,7 @@ inline bool scan_number_token(const unsigned char* s, std::size_t len, number_to
|
||||
}
|
||||
else if (*p >= '1' && *p <= '9')
|
||||
{
|
||||
do
|
||||
{
|
||||
++p;
|
||||
}
|
||||
while (p != e && is_digit(*p));
|
||||
p = digit_run(p + 1, e);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -288,10 +363,7 @@ inline bool scan_number_token(const unsigned char* s, std::size_t len, number_to
|
||||
if (p != e && *p == '.')
|
||||
{
|
||||
const unsigned char* const f0 = ++p;
|
||||
while (p != e && is_digit(*p))
|
||||
{
|
||||
++p;
|
||||
}
|
||||
p = digit_run(p, e);
|
||||
if (p == f0)
|
||||
{
|
||||
return false;
|
||||
@@ -402,10 +474,15 @@ struct float_range
|
||||
template<typename Check>
|
||||
bool check_distinct_ranges(std::vector<byte_range>& ranges, Check check)
|
||||
{
|
||||
std::sort(ranges.begin(), ranges.end(), [](const byte_range & a, const byte_range & b)
|
||||
// (save() writes the ranges in the order of the nodes, which is sorted already)
|
||||
const auto before = [](const byte_range & a, const byte_range & b)
|
||||
{
|
||||
return a.off != b.off ? a.off < b.off : a.len < b.len;
|
||||
});
|
||||
};
|
||||
if (!std::is_sorted(ranges.begin(), ranges.end(), before))
|
||||
{
|
||||
std::sort(ranges.begin(), ranges.end(), before);
|
||||
}
|
||||
std::size_t end = 0;
|
||||
for (std::size_t i = 0; i < ranges.size(); ++i)
|
||||
{
|
||||
@@ -427,7 +504,8 @@ bool check_distinct_ranges(std::vector<byte_range>& ranges, Check check)
|
||||
/// record layouts that match it), tokens must not overlap.
|
||||
inline bool check_float_ranges(std::vector<float_range>& ranges, const unsigned char* text)
|
||||
{
|
||||
std::sort(ranges.begin(), ranges.end(), [](const float_range & a, const float_range & b)
|
||||
// (save() writes the tokens in the order of the nodes, which is sorted already)
|
||||
const auto before = [](const float_range & a, const float_range & b)
|
||||
{
|
||||
if (a.off != b.off)
|
||||
{
|
||||
@@ -438,7 +516,11 @@ inline bool check_float_ranges(std::vector<float_range>& ranges, const unsigned
|
||||
return a.len < b.len;
|
||||
}
|
||||
return a.extra < b.extra;
|
||||
});
|
||||
};
|
||||
if (!std::is_sorted(ranges.begin(), ranges.end(), before))
|
||||
{
|
||||
std::sort(ranges.begin(), ranges.end(), before);
|
||||
}
|
||||
std::size_t end = 0;
|
||||
std::size_t i = 0;
|
||||
while (i < ranges.size())
|
||||
@@ -448,14 +530,18 @@ inline bool check_float_ranges(std::vector<float_range>& ranges, const unsigned
|
||||
{
|
||||
return false;
|
||||
}
|
||||
number_token t{};
|
||||
const unsigned char* const s = text + r.off;
|
||||
if (!scan_number_token(s, r.len, t) || !float_token_finite(s, r.len, t))
|
||||
std::uint16_t layout = r.extra;
|
||||
if (!plain_float_token(s, r.len, r.extra))
|
||||
{
|
||||
return false;
|
||||
number_token t{};
|
||||
if (!scan_number_token(s, r.len, t) || !float_token_finite(s, r.len, t))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// the digit layout the parser records (or "many", as compaction writes it)
|
||||
layout = float_layout(t);
|
||||
}
|
||||
// the digit layout the parser records (or "many", as compaction writes it)
|
||||
const std::uint16_t layout = float_layout(t);
|
||||
for (; i < ranges.size() && ranges[i].off == r.off && ranges[i].len == r.len; ++i)
|
||||
{
|
||||
if (ranges[i].extra != layout && ranges[i].extra != 0xFFFFu)
|
||||
@@ -515,13 +601,15 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha
|
||||
std::vector<float_range> floats;
|
||||
const auto check_string = [&](const node & n) -> bool
|
||||
{
|
||||
if ((n.flags & ~node_flags::escaped) != 0 || n.extra != 0)
|
||||
if ((n.flags & ~(node_flags::escaped | node_flags::is_new)) != 0 || n.extra != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const bool decoded = (n.flags & node_flags::escaped) != 0;
|
||||
const std::size_t limit = decoded ? arena_size : text_size;
|
||||
if (n.off > limit || n.len > limit - n.off)
|
||||
// (next: the source offset of a decoded string's opening quote plus
|
||||
// one, or 0; see node)
|
||||
if (n.off > limit || n.len > limit - n.off || n.next > text_size)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -593,13 +681,13 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha
|
||||
{
|
||||
case value_t::null:
|
||||
// (the offset of a literal is read to size the output of dump())
|
||||
if (n.flags != 0 || n.extra != 0 || n.off > text_size)
|
||||
if ((n.flags & ~node_flags::is_new) != 0 || n.extra != 0 || n.off > text_size)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case value_t::boolean:
|
||||
if ((n.flags & ~node_flags::is_true) != 0 || n.extra != 0 || n.off > text_size)
|
||||
if ((n.flags & ~(node_flags::is_true | node_flags::is_new)) != 0 || n.extra != 0 || n.off > text_size)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -613,7 +701,7 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha
|
||||
case value_t::number_integer:
|
||||
case value_t::number_unsigned:
|
||||
case value_t::number_float:
|
||||
if (n.flags != 0 || !number_in_bounds(n))
|
||||
if ((n.flags & ~node_flags::is_new) != 0 || !number_in_bounds(n))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -635,7 +723,7 @@ inline bool check_image(const node* nodes, std::size_t count, const unsigned cha
|
||||
case value_t::object:
|
||||
{
|
||||
const std::size_t limit = stack.empty() ? count : stack.back().end;
|
||||
if (n.flags != 0 || n.extra != 0 || n.next == 0 || n.next > limit - i || n.off > text_size)
|
||||
if ((n.flags & ~node_flags::is_new) != 0 || n.extra != 0 || n.next == 0 || n.next > limit - i || n.off > text_size)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <nlohmann/detail/view/macro_scope.hpp>
|
||||
#include <nlohmann/detail/view/node.hpp>
|
||||
#include <nlohmann/detail/view/number.hpp>
|
||||
#include <nlohmann/detail/view/scan.hpp>
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
namespace detail
|
||||
@@ -26,6 +27,189 @@ namespace detail
|
||||
namespace view
|
||||
{
|
||||
|
||||
#if JSON_DIAGNOSTIC_POSITIONS
|
||||
/*!
|
||||
@brief the source positions of the values materialize() replays
|
||||
|
||||
Stands in for the lexer of json_sax_dom_parser, which reads the positions of
|
||||
the values it stores from these three functions (diagnostic_positions). npos
|
||||
is a value without a source position. One position_source serves a whole
|
||||
materialize() call: before each value is handed to the handler, the range of
|
||||
that value is set (or cleared), overwriting that of the value before.
|
||||
*/
|
||||
struct position_source
|
||||
{
|
||||
/// the token, of which diagnostic_positions only reads the length
|
||||
struct token
|
||||
{
|
||||
std::size_t length;
|
||||
std::size_t size() const noexcept
|
||||
{
|
||||
return length;
|
||||
}
|
||||
};
|
||||
|
||||
std::size_t start = std::string::npos; ///< the first byte of the next value
|
||||
std::size_t end = std::string::npos; ///< one past the next value (start_object/start_array: one past the bracket)
|
||||
|
||||
std::size_t get_chars_read_total() const noexcept
|
||||
{
|
||||
return end;
|
||||
}
|
||||
std::size_t get_token_start_position() const noexcept
|
||||
{
|
||||
return start;
|
||||
}
|
||||
token get_string() const noexcept
|
||||
{
|
||||
return token{end - start};
|
||||
}
|
||||
|
||||
/// the range [first, last), or none if either is npos
|
||||
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;
|
||||
}
|
||||
|
||||
/// no range: the next value has no source position
|
||||
void clear() noexcept
|
||||
{
|
||||
start = std::string::npos;
|
||||
end = std::string::npos;
|
||||
}
|
||||
|
||||
/// one past the closing quote of the string whose opening quote is at
|
||||
/// src[quote], or npos (the scan is bounded by size, as images can be
|
||||
/// loaded without a check)
|
||||
static std::size_t string_end(const char* src, std::size_t size, std::size_t quote) noexcept
|
||||
{
|
||||
for (std::size_t i = quote + 1; i < size; ++i)
|
||||
{
|
||||
if (src[i] == '\\')
|
||||
{
|
||||
++i;
|
||||
}
|
||||
else if (src[i] == '"')
|
||||
{
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
return std::string::npos;
|
||||
}
|
||||
|
||||
/// one past the closing bracket of the array or object whose last element
|
||||
/// ends at src[pos] (or whose opening bracket is at src[pos - 1]), or npos;
|
||||
/// in between, there can only be whitespace, comments, and a trailing comma
|
||||
/// (the source was validated when it was parsed)
|
||||
static std::size_t container_end(const char* src, std::size_t size, std::size_t pos) noexcept
|
||||
{
|
||||
const auto* const b = reinterpret_cast<const unsigned char*>(src); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast): the text as bytes, as the builder reads it
|
||||
const unsigned char* const e = b + size;
|
||||
while (pos < size)
|
||||
{
|
||||
switch (src[pos])
|
||||
{
|
||||
case ' ':
|
||||
case '\t':
|
||||
case '\n':
|
||||
case '\r':
|
||||
case ',':
|
||||
++pos;
|
||||
break;
|
||||
case ']':
|
||||
case '}':
|
||||
return pos + 1;
|
||||
case '/':
|
||||
if (pos + 1 < size && src[pos + 1] == '/')
|
||||
{
|
||||
pos = static_cast<std::size_t>(skip_line_comment(b + pos + 2, e) - b);
|
||||
break;
|
||||
}
|
||||
if (pos + 1 < size && src[pos + 1] == '*')
|
||||
{
|
||||
// (unterminated: pos is size, which ends the loop)
|
||||
pos = static_cast<std::size_t>(find_block_comment_end(b + pos + 2, e) - b) + 2;
|
||||
break;
|
||||
}
|
||||
return std::string::npos;
|
||||
default:
|
||||
return std::string::npos;
|
||||
}
|
||||
}
|
||||
return std::string::npos;
|
||||
}
|
||||
|
||||
/// the range of the scalar n of document d
|
||||
void set_scalar(const document_data& d, const node& n) noexcept
|
||||
{
|
||||
if ((n.flags & node_flags::is_new) != 0)
|
||||
{
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
std::size_t first = std::string::npos;
|
||||
std::size_t last = std::string::npos;
|
||||
switch (n.kind)
|
||||
{
|
||||
case node_kind::string:
|
||||
if ((n.flags & node_flags::storage) == 0)
|
||||
{
|
||||
// off is the first byte after the opening quote
|
||||
if (n.off != 0)
|
||||
{
|
||||
first = n.off - 1;
|
||||
last = n.off + std::size_t{n.len} + 1;
|
||||
}
|
||||
}
|
||||
else if ((n.flags & node_flags::storage) == node_flags::escaped && n.next != 0)
|
||||
{
|
||||
// next is the offset of the opening quote plus one
|
||||
first = n.next - 1;
|
||||
last = string_end(d.base[0], d.size, first);
|
||||
}
|
||||
break;
|
||||
case node_kind::number_integer:
|
||||
case node_kind::number_unsigned:
|
||||
case node_kind::number_float:
|
||||
if ((n.flags & node_flags::storage) == 0)
|
||||
{
|
||||
first = n.off;
|
||||
last = n.off + number_length(n);
|
||||
}
|
||||
break;
|
||||
case node_kind::boolean:
|
||||
first = n.off;
|
||||
last = n.off + ((n.flags & node_flags::is_true) != 0 ? 4u : 5u);
|
||||
break;
|
||||
case node_kind::null:
|
||||
first = n.off;
|
||||
last = n.off + 4u;
|
||||
break;
|
||||
case node_kind::object:
|
||||
case node_kind::array:
|
||||
case node_kind::link:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// (a crafted image can point past the text)
|
||||
if (last == std::string::npos || last > d.size)
|
||||
{
|
||||
clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
set(first, last);
|
||||
}
|
||||
}
|
||||
};
|
||||
#endif
|
||||
|
||||
/*!
|
||||
@brief the basic_json value of the subtree at n
|
||||
|
||||
@@ -33,14 +217,24 @@ The subtree is replayed into the SAX handler that parse() uses to build its
|
||||
values, so the result is the value parse() would produce: duplicate keys keep
|
||||
the last value, and with JSON_DIAGNOSTICS the parent pointers are set. It is
|
||||
iterative, so the nesting depth is limited by memory only, as for parse().
|
||||
Without a lexer the handler records no source positions
|
||||
(JSON_DIAGNOSTIC_POSITIONS).
|
||||
|
||||
With JSON_DIAGNOSTIC_POSITIONS, the values get their source positions as with
|
||||
parse(): position_source stands in for the lexer. Values written by edits have
|
||||
none (npos), and neither has an array or object whose elements changed, or
|
||||
whose last element has none (the end of its closing bracket is found by
|
||||
scanning from the end of the last element).
|
||||
*/
|
||||
template<typename BasicJsonType, bool Editable>
|
||||
BasicJsonType materialize(const document_data& d, const node* n)
|
||||
{
|
||||
using string_t = typename BasicJsonType::string_t;
|
||||
using sax_t = json_sax_dom_parser<BasicJsonType, iterator_input_adapter<const char*>, true>;
|
||||
using input_t = iterator_input_adapter<const char*>;
|
||||
#if JSON_DIAGNOSTIC_POSITIONS
|
||||
using lexer_t = position_source;
|
||||
#else
|
||||
using lexer_t = lexer<BasicJsonType, input_t>; // (the default: unused, as no lexer is passed)
|
||||
#endif
|
||||
using sax_t = json_sax_dom_parser<BasicJsonType, input_t, true, lexer_t>;
|
||||
using nav = navigation<Editable>;
|
||||
|
||||
// the open containers: their next element (or key of the next member),
|
||||
@@ -52,9 +246,15 @@ BasicJsonType materialize(const document_data& d, const node* n)
|
||||
const node* pos;
|
||||
const node* end;
|
||||
bool object;
|
||||
#if JSON_DIAGNOSTIC_POSITIONS
|
||||
std::size_t start; ///< the opening bracket (npos: unknown)
|
||||
std::size_t last_end; ///< the end of the last element read (npos: unknown)
|
||||
#endif
|
||||
};
|
||||
|
||||
#if !JSON_DIAGNOSTIC_POSITIONS
|
||||
// a scalar needs neither the handler nor the stack of open containers
|
||||
// (with positions, it goes through the handler, which sets them)
|
||||
switch (static_cast<value_t>(n->kind))
|
||||
{
|
||||
case value_t::string:
|
||||
@@ -76,10 +276,16 @@ BasicJsonType materialize(const document_data& d, const node* n)
|
||||
default:
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
|
||||
BasicJsonType result;
|
||||
#if JSON_DIAGNOSTIC_POSITIONS
|
||||
position_source positions;
|
||||
sax_t sax(result, true, &positions);
|
||||
#else
|
||||
sax_t sax(result, true);
|
||||
const string_t no_token{};
|
||||
#endif
|
||||
const string_t no_token {};
|
||||
// (shallow subtrees on the stack: a call for a small value then allocates only for the value)
|
||||
std::array<frame, 16> shallow; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): filled as containers open
|
||||
std::vector<frame> deep{};
|
||||
@@ -94,6 +300,16 @@ BasicJsonType materialize(const document_data& d, const node* n)
|
||||
std::size_t depth = 0;
|
||||
for (;;)
|
||||
{
|
||||
#if JSON_DIAGNOSTIC_POSITIONS
|
||||
if (!is_container(*n))
|
||||
{
|
||||
positions.set_scalar(d, *n);
|
||||
if (depth != 0)
|
||||
{
|
||||
open[depth - 1].last_end = positions.end;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// false positive: n comes from nav::value(), which never returns null for a valid index
|
||||
// @infer-ignore NULLPTR_DEREFERENCE
|
||||
switch (static_cast<value_t>(n->kind))
|
||||
@@ -102,6 +318,17 @@ BasicJsonType materialize(const document_data& d, const node* n)
|
||||
case value_t::array:
|
||||
{
|
||||
const bool object = n->kind == node_kind::object;
|
||||
#if JSON_DIAGNOSTIC_POSITIONS
|
||||
// (moved containers are also new)
|
||||
if ((n->flags & node_flags::is_new) != 0 || n->off >= d.size)
|
||||
{
|
||||
positions.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
positions.set(std::size_t{n->off}, std::size_t{n->off} + 1);
|
||||
}
|
||||
#endif
|
||||
if (object)
|
||||
{
|
||||
sax.start_object(n->len);
|
||||
@@ -118,7 +345,13 @@ BasicJsonType materialize(const document_data& d, const node* n)
|
||||
open = deep.data();
|
||||
capacity = deep.size();
|
||||
}
|
||||
open[depth++] = frame{nav::first(d, n), nav::end(d, n), object};
|
||||
// (with positions: until an element is read, the container's
|
||||
// own bracket is the last thing read)
|
||||
open[depth++] = frame{nav::first(d, n), nav::end(d, n), object
|
||||
#if JSON_DIAGNOSTIC_POSITIONS
|
||||
, positions.start, positions.end
|
||||
#endif
|
||||
};
|
||||
break;
|
||||
}
|
||||
case value_t::string:
|
||||
@@ -159,6 +392,31 @@ BasicJsonType materialize(const document_data& d, const node* n)
|
||||
frame& f = open[depth - 1];
|
||||
if (f.pos == f.end)
|
||||
{
|
||||
#if JSON_DIAGNOSTIC_POSITIONS
|
||||
if (f.start == std::string::npos || f.last_end == std::string::npos)
|
||||
{
|
||||
positions.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::size_t end = position_source::container_end(d.base[0], d.size, f.last_end);
|
||||
if (end == std::string::npos)
|
||||
{
|
||||
positions.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
// the closing bracket, as the lexer reports it (end_object()
|
||||
// and end_array() only read its end; the start of the
|
||||
// container was set when it was opened)
|
||||
positions.set(end - 1, end);
|
||||
}
|
||||
}
|
||||
if (depth > 1)
|
||||
{
|
||||
open[depth - 2].last_end = positions.end;
|
||||
}
|
||||
#endif
|
||||
if (f.object)
|
||||
{
|
||||
sax.end_object();
|
||||
|
||||
@@ -69,7 +69,7 @@ struct node
|
||||
std::uint16_t extra; ///< numbers: integer digits (low byte) and fraction digits (high byte), 255 = "many"; objects: number of the object's key hash index (1-based, 0 = none); otherwise 0
|
||||
std::uint32_t off; ///< source offset (string content, number token, literal, bracket); arena offset if escaped/edited; number of the element sequence if moved
|
||||
std::uint32_t len; ///< string: decoded bytes; float: token bytes; array/object: element count
|
||||
std::uint32_t next; ///< array/object: number of nodes of the subtree (its extent in the enclosing sequence)
|
||||
std::uint32_t next; ///< array/object: number of nodes of the subtree (its extent in the enclosing sequence); escaped string: source offset of the opening quote plus one (JSON_DIAGNOSTIC_POSITIONS; 0: unknown)
|
||||
};
|
||||
static_assert(sizeof(node) == 16, "node must stay 16 bytes");
|
||||
|
||||
|
||||
@@ -172,6 +172,32 @@ NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* skip_digits(const unsigned char
|
||||
return p;
|
||||
}
|
||||
|
||||
/// advance over the text of a line comment (after its "//") to the line
|
||||
/// break or the end (with NulIsEnd also a null byte, which is left for the
|
||||
/// caller to see, as in parse())
|
||||
template<bool NulIsEnd = false>
|
||||
inline const unsigned char* skip_line_comment(const unsigned char* p, const unsigned char* e) noexcept
|
||||
{
|
||||
while (p != e && *p != '\n' && *p != '\r' && !(NulIsEnd && *p == 0))
|
||||
{
|
||||
++p;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/// the "*/" that ends a block comment whose text starts at p (after its "/*");
|
||||
/// if it is unterminated, the end (with NulIsEnd also a null byte), which is
|
||||
/// where the caller reports the error
|
||||
template<bool NulIsEnd = false>
|
||||
inline const unsigned char* find_block_comment_end(const unsigned char* p, const unsigned char* e) noexcept
|
||||
{
|
||||
while (p != e && !(NulIsEnd && *p == 0) && !(*p == '*' && p + 1 != e && p[1] == '/'))
|
||||
{
|
||||
++p;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/// powers of ten up to 10^19 as integers
|
||||
inline std::uint64_t int_pow10(unsigned k) noexcept
|
||||
{
|
||||
|
||||
@@ -29,6 +29,13 @@
|
||||
// NLOHMANN_VIEW_NO_TARGET_ATTRIBUTE (set by the GCC module interface, where GCC
|
||||
// ignores the target attribute and then rejects the SSSE3 intrinsics) keeps the
|
||||
// check portable without disabling the SSE2 code.
|
||||
//
|
||||
// The vector UTF-8 check (utf8_lookup4) is adapted from simdjson
|
||||
// (https://github.com/simdjson/simdjson, Daniel Lemire, Geoff Langdale, John
|
||||
// Keiser, and contributors, available under the Apache 2.0 and MIT licenses;
|
||||
// used here under the MIT license): the "lookup4" algorithm and tables of
|
||||
// J. Keiser and D. Lemire, "Validating UTF-8 In Less Than One Instruction Per
|
||||
// Byte" (2021).
|
||||
#if !defined(JSON_NO_SIMD) && defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) && NLOHMANN_VIEW_LITTLE_ENDIAN
|
||||
#include <arm_neon.h>
|
||||
#define NLOHMANN_VIEW_NEON 1
|
||||
|
||||
@@ -159,7 +159,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
friend class ::nlohmann::detail::binary_writer;
|
||||
template<typename BasicJsonType, typename InputType, typename SAX>
|
||||
friend class ::nlohmann::detail::binary_reader;
|
||||
template<typename BasicJsonType, typename InputAdapterType, bool Collect>
|
||||
template<typename BasicJsonType, typename InputAdapterType, bool Collect, typename LexerType>
|
||||
friend class ::nlohmann::detail::json_sax_dom_parser;
|
||||
template<typename BasicJsonType, typename InputAdapterType>
|
||||
friend class ::nlohmann::detail::json_sax_dom_binary_parser;
|
||||
@@ -6950,6 +6950,25 @@ public:
|
||||
/// @sa https://json.nlohmann.me/api/basic_json/patch/
|
||||
void patch_inplace(const basic_json& json_patch)
|
||||
{
|
||||
// the operations below change *this while iterating json_patch: apply
|
||||
// a copy when a value is applied as patch to itself or, with
|
||||
// JSON_DIAGNOSTICS, to a value stored inside it
|
||||
// NOLINTNEXTLINE(misc-const-correctness): only the JSON_DIAGNOSTICS build modifies it
|
||||
bool patch_aliases_this = (&json_patch == this);
|
||||
#if JSON_DIAGNOSTICS
|
||||
for (const basic_json* p = json_patch.m_parent; !patch_aliases_this && p != nullptr; p = p->m_parent)
|
||||
{
|
||||
patch_aliases_this = (p == this);
|
||||
}
|
||||
#endif
|
||||
if (JSON_HEDLEY_UNLIKELY(patch_aliases_this))
|
||||
{
|
||||
// NOLINTNEXTLINE(performance-unnecessary-copy-initialization): *this is modified below
|
||||
const basic_json json_patch_copy = json_patch;
|
||||
patch_inplace(json_patch_copy);
|
||||
return;
|
||||
}
|
||||
|
||||
basic_json& result = *this;
|
||||
// the valid JSON Patch operations
|
||||
enum class patch_operations {add, remove, replace, move, copy, test, invalid};
|
||||
|
||||
@@ -763,8 +763,12 @@ class basic_json_view
|
||||
template<typename, view_mode> friend class basic_json_document;
|
||||
template<typename, view_mode> friend class basic_json_view;
|
||||
template<typename, typename> friend class detail::view::editor;
|
||||
template<typename, typename> friend class detail::view::view_side;
|
||||
friend iterator;
|
||||
|
||||
/// 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)
|
||||
{}
|
||||
|
||||
Generated
+70
-6
@@ -10970,6 +10970,13 @@ NLOHMANN_JSON_NAMESPACE_END
|
||||
// use std::from_chars or std::strtold. They are free functions so the lexer
|
||||
// stays focused on scanning (see lexer::convert_number()) and so that other
|
||||
// parsers of JSON text can convert tokens exactly like it does.
|
||||
//
|
||||
// The Eisel-Lemire conversion (eisel_lemire()), the digit comparison for the
|
||||
// cases it cannot decide, and the eight-digits-at-a-time helpers
|
||||
// (is_eight_digits(), parse_eight_digits()) are adapted from fast_float
|
||||
// (https://github.com/fastfloat/fast_float, Daniel Lemire and contributors,
|
||||
// available under the MIT, Apache 2.0, and Boost Software licenses; used here
|
||||
// under the MIT license). The table of powers of five is in pow5_table.hpp.
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
namespace detail
|
||||
@@ -12078,6 +12085,13 @@ NLOHMANN_JSON_NAMESPACE_END
|
||||
// NLOHMANN_JSON_NO_TARGET_ATTRIBUTE (for compilers that ignore the target
|
||||
// attribute and then reject the SSSE3 intrinsics) keeps the check portable
|
||||
// without disabling the SSE2 code.
|
||||
//
|
||||
// The vector UTF-8 check (utf8_lookup4) is adapted from simdjson
|
||||
// (https://github.com/simdjson/simdjson, Daniel Lemire, Geoff Langdale, John
|
||||
// Keiser, and contributors, available under the Apache 2.0 and MIT licenses;
|
||||
// used here under the MIT license): the "lookup4" algorithm and tables of
|
||||
// J. Keiser and D. Lemire, "Validating UTF-8 In Less Than One Instruction Per
|
||||
// Byte" (2021).
|
||||
#if !defined(JSON_NO_SIMD) && defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) && defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
#include <arm_neon.h>
|
||||
#define JSON_SCAN_NEON 1
|
||||
@@ -15629,7 +15643,10 @@ struct diagnostic_positions
|
||||
{
|
||||
// Lexer has read the first character of the container, so
|
||||
// subtract 1 from the position to get the correct start position.
|
||||
v.start_position = lexer->get_chars_read_total() - 1;
|
||||
// (npos: a value without a source position, see materialize() of
|
||||
// json_view)
|
||||
const std::size_t read = lexer->get_chars_read_total();
|
||||
v.start_position = read == std::string::npos ? std::string::npos : read - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15644,6 +15661,12 @@ struct diagnostic_positions
|
||||
{
|
||||
// Lexer's position is past the closing brace or bracket, so set that as the end position.
|
||||
v.end_position = lexer->get_chars_read_total();
|
||||
if (v.end_position == std::string::npos)
|
||||
{
|
||||
// a range has both ends or none (exception::diagnostics()
|
||||
// prints only complete ones)
|
||||
v.start_position = std::string::npos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15660,6 +15683,12 @@ struct diagnostic_positions
|
||||
// The start position will be set below based on the length of the string representation
|
||||
// of the value.
|
||||
v.end_position = lexer->get_chars_read_total();
|
||||
if (v.end_position == std::string::npos)
|
||||
{
|
||||
// a value without a source position
|
||||
v.start_position = std::string::npos;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (v.type())
|
||||
{
|
||||
@@ -15872,8 +15901,12 @@ read up to it.
|
||||
@tparam Collect whether to collect the values of containers of unknown size on
|
||||
a stack (only for a caller that does not use the value after a
|
||||
parse error)
|
||||
@tparam LexerType the type of the lexer that reports the source positions
|
||||
(JSON_DIAGNOSTIC_POSITIONS); only diagnostic_positions uses
|
||||
it, so any type with its three functions works (as
|
||||
materialize() of json_view uses it)
|
||||
*/
|
||||
template<typename BasicJsonType, typename InputAdapterType, bool Collect = false>
|
||||
template<typename BasicJsonType, typename InputAdapterType, bool Collect = false, typename LexerType = lexer<BasicJsonType, InputAdapterType>>
|
||||
class json_sax_dom_parser : public json_sax_dom_error_state
|
||||
{
|
||||
public:
|
||||
@@ -15882,7 +15915,7 @@ class json_sax_dom_parser : public json_sax_dom_error_state
|
||||
using number_float_t = typename BasicJsonType::number_float_t;
|
||||
using string_t = typename BasicJsonType::string_t;
|
||||
using binary_t = typename BasicJsonType::binary_t;
|
||||
using lexer_t = lexer<BasicJsonType, InputAdapterType>;
|
||||
using lexer_t = LexerType;
|
||||
|
||||
/*!
|
||||
@param[in,out] r reference to a JSON value that is manipulated while
|
||||
@@ -22515,8 +22548,8 @@ using parser_callback_t =
|
||||
template<typename SAX>
|
||||
struct sax_ignores_number_text : std::false_type {};
|
||||
|
||||
template<typename BasicJsonType, typename InputAdapterType, bool Collect>
|
||||
struct sax_ignores_number_text<json_sax_dom_parser<BasicJsonType, InputAdapterType, Collect>> : std::true_type {};
|
||||
template<typename BasicJsonType, typename InputAdapterType, bool Collect, typename LexerType>
|
||||
struct sax_ignores_number_text<json_sax_dom_parser<BasicJsonType, InputAdapterType, Collect, LexerType>> : std::true_type {};
|
||||
|
||||
template<typename BasicJsonType, typename InputAdapterType>
|
||||
struct sax_ignores_number_text<json_sax_dom_callback_parser<BasicJsonType, InputAdapterType>> : std::true_type {};
|
||||
@@ -24331,6 +24364,13 @@ class json_pointer
|
||||
/// @sa https://json.nlohmann.me/api/json_pointer/operator_slasheq/
|
||||
json_pointer& operator/=(const json_pointer& ptr)
|
||||
{
|
||||
// insert() must not be given iterators into the vector it inserts
|
||||
// into: append a copy when a pointer is appended to itself
|
||||
if (JSON_HEDLEY_UNLIKELY(&ptr == this))
|
||||
{
|
||||
return *this /= json_pointer(ptr);
|
||||
}
|
||||
|
||||
reference_tokens.insert(reference_tokens.end(),
|
||||
ptr.reference_tokens.begin(),
|
||||
ptr.reference_tokens.end());
|
||||
@@ -32469,6 +32509,11 @@ NLOHMANN_JSON_NAMESPACE_END
|
||||
#include <type_traits> // conditional
|
||||
|
||||
|
||||
// The shortest-digit conversion of doubles (see zmij.hpp) and the writing of
|
||||
// its digits, including the conversion of the digits to text by Xiang JunBo
|
||||
// and the SIMD instruction sequence by Dougall Johnson, are ported from Zmij
|
||||
// by Victor Zverovich (https://github.com/vitaut/zmij, MIT license).
|
||||
//
|
||||
// SSE2 (every x86-64 CPU) and NEON (every 64-bit Arm CPU) convert the 16
|
||||
// digits of a double at once (unless JSON_NO_SIMD selects the portable code)
|
||||
#if !defined(JSON_NO_SIMD) && (defined(__x86_64__) || (defined(_M_X64) && !defined(_M_ARM64EC)))
|
||||
@@ -36910,7 +36955,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
friend class ::nlohmann::detail::binary_writer;
|
||||
template<typename BasicJsonType, typename InputType, typename SAX>
|
||||
friend class ::nlohmann::detail::binary_reader;
|
||||
template<typename BasicJsonType, typename InputAdapterType, bool Collect>
|
||||
template<typename BasicJsonType, typename InputAdapterType, bool Collect, typename LexerType>
|
||||
friend class ::nlohmann::detail::json_sax_dom_parser;
|
||||
template<typename BasicJsonType, typename InputAdapterType>
|
||||
friend class ::nlohmann::detail::json_sax_dom_binary_parser;
|
||||
@@ -43701,6 +43746,25 @@ public:
|
||||
/// @sa https://json.nlohmann.me/api/basic_json/patch/
|
||||
void patch_inplace(const basic_json& json_patch)
|
||||
{
|
||||
// the operations below change *this while iterating json_patch: apply
|
||||
// a copy when a value is applied as patch to itself or, with
|
||||
// JSON_DIAGNOSTICS, to a value stored inside it
|
||||
// NOLINTNEXTLINE(misc-const-correctness): only the JSON_DIAGNOSTICS build modifies it
|
||||
bool patch_aliases_this = (&json_patch == this);
|
||||
#if JSON_DIAGNOSTICS
|
||||
for (const basic_json* p = json_patch.m_parent; !patch_aliases_this && p != nullptr; p = p->m_parent)
|
||||
{
|
||||
patch_aliases_this = (p == this);
|
||||
}
|
||||
#endif
|
||||
if (JSON_HEDLEY_UNLIKELY(patch_aliases_this))
|
||||
{
|
||||
// NOLINTNEXTLINE(performance-unnecessary-copy-initialization): *this is modified below
|
||||
const basic_json json_patch_copy = json_patch;
|
||||
patch_inplace(json_patch_copy);
|
||||
return;
|
||||
}
|
||||
|
||||
basic_json& result = *this;
|
||||
// the valid JSON Patch operations
|
||||
enum class patch_operations {add, remove, replace, move, copy, test, invalid};
|
||||
|
||||
Generated
+1760
-480
File diff suppressed because it is too large.
Load diff
@@ -21,6 +21,10 @@ python3 tests/benchmarks/json_view/compare.py --data <json_test_data directory>
|
||||
- The other libraries come from the system: pkg-config, or Homebrew (`brew install yyjson simdjson boost`). With
|
||||
`--download`, pinned releases are downloaded instead and checked against their SHA-256. Without Boost headers (or
|
||||
with `--no-boost`), the Boost.JSON columns are skipped, and the results say so.
|
||||
- `--aligned` additionally builds and runs everything with all functions aligned to 64 bytes (`-mllvm
|
||||
-align-all-functions=6` for Clang, `-falign-functions=64 -falign-loops=64` for GCC), and reports both result
|
||||
tables as "default build" and "aligned build". Code layout alone can move a result by a few percent; a difference
|
||||
that appears in only one of the two tables is a layout artifact. Off by default, which doubles the run time.
|
||||
- `--corpus file...` adds files to the corpus benchmark, e.g. those of
|
||||
[simdjson-data](https://github.com/simdjson/simdjson-data) or the
|
||||
[yyjson benchmark](https://github.com/ibireme/yyjson_benchmark).
|
||||
@@ -39,7 +43,7 @@ where `json_view` stands on another architecture; they are not meant for publica
|
||||
|
||||
## What is measured
|
||||
|
||||
`bench_view.cpp` runs four workloads on twitter, citm_catalog, canada, jeopardy, a single tweet (`status`), and a
|
||||
`bench_view.cpp` runs these workloads on twitter, citm_catalog, canada, jeopardy, a single tweet (`status`), and a
|
||||
JSON-RPC request (`rpc`):
|
||||
|
||||
| workload | what it does |
|
||||
@@ -48,6 +52,7 @@ JSON-RPC request (`rpc`):
|
||||
| traverse | parse, then visit every value, convert every number, touch every string and key |
|
||||
| select | parse, then read a few fields per record (e.g. id, user name, and retweet count of each tweet) |
|
||||
| dump | serialize a parsed document (compact) |
|
||||
| parse+dump | build a fresh document, then serialize it (compact), in one timed call |
|
||||
|
||||
`bench_corpus.cpp` runs parse, traverse, and dump on any list of files, so that no library is tuned to a handful of
|
||||
documents. Its dump also writes the numbers as they are in the input: `json_view` with `number_format::source`, and
|
||||
@@ -72,11 +77,32 @@ call follows an untimed call of the same engine: otherwise the engine after `jso
|
||||
cleaning up the tens of thousands of nodes `json::parse` just freed (with glibc, this made `json_view` look 1.7 times
|
||||
slower on citm_catalog traverse).
|
||||
|
||||
`json_view` is an index into the source text, not a tree of owning nodes. Its like-for-like peers are yyjson and
|
||||
simdjson DOM (immutable, indexed, no per-node allocation). Boost.JSON and `json::parse` build mutable, owning DOMs and
|
||||
are reference points for what that costs, not equals; simdjson On-Demand is lazy and forward-only, so it is the other
|
||||
way round: it has no random access to pay for. All engines validate UTF-8 and syntax by default.
|
||||
|
||||
Number conversion differs: `json_view` converts integers while parsing but converts floating-point numbers lazily,
|
||||
when they are read or dumped; yyjson (default flags) and simdjson DOM convert every number while parsing. The cost of
|
||||
float conversion therefore appears in the traverse and dump rows for `json_view`, and in the parse row for the others.
|
||||
For float-heavy documents (canada), the fair reading is a sum of parse and use: the `traverse` workload already
|
||||
includes the parse, and `parse+dump` measures a fresh document and its dump in one timed call; the `dump` row alone
|
||||
leaves the conversion in `json_view` and out of the others. Eager conversion at parse time was prototyped as an opt-in
|
||||
and measured: canada traverse (with its parse) was 4-7 % faster, but parse+dump 11-30 % slower and parse 36-90 % slower. It was
|
||||
dropped, so lazy conversion is the only mode.
|
||||
|
||||
`json_view` can also be edited: `json_editable_document` has the same read API, and the `(editable)` rows in
|
||||
`bench_view` measure it on the same workloads. They are the fair counterpart of Boost.JSON and `json::parse`, which are
|
||||
mutable too; the plain `json_view` rows are the counterpart of yyjson and simdjson DOM, which are not. `bench_edit` adds
|
||||
the read-modify-write workloads. Read the tables with that in mind: an immutable-only library is not compared with an
|
||||
editable `json_view` unless it says so.
|
||||
|
||||
The engines do not all offer the same features, which the numbers should be read with:
|
||||
|
||||
| engine | document | random access | editable | notes |
|
||||
|---|---|---|---|---|
|
||||
| `json_view` | immutable index into the text | yes | no | a fresh document per parse; "reused" parses into the same document |
|
||||
| `json_view` (editable) | index into the text plus edits | yes | yes, in place | `json_editable_document`; keeps member order and number spellings |
|
||||
| yyjson | immutable (`yyjson_read`) | yes | via a mutable copy | |
|
||||
| simdjson DOM | immutable, parser reused | yes | no | "fresh" uses a new parser per parse |
|
||||
| simdjson On-Demand | none: forward-only, lazy | no | no | only traverse and select |
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
|
||||
// Same-feature-set benchmark: read-only JSON documents with random access.
|
||||
//
|
||||
// json_view nlohmann/json_view.hpp (fresh document per parse / reused)
|
||||
// yyjson yyjson_read(): immutable document, random access
|
||||
// json_view nlohmann/json_view.hpp (fresh document per parse / reused;
|
||||
// "(editable)": json_editable_document, same read API)
|
||||
// yyjson yyjson_read(): immutable document, random access
|
||||
// simdjson DOM dom::parser (reused, as recommended; "fresh": a new parser
|
||||
// per parse): immutable, random access
|
||||
// references (different feature sets):
|
||||
@@ -19,7 +20,11 @@
|
||||
//
|
||||
// Workloads: parse (build + free), traverse (visit everything, convert every
|
||||
// number, touch every string and key), select (a few fields per document),
|
||||
// dump (compact serialization of the parsed document).
|
||||
// dump (compact serialization of the parsed document), and parse+dump (a fresh
|
||||
// document, then its dump, in one timed call). json_view converts floating-point
|
||||
// numbers lazily (when read or dumped), yyjson and simdjson DOM while parsing:
|
||||
// for float-heavy documents, traverse (which includes the parse) and parse+dump
|
||||
// are the fair reading of the parse and dump rows.
|
||||
// All engines run interleaved in every round, each timed call after an untimed
|
||||
// one of the same engine; the best round is reported.
|
||||
#include <nlohmann/json_view.hpp>
|
||||
@@ -43,6 +48,8 @@
|
||||
|
||||
using nlohmann::json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::json_editable_document;
|
||||
using nlohmann::json_editable_view;
|
||||
using nlohmann::json_view;
|
||||
|
||||
static volatile double g_sink;
|
||||
@@ -55,7 +62,8 @@ struct stats
|
||||
|
||||
// ---------------- traversal ----------------
|
||||
|
||||
static void walk(json_view v, stats& st)
|
||||
template<class V>
|
||||
static void walk_view(V v, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
switch (v.type())
|
||||
@@ -64,32 +72,42 @@ static void walk(json_view v, stats& st)
|
||||
for (auto it = v.begin(); it != v.end(); ++it)
|
||||
{
|
||||
st.str += it.key().size();
|
||||
walk(*it, st);
|
||||
walk_view<V>(*it, st);
|
||||
}
|
||||
break;
|
||||
case json::value_t::array:
|
||||
for (const json_view e : v)
|
||||
for (const V e : v)
|
||||
{
|
||||
walk(e, st);
|
||||
walk_view<V>(e, st);
|
||||
}
|
||||
break;
|
||||
case json::value_t::string:
|
||||
st.str += v.get_string().size();
|
||||
break;
|
||||
case json::value_t::number_integer:
|
||||
st.num += static_cast<double>(v.get<std::int64_t>());
|
||||
st.num += static_cast<double>(v.template get<std::int64_t>());
|
||||
break;
|
||||
case json::value_t::number_unsigned:
|
||||
st.num += static_cast<double>(v.get<std::uint64_t>());
|
||||
st.num += static_cast<double>(v.template get<std::uint64_t>());
|
||||
break;
|
||||
case json::value_t::number_float:
|
||||
st.num += v.get<double>();
|
||||
st.num += v.template get<double>();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void walk(json_view v, stats& st)
|
||||
{
|
||||
walk_view<json_view>(v, st);
|
||||
}
|
||||
|
||||
static void walk(json_editable_view v, stats& st)
|
||||
{
|
||||
walk_view<json_editable_view>(v, st);
|
||||
}
|
||||
|
||||
static void walk(const json& j, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
@@ -300,20 +318,21 @@ static void walk(const boost::json::value& v, stats& st)
|
||||
// status (one tweet): id, user.screen_name, retweet_count
|
||||
// rpc: method, params.minuend, id
|
||||
|
||||
static double pick(const std::string& name, json_view r)
|
||||
template<class V>
|
||||
static double pick_view(const std::string& name, V r)
|
||||
{
|
||||
double acc = 0;
|
||||
if (name == "twitter" || name == "status")
|
||||
{
|
||||
auto one = [&](json_view s)
|
||||
auto one = [&](V s)
|
||||
{
|
||||
acc += static_cast<double>(s["id"].get<std::uint64_t>());
|
||||
acc += static_cast<double>(s["id"].template get<std::uint64_t>());
|
||||
acc += static_cast<double>(s["user"]["screen_name"].get_string().size());
|
||||
acc += static_cast<double>(s["retweet_count"].get<std::int64_t>());
|
||||
acc += static_cast<double>(s["retweet_count"].template get<std::int64_t>());
|
||||
};
|
||||
if (name == "twitter")
|
||||
{
|
||||
for (const json_view s : r["statuses"])
|
||||
for (const V s : r["statuses"])
|
||||
{
|
||||
one(s);
|
||||
}
|
||||
@@ -325,20 +344,20 @@ static double pick(const std::string& name, json_view r)
|
||||
}
|
||||
else if (name == "citm_catalog")
|
||||
{
|
||||
for (const json_view p : r["performances"])
|
||||
for (const V p : r["performances"])
|
||||
{
|
||||
acc += static_cast<double>(p["id"].get<std::uint64_t>() + p["eventId"].get<std::uint64_t>() + p["seatCategories"].size());
|
||||
acc += static_cast<double>(p["id"].template get<std::uint64_t>() + p["eventId"].template get<std::uint64_t>() + p["seatCategories"].size());
|
||||
}
|
||||
acc += static_cast<double>(r["events"].size());
|
||||
}
|
||||
else if (name == "canada")
|
||||
{
|
||||
const json_view g = r["features"][0]["geometry"];
|
||||
const V g = r["features"][0]["geometry"];
|
||||
acc += static_cast<double>(r["type"].get_string().size() + g["type"].get_string().size() + g["coordinates"].size());
|
||||
}
|
||||
else if (name == "jeopardy")
|
||||
{
|
||||
for (const json_view q : r)
|
||||
for (const V q : r)
|
||||
{
|
||||
acc += q["round"].get_string() == "Final Jeopardy!" ? 1 : 0;
|
||||
acc += static_cast<double>(q["category"].get_string().size());
|
||||
@@ -347,11 +366,21 @@ static double pick(const std::string& name, json_view r)
|
||||
else if (name == "rpc")
|
||||
{
|
||||
acc += static_cast<double>(r["method"].get_string().size());
|
||||
acc += static_cast<double>(r["params"]["minuend"].get<std::int64_t>() + r["id"].get<std::int64_t>());
|
||||
acc += static_cast<double>(r["params"]["minuend"].template get<std::int64_t>() + r["id"].template get<std::int64_t>());
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
static double pick(const std::string& name, json_view r)
|
||||
{
|
||||
return pick_view<json_view>(name, r);
|
||||
}
|
||||
|
||||
static double pick(const std::string& name, json_editable_view r)
|
||||
{
|
||||
return pick_view<json_editable_view>(name, r);
|
||||
}
|
||||
|
||||
static double pick(const std::string& name, const json& r)
|
||||
{
|
||||
double acc = 0;
|
||||
@@ -634,15 +663,35 @@ int main(int argc, char** argv)
|
||||
walk(json::parse(dc.text), a);
|
||||
auto d = json_document::parse(dc.text);
|
||||
walk(d.root(), b);
|
||||
auto ed = json_editable_document::parse(dc.text);
|
||||
stats e;
|
||||
walk(ed.root(), e);
|
||||
yyjson_doc* y = yyjson_read(dc.text.data(), dc.text.size(), 0);
|
||||
walk(yyjson_doc_get_root(y), c);
|
||||
simdjson::dom::parser p;
|
||||
walk(p.parse(dc.text).value(), dd);
|
||||
const bool ok = a.nodes == b.nodes && a.nodes == c.nodes && a.nodes == dd.nodes && a.str == b.str && a.str == c.str && a.str == dd.str
|
||||
const bool ok = a.nodes == b.nodes && a.nodes == e.nodes && a.str == e.str && std::fabs(a.num - e.num) <= 1e-9 * std::fabs(a.num) && a.nodes == c.nodes && a.nodes == dd.nodes && a.str == b.str && a.str == c.str && a.str == dd.str
|
||||
&& std::fabs(a.num - b.num) <= 1e-9 * std::fabs(a.num) && std::fabs(a.num - c.num) <= 1e-9 * std::fabs(a.num);
|
||||
const double pa = pick(dc.name, json::parse(dc.text)), pb = pick(dc.name, d.root()), pc = pick(dc.name, yyjson_doc_get_root(y)), pd = pick(dc.name, p.parse(dc.text).value());
|
||||
std::printf("check %-13s traverse %s select %s\n", dc.name.c_str(), ok ? "OK" : "MISMATCH", (pa == pb && pa == pc && pa == pd) ? "OK" : "MISMATCH");
|
||||
const double pa = pick(dc.name, json::parse(dc.text)), pb = pick(dc.name, d.root()), pe = pick(dc.name, ed.root()), pc = pick(dc.name, yyjson_doc_get_root(y)), pd = pick(dc.name, p.parse(dc.text).value());
|
||||
std::printf("check %-13s traverse %s select %s\n", dc.name.c_str(), ok ? "OK" : "MISMATCH", (pa == pb && pa == pe && pa == pc && pa == pd) ? "OK" : "MISMATCH");
|
||||
yyjson_doc_free(y);
|
||||
|
||||
// the dumps of the engines may format floats differently, so only: every dump is non-empty, and the
|
||||
// dump of json_view (also editable) and of json::parse parse back to the same number of nodes
|
||||
const std::string dj = json::parse(dc.text).dump();
|
||||
const std::string dv = d.root().dump();
|
||||
const std::string de = ed.root().dump();
|
||||
std::size_t yn = 0;
|
||||
yyjson_doc* y2 = yyjson_read(dc.text.data(), dc.text.size(), 0);
|
||||
char* yo = yyjson_write(y2, 0, &yn);
|
||||
const bool dumps_ok = !dj.empty() && !dv.empty() && !de.empty() && yo != nullptr && yn > 0 && !simdjson::to_string(p.parse(dc.text).value()).empty();
|
||||
std::free(yo);
|
||||
yyjson_doc_free(y2);
|
||||
stats sj_nodes, sv_nodes, se_nodes;
|
||||
walk(json::parse(dj), sj_nodes);
|
||||
walk(json::parse(dv), sv_nodes);
|
||||
walk(json::parse(de), se_nodes);
|
||||
std::printf("check %-13s dump %s\n", dc.name.c_str(), (dumps_ok && sj_nodes.nodes == a.nodes && sv_nodes.nodes == a.nodes && se_nodes.nodes == a.nodes) ? "OK" : "MISMATCH");
|
||||
}
|
||||
|
||||
std::FILE* csv = std::fopen("bench_view.csv", "w");
|
||||
@@ -669,6 +718,7 @@ int main(int argc, char** argv)
|
||||
workloads.push_back({"parse", {
|
||||
{"json_view", [&] { auto d = json_document::parse(s); g_sink = static_cast<double>(d.node_count()); }},
|
||||
{"json_view (reused)", [&] { reused.read(s); g_sink = static_cast<double>(reused.node_count()); }},
|
||||
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); g_sink = static_cast<double>(d.root().size()); }},
|
||||
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = static_cast<double>(yyjson_doc_get_val_count(d)); yyjson_doc_free(d); }},
|
||||
{"simdjson DOM", [&] { auto e = sj.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
|
||||
{"simdjson DOM (fresh)", [&] { simdjson::dom::parser p; auto e = p.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
|
||||
@@ -680,6 +730,7 @@ int main(int argc, char** argv)
|
||||
workloads.push_back({"traverse", {
|
||||
{"json_view", [&] { auto d = json_document::parse(s); stats st; walk(d.root(), st); g_sink = st.num; }},
|
||||
{"json_view (reused)", [&] { reused.read(s); stats st; walk(reused.root(), st); g_sink = st.num; }},
|
||||
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); stats st; walk(d.root(), st); g_sink = st.num; }},
|
||||
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); stats st; walk(yyjson_doc_get_root(d), st); g_sink = st.num; yyjson_doc_free(d); }},
|
||||
{"simdjson DOM", [&] { stats st; walk(sj.parse(ps).value_unsafe(), st); g_sink = st.num; }},
|
||||
{"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); stats st; walk_od(d.get_value().value_unsafe(), st); g_sink = st.num; }},
|
||||
@@ -691,15 +742,31 @@ int main(int argc, char** argv)
|
||||
workloads.push_back({"select", {
|
||||
{"json_view", [&] { auto d = json_document::parse(s); g_sink = pick(name, d.root()); }},
|
||||
{"json_view (reused)", [&] { reused.read(s); g_sink = pick(name, reused.root()); }},
|
||||
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); g_sink = pick(name, d.root()); }},
|
||||
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = pick(name, yyjson_doc_get_root(d)); yyjson_doc_free(d); }},
|
||||
{"simdjson DOM", [&] { g_sink = pick(name, sj.parse(ps).value_unsafe()); }},
|
||||
{"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); g_sink = pick_od(name, d); }},
|
||||
{"json::parse", [&] { json j = json::parse(s); g_sink = pick(name, j); }},
|
||||
}});
|
||||
// pipeline: a fresh document, then a dump of it, in one timed call
|
||||
// (simdjson DOM reuses its parser, as in the parse row); the traverse
|
||||
// workload above is already parse+traverse
|
||||
workloads.push_back({"parse+dump", {
|
||||
{"json_view", [&] { auto d = json_document::parse(s); std::string o = d.root().dump(); g_sink = static_cast<double>(o.size()); }},
|
||||
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); std::string o = d.root().dump(); g_sink = static_cast<double>(o.size()); }},
|
||||
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); std::size_t n = 0; char* o = yyjson_write(d, 0, &n); g_sink = static_cast<double>(n); std::free(o); yyjson_doc_free(d); }},
|
||||
{"simdjson DOM", [&] { std::string o = simdjson::to_string(sj.parse(ps).value_unsafe()); g_sink = static_cast<double>(o.size()); }},
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
{"Boost.JSON", [&] { boost::json::monotonic_resource mr; auto v = boost::json::parse(s, &mr); std::string o = boost::json::serialize(v); g_sink = static_cast<double>(o.size()); }},
|
||||
#endif
|
||||
{"json::parse", [&] { json j = json::parse(s); std::string o = j.dump(); g_sink = static_cast<double>(o.size()); }},
|
||||
}});
|
||||
{
|
||||
// serialization of an already parsed document
|
||||
static json_document vd;
|
||||
vd.read(s);
|
||||
static json_editable_document ed = json_editable_document::parse(s);
|
||||
ed = json_editable_document::parse(s);
|
||||
static yyjson_doc* yd = nullptr;
|
||||
if (yd)
|
||||
{
|
||||
@@ -713,6 +780,7 @@ int main(int argc, char** argv)
|
||||
jd = json::parse(s);
|
||||
workloads.push_back({"dump", {
|
||||
{"json_view", [&] { std::string o = vd.root().dump(); g_sink = static_cast<double>(o.size()); }},
|
||||
{"json_view (editable)", [&] { std::string o = ed.root().dump(); g_sink = static_cast<double>(o.size()); }},
|
||||
{"yyjson", [&] { std::size_t n = 0; char* o = yyjson_write(yd, 0, &n); g_sink = static_cast<double>(n); std::free(o); }},
|
||||
{"simdjson DOM", [&] { std::string o = simdjson::to_string(se); g_sink = static_cast<double>(o.size()); }},
|
||||
{"json::parse", [&] { std::string o = jd.dump(); g_sink = static_cast<double>(o.size()); }},
|
||||
@@ -740,7 +808,7 @@ int main(int argc, char** argv)
|
||||
}
|
||||
}
|
||||
const double ref = best[0];
|
||||
std::printf("%-13s %-9s", dc.name.c_str(), wl.first.c_str());
|
||||
std::printf("%-13s %-14s", dc.name.c_str(), wl.first.c_str());
|
||||
for (std::size_t k = 0; k < wl.second.size(); ++k)
|
||||
{
|
||||
const double us = best[k] / 1e3;
|
||||
|
||||
@@ -239,6 +239,8 @@ def main():
|
||||
ap.add_argument('--download', action='store_true', help='use pinned downloads instead of system libraries')
|
||||
ap.add_argument('--no-boost', action='store_true', help='skip Boost.JSON')
|
||||
ap.add_argument('--native', action='store_true', help='compile for this CPU (-march=native / -mcpu=native)')
|
||||
ap.add_argument('--aligned', action='store_true',
|
||||
help='also build and run with all functions (and loops) aligned, to make code layout artifacts visible')
|
||||
ap.add_argument('--rounds', type=int, default=30, help='rounds of bench_view (default: 30)')
|
||||
ap.add_argument('--corpus', nargs='*', default=[], help='more files for bench_corpus')
|
||||
ap.add_argument('--build-dir', default=os.path.join(HERE, 'build'), help='where to build (default: build/ next to this script)')
|
||||
@@ -282,32 +284,46 @@ def main():
|
||||
+ ['-I' + d for d in lib.include])
|
||||
objects.append(obj)
|
||||
|
||||
binaries = {}
|
||||
for bench in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']:
|
||||
exe = os.path.join(args.build_dir, bench)
|
||||
run(cxx + flags + include + [os.path.join(HERE, bench + '.cpp')] + objects + link + ['-o', exe])
|
||||
binaries[bench] = exe
|
||||
cxx_version = output(cxx + ['--version'])
|
||||
is_clang = 'clang' in cxx_version.lower()
|
||||
# alignment flags: functions and (gcc) loops on 64-byte boundaries
|
||||
align_flags = ['-mllvm', '-align-all-functions=6'] if is_clang else ['-falign-functions=64', '-falign-loops=64']
|
||||
|
||||
# the builds: the default one and, with --aligned, one with aligned code
|
||||
variants = [('default build', args.build_dir, [])]
|
||||
if args.aligned:
|
||||
variants.append(('aligned build', os.path.join(args.build_dir, 'aligned'), align_flags))
|
||||
|
||||
# run: bench_view on its documents, bench_corpus on those and the given files
|
||||
corpus = [os.path.join(args.data, f) for f in DEFAULT_CORPUS] + args.corpus
|
||||
outputs = {}
|
||||
outputs['bench_view'] = run([binaries['bench_view'], args.data, str(args.rounds)], cwd=args.build_dir,
|
||||
capture_output=True, text=True).stdout
|
||||
outputs['bench_corpus'] = run([binaries['bench_corpus']] + corpus, cwd=args.build_dir,
|
||||
capture_output=True, text=True).stdout
|
||||
outputs['bench_edit'] = run([binaries['bench_edit'], args.data, str(max(1, args.rounds // 2))], cwd=args.build_dir,
|
||||
capture_output=True, text=True).stdout
|
||||
outputs['bench_decode'] = run([binaries['bench_decode'], args.data, str(max(1, args.rounds // 2))], cwd=args.build_dir,
|
||||
capture_output=True, text=True).stdout
|
||||
for name, text in outputs.items():
|
||||
print(text)
|
||||
# label -> {benchmark: output}
|
||||
all_outputs = {}
|
||||
for label, build_dir, extra in variants:
|
||||
os.makedirs(build_dir, exist_ok=True)
|
||||
binaries = {}
|
||||
for bench in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']:
|
||||
exe = os.path.join(build_dir, bench)
|
||||
run(cxx + flags + extra + include + [os.path.join(HERE, bench + '.cpp')] + objects + link + ['-o', exe])
|
||||
binaries[bench] = exe
|
||||
|
||||
# run: bench_view on its documents, bench_corpus on those and the given files
|
||||
outputs = {}
|
||||
outputs['bench_view'] = run([binaries['bench_view'], args.data, str(args.rounds)], cwd=build_dir,
|
||||
capture_output=True, text=True).stdout
|
||||
outputs['bench_corpus'] = run([binaries['bench_corpus']] + corpus, cwd=build_dir,
|
||||
capture_output=True, text=True).stdout
|
||||
outputs['bench_edit'] = run([binaries['bench_edit'], args.data, str(max(1, args.rounds // 2))], cwd=build_dir,
|
||||
capture_output=True, text=True).stdout
|
||||
outputs['bench_decode'] = run([binaries['bench_decode'], args.data, str(max(1, args.rounds // 2))],
|
||||
cwd=build_dir, capture_output=True, text=True).stdout
|
||||
for name, text in outputs.items():
|
||||
print(text)
|
||||
all_outputs[label] = outputs
|
||||
|
||||
# results with their metadata
|
||||
now = datetime.datetime.now()
|
||||
host = re.sub(r'[^A-Za-z0-9-]+', '-', platform.node().split('.')[0]) or 'host'
|
||||
stem = os.path.join(HERE, 'results', f'{now:%Y-%m-%d}-{host}')
|
||||
os.makedirs(os.path.dirname(stem), exist_ok=True)
|
||||
cxx_version = output(cxx + ['--version'])
|
||||
compiler_version = cxx_version.splitlines()[0] if cxx_version else ' '.join(cxx)
|
||||
meta = [
|
||||
('date', f'{now:%Y-%m-%d %H:%M}'),
|
||||
@@ -316,6 +332,7 @@ def main():
|
||||
('OS', f'{platform.system()} {platform.release()} ({platform.machine()})'),
|
||||
('compiler', compiler_version),
|
||||
('flags', ' '.join(flags)),
|
||||
('aligned results', ('included (flags: ' + ' '.join(align_flags) + ')') if args.aligned else 'not included'),
|
||||
('yyjson', libs['yyjson'].version),
|
||||
('simdjson', libs['simdjson'].version),
|
||||
('Boost.JSON', libs['boost'].version if with_boost else 'skipped (not found)'),
|
||||
@@ -328,15 +345,20 @@ def main():
|
||||
f.write('| | |\n|---|---|\n')
|
||||
for key, value in meta:
|
||||
f.write(f'| {key} | {value} |\n')
|
||||
for name, text in outputs.items():
|
||||
f.write(f'\n## {name}\n\n```\n{text.rstrip()}\n```\n')
|
||||
for label, outputs in all_outputs.items():
|
||||
# (without --aligned there is only the default build, and no extra heading level)
|
||||
prefix = f'{label}: ' if args.aligned else ''
|
||||
for name, text in outputs.items():
|
||||
f.write(f'\n## {prefix}{name}\n\n```\n{text.rstrip()}\n```\n')
|
||||
with open(stem + '.csv', 'w', encoding='utf-8') as out:
|
||||
out.write(''.join(f'# {key}: {value}\n' for key, value in meta))
|
||||
for name in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']:
|
||||
path = os.path.join(args.build_dir, name + '.csv')
|
||||
if os.path.isfile(path):
|
||||
with open(path, encoding='utf-8') as f:
|
||||
out.write(f'# {name}\n' + f.read())
|
||||
for label, build_dir, _ in variants:
|
||||
for name in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']:
|
||||
path = os.path.join(build_dir, name + '.csv')
|
||||
if os.path.isfile(path):
|
||||
with open(path, encoding='utf-8') as f:
|
||||
# (the default build keeps its plain section names)
|
||||
out.write(f'# {name}' + (f' ({label})' if args.aligned else '') + '\n' + f.read())
|
||||
print(f'results: {stem}.md, {stem}.csv')
|
||||
|
||||
|
||||
|
||||
@@ -3030,3 +3030,144 @@ TEST_CASE("a json_sax_dom_parser keeps the values read before a parse error")
|
||||
CHECK(read_before_error<nlohmann::ordered_json>(c.first) == c.second);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("object keys are delivered decoded and validated")
|
||||
{
|
||||
// json_sax::key and the key event of a parser callback see the decoded name, and
|
||||
// only after the whole name was scanned and validated
|
||||
const auto keys_of_format = [](const std::string & text, const json::input_format_t format)
|
||||
{
|
||||
SaxEventLogger l;
|
||||
json::sax_parse(text, &l, format);
|
||||
std::vector<std::string> keys;
|
||||
for (const auto& e : l.events)
|
||||
{
|
||||
if (e.compare(0, 4, "key(") == 0)
|
||||
{
|
||||
keys.push_back(e.substr(4, e.size() - 5));
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
};
|
||||
|
||||
const auto keys_of = [&keys_of_format](const std::string & text)
|
||||
{
|
||||
return keys_of_format(text, json::input_format_t::json);
|
||||
};
|
||||
|
||||
const auto callback_keys_of = [](const std::string & text)
|
||||
{
|
||||
std::vector<std::string> keys;
|
||||
const json::parser_callback_t cb = [&keys](int /*depth*/, json::parse_event_t event, json & parsed)
|
||||
{
|
||||
if (event == json::parse_event_t::key)
|
||||
{
|
||||
keys.push_back(parsed.get<std::string>());
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const json parsed = json::parse(text, cb, false);
|
||||
static_cast<void>(parsed);
|
||||
return keys;
|
||||
};
|
||||
|
||||
SECTION("different spellings of a name yield the same string")
|
||||
{
|
||||
const std::string e_acute = "\xc3\xa9";
|
||||
const std::string text = std::string(R"({"/":1,"\/":2,"\u002f":3,"\u00e9":4,"\u00E9":5,")") + e_acute + R"(":6})";
|
||||
const std::vector<std::string> expected = {"/", "/", "/", e_acute, e_acute, e_acute};
|
||||
CHECK(keys_of(text) == expected);
|
||||
CHECK(callback_keys_of(text) == expected);
|
||||
}
|
||||
|
||||
SECTION("escape sequences are decoded")
|
||||
{
|
||||
const std::string simple = R"({"\"\\\/\b\f\n\r\t":1})";
|
||||
const std::vector<std::string> simple_expected = {std::string("\"\\/\b\f\n\r\t")};
|
||||
CHECK(keys_of(simple) == simple_expected);
|
||||
CHECK(callback_keys_of(simple) == simple_expected);
|
||||
|
||||
const std::string nul = R"({"\u0000":1})";
|
||||
const std::vector<std::string> nul_expected = {std::string(1, '\0')};
|
||||
CHECK(keys_of(nul) == nul_expected);
|
||||
CHECK(callback_keys_of(nul) == nul_expected);
|
||||
|
||||
const std::string pair = R"({"\ud83d\ude00":1})";
|
||||
const std::vector<std::string> pair_expected = {"\xf0\x9f\x98\x80"};
|
||||
CHECK(keys_of(pair) == pair_expected);
|
||||
CHECK(callback_keys_of(pair) == pair_expected);
|
||||
}
|
||||
|
||||
SECTION("no normalization and no case conversion")
|
||||
{
|
||||
const std::string text = std::string("{\"\xc3\xa9\":1,\"e\xcc\x81\":2,\"A\":3,\"a\":4}");
|
||||
const std::vector<std::string> expected = {"\xc3\xa9", "e\xcc\x81", "A", "a"};
|
||||
CHECK(keys_of(text) == expected);
|
||||
CHECK(callback_keys_of(text) == expected);
|
||||
}
|
||||
|
||||
SECTION("a name that is not valid is not delivered")
|
||||
{
|
||||
const std::vector<std::string> texts =
|
||||
{
|
||||
R"({"\u12G4":1})", // malformed escape
|
||||
R"({"\udc00":1})", // lone low surrogate
|
||||
R"({"\ud800":1})", // lone high surrogate
|
||||
R"({"\ud800\u0041":1})", // high surrogate not followed by a low one
|
||||
R"({"\x":1})", // unknown escape
|
||||
std::string("{\"a\x01\":1}"), // control character
|
||||
std::string("{\"\xff\":1}") // ill-formed UTF-8
|
||||
};
|
||||
for (const auto& text : texts)
|
||||
{
|
||||
CAPTURE(text)
|
||||
CHECK(!json::accept(text));
|
||||
CHECK(keys_of(text).empty());
|
||||
CHECK(callback_keys_of(text).empty());
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("a name is delivered before the following colon is read")
|
||||
{
|
||||
const std::string text = R"({"a" 1})";
|
||||
const std::vector<std::string> expected = {"a"};
|
||||
CHECK(!json::accept(text));
|
||||
CHECK(keys_of(text) == expected);
|
||||
CHECK(callback_keys_of(text) == expected);
|
||||
}
|
||||
|
||||
SECTION("a repeated name is delivered once per occurrence")
|
||||
{
|
||||
const std::string text = R"({"a":1,"a":2})";
|
||||
const std::vector<std::string> expected = {"a", "a"};
|
||||
CHECK(keys_of(text) == expected);
|
||||
CHECK(callback_keys_of(text) == expected);
|
||||
}
|
||||
|
||||
SECTION("binary formats pass the stored bytes through")
|
||||
{
|
||||
const std::string stored = "\xff\\";
|
||||
const std::vector<std::string> expected = {stored};
|
||||
const auto keys_of_bytes = [&keys_of_format](const std::vector<std::uint8_t>& v, json::input_format_t format)
|
||||
{
|
||||
std::string text;
|
||||
for (const auto byte : v)
|
||||
{
|
||||
text.push_back(static_cast<char>(byte));
|
||||
}
|
||||
return keys_of_format(text, format);
|
||||
};
|
||||
|
||||
CHECK(keys_of_bytes({0xA1, 0x62, 0xFF, 0x5C, 0x01}, json::input_format_t::cbor) == expected);
|
||||
CHECK(keys_of_bytes({0x81, 0xA2, 0xFF, 0x5C, 0x01}, json::input_format_t::msgpack) == expected);
|
||||
CHECK(keys_of_bytes({'{', 'i', 2, 0xFF, 0x5C, 'i', 1, '}'}, json::input_format_t::ubjson) == expected);
|
||||
CHECK(keys_of_bytes({0x0D, 0, 0, 0, 0x10, 0xFF, 0x5C, 0, 1, 0, 0, 0, 0}, json::input_format_t::bson) == expected);
|
||||
}
|
||||
|
||||
SECTION("BON8 rejects ill-formed UTF-8 in a name")
|
||||
{
|
||||
const std::vector<std::string> valid = {"\\"};
|
||||
CHECK(keys_of_format(std::string("\x87\x5C\x91"), json::input_format_t::bon8) == valid);
|
||||
CHECK(keys_of_format(std::string("\x87\xC3\x28\x91"), json::input_format_t::bon8).empty());
|
||||
}
|
||||
}
|
||||
@@ -826,6 +826,129 @@ TEST_CASE("JSON patch")
|
||||
CHECK(doc.at("foo") == "bar");
|
||||
}
|
||||
#endif // !defined(JSON_NOEXCEPTION)
|
||||
|
||||
SECTION("applying a value as patch to itself")
|
||||
{
|
||||
// a JSON patch is an array, so an array of operations can be
|
||||
// applied to itself; the operations are read from the patch as
|
||||
// it was before the first operation, as patch() does it
|
||||
const json copy_op = {{"op", "copy"}, {"from", "/0"}, {"path", "/-"}};
|
||||
json copy_ops = json::array();
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
copy_ops.push_back(copy_op);
|
||||
}
|
||||
|
||||
json expected_copies = copy_ops;
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
expected_copies.push_back(copy_op);
|
||||
}
|
||||
|
||||
SECTION("copy operations without spare capacity")
|
||||
{
|
||||
// a copy has exactly as much capacity as elements
|
||||
json doc = copy_ops;
|
||||
CHECK(doc.patch(doc) == expected_copies);
|
||||
|
||||
doc.patch_inplace(doc);
|
||||
CHECK(doc == expected_copies);
|
||||
}
|
||||
|
||||
#if JSON_DIAGNOSTICS
|
||||
SECTION("patch stored inside the patched value")
|
||||
{
|
||||
// the parent pointers of JSON_DIAGNOSTICS make the aliasing detectable
|
||||
const json op = {{"op", "replace"}, {"path", "/patch"}, {"value", 1}};
|
||||
json doc = {{"a", 1}, {"patch", json::array({op})}};
|
||||
const json expected = doc.patch(doc["patch"]);
|
||||
|
||||
doc.patch_inplace(doc["patch"]);
|
||||
CHECK(doc == expected);
|
||||
}
|
||||
#endif
|
||||
|
||||
SECTION("copy operations with spare capacity")
|
||||
{
|
||||
// reserve while the array is empty: reallocating non-empty
|
||||
// elements behind the library's back leaves stale parent
|
||||
// pointers with JSON_DIAGNOSTICS
|
||||
json doc = json::array();
|
||||
doc.get_ref<json::array_t&>().reserve(100);
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
doc.push_back(copy_op);
|
||||
}
|
||||
CHECK(doc.patch(doc) == expected_copies);
|
||||
|
||||
doc.patch_inplace(doc);
|
||||
CHECK(doc == expected_copies);
|
||||
}
|
||||
|
||||
SECTION("remove operations")
|
||||
{
|
||||
json doc = R"([
|
||||
{"op": "remove", "path": "/0"},
|
||||
{"op": "remove", "path": "/0"},
|
||||
{"op": "remove", "path": "/0"}
|
||||
])"_json;
|
||||
CHECK(doc.patch(doc) == json::array());
|
||||
|
||||
doc.patch_inplace(doc);
|
||||
CHECK(doc == json::array());
|
||||
}
|
||||
|
||||
SECTION("operations that read a changed element")
|
||||
{
|
||||
// the second operation is still a "test" operation, although
|
||||
// the first one replaced it in the document
|
||||
json doc = R"([
|
||||
{"op": "replace", "path": "/1", "value": 1},
|
||||
{"op": "test", "path": "/1", "value": 1},
|
||||
{"op": "add", "path": "/-", "value": 2}
|
||||
])"_json;
|
||||
const json expected = R"([
|
||||
{"op": "replace", "path": "/1", "value": 1},
|
||||
1,
|
||||
{"op": "add", "path": "/-", "value": 2},
|
||||
2
|
||||
])"_json;
|
||||
CHECK(doc.patch(doc) == expected);
|
||||
|
||||
doc.patch_inplace(doc);
|
||||
CHECK(doc == expected);
|
||||
}
|
||||
|
||||
SECTION("replacing the root")
|
||||
{
|
||||
json doc = R"([
|
||||
{"op": "add", "path": "", "value": [1, 2]},
|
||||
{"op": "add", "path": "/-", "value": 3},
|
||||
{"op": "replace", "path": "", "value": {"a": 4}}
|
||||
])"_json;
|
||||
const json expected = {{"a", 4}};
|
||||
CHECK(doc.patch(doc) == expected);
|
||||
|
||||
doc.patch_inplace(doc);
|
||||
CHECK(doc == expected);
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("failing operation")
|
||||
{
|
||||
// the operations before the failing one remain applied
|
||||
json doc = R"([
|
||||
{"op": "add", "path": "/-", "value": 1},
|
||||
{"op": "test", "path": "/0/op", "value": "remove"}
|
||||
])"_json;
|
||||
CHECK_THROWS_AS(doc.patch(doc), json::other_error&);
|
||||
|
||||
CHECK_THROWS_AS(doc.patch_inplace(doc), json::other_error&);
|
||||
CHECK(doc.size() == 3);
|
||||
CHECK(doc.back() == 1);
|
||||
}
|
||||
#endif // !defined(JSON_NOEXCEPTION)
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("errors")
|
||||
|
||||
@@ -917,6 +917,46 @@ TEST_CASE("JSON pointers")
|
||||
CHECK(ptr.to_string() == "/object/~1");
|
||||
}
|
||||
|
||||
SECTION("appending a pointer to itself")
|
||||
{
|
||||
// operator/= must not pass iterators into its own token vector to
|
||||
// insert(); each pointer is appended to itself and must yield the
|
||||
// pointer whose string representation is the original one twice
|
||||
const std::vector<std::string> pointers =
|
||||
{
|
||||
"",
|
||||
"/",
|
||||
"/a",
|
||||
"/a/b",
|
||||
"/foo/0/-",
|
||||
"/a~1b/~0c/~01",
|
||||
"/1/2/3/4/5/6/7/8/9/10/11/12/13/14/15/16/17"
|
||||
};
|
||||
|
||||
for (const auto& s : pointers)
|
||||
{
|
||||
CAPTURE(s)
|
||||
json::json_pointer ptr(s);
|
||||
const json::json_pointer expected(s + s);
|
||||
|
||||
const json::json_pointer& ret = (ptr /= ptr);
|
||||
CHECK(&ret == &ptr);
|
||||
CHECK(ptr == expected);
|
||||
CHECK(ptr.to_string() == s + s);
|
||||
|
||||
// appending to itself again doubles the pointer once more
|
||||
ptr /= ptr;
|
||||
std::string quadruple = s + s;
|
||||
quadruple += quadruple;
|
||||
CHECK(ptr.to_string() == quadruple);
|
||||
|
||||
// operator/ appends to a copy and leaves the operand unchanged
|
||||
const json::json_pointer original(s);
|
||||
CHECK((original / original) == expected);
|
||||
CHECK(original.to_string() == s);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("equality comparison")
|
||||
{
|
||||
std::string ptr_string{"/foo/bar"};
|
||||
|
||||
@@ -749,6 +749,20 @@ 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:
|
||||
@@ -957,24 +971,24 @@ TEST_CASE("json_view element access and iteration")
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
if (!j.is_object())
|
||||
{
|
||||
CHECK(exception_of([&] { static_cast<void>(v["a"]); }) == exception_of([&] { static_cast<void>(j["a"]); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v["a"]); }) == json_exception_of([&] { static_cast<void>(j["a"]); }));
|
||||
}
|
||||
if (!j.is_array())
|
||||
{
|
||||
CHECK(exception_of([&] { static_cast<void>(v[0]); }) == exception_of([&] { static_cast<void>(j[0]); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v[0]); }) == json_exception_of([&] { static_cast<void>(j[0]); }));
|
||||
}
|
||||
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)); }));
|
||||
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)); }));
|
||||
if (!(j.is_object() && j.empty())) // (key() of an end iterator)
|
||||
{
|
||||
CHECK(exception_of([&] { static_cast<void>(v.begin().key()); }) == exception_of([&] { static_cast<void>(j.begin().key()); }));
|
||||
CHECK(exception_of([&] { static_cast<void>(v.begin().key()); }) == json_exception_of([&] { static_cast<void>(j.begin().key()); }));
|
||||
}
|
||||
if (!j.empty() || j.is_null())
|
||||
{
|
||||
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()); }));
|
||||
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()); }));
|
||||
}
|
||||
#endif
|
||||
CHECK(v.contains("a") == j.contains("a"));
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json_view.hpp>
|
||||
using nlohmann::json;
|
||||
using nlohmann::ordered_json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::ordered_json_editable_document;
|
||||
using nlohmann::json_view;
|
||||
using nlohmann::ordered_json_document;
|
||||
|
||||
#include "json_view_test_helpers.hpp"
|
||||
using json_view_test::generator;
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// These tests were split off unit-json_view_dump.cpp, whose object file got too
|
||||
// large for the MinGW assembler ("file too big"). Like that file, this one is
|
||||
// also built with C++17, as it mentions JSON_HAS_CPP_17.
|
||||
|
||||
TEST_CASE("json_view comparison")
|
||||
{
|
||||
SECTION("equality of the values parse() produces")
|
||||
{
|
||||
generator g;
|
||||
std::vector<std::string> texts;
|
||||
for (int i = 0; i < 600; ++i)
|
||||
{
|
||||
std::string text;
|
||||
g.value(text, 0);
|
||||
texts.push_back(text);
|
||||
// the same value written differently: sorted keys, canonical numbers
|
||||
texts.push_back(json::parse(text).dump(1));
|
||||
}
|
||||
for (std::size_t i = 0; i + 2 < texts.size(); ++i)
|
||||
{
|
||||
for (std::size_t k = i; k < i + 3; ++k)
|
||||
{
|
||||
CAPTURE(texts[i])
|
||||
CAPTURE(texts[k])
|
||||
const json_document a = json_document::parse(texts[i]);
|
||||
const json_document b = json_document::parse(texts[k]);
|
||||
const json ja = json::parse(texts[i]);
|
||||
const json jb = json::parse(texts[k]);
|
||||
CHECK((a.root() == b.root()) == (ja == jb));
|
||||
CHECK((a.root() != b.root()) == (ja != jb));
|
||||
CHECK((a.root() == jb) == (ja == jb));
|
||||
CHECK((jb == a.root()) == (ja == jb));
|
||||
CHECK((a.root() != jb) == (ja != jb));
|
||||
CHECK((jb != a.root()) == (ja != jb));
|
||||
|
||||
// ordered_json compares members in order
|
||||
const ordered_json_document oa = ordered_json_document::parse(texts[i]);
|
||||
const ordered_json_document ob = ordered_json_document::parse(texts[k]);
|
||||
const ordered_json oja = ordered_json::parse(texts[i]);
|
||||
const ordered_json ojb = ordered_json::parse(texts[k]);
|
||||
CHECK((oa.root() == ob.root()) == (oja == ojb));
|
||||
CHECK((oa.root() == ojb) == (oja == ojb));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("objects whose first keys differ or repeat")
|
||||
{
|
||||
const std::array<const char*, 17> texts =
|
||||
{
|
||||
{
|
||||
R"({"a":1,"b":2})", R"({"a":1,"c":2})", R"({"b":1,"a":2})", R"({"a":1,"a":2,"b":3})", R"({"a":2,"b":3})",
|
||||
R"({"a":1,"b":1,"a":2})", R"({"a":2,"b":1})", R"({"a":1,"a":2})", R"({"b":1})", R"({"a":1,"b":2,"c":3,"d":4,"e":5})",
|
||||
R"({"a":1,"b":2,"c":3,"d":4,"f":5})", R"({"a":1,"b":2,"c":3,"a":4,"e":5})", R"({"a":4,"b":2,"c":3,"e":5})",
|
||||
R"({"\u0061":1,"b":2})", R"({})", R"([{"a":1,"b":2}])", R"([{"a":1,"c":2}])"
|
||||
}
|
||||
};
|
||||
for (const char* const x : texts)
|
||||
{
|
||||
for (const char* const y : texts)
|
||||
{
|
||||
CAPTURE(x)
|
||||
CAPTURE(y)
|
||||
const ordered_json_document dx = ordered_json_document::parse(x);
|
||||
const ordered_json_document dy = ordered_json_document::parse(y);
|
||||
const ordered_json_editable_document ex = ordered_json_editable_document::parse(x);
|
||||
const ordered_json jx = ordered_json::parse(x);
|
||||
const ordered_json jy = ordered_json::parse(y);
|
||||
CHECK((dx.root() == dy.root()) == (jx == jy));
|
||||
CHECK((dx.root() == jy) == (jx == jy));
|
||||
CHECK((ex.root() == dy.root()) == (jx == jy));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("objects with duplicate keys, small and large")
|
||||
{
|
||||
// n members with keys in descending order; with duplicates, some keys
|
||||
// repeat later (the first after unique ones in key order); in reverse,
|
||||
// the members are in ascending key order
|
||||
const auto object = [](int n, bool duplicates, bool reverse, int value)
|
||||
{
|
||||
std::string s = "{";
|
||||
for (int i = 0; i < n; ++i)
|
||||
{
|
||||
const int k = reverse ? i : n - 1 - i;
|
||||
s += (i != 0 ? ",\"k" : "\"k") + std::to_string(100 + k) + "\":" + std::to_string(value + k);
|
||||
}
|
||||
if (duplicates)
|
||||
{
|
||||
for (int k = n / 2; k < n; k += 3)
|
||||
{
|
||||
s += ",\"k" + std::to_string(100 + k) + "\":" + std::to_string(value + k);
|
||||
}
|
||||
}
|
||||
return s + "}";
|
||||
};
|
||||
std::vector<std::string> texts;
|
||||
for (const int n :
|
||||
{
|
||||
3, 16, 17, 40
|
||||
})
|
||||
{
|
||||
for (const int flags :
|
||||
{
|
||||
0, 1, 2, 3
|
||||
})
|
||||
{
|
||||
texts.push_back(object(n, (flags & 1) != 0, (flags & 2) != 0, 0));
|
||||
}
|
||||
texts.push_back(object(n, true, false, 1)); // other values
|
||||
}
|
||||
// many objects in one comparison (working memory reused across objects)
|
||||
std::string array = "[";
|
||||
std::string array_other = "[";
|
||||
for (std::size_t i = 0; i < texts.size(); ++i)
|
||||
{
|
||||
array += (i != 0 ? "," : "") + texts[i];
|
||||
array_other += (i != 0 ? "," : "") + texts[texts.size() - 1 - i];
|
||||
}
|
||||
texts.push_back(array + "]");
|
||||
texts.push_back(array_other + "]");
|
||||
for (const std::string& x : texts)
|
||||
{
|
||||
for (const std::string& y : texts)
|
||||
{
|
||||
CAPTURE(x)
|
||||
CAPTURE(y)
|
||||
const ordered_json_document dx = ordered_json_document::parse(x);
|
||||
const ordered_json_document dy = ordered_json_document::parse(y);
|
||||
const ordered_json jx = ordered_json::parse(x);
|
||||
const ordered_json jy = ordered_json::parse(y);
|
||||
CHECK((dx.root() == dy.root()) == (jx == jy));
|
||||
CHECK((dx.root() == jy) == (jx == jy));
|
||||
const json_document ux = json_document::parse(x);
|
||||
const json uy = json::parse(y);
|
||||
CHECK((ux.root() == uy) == (json::parse(x) == uy));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("numbers, duplicate keys, member order")
|
||||
{
|
||||
const auto same = [](const char* x, const char* y)
|
||||
{
|
||||
const json_document dx = json_document::parse(x);
|
||||
const json_document dy = json_document::parse(y);
|
||||
return dx.root() == dy.root();
|
||||
};
|
||||
CHECK(same("1", "1.0"));
|
||||
CHECK(same("[1, -1, 2.5]", "[1.0, -1.0, 25e-1]"));
|
||||
CHECK(!same("1", "1.5"));
|
||||
CHECK(same("18446744073709551615", "18446744073709551615"));
|
||||
CHECK(same(R"({"a": 1, "a": 2})", R"({"a": 2})"));
|
||||
CHECK(!same(R"({"a": 1, "a": 2})", R"({"a": 1})"));
|
||||
CHECK(same(R"({"a": 1, "b": 2})", R"({"b": 2, "a": 1})"));
|
||||
CHECK(!same(R"({"a": 1})", R"({"a": 1, "b": 2})"));
|
||||
CHECK(!same("[1, 2]", "[2, 1]"));
|
||||
CHECK(!same("\"a\"", "\"b\""));
|
||||
CHECK(same("\"\\u00e9\"", "\"\xc3\xa9\""));
|
||||
CHECK(!same("null", "false"));
|
||||
CHECK(!same("[]", "{}"));
|
||||
const ordered_json_document dup = ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})");
|
||||
const ordered_json_document last = ordered_json_document::parse(R"({"a": 3, "b": 2})");
|
||||
CHECK(dup.root() == last.root());
|
||||
// (== compares as parse() resolves duplicates, a lookup finds the first)
|
||||
CHECK(dup.root()["a"].materialize() == 1);
|
||||
CHECK(dup.root()["a"] != last.root()["a"]);
|
||||
const ordered_json_document ab = ordered_json_document::parse(R"({"a": 1, "b": 2})");
|
||||
const ordered_json_document ba = ordered_json_document::parse(R"({"b": 2, "a": 1})");
|
||||
CHECK(ab.root() != ba.root());
|
||||
|
||||
// discarded values compare as basic_json's do
|
||||
const json discarded(json::value_t::discarded);
|
||||
CHECK((json_view() == json_view()) == (discarded == discarded)); // NOLINT(readability-container-size-empty): operator== is tested
|
||||
CHECK((json_view() == discarded) == (discarded == discarded)); // NOLINT(readability-container-size-empty)
|
||||
const json_document null_document = json_document::parse("null");
|
||||
CHECK(!(json_view() == null_document.root())); // NOLINT(readability-container-size-empty)
|
||||
CHECK(!(null_document.root() == discarded));
|
||||
}
|
||||
|
||||
SECTION("values that are the same text, or the same value written differently, at any depth")
|
||||
{
|
||||
// nested arrays and objects around depths where the walk over equal text gives up
|
||||
for (const int depth :
|
||||
{
|
||||
1, 5, 47, 48, 49, 50, 60, 300
|
||||
})
|
||||
{
|
||||
CAPTURE(depth)
|
||||
std::string head;
|
||||
std::string tail;
|
||||
for (int i = 0; i < depth; ++i)
|
||||
{
|
||||
head += i % 2 == 0 ? R"({"k":[1,"s",{"x":true},)" : R"([null,2.5,{"y":)";
|
||||
tail.insert(0, i % 2 == 0 ? "]}" : "}]");
|
||||
}
|
||||
// the same text, one number changed, and one number written differently
|
||||
const auto with_number = [&](const char* number)
|
||||
{
|
||||
std::string result = head;
|
||||
result += number;
|
||||
result += tail;
|
||||
return result;
|
||||
};
|
||||
const std::string text = with_number("7");
|
||||
const std::string changed = with_number("8");
|
||||
const std::string spelled = with_number("7.0");
|
||||
const json_document a = json_document::parse(text);
|
||||
const json_document b = json_document::parse(text);
|
||||
const json_document c = json_document::parse(changed);
|
||||
const json_document d = json_document::parse(spelled);
|
||||
CHECK(a.root() == b.root());
|
||||
CHECK(a.root() != c.root());
|
||||
CHECK(c.root() != a.root());
|
||||
CHECK(a.root() == d.root());
|
||||
CHECK(d.root() == a.root());
|
||||
CHECK(a.root() == json::parse(text));
|
||||
CHECK(a.root() != json::parse(changed));
|
||||
CHECK(json::parse(spelled) == a.root());
|
||||
const nlohmann::json_editable_document e = nlohmann::json_editable_document::parse(text);
|
||||
CHECK(a.root() == e.root());
|
||||
CHECK(e.root() == a.root());
|
||||
CHECK(c.root() != e.root());
|
||||
CHECK(e.root() == e.root());
|
||||
const ordered_json_document oa = ordered_json_document::parse(text);
|
||||
const ordered_json_document oc = ordered_json_document::parse(changed);
|
||||
CHECK(oa.root() != oc.root());
|
||||
CHECK(oa.root() == ordered_json::parse(text));
|
||||
CHECK(oc.root() != ordered_json::parse(text));
|
||||
}
|
||||
|
||||
// not the same text, but the same value
|
||||
const auto same = [](const char* x, const char* y)
|
||||
{
|
||||
const json_document dx = json_document::parse(x);
|
||||
const json_document dy = json_document::parse(y);
|
||||
const bool equal = dx.root() == dy.root();
|
||||
CHECK(equal == (json::parse(x) == json::parse(y)));
|
||||
CHECK(equal == (dx.root() == json::parse(y)));
|
||||
return equal;
|
||||
};
|
||||
CHECK(same(R"({"a":1,"a":2})", R"({"a":3,"a":2})"));
|
||||
CHECK(same(R"({"a":1,"b":[1,{"c":2,"c":3}]})", R"({"b":[1,{"c":9,"c":3}],"a":1})"));
|
||||
CHECK(!same(R"({"a":1,"a":2})", R"({"a":2,"a":3})"));
|
||||
CHECK(same("[1.0,-0,0,1e2]", "[1.00,0,-0,100.0]"));
|
||||
CHECK(same(R"(["\u00e9","\n"])", "[\"\xc3\xa9\",\"\\n\"]"));
|
||||
CHECK(!same(R"({"a":[1,2,{"b":null}]})", R"({"a":[1,2,{"b":false}]})"));
|
||||
CHECK(!same(R"({"a":[1,2,{"b":null}]})", R"({"a":[1,2,{"c":null}]})"));
|
||||
CHECK(!same(R"({"a":[1,2,{"b":null}]})", R"({"a":[1,2,{"b":null,"c":1}]})"));
|
||||
|
||||
// edited documents: moved sequences, links, and values of new nodes
|
||||
nlohmann::json_editable_document edited = nlohmann::json_editable_document::parse(R"({"a":[1,2,3],"b":{"c":"d"},"e":0.5})");
|
||||
const json_document plain = json_document::parse(R"({"a":[1,2,3,4],"b":{"c":"x","y":null},"e":0.5,"z":[]})");
|
||||
edited.push_back(edited.root()["a"], 4);
|
||||
edited.set(edited.root()["b"], "c", "x");
|
||||
edited.set(edited.root()["b"], "y", nullptr);
|
||||
edited.set(edited.root(), "z", json::array());
|
||||
CHECK(edited.root() == plain.root());
|
||||
CHECK(plain.root() == edited.root());
|
||||
edited.erase(edited.root()["a"], 0);
|
||||
CHECK(edited.root() != plain.root());
|
||||
CHECK(edited.root()["a"] == json::parse("[2,3,4]"));
|
||||
const json_document tail = json_document::parse("[2,3,4]");
|
||||
CHECK(edited.root()["a"] == tail.root());
|
||||
}
|
||||
|
||||
SECTION("large documents, with a difference at any place")
|
||||
{
|
||||
const auto make = [](std::size_t count, std::size_t changed, bool swap_members)
|
||||
{
|
||||
std::string text = R"({"b":1,"list":[)";
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
text += (i != 0 ? "," : "");
|
||||
const std::string id = std::to_string(i == changed ? i + 1 : i);
|
||||
if (swap_members && i % 3 == 0)
|
||||
{
|
||||
text += R"({"v":[1.5,"s",{"k":)";
|
||||
text += id;
|
||||
text += R"(}],"id":)";
|
||||
text += id;
|
||||
text += "}";
|
||||
}
|
||||
else
|
||||
{
|
||||
text += R"({"id":)";
|
||||
text += id;
|
||||
text += R"(,"v":[1.5,"s",{"k":)";
|
||||
text += id;
|
||||
text += "}]}";
|
||||
}
|
||||
}
|
||||
return text + R"(],"a":{"y":2,"x":3}})";
|
||||
};
|
||||
const std::size_t count = 3000;
|
||||
for (const std::size_t changed :
|
||||
{
|
||||
count, std::size_t{0}, std::size_t{1}, count / 2, count - 1
|
||||
})
|
||||
{
|
||||
for (const bool swap :
|
||||
{
|
||||
false, true
|
||||
})
|
||||
{
|
||||
CAPTURE(changed)
|
||||
CAPTURE(swap)
|
||||
const std::string text_a = make(count, count, false);
|
||||
const std::string text_b = make(count, changed, swap);
|
||||
const json_document a = json_document::parse(text_a);
|
||||
const json_document b = json_document::parse(text_b);
|
||||
const bool equal = changed == count;
|
||||
CHECK((a.root() == b.root()) == equal);
|
||||
CHECK((b.root() == a.root()) == equal);
|
||||
CHECK((a.root() == json::parse(text_b)) == equal);
|
||||
CHECK((json::parse(text_b) == a.root()) == equal);
|
||||
CHECK((a.root()["list"] == b.root()["list"]) == equal);
|
||||
const ordered_json_document oa = ordered_json_document::parse(text_a);
|
||||
const ordered_json_document ob = ordered_json_document::parse(text_b);
|
||||
CHECK((oa.root() == ob.root()) == (equal && !swap));
|
||||
CHECK((oa.root() == ordered_json::parse(text_b)) == (equal && !swap));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("deep nesting")
|
||||
{
|
||||
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
|
||||
const json_document a = json_document::parse(deep);
|
||||
const json_document b = json_document::parse(deep);
|
||||
CHECK(a.root() == b.root());
|
||||
CHECK(a.root() == json::parse(deep));
|
||||
const std::string other = std::string(100000, '[') + "1" + std::string(100000, ']');
|
||||
const json_document c = json_document::parse(other);
|
||||
CHECK(a.root() != c.root());
|
||||
}
|
||||
}
|
||||
@@ -432,101 +432,6 @@ TEST_CASE("json_view dump")
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view comparison")
|
||||
{
|
||||
SECTION("equality of the values parse() produces")
|
||||
{
|
||||
generator g;
|
||||
std::vector<std::string> texts;
|
||||
for (int i = 0; i < 600; ++i)
|
||||
{
|
||||
std::string text;
|
||||
g.value(text, 0);
|
||||
texts.push_back(text);
|
||||
// the same value written differently: sorted keys, canonical numbers
|
||||
texts.push_back(json::parse(text).dump(1));
|
||||
}
|
||||
for (std::size_t i = 0; i + 2 < texts.size(); ++i)
|
||||
{
|
||||
for (std::size_t k = i; k < i + 3; ++k)
|
||||
{
|
||||
CAPTURE(texts[i])
|
||||
CAPTURE(texts[k])
|
||||
const json_document a = json_document::parse(texts[i]);
|
||||
const json_document b = json_document::parse(texts[k]);
|
||||
const json ja = json::parse(texts[i]);
|
||||
const json jb = json::parse(texts[k]);
|
||||
CHECK((a.root() == b.root()) == (ja == jb));
|
||||
CHECK((a.root() != b.root()) == (ja != jb));
|
||||
CHECK((a.root() == jb) == (ja == jb));
|
||||
CHECK((jb == a.root()) == (ja == jb));
|
||||
CHECK((a.root() != jb) == (ja != jb));
|
||||
CHECK((jb != a.root()) == (ja != jb));
|
||||
|
||||
// ordered_json compares members in order
|
||||
const ordered_json_document oa = ordered_json_document::parse(texts[i]);
|
||||
const ordered_json_document ob = ordered_json_document::parse(texts[k]);
|
||||
const ordered_json oja = ordered_json::parse(texts[i]);
|
||||
const ordered_json ojb = ordered_json::parse(texts[k]);
|
||||
CHECK((oa.root() == ob.root()) == (oja == ojb));
|
||||
CHECK((oa.root() == ojb) == (oja == ojb));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("numbers, duplicate keys, member order")
|
||||
{
|
||||
const auto same = [](const char* x, const char* y)
|
||||
{
|
||||
const json_document dx = json_document::parse(x);
|
||||
const json_document dy = json_document::parse(y);
|
||||
return dx.root() == dy.root();
|
||||
};
|
||||
CHECK(same("1", "1.0"));
|
||||
CHECK(same("[1, -1, 2.5]", "[1.0, -1.0, 25e-1]"));
|
||||
CHECK(!same("1", "1.5"));
|
||||
CHECK(same("18446744073709551615", "18446744073709551615"));
|
||||
CHECK(same(R"({"a": 1, "a": 2})", R"({"a": 2})"));
|
||||
CHECK(!same(R"({"a": 1, "a": 2})", R"({"a": 1})"));
|
||||
CHECK(same(R"({"a": 1, "b": 2})", R"({"b": 2, "a": 1})"));
|
||||
CHECK(!same(R"({"a": 1})", R"({"a": 1, "b": 2})"));
|
||||
CHECK(!same("[1, 2]", "[2, 1]"));
|
||||
CHECK(!same("\"a\"", "\"b\""));
|
||||
CHECK(same("\"\\u00e9\"", "\"\xc3\xa9\""));
|
||||
CHECK(!same("null", "false"));
|
||||
CHECK(!same("[]", "{}"));
|
||||
const ordered_json_document dup = ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})");
|
||||
const ordered_json_document last = ordered_json_document::parse(R"({"a": 3, "b": 2})");
|
||||
CHECK(dup.root() == last.root());
|
||||
// (== compares as parse() resolves duplicates, a lookup finds the first)
|
||||
CHECK(dup.root()["a"].materialize() == 1);
|
||||
CHECK(dup.root()["a"] != last.root()["a"]);
|
||||
const ordered_json_document ab = ordered_json_document::parse(R"({"a": 1, "b": 2})");
|
||||
const ordered_json_document ba = ordered_json_document::parse(R"({"b": 2, "a": 1})");
|
||||
CHECK(ab.root() != ba.root());
|
||||
|
||||
// discarded values compare as basic_json's do
|
||||
const json discarded(json::value_t::discarded);
|
||||
CHECK((json_view() == json_view()) == (discarded == discarded)); // NOLINT(readability-container-size-empty): operator== is tested
|
||||
CHECK((json_view() == discarded) == (discarded == discarded)); // NOLINT(readability-container-size-empty)
|
||||
const json_document null_document = json_document::parse("null");
|
||||
CHECK(!(json_view() == null_document.root())); // NOLINT(readability-container-size-empty)
|
||||
CHECK(!(null_document.root() == discarded));
|
||||
}
|
||||
|
||||
SECTION("deep nesting")
|
||||
{
|
||||
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
|
||||
const json_document a = json_document::parse(deep);
|
||||
const json_document b = json_document::parse(deep);
|
||||
CHECK(a.root() == b.root());
|
||||
CHECK(a.root() == json::parse(deep));
|
||||
const std::string other = std::string(100000, '[') + "1" + std::string(100000, ']');
|
||||
const json_document c = json_document::parse(other);
|
||||
CHECK(a.root() != c.root());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("json_view large objects")
|
||||
{
|
||||
// objects with 128 members or more are looked up with a hash index
|
||||
|
||||
@@ -941,3 +941,123 @@ TEST_CASE("json_view edits: duplicate keys")
|
||||
CHECK(d.root()["o"]["d"].get<int>() == 0);
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
TEST_CASE("json_view edits: check_utf8 agrees with the byte-wise check")
|
||||
{
|
||||
namespace dv = nlohmann::detail::view;
|
||||
|
||||
// "ok" or the exception message
|
||||
const auto run = [](bool fast, const std::string & s) -> std::string
|
||||
{
|
||||
try
|
||||
{
|
||||
if (fast)
|
||||
{
|
||||
dv::check_utf8(s.data(), s.size());
|
||||
}
|
||||
else
|
||||
{
|
||||
dv::check_utf8_bytewise(reinterpret_cast<const unsigned char*>(s.data()), s.size(), 0);
|
||||
}
|
||||
}
|
||||
catch (const json::type_error& e)
|
||||
{
|
||||
return {e.what()};
|
||||
}
|
||||
return {"ok"};
|
||||
};
|
||||
std::size_t errors = 0;
|
||||
const auto same = [&](const std::string & s)
|
||||
{
|
||||
const std::string a = run(true, s);
|
||||
const std::string b = run(false, s);
|
||||
if (a != b)
|
||||
{
|
||||
INFO("fast: " << a << ", byte-wise: " << b << ", length " << s.size());
|
||||
CHECK(a == b);
|
||||
}
|
||||
errors += b != "ok" ? 1u : 0u;
|
||||
};
|
||||
|
||||
const std::vector<std::string> pieces =
|
||||
{
|
||||
"a", "Z", " ", "\"", "\\", "\n", "\x01", "\x7f", "\xc3\xa4", "\xe4\xb8\xad", "\xf0\x9f\x98\x80",
|
||||
"\x80", "\xbf", "\xc0\x80", "\xc1\xbf", "\xc2", "\xe0\x80\x80", "\xe0\x9f\xbf", "\xed\xa0\x80", "\xed\xbf\xbf",
|
||||
"\xf0\x80\x80\x80", "\xf0\x8f\xbf\xbf", "\xf4\x90\x80\x80", "\xf4\x8f\xbf\xbf", "\xf5", "\xf8", "\xff", "\xfe",
|
||||
"\xe4\xb8", "\xf0\x9f\x98", "\xf0\x9f", "\xf0"
|
||||
};
|
||||
|
||||
// every piece behind prefixes of every length, and followed by more bytes
|
||||
for (std::size_t prefix = 0; prefix <= 64; ++prefix)
|
||||
{
|
||||
for (const auto& piece : pieces)
|
||||
{
|
||||
same(std::string(prefix, 'x') + piece);
|
||||
same(std::string(prefix, 'x') + piece + "yz");
|
||||
same(std::string(prefix, '"') + piece);
|
||||
same(std::string(prefix, '\xc3') + piece);
|
||||
same(std::string(prefix / 2, '\xc3').append(prefix / 2, '\xa4') + piece);
|
||||
same(std::string(prefix, 'x') + "\xe4\xb8\xad" + piece);
|
||||
}
|
||||
}
|
||||
// an invalid byte at every position of valid strings of every length
|
||||
for (std::size_t len = 0; len <= 64; ++len)
|
||||
{
|
||||
const std::string ascii(len, 'a');
|
||||
std::string mixed;
|
||||
while (mixed.size() < len)
|
||||
{
|
||||
if (mixed.size() % 7 == 0)
|
||||
{
|
||||
mixed += "\xc3\xa4";
|
||||
}
|
||||
else if (mixed.size() % 11 == 0)
|
||||
{
|
||||
mixed += "\xf0\x9f\x98\x80";
|
||||
}
|
||||
else
|
||||
{
|
||||
mixed += "a";
|
||||
}
|
||||
}
|
||||
same(ascii);
|
||||
same(mixed);
|
||||
for (std::size_t pos = 0; pos < len; ++pos)
|
||||
{
|
||||
for (const char bad :
|
||||
{'\x80', '\xc0', '\xc2', '\xe0', '\xf4', '\xff', '"', '\\', '\x1f'
|
||||
})
|
||||
{
|
||||
std::string s = ascii;
|
||||
s[pos] = bad;
|
||||
same(s);
|
||||
s = mixed;
|
||||
s[pos] = bad;
|
||||
same(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
// random bytes, biased to the interesting ones
|
||||
std::mt19937 gen(5869); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
|
||||
static const std::array<unsigned char, 23> interesting = {{0x00, 0x1f, 0x22, 0x5c, 0x7f, 0x80, 0x8f, 0x90, 0x9f, 0xa0, 0xbf, 0xc0, 0xc2, 0xdf, 0xe0, 0xe1, 0xed, 0xef, 0xf0, 0xf1, 0xf4, 0xf5, 0xff}};
|
||||
for (int i = 0; i < 100000; ++i)
|
||||
{
|
||||
std::string s(gen() % 80, 'a');
|
||||
for (auto& c : s)
|
||||
{
|
||||
const unsigned r = gen() % 8;
|
||||
if (r == 0)
|
||||
{
|
||||
c = static_cast<char>(gen() & 0xff);
|
||||
}
|
||||
else if (r == 1)
|
||||
{
|
||||
c = static_cast<char>(interesting[gen() % interesting.size()]);
|
||||
}
|
||||
}
|
||||
same(s);
|
||||
}
|
||||
CHECK(errors > 1000);
|
||||
}
|
||||
#endif
|
||||
@@ -54,14 +54,18 @@ int accepts_any(nlohmann::basic_json_view<BasicJsonType, Mode> /*unused*/)
|
||||
return Mode == nlohmann::view_mode::editable ? 3 : 4;
|
||||
}
|
||||
|
||||
template<typename View>
|
||||
auto overload(View v) -> decltype(accepts_editable(v))
|
||||
// overloads that differ only in the mode; deduction ignores the conversion from
|
||||
// read-only to editable views, so each view picks the overload of its own mode
|
||||
// (two templates that differ only in a decltype return type are a redefinition
|
||||
// for MSVC 2015)
|
||||
template<typename BasicJsonType>
|
||||
int overload(nlohmann::basic_json_view<BasicJsonType, nlohmann::view_mode::editable> v)
|
||||
{
|
||||
return accepts_editable(v);
|
||||
}
|
||||
|
||||
template<typename View>
|
||||
auto overload(View v) -> decltype(accepts_read_only(v))
|
||||
template<typename BasicJsonType>
|
||||
int overload(nlohmann::basic_json_view<BasicJsonType, nlohmann::view_mode::read_only> v)
|
||||
{
|
||||
return accepts_read_only(v);
|
||||
}
|
||||
|
||||
@@ -69,9 +69,12 @@ struct point
|
||||
struct shape
|
||||
{
|
||||
// initialized for GCC -Weffc++
|
||||
// NOLINTNEXTLINE(readability-redundant-member-init)
|
||||
std::string name{};
|
||||
// NOLINTNEXTLINE(readability-redundant-member-init)
|
||||
std::vector<point> points{};
|
||||
point center{};
|
||||
// NOLINTNEXTLINE(readability-redundant-member-init)
|
||||
std::map<std::string, point> named{};
|
||||
NLOHMANN_DEFINE_TYPE_INTRUSIVE(shape, name, points, center, named)
|
||||
};
|
||||
@@ -82,13 +85,17 @@ struct with_defaults
|
||||
std::string b = "default";
|
||||
std::vector<int> c = {1, 2};
|
||||
};
|
||||
// NOLINTNEXTLINE(misc-use-internal-linkage): found by ADL, like the from_json() functions of users
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(with_defaults, a, b, c)
|
||||
|
||||
struct with_names
|
||||
{
|
||||
int value = 0;
|
||||
std::string text{}; // initialized for GCC -Weffc++
|
||||
// initialized for GCC -Weffc++
|
||||
// NOLINTNEXTLINE(readability-redundant-member-init)
|
||||
std::string text{};
|
||||
};
|
||||
// NOLINTNEXTLINE(misc-use-internal-linkage): found by ADL, like the from_json() functions of users
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_NAMES(with_names, "Value", value, "Text", text)
|
||||
|
||||
struct empty
|
||||
@@ -102,6 +109,7 @@ enum class color
|
||||
green,
|
||||
blue
|
||||
};
|
||||
// NOLINTNEXTLINE(misc-use-internal-linkage): found by ADL, like the from_json() functions of users
|
||||
NLOHMANN_JSON_SERIALIZE_ENUM(color,
|
||||
{
|
||||
{color::red, "red"},
|
||||
@@ -114,6 +122,7 @@ enum class strict_color
|
||||
red,
|
||||
green
|
||||
};
|
||||
// NOLINTNEXTLINE(misc-use-internal-linkage): found by ADL, like the from_json() functions of users
|
||||
NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(strict_color,
|
||||
{
|
||||
{strict_color::red, "red"},
|
||||
@@ -154,6 +163,7 @@ struct derived_from_legacy : legacy_base
|
||||
{
|
||||
int extra = 0;
|
||||
};
|
||||
// NOLINTNEXTLINE(misc-use-internal-linkage): found by ADL, like the from_json() functions of users
|
||||
NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE(derived_from_legacy, legacy_base, extra)
|
||||
|
||||
// from_json() for json only: converted through materialize()
|
||||
@@ -301,18 +311,19 @@ TEST_CASE("json_view from_json")
|
||||
CHECK(same<std::string>(v, j, "text"));
|
||||
CHECK(same<std::nullptr_t>(v, j, "nothing"));
|
||||
CHECK(same<json>(v, j, "object"));
|
||||
const std::valarray<int> va = v["ints"].get<std::valarray<int>>();
|
||||
const auto va = v["ints"].get<std::valarray<int>>();
|
||||
CHECK(va.size() == 4);
|
||||
CHECK(va[0] == 3);
|
||||
int c_array[2][2] = {{0, 0}, {0, 0}}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays): the conversion to C arrays
|
||||
v["nested"][0].get_to(c_array[0]);
|
||||
CHECK(c_array[0][1] == 2);
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
CHECK(v["points"][0].get<std::optional<point>>()->y == 2);
|
||||
const auto first = v["points"][0].get<std::optional<point>>();
|
||||
CHECK((first.has_value() && first->y == 2));
|
||||
CHECK(!v["nothing"].get<std::optional<point>>().has_value());
|
||||
#endif
|
||||
// containers of views keep views of the elements
|
||||
const std::vector<json_view> views = v["points"].get<std::vector<json_view>>();
|
||||
const auto views = v["points"].get<std::vector<json_view>>();
|
||||
CHECK(views.size() == 2);
|
||||
CHECK(views[1]["x"].get<int>() == 3);
|
||||
CHECK(v["object"].get<std::map<std::string, json_view>>().at("a").get<int>() == 2);
|
||||
@@ -326,7 +337,8 @@ TEST_CASE("json_view from_json")
|
||||
CHECK(d.root().get<std::unordered_map<std::string, int>>().at("a") == 3);
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS
|
||||
// (the exceptions of views have neither the path nor the byte range of the value)
|
||||
#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS
|
||||
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])");
|
||||
@@ -352,7 +364,7 @@ TEST_CASE("json_view from_json")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"name": "s", "points": [{"x": 1, "y": 2}, {"x": 3, "y": 4}],
|
||||
"center": {"x": 5, "y": 6}, "named": {"a": {"x": 7, "y": 8}}})");
|
||||
const shape s = d.root().get<shape>();
|
||||
const auto s = d.root().get<shape>();
|
||||
CHECK(s.name == "s");
|
||||
CHECK(s.points.size() == 2);
|
||||
CHECK(s.points[1].y == 4);
|
||||
@@ -370,13 +382,13 @@ TEST_CASE("json_view from_json")
|
||||
SECTION("defaults and names")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"a": 5, "Value": 3, "Text": "t"})");
|
||||
const with_defaults w = d.root().get<with_defaults>();
|
||||
const auto w = d.root().get<with_defaults>();
|
||||
CHECK(w.a == 5);
|
||||
CHECK(w.b == "default");
|
||||
CHECK(w.c == std::vector<int> {1, 2});
|
||||
const json_document null_document = json_document::parse("null");
|
||||
CHECK(null_document.root().get<with_defaults>().a == 1);
|
||||
const with_names n = d.root().get<with_names>();
|
||||
const auto n = d.root().get<with_names>();
|
||||
CHECK(n.value == 3);
|
||||
CHECK(n.text == "t");
|
||||
}
|
||||
@@ -392,7 +404,7 @@ TEST_CASE("json_view from_json")
|
||||
#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
|
||||
#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>()); }));
|
||||
#endif
|
||||
#endif
|
||||
@@ -401,13 +413,13 @@ TEST_CASE("json_view from_json")
|
||||
SECTION("derived types")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"id": 7, "c": "green", "extra": 3})");
|
||||
const derived x = d.root().get<derived>();
|
||||
const auto x = d.root().get<derived>();
|
||||
CHECK(x.id == 7);
|
||||
CHECK(x.c == color::green);
|
||||
const derived_with_defaults y = d.root().get<derived_with_defaults>();
|
||||
const auto y = d.root().get<derived_with_defaults>();
|
||||
CHECK(y.id == 7);
|
||||
CHECK(y.label == "none");
|
||||
const derived_from_legacy z = d.root().get<derived_from_legacy>();
|
||||
const auto z = d.root().get<derived_from_legacy>();
|
||||
CHECK(z.id == 7);
|
||||
CHECK(z.extra == 3);
|
||||
}
|
||||
@@ -440,10 +452,11 @@ TEST_CASE("json_view from_json")
|
||||
CHECK(o.root().get<shape>().center.y == 2);
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS
|
||||
#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS
|
||||
SECTION("errors are those of the same conversion of a json value")
|
||||
{
|
||||
// (with JSON_DIAGNOSTICS, those of json name the path of the 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)
|
||||
|
||||
@@ -185,8 +185,10 @@ std::uint32_t rng()
|
||||
|
||||
TEST_CASE("json_view images: round trips")
|
||||
{
|
||||
static_assert(json_view_image_test::has_save<json_document>::value && json_view_image_test::has_load<json_document>::value, "save() and load() on a little-endian target");
|
||||
static_assert(json_view_image_test::has_save<ordered_json_editable_document>::value && json_view_image_test::has_load<ordered_json_editable_document>::value, "save() and load() on a little-endian target");
|
||||
static_assert(json_view_image_test::has_save<json_document>::value, "save() on a little-endian target");
|
||||
static_assert(json_view_image_test::has_load<json_document>::value, "load() on a little-endian target");
|
||||
static_assert(json_view_image_test::has_save<ordered_json_editable_document>::value, "save() on a little-endian target");
|
||||
static_assert(json_view_image_test::has_load<ordered_json_editable_document>::value, "load() on a little-endian target");
|
||||
|
||||
SECTION("small documents")
|
||||
{
|
||||
@@ -699,6 +701,64 @@ TEST_CASE("json_view images: ownership")
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
TEST_CASE("json_view images: errors")
|
||||
{
|
||||
SECTION("float tokens: a changed byte is accepted if the text is still a number with the digits the node records")
|
||||
{
|
||||
for (const char* const token :
|
||||
{
|
||||
"-12.375", "0.5", "123456.0", "-0.0", "9999999999999999999.5", "0.000000000000000000000001", "1.25e10", "5E-3"
|
||||
})
|
||||
{
|
||||
CAPTURE(token)
|
||||
const std::string text = std::string("[") + token + "]";
|
||||
const json_document d = json_document::parse(text);
|
||||
const std::vector<std::uint8_t> image = d.save();
|
||||
const std::size_t at = text_at(image) + 1; // the first byte of the token
|
||||
const std::uint16_t recorded = node_at(image, 1).extra;
|
||||
CHECK(loaded_dump(image) == json::parse(text).dump());
|
||||
for (std::size_t pos = 0; token[pos] != '\0'; ++pos)
|
||||
{
|
||||
for (unsigned int byte = 0; byte < 256; ++byte)
|
||||
{
|
||||
std::vector<std::uint8_t> changed = image;
|
||||
changed[at + pos] = static_cast<std::uint8_t>(byte);
|
||||
std::string t = token;
|
||||
t[pos] = static_cast<char>(byte);
|
||||
// the digits of the integer part and of the fraction of the changed token
|
||||
std::size_t integer = 0;
|
||||
std::size_t fraction = 0;
|
||||
std::size_t k = (t[0] == '-') ? 1 : 0;
|
||||
for (; k < t.size() && t[k] >= '0' && t[k] <= '9'; ++k)
|
||||
{
|
||||
++integer;
|
||||
}
|
||||
if (k < t.size() && t[k] == '.')
|
||||
{
|
||||
for (++k; k < t.size() && t[k] >= '0' && t[k] <= '9'; ++k)
|
||||
{
|
||||
++fraction;
|
||||
}
|
||||
}
|
||||
// (as a JSON text, the token may have white space around it)
|
||||
const bool float_token = json::accept(t) && t.find_first_of(std::string(" \t\r\n\0", 5)) == std::string::npos && (t.find('.') != std::string::npos || t.find_first_of("eE") != std::string::npos) // NOLINT(abseil-string-find-str-contains)
|
||||
&& ((integer | (fraction << 8U)) == recorded);
|
||||
CAPTURE(pos)
|
||||
CAPTURE(byte)
|
||||
std::string result;
|
||||
try
|
||||
{
|
||||
result = loaded_dump(changed);
|
||||
}
|
||||
catch (const json::parse_error& e)
|
||||
{
|
||||
CHECK(std::string(e.what()) == check_failed);
|
||||
result = "rejected";
|
||||
}
|
||||
CHECK((result != "rejected") == float_token);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("invalid UTF-8 of a decoded string: the compact dump writes it, the others throw")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"k\n":"\u00e9x"})");
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | 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
|
||||
|
||||
// source positions of materialize() (JSON_DIAGNOSTIC_POSITIONS, #5864)
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#ifndef JSON_DIAGNOSTICS
|
||||
#define JSON_DIAGNOSTICS 1
|
||||
#endif
|
||||
#define JSON_DIAGNOSTIC_POSITIONS 1
|
||||
#include <nlohmann/json_view.hpp>
|
||||
using nlohmann::json;
|
||||
using nlohmann::ordered_json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::json_editable_document;
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "json_view_test_helpers.hpp"
|
||||
#include <test_data.hpp>
|
||||
|
||||
namespace
|
||||
{
|
||||
// not constexpr: VS 2015 does not accept std::string::npos in a constant expression (C2131)
|
||||
const std::size_t npos = std::string::npos;
|
||||
|
||||
// whether every value of a has the source positions of the value of b at the
|
||||
// same place (a and b are equal; shallow test documents only)
|
||||
template<typename BasicJsonType>
|
||||
bool same_positions(const BasicJsonType& a, const BasicJsonType& b)
|
||||
{
|
||||
if (a.start_pos() != b.start_pos() || a.end_pos() != b.end_pos() || a.type() != b.type() || a.size() != b.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (a.is_object())
|
||||
{
|
||||
for (auto it = a.begin(); it != a.end(); ++it)
|
||||
{
|
||||
if (!same_positions(it.value(), b.at(it.key())))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (a.is_array())
|
||||
{
|
||||
for (std::size_t i = 0; i < a.size(); ++i)
|
||||
{
|
||||
if (!same_positions(a[i], b[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// the materialized root of text has the positions of parse()
|
||||
template<typename BasicJsonType>
|
||||
bool positions_as_parse(const std::string& text, bool comments = false, bool trailing_commas = false)
|
||||
{
|
||||
const auto j = BasicJsonType::parse(text, nullptr, true, comments, trailing_commas);
|
||||
const auto d = nlohmann::basic_json_document<BasicJsonType>::parse(text, true, comments, trailing_commas);
|
||||
const auto e = nlohmann::basic_json_document<BasicJsonType, nlohmann::view_mode::editable>::parse(text, true, comments, trailing_commas);
|
||||
return same_positions(d.root().materialize(), j) && same_positions(e.root().materialize(), j);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("json_view materialize() source positions")
|
||||
{
|
||||
SECTION("as parse()")
|
||||
{
|
||||
const std::vector<std::string> texts =
|
||||
{
|
||||
"null", "true", "false", "0", "-12", "3.25e-2", "18446744073709551615", "-9223372036854775809", R"("a")",
|
||||
R"("\n")", R"("\u00e9")", R"("\ud83d\ude00")", "\"\xf0\x9f\x98\x80\"", R"("a\"b\\")", R"("x\\")",
|
||||
" [ ] ", "{}", "[[[]]]", R"({"a":{"b":[1,{"c":[]}]}})",
|
||||
R"({"\u0041": "v\n", "k": "\"", "\t":[ "\/" ]})",
|
||||
R"([1, 2.5, true, false, null, "s", "\u0000", -0])",
|
||||
"\n\t[\r\n 1 ,\n {\n \"a\" : [ ]\n }\n]\n",
|
||||
"\xef\xbb\xbf[1, {\"a\": \"b\"}]", "\xef\xbb\xbf\"\\n\"",
|
||||
};
|
||||
for (const auto& text : texts)
|
||||
{
|
||||
CAPTURE(text)
|
||||
CHECK(positions_as_parse<json>(text));
|
||||
CHECK(positions_as_parse<ordered_json>(text));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("comments and trailing commas")
|
||||
{
|
||||
const std::vector<std::string> texts =
|
||||
{
|
||||
"[1 /* ] */]", "[1 // ]\n]", "{\"a\": 1 /* } */ , }", "[1, 2, ]", "[[1,],[2 , /**/ ] , ]",
|
||||
"{\"a\": [] // }\n, \"b\": {} /* } ] */ }", "[ /* empty */ ]", "{ // }\n}", "/* a */ [1] // b",
|
||||
R"(["\n" /* " */ , ])",
|
||||
};
|
||||
for (const auto& text : texts)
|
||||
{
|
||||
CAPTURE(text)
|
||||
CHECK(positions_as_parse<json>(text, true, true));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("generated documents")
|
||||
{
|
||||
json_view_test::generator gen;
|
||||
for (int i = 0; i < 300; ++i)
|
||||
{
|
||||
std::string text;
|
||||
gen.value(text, 0);
|
||||
CAPTURE(text)
|
||||
CHECK(positions_as_parse<json>(text));
|
||||
CHECK(positions_as_parse<ordered_json>(text));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("test data")
|
||||
{
|
||||
for (const char* name :
|
||||
{
|
||||
"/json_testsuite/sample.json", "/nativejson-benchmark/canada.json", "/nativejson-benchmark/citm_catalog.json",
|
||||
"/nativejson-benchmark/twitter.json", "/json_tests/pass1.json"
|
||||
})
|
||||
{
|
||||
CAPTURE(name)
|
||||
const std::ifstream f(std::string(TEST_DATA_DIRECTORY) + name, std::ios::binary);
|
||||
std::stringstream ss;
|
||||
ss << f.rdbuf();
|
||||
CHECK(positions_as_parse<json>(ss.str()));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("subtrees")
|
||||
{
|
||||
const std::string text = R"({"a": [1, "\n", {"b": null}], "s": "x\ty", "n": -1.5})";
|
||||
const json j = json::parse(text);
|
||||
const json_document d = json_document::parse(text);
|
||||
CHECK(same_positions(d.root()["a"].materialize(), j["a"]));
|
||||
CHECK(same_positions(d.root()["a"][2].materialize(), j["a"][2]));
|
||||
CHECK(same_positions(d.root()["a"][1].materialize(), j["a"][1]));
|
||||
CHECK(same_positions(d.root()["s"].materialize(), j["s"]));
|
||||
CHECK(same_positions(d.root()["n"].materialize(), j["n"]));
|
||||
CHECK(range(d.root()["a"][1].materialize()) == "10-14");
|
||||
CHECK(range(d.root()["s"].materialize()) == "35-41");
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("exception messages")
|
||||
{
|
||||
const std::string text = "{\n \"address\": {\"street\": \"Fake\\tStreet\", \"housenumber\": \"1\"},\n \"list\": [1, [true]]\n}";
|
||||
const json j = json::parse(text);
|
||||
const json_document d = json_document::parse(text);
|
||||
const json m = d.root().materialize();
|
||||
const std::string expected = json_view_test::exception_of_call([&]
|
||||
{
|
||||
static_cast<void>(j.at("address").at("housenumber").get<int>());
|
||||
});
|
||||
CHECK(expected.find("(bytes ") != std::string::npos);
|
||||
CHECK(json_view_test::exception_of_call([&]
|
||||
{
|
||||
static_cast<void>(m.at("address").at("housenumber").get<int>());
|
||||
}) == expected);
|
||||
CHECK(json_view_test::exception_of_call([&]
|
||||
{
|
||||
static_cast<void>(m.at("address").at("street").get<int>());
|
||||
}) == json_view_test::exception_of_call([&]
|
||||
{
|
||||
static_cast<void>(j.at("address").at("street").get<int>());
|
||||
}));
|
||||
CHECK(json_view_test::exception_of_call([&]
|
||||
{
|
||||
static_cast<void>(m.at("list").at(1).get<int>());
|
||||
}) == json_view_test::exception_of_call([&]
|
||||
{
|
||||
static_cast<void>(j.at("list").at(1).get<int>());
|
||||
}));
|
||||
}
|
||||
#endif
|
||||
|
||||
SECTION("edited documents")
|
||||
{
|
||||
const std::string text = R"({"a": [1, 2], "b": {"c": "\n", "d": true}, "e": [3, [4]], "f": "x"})";
|
||||
const json j = json::parse(text);
|
||||
|
||||
SECTION("new scalars have none")
|
||||
{
|
||||
json_editable_document d = json_editable_document::parse(text);
|
||||
d.set(d.root()["f"], 5);
|
||||
d.set(d.root()["b"]["d"], "s");
|
||||
const json m = d.root().materialize();
|
||||
CHECK(range(m["f"]) == "none");
|
||||
CHECK(range(m["b"]["d"]) == "none");
|
||||
// the object whose last member is new has none; the others keep theirs
|
||||
CHECK(range(m) == "none");
|
||||
CHECK(range(m["b"]) == "none");
|
||||
CHECK(same_positions(m["a"], j["a"]));
|
||||
CHECK(same_positions(m["e"], j["e"]));
|
||||
CHECK(same_positions(m["b"]["c"], j["b"]["c"]));
|
||||
}
|
||||
|
||||
SECTION("containers whose elements change have none")
|
||||
{
|
||||
json_editable_document d = json_editable_document::parse(text);
|
||||
d.push_back(d.root()["a"], 7);
|
||||
d.erase(d.root()["e"], 0);
|
||||
const json m = d.root().materialize();
|
||||
CHECK(range(m["a"]) == "none");
|
||||
CHECK(range(m["a"][2]) == "none");
|
||||
CHECK(range(m["e"]) == "none");
|
||||
// the elements kept keep theirs
|
||||
CHECK(range(m["a"][0]) == range(j["a"][0]));
|
||||
CHECK(range(m["a"][1]) == range(j["a"][1]));
|
||||
CHECK(same_positions(m["e"][0], j["e"][1]));
|
||||
CHECK(same_positions(m["b"], j["b"]));
|
||||
// the root's elements did not change
|
||||
CHECK(range(m) == range(j));
|
||||
}
|
||||
|
||||
SECTION("new containers have none")
|
||||
{
|
||||
json_editable_document d = json_editable_document::parse(text);
|
||||
d.set(d.root()["b"], json::parse(R"({"x": [1]})"));
|
||||
const json m = d.root().materialize();
|
||||
CHECK(range(m["b"]) == "none");
|
||||
CHECK(range(m["b"]["x"]) == "none");
|
||||
CHECK(range(m["b"]["x"][0]) == "none");
|
||||
CHECK(range(m) == range(j));
|
||||
}
|
||||
|
||||
SECTION("copies of scalars of the document keep theirs")
|
||||
{
|
||||
// (a copy of an array or object is a new one; its scalars are the
|
||||
// parsed ones)
|
||||
json_editable_document d = json_editable_document::parse(text);
|
||||
d.set(d.root()["f"], d.root()["e"]);
|
||||
d.set(d.root()["a"], d.root()["b"]["c"]);
|
||||
const json m = d.root().materialize();
|
||||
CHECK(range(m["a"]) == range(j["b"]["c"]));
|
||||
CHECK(range(m["f"]) == "none");
|
||||
CHECK(range(m["f"][1]) == "none");
|
||||
CHECK(range(m["f"][0]) == range(j["e"][0]));
|
||||
CHECK(range(m["f"][1][0]) == range(j["e"][1][0]));
|
||||
}
|
||||
}
|
||||
|
||||
#if !(defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
|
||||
SECTION("images")
|
||||
{
|
||||
const std::string text = R"({"a\n": ["\u00e9", 1.5, {"b": [true, null]}], "c": "d"})";
|
||||
const json j = json::parse(text);
|
||||
|
||||
SECTION("round trip")
|
||||
{
|
||||
const json_document d = json_document::load(json_document::parse(text).save());
|
||||
CHECK(same_positions(d.root().materialize(), j));
|
||||
}
|
||||
|
||||
SECTION("edited")
|
||||
{
|
||||
json_editable_document e = json_editable_document::parse(text);
|
||||
e.set(e.root()["c"], 1);
|
||||
e.push_back(e.root()[std::string("a") + '\n'], false);
|
||||
e.set(e.root()[std::string("a") + '\n'][2]["b"][0], "\"");
|
||||
const json_document d = json_document::load(e.save());
|
||||
const json m = d.root().materialize();
|
||||
CHECK(range(m["c"]) == "none");
|
||||
CHECK(range(m) == "none");
|
||||
const json& a = m[std::string("a") + '\n'];
|
||||
const json& ja = j[std::string("a") + '\n'];
|
||||
CHECK(range(a) == "none");
|
||||
CHECK(range(a[3]) == "none");
|
||||
CHECK(range(a[0]) == range(ja[0]));
|
||||
CHECK(range(a[1]) == range(ja[1]));
|
||||
CHECK(range(a[2]["b"][0]) == "none");
|
||||
CHECK(range(a[2]["b"][1]) == range(ja[2]["b"][1]));
|
||||
// the last element of b is still the parsed one
|
||||
CHECK(range(a[2]["b"]) == range(ja[2]["b"]));
|
||||
CHECK(range(a[2]) == range(ja[2]));
|
||||
}
|
||||
|
||||
SECTION("without positions")
|
||||
{
|
||||
// an image saved without JSON_DIAGNOSTIC_POSITIONS: next is 0 for
|
||||
// the strings with escapes
|
||||
std::vector<std::uint8_t> image = json_document::parse(text).save();
|
||||
std::uint64_t count = 0;
|
||||
std::memcpy(&count, image.data() + 8, sizeof(count));
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
nlohmann::detail::view::node n{};
|
||||
std::memcpy(&n, image.data() + 64 + (i * sizeof(n)), sizeof(n));
|
||||
if (n.kind == nlohmann::detail::view::node_kind::string)
|
||||
{
|
||||
n.next = 0;
|
||||
}
|
||||
std::memcpy(image.data() + 64 + (i * sizeof(n)), &n, sizeof(n));
|
||||
}
|
||||
const json_document d = json_document::load(image);
|
||||
const json m = d.root().materialize();
|
||||
CHECK(range(m[std::string("a") + '\n'][0]) == "none");
|
||||
CHECK(range(m["c"]) == range(j["c"]));
|
||||
CHECK(same_positions(m[std::string("a") + '\n'][2], j[std::string("a") + '\n'][2]));
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("a decoded string's position must be within the text")
|
||||
{
|
||||
std::vector<std::uint8_t> image = json_document::parse(R"(["\n"])").save();
|
||||
nlohmann::detail::view::node n{};
|
||||
std::memcpy(&n, image.data() + 64 + sizeof(n), sizeof(n));
|
||||
REQUIRE(n.next == 2);
|
||||
n.next = 7;
|
||||
std::memcpy(image.data() + 64 + sizeof(n), &n, sizeof(n));
|
||||
const auto load_image = [&image]
|
||||
{
|
||||
// GCC ignores a cast to void for [[nodiscard]]-like results, so keep the document
|
||||
const json_document d = json_document::load(image);
|
||||
static_cast<void>(d);
|
||||
};
|
||||
CHECK_THROWS_AS(load_image(), json::parse_error&);
|
||||
// without the check, the position is out of bounds and ignored
|
||||
const json_document d = json_document::load(image, json_document::image_check::none);
|
||||
CHECK(range(d.root()[0].materialize()) == "none");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
# ci_jobs
|
||||
|
||||
The jobs of the compiler workflows [`ubuntu.yml`](../../.github/workflows/ubuntu.yml),
|
||||
[`macos.yml`](../../.github/workflows/macos.yml), and [`windows.yml`](../../.github/workflows/windows.yml) are split into
|
||||
three sets, chosen by each job's
|
||||
The compiler jobs of [`ubuntu.yml`](../../.github/workflows/ubuntu.yml) are split into three sets, chosen by each job's
|
||||
`if:` condition (see [How CI runs](https://json.nlohmann.me/community/quality_assurance/#how-ci-runs)):
|
||||
|
||||
- **PR set**: every push to a pull request, and every push to `develop`, `master`, and `release/*`;
|
||||
@@ -14,9 +12,8 @@ A matrix whose entries belong to different sets is split into one job per set: t
|
||||
|
||||
[`ci_jobs.py`](ci_jobs.py) checks that
|
||||
|
||||
- every job has the `if:` condition of exactly one set (the conditions are in `SETS`),
|
||||
- every full-set job waits for (`needs:`) exactly the PR set of its workflow, and `nightly_report` for exactly the
|
||||
nightly set, and
|
||||
- every job of `ubuntu.yml` has the `if:` condition of exactly one set (the conditions are in `SETS`),
|
||||
- every full-set job waits for (`needs:`) exactly the PR set, and `nightly_report` for exactly the nightly set, and
|
||||
- the job table in [`quality_assurance.md`](../../docs/mkdocs/docs/community/quality_assurance.md) is up to date.
|
||||
|
||||
```shell
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""List and check the jobs of the compiler workflows and the set each job belongs to.
|
||||
|
||||
The jobs of the compiler workflows belong to one of three sets, chosen by the
|
||||
job's `if:` condition:
|
||||
The jobs of a tiered workflow (ubuntu.yml) belong to one of three sets, chosen
|
||||
by the job's `if:` condition:
|
||||
|
||||
- PR set: every pull request push and every push,
|
||||
- full set: after the PR set passed, on pushes and on pull requests with the
|
||||
@@ -10,9 +10,11 @@ job's `if:` condition:
|
||||
- nightly set: nightly on develop, on pushes to master and release/*, and when
|
||||
started by hand.
|
||||
|
||||
The script checks that every job belongs to exactly one set, that the full set
|
||||
of a workflow waits for its whole PR set, that the nightly report waits for the
|
||||
whole nightly set, and that the job table in
|
||||
The jobs of the other workflows run on every push and pull request.
|
||||
|
||||
The script checks that every job of a tiered workflow belongs to exactly one
|
||||
set, that the full set waits for the whole PR set, that the nightly report
|
||||
waits for the whole nightly set, and that the job table in
|
||||
docs/mkdocs/docs/community/quality_assurance.md is up to date.
|
||||
|
||||
--markdown print the job table for the documentation
|
||||
@@ -35,7 +37,7 @@ DOCS = 'docs/mkdocs/docs/community/quality_assurance.md'
|
||||
BEGIN, END = '<!-- ci_jobs: begin -->', '<!-- ci_jobs: end -->'
|
||||
|
||||
WORKFLOWS = ['.github/workflows/ubuntu.yml', '.github/workflows/macos.yml', '.github/workflows/windows.yml']
|
||||
TIERED = WORKFLOWS
|
||||
TIERED = ['.github/workflows/ubuntu.yml']
|
||||
# --compare also counts the jobs of these workflows, so a job moved between workflows is not a removed one
|
||||
COMPARED = WORKFLOWS + ['.github/workflows/repository_checks.yml', '.github/workflows/check_amalgamation.yml']
|
||||
|
||||
@@ -78,10 +80,7 @@ def expand(matrix):
|
||||
if not matrix:
|
||||
return [{}]
|
||||
keys = [k for k in matrix if k not in ('include', 'exclude')]
|
||||
if not keys:
|
||||
# a matrix of only include entries runs each entry
|
||||
return [dict(inc) for inc in matrix.get('include') or []]
|
||||
combos = [dict(zip(keys, values)) for values in itertools.product(*(matrix[k] for k in keys))]
|
||||
combos = [dict(zip(keys, values)) for values in itertools.product(*(matrix[k] for k in keys))] if keys else []
|
||||
for ex in matrix.get('exclude') or []:
|
||||
combos = [c for c in combos if any(c.get(k) != v for k, v in ex.items())]
|
||||
for inc in matrix.get('include') or []:
|
||||
|
||||
Reference in new issue
Block a user