Compare commits

...
Author SHA1 Message Date
Niels Lohmann 2241af3297 Declare the partner iterator in the loop initializer in diff_object_keys()
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-16 19:38:26 +02:00
Niels Lohmann f941cf7a7e Use std::array for the index buffer in append_index()
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-16 19:38:25 +02:00
Niels Lohmann 994099ca28 Diff objects with the same keys in lockstep and keep one path buffer
diff() looked up every key of the source object in the target object,
and built a new string for the path of every value it descended into.
Objects of the same size usually have the same keys in the same order,
so they are now walked together as long as that holds, without a
lookup; objects that differ fall back to the lookup from the first key
that does not match. The path is one buffer that is extended before
descending and shortened again afterwards. Objects whose members are
equal but in a different order (possible with ordered_json) still get
no operations.

Together with the previous commit, diff() of canada.json,
citm_catalog.json and twitter.json is 1.5x to 2.0x as fast; ordered_json
1.65x to 1.85x. The output is unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-16 19:38:24 +02:00
Niels Lohmann 80407e8316 Build diff operations member by member
diff() created every operation from an initializer list, so each
{"op", "replace"} pair became a temporary array with two heap strings
before it was turned into an object member; an operation took about 25
allocations. The operations are now built as objects whose members are
assigned one by one. Paths of members and elements are also built in
place, without a temporary string for each escaped key or array index.

diff() of canada.json, citm_catalog.json and twitter.json, with every
eighth scalar changed, is 1.3x to 1.6x as fast; the output is unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-16 19:38:23 +02:00
Niels Lohmann 72c45349c9 Apply plain JSON Patch operations without building a json_pointer
patch() built a json_pointer for every operation, which splits the path
into a vector of strings, and for "add" and "remove" a second one for
the parent. That took 20% to 45% of the time not spent copying the
document.

"add", "remove", "replace" and "test" with a path that has no escapes and
denotes existing values (the parent, for "add"), as diff() creates them,
are now applied by resolving the path in place, reusing one string for
the reference tokens. Every other operation, and every operation that
would fail, takes the existing code, so the results, the exceptions and
the state left after a failing operation are unchanged.

patch() of the diffs of canada.json, citm_catalog.json and twitter.json
is 1.02x to 1.10x as fast; the rest is the copy of the document.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-16 19:38:22 +02:00
Niels Lohmann 7ed54e3d59 Unflatten without splitting keys or walking each path from the root
unflatten() split every key into a vector of reference tokens, stored the
pointer prefixes in a tree to decide which values become arrays, and then
looked up every token of every key from the root again.

Keys are now checked by a scan that raises the same parse_error.107/108 as
split(), skipping the part a key shares with the previous one, which was
checked already. The prefixes that denote arrays are kept in a small hash
set of key slices; the array/object decision is unchanged. The walk keeps
the values along the previous key's path, so only the part of a key that
differs from the previous one is looked up. Keys of a flattened object are
usually sorted, so that part is short.

unflatten() of canada.json, citm_catalog.json and twitter.json is 1.9x to
2.3x as fast; the output and the exceptions are unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-16 19:38:21 +02:00
Niels Lohmann 5f2d976129 Flatten into a std::map by inserting next to the previous entry
flatten() added every path with operator[], so each insertion into the
std::map of the result searched the tree from the root and compared long
shared path prefixes, which took half the time on canada.json. Paths come
out in nearly sorted order, so they are now inserted with emplace_hint
next to the entry added last, for object types that have it. ordered_map
keeps its append without lookup, and other object types keep
operator[].

Path segments are also appended in place, without a temporary string
for each escaped key or array index.

flatten() of canada.json, citm_catalog.json and twitter.json is 1.4x to
1.9x as fast; ordered_json is unchanged (1.05x). The output is
unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-16 19:37:46 +02:00
3 changed files with 1232 additions and 426 deletions

No files matched your search

+375 -183
View File
@@ -9,10 +9,13 @@
#pragma once #pragma once
#include <algorithm> // all_of #include <algorithm> // all_of
#include <array> // array
#include <cctype> // isdigit #include <cctype> // isdigit
#include <cerrno> // errno, ERANGE #include <cerrno> // errno, ERANGE
#include <cstddef> // size_t #include <cstddef> // size_t
#include <cstdint> // uint64_t
#include <cstdlib> // strtoull #include <cstdlib> // strtoull
#include <cstring> // memcmp, memcpy
#ifndef JSON_NO_IO #ifndef JSON_NO_IO
#include <iosfwd> // ostream #include <iosfwd> // ostream
#endif // JSON_NO_IO #endif // JSON_NO_IO
@@ -492,50 +495,111 @@ class json_pointer
private: private:
/*! /*!
@brief the pointer prefixes of a flattened object, and which of them denote arrays @brief the pointer prefixes of a flattened object that denote arrays
@ref unflatten collects the pointer prefixes that have a reference token 0 @ref unflatten creates arrays exactly below the prefixes that have a
among their children; @ref get_and_create creates arrays exactly below reference token 0 among their children, and objects everywhere else.
those prefixes and objects everywhere else. Deciding this up front keeps Deciding this up front keeps the result independent of the order in which
the result independent of the order in which the flattened object is the flattened object is iterated, which is unspecified for some object
iterated, which is unspecified for some object types. types.
The prefixes form a tree and are numbered (number 0 is the empty prefix), A prefix is identified by the text of the key it is the beginning of:
so each of them is stored only once (as a node) rather than as a copy of escaping is unambiguous, so equal prefixes of valid keys are equal texts.
all of its reference tokens. The tree does not own the reference tokens: The set refers to the keys, which must outlive it.
it refers to the tokens of the parsed keys, which must outlive it.
*/ */
struct prefix_tree class array_prefix_set
{ {
// open-addressing hash table from (parent, last reference token) to public:
// the number of the node; empty slots have token == nullptr void insert(const char* data, std::size_t size)
{
// flattened objects are usually sorted, so most keys that have a
// 0 yield the prefix of the previous one
if (last_data != nullptr && last_size == size && std::memcmp(last_data, data, size) == 0)
{
return;
}
last_data = data;
last_size = size;
if (slots.empty())
{
slots.resize(16);
}
const std::size_t hash = hash_of(data, size);
const std::size_t mask = slots.size() - 1;
std::size_t i = hash & mask;
while (slots[i].data != nullptr)
{
if (slots[i].hash == hash && slots[i].size == size && std::memcmp(slots[i].data, data, size) == 0)
{
return;
}
i = (i + 1) & mask;
}
slots[i] = slot(hash, data, size);
// keep the load factor below 1/2
if (++used * 2 > slots.size())
{
grow();
}
}
bool empty() const noexcept
{
return used == 0;
}
bool contains(const char* data, std::size_t size) const noexcept
{
if (used == 0)
{
return false;
}
const std::size_t hash = hash_of(data, size);
const std::size_t mask = slots.size() - 1;
for (std::size_t i = hash & mask; slots[i].data != nullptr; i = (i + 1) & mask)
{
if (slots[i].hash == hash && slots[i].size == size && std::memcmp(slots[i].data, data, size) == 0)
{
return true;
}
}
return false;
}
private:
struct slot struct slot
{ {
std::size_t hash; slot() noexcept = default;
std::size_t parent;
const string_t* token;
std::size_t id;
slot() noexcept slot(std::size_t h, const char* d, std::size_t s) noexcept
: hash(0), parent(0), token(nullptr), id(0) : hash(h), data(d), size(s)
{} {}
slot(std::size_t h, std::size_t p, const string_t* t, std::size_t i) noexcept std::size_t hash = 0;
: hash(h), parent(p), token(t), id(i) const char* data = nullptr; // nullptr: the slot is empty
{} std::size_t size = 0;
}; };
std::vector<slot> slots; static std::size_t hash_of(const char* data, std::size_t size) noexcept
std::size_t used = 0;
static std::size_t hash_of(std::size_t parent, const string_t& token) noexcept
{ {
std::size_t h = parent * static_cast<std::size_t>(0x9E3779B97F4A7C15ULL & (std::numeric_limits<std::size_t>::max)()); std::uint64_t h = 0x9E3779B97F4A7C15ULL ^ size;
for (std::size_t i = 0; i < token.size(); ++i) std::size_t i = 0;
for (; i + 8 <= size; i += 8)
{ {
h = (h ^ static_cast<unsigned char>(token[i])) * static_cast<std::size_t>(0x100000001B3ULL & (std::numeric_limits<std::size_t>::max)()); std::uint64_t w = 0;
std::memcpy(&w, data + i, 8);
h = (h ^ w) * 0x100000001B3ULL;
h ^= h >> 32u;
} }
return h ^ (h >> 29u); std::uint64_t w = 0;
if (i < size)
{
std::memcpy(&w, data + i, size - i);
}
h = (h ^ w) * 0x9E3779B97F4A7C15ULL;
return static_cast<std::size_t>(h ^ (h >> 29u));
} }
void grow() void grow()
@@ -545,10 +609,10 @@ class json_pointer
const std::size_t mask = slots.size() - 1; const std::size_t mask = slots.size() - 1;
for (const auto& s : old) for (const auto& s : old)
{ {
if (s.token != nullptr) if (s.data != nullptr)
{ {
std::size_t i = s.hash & mask; std::size_t i = s.hash & mask;
while (slots[i].token != nullptr) while (slots[i].data != nullptr)
{ {
i = (i + 1) & mask; i = (i + 1) & mask;
} }
@@ -557,117 +621,160 @@ class json_pointer
} }
} }
// parent[id] is the number of the prefix without its last token std::vector<slot> slots{};
std::vector<std::size_t> parent; std::size_t used = 0;
// is_array[id] is true iff some flattened key has the reference token const char* last_data = nullptr;
// 0 directly below the prefix with number id std::size_t last_size = 0;
std::vector<bool> is_array;
// start with the empty prefix only
prefix_tree()
: slots(16)
, parent(1, 0)
, is_array(1, false)
{}
// return the number of the prefix with number id extended by
// reference_token, adding it if it is new
std::size_t add_child(std::size_t id, const string_t& reference_token)
{
const std::size_t hash = hash_of(id, reference_token);
const std::size_t mask = slots.size() - 1;
std::size_t i = hash & mask;
while (slots[i].token != nullptr)
{
if (slots[i].hash == hash && slots[i].parent == id && *slots[i].token == reference_token)
{
return slots[i].id;
}
i = (i + 1) & mask;
}
// new prefix
const std::size_t child = parent.size();
slots[i] = slot(hash, id, &reference_token, child);
if (reference_token == "0")
{
is_array[id] = true;
}
parent.push_back(id);
is_array.push_back(false);
// keep the load factor below 1/2
if (++used * 2 > slots.size())
{
grow();
}
return child;
}
}; };
/// @brief the length of the longest common prefix of the first @a limit bytes of @a a and @a b
static std::size_t common_prefix_length(const char* a, const char* b, const std::size_t limit) noexcept
{
std::size_t i = 0;
for (; i + 8 <= limit; i += 8)
{
std::uint64_t x = 0;
std::uint64_t y = 0;
std::memcpy(&x, a + i, 8);
std::memcpy(&y, b + i, 8);
if (x != y)
{
break;
}
}
while (i < limit && a[i] == b[i])
{
++i;
}
return i;
}
/*! /*!
@brief create and return a reference to the pointed to value @brief check that @a key is a valid JSON pointer, and add the prefixes of
it that denote arrays to @a arrays
@param[in] j the value to create the pointed to value in @throw parse_error.107 if the key is not empty or begins with '/'
@param[in] tokens the reference tokens of the pointer @throw parse_error.108 if character '~' is not followed by '0' or '1'
@param[in] prefixes prefixes[i] is the number in @a tree of the prefix
consisting of the first i reference tokens
Complexity: Linear in the number of reference tokens. The same checks as @ref split, which throws the same exceptions, but
without building the reference tokens.
*/
static void scan_key(const string_t& key, const string_t* previous, array_prefix_set& arrays)
{
const std::size_t size = key.size();
if (size == 0)
{
return;
}
@throw parse_error.106 if an array index begins with '0' const char* const data = key.data();
@throw parse_error.109 if array index is not a number if (JSON_HEDLEY_UNLIKELY(data[0] != '/'))
@throw type_error.313 if value cannot be unflattened {
JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_missing_slash, 1, detail::concat("JSON pointer must be empty or begin with '/' - was: '", key, "'"), nullptr));
}
// The beginning of the key that it has in common with the previous
// one, which was checked already, is skipped: the scan starts with
// the token after the last slash in it.
std::size_t begin = 1;
if (previous != nullptr)
{
std::size_t same = common_prefix_length(data, previous->data(), (std::min)(size, previous->size()));
while (same > 1 && data[same - 1] != '/')
{
--same;
}
if (same > 1)
{
begin = same;
}
}
for (std::size_t i = begin; i <= size; ++i)
{
if (i == size || data[i] == '/')
{
// a token 0 makes the pointer before it an array
if (i - begin == 1 && data[begin] == '0')
{
arrays.insert(data, begin - 1);
}
begin = i + 1;
}
else if (data[i] == '~')
{
// ~ must be followed by 0 or 1
if (JSON_HEDLEY_UNLIKELY(i + 1 == size || (data[i + 1] != '0' && data[i + 1] != '1')))
{
JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_invalid_escape, 0, "escape character '~' must be followed with '0' or '1'", nullptr));
}
}
}
}
/*!
@brief resolve a plain JSON pointer without building its reference tokens
@param[in] ptr the value the pointer starts at
@param[in] path a JSON pointer string
@param[in] length the number of leading bytes of @a path to resolve; 0 or
the position of a slash
@param[in,out] token a string to hold the reference tokens, to reuse its storage
@return the pointed to value, or `nullptr` if the pointer does not denote
an existing value, or does so in a way that is not plain: a
reference token with an escape, an array index that is malformed or
out of range, or a "-". No exception is thrown for these; callers
use @ref json_pointer instead, which handles them.
*/ */
template<typename BasicJsonType> template<typename BasicJsonType>
static BasicJsonType& get_and_create(BasicJsonType& j, const std::vector<string_t>& tokens, static BasicJsonType* resolve_plain(BasicJsonType* ptr, const string_t& path, const std::size_t length, string_t& token)
const std::vector<std::size_t>& prefixes, const prefix_tree& tree)
{ {
auto* result = &j; for (std::size_t pos = 0; pos < length;)
// in case no reference tokens exist, return a reference to the JSON value
// j which will be overwritten by a primitive value
for (std::size_t i = 0; i < tokens.size(); ++i)
{ {
const auto& reference_token = tokens[i]; if (JSON_HEDLEY_UNLIKELY(path[pos] != '/'))
switch (result->type())
{ {
case detail::value_t::null: return nullptr;
{ }
if (tree.is_array[prefixes[i]])
{
// some reference token below this position is 0, so the
// value is an array
result = &result->operator[](array_index<BasicJsonType>(reference_token));
}
else
{
// start a new object otherwise
result = &result->operator[](reference_token);
}
break;
}
// the reference token between the slash at pos and the next one
std::size_t end = path.find_first_of('/', pos + 1);
if (end == string_t::npos || end > length)
{
end = length;
}
token.clear();
token.append(path.data() + pos + 1, end - pos - 1);
if (JSON_HEDLEY_UNLIKELY(token.find_first_of('~') != string_t::npos))
{
return nullptr;
}
switch (ptr->type())
{
case detail::value_t::object: case detail::value_t::object:
{ {
// create an entry in the object const auto it = ptr->m_data.m_value.object->find(token);
result = &object_member(*result, reference_token); if (JSON_HEDLEY_UNLIKELY(it == ptr->m_data.m_value.object->end()))
{
return nullptr;
}
ptr = &it->second;
break; break;
} }
case detail::value_t::array: case detail::value_t::array:
{ {
// create an entry in the array typename BasicJsonType::size_type idx{};
result = &result->operator[](array_index<BasicJsonType>(reference_token)); if (JSON_HEDLEY_UNLIKELY(parse_array_index<BasicJsonType>(token, idx) != array_index_status::ok || idx >= ptr->m_data.m_value.array->size()))
{
return nullptr;
}
ptr = &(*ptr->m_data.m_value.array)[idx];
break; break;
} }
/* case detail::value_t::null:
The following code is only reached if there exists a reference
token _and_ the current value is primitive. In this case, we have
an error situation, because primitive values may only occur as
a single value; that is, with an empty list of reference tokens.
*/
case detail::value_t::string: case detail::value_t::string:
case detail::value_t::boolean: case detail::value_t::boolean:
case detail::value_t::number_integer: case detail::value_t::number_integer:
@@ -676,11 +783,12 @@ class json_pointer
case detail::value_t::binary: case detail::value_t::binary:
case detail::value_t::discarded: case detail::value_t::discarded:
default: default:
JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_invalid_value, "invalid value to unflatten", &j)); return nullptr;
} }
pos = end;
} }
return *result; return ptr;
} }
/*! /*!
@@ -1135,20 +1243,45 @@ class json_pointer
// Add the entry (key, v) to the object `obj`. Flattened paths are pairwise // Add the entry (key, v) to the object `obj`. Flattened paths are pairwise
// distinct, so objects that can append a key known to be absent // distinct, so objects that can append a key known to be absent
// (ordered_map) skip the lookup, which is linear and would make flatten // (ordered_map) skip the lookup, which is linear and would make flatten
// quadratic; all other object types use operator[]. // quadratic. Objects with a positional insert (std::map) insert next to
// the entry added last: paths come out in nearly sorted order, so the
// position is usually right and no search from the root is needed.
// All other object types use operator[].
template<typename ObjectType, typename ValueType> template<typename ObjectType, typename ValueType>
static auto add_flattened(ObjectType& obj, const string_t& key, ValueType&& v, detail::priority_tag<1> /*unused*/) static auto add_flattened(ObjectType& obj, typename ObjectType::iterator& /*hint*/, const string_t& key, ValueType&& v, detail::priority_tag<2> /*unused*/)
-> decltype(obj.emplace_new_key(key, std::forward<ValueType>(v)), void()) -> decltype(obj.emplace_new_key(key, std::forward<ValueType>(v)), void())
{ {
obj.emplace_new_key(key, std::forward<ValueType>(v)); obj.emplace_new_key(key, std::forward<ValueType>(v));
} }
template<typename ObjectType, typename ValueType> template<typename ObjectType, typename ValueType>
static void add_flattened(ObjectType& obj, const string_t& key, ValueType&& v, detail::priority_tag<0> /*unused*/) static auto add_flattened(ObjectType& obj, typename ObjectType::iterator& hint, const string_t& key, ValueType&& v, detail::priority_tag<1> /*unused*/)
-> decltype(hint = obj.emplace_hint(hint, key, std::forward<ValueType>(v)), void())
{
hint = obj.emplace_hint(hint, key, std::forward<ValueType>(v));
++hint;
}
template<typename ObjectType, typename ValueType>
static void add_flattened(ObjectType& obj, typename ObjectType::iterator& /*hint*/, const string_t& key, ValueType&& v, detail::priority_tag<0> /*unused*/)
{ {
obj[key] = std::forward<ValueType>(v); obj[key] = std::forward<ValueType>(v);
} }
/// append the decimal representation of @a i to @a out
static void append_index(string_t& out, std::size_t i)
{
std::array<char, 24> buf{};
std::size_t pos = buf.size();
do
{
buf[--pos] = static_cast<char>('0' + i % 10);
i /= 10;
}
while (i != 0);
out.append(buf.data() + pos, buf.size() - pos);
}
/*! /*!
@param[in] reference_string the reference string to the current value @param[in] reference_string the reference string to the current value
@param[in] value the value to consider @param[in] value the value to consider
@@ -1194,7 +1327,8 @@ class json_pointer
// flatten `v`, whose path is `path`: primitives and empty containers // flatten `v`, whose path is `path`: primitives and empty containers
// are added to the result right away; other containers get a frame // are added to the result right away; other containers get a frame
auto& object = *result.m_data.m_value.object; auto& object = *result.m_data.m_value.object;
const auto enter = [&stack, &path, &object](const BasicJsonType & v) typename BasicJsonType::object_t::iterator hint = object.end();
const auto enter = [&stack, &path, &object, &hint](const BasicJsonType & v)
{ {
switch (v.type()) switch (v.type())
{ {
@@ -1203,7 +1337,7 @@ class json_pointer
if (v.m_data.m_value.array->empty()) if (v.m_data.m_value.array->empty())
{ {
// flatten empty array as null // flatten empty array as null
add_flattened(object, path, nullptr, detail::priority_tag<1> {}); add_flattened(object, hint, path, nullptr, detail::priority_tag<2> {});
} }
else else
{ {
@@ -1217,7 +1351,7 @@ class json_pointer
if (v.m_data.m_value.object->empty()) if (v.m_data.m_value.object->empty())
{ {
// flatten empty object as null // flatten empty object as null
add_flattened(object, path, nullptr, detail::priority_tag<1> {}); add_flattened(object, hint, path, nullptr, detail::priority_tag<2> {});
} }
else else
{ {
@@ -1237,7 +1371,7 @@ class json_pointer
default: default:
{ {
// add a primitive value with its reference string // add a primitive value with its reference string
add_flattened(object, path, v, detail::priority_tag<1> {}); add_flattened(object, hint, path, v, detail::priority_tag<2> {});
return; return;
} }
} }
@@ -1265,7 +1399,8 @@ class json_pointer
// iterate array and use index as a reference string // iterate array and use index as a reference string
++stack.back().index; ++stack.back().index;
detail::concat_into(path, '/', detail::to_string<string_t>(i)); path.push_back('/');
append_index(path, i);
enter(array[i]); enter(array[i]);
} }
else else
@@ -1279,7 +1414,8 @@ class json_pointer
// iterate object and use keys as reference string // iterate object and use keys as reference string
++stack.back().member; ++stack.back().member;
detail::concat_into(path, '/', detail::escape(it->first)); path.push_back('/');
detail::escape_into(path, it->first);
enter(it->second); enter(it->second);
} }
} }
@@ -1310,53 +1446,30 @@ class json_pointer
BasicJsonType result; BasicJsonType result;
// parse every key once; the reference tokens are kept for the second // check every key, and collect the prefixes that have a reference
// pass (the vectors are never resized, so the tree may refer to them) // token 0 among their children; the values below them are arrays,
std::vector<std::vector<string_t>> keys; // all others are objects (see array_prefix_set)
keys.reserve(value.m_data.m_value.object->size()); array_prefix_set arrays;
const string_t* previous_key = nullptr;
for (const auto& element : *value.m_data.m_value.object) for (const auto& element : *value.m_data.m_value.object)
{ {
json_pointer ptr(element.first); scan_key(element.first, previous_key, arrays);
keys.push_back(std::move(ptr.reference_tokens)); previous_key = &element.first;
} }
// collect the pointer prefixes that have a reference token 0 among // values[i] is the value after the first i reference tokens of the
// their children; the values below them are arrays, all others are // previous key, ends[i] the length of that prefix of the key. Keys are
// objects (see prefix_tree) // often sorted, so most of the path to the value of a key is the
prefix_tree tree; // same as for the previous key and need not be looked up again. Only
// the number of the prefix with i + 1 tokens of the previous key; keys // the values below the common prefix are dropped, and only they can
// are often sorted, so most of the prefix is the same as for the // move when a value is added: the ones above are not inside the
// previous key and need not be looked up again // container that grows.
std::vector<std::size_t> path; std::vector<BasicJsonType*> values(1, &result);
const std::vector<string_t>* previous = nullptr; std::vector<std::size_t> ends(1, 0);
// the number of the complete prefix (i.e., the key) of each key const string_t* previous = nullptr;
std::vector<std::size_t> leaves; string_t token;
leaves.reserve(keys.size());
for (const auto& tokens : keys)
{
std::size_t common = 0;
if (previous != nullptr)
{
const std::size_t limit = (std::min)(tokens.size(), previous->size());
while (common < limit && tokens[common] == (*previous)[common])
{
++common;
}
}
path.resize(common);
std::size_t id = common == 0 ? 0 : path[common - 1];
for (std::size_t i = common; i < tokens.size(); ++i)
{
id = tree.add_child(id, tokens[i]);
path.push_back(id);
}
leaves.push_back(id);
previous = &tokens;
}
// iterate the JSON object values // iterate the JSON object values
std::vector<std::size_t> prefixes;
std::size_t key_index = 0;
for (const auto& element : *value.m_data.m_value.object) for (const auto& element : *value.m_data.m_value.object)
{ {
if (JSON_HEDLEY_UNLIKELY(!element.second.is_primitive())) if (JSON_HEDLEY_UNLIKELY(!element.second.is_primitive()))
@@ -1364,21 +1477,100 @@ class json_pointer
JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_value_not_primitive, "values in object must be primitive", &element.second)); JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_value_not_primitive, "values in object must be primitive", &element.second));
} }
const auto& tokens = keys[key_index]; const string_t& key = element.first;
// number the prefixes of this key from its last one to the first const char* const data = key.data();
prefixes.resize(tokens.size());
for (std::size_t i = tokens.size(), id = leaves[key_index]; i > 0; --i) // the number of reference tokens the key has in common with the
// previous one
std::size_t depth = 0;
if (previous != nullptr)
{ {
id = tree.parent[id]; const std::size_t same = common_prefix_length(data, previous->data(), (std::min)(key.size(), previous->size()));
prefixes[i - 1] = id; depth = ends.size() - 1;
while (depth > 0 && (ends[depth] > same || (ends[depth] == same && same < key.size() && data[same] != '/')))
{
--depth;
}
} }
++key_index; values.resize(depth + 1);
ends.resize(depth + 1);
previous = &key;
// Assign the value to the reference pointed to by JSON pointer. Note // Assign the value to the reference pointed to by JSON pointer. Note
// that if the JSON pointer is "" (i.e., points to the whole value), // that if the JSON pointer is "" (i.e., points to the whole value),
// function get_and_create returns a reference to the result itself. // the value is the result itself. An assignment will then create a
// An assignment will then create a primitive value. // primitive value.
get_and_create(result, tokens, prefixes, tree) = element.second; BasicJsonType* current = values.back();
for (std::size_t pos = ends.back(); pos < key.size();)
{
// the reference token between the slash at pos and the next
std::size_t end = key.find_first_of('/', pos + 1);
if (end == string_t::npos)
{
end = key.size();
}
token.clear();
token.append(data + pos + 1, end - pos - 1);
if (token.find_first_of('~') != string_t::npos)
{
detail::unescape(token);
}
switch (current->type())
{
case detail::value_t::null:
{
if (arrays.contains(data, pos))
{
// some reference token below this position is 0, so the
// value is an array
current = &current->operator[](array_index<BasicJsonType>(token));
}
else
{
// start a new object otherwise
current = &current->operator[](token);
}
break;
}
case detail::value_t::object:
{
// create an entry in the object
current = &object_member(*current, token);
break;
}
case detail::value_t::array:
{
// create an entry in the array
current = &current->operator[](array_index<BasicJsonType>(token));
break;
}
/*
The following code is only reached if there exists a reference
token _and_ the current value is primitive. In this case, we have
an error situation, because primitive values may only occur as
a single value; that is, with an empty list of reference tokens.
*/
case detail::value_t::string:
case detail::value_t::boolean:
case detail::value_t::number_integer:
case detail::value_t::number_unsigned:
case detail::value_t::number_float:
case detail::value_t::binary:
case detail::value_t::discarded:
default:
JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_invalid_value, "invalid value to unflatten", &result));
}
values.push_back(current);
ends.push_back(end);
pos = end;
}
*current = element.second;
} }
return result; return result;
+241 -30
View File
@@ -7066,6 +7066,125 @@ public:
detail::throw_patch_not_an_array(json_patch); detail::throw_patch_not_an_array(json_patch);
} }
// the usual patch operations "add", "remove", "replace" and "test" on
// a pointer without escapes that denotes existing array elements and
// object members (as "add" does for the parent of the new value), as
// a diff creates them: apply the operation and return true; return
// false, with nothing changed, for all others
string_t token;
const string_t value_key = "value";
const auto try_plain = [&result, &token, &value_key](patch_operations operation, const string_t& path, const basic_json & val)
{
if (path.empty() || path.find_first_of('~') != string_t::npos)
{
return false;
}
// a missing "value" is reported below
const auto value_it = val.m_data.m_value.object->find(value_key);
const basic_json& value = value_it != val.m_data.m_value.object->end() ? value_it->second : val;
if (operation != patch_operations::remove && value_it == val.m_data.m_value.object->end())
{
return false;
}
switch (operation)
{
case patch_operations::replace:
{
basic_json* const target = json_pointer::resolve_plain(&result, path, path.size(), token);
if (target == nullptr)
{
return false;
}
*target = value;
return true;
}
case patch_operations::test:
{
const basic_json* const target = json_pointer::resolve_plain(&result, path, path.size(), token);
// a failing test is reported below
return target != nullptr && *target == value;
}
case patch_operations::add:
case patch_operations::remove:
{
// the parent of the pointer and its last reference token
std::size_t slash = path.size();
while (slash > 0 && path[slash - 1] != '/')
{
--slash;
}
if (slash == 0)
{
return false;
}
--slash;
basic_json* const parent = json_pointer::resolve_plain(&result, path, slash, token);
if (parent == nullptr)
{
return false;
}
token.clear();
token.append(path.data() + slash + 1, path.size() - slash - 1);
if (operation == patch_operations::add)
{
if (parent->is_object())
{
(*parent)[token] = value;
return true;
}
if (parent->is_array())
{
typename basic_json::size_type idx{};
if (token == "-")
{
parent->push_back(value);
return true;
}
if (json_pointer::template parse_array_index<basic_json_t>(token, idx) == json_pointer::array_index_status::ok && idx <= parent->size())
{
parent->insert(parent->begin() + static_cast<difference_type>(idx), value);
return true;
}
}
return false;
}
if (parent->is_object())
{
const auto it = parent->find(token);
if (it == parent->end())
{
return false;
}
parent->erase(it);
return true;
}
if (parent->is_array())
{
typename basic_json::size_type idx{};
if (json_pointer::template parse_array_index<basic_json_t>(token, idx) == json_pointer::array_index_status::ok && idx < parent->size())
{
parent->erase(idx);
return true;
}
}
return false;
}
case patch_operations::move:
case patch_operations::copy:
case patch_operations::invalid:
default:
return false;
}
};
// iterate and apply the operations // iterate and apply the operations
for (const auto& val : json_patch) for (const auto& val : json_patch)
{ {
@@ -7102,9 +7221,19 @@ public:
// collect mandatory members // collect mandatory members
const string_t& op = *get_value("op", "op", true).m_data.m_value.string; const string_t& op = *get_value("op", "op", true).m_data.m_value.string;
const string_t& path = *get_value(op, "path", true).m_data.m_value.string; const string_t& path = *get_value(op, "path", true).m_data.m_value.string;
const patch_operations operation = get_op(op);
// the usual operations on a plain pointer are applied without
// building the pointer's reference tokens; anything else (and
// every error) is handled below
if (try_plain(operation, path, val))
{
continue;
}
json_pointer ptr(path); json_pointer ptr(path);
switch (get_op(op)) switch (operation)
{ {
case patch_operations::add: case patch_operations::add:
{ {
@@ -7212,7 +7341,8 @@ public:
{ {
// the patch // the patch
basic_json result(value_t::array); basic_json result(value_t::array);
diff_recursively(result, source, target, path, 0); string_t buffer = path;
diff_recursively(result, source, target, buffer, 0);
return result; return result;
} }
@@ -7256,31 +7386,69 @@ public:
// function it appears in. In diff_recursively, which is on the call stack // function it appears in. In diff_recursively, which is on the call stack
// once per nesting level, that made every level cost kilobytes of stack. // once per nesting level, that made every level cost kilobytes of stack.
/// @brief append an operation @a op for @a path to @a result, with @a value if it is not null
static void diff_operation(basic_json& result, const char* op, const string_t& path, const basic_json* value)
{
basic_json entry(value_t::object);
entry["op"] = op;
entry["path"] = path;
if (value != nullptr)
{
entry["value"] = *value;
}
result.push_back(std::move(entry));
}
/// @brief append a "replace" operation for @a path with @a value to @a result /// @brief append a "replace" operation for @a path with @a value to @a result
static void diff_replace(basic_json& result, const string_t& path, const basic_json& value) static void diff_replace(basic_json& result, const string_t& path, const basic_json& value)
{ {
result.push_back( diff_operation(result, "replace", path, &value);
{
{"op", "replace"}, {"path", path}, {"value", value}
});
} }
/// @brief append a "remove" operation for @a path to @a result /// @brief append a "remove" operation for @a path to @a result
static void diff_remove(basic_json& result, const string_t& path) static void diff_remove(basic_json& result, const string_t& path)
{ {
result.push_back(object( diff_operation(result, "remove", path, nullptr);
{
{"op", "remove"}, {"path", path}
}));
} }
/// @brief append an "add" operation for @a path with @a value to @a result /// @brief append an "add" operation for @a path with @a value to @a result
static void diff_add(basic_json& result, const string_t& path, const basic_json& value) static void diff_add(basic_json& result, const string_t& path, const basic_json& value)
{ {
result.push_back( diff_operation(result, "add", path, &value);
{ }
{"op", "add"}, {"path", path}, {"value", value}
}); /// @brief append "/" and the decimal representation of @a index to @a path
static void diff_append_index(string_t& path, std::size_t index)
{
path.push_back('/');
json_pointer::append_index(path, index);
}
/// @brief append "/" and the escaped @a key to @a path
static void diff_append_key(string_t& path, const string_t& key)
{
path.push_back('/');
detail::escape_into(path, key);
}
/// @brief the path of the member @a key of the value at @a path
static string_t diff_key_path(const string_t& path, const string_t& key)
{
string_t result;
result.reserve(path.size() + key.size() + 1);
result.append(path.data(), path.size());
diff_append_key(result, key);
return result;
}
/// @brief the path of the element @a index of the value at @a path
static string_t diff_index_path(const string_t& path, const std::size_t index)
{
string_t result;
result.reserve(path.size() + 8);
result.append(path.data(), path.size());
diff_append_index(result, index);
return result;
} }
/// @brief append the "remove" operations for the elements of array /// @brief append the "remove" operations for the elements of array
@@ -7293,13 +7461,18 @@ public:
// in that order avoids the quadratic reinsertion done before // in that order avoids the quadratic reinsertion done before
for (std::size_t j = source.size(); j > index; --j) for (std::size_t j = source.size(); j > index; --j)
{ {
diff_remove(result, detail::concat<string_t>(path, '/', detail::to_string<string_t>(j - 1))); diff_remove(result, diff_index_path(path, j - 1));
} }
// add other remaining elements // add other remaining elements
for (std::size_t i = source.size(); i < target.size(); ++i) if (source.size() < target.size())
{ {
diff_add(result, detail::concat<string_t>(path, "/-"), target[i]); string_t tail_path = path;
tail_path.append("/-", 2);
for (std::size_t i = source.size(); i < target.size(); ++i)
{
diff_add(result, tail_path, target[i]);
}
} }
} }
@@ -7308,7 +7481,7 @@ public:
{ {
for (const auto& it : added) for (const auto& it : added)
{ {
diff_add(result, detail::concat<string_t>(path, '/', detail::escape(it.key())), it.value()); diff_add(result, diff_key_path(path, it.key()), it.value());
} }
} }
@@ -7329,10 +7502,25 @@ public:
std::vector<const_iterator>& added) std::vector<const_iterator>& added)
{ {
// first pass: one lookup per source key // first pass: one lookup per source key
// Objects of the same size usually have the same keys, in the same
// order: walk them together as long as that holds, which takes no lookups.
target_values.reserve(source.size()); target_values.reserve(source.size());
std::size_t common = 0; std::size_t common = 0;
for (auto it = source.cbegin(); it != source.cend(); ++it) bool together = source.size() == target.size();
for (auto it = source.cbegin(), partner = target.cbegin(); it != source.cend(); ++it)
{ {
if (together)
{
if (it.key() == partner.key())
{
target_values.push_back(&partner.value());
++common;
++partner;
continue;
}
together = false;
}
const auto found = target.find(it.key()); const auto found = target.find(it.key());
if (found != target.cend()) if (found != target.cend())
{ {
@@ -7345,6 +7533,12 @@ public:
} }
} }
// the same keys in the same order
if (together)
{
return true;
}
// second pass: find the keys only target has. An object type that // second pass: find the keys only target has. An object type that
// keeps its members in insertion order, such as the one backing // keeps its members in insertion order, such as the one backing
// ordered_json, also needs the common keys to be in the same order // ordered_json, also needs the common keys to be in the same order
@@ -7410,6 +7604,13 @@ public:
return true; return true;
} }
// equal objects, whose members are in a different order, need no
// operations
if (source == target)
{
return false;
}
// slow path: the common keys are in a different relative // slow path: the common keys are in a different relative
// order in source and target (only possible for a // order in source and target (only possible for a
// reorderable object_t like ordered_map). Building a // reorderable object_t like ordered_map). Building a
@@ -7426,7 +7627,7 @@ public:
// it moves it to the end, fixing its position. // it moves it to the end, fixing its position.
for (auto it = source.cbegin(); it != source.cend(); ++it) for (auto it = source.cbegin(); it != source.cend(); ++it)
{ {
diff_remove(result, detail::concat<string_t>(path, '/', detail::escape(it.key()))); diff_remove(result, diff_key_path(path, it.key()));
} }
// add every key that is either common (just removed // add every key that is either common (just removed
@@ -7435,7 +7636,7 @@ public:
// target exactly // target exactly
for (auto it = target.cbegin(); it != target.cend(); ++it) for (auto it = target.cbegin(); it != target.cend(); ++it)
{ {
diff_add(result, detail::concat<string_t>(path, '/', detail::escape(it.key())), it.value()); diff_add(result, diff_key_path(path, it.key()), it.value());
} }
return false; return false;
} }
@@ -7444,6 +7645,9 @@ public:
@brief @ref diff, for values at nesting level @a depth, appending the @brief @ref diff, for values at nesting level @a depth, appending the
operations to @a result operations to @a result
@a path is the path of the values; it is a buffer that is extended and
shortened again below, and is the same when the function returns.
Diffing two arrays or objects calls this function again, once per nesting 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 level, so values nested deeply enough used to exhaust the call stack and
terminate the process. The descent is bounded here: once @ref terminate the process. The descent is bounded here: once @ref
@@ -7451,7 +7655,7 @@ public:
diff_iteratively diffs what is left without the call stack. diff_iteratively diffs what is left without the call stack.
*/ */
static void diff_recursively(basic_json& result, const basic_json& source, const basic_json& target, static void diff_recursively(basic_json& result, const basic_json& source, const basic_json& target,
const string_t& path, const std::size_t depth) string_t& path, const std::size_t depth)
{ {
// if the values are the same, there is nothing to do // if the values are the same, there is nothing to do
if (source == target) if (source == target)
@@ -7463,9 +7667,9 @@ public:
} }
/// @brief @ref diff_recursively for values known to be unequal; callers /// @brief @ref diff_recursively for values known to be unequal; callers
/// compare first so they build no path for equal ones /// compare first so they extend the path only for unequal ones
static void diff_unequal(basic_json& result, const basic_json& source, const basic_json& target, static void diff_unequal(basic_json& result, const basic_json& source, const basic_json& target,
const string_t& path, const std::size_t depth) string_t& path, const std::size_t depth)
{ {
if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit()))
{ {
@@ -7484,6 +7688,8 @@ public:
{ {
case value_t::array: case value_t::array:
{ {
const std::size_t path_length = path.size();
// first pass: traverse common elements // first pass: traverse common elements
std::size_t i = 0; std::size_t i = 0;
while (i < source.size() && i < target.size()) while (i < source.size() && i < target.size())
@@ -7491,7 +7697,9 @@ public:
// recursive call to compare array values at index i // recursive call to compare array values at index i
if (!(source[i] == target[i])) if (!(source[i] == target[i]))
{ {
diff_unequal(result, source[i], target[i], detail::concat<string_t>(path, '/', detail::to_string<string_t>(i)), depth + 1); diff_append_index(path, i);
diff_unequal(result, source[i], target[i], path, depth + 1);
path.resize(path_length);
} }
++i; ++i;
} }
@@ -7513,6 +7721,7 @@ public:
// lookup is needed. Deleted keys are interleaved here // lookup is needed. Deleted keys are interleaved here
// too, in source's original order, to match the // too, in source's original order, to match the
// historical (pre-reordering-aware) output order. // historical (pre-reordering-aware) output order.
const std::size_t path_length = path.size();
std::size_t i = 0; std::size_t i = 0;
for (auto it = source.cbegin(); it != source.cend(); ++it, ++i) for (auto it = source.cbegin(); it != source.cend(); ++it, ++i)
{ {
@@ -7520,13 +7729,15 @@ public:
{ {
if (!(it.value() == *target_values[i])) if (!(it.value() == *target_values[i]))
{ {
diff_unequal(result, it.value(), *target_values[i], detail::concat<string_t>(path, '/', detail::escape(it.key())), depth + 1); diff_append_key(path, it.key());
diff_unequal(result, it.value(), *target_values[i], path, depth + 1);
path.resize(path_length);
} }
} }
else else
{ {
// found a key that is not in target -> remove it // found a key that is not in target -> remove it
diff_remove(result, detail::concat<string_t>(path, '/', detail::escape(it.key()))); diff_remove(result, diff_key_path(path, it.key()));
} }
} }
@@ -7657,7 +7868,7 @@ public:
if (i < source_array.size() && i < target_array.size()) if (i < source_array.size() && i < target_array.size())
{ {
++stack.back().index; ++stack.back().index;
detail::concat_into(current_path, '/', detail::to_string<string_t>(i)); diff_append_index(current_path, i);
enter(source_array[i], target_array[i]); enter(source_array[i], target_array[i]);
if (stack.size() == depth) if (stack.size() == depth)
{ {
@@ -7680,7 +7891,7 @@ public:
if (target_value_ptr != nullptr) if (target_value_ptr != nullptr)
{ {
const basic_json& target_value = *target_value_ptr; const basic_json& target_value = *target_value_ptr;
detail::concat_into(current_path, '/', detail::escape(it.key())); diff_append_key(current_path, it.key());
enter(it.value(), target_value); enter(it.value(), target_value);
if (stack.size() == depth) if (stack.size() == depth)
{ {
@@ -7690,7 +7901,7 @@ public:
else else
{ {
// found a key that is not in target -> remove it // found a key that is not in target -> remove it
diff_remove(result, detail::concat<string_t>(current_path, '/', detail::escape(it.key()))); diff_remove(result, diff_key_path(current_path, it.key()));
} }
continue; continue;
} }
+616 -213
View File
File diff suppressed because it is too large. Load diff