mirror of
https://github.com/nlohmann/json.git
synced 2026-10-03 05:00:30 +00:00
Preserve the object comparator's state in a deep copy past the nesting bound
copy_object_level(), used by the copy constructor and copy assignment once a value is nested deeper than the iterative deep copy's bound (128 levels, or every copy under JSON_NO_THREAD_LOCAL), built each object's copy with the object type's plain range constructor. That default-constructs the object's comparator instead of copying the original's. For an object type whose comparator carries state, such as a std::map that compares keys case-sensitively only when constructed that way, the copy then ordered - and could even deduplicate - its keys differently from the original. Add detail::is_comparator_constructible_object_type, a detection trait for object types that provide a key_comp() and a constructor taking a range and a comparator, the way std::map does. copy_object_level now dispatches on it: an object type that qualifies gets its copy built with src_object.key_comp() passed along; other object types, such as nlohmann::ordered_map (which has a key_compare for its std::map-like interface, but no key_comp()), keep using the plain range constructor exactly as before. merge_patch and update() were checked for the same pattern; neither is affected, since both only ever add members one at a time to an object that already has its own comparator (or start a brand new default-constructed one), rather than rebuilding an object_t from a range copied out of an existing, possibly custom-comparator object. Fixes #5649. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
@@ -825,6 +826,46 @@ Json nest(Json j, const std::size_t depth)
|
||||
}
|
||||
return j;
|
||||
}
|
||||
|
||||
// a std::map comparator with state: case-insensitive, unless constructed
|
||||
// case-sensitive. Used to check that copying an object copies the original's
|
||||
// comparator rather than default-constructing a new one (see #5649).
|
||||
struct key_case_less
|
||||
{
|
||||
key_case_less() = default;
|
||||
explicit key_case_less(const bool cs) noexcept : case_sensitive(cs) {}
|
||||
|
||||
bool operator()(const std::string& a, const std::string& b) const
|
||||
{
|
||||
if (case_sensitive)
|
||||
{
|
||||
return a < b;
|
||||
}
|
||||
return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end(),
|
||||
[](unsigned char x, unsigned char y)
|
||||
{
|
||||
return std::tolower(x) < std::tolower(y);
|
||||
});
|
||||
}
|
||||
|
||||
bool case_sensitive = false;
|
||||
};
|
||||
|
||||
template<class Key, class Value, class /*Compare*/, class Allocator>
|
||||
using key_case_map = std::map<Key, Value, key_case_less, Allocator>;
|
||||
using key_case_json = nlohmann::basic_json<key_case_map>;
|
||||
|
||||
// the innermost value of a chain of single-element arrays
|
||||
template<typename Json>
|
||||
const Json& innermost(const Json& j)
|
||||
{
|
||||
const Json* p = &j;
|
||||
while (p->is_array())
|
||||
{
|
||||
p = &(*p)[0];
|
||||
}
|
||||
return *p;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("equality of objects whose entries have no fixed order")
|
||||
@@ -872,6 +913,47 @@ TEST_CASE("equality of objects whose entries have no fixed order")
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("copying an object preserves its comparator's state")
|
||||
{
|
||||
// Past the iterative deep copy's nesting bound, an object copy used to be
|
||||
// built with a default-constructed comparator instead of a copy of the
|
||||
// original's. For an object type whose comparator carries state - here, a
|
||||
// std::map that compares keys case-sensitively only when created that way
|
||||
// - this reordered the copy's keys and could even drop entries that the
|
||||
// original's comparator kept distinct (see #5649).
|
||||
key_case_json object = key_case_json::object_t(key_case_less(true)); // case-sensitive
|
||||
object["b"] = 1;
|
||||
object["B"] = 2;
|
||||
object["a"] = 3;
|
||||
REQUIRE(object.dump() == R"({"B":2,"a":3,"b":1})");
|
||||
|
||||
for (const std::size_t depth : std::vector<std::size_t> {0, 127, 128, 200})
|
||||
{
|
||||
CAPTURE(depth);
|
||||
|
||||
key_case_json original = object;
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
original = key_case_json::array({std::move(original)});
|
||||
}
|
||||
|
||||
{
|
||||
const key_case_json copy = original; // NOLINT(performance-unnecessary-copy-initialization)
|
||||
CHECK(innermost(copy).size() == 3);
|
||||
CHECK(innermost(copy).dump() == R"({"B":2,"a":3,"b":1})");
|
||||
CHECK(copy == original);
|
||||
}
|
||||
|
||||
{
|
||||
key_case_json copy = key_case_json::array();
|
||||
copy = original;
|
||||
CHECK(innermost(copy).size() == 3);
|
||||
CHECK(innermost(copy).dump() == R"({"B":2,"a":3,"b":1})");
|
||||
CHECK(copy == original);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("containers are compared element by element")
|
||||
{
|
||||
// Containers nested deeper than a bound are compared without the call
|
||||
|
||||
Reference in New Issue
Block a user