Compare commits

..
Author SHA1 Message Date
Niels Lohmann b41e43fffc Bound diff()'s descent with a depth count instead of scanning the source
Now that operator== no longer recurses (#5390), diff() can keep its per-level
equality shortcut all the way down. It diffs recursively for the first
detail::recursion_depth_limit() levels, as merge_patch() does, and hands
anything deeper to diff_iteratively(). The nesting_exceeds() scan, which
cost about 30% on equal documents, is gone, and diff() is on par with
develop again.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 22:23:57 +02:00
Niels Lohmann 958e0a906b Merge remote-tracking branch 'origin/develop' into claude/iterative-diff 2026-09-25 22:19:57 +02:00
Niels Lohmann 49f038b86a Merge branch 'develop' into claude/iterative-diff
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 20:22:45 +02:00
Niels Lohmann 722c2bb561 Merge branch 'develop' into claude/iterative-diff
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 18:06:10 +02:00
Niels Lohmann f41296276c Mark the diff frame's value-initialized members for clang-tidy
The braces are kept for GCC's -Weffc++, as in json_sax.hpp.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 18:06:10 +02:00
Niels Lohmann 481b8d17fa Merge branch 'develop' into claude/iterative-diff
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 08:24:38 +02:00
Niels Lohmann 484f644b86 Diff fewer nesting depths so the test does not time out under Valgrind
Checking every depth up to 300 made test-json_patch exceed the 1500 s ctest
timeout in ci_test_valgrind. Check the depths up to 16, those around the
recursion limit of 128, and 300 instead.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 08:21:38 +02:00
Niels Lohmann 08e30eca78 Use the shared recursion limit in diff()
diff_depth_limit() is gone in favor of detail::recursion_depth_limit().

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-24 17:12:07 +02:00
Niels Lohmann 582223eb8a Make diff_frame a member struct that declares its special members
GCC's -Weffc++ (an error in CI) asks a class with pointer members, a
user constructor and a non-trivial destructor to declare its copy
constructor and copy assignment; diff_frame's vector and basic_json
members make its destructor non-trivial. Declare all five as defaulted,
which also satisfies clang-tidy's special-member-functions check. Leave
their exception specifications implicit: GCC 4.8 rejects an explicit
one that differs from the implicit one, as it does for flatten_task in
#5517.

The converting constructor cannot throw, and is now declared noexcept
for GCC's -Wnoexcept, which flags the emplace_back() under C++26
otherwise. The struct also moves from diff_iteratively() into the class,
like dump_frame in the serializer.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-24 17:12:07 +02:00
Niels Lohmann c14208a8e0 Diff deeply nested values without recursing per nesting level
diff() descended into both values once per nesting level, and compared
them with operator== on every level on the way, which recurses as well.
Values nested deeply enough - 25,000 levels on an 8 MiB stack - exhausted
the call stack and terminated the process, although parse() accepts
them without complaint. On such a chain the per-level comparisons and
path strings also made diff() quadratic in time and memory.

Both the recursion and operator== only descend as far as the source is
nested. So diff() first checks, recursing at most diff_depth_limit()
(128) levels, whether the source is nested more deeply than that. If not
- all but a vanishing minority of values - the recursive algorithm
diffs it exactly as before, now as diff_recursively(). Otherwise
diff_iteratively() walks the two values on an explicit stack, emitting
the same operations in the same order. It does not compare arrays and
objects with operator== up front (equal ones yield no operations
anyway), keeps the path in one buffer instead of a new string per
level, and hands every subtree that is not nested too deeply back to
diff_recursively(), so equal parts are still skipped quickly.

The check costs one pass over the source. On a 3,000-object document
that is about 30% of diffing two equal values (which is just an
operator== call), about 10% of diffing values that differ in a few
places, and noise when arrays change length. Once operator== no longer
recurses (#5390), the check can go.

Tests check that the patch reproduces the target at every depth up to
300, for json and ordered_json, including reordered members. They also
check the exact operation for a difference deep inside, and diff values
nested 100,000 levels deep.

Fixes #5393 for diff().

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-24 17:12:07 +02:00
8 changed files with 877 additions and 137 deletions
@@ -30,15 +30,6 @@ The exact mapping and its limitations are described on a [dedicated page](../../
1. MessagePack serialization as a byte vector
2. (none)
## Exceptions
- Throws [`out_of_range.412`](../../home/exceptions.md#jsonexceptionout_of_range412) if the length of a string, binary
value, array, or object exceeds 4294967295, the maximum MessagePack can store; example:
`"MessagePack length 4294967296 exceeds maximum of 4294967295"`
- Throws [`out_of_range.415`](../../home/exceptions.md#jsonexceptionout_of_range415) if the subtype of a binary value
exceeds 255, the maximum of the MessagePack ext type; example:
`"subtype 70000 is too large for the MessagePack ext type (max 255)"`
## Exception safety
Strong guarantee: if an exception is thrown, there are no changes in the JSON value.
@@ -74,4 +65,3 @@ Linear in the size of the JSON value `j`.
## Version history
- Added in version 2.0.9.
- Throws `out_of_range.412` and `out_of_range.415` since version 3.13.0.
@@ -65,8 +65,6 @@ specification:
- arrays with more than 4294967295 elements
- objects with more than 4294967295 elements
Serializing such a value throws [`out_of_range.412`](../../home/exceptions.md#jsonexceptionout_of_range412).
!!! info "NaN/infinity handling"
`NaN`, `Infinity`, and `-Infinity` are serialized as a MessagePack float 32 (type 0xCA, 5 bytes total),
+4 -10
View File
@@ -932,25 +932,19 @@ A JSON Patch `add` operation cannot be applied because the target location's par
### json.exception.out_of_range.412
BSON stores the length of documents, arrays, strings, and binary values in a signed 32-bit integer, and MessagePack
stores the length of strings, binary values, arrays, and objects in at most an unsigned 32-bit integer. This exception
is thrown when a value is too large to be described by such a length field.
BSON stores the length of documents, arrays, strings, and binary values in a signed 32-bit integer. This exception is thrown when a value is too large to be described by such a length field.
!!! failure "Example messages"
!!! failure "Example message"
```
BSON length 2147483661 exceeds maximum of 2147483647
```
```
MessagePack length 4294967296 exceeds maximum of 4294967295
```
!!! note
This exception was added in version 3.13.0. Before that, the BSON length was silently truncated, and
This exception was added in version 3.13.0. Before that, the length was silently truncated, and
[`to_bson`](../api/basic_json/to_bson.md) produced documents with negative length prefixes that
[`from_bson`](../api/basic_json/from_bson.md) rejected; [`to_msgpack`](../api/basic_json/to_msgpack.md) wrote such
a value without any length, producing output that could not be read back.
[`from_bson`](../api/basic_json/from_bson.md) rejected.
### json.exception.out_of_range.413
@@ -466,22 +466,6 @@ class binary_writer
}
}
/*!
@brief check that @a length fits into the 32 bits that MessagePack stores
the length of a string, binary value, array, or object in
@return the length as an unsigned 32-bit integer
@throw out_of_range.412 if @a length exceeds the range of std::uint32_t
*/
static std::uint32_t to_msgpack_length(const std::size_t length, const BasicJsonType& j)
{
if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::uint32_t>(length)))
{
JSON_THROW(out_of_range::create(412, concat("MessagePack length ", std::to_string(length), " exceeds maximum of ", std::to_string((std::numeric_limits<std::uint32_t>::max)())), &j));
}
return static_cast<std::uint32_t>(length);
}
/*!
@param[in] j JSON value to serialize
*/
@@ -622,7 +606,7 @@ class binary_writer
case value_t::string:
{
// step 1: write control byte and the string length
const auto N = to_msgpack_length(j.m_data.m_value.string->size(), j);
const auto N = j.m_data.m_value.string->size();
if (N <= 31)
{
// fixstr
@@ -640,7 +624,7 @@ class binary_writer
oa.write_character(to_char_type(0xDA));
write_number(static_cast<std::uint16_t>(N));
}
else
else if (N <= (std::numeric_limits<std::uint32_t>::max)())
{
// str 32
oa.write_character(to_char_type(0xDB));
@@ -657,7 +641,7 @@ class binary_writer
case value_t::array:
{
// step 1: write control byte and the array size
const auto N = to_msgpack_length(j.m_data.m_value.array->size(), j);
const auto N = j.m_data.m_value.array->size();
if (N <= 15)
{
// fixarray
@@ -669,7 +653,7 @@ class binary_writer
oa.write_character(to_char_type(0xDC));
write_number(static_cast<std::uint16_t>(N));
}
else
else if (N <= (std::numeric_limits<std::uint32_t>::max)())
{
// array 32
oa.write_character(to_char_type(0xDD));
@@ -691,7 +675,7 @@ class binary_writer
const bool use_ext = j.m_data.m_value.binary->has_subtype();
// step 1: write control byte and the byte string length
const auto N = to_msgpack_length(j.m_data.m_value.binary->size(), j);
const auto N = j.m_data.m_value.binary->size();
if (N <= (std::numeric_limits<std::uint8_t>::max)())
{
std::uint8_t output_type{};
@@ -743,7 +727,7 @@ class binary_writer
oa.write_character(to_char_type(output_type));
write_number(static_cast<std::uint16_t>(N));
}
else
else if (N <= (std::numeric_limits<std::uint32_t>::max)())
{
const std::uint8_t output_type = use_ext
? 0xC9 // ext 32
@@ -775,7 +759,7 @@ class binary_writer
case value_t::object:
{
// step 1: write control byte and the object size
const auto N = to_msgpack_length(j.m_data.m_value.object->size(), j);
const auto N = j.m_data.m_value.object->size();
if (N <= 15)
{
// fixmap
@@ -787,7 +771,7 @@ class binary_writer
oa.write_character(to_char_type(0xDE));
write_number(static_cast<std::uint16_t>(N));
}
else
else if (N <= (std::numeric_limits<std::uint32_t>::max)())
{
// map 32
oa.write_character(to_char_type(0xDF));
+352 -2
View File
@@ -5993,6 +5993,56 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
JSON_HEDLEY_WARN_UNUSED_RESULT
static basic_json diff(const basic_json& source, const basic_json& target,
const string_t& path = "")
{
return diff_recursively(source, target, path, 0);
}
private:
/// @brief two arrays or two objects @ref diff_iteratively is diffing
struct diff_frame
{
diff_frame(const basic_json* source_, const basic_json* target_, const std::size_t path_length_) noexcept
: source(source_), target(target_), path_length(path_length_)
{}
// declared for GCC's -Weffc++, which asks for them in a class with
// pointer members and a non-trivial destructor; the exception
// specifications are left implicit, as GCC 4.8 rejects explicit ones
// that differ from them
diff_frame(const diff_frame&) = default;
diff_frame(diff_frame&&) = default;
diff_frame& operator=(const diff_frame&) = default;
diff_frame& operator=(diff_frame&&) = default;
~diff_frame() = default;
/// the values being diffed, both arrays or both objects
const basic_json* source;
const basic_json* target;
/// the length of their path in `current_path`
std::size_t path_length;
/// arrays: the next index to diff
std::size_t index = 0;
/// objects: the next member of source to look at
const_iterator member{}; // NOLINT(readability-redundant-member-init)
/// objects: the keys common to both, in source's order
std::vector<typename object_t::key_type> common_keys{}; // NOLINT(readability-redundant-member-init)
/// objects: the next entry of common_keys
std::size_t next_common = 0;
/// objects: the "add" operations for keys only target has
basic_json added_ops{}; // NOLINT(readability-redundant-member-init)
};
/*!
@brief @ref diff, for values at nesting level @a depth
Diffing two arrays or objects calls this function again, once per nesting
level, so values nested deeply enough used to exhaust the call stack and
terminate the process. The descent is bounded here: once @ref
detail::recursion_depth_limit levels have been entered, @ref
diff_iteratively diffs what is left without the call stack.
*/
static basic_json diff_recursively(const basic_json& source, const basic_json& target,
const string_t& path, const std::size_t depth)
{
// the patch
basic_json result(value_t::array);
@@ -6003,6 +6053,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
return result;
}
if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit()))
{
return diff_iteratively(source, target, path);
}
if (source.type() != target.type())
{
// different types: replace value
@@ -6022,7 +6077,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
while (i < source.size() && i < target.size())
{
// recursive call to compare array values at index i
auto temp_diff = diff(source[i], target[i], detail::concat<string_t>(path, '/', detail::to_string<string_t>(i)));
auto temp_diff = diff_recursively(source[i], target[i], detail::concat<string_t>(path, '/', detail::to_string<string_t>(i)), depth + 1);
result.insert(result.end(), temp_diff.begin(), temp_diff.end());
++i;
}
@@ -6141,7 +6196,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
if (common_it != common_keys_source_order.cend() && it.key() == *common_it)
{
const auto path_key = detail::concat<string_t>(path, '/', detail::escape(it.key()));
auto temp_diff = diff(it.value(), target[it.key()], path_key);
auto temp_diff = diff_recursively(it.value(), target[it.key()], path_key, depth + 1);
result.insert(result.end(), temp_diff.begin(), temp_diff.end());
++common_it;
}
@@ -6225,6 +6280,301 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
return result;
}
/*!
@brief @ref diff without the call stack
Produces the same patch as @ref diff_recursively. Only reached for values
nested more deeply than @ref detail::recursion_depth_limit.
*/
static basic_json diff_iteratively(const basic_json& source, const basic_json& target,
const string_t& path)
{
// the patch
basic_json result(value_t::array);
// The arrays and objects being diffed are kept on an explicit stack,
// and every pair of elements is still diffed completely before the
// next one, so the operations come out in the same order as in
// diff_recursively. The path of the values being diffed is kept in
// one buffer that grows and shrinks with the stack, rather than in a
// new string per level.
std::vector<diff_frame> stack;
string_t current_path = path;
// diff `s` against `t`, whose path is current_path: primitives,
// values of different types, and objects whose members were reordered
// are handled right away; arrays and other objects get a frame
const auto enter = [&result, &stack, &current_path](const basic_json & s, const basic_json & t)
{
// if the values are the same, there is nothing to do. Arrays and
// objects are not compared up front: comparing them visits
// everything below them, so doing that at every level would take
// quadratic time in the nesting depth - equal ones yield no
// operations anyway.
if ((!s.is_structured() || !t.is_structured()) && s == t)
{
return;
}
if (s.type() != t.type())
{
// different types: replace value
result.push_back(
{
{"op", "replace"}, {"path", current_path}, {"value", t}
});
return;
}
switch (s.type())
{
case value_t::array:
{
stack.emplace_back(&s, &t, current_path.size());
return;
}
case value_t::object:
{
// first pass: record, for every source key, whether it is
// common to both objects (in source's iteration order) or
// was deleted (i.e., in source but not in target) -- this is
// a by-product of the t.find() call already needed to
// tell the two cases apart, so it adds no extra lookups. The
// "remove" ops themselves are emitted later, interleaved
// with the per-key diffs in the fast path below, to match
// source's original iteration order (as the original,
// pre-reordering-aware implementation did) instead of
// grouping all removes before all per-key diffs.
std::vector<typename object_t::key_type> common_keys_source_order;
for (auto it = s.cbegin(); it != s.cend(); ++it)
{
if (t.find(it.key()) != t.end())
{
common_keys_source_order.push_back(it.key());
}
}
// second pass: find keys that were added (i.e., in target but
// not in source), and record the keys common to both, in
// target's iteration order -- again a by-product of the
// s.find() call already needed to detect added keys. At
// the same time, determine whether every added key comes
// after every common key in target's order (a precondition
// for the fast path below, which only ever appends new keys
// at the very end): for an object_t whose iteration order is
// a pure function of the key set (e.g. the default std::map,
// which always iterates in sorted key order), the order
// check further below is always true and this whole
// mechanism is effectively a no-op; it only matters for a
// reorderable object_t such as the one backing `ordered_json`.
// patch ops for keys that were added (i.e., in target but not
// in source); built here so the fast path below can reuse
// them without a second s.find() per target key. Only
// used by the fast path -- the slow (reordering) path
// rebuilds "add" ops for every key itself.
std::vector<typename object_t::key_type> common_keys_target_order;
basic_json added_ops(value_t::array);
bool new_keys_form_suffix = true;
bool seen_new_key = false;
for (auto it = t.cbegin(); it != t.cend(); ++it)
{
if (s.find(it.key()) == s.end())
{
seen_new_key = true;
const auto path_key = detail::concat<string_t>(current_path, '/', detail::escape(it.key()));
added_ops.push_back(
{
{"op", "add"}, {"path", path_key},
{"value", it.value()}
});
}
else
{
common_keys_target_order.push_back(it.key());
if (seen_new_key)
{
new_keys_form_suffix = false;
}
}
}
if (common_keys_source_order == common_keys_target_order && new_keys_form_suffix)
{
// fast path: order of common keys already matches (or the
// object_t's iteration order does not depend on
// insertion history), so a plain per-key diff is correct
// and minimal, as before. The frame walks source in
// lockstep with common_keys_source_order, which is, by
// construction, the subsequence of source's keys that
// are common to both objects, in source's iteration
// order -- so a cheap key comparison replaces another
// lookup. Deleted keys are interleaved there too, in
// source's original order, and the "add" ops collected
// above are appended once all members are done.
stack.emplace_back(&s, &t, current_path.size());
stack.back().member = s.cbegin();
stack.back().common_keys = std::move(common_keys_source_order);
stack.back().added_ops = std::move(added_ops);
return;
}
// slow path: the common keys are in a different relative
// order in source and target (only possible for a
// reorderable object_t like ordered_map). Building a
// minimal reordering patch is a nontrivial (LCS-like)
// problem; instead, remove every source key -- both
// deleted keys (which must be removed regardless) and
// common keys (removed so they can be re-added in
// target's order) -- and re-add every key that should
// remain, with its final target value, in target's
// order. basic_json::patch()'s "add" operation on an
// object uses operator[], which appends at the end for a
// vector-backed insertion-ordered map when the key does
// not already exist -- so removing a key and then adding
// it moves it to the end, fixing its position.
for (auto it = s.cbegin(); it != s.cend(); ++it)
{
const auto path_key = detail::concat<string_t>(current_path, '/', detail::escape(it.key()));
result.push_back(object(
{
{"op", "remove"}, {"path", path_key}
}));
}
// add every key that is either common (just removed
// above) or brand new, in target's iteration order, so
// that the final order after applying the patch matches
// target exactly
for (auto it = t.cbegin(); it != t.cend(); ++it)
{
const auto path_key = detail::concat<string_t>(current_path, '/', detail::escape(it.key()));
result.push_back(
{
{"op", "add"}, {"path", path_key},
{"value", it.value()}
});
}
return;
}
case value_t::null:
case value_t::string:
case value_t::boolean:
case value_t::number_integer:
case value_t::number_unsigned:
case value_t::number_float:
case value_t::binary:
case value_t::discarded:
default:
{
// both primitive types: replace value
result.push_back(
{
{"op", "replace"}, {"path", current_path}, {"value", t}
});
return;
}
}
};
enter(source, target);
while (!stack.empty())
{
diff_frame& frame = stack.back();
const std::size_t path_length = frame.path_length;
const std::size_t depth = stack.size();
if (frame.source->is_array())
{
const auto& source_array = *frame.source->m_data.m_value.array;
const auto& target_array = *frame.target->m_data.m_value.array;
// first pass: traverse common elements
if (frame.index < source_array.size() && frame.index < target_array.size())
{
const std::size_t i = frame.index++;
detail::concat_into(current_path, '/', detail::to_string<string_t>(i));
enter(source_array[i], target_array[i]); // may push, which invalidates `frame`
if (stack.size() == depth)
{
current_path.resize(path_length);
}
continue;
}
// We now reached the end of at least one array
// in a second pass, traverse the remaining elements
// remove my remaining elements, highest index first; appending
// in that order avoids the quadratic reinsertion done before
for (std::size_t j = source_array.size(); j > frame.index; --j)
{
result.push_back(object(
{
{"op", "remove"},
{"path", detail::concat<string_t>(current_path, '/', detail::to_string<string_t>(j - 1))}
}));
}
// add other remaining elements
for (std::size_t i = source_array.size(); i < target_array.size(); ++i)
{
result.push_back(
{
{"op", "add"},
{"path", detail::concat<string_t>(current_path, "/-")},
{"value", target_array[i]}
});
}
}
else
{
if (frame.member != frame.source->cend())
{
const const_iterator it = frame.member;
++frame.member;
if (frame.next_common < frame.common_keys.size() && it.key() == frame.common_keys[frame.next_common])
{
++frame.next_common;
const basic_json& target_value = (*frame.target)[it.key()];
detail::concat_into(current_path, '/', detail::escape(it.key()));
enter(it.value(), target_value); // may push, which invalidates `frame`
if (stack.size() == depth)
{
current_path.resize(path_length);
}
}
else
{
// found a key that is not in target -> remove it
const auto path_key = detail::concat<string_t>(current_path, '/', detail::escape(it.key()));
result.push_back(object(
{
{"op", "remove"}, {"path", path_key}
}));
}
continue;
}
// append the "add" ops for brand-new keys collected when the
// object was entered
result.insert(result.end(), frame.added_ops.begin(), frame.added_ops.end());
}
// this array or object is done: continue with the one it is in
stack.pop_back();
if (!stack.empty())
{
current_path.resize(stack.back().path_length);
}
}
return result;
}
public:
/// @}
////////////////////////////////
+360 -26
View File
@@ -19931,22 +19931,6 @@ class binary_writer
}
}
/*!
@brief check that @a length fits into the 32 bits that MessagePack stores
the length of a string, binary value, array, or object in
@return the length as an unsigned 32-bit integer
@throw out_of_range.412 if @a length exceeds the range of std::uint32_t
*/
static std::uint32_t to_msgpack_length(const std::size_t length, const BasicJsonType& j)
{
if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::uint32_t>(length)))
{
JSON_THROW(out_of_range::create(412, concat("MessagePack length ", std::to_string(length), " exceeds maximum of ", std::to_string((std::numeric_limits<std::uint32_t>::max)())), &j));
}
return static_cast<std::uint32_t>(length);
}
/*!
@param[in] j JSON value to serialize
*/
@@ -20087,7 +20071,7 @@ class binary_writer
case value_t::string:
{
// step 1: write control byte and the string length
const auto N = to_msgpack_length(j.m_data.m_value.string->size(), j);
const auto N = j.m_data.m_value.string->size();
if (N <= 31)
{
// fixstr
@@ -20105,7 +20089,7 @@ class binary_writer
oa.write_character(to_char_type(0xDA));
write_number(static_cast<std::uint16_t>(N));
}
else
else if (N <= (std::numeric_limits<std::uint32_t>::max)())
{
// str 32
oa.write_character(to_char_type(0xDB));
@@ -20122,7 +20106,7 @@ class binary_writer
case value_t::array:
{
// step 1: write control byte and the array size
const auto N = to_msgpack_length(j.m_data.m_value.array->size(), j);
const auto N = j.m_data.m_value.array->size();
if (N <= 15)
{
// fixarray
@@ -20134,7 +20118,7 @@ class binary_writer
oa.write_character(to_char_type(0xDC));
write_number(static_cast<std::uint16_t>(N));
}
else
else if (N <= (std::numeric_limits<std::uint32_t>::max)())
{
// array 32
oa.write_character(to_char_type(0xDD));
@@ -20156,7 +20140,7 @@ class binary_writer
const bool use_ext = j.m_data.m_value.binary->has_subtype();
// step 1: write control byte and the byte string length
const auto N = to_msgpack_length(j.m_data.m_value.binary->size(), j);
const auto N = j.m_data.m_value.binary->size();
if (N <= (std::numeric_limits<std::uint8_t>::max)())
{
std::uint8_t output_type{};
@@ -20208,7 +20192,7 @@ class binary_writer
oa.write_character(to_char_type(output_type));
write_number(static_cast<std::uint16_t>(N));
}
else
else if (N <= (std::numeric_limits<std::uint32_t>::max)())
{
const std::uint8_t output_type = use_ext
? 0xC9 // ext 32
@@ -20240,7 +20224,7 @@ class binary_writer
case value_t::object:
{
// step 1: write control byte and the object size
const auto N = to_msgpack_length(j.m_data.m_value.object->size(), j);
const auto N = j.m_data.m_value.object->size();
if (N <= 15)
{
// fixmap
@@ -20252,7 +20236,7 @@ class binary_writer
oa.write_character(to_char_type(0xDE));
write_number(static_cast<std::uint16_t>(N));
}
else
else if (N <= (std::numeric_limits<std::uint32_t>::max)())
{
// map 32
oa.write_character(to_char_type(0xDF));
@@ -30967,6 +30951,56 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
JSON_HEDLEY_WARN_UNUSED_RESULT
static basic_json diff(const basic_json& source, const basic_json& target,
const string_t& path = "")
{
return diff_recursively(source, target, path, 0);
}
private:
/// @brief two arrays or two objects @ref diff_iteratively is diffing
struct diff_frame
{
diff_frame(const basic_json* source_, const basic_json* target_, const std::size_t path_length_) noexcept
: source(source_), target(target_), path_length(path_length_)
{}
// declared for GCC's -Weffc++, which asks for them in a class with
// pointer members and a non-trivial destructor; the exception
// specifications are left implicit, as GCC 4.8 rejects explicit ones
// that differ from them
diff_frame(const diff_frame&) = default;
diff_frame(diff_frame&&) = default;
diff_frame& operator=(const diff_frame&) = default;
diff_frame& operator=(diff_frame&&) = default;
~diff_frame() = default;
/// the values being diffed, both arrays or both objects
const basic_json* source;
const basic_json* target;
/// the length of their path in `current_path`
std::size_t path_length;
/// arrays: the next index to diff
std::size_t index = 0;
/// objects: the next member of source to look at
const_iterator member{}; // NOLINT(readability-redundant-member-init)
/// objects: the keys common to both, in source's order
std::vector<typename object_t::key_type> common_keys{}; // NOLINT(readability-redundant-member-init)
/// objects: the next entry of common_keys
std::size_t next_common = 0;
/// objects: the "add" operations for keys only target has
basic_json added_ops{}; // NOLINT(readability-redundant-member-init)
};
/*!
@brief @ref diff, for values at nesting level @a depth
Diffing two arrays or objects calls this function again, once per nesting
level, so values nested deeply enough used to exhaust the call stack and
terminate the process. The descent is bounded here: once @ref
detail::recursion_depth_limit levels have been entered, @ref
diff_iteratively diffs what is left without the call stack.
*/
static basic_json diff_recursively(const basic_json& source, const basic_json& target,
const string_t& path, const std::size_t depth)
{
// the patch
basic_json result(value_t::array);
@@ -30977,6 +31011,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
return result;
}
if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit()))
{
return diff_iteratively(source, target, path);
}
if (source.type() != target.type())
{
// different types: replace value
@@ -30996,7 +31035,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
while (i < source.size() && i < target.size())
{
// recursive call to compare array values at index i
auto temp_diff = diff(source[i], target[i], detail::concat<string_t>(path, '/', detail::to_string<string_t>(i)));
auto temp_diff = diff_recursively(source[i], target[i], detail::concat<string_t>(path, '/', detail::to_string<string_t>(i)), depth + 1);
result.insert(result.end(), temp_diff.begin(), temp_diff.end());
++i;
}
@@ -31115,7 +31154,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
if (common_it != common_keys_source_order.cend() && it.key() == *common_it)
{
const auto path_key = detail::concat<string_t>(path, '/', detail::escape(it.key()));
auto temp_diff = diff(it.value(), target[it.key()], path_key);
auto temp_diff = diff_recursively(it.value(), target[it.key()], path_key, depth + 1);
result.insert(result.end(), temp_diff.begin(), temp_diff.end());
++common_it;
}
@@ -31199,6 +31238,301 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
return result;
}
/*!
@brief @ref diff without the call stack
Produces the same patch as @ref diff_recursively. Only reached for values
nested more deeply than @ref detail::recursion_depth_limit.
*/
static basic_json diff_iteratively(const basic_json& source, const basic_json& target,
const string_t& path)
{
// the patch
basic_json result(value_t::array);
// The arrays and objects being diffed are kept on an explicit stack,
// and every pair of elements is still diffed completely before the
// next one, so the operations come out in the same order as in
// diff_recursively. The path of the values being diffed is kept in
// one buffer that grows and shrinks with the stack, rather than in a
// new string per level.
std::vector<diff_frame> stack;
string_t current_path = path;
// diff `s` against `t`, whose path is current_path: primitives,
// values of different types, and objects whose members were reordered
// are handled right away; arrays and other objects get a frame
const auto enter = [&result, &stack, &current_path](const basic_json & s, const basic_json & t)
{
// if the values are the same, there is nothing to do. Arrays and
// objects are not compared up front: comparing them visits
// everything below them, so doing that at every level would take
// quadratic time in the nesting depth - equal ones yield no
// operations anyway.
if ((!s.is_structured() || !t.is_structured()) && s == t)
{
return;
}
if (s.type() != t.type())
{
// different types: replace value
result.push_back(
{
{"op", "replace"}, {"path", current_path}, {"value", t}
});
return;
}
switch (s.type())
{
case value_t::array:
{
stack.emplace_back(&s, &t, current_path.size());
return;
}
case value_t::object:
{
// first pass: record, for every source key, whether it is
// common to both objects (in source's iteration order) or
// was deleted (i.e., in source but not in target) -- this is
// a by-product of the t.find() call already needed to
// tell the two cases apart, so it adds no extra lookups. The
// "remove" ops themselves are emitted later, interleaved
// with the per-key diffs in the fast path below, to match
// source's original iteration order (as the original,
// pre-reordering-aware implementation did) instead of
// grouping all removes before all per-key diffs.
std::vector<typename object_t::key_type> common_keys_source_order;
for (auto it = s.cbegin(); it != s.cend(); ++it)
{
if (t.find(it.key()) != t.end())
{
common_keys_source_order.push_back(it.key());
}
}
// second pass: find keys that were added (i.e., in target but
// not in source), and record the keys common to both, in
// target's iteration order -- again a by-product of the
// s.find() call already needed to detect added keys. At
// the same time, determine whether every added key comes
// after every common key in target's order (a precondition
// for the fast path below, which only ever appends new keys
// at the very end): for an object_t whose iteration order is
// a pure function of the key set (e.g. the default std::map,
// which always iterates in sorted key order), the order
// check further below is always true and this whole
// mechanism is effectively a no-op; it only matters for a
// reorderable object_t such as the one backing `ordered_json`.
// patch ops for keys that were added (i.e., in target but not
// in source); built here so the fast path below can reuse
// them without a second s.find() per target key. Only
// used by the fast path -- the slow (reordering) path
// rebuilds "add" ops for every key itself.
std::vector<typename object_t::key_type> common_keys_target_order;
basic_json added_ops(value_t::array);
bool new_keys_form_suffix = true;
bool seen_new_key = false;
for (auto it = t.cbegin(); it != t.cend(); ++it)
{
if (s.find(it.key()) == s.end())
{
seen_new_key = true;
const auto path_key = detail::concat<string_t>(current_path, '/', detail::escape(it.key()));
added_ops.push_back(
{
{"op", "add"}, {"path", path_key},
{"value", it.value()}
});
}
else
{
common_keys_target_order.push_back(it.key());
if (seen_new_key)
{
new_keys_form_suffix = false;
}
}
}
if (common_keys_source_order == common_keys_target_order && new_keys_form_suffix)
{
// fast path: order of common keys already matches (or the
// object_t's iteration order does not depend on
// insertion history), so a plain per-key diff is correct
// and minimal, as before. The frame walks source in
// lockstep with common_keys_source_order, which is, by
// construction, the subsequence of source's keys that
// are common to both objects, in source's iteration
// order -- so a cheap key comparison replaces another
// lookup. Deleted keys are interleaved there too, in
// source's original order, and the "add" ops collected
// above are appended once all members are done.
stack.emplace_back(&s, &t, current_path.size());
stack.back().member = s.cbegin();
stack.back().common_keys = std::move(common_keys_source_order);
stack.back().added_ops = std::move(added_ops);
return;
}
// slow path: the common keys are in a different relative
// order in source and target (only possible for a
// reorderable object_t like ordered_map). Building a
// minimal reordering patch is a nontrivial (LCS-like)
// problem; instead, remove every source key -- both
// deleted keys (which must be removed regardless) and
// common keys (removed so they can be re-added in
// target's order) -- and re-add every key that should
// remain, with its final target value, in target's
// order. basic_json::patch()'s "add" operation on an
// object uses operator[], which appends at the end for a
// vector-backed insertion-ordered map when the key does
// not already exist -- so removing a key and then adding
// it moves it to the end, fixing its position.
for (auto it = s.cbegin(); it != s.cend(); ++it)
{
const auto path_key = detail::concat<string_t>(current_path, '/', detail::escape(it.key()));
result.push_back(object(
{
{"op", "remove"}, {"path", path_key}
}));
}
// add every key that is either common (just removed
// above) or brand new, in target's iteration order, so
// that the final order after applying the patch matches
// target exactly
for (auto it = t.cbegin(); it != t.cend(); ++it)
{
const auto path_key = detail::concat<string_t>(current_path, '/', detail::escape(it.key()));
result.push_back(
{
{"op", "add"}, {"path", path_key},
{"value", it.value()}
});
}
return;
}
case value_t::null:
case value_t::string:
case value_t::boolean:
case value_t::number_integer:
case value_t::number_unsigned:
case value_t::number_float:
case value_t::binary:
case value_t::discarded:
default:
{
// both primitive types: replace value
result.push_back(
{
{"op", "replace"}, {"path", current_path}, {"value", t}
});
return;
}
}
};
enter(source, target);
while (!stack.empty())
{
diff_frame& frame = stack.back();
const std::size_t path_length = frame.path_length;
const std::size_t depth = stack.size();
if (frame.source->is_array())
{
const auto& source_array = *frame.source->m_data.m_value.array;
const auto& target_array = *frame.target->m_data.m_value.array;
// first pass: traverse common elements
if (frame.index < source_array.size() && frame.index < target_array.size())
{
const std::size_t i = frame.index++;
detail::concat_into(current_path, '/', detail::to_string<string_t>(i));
enter(source_array[i], target_array[i]); // may push, which invalidates `frame`
if (stack.size() == depth)
{
current_path.resize(path_length);
}
continue;
}
// We now reached the end of at least one array
// in a second pass, traverse the remaining elements
// remove my remaining elements, highest index first; appending
// in that order avoids the quadratic reinsertion done before
for (std::size_t j = source_array.size(); j > frame.index; --j)
{
result.push_back(object(
{
{"op", "remove"},
{"path", detail::concat<string_t>(current_path, '/', detail::to_string<string_t>(j - 1))}
}));
}
// add other remaining elements
for (std::size_t i = source_array.size(); i < target_array.size(); ++i)
{
result.push_back(
{
{"op", "add"},
{"path", detail::concat<string_t>(current_path, "/-")},
{"value", target_array[i]}
});
}
}
else
{
if (frame.member != frame.source->cend())
{
const const_iterator it = frame.member;
++frame.member;
if (frame.next_common < frame.common_keys.size() && it.key() == frame.common_keys[frame.next_common])
{
++frame.next_common;
const basic_json& target_value = (*frame.target)[it.key()];
detail::concat_into(current_path, '/', detail::escape(it.key()));
enter(it.value(), target_value); // may push, which invalidates `frame`
if (stack.size() == depth)
{
current_path.resize(path_length);
}
}
else
{
// found a key that is not in target -> remove it
const auto path_key = detail::concat<string_t>(current_path, '/', detail::escape(it.key()));
result.push_back(object(
{
{"op", "remove"}, {"path", path_key}
}));
}
continue;
}
// append the "add" ops for brand-new keys collected when the
// object was entered
result.insert(result.end(), frame.added_ops.begin(), frame.added_ops.end());
}
// this array or object is done: continue with the one it is in
stack.pop_back();
if (!stack.empty())
{
current_path.resize(stack.back().path_length);
}
}
return result;
}
public:
/// @}
////////////////////////////////
+153
View File
@@ -15,8 +15,65 @@ using nlohmann::json;
#endif
#include <fstream>
#include <string>
#include <vector>
#include "make_test_data_available.hpp"
namespace
{
// alternating objects and arrays nested `depth` levels deep, with members that
// depend on `variant` at some levels, so diffing two variants yields
// operations on many levels: replacing the innermost value, adding, removing,
// and (for ordered_json) reordering members, and changing array lengths
template<typename BasicJsonType>
BasicJsonType nested(const std::size_t depth, const int variant)
{
BasicJsonType value = variant;
for (std::size_t i = 0; i < depth; ++i)
{
if (i % 2 == 0)
{
BasicJsonType object = BasicJsonType::object();
if ((i + static_cast<std::size_t>(variant)) % 7 == 0)
{
object["x"] = i;
}
if (variant == 2 && i % 11 == 0)
{
object["z"] = "z";
}
object["a"] = std::move(value);
if (variant == 1 && i % 5 == 0)
{
object["y"] = 1;
}
value = std::move(object);
}
else
{
BasicJsonType array = BasicJsonType::array({std::move(value)});
if ((i + static_cast<std::size_t>(variant)) % 3 == 0)
{
array.push_back(i);
}
value = std::move(array);
}
}
return value;
}
// a path of `depth` reference tokens, as nested() nests its values
std::string nested_path(const std::size_t depth)
{
std::string path;
for (std::size_t i = depth; i > 0; --i)
{
path += (i - 1) % 2 == 0 ? "/a" : "/0";
}
return path;
}
} // namespace
TEST_CASE("JSON patch")
{
SECTION("examples from RFC 6902")
@@ -1751,3 +1808,99 @@ TEST_CASE("JSON patch - diff emits array removals in descending index order")
CHECK(source.patch(patch) == target);
}
}
TEST_CASE("JSON patch: diff of deeply nested values")
{
SECTION("the diff reproduces the target at every depth")
{
// depths on either side of the nesting depth up to which diff()
// recurses (detail::recursion_depth_limit(), 128); not every depth up
// to 300, as the test would then time out under Valgrind
std::vector<std::size_t> depths;
for (std::size_t depth = 0; depth <= 16; ++depth)
{
depths.push_back(depth);
}
for (std::size_t depth = 120; depth <= 136; ++depth)
{
depths.push_back(depth);
}
depths.push_back(300);
for (const auto depth : depths)
{
CAPTURE(depth);
for (int from = 0; from < 3; ++from)
{
for (int to = 0; to < 3; ++to)
{
CAPTURE(from);
CAPTURE(to);
const auto source = nested<json>(depth, from);
const auto target = nested<json>(depth, to);
const auto patch = json::diff(source, target);
CHECK(source.patch(patch) == target);
CHECK(patch.empty() == (from == to));
const auto ordered_source = nested<nlohmann::ordered_json>(depth, from);
const auto ordered_target = nested<nlohmann::ordered_json>(depth, to);
CHECK(ordered_source.patch(nlohmann::ordered_json::diff(ordered_source, ordered_target)) == ordered_target);
}
}
}
}
SECTION("a difference only in the innermost value is one replace operation")
{
for (std::size_t depth = 0; depth <= 300; ++depth)
{
CAPTURE(depth);
json source = 1;
json target = 2;
for (std::size_t i = 0; i < depth; ++i)
{
source = i % 2 == 0 ? json::object({{"a", std::move(source)}}) : json::array({std::move(source)});
target = i % 2 == 0 ? json::object({{"a", std::move(target)}}) : json::array({std::move(target)});
}
CHECK(json::diff(source, target, "/root") == json::array({{{"op", "replace"}, {"path", "/root" + nested_path(depth)}, {"value", 2}}}));
}
}
SECTION("values nested too deeply for the call stack (#5393)")
{
// diff() used to recurse once per nesting level, and compared the
// values with operator== on every level. The values are only
// parsed and diffed, never copied or compared, since those recurse
// too.
const std::size_t depth = 100000;
for (const bool objects :
{
false, true
})
{
CAPTURE(objects);
std::string source_text;
std::string target_text;
std::string equal_text;
std::string path;
for (std::size_t i = 0; i < depth; ++i)
{
source_text += objects ? "{\"a\":" : "[";
path += objects ? "/a" : "/0";
}
target_text = source_text + "2";
equal_text = source_text + "1";
source_text += "1";
const std::string closing(depth, objects ? '}' : ']');
const auto source = json::parse(source_text + closing);
const auto patch = json::diff(source, json::parse(target_text + closing));
REQUIRE(patch.size() == 1);
CHECK(patch[0]["op"] == "replace");
CHECK(patch[0]["path"] == path);
CHECK(patch[0]["value"] == 2);
CHECK(json::diff(source, json::parse(equal_text + closing)).empty());
}
}
}
-63
View File
@@ -14,7 +14,6 @@ using nlohmann::json;
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
#endif
#include <cstdint> // SIZE_MAX, UINT32_MAX
#include <fstream>
#include <sstream>
#include <iomanip>
@@ -2151,65 +2150,3 @@ TEST_CASE("MessagePack with std::byte")
}
}
#endif
namespace
{
// types that report a size beyond UINT32_MAX without allocating that much
// memory, so the MessagePack length limit can be tested cheaply; see the
// similar types in unit-bson.cpp
std::size_t beyond_uint32_size()
{
return static_cast<std::size_t>((std::numeric_limits<std::uint32_t>::max)()) + 1;
}
class beyond_uint32_binary_t : public std::vector<std::uint8_t>
{
public:
using std::vector<std::uint8_t>::vector;
size_type size() const noexcept // NOLINT(readability-convert-member-functions-to-static)
{
return beyond_uint32_size();
}
};
class beyond_uint32_string_t : public std::string
{
public:
using std::string::string;
size_type size() const noexcept // NOLINT(readability-convert-member-functions-to-static)
{
return beyond_uint32_size();
}
};
using beyond_uint32_binary_json = nlohmann::basic_json <
std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t,
double, std::allocator, nlohmann::adl_serializer, beyond_uint32_binary_t, void >;
using beyond_uint32_string_json = nlohmann::basic_json <
std::map, std::vector, beyond_uint32_string_t, bool, std::int64_t, std::uint64_t,
double, std::allocator, nlohmann::adl_serializer, std::vector<std::uint8_t>, void >;
} // namespace
TEST_CASE("MessagePack lengths beyond UINT32_MAX cannot be serialized")
{
// MessagePack stores the length of a string, binary value, array, or
// object in at most 32 bits; a larger one used to be written without any
// length at all
#if SIZE_MAX > UINT32_MAX
{
const char* const expected = "[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295";
const beyond_uint32_binary_json binary = beyond_uint32_binary_json::binary(beyond_uint32_binary_t{});
CHECK_THROWS_WITH_AS(beyond_uint32_binary_json::to_msgpack(binary), expected, beyond_uint32_binary_json::out_of_range&);
const beyond_uint32_binary_json ext = beyond_uint32_binary_json::binary(beyond_uint32_binary_t{}, 42);
CHECK_THROWS_WITH_AS(beyond_uint32_binary_json::to_msgpack(ext), expected, beyond_uint32_binary_json::out_of_range&);
const beyond_uint32_string_json string = beyond_uint32_string_t("value");
CHECK_THROWS_WITH_AS(beyond_uint32_string_json::to_msgpack(string), expected, beyond_uint32_string_json::out_of_range&);
}
#endif
}