Compare commits

..
Author SHA1 Message Date
Niels Lohmann 77052ec0f2 Keep the Qt keyword macros in the test from failing -Wunused-macros
Clang (-Werror) rejected the macros that the test defines but the library
must not use, and clang-tidy asked for parentheses around the arguments
of the Q_FOREACH imitation, where the variable is a declaration.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-17 20:48:15 +02:00
Niels Lohmann 22497c7128 Do not use Qt's keyword macros as identifiers
The prefix tree used by unflatten() (#5836) had a member named slots, which
Qt defines as an empty macro, so including a Qt header before this library
failed to compile. Rename it to table and add a test that includes the
library with Qt's keyword macros (slots, signals, emit, foreach, forever)
defined.

Fixes #5865

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-17 14:02:41 +02:00
5 changed files with 504 additions and 1243 deletions

No files matched your search

@@ -135,6 +135,9 @@ Note: Some modern features (like C++20 ranges or filesystem support) may be disa
headers are included. headers are included.
- [x] On Windows, the library is compiled with `<Windows.h>` being included to detect and avoid common bugs (see - [x] On Windows, the library is compiled with `<Windows.h>` being included to detect and avoid common bugs (see
[`unit-windows_h.cpp`](https://github.com/nlohmann/json/blob/develop/tests/src/unit-windows_h.cpp)). [`unit-windows_h.cpp`](https://github.com/nlohmann/json/blob/develop/tests/src/unit-windows_h.cpp)).
- [x] The library is compiled with Qt's keyword macros (`slots`, `signals`, `emit`, `foreach`, `forever`) being defined
to make sure it does not use them as identifiers (see
[`unit-qt_keywords.cpp`](https://github.com/nlohmann/json/blob/develop/tests/src/unit-qt_keywords.cpp)).
- [x] The library is compiled with exceptions disabled to support alternative means of error handling. - [x] The library is compiled with exceptions disabled to support alternative means of error handling.
## Stable public API ## Stable public API
+187 -380
View File
@@ -9,13 +9,10 @@
#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
@@ -27,7 +24,6 @@
#include <nlohmann/detail/exceptions.hpp> #include <nlohmann/detail/exceptions.hpp>
#include <nlohmann/detail/macro_scope.hpp> #include <nlohmann/detail/macro_scope.hpp>
#include <nlohmann/detail/meta/cpp_future.hpp> #include <nlohmann/detail/meta/cpp_future.hpp>
#include <nlohmann/detail/meta/type_traits.hpp>
#include <nlohmann/detail/string_concat.hpp> #include <nlohmann/detail/string_concat.hpp>
#include <nlohmann/detail/string_escape.hpp> #include <nlohmann/detail/string_escape.hpp>
#include <nlohmann/detail/string_utils.hpp> #include <nlohmann/detail/string_utils.hpp>
@@ -496,286 +492,182 @@ class json_pointer
private: private:
/*! /*!
@brief the pointer prefixes of a flattened object that denote arrays @brief the pointer prefixes of a flattened object, and which of them denote arrays
@ref unflatten creates arrays exactly below the prefixes that have a @ref unflatten collects the pointer prefixes that have a reference token 0
reference token 0 among their children, and objects everywhere else. among their children; @ref get_and_create creates arrays exactly below
Deciding this up front keeps the result independent of the order in which those prefixes and objects everywhere else. Deciding this up front keeps
the flattened object is iterated, which is unspecified for some object the result independent of the order in which the flattened object is
types. iterated, which is unspecified for some object types.
A prefix is identified by the text of the key it is the beginning of: The prefixes form a tree and are numbered (number 0 is the empty prefix),
escaping is unambiguous, so equal prefixes of valid keys are equal texts. so each of them is stored only once (as a node) rather than as a copy of
The set refers to the keys, which must outlive it. 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.
*/ */
class array_prefix_set struct prefix_tree
{ {
public: // open-addressing hash table from (parent, last reference token) to
void insert(const char* data, std::size_t size) // the number of the node; empty slots have token == nullptr
{
// 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
{ {
slot() noexcept = default; std::size_t hash;
std::size_t parent;
const string_t* token;
std::size_t id;
slot(std::size_t h, const char* d, std::size_t s) noexcept slot() noexcept
: hash(h), data(d), size(s) : hash(0), parent(0), token(nullptr), id(0)
{} {}
std::size_t hash = 0; slot(std::size_t h, std::size_t p, const string_t* t, std::size_t i) noexcept
const char* data = nullptr; // nullptr: the slot is empty : hash(h), parent(p), token(t), id(i)
std::size_t size = 0; {}
}; };
static std::size_t hash_of(const char* data, std::size_t size) noexcept std::vector<slot> table;
std::size_t used = 0;
static std::size_t hash_of(std::size_t parent, const string_t& token) noexcept
{ {
std::uint64_t h = 0x9E3779B97F4A7C15ULL ^ size; std::size_t h = parent * static_cast<std::size_t>(0x9E3779B97F4A7C15ULL & (std::numeric_limits<std::size_t>::max)());
std::size_t i = 0; for (std::size_t i = 0; i < token.size(); ++i)
for (; i + 8 <= size; i += 8)
{ {
std::uint64_t w = 0; h = (h ^ static_cast<unsigned char>(token[i])) * static_cast<std::size_t>(0x100000001B3ULL & (std::numeric_limits<std::size_t>::max)());
std::memcpy(&w, data + i, 8);
h = (h ^ w) * 0x100000001B3ULL;
h ^= h >> 32u;
} }
std::uint64_t w = 0; return h ^ (h >> 29u);
if (i < size)
{
std::memcpy(&w, data + i, size - i);
}
h = (h ^ w) * 0x9E3779B97F4A7C15ULL;
return detail::conditional_static_cast<std::size_t>(h ^ (h >> 29u));
} }
void grow() void grow()
{ {
std::vector<slot> old(slots.size() * 2); std::vector<slot> old(table.size() * 2);
old.swap(slots); old.swap(table);
const std::size_t mask = slots.size() - 1; const std::size_t mask = table.size() - 1;
for (const auto& s : old) for (const auto& s : old)
{ {
if (s.data != nullptr) if (s.token != nullptr)
{ {
std::size_t i = s.hash & mask; std::size_t i = s.hash & mask;
while (slots[i].data != nullptr) while (table[i].token != nullptr)
{ {
i = (i + 1) & mask; i = (i + 1) & mask;
} }
slots[i] = s; table[i] = s;
} }
} }
} }
std::vector<slot> slots{}; // parent[id] is the number of the prefix without its last token
std::size_t used = 0; std::vector<std::size_t> parent;
const char* last_data = nullptr; // is_array[id] is true iff some flattened key has the reference token
std::size_t last_size = 0; // 0 directly below the prefix with number id
std::vector<bool> 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;
}
}; };
/// @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 check that @a key is a valid JSON pointer, and add the prefixes of @brief create and return a reference to the pointed to value
it that denote arrays to @a arrays
@throw parse_error.107 if the key is not empty or begins with '/' @param[in] j the value to create the pointed to value in
@throw parse_error.108 if character '~' is not followed by '0' or '1' @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
The same checks as @ref split, which throws the same exceptions, but Complexity: Linear in the number of reference tokens.
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;
}
const char* const data = key.data(); @throw parse_error.106 if an array index begins with '0'
if (JSON_HEDLEY_UNLIKELY(data[0] != '/')) @throw parse_error.109 if array index is not a number
{ @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* resolve_plain(BasicJsonType* ptr, const string_t& path, const std::size_t length, string_t& token) static BasicJsonType& get_and_create(BasicJsonType& j, const std::vector<string_t>& tokens,
const std::vector<std::size_t>& prefixes, const prefix_tree& tree)
{ {
for (std::size_t pos = 0; pos < length;) 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)
{ {
if (JSON_HEDLEY_UNLIKELY(path[pos] != '/')) const auto& reference_token = tokens[i];
switch (result->type())
{ {
return nullptr; 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<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:
{ {
const auto it = ptr->m_data.m_value.object->find(token); // create an entry in the object
if (JSON_HEDLEY_UNLIKELY(it == ptr->m_data.m_value.object->end())) result = &object_member(*result, reference_token);
{
return nullptr;
}
ptr = &it->second;
break; break;
} }
case detail::value_t::array: case detail::value_t::array:
{ {
typename BasicJsonType::size_type idx{}; // create an entry in the array
if (JSON_HEDLEY_UNLIKELY(parse_array_index<BasicJsonType>(token, idx) != array_index_status::ok || idx >= ptr->m_data.m_value.array->size())) result = &result->operator[](array_index<BasicJsonType>(reference_token));
{
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:
@@ -784,12 +676,11 @@ 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:
return nullptr; JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_invalid_value, "invalid value to unflatten", &j));
} }
pos = end;
} }
return ptr; return *result;
} }
/*! /*!
@@ -1244,45 +1135,20 @@ 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. Objects with a positional insert (std::map) insert next to // quadratic; all other object types use operator[].
// 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, typename ObjectType::iterator& /*hint*/, const string_t& key, ValueType&& v, detail::priority_tag<2> /*unused*/) static auto add_flattened(ObjectType& obj, const string_t& key, ValueType&& v, detail::priority_tag<1> /*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 auto add_flattened(ObjectType& obj, typename ObjectType::iterator& hint, const string_t& key, ValueType&& v, detail::priority_tag<1> /*unused*/) static void add_flattened(ObjectType& obj, const string_t& key, ValueType&& v, detail::priority_tag<0> /*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
@@ -1328,8 +1194,7 @@ 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;
auto hint = object.end(); const auto enter = [&stack, &path, &object](const BasicJsonType & v)
const auto enter = [&stack, &path, &object, &hint](const BasicJsonType & v)
{ {
switch (v.type()) switch (v.type())
{ {
@@ -1338,7 +1203,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, hint, path, nullptr, detail::priority_tag<2> {}); add_flattened(object, path, nullptr, detail::priority_tag<1> {});
} }
else else
{ {
@@ -1352,7 +1217,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, hint, path, nullptr, detail::priority_tag<2> {}); add_flattened(object, path, nullptr, detail::priority_tag<1> {});
} }
else else
{ {
@@ -1372,7 +1237,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, hint, path, v, detail::priority_tag<2> {}); add_flattened(object, path, v, detail::priority_tag<1> {});
return; return;
} }
} }
@@ -1400,8 +1265,7 @@ 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;
path.push_back('/'); detail::concat_into(path, '/', detail::to_string<string_t>(i));
append_index(path, i);
enter(array[i]); enter(array[i]);
} }
else else
@@ -1415,8 +1279,7 @@ 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;
path.push_back('/'); detail::concat_into(path, '/', detail::escape(it->first));
detail::escape_into(path, it->first);
enter(it->second); enter(it->second);
} }
} }
@@ -1447,30 +1310,53 @@ class json_pointer
BasicJsonType result; BasicJsonType result;
// check every key, and collect the prefixes that have a reference // parse every key once; the reference tokens are kept for the second
// token 0 among their children; the values below them are arrays, // pass (the vectors are never resized, so the tree may refer to them)
// all others are objects (see array_prefix_set) std::vector<std::vector<string_t>> keys;
array_prefix_set arrays; keys.reserve(value.m_data.m_value.object->size());
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)
{ {
scan_key(element.first, previous_key, arrays); json_pointer ptr(element.first);
previous_key = &element.first; keys.push_back(std::move(ptr.reference_tokens));
} }
// values[i] is the value after the first i reference tokens of the // collect the pointer prefixes that have a reference token 0 among
// previous key, ends[i] the length of that prefix of the key. Keys are // their children; the values below them are arrays, all others are
// often sorted, so most of the path to the value of a key is the // objects (see prefix_tree)
// same as for the previous key and need not be looked up again. Only prefix_tree tree;
// the values below the common prefix are dropped, and only they can // the number of the prefix with i + 1 tokens of the previous key; keys
// move when a value is added: the ones above are not inside the // are often sorted, so most of the prefix is the same as for the
// container that grows. // previous key and need not be looked up again
std::vector<BasicJsonType*> values(1, &result); std::vector<std::size_t> path;
std::vector<std::size_t> ends(1, 0); const std::vector<string_t>* previous = nullptr;
const string_t* previous = nullptr; // the number of the complete prefix (i.e., the key) of each key
string_t token; std::vector<std::size_t> 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;
}
// 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()))
@@ -1478,100 +1364,21 @@ 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 string_t& key = element.first; const auto& tokens = keys[key_index];
const char* const data = key.data(); // number the prefixes of this key from its last one to the first
prefixes.resize(tokens.size());
// the number of reference tokens the key has in common with the for (std::size_t i = tokens.size(), id = leaves[key_index]; i > 0; --i)
// previous one
std::size_t depth = 0;
if (previous != nullptr)
{ {
const std::size_t same = common_prefix_length(data, previous->data(), (std::min)(key.size(), previous->size())); id = tree.parent[id];
depth = ends.size() - 1; prefixes[i - 1] = id;
while (depth > 0 && (ends[depth] > same || (ends[depth] == same && same < key.size() && data[same] != '/')))
{
--depth;
}
} }
values.resize(depth + 1); ++key_index;
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),
// the value is the result itself. An assignment will then create a // function get_and_create returns a reference to the result itself.
// primitive value. // An assignment will then create a primitive value.
BasicJsonType* current = values.back(); get_and_create(result, tokens, prefixes, tree) = element.second;
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;
+30 -241
View File
@@ -7066,125 +7066,6 @@ 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)
{ {
@@ -7221,19 +7102,9 @@ 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 (operation) switch (get_op(op))
{ {
case patch_operations::add: case patch_operations::add:
{ {
@@ -7341,8 +7212,7 @@ public:
{ {
// the patch // the patch
basic_json result(value_t::array); basic_json result(value_t::array);
string_t buffer = path; diff_recursively(result, source, target, path, 0);
diff_recursively(result, source, target, buffer, 0);
return result; return result;
} }
@@ -7386,69 +7256,31 @@ 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)
{ {
diff_operation(result, "replace", path, &value); result.push_back(
{
{"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)
{ {
diff_operation(result, "remove", path, nullptr); result.push_back(object(
{
{"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)
{ {
diff_operation(result, "add", path, &value); result.push_back(
} {
{"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
@@ -7461,18 +7293,13 @@ 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, diff_index_path(path, j - 1)); diff_remove(result, detail::concat<string_t>(path, '/', detail::to_string<string_t>(j - 1)));
} }
// add other remaining elements // add other remaining elements
if (source.size() < target.size()) for (std::size_t i = source.size(); i < target.size(); ++i)
{ {
string_t tail_path = path; diff_add(result, detail::concat<string_t>(path, "/-"), target[i]);
tail_path.append("/-", 2);
for (std::size_t i = source.size(); i < target.size(); ++i)
{
diff_add(result, tail_path, target[i]);
}
} }
} }
@@ -7481,7 +7308,7 @@ public:
{ {
for (const auto& it : added) for (const auto& it : added)
{ {
diff_add(result, diff_key_path(path, it.key()), it.value()); diff_add(result, detail::concat<string_t>(path, '/', detail::escape(it.key())), it.value());
} }
} }
@@ -7502,25 +7329,10 @@ 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;
bool together = source.size() == target.size(); for (auto it = source.cbegin(); it != source.cend(); ++it)
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())
{ {
@@ -7533,12 +7345,6 @@ 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
@@ -7604,13 +7410,6 @@ 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
@@ -7627,7 +7426,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, diff_key_path(path, it.key())); diff_remove(result, detail::concat<string_t>(path, '/', detail::escape(it.key())));
} }
// add every key that is either common (just removed // add every key that is either common (just removed
@@ -7636,7 +7435,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, diff_key_path(path, it.key()), it.value()); diff_add(result, detail::concat<string_t>(path, '/', detail::escape(it.key())), it.value());
} }
return false; return false;
} }
@@ -7645,9 +7444,6 @@ 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
@@ -7655,7 +7451,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,
string_t& path, const std::size_t depth) const 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)
@@ -7667,9 +7463,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 extend the path only for unequal ones /// compare first so they build no path for equal 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,
string_t& path, const std::size_t depth) const 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()))
{ {
@@ -7688,8 +7484,6 @@ 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())
@@ -7697,9 +7491,7 @@ 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_append_index(path, i); diff_unequal(result, source[i], target[i], detail::concat<string_t>(path, '/', detail::to_string<string_t>(i)), depth + 1);
diff_unequal(result, source[i], target[i], path, depth + 1);
path.resize(path_length);
} }
++i; ++i;
} }
@@ -7721,7 +7513,6 @@ 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)
{ {
@@ -7729,15 +7520,13 @@ public:
{ {
if (!(it.value() == *target_values[i])) if (!(it.value() == *target_values[i]))
{ {
diff_append_key(path, it.key()); diff_unequal(result, it.value(), *target_values[i], detail::concat<string_t>(path, '/', detail::escape(it.key())), depth + 1);
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, diff_key_path(path, it.key())); diff_remove(result, detail::concat<string_t>(path, '/', detail::escape(it.key())));
} }
} }
@@ -7868,7 +7657,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;
diff_append_index(current_path, i); detail::concat_into(current_path, '/', detail::to_string<string_t>(i));
enter(source_array[i], target_array[i]); enter(source_array[i], target_array[i]);
if (stack.size() == depth) if (stack.size() == depth)
{ {
@@ -7891,7 +7680,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;
diff_append_key(current_path, it.key()); detail::concat_into(current_path, '/', detail::escape(it.key()));
enter(it.value(), target_value); enter(it.value(), target_value);
if (stack.size() == depth) if (stack.size() == depth)
{ {
@@ -7901,7 +7690,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, diff_key_path(current_path, it.key())); diff_remove(result, detail::concat<string_t>(current_path, '/', detail::escape(it.key())));
} }
continue; continue;
} }
+217 -622
View File
File diff suppressed because it is too large. Load diff
+67
View File
@@ -0,0 +1,67 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
// Qt defines its keywords as macros (see qtmetamacros.h and qforeach.h) unless
// QT_NO_KEYWORDS is defined, and it is common to include Qt headers before this
// library; it must therefore not use these words as identifiers (see #5865)
#define slots
#define signals public
#define emit
#define Q_FOREACH(variable, container) for (variable : container) // NOLINT(bugprone-macro-parentheses): variable is a declaration, as in Qt
#define foreach Q_FOREACH
#define forever for (;;)
#include <nlohmann/json.hpp>
using nlohmann::json;
TEST_CASE("Qt keyword macros")
{
SECTION("parse and dump")
{
const json j = json::parse(R"({"a": [1, 2.5, "x", null, true]})");
CHECK(j.dump() == R"({"a":[1,2.5,"x",null,true]})");
}
SECTION("flatten and unflatten")
{
const json j = {{"a", {{"b", {1, 2}}, {"c", "d"}}}, {"e", nullptr}};
CHECK(j.flatten().unflatten() == j);
}
SECTION("JSON pointer")
{
json j = {{"a", {{"b", {1, 2}}}}};
CHECK(j.at(json::json_pointer("/a/b/1")) == 2);
j[json::json_pointer("/a/c")] = 3;
CHECK(j.contains(json::json_pointer("/a/c")));
}
SECTION("binary formats")
{
const json j = {{"a", {1, 2, 3}}, {"b", "c"}};
CHECK(json::from_cbor(json::to_cbor(j)) == j);
CHECK(json::from_msgpack(json::to_msgpack(j)) == j);
CHECK(json::from_ubjson(json::to_ubjson(j)) == j);
CHECK(json::from_bjdata(json::to_bjdata(j)) == j);
CHECK(json::from_bson(json::to_bson(j)) == j);
}
SECTION("patch and diff")
{
const json source = {{"a", 1}, {"b", {1, 2}}};
const json target = {{"a", 2}, {"c", "d"}};
CHECK(source.patch(json::diff(source, target)) == target);
}
}
// testing for the macros also keeps -Wunused-macros from rejecting them
#if !defined(slots) || !defined(signals) || !defined(emit) || !defined(Q_FOREACH) || !defined(foreach) || !defined(forever)
#error "the Qt keyword macros must stay defined for the tests above"
#endif