mirror of
https://github.com/nlohmann/json.git
synced 2026-10-07 15:07:13 +00:00
- Say "may hash differently" for null, false, and numbers, since a collision across types is possible. - Name the storage types (signed integer, unsigned integer, floating-point number) instead of example literals. - Explain that the hash survives converting an integer to number_float_t but not the lossy conversion back, and that unequal numbers may share a hash. - State that the example hash values are illustrative only and vary by platform, compiler, compiler version, and library version. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
231 lines
8.1 KiB
C++
231 lines
8.1 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, so null, false, and numbers may
|
|
hash differently from each other, but any two numbers that compare equal
|
|
under operator== hash equally regardless of which of number_integer,
|
|
number_unsigned, or number_float actually holds the value.
|
|
|
|
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_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:
|
|
case BasicJsonType::value_t::number_unsigned:
|
|
case BasicJsonType::value_t::number_float:
|
|
{
|
|
// operator== compares numbers by their mathematical value across
|
|
// number_integer, number_unsigned, and number_float, so equal
|
|
// numbers of different internal types (0, 0U, 0.0) must hash the
|
|
// same. Two equal numbers have the same value, which converts to
|
|
// the same number_float_t, so all numbers share one type tag and
|
|
// hash that converted value. Adding zero turns -0.0 (equal to 0)
|
|
// into 0.0, as std::hash need not map both to the same hash.
|
|
// The converse does not hold: converting a number_float_t value
|
|
// to an integer type is lossy, so the result can hash
|
|
// differently, and unequal numbers that convert to the same
|
|
// number_float_t (e.g., 2^53 and 2^53 + 1) share a hash.
|
|
const auto number_type = static_cast<std::size_t>(BasicJsonType::value_t::number_float);
|
|
const auto value = j.template get<number_float_t>() + static_cast<number_float_t>(0);
|
|
const auto h = std::hash<number_float_t> {}(value);
|
|
return combine(number_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
|