diff --git a/include/nlohmann/detail/json_pointer.hpp b/include/nlohmann/detail/json_pointer.hpp index 66363f3ce..a382ae6ef 100644 --- a/include/nlohmann/detail/json_pointer.hpp +++ b/include/nlohmann/detail/json_pointer.hpp @@ -9,10 +9,13 @@ #pragma once #include // all_of +#include // array #include // isdigit #include // errno, ERANGE #include // size_t +#include // uint64_t #include // strtoull +#include // memcmp, memcpy #ifndef JSON_NO_IO #include // ostream #endif // JSON_NO_IO @@ -24,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -499,50 +503,111 @@ class json_pointer 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 - among their children; @ref get_and_create creates arrays exactly below - those prefixes and objects everywhere else. Deciding this up front keeps - the result independent of the order in which the flattened object is - iterated, which is unspecified for some object types. + @ref unflatten creates arrays exactly below the prefixes that have a + reference token 0 among their children, and objects everywhere else. + Deciding this up front keeps the result independent of the order in which + the flattened object is iterated, which is unspecified for some object + types. - The prefixes form a tree and are numbered (number 0 is the empty prefix), - so each of them is stored only once (as a node) rather than as a copy of - all of its reference tokens. The tree does not own the reference tokens: - it refers to the tokens of the parsed keys, which must outlive it. + A prefix is identified by the text of the key it is the beginning of: + escaping is unambiguous, so equal prefixes of valid keys are equal texts. + The set refers to the keys, which must outlive it. */ - struct prefix_tree + class array_prefix_set { - // open-addressing hash table from (parent, last reference token) to - // the number of the node; empty slots have token == nullptr + public: + 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 (table.empty()) + { + table.resize(16); + } + const std::size_t hash = hash_of(data, size); + const std::size_t mask = table.size() - 1; + std::size_t i = hash & mask; + while (table[i].data != nullptr) + { + if (table[i].hash == hash && table[i].size == size && std::memcmp(table[i].data, data, size) == 0) + { + return; + } + i = (i + 1) & mask; + } + table[i] = slot(hash, data, size); + + // keep the load factor below 1/2 + if (++used * 2 > table.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 = table.size() - 1; + for (std::size_t i = hash & mask; table[i].data != nullptr; i = (i + 1) & mask) + { + if (table[i].hash == hash && table[i].size == size && std::memcmp(table[i].data, data, size) == 0) + { + return true; + } + } + return false; + } + + private: struct slot { - std::size_t hash; - std::size_t parent; - const string_t* token; - std::size_t id; + slot() noexcept = default; - slot() noexcept - : hash(0), parent(0), token(nullptr), id(0) + slot(std::size_t h, const char* d, std::size_t s) noexcept + : hash(h), data(d), size(s) {} - slot(std::size_t h, std::size_t p, const string_t* t, std::size_t i) noexcept - : hash(h), parent(p), token(t), id(i) - {} + std::size_t hash = 0; + const char* data = nullptr; // nullptr: the slot is empty + std::size_t size = 0; }; - std::vector table; - std::size_t used = 0; - - static std::size_t hash_of(std::size_t parent, const string_t& token) noexcept + static std::size_t hash_of(const char* data, std::size_t size) noexcept { - std::size_t h = parent * static_cast(0x9E3779B97F4A7C15ULL & (std::numeric_limits::max)()); - for (std::size_t i = 0; i < token.size(); ++i) + std::uint64_t h = 0x9E3779B97F4A7C15ULL ^ size; + std::size_t i = 0; + for (; i + 8 <= size; i += 8) { - h = (h ^ static_cast(token[i])) * static_cast(0x100000001B3ULL & (std::numeric_limits::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 detail::conditional_static_cast(h ^ (h >> 29u)); } void grow() @@ -552,10 +617,10 @@ class json_pointer const std::size_t mask = table.size() - 1; for (const auto& s : old) { - if (s.token != nullptr) + if (s.data != nullptr) { std::size_t i = s.hash & mask; - while (table[i].token != nullptr) + while (table[i].data != nullptr) { i = (i + 1) & mask; } @@ -564,117 +629,160 @@ class json_pointer } } - // parent[id] is the number of the prefix without its last token - std::vector parent; - // is_array[id] is true iff some flattened key has the reference token - // 0 directly below the prefix with number id - std::vector is_array; - - // start with the empty prefix only - prefix_tree() - : table(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 = table.size() - 1; - std::size_t i = hash & mask; - while (table[i].token != nullptr) - { - if (table[i].hash == hash && table[i].parent == id && *table[i].token == reference_token) - { - return table[i].id; - } - i = (i + 1) & mask; - } - - // new prefix - const std::size_t child = parent.size(); - table[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 > table.size()) - { - grow(); - } - return child; - } + std::vector table{}; + std::size_t used = 0; + const char* last_data = nullptr; + std::size_t last_size = 0; }; + /// @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 - @param[in] tokens the reference tokens of the pointer - @param[in] prefixes prefixes[i] is the number in @a tree of the prefix - consisting of the first i reference tokens + @throw parse_error.107 if the key is not empty or begins with '/' + @throw parse_error.108 if character '~' is not followed by '0' or '1' - 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' - @throw parse_error.109 if array index is not a number - @throw type_error.313 if value cannot be unflattened + const char* const data = key.data(); + if (JSON_HEDLEY_UNLIKELY(data[0] != '/')) + { + 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 - static BasicJsonType& get_and_create(BasicJsonType& j, const std::vector& tokens, - const std::vector& prefixes, const prefix_tree& tree) + static BasicJsonType* resolve_plain(BasicJsonType* ptr, const string_t& path, const std::size_t length, string_t& token) { - auto* result = &j; - - // 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) + for (std::size_t pos = 0; pos < length;) { - const auto& reference_token = tokens[i]; - switch (result->type()) + if (JSON_HEDLEY_UNLIKELY(path[pos] != '/')) { - case detail::value_t::null: - { - 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(reference_token)); - } - else - { - // start a new object otherwise - result = &result->operator[](reference_token); - } - break; - } + return nullptr; + } + // 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: { - // create an entry in the object - result = &object_member(*result, reference_token); + const auto it = ptr->m_data.m_value.object->find(token); + if (JSON_HEDLEY_UNLIKELY(it == ptr->m_data.m_value.object->end())) + { + return nullptr; + } + ptr = &it->second; break; } case detail::value_t::array: { - // create an entry in the array - result = &result->operator[](array_index(reference_token)); + typename BasicJsonType::size_type idx{}; + if (JSON_HEDLEY_UNLIKELY(parse_array_index(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; } - /* - 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::null: case detail::value_t::string: case detail::value_t::boolean: case detail::value_t::number_integer: @@ -683,11 +791,12 @@ class json_pointer 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", &j)); + return nullptr; } + pos = end; } - return *result; + return ptr; } /*! @@ -1142,20 +1251,45 @@ class json_pointer // 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 // (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 - 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(v)), void()) { obj.emplace_new_key(key, std::forward(v)); } template - 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(v)), void()) + { + hint = obj.emplace_hint(hint, key, std::forward(v)); + ++hint; + } + + template + 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(v); } + /// append the decimal representation of @a i to @a out + static void append_index(string_t& out, std::size_t i) + { + std::array buf{}; + std::size_t pos = buf.size(); + do + { + buf[--pos] = static_cast('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] value the value to consider @@ -1201,7 +1335,8 @@ class json_pointer // flatten `v`, whose path is `path`: primitives and empty containers // are added to the result right away; other containers get a frame auto& object = *result.m_data.m_value.object; - const auto enter = [&stack, &path, &object](const BasicJsonType & v) + auto hint = object.end(); + const auto enter = [&stack, &path, &object, &hint](const BasicJsonType & v) { switch (v.type()) { @@ -1210,7 +1345,7 @@ class json_pointer if (v.m_data.m_value.array->empty()) { // 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 { @@ -1224,7 +1359,7 @@ class json_pointer if (v.m_data.m_value.object->empty()) { // 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 { @@ -1244,7 +1379,7 @@ class json_pointer default: { // 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; } } @@ -1272,7 +1407,8 @@ class json_pointer // iterate array and use index as a reference string ++stack.back().index; - detail::concat_into(path, '/', detail::to_string(i)); + path.push_back('/'); + append_index(path, i); enter(array[i]); } else @@ -1286,7 +1422,8 @@ class json_pointer // iterate object and use keys as reference string ++stack.back().member; - detail::concat_into(path, '/', detail::escape(it->first)); + path.push_back('/'); + detail::escape_into(path, it->first); enter(it->second); } } @@ -1317,53 +1454,30 @@ class json_pointer BasicJsonType result; - // parse every key once; the reference tokens are kept for the second - // pass (the vectors are never resized, so the tree may refer to them) - std::vector> keys; - keys.reserve(value.m_data.m_value.object->size()); + // check every key, and collect the prefixes that have a reference + // token 0 among their children; the values below them are arrays, + // all others are objects (see array_prefix_set) + array_prefix_set arrays; + const string_t* previous_key = nullptr; for (const auto& element : *value.m_data.m_value.object) { - json_pointer ptr(element.first); - keys.push_back(std::move(ptr.reference_tokens)); + scan_key(element.first, previous_key, arrays); + previous_key = &element.first; } - // collect the pointer prefixes that have a reference token 0 among - // their children; the values below them are arrays, all others are - // objects (see prefix_tree) - prefix_tree tree; - // the number of the prefix with i + 1 tokens of the previous key; keys - // are often sorted, so most of the prefix is the same as for the - // previous key and need not be looked up again - std::vector path; - const std::vector* previous = nullptr; - // the number of the complete prefix (i.e., the key) of each key - std::vector leaves; - 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; - } + // values[i] is the value after the first i reference tokens of the + // previous key, ends[i] the length of that prefix of the key. Keys are + // often sorted, so most of the path to the value of a key is the + // same as for the previous key and need not be looked up again. Only + // the values below the common prefix are dropped, and only they can + // move when a value is added: the ones above are not inside the + // container that grows. + std::vector values(1, &result); + std::vector ends(1, 0); + const string_t* previous = nullptr; + string_t token; // iterate the JSON object values - std::vector prefixes; - std::size_t key_index = 0; for (const auto& element : *value.m_data.m_value.object) { if (JSON_HEDLEY_UNLIKELY(!element.second.is_primitive())) @@ -1371,21 +1485,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)); } - const auto& tokens = keys[key_index]; - // number the prefixes of this key from its last one to the first - prefixes.resize(tokens.size()); - for (std::size_t i = tokens.size(), id = leaves[key_index]; i > 0; --i) + const string_t& key = element.first; + const char* const data = key.data(); + + // 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]; - prefixes[i - 1] = id; + const std::size_t same = common_prefix_length(data, previous->data(), (std::min)(key.size(), previous->size())); + 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 // that if the JSON pointer is "" (i.e., points to the whole value), - // function get_and_create returns a reference to the result itself. - // An assignment will then create a primitive value. - get_and_create(result, tokens, prefixes, tree) = element.second; + // the value is the result itself. An assignment will then create a + // primitive value. + 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 = ¤t->operator[](array_index(token)); + } + else + { + // start a new object otherwise + current = ¤t->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 = ¤t->operator[](array_index(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; diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 52e037751..9c0e11632 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -7045,6 +7045,125 @@ public: 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(token, idx) == json_pointer::array_index_status::ok && idx <= parent->size()) + { + parent->insert(parent->begin() + static_cast(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(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 for (const auto& val : json_patch) { @@ -7081,9 +7200,19 @@ public: // collect mandatory members 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 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); - switch (get_op(op)) + switch (operation) { case patch_operations::add: { @@ -7191,7 +7320,8 @@ public: { // the patch 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; } @@ -7235,31 +7365,69 @@ public: // 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. + /// @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 static void diff_replace(basic_json& result, const string_t& path, const basic_json& value) { - result.push_back( - { - {"op", "replace"}, {"path", path}, {"value", value} - }); + diff_operation(result, "replace", path, &value); } /// @brief append a "remove" operation for @a path to @a result static void diff_remove(basic_json& result, const string_t& path) { - result.push_back(object( - { - {"op", "remove"}, {"path", path} - })); + diff_operation(result, "remove", path, nullptr); } /// @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) { - result.push_back( - { - {"op", "add"}, {"path", path}, {"value", value} - }); + diff_operation(result, "add", path, &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 @@ -7272,13 +7440,18 @@ public: // in that order avoids the quadratic reinsertion done before for (std::size_t j = source.size(); j > index; --j) { - diff_remove(result, detail::concat(path, '/', detail::to_string(j - 1))); + diff_remove(result, diff_index_path(path, j - 1)); } // 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(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]); + } } } @@ -7287,7 +7460,7 @@ public: { for (const auto& it : added) { - diff_add(result, detail::concat(path, '/', detail::escape(it.key())), it.value()); + diff_add(result, diff_key_path(path, it.key()), it.value()); } } @@ -7308,10 +7481,25 @@ public: std::vector& added) { // 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()); 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()); if (found != target.cend()) { @@ -7324,6 +7512,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 // keeps its members in insertion order, such as the one backing // ordered_json, also needs the common keys to be in the same order @@ -7389,6 +7583,13 @@ public: 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 // order in source and target (only possible for a // reorderable object_t like ordered_map). Building a @@ -7405,7 +7606,7 @@ public: // it moves it to the end, fixing its position. for (auto it = source.cbegin(); it != source.cend(); ++it) { - diff_remove(result, detail::concat(path, '/', detail::escape(it.key()))); + diff_remove(result, diff_key_path(path, it.key())); } // add every key that is either common (just removed @@ -7414,7 +7615,7 @@ public: // target exactly for (auto it = target.cbegin(); it != target.cend(); ++it) { - diff_add(result, detail::concat(path, '/', detail::escape(it.key())), it.value()); + diff_add(result, diff_key_path(path, it.key()), it.value()); } return false; } @@ -7423,6 +7624,9 @@ public: @brief @ref diff, for values at nesting level @a depth, appending the 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 level, so values nested deeply enough used to exhaust the call stack and terminate the process. The descent is bounded here: once @ref @@ -7430,7 +7634,7 @@ public: 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, - 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 (source == target) @@ -7442,9 +7646,9 @@ public: } /// @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, - 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())) { @@ -7463,6 +7667,8 @@ public: { case value_t::array: { + const std::size_t path_length = path.size(); + // first pass: traverse common elements std::size_t i = 0; while (i < source.size() && i < target.size()) @@ -7470,7 +7676,9 @@ public: // recursive call to compare array values at index i if (!(source[i] == target[i])) { - diff_unequal(result, source[i], target[i], detail::concat(path, '/', detail::to_string(i)), depth + 1); + diff_append_index(path, i); + diff_unequal(result, source[i], target[i], path, depth + 1); + path.resize(path_length); } ++i; } @@ -7492,6 +7700,7 @@ public: // lookup is needed. Deleted keys are interleaved here // too, in source's original order, to match the // historical (pre-reordering-aware) output order. + const std::size_t path_length = path.size(); std::size_t i = 0; for (auto it = source.cbegin(); it != source.cend(); ++it, ++i) { @@ -7499,13 +7708,15 @@ public: { if (!(it.value() == *target_values[i])) { - diff_unequal(result, it.value(), *target_values[i], detail::concat(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 { // found a key that is not in target -> remove it - diff_remove(result, detail::concat(path, '/', detail::escape(it.key()))); + diff_remove(result, diff_key_path(path, it.key())); } } @@ -7636,7 +7847,7 @@ public: if (i < source_array.size() && i < target_array.size()) { ++stack.back().index; - detail::concat_into(current_path, '/', detail::to_string(i)); + diff_append_index(current_path, i); enter(source_array[i], target_array[i]); if (stack.size() == depth) { @@ -7659,7 +7870,7 @@ public: if (target_value_ptr != nullptr) { 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); if (stack.size() == depth) { @@ -7669,7 +7880,7 @@ public: else { // found a key that is not in target -> remove it - diff_remove(result, detail::concat(current_path, '/', detail::escape(it.key()))); + diff_remove(result, diff_key_path(current_path, it.key())); } continue; } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 0c0d763cf..2c9cda422 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -22351,10 +22351,13 @@ NLOHMANN_JSON_NAMESPACE_END #include // all_of +#include // array #include // isdigit #include // errno, ERANGE #include // size_t +#include // uint64_t #include // strtoull +#include // memcmp, memcpy #ifndef JSON_NO_IO #include // ostream #endif // JSON_NO_IO @@ -22369,6 +22372,8 @@ NLOHMANN_JSON_NAMESPACE_END // #include +// #include + // #include // #include @@ -22848,50 +22853,111 @@ class json_pointer 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 - among their children; @ref get_and_create creates arrays exactly below - those prefixes and objects everywhere else. Deciding this up front keeps - the result independent of the order in which the flattened object is - iterated, which is unspecified for some object types. + @ref unflatten creates arrays exactly below the prefixes that have a + reference token 0 among their children, and objects everywhere else. + Deciding this up front keeps the result independent of the order in which + the flattened object is iterated, which is unspecified for some object + types. - The prefixes form a tree and are numbered (number 0 is the empty prefix), - so each of them is stored only once (as a node) rather than as a copy of - all of its reference tokens. The tree does not own the reference tokens: - it refers to the tokens of the parsed keys, which must outlive it. + A prefix is identified by the text of the key it is the beginning of: + escaping is unambiguous, so equal prefixes of valid keys are equal texts. + The set refers to the keys, which must outlive it. */ - struct prefix_tree + class array_prefix_set { - // open-addressing hash table from (parent, last reference token) to - // the number of the node; empty slots have token == nullptr + public: + 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 (table.empty()) + { + table.resize(16); + } + const std::size_t hash = hash_of(data, size); + const std::size_t mask = table.size() - 1; + std::size_t i = hash & mask; + while (table[i].data != nullptr) + { + if (table[i].hash == hash && table[i].size == size && std::memcmp(table[i].data, data, size) == 0) + { + return; + } + i = (i + 1) & mask; + } + table[i] = slot(hash, data, size); + + // keep the load factor below 1/2 + if (++used * 2 > table.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 = table.size() - 1; + for (std::size_t i = hash & mask; table[i].data != nullptr; i = (i + 1) & mask) + { + if (table[i].hash == hash && table[i].size == size && std::memcmp(table[i].data, data, size) == 0) + { + return true; + } + } + return false; + } + + private: struct slot { - std::size_t hash; - std::size_t parent; - const string_t* token; - std::size_t id; + slot() noexcept = default; - slot() noexcept - : hash(0), parent(0), token(nullptr), id(0) + slot(std::size_t h, const char* d, std::size_t s) noexcept + : hash(h), data(d), size(s) {} - slot(std::size_t h, std::size_t p, const string_t* t, std::size_t i) noexcept - : hash(h), parent(p), token(t), id(i) - {} + std::size_t hash = 0; + const char* data = nullptr; // nullptr: the slot is empty + std::size_t size = 0; }; - std::vector table; - std::size_t used = 0; - - static std::size_t hash_of(std::size_t parent, const string_t& token) noexcept + static std::size_t hash_of(const char* data, std::size_t size) noexcept { - std::size_t h = parent * static_cast(0x9E3779B97F4A7C15ULL & (std::numeric_limits::max)()); - for (std::size_t i = 0; i < token.size(); ++i) + std::uint64_t h = 0x9E3779B97F4A7C15ULL ^ size; + std::size_t i = 0; + for (; i + 8 <= size; i += 8) { - h = (h ^ static_cast(token[i])) * static_cast(0x100000001B3ULL & (std::numeric_limits::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 detail::conditional_static_cast(h ^ (h >> 29u)); } void grow() @@ -22901,10 +22967,10 @@ class json_pointer const std::size_t mask = table.size() - 1; for (const auto& s : old) { - if (s.token != nullptr) + if (s.data != nullptr) { std::size_t i = s.hash & mask; - while (table[i].token != nullptr) + while (table[i].data != nullptr) { i = (i + 1) & mask; } @@ -22913,117 +22979,160 @@ class json_pointer } } - // parent[id] is the number of the prefix without its last token - std::vector parent; - // is_array[id] is true iff some flattened key has the reference token - // 0 directly below the prefix with number id - std::vector is_array; - - // start with the empty prefix only - prefix_tree() - : table(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 = table.size() - 1; - std::size_t i = hash & mask; - while (table[i].token != nullptr) - { - if (table[i].hash == hash && table[i].parent == id && *table[i].token == reference_token) - { - return table[i].id; - } - i = (i + 1) & mask; - } - - // new prefix - const std::size_t child = parent.size(); - table[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 > table.size()) - { - grow(); - } - return child; - } + std::vector table{}; + std::size_t used = 0; + const char* last_data = nullptr; + std::size_t last_size = 0; }; + /// @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 - @param[in] tokens the reference tokens of the pointer - @param[in] prefixes prefixes[i] is the number in @a tree of the prefix - consisting of the first i reference tokens + @throw parse_error.107 if the key is not empty or begins with '/' + @throw parse_error.108 if character '~' is not followed by '0' or '1' - 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' - @throw parse_error.109 if array index is not a number - @throw type_error.313 if value cannot be unflattened + const char* const data = key.data(); + if (JSON_HEDLEY_UNLIKELY(data[0] != '/')) + { + 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 - static BasicJsonType& get_and_create(BasicJsonType& j, const std::vector& tokens, - const std::vector& prefixes, const prefix_tree& tree) + static BasicJsonType* resolve_plain(BasicJsonType* ptr, const string_t& path, const std::size_t length, string_t& token) { - auto* result = &j; - - // 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) + for (std::size_t pos = 0; pos < length;) { - const auto& reference_token = tokens[i]; - switch (result->type()) + if (JSON_HEDLEY_UNLIKELY(path[pos] != '/')) { - case detail::value_t::null: - { - 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(reference_token)); - } - else - { - // start a new object otherwise - result = &result->operator[](reference_token); - } - break; - } + return nullptr; + } + // 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: { - // create an entry in the object - result = &object_member(*result, reference_token); + const auto it = ptr->m_data.m_value.object->find(token); + if (JSON_HEDLEY_UNLIKELY(it == ptr->m_data.m_value.object->end())) + { + return nullptr; + } + ptr = &it->second; break; } case detail::value_t::array: { - // create an entry in the array - result = &result->operator[](array_index(reference_token)); + typename BasicJsonType::size_type idx{}; + if (JSON_HEDLEY_UNLIKELY(parse_array_index(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; } - /* - 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::null: case detail::value_t::string: case detail::value_t::boolean: case detail::value_t::number_integer: @@ -23032,11 +23141,12 @@ class json_pointer 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", &j)); + return nullptr; } + pos = end; } - return *result; + return ptr; } /*! @@ -23491,20 +23601,45 @@ class json_pointer // 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 // (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 - 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(v)), void()) { obj.emplace_new_key(key, std::forward(v)); } template - 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(v)), void()) + { + hint = obj.emplace_hint(hint, key, std::forward(v)); + ++hint; + } + + template + 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(v); } + /// append the decimal representation of @a i to @a out + static void append_index(string_t& out, std::size_t i) + { + std::array buf{}; + std::size_t pos = buf.size(); + do + { + buf[--pos] = static_cast('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] value the value to consider @@ -23550,7 +23685,8 @@ class json_pointer // flatten `v`, whose path is `path`: primitives and empty containers // are added to the result right away; other containers get a frame auto& object = *result.m_data.m_value.object; - const auto enter = [&stack, &path, &object](const BasicJsonType & v) + auto hint = object.end(); + const auto enter = [&stack, &path, &object, &hint](const BasicJsonType & v) { switch (v.type()) { @@ -23559,7 +23695,7 @@ class json_pointer if (v.m_data.m_value.array->empty()) { // 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 { @@ -23573,7 +23709,7 @@ class json_pointer if (v.m_data.m_value.object->empty()) { // 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 { @@ -23593,7 +23729,7 @@ class json_pointer default: { // 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; } } @@ -23621,7 +23757,8 @@ class json_pointer // iterate array and use index as a reference string ++stack.back().index; - detail::concat_into(path, '/', detail::to_string(i)); + path.push_back('/'); + append_index(path, i); enter(array[i]); } else @@ -23635,7 +23772,8 @@ class json_pointer // iterate object and use keys as reference string ++stack.back().member; - detail::concat_into(path, '/', detail::escape(it->first)); + path.push_back('/'); + detail::escape_into(path, it->first); enter(it->second); } } @@ -23666,53 +23804,30 @@ class json_pointer BasicJsonType result; - // parse every key once; the reference tokens are kept for the second - // pass (the vectors are never resized, so the tree may refer to them) - std::vector> keys; - keys.reserve(value.m_data.m_value.object->size()); + // check every key, and collect the prefixes that have a reference + // token 0 among their children; the values below them are arrays, + // all others are objects (see array_prefix_set) + array_prefix_set arrays; + const string_t* previous_key = nullptr; for (const auto& element : *value.m_data.m_value.object) { - json_pointer ptr(element.first); - keys.push_back(std::move(ptr.reference_tokens)); + scan_key(element.first, previous_key, arrays); + previous_key = &element.first; } - // collect the pointer prefixes that have a reference token 0 among - // their children; the values below them are arrays, all others are - // objects (see prefix_tree) - prefix_tree tree; - // the number of the prefix with i + 1 tokens of the previous key; keys - // are often sorted, so most of the prefix is the same as for the - // previous key and need not be looked up again - std::vector path; - const std::vector* previous = nullptr; - // the number of the complete prefix (i.e., the key) of each key - std::vector leaves; - 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; - } + // values[i] is the value after the first i reference tokens of the + // previous key, ends[i] the length of that prefix of the key. Keys are + // often sorted, so most of the path to the value of a key is the + // same as for the previous key and need not be looked up again. Only + // the values below the common prefix are dropped, and only they can + // move when a value is added: the ones above are not inside the + // container that grows. + std::vector values(1, &result); + std::vector ends(1, 0); + const string_t* previous = nullptr; + string_t token; // iterate the JSON object values - std::vector prefixes; - std::size_t key_index = 0; for (const auto& element : *value.m_data.m_value.object) { if (JSON_HEDLEY_UNLIKELY(!element.second.is_primitive())) @@ -23720,21 +23835,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)); } - const auto& tokens = keys[key_index]; - // number the prefixes of this key from its last one to the first - prefixes.resize(tokens.size()); - for (std::size_t i = tokens.size(), id = leaves[key_index]; i > 0; --i) + const string_t& key = element.first; + const char* const data = key.data(); + + // 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]; - prefixes[i - 1] = id; + const std::size_t same = common_prefix_length(data, previous->data(), (std::min)(key.size(), previous->size())); + 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 // that if the JSON pointer is "" (i.e., points to the whole value), - // function get_and_create returns a reference to the result itself. - // An assignment will then create a primitive value. - get_and_create(result, tokens, prefixes, tree) = element.second; + // the value is the result itself. An assignment will then create a + // primitive value. + 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 = ¤t->operator[](array_index(token)); + } + else + { + // start a new object otherwise + current = ¤t->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 = ¤t->operator[](array_index(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; @@ -39161,6 +39355,125 @@ public: 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(token, idx) == json_pointer::array_index_status::ok && idx <= parent->size()) + { + parent->insert(parent->begin() + static_cast(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(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 for (const auto& val : json_patch) { @@ -39197,9 +39510,19 @@ public: // collect mandatory members 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 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); - switch (get_op(op)) + switch (operation) { case patch_operations::add: { @@ -39307,7 +39630,8 @@ public: { // the patch 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; } @@ -39351,31 +39675,69 @@ public: // 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. + /// @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 static void diff_replace(basic_json& result, const string_t& path, const basic_json& value) { - result.push_back( - { - {"op", "replace"}, {"path", path}, {"value", value} - }); + diff_operation(result, "replace", path, &value); } /// @brief append a "remove" operation for @a path to @a result static void diff_remove(basic_json& result, const string_t& path) { - result.push_back(object( - { - {"op", "remove"}, {"path", path} - })); + diff_operation(result, "remove", path, nullptr); } /// @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) { - result.push_back( - { - {"op", "add"}, {"path", path}, {"value", value} - }); + diff_operation(result, "add", path, &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 @@ -39388,13 +39750,18 @@ public: // in that order avoids the quadratic reinsertion done before for (std::size_t j = source.size(); j > index; --j) { - diff_remove(result, detail::concat(path, '/', detail::to_string(j - 1))); + diff_remove(result, diff_index_path(path, j - 1)); } // 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(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]); + } } } @@ -39403,7 +39770,7 @@ public: { for (const auto& it : added) { - diff_add(result, detail::concat(path, '/', detail::escape(it.key())), it.value()); + diff_add(result, diff_key_path(path, it.key()), it.value()); } } @@ -39424,10 +39791,25 @@ public: std::vector& added) { // 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()); 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()); if (found != target.cend()) { @@ -39440,6 +39822,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 // keeps its members in insertion order, such as the one backing // ordered_json, also needs the common keys to be in the same order @@ -39505,6 +39893,13 @@ public: 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 // order in source and target (only possible for a // reorderable object_t like ordered_map). Building a @@ -39521,7 +39916,7 @@ public: // it moves it to the end, fixing its position. for (auto it = source.cbegin(); it != source.cend(); ++it) { - diff_remove(result, detail::concat(path, '/', detail::escape(it.key()))); + diff_remove(result, diff_key_path(path, it.key())); } // add every key that is either common (just removed @@ -39530,7 +39925,7 @@ public: // target exactly for (auto it = target.cbegin(); it != target.cend(); ++it) { - diff_add(result, detail::concat(path, '/', detail::escape(it.key())), it.value()); + diff_add(result, diff_key_path(path, it.key()), it.value()); } return false; } @@ -39539,6 +39934,9 @@ public: @brief @ref diff, for values at nesting level @a depth, appending the 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 level, so values nested deeply enough used to exhaust the call stack and terminate the process. The descent is bounded here: once @ref @@ -39546,7 +39944,7 @@ public: 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, - 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 (source == target) @@ -39558,9 +39956,9 @@ public: } /// @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, - 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())) { @@ -39579,6 +39977,8 @@ public: { case value_t::array: { + const std::size_t path_length = path.size(); + // first pass: traverse common elements std::size_t i = 0; while (i < source.size() && i < target.size()) @@ -39586,7 +39986,9 @@ public: // recursive call to compare array values at index i if (!(source[i] == target[i])) { - diff_unequal(result, source[i], target[i], detail::concat(path, '/', detail::to_string(i)), depth + 1); + diff_append_index(path, i); + diff_unequal(result, source[i], target[i], path, depth + 1); + path.resize(path_length); } ++i; } @@ -39608,6 +40010,7 @@ public: // lookup is needed. Deleted keys are interleaved here // too, in source's original order, to match the // historical (pre-reordering-aware) output order. + const std::size_t path_length = path.size(); std::size_t i = 0; for (auto it = source.cbegin(); it != source.cend(); ++it, ++i) { @@ -39615,13 +40018,15 @@ public: { if (!(it.value() == *target_values[i])) { - diff_unequal(result, it.value(), *target_values[i], detail::concat(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 { // found a key that is not in target -> remove it - diff_remove(result, detail::concat(path, '/', detail::escape(it.key()))); + diff_remove(result, diff_key_path(path, it.key())); } } @@ -39752,7 +40157,7 @@ public: if (i < source_array.size() && i < target_array.size()) { ++stack.back().index; - detail::concat_into(current_path, '/', detail::to_string(i)); + diff_append_index(current_path, i); enter(source_array[i], target_array[i]); if (stack.size() == depth) { @@ -39775,7 +40180,7 @@ public: if (target_value_ptr != nullptr) { 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); if (stack.size() == depth) { @@ -39785,7 +40190,7 @@ public: else { // found a key that is not in target -> remove it - diff_remove(result, detail::concat(current_path, '/', detail::escape(it.key()))); + diff_remove(result, diff_key_path(current_path, it.key())); } continue; }