diff --git a/BUILD.bazel b/BUILD.bazel index de0ff7145..1eb49a557 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -55,6 +55,7 @@ cc_library( "include/nlohmann/detail/output/binary_writer.hpp", "include/nlohmann/detail/output/output_adapters.hpp", "include/nlohmann/detail/output/serializer.hpp", + "include/nlohmann/detail/recursion_depth_limit.hpp", "include/nlohmann/detail/string_concat.hpp", "include/nlohmann/detail/string_escape.hpp", "include/nlohmann/detail/string_utils.hpp", diff --git a/include/nlohmann/detail/hash.hpp b/include/nlohmann/detail/hash.hpp index 49177366e..20a971886 100644 --- a/include/nlohmann/detail/hash.hpp +++ b/include/nlohmann/detail/hash.hpp @@ -14,6 +14,7 @@ #include // vector #include +#include #include NLOHMANN_JSON_NAMESPACE_BEGIN @@ -27,13 +28,6 @@ inline std::size_t combine(std::size_t seed, std::size_t h) noexcept return seed; } -/// the number of levels @ref hash descends into before handing over to -/// @ref hash_iteratively -constexpr std::size_t hash_depth_limit() noexcept -{ - return 128; -} - template std::size_t hash_iteratively(const BasicJsonType& j); @@ -47,7 +41,7 @@ null, 0, 0U, and false, etc. Hashing an array or an object hashes its elements, which used to call this function again once per nesting level, so a value nested deeply enough exhausted the call stack and terminated the process. The descent is bounded -here: once @ref hash_depth_limit levels have been entered, @ref +here: once @ref recursion_depth_limit levels have been entered, @ref hash_iteratively hashes what is left without the call stack. A value nested less deeply than that - all but a vanishing minority - is hashed exactly as before, without allocating. @@ -76,7 +70,7 @@ std::size_t hash(const BasicJsonType& j, const std::size_t depth = 0) case BasicJsonType::value_t::object: { - if (JSON_HEDLEY_UNLIKELY(depth >= hash_depth_limit())) + if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit())) { return hash_iteratively(j); } @@ -93,7 +87,7 @@ std::size_t hash(const BasicJsonType& j, const std::size_t depth = 0) case BasicJsonType::value_t::array: { - if (JSON_HEDLEY_UNLIKELY(depth >= hash_depth_limit())) + if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit())) { return hash_iteratively(j); } @@ -175,7 +169,7 @@ struct hash_frame Computes the same value as @ref hash, keeping the arrays and objects it has entered on an explicit stack instead of descending into them. Only reached for -values nested deeper than @ref hash_depth_limit. +values nested deeper than @ref recursion_depth_limit. @tparam BasicJsonType basic_json specialization @param j array or object to hash @@ -191,7 +185,9 @@ std::size_t hash_iteratively(const BasicJsonType& j) while (true) { - hash_frame& frame = stack.back(); + // a copy, as entering an element below can reallocate the stack; the + // frame itself is only changed through stack.back() + const hash_frame frame = stack.back(); if (frame.position == frame.value->cend()) { @@ -209,13 +205,12 @@ std::size_t hash_iteratively(const BasicJsonType& j) if (frame.value->is_object()) { - frame.seed = combine(frame.seed, std::hash {}(frame.position.key())); + stack.back().seed = combine(stack.back().seed, std::hash {}(frame.position.key())); } - // read the element and advance before entering it: entering can - // reallocate the stack and so invalidate `frame` + // advance before entering the element, which pushes onto the stack const BasicJsonType& element = *frame.position; - ++frame.position; + ++stack.back().position; if (element.is_structured()) { @@ -223,7 +218,7 @@ std::size_t hash_iteratively(const BasicJsonType& j) } else { - frame.seed = combine(frame.seed, hash(element)); + stack.back().seed = combine(stack.back().seed, hash(element)); } } } diff --git a/include/nlohmann/detail/output/serializer.hpp b/include/nlohmann/detail/output/serializer.hpp index f9e7f7840..7c38276ce 100644 --- a/include/nlohmann/detail/output/serializer.hpp +++ b/include/nlohmann/detail/output/serializer.hpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -133,7 +134,7 @@ class serializer Serializing a container descends into its elements, so a value nested deeply enough used to exhaust the call stack and terminate the process with no - exception to catch. The descent is bounded here: once @ref dump_depth_limit + exception to catch. The descent is bounded here: once @ref recursion_depth_limit levels have been entered, @ref dump_iteratively writes out what is left without the call stack. A value nested less deeply than that - all but a vanishing minority - is written by exactly the code that always wrote it. @@ -148,7 +149,7 @@ class serializer { case value_t::object: { - if (JSON_HEDLEY_UNLIKELY(depth >= dump_depth_limit())) + if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit())) { dump_iteratively(val, current_indent); return; @@ -223,7 +224,7 @@ class serializer case value_t::array: { - if (JSON_HEDLEY_UNLIKELY(depth >= dump_depth_limit())) + if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit())) { dump_iteratively(val, current_indent); return; @@ -408,19 +409,12 @@ class serializer } private: - /// the number of levels @ref dump_internal descends into before it hands - /// over to @ref dump_iteratively - static constexpr std::size_t dump_depth_limit() - { - return 128; - } - /*! @brief write out @a val and everything below it without the call stack Emits the same bytes as @ref dump_internal, keeping the containers it has entered on an explicit stack instead of descending into them. Only reached - for values nested deeper than @ref dump_depth_limit, which is why it is not + for values nested deeper than @ref recursion_depth_limit, which is why it is not written for speed: walking every value this way measured up to 20% slower on object-heavy documents than letting the compiler drive the descent. */ diff --git a/include/nlohmann/detail/recursion_depth_limit.hpp b/include/nlohmann/detail/recursion_depth_limit.hpp new file mode 100644 index 000000000..fe3bd8026 --- /dev/null +++ b/include/nlohmann/detail/recursion_depth_limit.hpp @@ -0,0 +1,35 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // size_t + +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +/*! +@brief the number of nesting levels an operation recurses into + +Operations that walk a value (serializing, hashing, merging, ...) recurse once +per nesting level, which is fastest, but a value nested deeply enough would +exhaust the call stack. So they recurse only this many levels deep and finish +whatever lies below with an explicit stack. All of them share this limit. + +@sa https://github.com/nlohmann/json/issues/5387 +*/ +constexpr std::size_t recursion_depth_limit() noexcept +{ + return 128; +} + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index a63363fbf..f2fd5d1a7 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -68,6 +68,7 @@ #include #include #include +#include #include #include #include diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 3745c365a..e3d11a2b1 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -6988,6 +6988,44 @@ NLOHMANN_JSON_NAMESPACE_END // #include +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // size_t + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +/*! +@brief the number of nesting levels an operation recurses into + +Operations that walk a value (serializing, hashing, merging, ...) recurse once +per nesting level, which is fastest, but a value nested deeply enough would +exhaust the call stack. So they recurse only this many levels deep and finish +whatever lies below with an explicit stack. All of them share this limit. + +@sa https://github.com/nlohmann/json/issues/5387 +*/ +constexpr std::size_t recursion_depth_limit() noexcept +{ + return 128; +} + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + // #include @@ -7002,13 +7040,6 @@ inline std::size_t combine(std::size_t seed, std::size_t h) noexcept return seed; } -/// the number of levels @ref hash descends into before handing over to -/// @ref hash_iteratively -constexpr std::size_t hash_depth_limit() noexcept -{ - return 128; -} - template std::size_t hash_iteratively(const BasicJsonType& j); @@ -7022,7 +7053,7 @@ null, 0, 0U, and false, etc. Hashing an array or an object hashes its elements, which used to call this function again once per nesting level, so a value nested deeply enough exhausted the call stack and terminated the process. The descent is bounded -here: once @ref hash_depth_limit levels have been entered, @ref +here: once @ref recursion_depth_limit levels have been entered, @ref hash_iteratively hashes what is left without the call stack. A value nested less deeply than that - all but a vanishing minority - is hashed exactly as before, without allocating. @@ -7051,7 +7082,7 @@ std::size_t hash(const BasicJsonType& j, const std::size_t depth = 0) case BasicJsonType::value_t::object: { - if (JSON_HEDLEY_UNLIKELY(depth >= hash_depth_limit())) + if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit())) { return hash_iteratively(j); } @@ -7068,7 +7099,7 @@ std::size_t hash(const BasicJsonType& j, const std::size_t depth = 0) case BasicJsonType::value_t::array: { - if (JSON_HEDLEY_UNLIKELY(depth >= hash_depth_limit())) + if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit())) { return hash_iteratively(j); } @@ -7150,7 +7181,7 @@ struct hash_frame Computes the same value as @ref hash, keeping the arrays and objects it has entered on an explicit stack instead of descending into them. Only reached for -values nested deeper than @ref hash_depth_limit. +values nested deeper than @ref recursion_depth_limit. @tparam BasicJsonType basic_json specialization @param j array or object to hash @@ -7166,7 +7197,9 @@ std::size_t hash_iteratively(const BasicJsonType& j) while (true) { - hash_frame& frame = stack.back(); + // a copy, as entering an element below can reallocate the stack; the + // frame itself is only changed through stack.back() + const hash_frame frame = stack.back(); if (frame.position == frame.value->cend()) { @@ -7184,13 +7217,12 @@ std::size_t hash_iteratively(const BasicJsonType& j) if (frame.value->is_object()) { - frame.seed = combine(frame.seed, std::hash {}(frame.position.key())); + stack.back().seed = combine(stack.back().seed, std::hash {}(frame.position.key())); } - // read the element and advance before entering it: entering can - // reallocate the stack and so invalidate `frame` + // advance before entering the element, which pushes onto the stack const BasicJsonType& element = *frame.position; - ++frame.position; + ++stack.back().position; if (element.is_structured()) { @@ -7198,7 +7230,7 @@ std::size_t hash_iteratively(const BasicJsonType& j) } else { - frame.seed = combine(frame.seed, hash(element)); + stack.back().seed = combine(stack.back().seed, hash(element)); } } } @@ -22297,6 +22329,8 @@ NLOHMANN_JSON_NAMESPACE_END // #include +// #include + // #include // #include @@ -22402,7 +22436,7 @@ class serializer Serializing a container descends into its elements, so a value nested deeply enough used to exhaust the call stack and terminate the process with no - exception to catch. The descent is bounded here: once @ref dump_depth_limit + exception to catch. The descent is bounded here: once @ref recursion_depth_limit levels have been entered, @ref dump_iteratively writes out what is left without the call stack. A value nested less deeply than that - all but a vanishing minority - is written by exactly the code that always wrote it. @@ -22417,7 +22451,7 @@ class serializer { case value_t::object: { - if (JSON_HEDLEY_UNLIKELY(depth >= dump_depth_limit())) + if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit())) { dump_iteratively(val, current_indent); return; @@ -22492,7 +22526,7 @@ class serializer case value_t::array: { - if (JSON_HEDLEY_UNLIKELY(depth >= dump_depth_limit())) + if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit())) { dump_iteratively(val, current_indent); return; @@ -22677,19 +22711,12 @@ class serializer } private: - /// the number of levels @ref dump_internal descends into before it hands - /// over to @ref dump_iteratively - static constexpr std::size_t dump_depth_limit() - { - return 128; - } - /*! @brief write out @a val and everything below it without the call stack Emits the same bytes as @ref dump_internal, keeping the containers it has entered on an explicit stack instead of descending into them. Only reached - for values nested deeper than @ref dump_depth_limit, which is why it is not + for values nested deeper than @ref recursion_depth_limit, which is why it is not written for speed: walking every value this way measured up to 20% slower on object-heavy documents than letting the compiler drive the descent. */ @@ -24002,6 +24029,8 @@ class serializer } // namespace detail NLOHMANN_JSON_NAMESPACE_END +// #include + // #include // #include diff --git a/tests/src/unit-hash.cpp b/tests/src/unit-hash.cpp index 3f2900443..31b9512cd 100644 --- a/tests/src/unit-hash.cpp +++ b/tests/src/unit-hash.cpp @@ -20,7 +20,7 @@ namespace // how detail::hash defines the hash of an array or object: the seeds of the // elements, combined in order. Recursive, so only usable on values nested a // few hundred levels deep - which is exactly what is needed to check that the -// iterative path taken below detail::hash_depth_limit() computes the same. +// iterative path taken below detail::recursion_depth_limit() computes the same. template std::size_t reference_hash(const BasicJsonType& j) { @@ -189,7 +189,7 @@ TEST_CASE("hash of deeply nested values") SECTION("hashing past the descent bound computes the same values") { // every depth on either side of where the iterative path takes over - for (std::size_t depth = 0; depth <= 2 * nlohmann::detail::hash_depth_limit() + 10; ++depth) + for (std::size_t depth = 0; depth <= 2 * nlohmann::detail::recursion_depth_limit() + 10; ++depth) { CAPTURE(depth); const auto arrays = nested(depth, false);