Compare commits

..
Author SHA1 Message Date
Niels Lohmann aeafe1ad09 Merge branch 'develop' into issue-5405-binary-reader-reserve
Resolves conflicting test-case insertions in tests/src/unit-cbor.cpp
and tests/src/unit-ubjson.cpp: both branches independently appended a
new TEST_CASE near the end of each file. Keep both -- the #5405
array-reserve regression tests from this branch, and develop's
unrelated call-stack-depth / indefinite-length-string hardening tests.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-11 09:50:27 +02:00
Niels Lohmann d753f53e68 Use (std::min)(len, reserve_cap) instead of a ternary in start_array()
Addresses review feedback from @gregmarr on PR #5476.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-08 19:48:47 +02:00
Niels Lohmann 1c9475d68f Fix remaining CI failures in the huge-claimed-length DoS regression tests
- Apply the same google-readability-casting fix (std::size_t{N} instead
  of std::size_t(N)) to the "arrays of various sizes" section in
  unit-msgpack.cpp, unit-ubjson.cpp, and unit-bjdata.cpp; only
  unit-cbor.cpp had been fixed previously, since clang-tidy's build
  didn't get far enough to report the other three in the same pass.

- json_sax_dom_parser::start_array()'s max_size() check calls JSON_THROW
  directly rather than going through sax->parse_error(), so unlike the
  scanner's own "not enough data" parse_error it is not gated by
  allow_exceptions=false. On a platform where a header's claimed count
  exceeds max_size() (e.g. 32-bit, for UBJSON/BJData's 0x7FFFFFFF test
  value), from_ubjson/from_bjdata(input, true, false) can therefore still
  throw instead of returning a discarded value. Make that assertion
  tolerant of either outcome, same as the main exception-catching check
  above it.

- Guard all four "a huge claimed length..." SECTIONs with
  #if !defined(JSON_NOEXCEPTION), matching this test suite's existing
  convention for exception-dependent tests: under JSON_NOEXCEPTION,
  JSON_THROW never produces a catchable C++ exception at all (it aborts
  the process), so a section that relies on try/catch to distinguish
  between two acceptable outcomes cannot be expressed under that build
  configuration regardless of platform.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-06 20:08:27 +02:00
Niels Lohmann 98ab9f31c3 Make the huge-claimed-length DoS regression tests portable across size_t widths
On a platform where size_t is narrower than 64 bits (e.g. 32-bit mingw/msvc
x86), the previously-hardcoded huge test lengths either collide with that
platform's unknown_size() sentinel (CBOR/MessagePack, both using exactly
SIZE_MAX) or exceed the platform's smaller vector<json>::max_size()
(UBJSON/BJData's 0x7FFFFFFF), so the header is now rejected outright
(out_of_range.408) instead of being accepted and only found short of data
(parse_error.110). Both are safe, bounded rejections of the hostile input;
the property under test -- no attempt to allocate space for billions of
elements -- holds either way. Accept both outcomes instead of pinning the
64-bit-only exact result.

Also fixed an unrelated clang-tidy finding (google-readability-casting) on
the functional-style std::size_t(...) casts in the neighboring "arrays of
various sizes" section.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-06 11:19:06 +02:00
Niels Lohmann aee9421883 Reserve capped array capacity for definite-length binary arrays
CBOR, MessagePack, and the optimized [$type#count UBJSON/BJData form all
pass an exact element count to sax->start_array(len), but
json_sax_dom_parser::start_array() (and the callback variant) only used
len for an overflow check against max_size() and never reserved the
underlying vector, so each element triggered a reallocation cascade via
emplace_back().

Reserve upfront, but cap the reservation at 16384 elements: max_size()
for a std::vector is far larger than any realistic input, so an
unbounded reserve(len) would let a crafted/truncated header (e.g. CBOR
0x9A + a huge uint32 count with no data) trigger a multi-gigabyte
allocation attempt instead of the normal graceful parse_error. With the
cap, a hostile length still fails fast with the existing parse_error,
while realistic arrays get a single up-front allocation.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-05 20:51:47 +02:00
16 changed files with 425 additions and 378 deletions
+1 -1
View File
@@ -100,7 +100,7 @@ jobs:
container: ubuntu:focal container: ubuntu:focal
strategy: strategy:
matrix: matrix:
target: [ci_cmake_flags, ci_test_diagnostics, ci_test_diagnostic_positions, ci_test_noexceptions, ci_test_noimplicitconversions, ci_test_legacycomparison, ci_test_noglobaludls, ci_test_disableenumserialization, ci_test_skiplibraryversioncheck, ci_test_simdutf] target: [ci_cmake_flags, ci_test_diagnostics, ci_test_diagnostic_positions, ci_test_noexceptions, ci_test_noimplicitconversions, ci_test_legacycomparison, ci_test_noglobaludls, ci_test_simdutf]
steps: steps:
- name: Install build-essential - name: Install build-essential
run: apt-get update ; apt-get install -y build-essential unzip wget git libssl-dev run: apt-get update ; apt-get install -y build-essential unzip wget git libssl-dev
-34
View File
@@ -260,40 +260,6 @@ add_custom_target(ci_test_noglobaludls
COMMENT "Compile and test with global UDLs disabled" COMMENT "Compile and test with global UDLs disabled"
) )
###############################################################################
# Disable enum serialization.
###############################################################################
add_custom_target(ci_test_disableenumserialization
COMMAND ${CMAKE_COMMAND}
-DCMAKE_BUILD_TYPE=Debug -GNinja
-DJSON_BuildTests=ON -DJSON_FastTests=ON -DJSON_DisableEnumSerialization=ON
-S${PROJECT_SOURCE_DIR} -B${PROJECT_BINARY_DIR}/build_disableenumserialization
COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_disableenumserialization
COMMAND cd ${PROJECT_BINARY_DIR}/build_disableenumserialization && ${CMAKE_CTEST_COMMAND} --parallel ${N} --output-on-failure
COMMENT "Compile and test with enum serialization disabled"
)
###############################################################################
# Skip the multiple-inclusion library version check.
###############################################################################
# tests/src/skip_library_version_check.cpp deliberately simulates a scenario
# (mixing two differently-versioned inclusions of the library in one
# translation unit) that unavoidably triggers the compiler's own "macro
# redefined" warning, so -- unlike the ci_test_* targets above -- it is
# compiled directly here, with a modest warning set, instead of being folded
# into the library's own -Weverything/-Werror unit test matrix.
add_custom_target(ci_test_skiplibraryversioncheck
COMMAND ${CMAKE_COMMAND} -E make_directory ${PROJECT_BINARY_DIR}/skip_library_version_check
COMMAND ${CMAKE_CXX_COMPILER} -std=c++11 -Wall -Wextra
-I${PROJECT_SOURCE_DIR}/include
${PROJECT_SOURCE_DIR}/tests/src/skip_library_version_check.cpp
-o ${PROJECT_BINARY_DIR}/skip_library_version_check/skip_library_version_check
COMMAND ${PROJECT_BINARY_DIR}/skip_library_version_check/skip_library_version_check
COMMENT "Compile and run a translation unit simulating a mismatched library version, with JSON_SKIP_LIBRARY_VERSION_CHECK defined"
)
############################################################################### ###############################################################################
# Coverage. # Coverage.
############################################################################### ###############################################################################
@@ -8,6 +8,7 @@
#pragma once #pragma once
#include <algorithm> // min
#include <cstddef> #include <cstddef>
#include <string> // string #include <string> // string
#include <type_traits> // enable_if_t #include <type_traits> // enable_if_t
@@ -305,6 +306,16 @@ class json_sax_dom_parser
JSON_THROW(out_of_range::create(408, concat("excessive array size: ", std::to_string(len)), ref_stack.back())); JSON_THROW(out_of_range::create(408, concat("excessive array size: ", std::to_string(len)), ref_stack.back()));
} }
if (len != detail::unknown_size())
{
// reserve upfront to avoid repeated reallocations while adding elements,
// but cap the reservation so a bogus/hostile length (which is not bounded
// by max_size(), unlike e.g. std::vector) cannot trigger an oversized
// allocation for a small or truncated input
constexpr std::size_t reserve_cap = 16384;
ref_stack.back()->m_data.m_value.array->reserve((std::min)(len, reserve_cap));
}
return true; return true;
} }
@@ -683,6 +694,16 @@ class json_sax_dom_callback_parser
{ {
JSON_THROW(out_of_range::create(408, concat("excessive array size: ", std::to_string(len)), ref_stack.back())); JSON_THROW(out_of_range::create(408, concat("excessive array size: ", std::to_string(len)), ref_stack.back()));
} }
if (len != detail::unknown_size())
{
// reserve upfront to avoid repeated reallocations while adding elements,
// but cap the reservation so a bogus/hostile length (which is not bounded
// by max_size(), unlike e.g. std::vector) cannot trigger an oversized
// allocation for a small or truncated input
constexpr std::size_t reserve_cap = 16384;
ref_stack.back()->m_data.m_value.array->reserve((std::min)(len, reserve_cap));
}
} }
return true; return true;
+21
View File
@@ -7892,6 +7892,7 @@ NLOHMANN_JSON_NAMESPACE_END
#include <algorithm> // min
#include <cstddef> #include <cstddef>
#include <string> // string #include <string> // string
#include <type_traits> // enable_if_t #include <type_traits> // enable_if_t
@@ -11018,6 +11019,16 @@ class json_sax_dom_parser
JSON_THROW(out_of_range::create(408, concat("excessive array size: ", std::to_string(len)), ref_stack.back())); JSON_THROW(out_of_range::create(408, concat("excessive array size: ", std::to_string(len)), ref_stack.back()));
} }
if (len != detail::unknown_size())
{
// reserve upfront to avoid repeated reallocations while adding elements,
// but cap the reservation so a bogus/hostile length (which is not bounded
// by max_size(), unlike e.g. std::vector) cannot trigger an oversized
// allocation for a small or truncated input
constexpr std::size_t reserve_cap = 16384;
ref_stack.back()->m_data.m_value.array->reserve((std::min)(len, reserve_cap));
}
return true; return true;
} }
@@ -11396,6 +11407,16 @@ class json_sax_dom_callback_parser
{ {
JSON_THROW(out_of_range::create(408, concat("excessive array size: ", std::to_string(len)), ref_stack.back())); JSON_THROW(out_of_range::create(408, concat("excessive array size: ", std::to_string(len)), ref_stack.back()));
} }
if (len != detail::unknown_size())
{
// reserve upfront to avoid repeated reallocations while adding elements,
// but cap the reservation so a bogus/hostile length (which is not bounded
// by max_size(), unlike e.g. std::vector) cannot trigger an oversized
// allocation for a small or truncated input
constexpr std::size_t reserve_cap = 16384;
ref_stack.back()->m_data.m_value.array->reserve((std::min)(len, reserve_cap));
}
} }
return true; return true;
-61
View File
@@ -1,61 +0,0 @@
// __ _____ _____ _____
// __| | __| | | | 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
// Standalone compile-and-run check for the JSON_SKIP_LIBRARY_VERSION_CHECK
// configuration macro, which (per #5423) was never exercised anywhere in the
// test matrix.
//
// include/nlohmann/detail/abi_macros.hpp normally emits a #warning if
// NLOHMANN_JSON_VERSION_MAJOR/MINOR/PATCH are already defined (as they would
// be by an earlier inclusion of a different version of the library) with
// values that mismatch the version about to be defined -- unless
// JSON_SKIP_LIBRARY_VERSION_CHECK is defined, in which case the check (and
// that #warning) is skipped.
//
// This file deliberately is not named tests/src/unit-*.cpp: it is compiled
// directly (with a modest, non-strict warning set) by the dedicated
// ci_test_skiplibraryversioncheck target in cmake/ci.cmake, rather than being
// folded into the library's own -Weverything/-Werror unit test matrix. That
// is because the scenario simulated here -- mixing two different, already
// differently-versioned inclusions of the library in one translation unit --
// unavoidably also triggers the *compiler's own* "macro redefined" warning,
// independent of (and unaffected by) JSON_SKIP_LIBRARY_VERSION_CHECK, which
// only ever silences the library's own #warning. Building this file under
// -Weverything -Werror would therefore fail for a reason unrelated to the
// macro under test.
#define NLOHMANN_JSON_VERSION_MAJOR 0
#define NLOHMANN_JSON_VERSION_MINOR 0
#define NLOHMANN_JSON_VERSION_PATCH 0
#define JSON_SKIP_LIBRARY_VERSION_CHECK 1
#include <nlohmann/json.hpp>
int main()
{
// reaching this point at all already proves that the mismatched,
// pre-defined version macros above did not stop compilation -- which is
// exactly what JSON_SKIP_LIBRARY_VERSION_CHECK is for. The library must
// also still be fully usable.
const nlohmann::json j = {{"a", 1}, {"b", {1, 2, 3}}};
if (j.dump() != "{\"a\":1,\"b\":[1,2,3]}")
{
return 1;
}
// include/nlohmann/detail/abi_macros.hpp unconditionally (re)defines the
// version macros to the library's real, current version right after the
// (here, skipped) mismatch check, regardless of the deliberately wrong
// stand-in values defined above.
if (NLOHMANN_JSON_VERSION_MAJOR == 0 && NLOHMANN_JSON_VERSION_MINOR == 0 && NLOHMANN_JSON_VERSION_PATCH == 0)
{
return 1;
}
return 0;
}
+105
View File
@@ -3551,6 +3551,111 @@ TEST_CASE("BJData")
} }
} }
TEST_CASE("issue #5405 - array reserve for definite-length BJData arrays")
{
#if !defined(JSON_NOEXCEPTION)
// this SECTION relies on catching a thrown exception to distinguish
// which of two acceptable, bounded rejections a hostile header took;
// under JSON_NOEXCEPTION, JSON_THROW never produces a catchable C++
// exception (it aborts instead), so this cannot be tested that way here
SECTION("a huge claimed length with no element data must not over-allocate")
{
// optimized form [$type#count: type 'i' (int8), count as a four-byte
// little-endian 'l' (int32) of 0x7FFFFFFF (2147483647), but no
// element data at all. max_size() for a std::vector is far larger
// than this count, so it does not reject the header outright; the
// (capped) reservation must not attempt to allocate space for
// billions of elements before the missing data is detected.
json _;
const std::vector<uint8_t> input = {'[', '$', 'i', '#', 'l', 0xFF, 0xFF, 0xFF, 0x7F};
// On a platform where std::vector<json>::max_size() is smaller than
// the claimed count (e.g. 32-bit, where max_size() is bounded by a
// 32-bit SIZE_MAX divided by sizeof(json)), the SAX consumer's own
// check rejects the header outright (out_of_range.408, with the
// claimed count in the message) instead of accepting it and only
// finding it short of data once the (capped) reservation looks for
// element bytes that were never provided (parse_error.110). Either
// is an acceptable, bounded rejection of the hostile header -- the
// property under test is that no path attempts to allocate space
// for billions of elements.
bool threw = false;
try
{
_ = json::from_bjdata(input);
}
catch (const json::parse_error& e)
{
threw = true;
CHECK(e.id == 110);
CHECK(std::string(e.what()) == "[json.exception.parse_error.110] parse error at byte 10: syntax error while parsing BJData number: unexpected end of input");
}
catch (const json::out_of_range& e)
{
threw = true;
CHECK(e.id == 408);
CHECK(std::string(e.what()).find("excessive array size") != std::string::npos);
}
CHECK(threw);
// json_sax_dom_parser::start_array()'s max_size() check (unlike the
// scanner's own parse_error path) throws unconditionally via
// JSON_THROW rather than going through sax->parse_error(), so it is
// not gated by allow_exceptions=false on a platform where this
// header hits that check (e.g. 32-bit, see above) -- allow either
// a discarded result or the same out_of_range it throws with
// exceptions enabled.
try
{
CHECK(json::from_bjdata(input, true, false).is_discarded());
}
catch (const json::out_of_range& e)
{
CHECK(e.id == 408);
}
}
#endif
SECTION("arrays of various sizes decode to the same value as before the reserve optimization")
{
for (const auto size :
{
std::size_t{0}, std::size_t{1}, std::size_t{5}, // small
std::size_t{16384}, // exactly at the reserve cap
std::size_t{20000} // above the reserve cap
})
{
CAPTURE(size)
json j = json::array();
for (std::size_t i = 0; i < size; ++i)
{
j.push_back(static_cast<int>(i % 1000));
}
// exercise both the plain and the optimized [$type#count encoding
const auto packed_plain = json::to_bjdata(j);
CHECK(json::from_bjdata(packed_plain) == j);
const auto packed_optimized = json::to_bjdata(j, true, true);
CHECK(json::from_bjdata(packed_optimized) == j);
}
}
SECTION("a user-defined SAX consumer is unaffected by the internal DOM reserve optimization")
{
// the reserve() call is local to json_sax_dom_parser / json_sax_dom_callback_parser;
// a custom SAX consumer that does not touch a DOM array sees identical events
json j = json::array();
for (int i = 0; i < 100; ++i)
{
j.push_back(i);
}
const auto packed = json::to_bjdata(j, true, true);
SaxCountdown scp(1000000); // large enough to never trigger an abort
CHECK(json::sax_parse(packed, &scp, json::input_format_t::bjdata));
}
}
TEST_CASE("Universal Binary JSON Specification Examples 1") TEST_CASE("Universal Binary JSON Specification Examples 1")
{ {
SECTION("Null Value") SECTION("Null Value")
@@ -42,39 +42,6 @@ TEST_CASE("byte_container_with_subtype")
CHECK(container.subtype() == static_cast<subtype_type>(-1)); CHECK(container.subtype() == static_cast<subtype_type>(-1));
} }
SECTION("move semantics")
{
// the rvalue-reference constructor (without a subtype) must actually move
// the passed-in container rather than copy it; comparing the buffer address
// before and after is a stronger check than just observing the source is
// empty afterward, since a copy-then-clear could also leave it empty
{
std::vector<std::uint8_t> bytes = {{0xCA, 0xFE, 0xBA, 0xBE}};
const auto* const data_ptr = bytes.data();
nlohmann::byte_container_with_subtype<std::vector<std::uint8_t>> container(std::move(bytes));
CHECK(container.size() == 4);
CHECK(container.data() == data_ptr);
CHECK(!container.has_subtype());
CHECK(bytes.empty()); // NOLINT(bugprone-use-after-move,clang-analyzer-cplusplus.Move,hicpp-invalid-access-moved)
}
// same check for the rvalue-reference constructor that also takes a subtype
{
std::vector<std::uint8_t> bytes = {{0xCA, 0xFE, 0xBA, 0xBE}};
const auto* const data_ptr = bytes.data();
nlohmann::byte_container_with_subtype<std::vector<std::uint8_t>> container(std::move(bytes), 42);
CHECK(container.size() == 4);
CHECK(container.data() == data_ptr);
CHECK(container.has_subtype());
CHECK(container.subtype() == 42);
CHECK(bytes.empty()); // NOLINT(bugprone-use-after-move,clang-analyzer-cplusplus.Move,hicpp-invalid-access-moved)
}
}
SECTION("comparisons") SECTION("comparisons")
{ {
std::vector<std::uint8_t> const bytes = {{0xCA, 0xFE, 0xBA, 0xBE}}; std::vector<std::uint8_t> const bytes = {{0xCA, 0xFE, 0xBA, 0xBE}};
+86
View File
@@ -2035,6 +2035,92 @@ TEST_CASE("CBOR definite length equal to the indefinite-length sentinel")
} }
} }
TEST_CASE("issue #5405 - array reserve for definite-length CBOR arrays")
{
#if !defined(JSON_NOEXCEPTION)
// this SECTION relies on catching a thrown exception to distinguish
// which of two acceptable, bounded rejections a hostile header took;
// under JSON_NOEXCEPTION, JSON_THROW never produces a catchable C++
// exception (it aborts instead), so this cannot be tested that way here
SECTION("a huge claimed length with no element data must not over-allocate")
{
// 0x9A: array with a four-byte length; claims 0xFFFFFFFF (4294967295)
// elements but provides none. max_size() for a std::vector is far
// larger than this count, so it does not reject the header outright;
// the (capped) reservation must not attempt to allocate space for
// billions of elements before the missing data is detected.
json _;
const std::vector<uint8_t> input = {0x9A, 0xFF, 0xFF, 0xFF, 0xFF};
// On a platform where std::size_t is narrower than 64 bits (e.g.
// 32-bit), the claimed count 0xFFFFFFFF coincides with that
// platform's detail::unknown_size() sentinel (SIZE_MAX), so the
// format-level size check rejects it outright (out_of_range.408,
// "excessive ... size") before the SAX consumer's own max_size()
// check would even run; on a 64-bit platform it passes both of
// those checks and is only found short of data once the (capped)
// reservation looks for element bytes that were never provided
// (parse_error.110). Either is an acceptable, bounded rejection of
// the hostile header -- the property under test is that no path
// attempts to allocate space for billions of elements.
bool threw = false;
try
{
_ = json::from_cbor(input);
}
catch (const json::parse_error& e)
{
threw = true;
CHECK(e.id == 110);
CHECK(std::string(e.what()) == "[json.exception.parse_error.110] parse error at byte 6: syntax error while parsing CBOR value: unexpected end of input");
}
catch (const json::out_of_range& e)
{
threw = true;
CHECK(e.id == 408);
CHECK(std::string(e.what()).find("excessive") != std::string::npos);
}
CHECK(threw);
CHECK(json::from_cbor(input, true, false).is_discarded());
}
#endif
SECTION("arrays of various sizes decode to the same value as before the reserve optimization")
{
for (const auto size :
{
std::size_t{0}, std::size_t{1}, std::size_t{5}, // small
std::size_t{16384}, // exactly at the reserve cap
std::size_t{20000} // above the reserve cap
})
{
CAPTURE(size)
json j = json::array();
for (std::size_t i = 0; i < size; ++i)
{
j.push_back(static_cast<int>(i % 1000));
}
const auto packed = json::to_cbor(j);
CHECK(json::from_cbor(packed) == j);
}
}
SECTION("a user-defined SAX consumer is unaffected by the internal DOM reserve optimization")
{
// the reserve() call is local to json_sax_dom_parser / json_sax_dom_callback_parser;
// a custom SAX consumer that does not touch a DOM array sees identical events
json j = json::array();
for (int i = 0; i < 100; ++i)
{
j.push_back(i);
}
const auto packed = json::to_cbor(j);
SaxCountdown scp(1000000); // large enough to never trigger an abort
CHECK(json::sax_parse(packed, &scp, json::input_format_t::cbor));
}
}
TEST_CASE("CBOR nesting does not consume the call stack") TEST_CASE("CBOR nesting does not consume the call stack")
{ {
// Containers used to be read by calling back into the value reader once // Containers used to be read by calling back into the value reader once
-34
View File
@@ -1792,40 +1792,6 @@ TEST_CASE("std::filesystem::path")
} }
#endif #endif
// the ADL to_json overload for std::u8string only exists under the same guard
// as std::filesystem::path support (it is otherwise only reached indirectly,
// via std::filesystem::path::u8string()) -- mirror both #if conditions from
// include/nlohmann/detail/conversions/to_json.hpp exactly
#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM
#if defined(__cpp_lib_char8_t)
TEST_CASE("std::u8string")
{
SECTION("ascii")
{
const std::u8string s = u8"Path";
json const j = s;
CHECK(j.template get<std::string>() == "Path");
}
SECTION("utf-8")
{
// use \u universal-character-names (rather than raw \x byte escapes
// or literal non-ASCII source bytes) to compose the multi-byte UTF-8
// encoding -- MSVC treats \x escapes used that way inside a u8
// literal as a nonstandard extension (warning C5321), which some of
// our CI configs promote to an error; \u is portable and produces
// the exact same encoded bytes without depending on the source
// file's encoding
const std::u8string s = u8"P\u011B\u0161ina";
json const j = s;
CHECK(j.template get<std::string>() == "P\xc4\x9b\xc5\xa1ina");
}
}
#endif
#endif
TEST_CASE("std::optional") TEST_CASE("std::optional")
{ {
SECTION("null") SECTION("null")
-96
View File
@@ -672,102 +672,6 @@ TEST_CASE("JSON patch")
} }
} }
SECTION("patch_inplace")
{
SECTION("happy path: patch_inplace mirrors patch() on success")
{
// mirrors "A.5. Replacing a Value" above, but applies the patch with
// patch_inplace() to a mutable copy instead of using patch()'s
// returned copy
json doc = R"(
{
"baz": "qux",
"foo": "bar"
}
)"_json;
json const patch = R"(
[
{ "op": "replace", "path": "/baz", "value": "boo" }
]
)"_json;
json const expected = R"(
{
"baz": "boo",
"foo": "bar"
}
)"_json;
doc.patch_inplace(patch);
CHECK(doc == expected);
}
// this test relies on the "test" operation actually throwing so the
// partial-application state can be observed right after the throw
// point; under JSON_NOEXCEPTION, JSON_THROW() calls std::abort()
// instead (there is no C++ exception to throw), and doctest's
// CHECK_THROWS_AS() is compiled out to a no-op that never even
// invokes the given expression (see doctest's "--no-throw" test
// filter, which ci_test_noexceptions passes) -- so patch()/
// patch_inplace() would never be called at all and the follow-up
// state assertions below would fail against the untouched original
#if !defined(JSON_NOEXCEPTION)
SECTION("distinguishing contract vs patch(): partial application on failure")
{
// Unlike patch(), which is all-or-nothing because it applies the
// patch to an internal copy that is simply discarded when an
// exception is thrown (leaving the original untouched no matter
// what), patch_inplace() mutates the document it is called on
// directly and immediately, operation by operation. So if a JSON
// Patch fails partway through, whatever operations already
// succeeded remain applied -- the document is left in a partially
// patched state. This is empirically verified current behavior,
// not just documented intent, and is pinned here as such.
json const original = R"(
{
"baz": "qux",
"foo": "bar"
}
)"_json;
// the first operation ("replace") succeeds; the second ("test")
// fails because the value at "/baz" no longer (and never did)
// equal "not boo"
json const patch = R"(
[
{ "op": "replace", "path": "/baz", "value": "boo" },
{ "op": "test", "path": "/baz", "value": "not boo" }
]
)"_json;
// patch() never modifies the object it is called on -- it always
// operates on (and returns) a separate copy, so the original is
// left completely untouched, regardless of success or failure.
// copy_for_patch is intentionally a real copy, not a reference
// to `original`: the whole point of this check is to catch a
// hypothetical future regression where patch() *does* mutate its
// receiver. Using a reference here would make the assertion
// below compare `original` to itself -- trivially true even if
// such a bug existed -- which is exactly what a static analyzer
// can't see when it suggests "this copy is never modified, use
// a reference instead".
json copy_for_patch = original; // NOLINT(performance-unnecessary-copy-initialization)
CHECK_THROWS_AS(copy_for_patch.patch(patch), json::other_error&);
CHECK(copy_for_patch == original);
// patch_inplace(), in contrast, already applied the successful
// "replace" operation to the document before the "test" operation
// threw -- that change is not rolled back
json doc = original;
CHECK_THROWS_AS(doc.patch_inplace(patch), json::other_error&);
CHECK(doc != original);
CHECK(doc.at("baz") == "boo");
CHECK(doc.at("foo") == "bar");
}
#endif // !defined(JSON_NOEXCEPTION)
}
SECTION("errors") SECTION("errors")
{ {
SECTION("unknown operation") SECTION("unknown operation")
+85
View File
@@ -1597,6 +1597,91 @@ TEST_CASE("MessagePack")
} }
} }
TEST_CASE("issue #5405 - array reserve for definite-length MessagePack arrays")
{
#if !defined(JSON_NOEXCEPTION)
// this SECTION relies on catching a thrown exception to distinguish
// which of two acceptable, bounded rejections a hostile header took;
// under JSON_NOEXCEPTION, JSON_THROW never produces a catchable C++
// exception (it aborts instead), so this cannot be tested that way here
SECTION("a huge claimed length with no element data must not over-allocate")
{
// 0xdd: array 32 (four-byte length); claims 0xFFFFFFFF (4294967295)
// elements but provides none. max_size() for a std::vector is far
// larger than this count, so it does not reject the header outright;
// the (capped) reservation must not attempt to allocate space for
// billions of elements before the missing data is detected.
json _;
const std::vector<uint8_t> input = {0xdd, 0xFF, 0xFF, 0xFF, 0xFF};
// On a platform where std::size_t is narrower than 64 bits (e.g.
// 32-bit), the claimed count 0xFFFFFFFF coincides with that
// platform's SIZE_MAX, which some size-narrowing checks treat the
// same as detail::unknown_size(); it may then be rejected before
// the SAX consumer's own max_size() check (out_of_range.408) rather
// than being accepted and only found short of data once the
// (capped) reservation looks for element bytes that were never
// provided (parse_error.110). Either is an acceptable, bounded
// rejection of the hostile header -- the property under test is
// that no path attempts to allocate space for billions of elements.
bool threw = false;
try
{
_ = json::from_msgpack(input);
}
catch (const json::parse_error& e)
{
threw = true;
CHECK(e.id == 110);
CHECK(std::string(e.what()) == "[json.exception.parse_error.110] parse error at byte 6: syntax error while parsing MessagePack value: unexpected end of input");
}
catch (const json::out_of_range& e)
{
threw = true;
CHECK(e.id == 408);
CHECK(std::string(e.what()).find("excessive") != std::string::npos);
}
CHECK(threw);
CHECK(json::from_msgpack(input, true, false).is_discarded());
}
#endif
SECTION("arrays of various sizes decode to the same value as before the reserve optimization")
{
for (const auto size :
{
std::size_t{0}, std::size_t{1}, std::size_t{5}, // small
std::size_t{16384}, // exactly at the reserve cap
std::size_t{20000} // above the reserve cap
})
{
CAPTURE(size)
json j = json::array();
for (std::size_t i = 0; i < size; ++i)
{
j.push_back(static_cast<int>(i % 1000));
}
const auto packed = json::to_msgpack(j);
CHECK(json::from_msgpack(packed) == j);
}
}
SECTION("a user-defined SAX consumer is unaffected by the internal DOM reserve optimization")
{
// the reserve() call is local to json_sax_dom_parser / json_sax_dom_callback_parser;
// a custom SAX consumer that does not touch a DOM array sees identical events
json j = json::array();
for (int i = 0; i < 100; ++i)
{
j.push_back(i);
}
const auto packed = json::to_msgpack(j);
SaxCountdown scp(1000000); // large enough to never trigger an abort
CHECK(json::sax_parse(packed, &scp, json::input_format_t::msgpack));
}
}
// use this testcase outside [hide] to run it with Valgrind // use this testcase outside [hide] to run it with Valgrind
TEST_CASE("MessagePack nesting does not consume the call stack") TEST_CASE("MessagePack nesting does not consume the call stack")
{ {
@@ -70,7 +70,7 @@ TEST_CASE("check_for_mem_leak_on_adl_to_json-2")
} }
} }
TEST_CASE("check_for_mem_leak_on_adl_to_json-3") TEST_CASE("check_for_mem_leak_on_adl_to_json-2")
{ {
try try
{ {
@@ -1,91 +0,0 @@
// __ _____ _____ _____
// __| | __| | | | 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
// This translation unit is a dedicated, small compile-and-run check for two
// configuration macros that (per #5423) were never exercised anywhere in the
// test matrix:
// - JSON_NO_IO, which removes the library's <istream>/<ostream> support
// (operator<<, operator>>, and the stream-based overloads of dump()/parse())
// - the JSON_THROW_USER / JSON_TRY_USER / JSON_CATCH_USER trio, which lets a
// user replace the library's internal exception handling
//
// Both macros are about excluding/replacing a facility the library would
// otherwise pull in on its own, and defining one has no bearing on the other,
// so -- to keep the test matrix small -- they are exercised together in a
// single dedicated file instead of two.
//
// JSON_NO_IO requires this file itself to never rely on <iostream>/<sstream>;
// only string-based parsing/dumping is used below.
#define JSON_NO_IO 1
// The user-supplied exception macros below are a *conforming* replacement:
// they simply forward to the real throw/try/catch keywords (via a counter so
// the test can assert each macro was actually invoked, not just defined), so
// every exception-related behavior the library relies on internally --
// including rethrowing std::out_of_range as json::out_of_range in at() --
// keeps working exactly as it would with the library's own default macros.
static int json_throw_user_call_count = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#define JSON_THROW_USER(exception) do { ++json_throw_user_call_count; throw (exception); } while (false) // NOLINT(cppcoreguidelines-macro-usage)
#define JSON_TRY_USER try // NOLINT(cppcoreguidelines-macro-usage)
#define JSON_CATCH_USER(exception) catch (exception) // NOLINT(cppcoreguidelines-macro-usage)
#include "doctest_compatibility.h"
#include <nlohmann/json.hpp>
using json = nlohmann::json;
TEST_CASE("JSON_NO_IO")
{
// everything that does not touch <istream>/<ostream> must keep working:
// parsing from and dumping to std::string
const json j = json::parse(R"({"a":[1,2,3],"b":true})");
CHECK(j.dump() == R"({"a":[1,2,3],"b":true})");
CHECK(j.at("a").size() == 3);
CHECK(j.at("b").get<bool>() == true);
}
// this test relies on CHECK_THROWS_AS() actually invoking the guarded
// expression so json_throw_user_call_count gets bumped and can be observed
// afterwards; doctest's "--no-throw" test filter (which ci_test_noexceptions
// passes, together with a global -DJSON_NOEXCEPTION added to CMAKE_CXX_FLAGS
// for every translation unit in that build, this file included) compiles
// CHECK_THROWS_AS() out to a no-op that never even invokes the given
// expression -- so json::parse()/at() below would never be called at all and
// the call-count assertions would fail even though our JSON_THROW_USER
// override (which always really throws, regardless of JSON_NOEXCEPTION) would
// have worked fine on its own
#if !defined(JSON_NOEXCEPTION)
TEST_CASE("JSON_THROW_USER, JSON_TRY_USER, JSON_CATCH_USER")
{
json_throw_user_call_count = 0;
// json::parse() is [[nodiscard]] (JSON_HEDLEY_WARN_UNUSED_RESULT); under
// GCC in C++11 mode that expands to __attribute__((warn_unused_result)),
// which -- unlike a [[nodiscard]] attribute proper -- GCC does not
// consider satisfied by doctest's CHECK_THROWS_AS() wrapping the
// expression in a (void) cast, so the discarded return value would still
// be flagged under -Werror=unused-result; assign it to discard it instead,
// matching the established `json _ = json::parse(...)` pattern used
// elsewhere in the test suite (see unit-class_parser.cpp)
json _; // NOLINT(readability-identifier-naming)
// a parse error goes through JSON_THROW directly, i.e., through our
// JSON_THROW_USER override
CHECK_THROWS_AS(_ = json::parse("this is not JSON"), json::parse_error&);
CHECK(json_throw_user_call_count > 0);
// at() on an out-of-range array index internally catches std::out_of_range
// (JSON_TRY_USER/JSON_CATCH_USER) and rethrows it as json::out_of_range
// (JSON_THROW_USER again), so this exercises all three macros together
const int count_before = json_throw_user_call_count;
const json arr = json::array({1, 2, 3});
CHECK_THROWS_AS(arr.at(10), json::out_of_range&);
CHECK(json_throw_user_call_count > count_before);
}
#endif
-10
View File
@@ -18,14 +18,6 @@
// for some reason including this after the json header leads to linker errors with VS 2017... // for some reason including this after the json header leads to linker errors with VS 2017...
#include <locale> #include <locale>
// skip tests if JSON_DisableEnumSerialization=ON (#4384): std::byte is a
// scoped enum, so get<std::byte>() (needed below to get<std::vector<std::byte>>()
// from a plain JSON array, not just from an already-binary value) relies on
// enum serialization being enabled
#if defined(JSON_DISABLE_ENUM_SERIALIZATION) && (JSON_DISABLE_ENUM_SERIALIZATION == 1)
#define SKIP_TESTS_FOR_ENUM_SERIALIZATION
#endif
#define JSON_TESTS_PRIVATE #define JSON_TESTS_PRIVATE
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
using json = nlohmann::json; using json = nlohmann::json;
@@ -473,7 +465,6 @@ TEST_CASE("regression tests 3")
CHECK((decoded == json_4804::array())); CHECK((decoded == json_4804::array()));
} }
#ifndef SKIP_TESTS_FOR_ENUM_SERIALIZATION
SECTION("discussion #4209 - custom BinaryType direct assignment and round-tripping") SECTION("discussion #4209 - custom BinaryType direct assignment and round-tripping")
{ {
// Test that assigning a custom BinaryType directly creates a binary value, not an array // Test that assigning a custom BinaryType directly creates a binary value, not an array
@@ -507,7 +498,6 @@ TEST_CASE("regression tests 3")
CHECK(extracted[1] == std::byte{2}); CHECK(extracted[1] == std::byte{2});
CHECK(extracted[2] == std::byte{3}); CHECK(extracted[2] == std::byte{3});
} }
#endif
SECTION("issue #5046 - implicit conversion of return json to std::optional no longer implicit") SECTION("issue #5046 - implicit conversion of return json to std::optional no longer implicit")
{ {
-17
View File
@@ -52,23 +52,6 @@ TEST_CASE("std::formatter<nlohmann::json>")
CHECK(std::format("{:2}", j) == j.dump(2)); CHECK(std::format("{:2}", j) == j.dump(2));
CHECK(std::format("{:#2}", j) == j.dump(2)); CHECK(std::format("{:#2}", j) == j.dump(2));
CHECK(std::format("{:8}", j) == j.dump(8)); CHECK(std::format("{:8}", j) == j.dump(8));
// multi-digit widths must accumulate every digit, not just the first
CHECK(std::format("{:12}", j) == j.dump(12));
CHECK(std::format("{:#12}", j) == j.dump(12));
CHECK(std::format("{:10}", j) == j.dump(10));
}
SECTION("bare alignment with no fill character defaults to a space indent character")
{
const json j = {{"foo", 1}, {"bar", {1, 2, 3}}};
// without a preceding fill character, the alignment character itself must not
// be mistaken for the indent character -- the default space is kept
CHECK(std::format("{:<}", j) == j.dump());
CHECK(std::format("{:>}", j) == j.dump());
CHECK(std::format("{:^}", j) == j.dump());
CHECK(std::format("{:<3}", j) == j.dump(3, ' '));
CHECK(std::format("{:>3}", j) == j.dump(3, ' '));
CHECK(std::format("{:^3}", j) == j.dump(3, ' '));
} }
SECTION("fill-and-align sets the indent character, like dump(indent, indent_char)") SECTION("fill-and-align sets the indent character, like dump(indent, indent_char)")
+105
View File
@@ -2149,6 +2149,111 @@ TEST_CASE("UBJSON")
} }
} }
TEST_CASE("issue #5405 - array reserve for definite-length UBJSON arrays")
{
#if !defined(JSON_NOEXCEPTION)
// this SECTION relies on catching a thrown exception to distinguish
// which of two acceptable, bounded rejections a hostile header took;
// under JSON_NOEXCEPTION, JSON_THROW never produces a catchable C++
// exception (it aborts instead), so this cannot be tested that way here
SECTION("a huge claimed length with no element data must not over-allocate")
{
// optimized form [$type#count: type 'i' (int8), count as a four-byte
// 'l' (int32) of 0x7FFFFFFF (2147483647), but no element data at all.
// max_size() for a std::vector is far larger than this count, so it
// does not reject the header outright; the (capped) reservation must
// not attempt to allocate space for billions of elements before the
// missing data is detected.
json _;
const std::vector<uint8_t> input = {'[', '$', 'i', '#', 'l', 0x7F, 0xFF, 0xFF, 0xFF};
// On a platform where std::vector<json>::max_size() is smaller than
// the claimed count (e.g. 32-bit, where max_size() is bounded by a
// 32-bit SIZE_MAX divided by sizeof(json)), the SAX consumer's own
// check rejects the header outright (out_of_range.408, with the
// claimed count in the message) instead of accepting it and only
// finding it short of data once the (capped) reservation looks for
// element bytes that were never provided (parse_error.110). Either
// is an acceptable, bounded rejection of the hostile header -- the
// property under test is that no path attempts to allocate space
// for billions of elements.
bool threw = false;
try
{
_ = json::from_ubjson(input);
}
catch (const json::parse_error& e)
{
threw = true;
CHECK(e.id == 110);
CHECK(std::string(e.what()) == "[json.exception.parse_error.110] parse error at byte 10: syntax error while parsing UBJSON number: unexpected end of input");
}
catch (const json::out_of_range& e)
{
threw = true;
CHECK(e.id == 408);
CHECK(std::string(e.what()).find("excessive array size") != std::string::npos);
}
CHECK(threw);
// json_sax_dom_parser::start_array()'s max_size() check (unlike the
// scanner's own parse_error path) throws unconditionally via
// JSON_THROW rather than going through sax->parse_error(), so it is
// not gated by allow_exceptions=false on a platform where this
// header hits that check (e.g. 32-bit, see above) -- allow either
// a discarded result or the same out_of_range it throws with
// exceptions enabled.
try
{
CHECK(json::from_ubjson(input, true, false).is_discarded());
}
catch (const json::out_of_range& e)
{
CHECK(e.id == 408);
}
}
#endif
SECTION("arrays of various sizes decode to the same value as before the reserve optimization")
{
for (const auto size :
{
std::size_t{0}, std::size_t{1}, std::size_t{5}, // small
std::size_t{16384}, // exactly at the reserve cap
std::size_t{20000} // above the reserve cap
})
{
CAPTURE(size)
json j = json::array();
for (std::size_t i = 0; i < size; ++i)
{
j.push_back(static_cast<int>(i % 1000));
}
// exercise both the plain and the optimized [$type#count encoding
const auto packed_plain = json::to_ubjson(j);
CHECK(json::from_ubjson(packed_plain) == j);
const auto packed_optimized = json::to_ubjson(j, true, true);
CHECK(json::from_ubjson(packed_optimized) == j);
}
}
SECTION("a user-defined SAX consumer is unaffected by the internal DOM reserve optimization")
{
// the reserve() call is local to json_sax_dom_parser / json_sax_dom_callback_parser;
// a custom SAX consumer that does not touch a DOM array sees identical events
json j = json::array();
for (int i = 0; i < 100; ++i)
{
j.push_back(i);
}
const auto packed = json::to_ubjson(j, true, true);
SaxCountdown scp(1000000); // large enough to never trigger an abort
CHECK(json::sax_parse(packed, &scp, json::input_format_t::ubjson));
}
}
TEST_CASE("UBJSON nesting does not consume the call stack") TEST_CASE("UBJSON nesting does not consume the call stack")
{ {
// Containers used to be read by calling back into the value reader once // Containers used to be read by calling back into the value reader once