From ee211df64afc510e06646ac1d6b6c9fa8e7d3dc7 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sun, 13 Sep 2026 18:02:01 +0200 Subject: [PATCH] Only reserve array capacity if the array type supports it PR #5476 added an unconditional `reserve()` call to the `start_array()` implementations of both `json_sax_dom_parser` and `json_sax_dom_callback_parser`. `ArrayType` is a template parameter of `basic_json`, however, and is not required to have a `reserve()` member function: instantiating either parser for, e.g., `basic_json` fails to compile, which breaks every use of `parse()` and the binary readers for such a type. Move the capped reservation into a `reserve_array()` helper that selects a no-op overload through `priority_tag` when `ArrayType` has no `reserve()`, mirroring `from_json_object_reserve()`. `std::vector` array types keep reserving as before, and the 16384-element cap is unchanged. Signed-off-by: Niels Lohmann Co-Authored-By: Claude Opus 5 --- include/nlohmann/detail/input/json_sax.hpp | 38 ++++++++++++------ single_include/nlohmann/json.hpp | 39 ++++++++++++------ tests/src/unit-regression3.cpp | 46 ++++++++++++++++++++++ 3 files changed, 99 insertions(+), 24 deletions(-) diff --git a/include/nlohmann/detail/input/json_sax.hpp b/include/nlohmann/detail/input/json_sax.hpp index 60c468f30..37d0ab270 100644 --- a/include/nlohmann/detail/input/json_sax.hpp +++ b/include/nlohmann/detail/input/json_sax.hpp @@ -18,6 +18,7 @@ #include #include #include +#include #include NLOHMANN_JSON_NAMESPACE_BEGIN @@ -151,6 +152,29 @@ constexpr std::size_t unknown_size() return (std::numeric_limits::max)(); } +/*! +@brief reserve capacity for @a len elements in array @a arr + +Reserving upfront avoids repeated reallocations while the elements are added, +but the reservation is capped 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. + +The overload below is selected for array types without reserve() (e.g., +std::deque), which are then left untouched. +*/ +template +auto reserve_array(ArrayType& arr, std::size_t len, priority_tag<1> /*unused*/) +-> decltype(arr.reserve(len), void()) +{ + constexpr std::size_t reserve_cap = 16384; + arr.reserve((std::min)(len, reserve_cap)); +} + +template +inline void reserve_array(ArrayType& /*arr*/, std::size_t /*len*/, priority_tag<0> /*unused*/) +{} + /*! @brief SAX implementation to create a JSON value from SAX events @@ -308,12 +332,7 @@ class json_sax_dom_parser 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)); + reserve_array(*ref_stack.back()->m_data.m_value.array, len, priority_tag<1> {}); } return true; @@ -697,12 +716,7 @@ class json_sax_dom_callback_parser 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)); + reserve_array(*ref_stack.back()->m_data.m_value.array, len, priority_tag<1> {}); } } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 85ebca27b..2bb7a5b7b 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -10730,6 +10730,8 @@ NLOHMANN_JSON_NAMESPACE_END // #include +// #include + // #include NLOHMANN_JSON_NAMESPACE_BEGIN @@ -10864,6 +10866,29 @@ constexpr std::size_t unknown_size() return (std::numeric_limits::max)(); } +/*! +@brief reserve capacity for @a len elements in array @a arr + +Reserving upfront avoids repeated reallocations while the elements are added, +but the reservation is capped 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. + +The overload below is selected for array types without reserve() (e.g., +std::deque), which are then left untouched. +*/ +template +auto reserve_array(ArrayType& arr, std::size_t len, priority_tag<1> /*unused*/) +-> decltype(arr.reserve(len), void()) +{ + constexpr std::size_t reserve_cap = 16384; + arr.reserve((std::min)(len, reserve_cap)); +} + +template +inline void reserve_array(ArrayType& /*arr*/, std::size_t /*len*/, priority_tag<0> /*unused*/) +{} + /*! @brief SAX implementation to create a JSON value from SAX events @@ -11021,12 +11046,7 @@ class json_sax_dom_parser 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)); + reserve_array(*ref_stack.back()->m_data.m_value.array, len, priority_tag<1> {}); } return true; @@ -11410,12 +11430,7 @@ class json_sax_dom_callback_parser 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)); + reserve_array(*ref_stack.back()->m_data.m_value.array, len, priority_tag<1> {}); } } diff --git a/tests/src/unit-regression3.cpp b/tests/src/unit-regression3.cpp index cb2ed59a6..2f41789fc 100644 --- a/tests/src/unit-regression3.cpp +++ b/tests/src/unit-regression3.cpp @@ -27,6 +27,7 @@ using ordered_json = nlohmann::ordered_json; #endif #include +#include #include #include #include @@ -896,4 +897,49 @@ TEST_CASE("issue #5402 - update(merge_objects=true) overwrites a primitive with } +TEST_CASE("regression test #5476 - array type without reserve()") +{ + // the capacity reserved for definite-length arrays must not require the + // array type to have a reserve() member function + using deque_json = nlohmann::basic_json; + + SECTION("std::deque") + { + const auto j = deque_json::parse(R"({"a":[1,[2,3]],"b":[]})"); + CHECK(j.dump() == R"({"a":[1,[2,3]],"b":[]})"); + + // the binary formats pass a definite length to start_array() + CHECK(deque_json::from_cbor(deque_json::to_cbor(j)) == j); + CHECK(deque_json::from_msgpack(deque_json::to_msgpack(j)) == j); + + // parse() instantiates the callback parser as well, which reserves too + const auto with_callback = deque_json::parse(R"([1,2,3])", [](int /*depth*/, deque_json::parse_event_t /*event*/, deque_json& /*parsed*/) + { + return true; + }); + CHECK(with_callback == deque_json({1, 2, 3})); + } + + SECTION("std::vector still reserves") + { + json array = json::array(); + for (int i = 0; i < 100; ++i) + { + array.push_back(i); + } + + const auto j = json::from_cbor(json::to_cbor(array)); + CHECK(j == array); + CHECK(j.get_ref().capacity() >= 100); + } + + SECTION("the reservation stays capped") + { + // CBOR array announcing 2^32-1 elements, but truncated right after the + // header: the input must be rejected without reserving that capacity + const std::vector truncated = {0x9A, 0xFF, 0xFF, 0xFF, 0xFF}; + CHECK(json::from_cbor(truncated, true, false).is_discarded()); + } +} + DOCTEST_CLANG_SUPPRESS_WARNING_POP