diff --git a/.github/workflows/check_amalgamation.yml b/.github/workflows/check_amalgamation.yml index b3f2bb023..60a3f8240 100644 --- a/.github/workflows/check_amalgamation.yml +++ b/.github/workflows/check_amalgamation.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -34,7 +34,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index a14be2111..da3df626f 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Harden Runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index be556daad..c21a3cd24 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -38,14 +38,14 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 + uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 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@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 + uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 + uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 diff --git a/.github/workflows/comment_check_amalgamation.yml b/.github/workflows/comment_check_amalgamation.yml index 9dada803c..87e1b7b58 100644 --- a/.github/workflows/comment_check_amalgamation.yml +++ b/.github/workflows/comment_check_amalgamation.yml @@ -19,7 +19,7 @@ jobs: pull-requests: write steps: - name: Harden Runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 892e5d429..2c6300252 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit diff --git a/.github/workflows/flawfinder.yml b/.github/workflows/flawfinder.yml index 18e172c62..5477195d8 100644 --- a/.github/workflows/flawfinder.yml +++ b/.github/workflows/flawfinder.yml @@ -27,7 +27,7 @@ jobs: security-events: write steps: - name: Harden Runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -43,6 +43,6 @@ jobs: output: 'flawfinder_results.sarif' - name: Upload analysis results to GitHub Security tab - uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 + uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: sarif_file: ${{github.workspace}}/flawfinder_results.sarif diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index c8382168f..2222b77f2 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit diff --git a/.github/workflows/publish_documentation.yml b/.github/workflows/publish_documentation.yml index e717d15c3..d0066885e 100644 --- a/.github/workflows/publish_documentation.yml +++ b/.github/workflows/publish_documentation.yml @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Harden Runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 728049618..63ca0b90d 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -76,6 +76,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 + uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: sarif_file: results.sarif diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 07ca19276..812e4a0d7 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -32,7 +32,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -61,7 +61,7 @@ jobs: # Upload SARIF file generated in previous step - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 + uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: sarif_file: semgrep.sarif if: always() diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 1dbf6a571..fd9cbb80f 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 798885a8c..2ef56a80b 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -25,7 +25,7 @@ jobs: with: persist-credentials: false - name: Get latest CMake and ninja - uses: lukka/get-cmake@e6906078ebd1ccb8ce51ab4626ac46a1b5a517e3 # v4.4.0 + uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 - name: Run CMake run: cmake -S . -B build -DJSON_CI=On - name: Build @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -47,7 +47,7 @@ jobs: with: persist-credentials: false - name: Get latest CMake and ninja - uses: lukka/get-cmake@e6906078ebd1ccb8ce51ab4626ac46a1b5a517e3 # v4.4.0 + uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 - name: Run CMake run: cmake -S . -B build -DJSON_CI=On - name: Build @@ -60,7 +60,7 @@ jobs: target: [ci_test_amalgamation, ci_test_single_header, ci_cppcheck, ci_cpplint, ci_reproducible_tests, ci_non_git_tests, ci_offline_testdata, ci_reuse_compliance, ci_test_valgrind] steps: - name: Harden Runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -70,7 +70,7 @@ jobs: with: persist-credentials: false - name: Get latest CMake and ninja - uses: lukka/get-cmake@e6906078ebd1ccb8ce51ab4626ac46a1b5a517e3 # v4.4.0 + uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 - name: Run CMake run: cmake -S . -B build -DJSON_CI=On - name: Build @@ -89,7 +89,7 @@ jobs: with: persist-credentials: false - name: Get latest CMake and ninja - uses: lukka/get-cmake@e6906078ebd1ccb8ce51ab4626ac46a1b5a517e3 # v4.4.0 + uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 - name: Run CMake run: cmake -S . -B build -DJSON_CI=On - name: Build @@ -108,7 +108,7 @@ jobs: with: persist-credentials: false - name: Get latest CMake and ninja - uses: lukka/get-cmake@e6906078ebd1ccb8ce51ab4626ac46a1b5a517e3 # v4.4.0 + uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 - name: Run CMake run: cmake -S . -B build -DJSON_CI=On - name: Build @@ -118,7 +118,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -184,7 +184,7 @@ jobs: with: persist-credentials: false - name: Get latest CMake and ninja - uses: lukka/get-cmake@e6906078ebd1ccb8ce51ab4626ac46a1b5a517e3 # v4.4.0 + uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 - name: Run CMake run: CXX=g++-${{ matrix.compiler }} cmake -S . -B build -DJSON_CI=On - name: Build @@ -202,7 +202,7 @@ jobs: with: persist-credentials: false - name: Get latest CMake and ninja - uses: lukka/get-cmake@e6906078ebd1ccb8ce51ab4626ac46a1b5a517e3 # v4.4.0 + uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 - name: Run CMake run: cmake -S . -B build -DJSON_CI=On - name: Build @@ -212,14 +212,14 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - compiler: ['3.4', '3.5', '3.6', '3.7', '3.8', '3.9', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15-bullseye', '16', '17', '18', '19', '20', 'latest'] + compiler: ['3.4', '3.5', '3.6', '3.7', '3.8', '3.9', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15-bullseye', '16', '17', '18', '19', '20', '21', '22', 'latest'] container: silkeh/clang:${{ matrix.compiler }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Get latest CMake and ninja - uses: lukka/get-cmake@e6906078ebd1ccb8ce51ab4626ac46a1b5a517e3 # v4.4.0 + uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 - name: Set env FORCE_STDCPPFS_FLAG for clang 7 / 8 / 9 / 10 run: echo "JSON_FORCED_GLOBAL_COMPILE_OPTIONS=-DJSON_HAS_FILESYSTEM=0;-DJSON_HAS_EXPERIMENTAL_FILESYSTEM=0" >> "$GITHUB_ENV" if: ${{ matrix.compiler == '7' || matrix.compiler == '8' || matrix.compiler == '9' || matrix.compiler == '10' }} @@ -239,7 +239,7 @@ jobs: with: persist-credentials: false - name: Get latest CMake and ninja - uses: lukka/get-cmake@e6906078ebd1ccb8ce51ab4626ac46a1b5a517e3 # v4.4.0 + uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 - name: Run CMake run: cmake -S . -B build -DJSON_CI=On - name: Build @@ -259,7 +259,7 @@ jobs: with: persist-credentials: false - name: Get latest CMake and ninja - uses: lukka/get-cmake@e6906078ebd1ccb8ce51ab4626ac46a1b5a517e3 # v4.4.0 + uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 - name: Run CMake run: cmake -S . -B build -DJSON_CI=On - name: Build with libc++ @@ -286,7 +286,7 @@ jobs: with: persist-credentials: false - name: Get latest CMake and ninja - uses: lukka/get-cmake@e6906078ebd1ccb8ce51ab4626ac46a1b5a517e3 # v4.4.0 + uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 - name: Run CMake run: cmake -S . -B build -DJSON_CI=On - name: Build @@ -306,7 +306,7 @@ jobs: # import-std support. Its opt-in token is CMake-version-specific, so pin # CMake to the version whose token is set in tests/module_cpp20/CMakeLists.txt. - name: Get pinned CMake and ninja - uses: lukka/get-cmake@e6906078ebd1ccb8ce51ab4626ac46a1b5a517e3 # v4.4.0 + uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 with: cmakeVersion: 4.3.4 # Clang: the std library module is provided by libc++ (the image's libstdc++ @@ -332,7 +332,7 @@ jobs: with: persist-credentials: false - name: Get latest CMake and ninja - uses: lukka/get-cmake@e6906078ebd1ccb8ce51ab4626ac46a1b5a517e3 # v4.4.0 + uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 - name: Run CMake run: cmake -S . -B build -DJSON_CI=On - name: Build @@ -347,7 +347,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Get latest CMake and ninja - uses: lukka/get-cmake@e6906078ebd1ccb8ce51ab4626ac46a1b5a517e3 # v4.4.0 + uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 - name: Run CMake run: cmake -S . -B build -DJSON_CI=On - name: Build @@ -359,7 +359,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Get latest CMake and ninja - uses: lukka/get-cmake@e6906078ebd1ccb8ce51ab4626ac46a1b5a517e3 # v4.4.0 + uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 - name: Run CMake run: cmake -S . -B build -DJSON_CI=On - name: Build @@ -369,7 +369,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -379,7 +379,7 @@ jobs: with: persist-credentials: false - name: Get latest CMake and ninja - uses: lukka/get-cmake@e6906078ebd1ccb8ce51ab4626ac46a1b5a517e3 # v4.4.0 + uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 - name: Run CMake run: cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake -GNinja - name: Build @@ -392,7 +392,7 @@ jobs: target: [ci_test_examples, ci_test_build_documentation] steps: - name: Harden Runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index b7c640ce8..068c5a0f1 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -88,7 +88,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Get latest CMake and ninja - uses: lukka/get-cmake@e6906078ebd1ccb8ce51ab4626ac46a1b5a517e3 # v4.4.0 + uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 - name: Set extra CXX_FLAGS for latest std_version # /wd5285 silences C5285 emitted by the bundled third-party doctest.h, which # specializes std::tuple (newly diagnosed by the VS2026 v145 toolset) @@ -153,10 +153,16 @@ jobs: with: platform: x64 version: 12.2.0 # https://github.com/egor-tensin/setup-mingw/issues/14 + # CMAKE_CXX_FLAGS_DEBUG is overridden to drop the default -g: linking + # test-regression2_cpp20 intermittently fails with "relocation truncated + # to fit: IMAGE_REL_AMD64_SECREL against `.debug_line'" because the + # MinGW linker cannot relocate the debug sections this test produces. + # The tests are only built and run here, so the debug info is not used. - name: Run CMake run: cmake -S . -B build ^ -DCMAKE_CXX_COMPILER="C:/Program Files/LLVM/bin/clang++.exe" ^ -DCMAKE_CXX_FLAGS="--target=x86_64-w64-mingw32 -stdlib=libstdc++ -pthread" ^ + -DCMAKE_CXX_FLAGS_DEBUG="-g0" ^ -DCMAKE_EXE_LINKER_FLAGS="-lwinpthread" ^ -G"MinGW Makefiles" ^ -DCMAKE_BUILD_TYPE=Debug ^ @@ -193,7 +199,7 @@ jobs: # import-std support. Its opt-in token is CMake-version-specific, so pin # CMake to the version whose token is set in tests/module_cpp20/CMakeLists.txt. - name: Get pinned CMake and ninja - uses: lukka/get-cmake@e6906078ebd1ccb8ce51ab4626ac46a1b5a517e3 # v4.4.0 + uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 with: cmakeVersion: 4.3.4 - name: Run CMake (Debug) diff --git a/README.md b/README.md index d4d2394a2..4b4236bb6 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ - [Specializing enum conversion](#specializing-enum-conversion) - [Binary formats (BSON, CBOR, MessagePack, UBJSON, and BJData)](#binary-formats-bson-cbor-messagepack-ubjson-and-bjdata) - [Customers](#customers) +- [Ecosystem](#ecosystem) - [Supported compilers](#supported-compilers) - [Integration](#integration) - [CMake](#cmake) @@ -1186,6 +1187,11 @@ The library is used in multiple projects, applications, operating systems, etc. [![logos of customers using the library](docs/mkdocs/docs/images/customers.png)](https://json.nlohmann.me/home/customers/) +## Ecosystem + +Beyond projects that use the library, there are third-party projects that build on top of it - schema validators, +language bindings, format converters, and the like. See the curated [Ecosystem](https://json.nlohmann.me/community/ecosystem/) page. + ## Supported compilers Though it's 2026 already, the support for C++11 is still a bit sparse. Currently, the following compilers are known to work: diff --git a/docs/mkdocs/docs/community/ecosystem.md b/docs/mkdocs/docs/community/ecosystem.md new file mode 100644 index 000000000..801111e7d --- /dev/null +++ b/docs/mkdocs/docs/community/ecosystem.md @@ -0,0 +1,40 @@ +# Ecosystem + +The projects below build on top of `nlohmann::json` rather than merely using it - schema validators, language +bindings, format converters, and similar building blocks. The list is not exhaustive, and is curated rather than +automatically generated. If you maintain or know of a project that belongs here, +[please let me know](mailto:mail@nlohmann.me). + +For products, applications, and organizations that use the library, see [Customers](../home/customers.md) instead. + +## Schema validation + +- [**json-schema-validator**](https://github.com/pboettch/json-schema-validator), a JSON Schema (draft 7) validator + with human-readable error messages + +## Serialization and reflection + +- [**nlohmann_json_reflect**](https://github.com/1261385937/nlohmann_json_reflect), a reflection extension for + (de)serializing nested containers-in-structs-in-containers + +## Encodings + +- [**base-encode-decode**](https://github.com/saxonnicholls/base-encode-decode), a header-only Base64/32/16/8/4/2 + (and DNA/RNA) encoding library, with an adapter that serializes binary data through `nlohmann::json` + +## Language bindings and interop + +- [**pybind11_json**](https://github.com/pybind/pybind11_json), a bidirectional type caster between + `nlohmann::json` and Python objects for [pybind11](https://github.com/pybind/pybind11) bindings +- [**nanobind_json**](https://github.com/ianhbell/nanobind_json), the same idea for + [nanobind](https://github.com/wjakob/nanobind) bindings +- [**nlohmann_json_qt**](https://github.com/dpurgin/nlohmann_json_qt), deserialization helpers for Qt types + (`QString`, `QUrl`, `QDateTime`, `QVector`, ...) from `nlohmann::json` +- [**vulkan2json**](https://github.com/Fadis/vulkan2json), serialization and deserialization of Vulkan API structs + +## Format converters + +- [**tojson**](https://github.com/mircodz/tojson), a header-only converter between YAML/XML documents and + `nlohmann::json` +- [**json2xml**](https://github.com/testillano/json2xml), a header-only converter from `nlohmann::json` to XML for + simple configuration documents diff --git a/docs/mkdocs/docs/community/index.md b/docs/mkdocs/docs/community/index.md index caef17be3..50baeab25 100644 --- a/docs/mkdocs/docs/community/index.md +++ b/docs/mkdocs/docs/community/index.md @@ -1,5 +1,6 @@ # Community +- [Ecosystem](ecosystem.md) - third-party projects built on top of this library - [Code of Conduct](code_of_conduct.md) - the rules and norms of this project - [Contribution Guidelines](contribution_guidelines.md) - guidelines how to contribute to this project - [Governance](governance.md) - the governance model of this project diff --git a/docs/mkdocs/docs/community/quality_assurance.md b/docs/mkdocs/docs/community/quality_assurance.md index 200a129ef..4196f3532 100644 --- a/docs/mkdocs/docs/community/quality_assurance.md +++ b/docs/mkdocs/docs/community/quality_assurance.md @@ -66,6 +66,7 @@ Note: Some modern features (like C++20 ranges or filesystem support) may be disa | Clang 20.1.1 | x86_64 | Ubuntu 22.04.1 LTS | GitHub | | Clang 20.1.8 with GNU-like command-line | x86_64 | Windows Server 2022 (Build 20348) | GitHub | | Clang 21.1.8 | x86_64 | Ubuntu 22.04.1 LTS | GitHub | + | Clang 22.1.8 | x86_64 | Ubuntu 22.04.1 LTS | GitHub | | CUDA 11.8.0 (nvcc) | x86_64 | Ubuntu 22.04 LTS | GitHub | | CUDA 12.1.1 (nvcc) | x86_64 | Ubuntu 22.04 LTS | GitHub | | CUDA 12.6.3 (nvcc) | x86_64 | Ubuntu 22.04 LTS | GitHub | diff --git a/docs/mkdocs/docs/features/binary_formats/bjdata.md b/docs/mkdocs/docs/features/binary_formats/bjdata.md index b1b98dfe2..faff64f16 100644 --- a/docs/mkdocs/docs/features/binary_formats/bjdata.md +++ b/docs/mkdocs/docs/features/binary_formats/bjdata.md @@ -116,9 +116,19 @@ The library uses the following mapping from JSON values types to BJData types ac ``` Likewise, when a JSON object in the above form is serialized using - [`to_bjdata`](../../api/basic_json/to_bjdata.md), it is automatically converted into a compact BJData ND-array. The - only exception is, that when the 1-dimensional vector stored in `"_ArraySize_"` contains a single integer or two - integers with one being 1, a regular 1-D optimized array is generated. + [`to_bjdata`](../../api/basic_json/to_bjdata.md), it is automatically converted into a compact BJData ND-array. When + the 1-dimensional vector stored in `"_ArraySize_"` contains a single integer or two integers with one being 1, a + regular 1-D optimized array is generated instead. + + An object is only converted if the annotation actually describes a packed array; otherwise it is serialized as a + regular JSON object. This requires all of the following: + + - `"_ArrayType_"` is one of `uint8`, `int8`, `uint16`, `int16`, `uint32`, `int32`, `uint64`, `int64`, `single`, + `double`, `char`, or `byte`, + - every entry of `"_ArraySize_"` is a non-negative integer, and their product is representable as a `std::size_t`, + - `"_ArrayData_"` holds exactly that many elements, and + - every element of `"_ArrayData_"` is a number of the kind named by `"_ArrayType_"` (a floating-point number for + `single` and `double`, an integer otherwise). The current version of this library does not yet support automatic detection of and conversion from a nested JSON array input to a BJData ND-array. diff --git a/docs/mkdocs/docs/features/binary_formats/cbor.md b/docs/mkdocs/docs/features/binary_formats/cbor.md index 5952bce39..670a23455 100644 --- a/docs/mkdocs/docs/features/binary_formats/cbor.md +++ b/docs/mkdocs/docs/features/binary_formats/cbor.md @@ -160,14 +160,11 @@ The library maps CBOR types to JSON value types as follows: The mapping is **incomplete** in the sense that not all CBOR types can be converted to a JSON value. The following CBOR types are not supported and will yield parse errors: - - date/time (0xC0..0xC1) - - bignum (0xC2..0xC3) - - decimal fraction (0xC4) - - bigfloat (0xC5) - - expected conversions (0xD5..0xD7) - simple values (0xE0..0xF3, 0xF8) - undefined (0xF7) + Tagged items (0xC0..0xDB) are not interpreted either; see the note on tagged items below. + !!! warning "Negative integer overflow" CBOR negative integers (major type 1) are decoded as `-1 - n`. If the encoded magnitude `n` is too large for the @@ -181,7 +178,7 @@ The library maps CBOR types to JSON value types as follows: !!! warning "Tagged items" - Tagged items will throw a parse error by default. They can be ignored by passing `cbor_tag_handler_t::ignore` to function `from_cbor`. They can be stored by passing `cbor_tag_handler_t::store` to function `from_cbor`. + Tagged items (0xC0..0xDB) will throw a parse error by default. They can be ignored by passing `cbor_tag_handler_t::ignore` to function `from_cbor`, in which case the tag is skipped and the enclosed data item is parsed on its own. They can be stored by passing `cbor_tag_handler_t::store` to function `from_cbor`. Note that no tag is ever interpreted: for instance, a text string tagged with tag 0 (date/time) stays a string. ??? example diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml index fe786f7d5..2e1337f47 100644 --- a/docs/mkdocs/mkdocs.yml +++ b/docs/mkdocs/mkdocs.yml @@ -308,6 +308,7 @@ nav: - 'NLOHMANN_JSON_VERSION_MAJOR, NLOHMANN_JSON_VERSION_MINOR, NLOHMANN_JSON_VERSION_PATCH': api/macros/nlohmann_json_version_major.md - Community: - community/index.md + - community/ecosystem.md - "Code of Conduct": community/code_of_conduct.md - community/contribution_guidelines.md - community/quality_assurance.md diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index 397683744..557d7669c 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -465,15 +465,6 @@ class binary_reader // CBOR // ////////// - /*! - @param[in] get_char whether a new character should be retrieved from the - input (true) or whether the last read character should - be considered instead (false) - @param[in] tag_handler how CBOR tags should be treated - - @return whether a valid CBOR value was passed to the SAX parser - */ - template bool get_cbor_negative_integer() { @@ -492,6 +483,14 @@ class binary_reader return sax->number_integer(static_cast(-1) - static_cast(number)); } + /*! + @param[in] get_char whether a new character should be retrieved from the + input (true) or whether the last read character should + be considered instead (false) + @param[in] tag_handler how CBOR tags should be treated + + @return whether a valid CBOR value was passed to the SAX parser + */ bool parse_cbor_internal(const bool get_char, const cbor_tag_handler_t tag_handler) { @@ -774,7 +773,13 @@ class binary_reader case 0xBF: // map (indefinite length) return get_cbor_object(detail::unknown_size(), tag_handler); - case 0xC6: // tagged item + case 0xC0: // tagged item + case 0xC1: + case 0xC2: + case 0xC3: + case 0xC4: + case 0xC5: + case 0xC6: case 0xC7: case 0xC8: case 0xC9: @@ -789,6 +794,9 @@ class binary_reader case 0xD2: case 0xD3: case 0xD4: + case 0xD5: + case 0xD6: + case 0xD7: case 0xD8: // tagged item (1 byte follows) case 0xD9: // tagged item (2 bytes follow) case 0xDA: // tagged item (4 bytes follow) @@ -1988,7 +1996,11 @@ class binary_reader { if (get_char) { - get(); // TODO(niels): may we ignore N here? + // no get_ignore_noop() here: the byte read next must be a string + // length type specification, and a no-op ('N') is not valid in + // that position. No-ops at positions where a value may appear are + // already consumed by the callers via get_ignore_noop(). + get(); } if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format, "value"))) diff --git a/include/nlohmann/detail/input/input_adapters.hpp b/include/nlohmann/detail/input/input_adapters.hpp index 5c6136633..05d27f256 100644 --- a/include/nlohmann/detail/input/input_adapters.hpp +++ b/include/nlohmann/detail/input/input_adapters.hpp @@ -393,8 +393,12 @@ struct wide_string_input_helper } else { - // unknown character - utf8_bytes[0] = static_cast::int_type>(wc); + // A code point above U+10FFFF has no UTF-8 encoding. Passing the + // unit through would narrow it to int, where 0xFFFFFFFF becomes + // char_traits::eof() and would end the input silently, so + // emit a byte that is never valid UTF-8 and let the decoder + // reject it. + utf8_bytes[0] = 0xFF; utf8_bytes_filled = 1; } } diff --git a/include/nlohmann/detail/input/json_sax.hpp b/include/nlohmann/detail/input/json_sax.hpp index 4c5f24a7b..8a98ee728 100644 --- a/include/nlohmann/detail/input/json_sax.hpp +++ b/include/nlohmann/detail/input/json_sax.hpp @@ -370,8 +370,10 @@ class json_sax_dom_parser case value_t::string: { - // include the length of the quotes, which is 2 - v.start_position = v.end_position - v.m_data.m_value.string->size() - 2; + // escape sequences make the token longer than the value it + // parses to, so the start position cannot be derived from + // the value; use the offset the lexer recorded instead + v.start_position = m_lexer_ref->get_token_start_position(); break; } @@ -769,8 +771,10 @@ class json_sax_dom_callback_parser case value_t::string: { - // include the length of the quotes, which is 2 - v.start_position = v.end_position - v.m_data.m_value.string->size() - 2; + // escape sequences make the token longer than the value it + // parses to, so the start position cannot be derived from + // the value; use the offset the lexer recorded instead + v.start_position = m_lexer_ref->get_token_start_position(); break; } diff --git a/include/nlohmann/detail/input/lexer.hpp b/include/nlohmann/detail/input/lexer.hpp index fe57a83c8..ff00facb4 100644 --- a/include/nlohmann/detail/input/lexer.hpp +++ b/include/nlohmann/detail/input/lexer.hpp @@ -1381,6 +1381,11 @@ scan_number_done: token_buffer.clear(); decimal_point_position = std::string::npos; +#if JSON_DIAGNOSTIC_POSITIONS + // the first character of the token has already been read, hence the -1 + token_start_position = position.chars_read_total - 1; +#endif + note_token_start(std::integral_constant {}); } @@ -1581,6 +1586,15 @@ scan_number_done: release_lookahead_impl(std::integral_constant {}); } +#if JSON_DIAGNOSTIC_POSITIONS + /// return the offset of the first character of the last read token; unlike + /// the token's parsed value, this accounts for escape sequences + constexpr std::size_t get_token_start_position() const noexcept + { + return token_start_position; + } +#endif + /// seekable adapter: rebuild the last read token from the input on demand const std::vector& collect_token_chars(std::vector& out, std::true_type /*lazy*/) const { @@ -1781,6 +1795,12 @@ scan_number_done: /// the last read token on error for seekable adapters (see collect_token_chars) std::size_t token_string_start = 0; +#if JSON_DIAGNOSTIC_POSITIONS + /// start offset of the current token within the input, used to report + /// diagnostic positions (see reset()) + std::size_t token_start_position = 0; +#endif + /// buffer for variable-length tokens (numbers, strings) string_t token_buffer {}; diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index 9a8b53df7..35b5efa8a 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -1670,7 +1670,23 @@ class binary_writer { return true; } - len *= static_cast(el.template get()); + + // a dimension that does not fit into std::size_t, or a product that + // overflows it, would wrap around and could match the size of + // _ArrayData_ by accident; the resulting header announces an + // element count that no reader can honor (the binary reader rejects + // it with out_of_range.408), so encode as a plain object instead + const auto dim = el.template get(); + if (!value_in_range_of(dim)) + { + return true; + } + const auto dim_size = static_cast(dim); + if (dim_size != 0 && len > (std::numeric_limits::max)() / dim_size) + { + return true; + } + len *= dim_size; } key = "_ArrayData_"; diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index a460bb29f..235e6b737 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -3652,6 +3652,12 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // note parentheses around operands are necessary; see // https://github.com/nlohmann/json/issues/1530 + // Mixed signed/unsigned integer comparisons check whether the signed value + // is negative before casting. If it is, the comparison is performed with + // the fixed values -1 and 1, which preserves the ordering relationship + // because any negative signed value is smaller than any unsigned value. + // Otherwise, the non-negative signed value is cast to unsigned before the + // comparison to avoid wraparound. #define JSON_IMPLEMENT_OPERATOR(op, null_result, unordered_result, default_result) \ const auto lhs_type = lhs.type(); \ const auto rhs_type = rhs.type(); \ @@ -3710,12 +3716,16 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } \ else if (lhs_type == value_t::number_unsigned && rhs_type == value_t::number_integer) \ { \ - return static_cast(lhs.m_data.m_value.number_unsigned) op rhs.m_data.m_value.number_integer; \ + return (rhs.m_data.m_value.number_integer < 0) \ + ? (number_integer_t(1) op number_integer_t(-1)) \ + : (lhs.m_data.m_value.number_unsigned op static_cast(rhs.m_data.m_value.number_integer)); \ } \ else if (lhs_type == value_t::number_integer && rhs_type == value_t::number_unsigned) \ { \ - return lhs.m_data.m_value.number_integer op static_cast(rhs.m_data.m_value.number_unsigned); \ - } \ + return (lhs.m_data.m_value.number_integer < 0) \ + ? (number_integer_t(-1) op number_integer_t(1)) \ + : (static_cast(lhs.m_data.m_value.number_integer) op rhs.m_data.m_value.number_unsigned); \ + } \ else if(compares_unordered(lhs, rhs))\ {\ return (unordered_result);\ diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index b149b3123..8b7d94f69 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -7396,8 +7396,12 @@ struct wide_string_input_helper } else { - // unknown character - utf8_bytes[0] = static_cast::int_type>(wc); + // A code point above U+10FFFF has no UTF-8 encoding. Passing the + // unit through would narrow it to int, where 0xFFFFFFFF becomes + // char_traits::eof() and would end the input silently, so + // emit a byte that is never valid UTF-8 and let the decoder + // reject it. + utf8_bytes[0] = 0xFF; utf8_bytes_filled = 1; } } @@ -9143,6 +9147,11 @@ scan_number_done: token_buffer.clear(); decimal_point_position = std::string::npos; +#if JSON_DIAGNOSTIC_POSITIONS + // the first character of the token has already been read, hence the -1 + token_start_position = position.chars_read_total - 1; +#endif + note_token_start(std::integral_constant {}); } @@ -9343,6 +9352,15 @@ scan_number_done: release_lookahead_impl(std::integral_constant {}); } +#if JSON_DIAGNOSTIC_POSITIONS + /// return the offset of the first character of the last read token; unlike + /// the token's parsed value, this accounts for escape sequences + constexpr std::size_t get_token_start_position() const noexcept + { + return token_start_position; + } +#endif + /// seekable adapter: rebuild the last read token from the input on demand const std::vector& collect_token_chars(std::vector& out, std::true_type /*lazy*/) const { @@ -9543,6 +9561,12 @@ scan_number_done: /// the last read token on error for seekable adapters (see collect_token_chars) std::size_t token_string_start = 0; +#if JSON_DIAGNOSTIC_POSITIONS + /// start offset of the current token within the input, used to report + /// diagnostic positions (see reset()) + std::size_t token_start_position = 0; +#endif + /// buffer for variable-length tokens (numbers, strings) string_t token_buffer {}; @@ -9919,8 +9943,10 @@ class json_sax_dom_parser case value_t::string: { - // include the length of the quotes, which is 2 - v.start_position = v.end_position - v.m_data.m_value.string->size() - 2; + // escape sequences make the token longer than the value it + // parses to, so the start position cannot be derived from + // the value; use the offset the lexer recorded instead + v.start_position = m_lexer_ref->get_token_start_position(); break; } @@ -10318,8 +10344,10 @@ class json_sax_dom_callback_parser case value_t::string: { - // include the length of the quotes, which is 2 - v.start_position = v.end_position - v.m_data.m_value.string->size() - 2; + // escape sequences make the token longer than the value it + // parses to, so the start position cannot be derived from + // the value; use the offset the lexer recorded instead + v.start_position = m_lexer_ref->get_token_start_position(); break; } @@ -11169,15 +11197,6 @@ class binary_reader // CBOR // ////////// - /*! - @param[in] get_char whether a new character should be retrieved from the - input (true) or whether the last read character should - be considered instead (false) - @param[in] tag_handler how CBOR tags should be treated - - @return whether a valid CBOR value was passed to the SAX parser - */ - template bool get_cbor_negative_integer() { @@ -11196,6 +11215,14 @@ class binary_reader return sax->number_integer(static_cast(-1) - static_cast(number)); } + /*! + @param[in] get_char whether a new character should be retrieved from the + input (true) or whether the last read character should + be considered instead (false) + @param[in] tag_handler how CBOR tags should be treated + + @return whether a valid CBOR value was passed to the SAX parser + */ bool parse_cbor_internal(const bool get_char, const cbor_tag_handler_t tag_handler) { @@ -11478,7 +11505,13 @@ class binary_reader case 0xBF: // map (indefinite length) return get_cbor_object(detail::unknown_size(), tag_handler); - case 0xC6: // tagged item + case 0xC0: // tagged item + case 0xC1: + case 0xC2: + case 0xC3: + case 0xC4: + case 0xC5: + case 0xC6: case 0xC7: case 0xC8: case 0xC9: @@ -11493,6 +11526,9 @@ class binary_reader case 0xD2: case 0xD3: case 0xD4: + case 0xD5: + case 0xD6: + case 0xD7: case 0xD8: // tagged item (1 byte follows) case 0xD9: // tagged item (2 bytes follow) case 0xDA: // tagged item (4 bytes follow) @@ -12692,7 +12728,11 @@ class binary_reader { if (get_char) { - get(); // TODO(niels): may we ignore N here? + // no get_ignore_noop() here: the byte read next must be a string + // length type specification, and a no-op ('N') is not valid in + // that position. No-ops at positions where a value may appear are + // already consumed by the callers via get_ignore_noop(). + get(); } if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format, "value"))) @@ -18700,7 +18740,23 @@ class binary_writer { return true; } - len *= static_cast(el.template get()); + + // a dimension that does not fit into std::size_t, or a product that + // overflows it, would wrap around and could match the size of + // _ArrayData_ by accident; the resulting header announces an + // element count that no reader can honor (the binary reader rejects + // it with out_of_range.408), so encode as a plain object instead + const auto dim = el.template get(); + if (!value_in_range_of(dim)) + { + return true; + } + const auto dim_size = static_cast(dim); + if (dim_size != 0 && len > (std::numeric_limits::max)() / dim_size) + { + return true; + } + len *= dim_size; } key = "_ArrayData_"; @@ -25068,6 +25124,12 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // note parentheses around operands are necessary; see // https://github.com/nlohmann/json/issues/1530 + // Mixed signed/unsigned integer comparisons check whether the signed value + // is negative before casting. If it is, the comparison is performed with + // the fixed values -1 and 1, which preserves the ordering relationship + // because any negative signed value is smaller than any unsigned value. + // Otherwise, the non-negative signed value is cast to unsigned before the + // comparison to avoid wraparound. #define JSON_IMPLEMENT_OPERATOR(op, null_result, unordered_result, default_result) \ const auto lhs_type = lhs.type(); \ const auto rhs_type = rhs.type(); \ @@ -25126,12 +25188,16 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } \ else if (lhs_type == value_t::number_unsigned && rhs_type == value_t::number_integer) \ { \ - return static_cast(lhs.m_data.m_value.number_unsigned) op rhs.m_data.m_value.number_integer; \ + return (rhs.m_data.m_value.number_integer < 0) \ + ? (number_integer_t(1) op number_integer_t(-1)) \ + : (lhs.m_data.m_value.number_unsigned op static_cast(rhs.m_data.m_value.number_integer)); \ } \ else if (lhs_type == value_t::number_integer && rhs_type == value_t::number_unsigned) \ { \ - return lhs.m_data.m_value.number_integer op static_cast(rhs.m_data.m_value.number_unsigned); \ - } \ + return (lhs.m_data.m_value.number_integer < 0) \ + ? (number_integer_t(-1) op number_integer_t(1)) \ + : (static_cast(lhs.m_data.m_value.number_integer) op rhs.m_data.m_value.number_unsigned); \ + } \ else if(compares_unordered(lhs, rhs))\ {\ return (unordered_result);\ diff --git a/tests/src/unit-bjdata.cpp b/tests/src/unit-bjdata.cpp index d00e1fa67..d5f9c3acd 100644 --- a/tests/src/unit-bjdata.cpp +++ b/tests/src/unit-bjdata.cpp @@ -2730,6 +2730,27 @@ TEST_CASE("BJData") CHECK(json::from_bjdata(json::to_bjdata(j_type), true, true) == j_type); CHECK(json::from_bjdata(json::to_bjdata(j_size), true, true) == j_size); } + + SECTION("ndarray whose dimensions overflow stays as object") + { + // the product of the dimensions wraps around std::size_t to 0 + // and so matches the size of the empty _ArrayData_; writing this + // as an ndarray would announce an element count no reader can + // honor, so it has to stay a plain object + json j_overflow = json({{"_ArrayData_", json::array()}, {"_ArraySize_", {9223372036854775808ull, 2}}, {"_ArrayType_", "uint8"}}); + CHECK(json::from_bjdata(json::to_bjdata(j_overflow), true, true) == j_overflow); + + // a single dimension that does not fit into std::size_t is + // rejected for the same reason (only observable where + // std::size_t is narrower than 64 bit) + json j_huge = json({{"_ArrayData_", json::array()}, {"_ArraySize_", {18446744073709551615ull}}, {"_ArrayType_", "uint8"}}); + CHECK(json::from_bjdata(json::to_bjdata(j_huge), true, true) == j_huge); + + // a well-formed ndarray is still encoded as one + json j_ok = json({{"_ArrayData_", {1, 2, 3, 4, 5, 6}}, {"_ArraySize_", {2, 3}}, {"_ArrayType_", "uint8"}}); + CHECK(json::to_bjdata(j_ok) == std::vector({'[', '$', 'U', '#', '[', 'i', 2, 'i', 3, ']', 1, 2, 3, 4, 5, 6})); + CHECK(json::from_bjdata(json::to_bjdata(j_ok), true, true) == j_ok); + } } } diff --git a/tests/src/unit-cbor.cpp b/tests/src/unit-cbor.cpp index 1b4b95dbf..2a6bd41d7 100644 --- a/tests/src/unit-cbor.cpp +++ b/tests/src/unit-cbor.cpp @@ -2565,11 +2565,16 @@ TEST_CASE("Tagged values") const json j = "s"; auto v = json::to_cbor(j); - SECTION("0xC6..0xD4") + const json j_bin_payload = json::binary(std::vector {0x01, 0x02, 0x03}); + auto v_bin_payload = json::to_cbor(j_bin_payload); + + SECTION("0xC0..0xD7") { for (const auto b : std::vector { - 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xD2, 0xD3, 0xD4 + 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, + 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, + 0xD5, 0xD6, 0xD7 }) { CAPTURE(b); @@ -2589,6 +2594,12 @@ TEST_CASE("Tagged values") auto j_tagged_stored = json::from_cbor(v_tagged, true, true, json::cbor_tag_handler_t::store); CHECK(j_tagged_stored == j); + + auto v_binary_tagged = v_bin_payload; + v_binary_tagged.insert(v_binary_tagged.begin(), b); + auto j_binary_tagged_stored = json::from_cbor(v_binary_tagged, true, true, json::cbor_tag_handler_t::store); + CHECK(j_binary_tagged_stored == j_bin_payload); + CHECK(!j_binary_tagged_stored.get_binary().has_subtype()); } } diff --git a/tests/src/unit-comparison.cpp b/tests/src/unit-comparison.cpp index d9df1a3a6..31fbdc57a 100644 --- a/tests/src/unit-comparison.cpp +++ b/tests/src/unit-comparison.cpp @@ -15,6 +15,8 @@ #include "doctest_compatibility.h" +#include + #define JSON_TESTS_PRIVATE #include using nlohmann::json; @@ -255,6 +257,75 @@ TEST_CASE("lexicographical comparison operators") {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 21 }; + SECTION("signed/unsigned mixed comparison above INT64_MAX") + { + const json above_int64_max = static_cast((std::numeric_limits::max)()) + 1ULL; + const json max_uint64 = (std::numeric_limits::max)(); + const json negative_one = -1; + const json one = 1; + const json max_int64 = (std::numeric_limits::max)(); + + CHECK_FALSE(above_int64_max == negative_one); + CHECK(above_int64_max != negative_one); + CHECK(negative_one < above_int64_max); + CHECK(negative_one <= above_int64_max); + CHECK_FALSE(negative_one > above_int64_max); + CHECK_FALSE(negative_one >= above_int64_max); + CHECK_FALSE(above_int64_max < negative_one); + CHECK_FALSE(above_int64_max <= negative_one); + CHECK(above_int64_max > negative_one); + CHECK(above_int64_max >= negative_one); + CHECK(negative_one != above_int64_max); + CHECK_FALSE(negative_one == above_int64_max); + + CHECK_FALSE(max_uint64 == negative_one); + CHECK(max_uint64 != negative_one); + CHECK(negative_one < max_uint64); + CHECK(negative_one <= max_uint64); + CHECK_FALSE(negative_one > max_uint64); + CHECK_FALSE(negative_one >= max_uint64); + CHECK_FALSE(max_uint64 < negative_one); + CHECK_FALSE(max_uint64 <= negative_one); + CHECK(max_uint64 > negative_one); + CHECK(max_uint64 >= negative_one); + CHECK(negative_one != max_uint64); + CHECK_FALSE(negative_one == max_uint64); + + CHECK_FALSE(one == above_int64_max); + CHECK(one != above_int64_max); + CHECK(one < above_int64_max); + CHECK(one <= above_int64_max); + CHECK_FALSE(one > above_int64_max); + CHECK_FALSE(one >= above_int64_max); + CHECK_FALSE(above_int64_max < one); + CHECK_FALSE(above_int64_max <= one); + CHECK(above_int64_max > one); + CHECK(above_int64_max >= one); + + CHECK_FALSE(max_int64 == above_int64_max); + CHECK(max_int64 != above_int64_max); + CHECK(max_int64 < above_int64_max); + CHECK(max_int64 <= above_int64_max); + CHECK_FALSE(max_int64 > above_int64_max); + CHECK_FALSE(max_int64 >= above_int64_max); + CHECK_FALSE(above_int64_max < max_int64); + CHECK_FALSE(above_int64_max <= max_int64); + CHECK(above_int64_max > max_int64); + CHECK(above_int64_max >= max_int64); + +#if JSON_HAS_THREE_WAY_COMPARISON + // JSON_HAS_CPP_20 (do not remove; see note at top of file) + CHECK((negative_one <=> above_int64_max) == std::partial_ordering::less); // *NOPAD* + CHECK((above_int64_max <=> negative_one) == std::partial_ordering::greater); // *NOPAD* + CHECK((negative_one <=> max_uint64) == std::partial_ordering::less); // *NOPAD* + CHECK((max_uint64 <=> negative_one) == std::partial_ordering::greater); // *NOPAD* + CHECK((one <=> above_int64_max) == std::partial_ordering::less); // *NOPAD* + CHECK((above_int64_max <=> one) == std::partial_ordering::greater); // *NOPAD* + CHECK((max_int64 <=> above_int64_max) == std::partial_ordering::less); // *NOPAD* + CHECK((above_int64_max <=> max_int64) == std::partial_ordering::greater); // *NOPAD* +#endif + } + SECTION("compares unordered") { std::vector> expected = diff --git a/tests/src/unit-diagnostic-positions.cpp b/tests/src/unit-diagnostic-positions.cpp index 59c21dc59..ad9527540 100644 --- a/tests/src/unit-diagnostic-positions.cpp +++ b/tests/src/unit-diagnostic-positions.cpp @@ -38,6 +38,36 @@ TEST_CASE("Better diagnostics with positions") "[json.exception.type_error.302] type must be number, but is string", json::type_error); } + SECTION("positions of strings containing escape sequences") + { + // escape sequences make the token longer than the string it parses to, + // so the positions must not be derived from the parsed value's length + const auto check = [](const std::string & text, const std::string & token) + { + CAPTURE(text) + CAPTURE(token) + const json j = json::parse(text); + const json& v = j.at("a"); + CHECK(text.substr(v.start_pos(), v.end_pos() - v.start_pos()) == token); + }; + + check(R"({"a":"plain"})", R"("plain")"); + check(R"({"a":"tab\there"})", R"("tab\there")"); + check(R"({"a":"\n\n\n\n\n\n"})", R"("\n\n\n\n\n\n")"); + check(R"({"a":"\""})", R"("\"")"); + check(R"({"a":"\\"})", R"("\\")"); + check(R"({"a":"é"})", R"("é")"); + check(R"({"a":"🌞"})", R"("🌞")"); + check("{\"a\":\"\xc3\xa9\"}", "\"\xc3\xa9\""); // multi-byte UTF-8, no escapes + + // a string at the root, where an escape would otherwise push the + // reported start position past the opening quote + const std::string root = R"("a\tb")"; + const json j = json::parse(root); + CHECK(j.start_pos() == 0); + CHECK(j.end_pos() == root.size()); + } + SECTION("JSON patch add to primitive parent (#4292)") { // the JSON Patch "add" target /foo/bar/baz has a string parent diff --git a/tests/src/unit-ubjson.cpp b/tests/src/unit-ubjson.cpp index d5668833d..e1e327563 100644 --- a/tests/src/unit-ubjson.cpp +++ b/tests/src/unit-ubjson.cpp @@ -1713,6 +1713,44 @@ TEST_CASE("UBJSON") CHECK(json::to_ubjson(json::from_ubjson(s_L)) == s_i); } + SECTION("no-op markers") + { + // A no-op ('N') is valid wherever a value may start; it is consumed + // by get_ignore_noop() before the value is read. It is not valid + // where a string length type specification is expected. + + SECTION("accepted where a value may start") + { + // at top level, also repeated + CHECK(json::from_ubjson(std::vector({'N', 'i', 1})) == json(1)); + CHECK(json::from_ubjson(std::vector({'N', 'N', 'N', 'i', 1})) == json(1)); + + // inside an array of unknown size, before and after an element + CHECK(json::from_ubjson(std::vector({'[', 'N', 'i', 1, ']'})) == json({1})); + CHECK(json::from_ubjson(std::vector({'[', 'i', 1, 'N', ']'})) == json({1})); + + // inside an object of unknown size: before a key, between key + // and value, and before the closing '}' + CHECK(json::from_ubjson(std::vector({'{', 'N', 'U', 1, 'a', 'i', 1, '}'})) == json({{"a", 1}})); + CHECK(json::from_ubjson(std::vector({'{', 'U', 1, 'a', 'N', 'i', 1, '}'})) == json({{"a", 1}})); + CHECK(json::from_ubjson(std::vector({'{', 'U', 1, 'a', 'i', 1, 'N', '}'})) == json({{"a", 1}})); + } + + SECTION("rejected where a length type specification is expected") + { + json _; + + // after the 'S' marker of a string value + std::vector const v_S = {'S', 'N', 'U', 1, 'a'}; + CHECK_THROWS_WITH_AS(_ = json::from_ubjson(v_S), "[json.exception.parse_error.113] parse error at byte 2: syntax error while parsing UBJSON string: expected length type specification (U, i, I, l, L); last byte: 0x4E", json::parse_error&); + + // as the key length of an object with a known size, where + // no-ops are not permitted in the first place + std::vector const v_key = {'{', '#', 'i', 1, 'N', 'U', 1, 'a', 'i', 1}; + CHECK_THROWS_WITH_AS(_ = json::from_ubjson(v_key), "[json.exception.parse_error.113] parse error at byte 5: syntax error while parsing UBJSON string: expected length type specification (U, i, I, l, L); last byte: 0x4E", json::parse_error&); + } + } + SECTION("number") { SECTION("float") diff --git a/tests/src/unit-wstring.cpp b/tests/src/unit-wstring.cpp index ffbe70e7e..a38df3aaa 100644 --- a/tests/src/unit-wstring.cpp +++ b/tests/src/unit-wstring.cpp @@ -125,6 +125,16 @@ TEST_CASE("wide strings") std::u32string const w = U"\"\x110000"; json _; CHECK_THROWS_AS(_ = json::parse(w), json::parse_error&); + + // a code unit above U+10FFFF must not be narrowed onto the EOF + // sentinel: 0xFFFFFFFF would otherwise end the document silently and + // let everything following it pass the strict end-of-input check + std::u32string const trailing{U'[', U'1', U']', static_cast(0xFFFFFFFF), U'x'}; + CHECK_THROWS_WITH_AS(_ = json::parse(trailing), "[json.exception.parse_error.101] parse error at line 1, column 4: syntax error while parsing value - invalid literal; last read: '1]\xFF'; expected end of input", json::parse_error&); + CHECK(!json::accept(trailing)); + + // the same unit inside a string is reported as an ill-formed byte + CHECK_THROWS_WITH_AS(_ = json::parse(std::u32string{U'"', static_cast(0xFFFFFFFF), U'"'}), "[json.exception.parse_error.101] parse error at line 1, column 2: syntax error while parsing value - invalid string: ill-formed UTF-8 byte; last read: '\"\xFF'", json::parse_error&); } } }