mirror of
https://github.com/nlohmann/json.git
synced 2026-09-26 01:40:32 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35173b9d2d |
@@ -30,6 +30,15 @@ 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.
|
||||
@@ -65,3 +74,4 @@ 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,6 +65,8 @@ 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),
|
||||
|
||||
@@ -932,19 +932,25 @@ 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. 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, 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.
|
||||
|
||||
!!! failure "Example message"
|
||||
!!! failure "Example messages"
|
||||
|
||||
```
|
||||
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 length was silently truncated, and
|
||||
This exception was added in version 3.13.0. Before that, the BSON 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.
|
||||
[`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.
|
||||
|
||||
### json.exception.out_of_range.413
|
||||
|
||||
|
||||
@@ -466,6 +466,22 @@ 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
|
||||
*/
|
||||
@@ -606,7 +622,7 @@ class binary_writer
|
||||
case value_t::string:
|
||||
{
|
||||
// step 1: write control byte and the string length
|
||||
const auto N = j.m_data.m_value.string->size();
|
||||
const auto N = to_msgpack_length(j.m_data.m_value.string->size(), j);
|
||||
if (N <= 31)
|
||||
{
|
||||
// fixstr
|
||||
@@ -624,7 +640,7 @@ class binary_writer
|
||||
oa.write_character(to_char_type(0xDA));
|
||||
write_number(static_cast<std::uint16_t>(N));
|
||||
}
|
||||
else if (N <= (std::numeric_limits<std::uint32_t>::max)())
|
||||
else
|
||||
{
|
||||
// str 32
|
||||
oa.write_character(to_char_type(0xDB));
|
||||
@@ -641,7 +657,7 @@ class binary_writer
|
||||
case value_t::array:
|
||||
{
|
||||
// step 1: write control byte and the array size
|
||||
const auto N = j.m_data.m_value.array->size();
|
||||
const auto N = to_msgpack_length(j.m_data.m_value.array->size(), j);
|
||||
if (N <= 15)
|
||||
{
|
||||
// fixarray
|
||||
@@ -653,7 +669,7 @@ class binary_writer
|
||||
oa.write_character(to_char_type(0xDC));
|
||||
write_number(static_cast<std::uint16_t>(N));
|
||||
}
|
||||
else if (N <= (std::numeric_limits<std::uint32_t>::max)())
|
||||
else
|
||||
{
|
||||
// array 32
|
||||
oa.write_character(to_char_type(0xDD));
|
||||
@@ -675,7 +691,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 = j.m_data.m_value.binary->size();
|
||||
const auto N = to_msgpack_length(j.m_data.m_value.binary->size(), j);
|
||||
if (N <= (std::numeric_limits<std::uint8_t>::max)())
|
||||
{
|
||||
std::uint8_t output_type{};
|
||||
@@ -727,7 +743,7 @@ class binary_writer
|
||||
oa.write_character(to_char_type(output_type));
|
||||
write_number(static_cast<std::uint16_t>(N));
|
||||
}
|
||||
else if (N <= (std::numeric_limits<std::uint32_t>::max)())
|
||||
else
|
||||
{
|
||||
const std::uint8_t output_type = use_ext
|
||||
? 0xC9 // ext 32
|
||||
@@ -759,7 +775,7 @@ class binary_writer
|
||||
case value_t::object:
|
||||
{
|
||||
// step 1: write control byte and the object size
|
||||
const auto N = j.m_data.m_value.object->size();
|
||||
const auto N = to_msgpack_length(j.m_data.m_value.object->size(), j);
|
||||
if (N <= 15)
|
||||
{
|
||||
// fixmap
|
||||
@@ -771,7 +787,7 @@ class binary_writer
|
||||
oa.write_character(to_char_type(0xDE));
|
||||
write_number(static_cast<std::uint16_t>(N));
|
||||
}
|
||||
else if (N <= (std::numeric_limits<std::uint32_t>::max)())
|
||||
else
|
||||
{
|
||||
// map 32
|
||||
oa.write_character(to_char_type(0xDF));
|
||||
|
||||
+2
-352
@@ -5993,56 +5993,6 @@ 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);
|
||||
@@ -6053,11 +6003,6 @@ 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
|
||||
@@ -6077,7 +6022,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_recursively(source[i], target[i], detail::concat<string_t>(path, '/', detail::to_string<string_t>(i)), depth + 1);
|
||||
auto temp_diff = diff(source[i], target[i], detail::concat<string_t>(path, '/', detail::to_string<string_t>(i)));
|
||||
result.insert(result.end(), temp_diff.begin(), temp_diff.end());
|
||||
++i;
|
||||
}
|
||||
@@ -6196,7 +6141,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_recursively(it.value(), target[it.key()], path_key, depth + 1);
|
||||
auto temp_diff = diff(it.value(), target[it.key()], path_key);
|
||||
result.insert(result.end(), temp_diff.begin(), temp_diff.end());
|
||||
++common_it;
|
||||
}
|
||||
@@ -6280,301 +6225,6 @@ 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, ¤t_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:
|
||||
/// @}
|
||||
|
||||
////////////////////////////////
|
||||
|
||||
@@ -19931,6 +19931,22 @@ 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
|
||||
*/
|
||||
@@ -20071,7 +20087,7 @@ class binary_writer
|
||||
case value_t::string:
|
||||
{
|
||||
// step 1: write control byte and the string length
|
||||
const auto N = j.m_data.m_value.string->size();
|
||||
const auto N = to_msgpack_length(j.m_data.m_value.string->size(), j);
|
||||
if (N <= 31)
|
||||
{
|
||||
// fixstr
|
||||
@@ -20089,7 +20105,7 @@ class binary_writer
|
||||
oa.write_character(to_char_type(0xDA));
|
||||
write_number(static_cast<std::uint16_t>(N));
|
||||
}
|
||||
else if (N <= (std::numeric_limits<std::uint32_t>::max)())
|
||||
else
|
||||
{
|
||||
// str 32
|
||||
oa.write_character(to_char_type(0xDB));
|
||||
@@ -20106,7 +20122,7 @@ class binary_writer
|
||||
case value_t::array:
|
||||
{
|
||||
// step 1: write control byte and the array size
|
||||
const auto N = j.m_data.m_value.array->size();
|
||||
const auto N = to_msgpack_length(j.m_data.m_value.array->size(), j);
|
||||
if (N <= 15)
|
||||
{
|
||||
// fixarray
|
||||
@@ -20118,7 +20134,7 @@ class binary_writer
|
||||
oa.write_character(to_char_type(0xDC));
|
||||
write_number(static_cast<std::uint16_t>(N));
|
||||
}
|
||||
else if (N <= (std::numeric_limits<std::uint32_t>::max)())
|
||||
else
|
||||
{
|
||||
// array 32
|
||||
oa.write_character(to_char_type(0xDD));
|
||||
@@ -20140,7 +20156,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 = j.m_data.m_value.binary->size();
|
||||
const auto N = to_msgpack_length(j.m_data.m_value.binary->size(), j);
|
||||
if (N <= (std::numeric_limits<std::uint8_t>::max)())
|
||||
{
|
||||
std::uint8_t output_type{};
|
||||
@@ -20192,7 +20208,7 @@ class binary_writer
|
||||
oa.write_character(to_char_type(output_type));
|
||||
write_number(static_cast<std::uint16_t>(N));
|
||||
}
|
||||
else if (N <= (std::numeric_limits<std::uint32_t>::max)())
|
||||
else
|
||||
{
|
||||
const std::uint8_t output_type = use_ext
|
||||
? 0xC9 // ext 32
|
||||
@@ -20224,7 +20240,7 @@ class binary_writer
|
||||
case value_t::object:
|
||||
{
|
||||
// step 1: write control byte and the object size
|
||||
const auto N = j.m_data.m_value.object->size();
|
||||
const auto N = to_msgpack_length(j.m_data.m_value.object->size(), j);
|
||||
if (N <= 15)
|
||||
{
|
||||
// fixmap
|
||||
@@ -20236,7 +20252,7 @@ class binary_writer
|
||||
oa.write_character(to_char_type(0xDE));
|
||||
write_number(static_cast<std::uint16_t>(N));
|
||||
}
|
||||
else if (N <= (std::numeric_limits<std::uint32_t>::max)())
|
||||
else
|
||||
{
|
||||
// map 32
|
||||
oa.write_character(to_char_type(0xDF));
|
||||
@@ -30951,56 +30967,6 @@ 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);
|
||||
@@ -31011,11 +30977,6 @@ 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
|
||||
@@ -31035,7 +30996,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_recursively(source[i], target[i], detail::concat<string_t>(path, '/', detail::to_string<string_t>(i)), depth + 1);
|
||||
auto temp_diff = diff(source[i], target[i], detail::concat<string_t>(path, '/', detail::to_string<string_t>(i)));
|
||||
result.insert(result.end(), temp_diff.begin(), temp_diff.end());
|
||||
++i;
|
||||
}
|
||||
@@ -31154,7 +31115,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_recursively(it.value(), target[it.key()], path_key, depth + 1);
|
||||
auto temp_diff = diff(it.value(), target[it.key()], path_key);
|
||||
result.insert(result.end(), temp_diff.begin(), temp_diff.end());
|
||||
++common_it;
|
||||
}
|
||||
@@ -31238,301 +31199,6 @@ 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, ¤t_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:
|
||||
/// @}
|
||||
|
||||
////////////////////////////////
|
||||
|
||||
@@ -15,65 +15,8 @@ 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")
|
||||
@@ -1808,99 +1751,3 @@ 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ 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>
|
||||
@@ -2150,3 +2151,65 @@ 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user