mirror of
https://github.com/nlohmann/json.git
synced 2026-09-29 05:25:43 +00:00
* Hash deeply nested values without recursing per nesting level std::hash<basic_json> hashed an array or object by hashing each element, which called detail::hash again once per nesting level. A value nested deeply enough - 50,000 levels of objects on an 8 MiB stack - exhausted the call stack and terminated the process. parse() accepts such values without complaint, since the parser is iterative, and a parsed value is hashed wherever it is used as a key in an unordered container. Bound the descent the same way dump() does: detail::hash takes the nesting level, and once hash_depth_limit() (128) levels have been entered, hash_iteratively() hashes what is left on an explicit stack. It combines the seeds in exactly the same order, so hash values are unchanged. A value nested less deeply than the bound is hashed by the same code as before, without allocating, and is as fast as before. Tests check that every depth up to twice the bound hashes exactly like the recursive definition of the hash, and that values nested 100,000 levels deep hash without crashing. Fixes #5545 for std::hash. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Declare hash_frame's constructor noexcept GCC's -Wnoexcept (an error in CI) flags the emplace_back() into the hash stack under C++26: the constructor cannot throw, since cbegin() is noexcept, but it did not say so. dump_frame's constructor is noexcept for the same reason. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Share one recursion depth limit, and copy the hash frame out of the stack dump() and hash() each defined their own limit on how many nesting levels they recurse into, and the operations still to come would have added more, free to diverge over time. They now all use detail::recursion_depth_limit(), in a header of its own; serializer::dump_depth_limit() and hash_depth_limit() are gone. hash_iteratively() now copies the frame it works on out of the stack and changes the frame only through stack.back(), so nothing can refer into the stack after entering an element has grown it. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Parenthesize multiplications in the hash test for clang-tidy Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me>
228 lines
7.4 KiB
C++
228 lines
7.4 KiB
C++
// __ _____ _____ _____
|
|
// __| | __| | | | JSON for Modern C++
|
|
// | | |__ | | | | | | version 3.12.0
|
|
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
|
//
|
|
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#pragma once
|
|
|
|
#include <cstdint> // uint8_t
|
|
#include <cstddef> // size_t
|
|
#include <functional> // hash
|
|
#include <vector> // vector
|
|
|
|
#include <nlohmann/detail/abi_macros.hpp>
|
|
#include <nlohmann/detail/recursion_depth_limit.hpp>
|
|
#include <nlohmann/detail/value_t.hpp>
|
|
|
|
NLOHMANN_JSON_NAMESPACE_BEGIN
|
|
namespace detail
|
|
{
|
|
|
|
// boost::hash_combine
|
|
inline std::size_t combine(std::size_t seed, std::size_t h) noexcept
|
|
{
|
|
seed ^= h + 0x9e3779b9 + (seed << 6U) + (seed >> 2U);
|
|
return seed;
|
|
}
|
|
|
|
template<typename BasicJsonType>
|
|
std::size_t hash_iteratively(const BasicJsonType& j);
|
|
|
|
/*!
|
|
@brief hash a JSON value
|
|
|
|
The hash function tries to rely on std::hash where possible. Furthermore, the
|
|
type of the JSON value is taken into account to have different hash values for
|
|
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 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.
|
|
|
|
@tparam BasicJsonType basic_json specialization
|
|
@param j JSON value to hash
|
|
@param depth nesting level of @a j, counted from the value passed by the caller
|
|
@return hash value of j
|
|
*/
|
|
template<typename BasicJsonType>
|
|
std::size_t hash(const BasicJsonType& j, const std::size_t depth = 0)
|
|
{
|
|
using string_t = typename BasicJsonType::string_t;
|
|
using number_integer_t = typename BasicJsonType::number_integer_t;
|
|
using number_unsigned_t = typename BasicJsonType::number_unsigned_t;
|
|
using number_float_t = typename BasicJsonType::number_float_t;
|
|
|
|
const auto type = static_cast<std::size_t>(j.type());
|
|
switch (j.type())
|
|
{
|
|
case BasicJsonType::value_t::null:
|
|
case BasicJsonType::value_t::discarded:
|
|
{
|
|
return combine(type, 0);
|
|
}
|
|
|
|
case BasicJsonType::value_t::object:
|
|
{
|
|
if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit()))
|
|
{
|
|
return hash_iteratively(j);
|
|
}
|
|
|
|
auto seed = combine(type, j.size());
|
|
for (const auto& element : j.items())
|
|
{
|
|
const auto h = std::hash<string_t> {}(element.key());
|
|
seed = combine(seed, h);
|
|
seed = combine(seed, hash(element.value(), depth + 1));
|
|
}
|
|
return seed;
|
|
}
|
|
|
|
case BasicJsonType::value_t::array:
|
|
{
|
|
if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit()))
|
|
{
|
|
return hash_iteratively(j);
|
|
}
|
|
|
|
auto seed = combine(type, j.size());
|
|
for (const auto& element : j)
|
|
{
|
|
seed = combine(seed, hash(element, depth + 1));
|
|
}
|
|
return seed;
|
|
}
|
|
|
|
case BasicJsonType::value_t::string:
|
|
{
|
|
const auto h = std::hash<string_t> {}(j.template get_ref<const string_t&>());
|
|
return combine(type, h);
|
|
}
|
|
|
|
case BasicJsonType::value_t::boolean:
|
|
{
|
|
const auto h = std::hash<bool> {}(j.template get<bool>());
|
|
return combine(type, h);
|
|
}
|
|
|
|
case BasicJsonType::value_t::number_integer:
|
|
{
|
|
const auto h = std::hash<number_integer_t> {}(j.template get<number_integer_t>());
|
|
return combine(type, h);
|
|
}
|
|
|
|
case BasicJsonType::value_t::number_unsigned:
|
|
{
|
|
const auto h = std::hash<number_unsigned_t> {}(j.template get<number_unsigned_t>());
|
|
return combine(type, h);
|
|
}
|
|
|
|
case BasicJsonType::value_t::number_float:
|
|
{
|
|
const auto h = std::hash<number_float_t> {}(j.template get<number_float_t>());
|
|
return combine(type, h);
|
|
}
|
|
|
|
case BasicJsonType::value_t::binary:
|
|
{
|
|
auto seed = combine(type, j.get_binary().size());
|
|
const auto h = std::hash<bool> {}(j.get_binary().has_subtype());
|
|
seed = combine(seed, h);
|
|
seed = combine(seed, static_cast<std::size_t>(j.get_binary().subtype()));
|
|
for (const auto byte : j.get_binary())
|
|
{
|
|
// the cast is needed for binary types whose value type is not
|
|
// an integer (e.g., std::byte)
|
|
seed = combine(seed, std::hash<std::uint8_t> {}(static_cast<std::uint8_t>(byte)));
|
|
}
|
|
return seed;
|
|
}
|
|
|
|
default: // LCOV_EXCL_LINE
|
|
JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE
|
|
return 0; // LCOV_EXCL_LINE
|
|
}
|
|
}
|
|
|
|
/// an array or object whose elements @ref hash_iteratively is hashing
|
|
template<typename BasicJsonType>
|
|
struct hash_frame
|
|
{
|
|
hash_frame(const BasicJsonType* value_, std::size_t seed_) noexcept
|
|
: value(value_), position(value_->cbegin()), seed(seed_)
|
|
{}
|
|
|
|
const BasicJsonType* value;
|
|
typename BasicJsonType::const_iterator position;
|
|
std::size_t seed;
|
|
};
|
|
|
|
/*!
|
|
@brief hash the array or object @a j without the call stack
|
|
|
|
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 recursion_depth_limit.
|
|
|
|
@tparam BasicJsonType basic_json specialization
|
|
@param j array or object to hash
|
|
@return hash value of j
|
|
*/
|
|
template<typename BasicJsonType>
|
|
std::size_t hash_iteratively(const BasicJsonType& j)
|
|
{
|
|
using string_t = typename BasicJsonType::string_t;
|
|
|
|
std::vector<hash_frame<BasicJsonType>> stack;
|
|
stack.emplace_back(&j, combine(static_cast<std::size_t>(j.type()), j.size()));
|
|
|
|
while (true)
|
|
{
|
|
// a copy, as entering an element below can reallocate the stack; the
|
|
// frame itself is only changed through stack.back()
|
|
const hash_frame<BasicJsonType> frame = stack.back();
|
|
|
|
if (frame.position == frame.value->cend())
|
|
{
|
|
// all elements are hashed: fold this value's hash into its parent's
|
|
// seed, exactly where the recursive version returns it
|
|
const std::size_t h = frame.seed;
|
|
stack.pop_back();
|
|
if (stack.empty())
|
|
{
|
|
return h;
|
|
}
|
|
stack.back().seed = combine(stack.back().seed, h);
|
|
continue;
|
|
}
|
|
|
|
if (frame.value->is_object())
|
|
{
|
|
stack.back().seed = combine(stack.back().seed, std::hash<string_t> {}(frame.position.key()));
|
|
}
|
|
|
|
// advance before entering the element, which pushes onto the stack
|
|
const BasicJsonType& element = *frame.position;
|
|
++stack.back().position;
|
|
|
|
if (element.is_structured())
|
|
{
|
|
stack.emplace_back(&element, combine(static_cast<std::size_t>(element.type()), element.size()));
|
|
}
|
|
else
|
|
{
|
|
stack.back().seed = combine(stack.back().seed, hash(element));
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace detail
|
|
NLOHMANN_JSON_NAMESPACE_END
|