From a32f61eb98a27cbf2b9186780d46da2f996054eb Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 30 Sep 2026 23:04:57 +0200 Subject: [PATCH] Fix update() and merge_patch() when the argument is *this or one of its members (#5678) --- .../mkdocs/docs/api/basic_json/merge_patch.md | 8 ++ docs/mkdocs/docs/api/basic_json/update.md | 8 ++ include/nlohmann/json.hpp | 101 ++++++++++++------ single_include/nlohmann/json.hpp | 101 ++++++++++++------ tests/src/unit-diagnostics.cpp | 25 +++++ tests/src/unit-merge_patch.cpp | 49 +++++++++ tests/src/unit-modifiers.cpp | 69 ++++++++++++ 7 files changed, 295 insertions(+), 66 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/merge_patch.md b/docs/mkdocs/docs/api/basic_json/merge_patch.md index 1718c9227..634d889d3 100644 --- a/docs/mkdocs/docs/api/basic_json/merge_patch.md +++ b/docs/mkdocs/docs/api/basic_json/merge_patch.md @@ -37,6 +37,12 @@ Thereby, `Target` is the current object; that is, the patch is applied to the cu Linear in the lengths of `apply_patch`. +## Notes + +`apply_patch` may be `#!cpp *this` itself or refer to a value contained in `#!cpp *this` (for example, a subobject +returned by `#!cpp (*this)[key]`); it is read as it was when `merge_patch()` was called, before any modification of +`#!cpp *this`. + ## Examples ??? example @@ -61,3 +67,5 @@ Linear in the lengths of `apply_patch`. ## Version history - Added in version 3.0.0. +- Fixed use of freed or relocated memory when `apply_patch` is `#!cpp *this` or refers to a value contained in + `#!cpp *this`, in version 3.13.0. diff --git a/docs/mkdocs/docs/api/basic_json/update.md b/docs/mkdocs/docs/api/basic_json/update.md index b34140dbc..db8bfb745 100644 --- a/docs/mkdocs/docs/api/basic_json/update.md +++ b/docs/mkdocs/docs/api/basic_json/update.md @@ -59,6 +59,12 @@ Basic guarantee: if an exception is thrown during the operation, the JSON value 1. O(N*log(size() + N)), where N is the number of elements to insert. 2. O(N*log(size() + N)), where N is the number of elements to insert. +## Notes + +The argument `j` (or, for overload (2), the range `[first, last)`) may be `#!cpp *this` itself or refer to a value +contained in `#!cpp *this` (for example, a subobject returned by `#!cpp (*this)[key]`); it is read as it was when +`update()` was called, before any modification of `#!cpp *this`. + ## Examples ??? example @@ -155,3 +161,5 @@ Basic guarantee: if an exception is thrown during the operation, the JSON value - Added in version 3.0.0. - Added `merge_objects` parameter in 3.10.5. +- Fixed use of freed or relocated memory when the argument is `#!cpp *this` or refers to a value contained in + `#!cpp *this`, in version 3.13.0. diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index de82cad37..117391edd 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -4415,25 +4415,26 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/update/ void update(const_reference j, bool merge_objects = false) { - update(j.begin(), j.end(), merge_objects); + prepare_update(); + + // passed value must be an object (checked here so a type_error names + // j, not the copy made below) + if (JSON_HEDLEY_UNLIKELY(!j.is_object())) + { + JSON_THROW(type_error::create(312, detail::concat("cannot use update() with ", j.type_name()), &j)); + } + + // copy first: j may be *this or one of its descendants, and is + // iterated (and moved from) below to update *this + basic_json source = j; + update_from(source, merge_objects); } /// @brief updates a JSON object from another object, overwriting existing keys /// @sa https://json.nlohmann.me/api/basic_json/update/ void update(const_iterator first, const_iterator last, bool merge_objects = false) // NOLINT(performance-unnecessary-value-param) { - // implicitly convert a null value to an empty object - if (is_null()) - { - m_data.m_type = value_t::object; - m_data.m_value.object = create(); - assert_invariant(); - } - - if (JSON_HEDLEY_UNLIKELY(!is_object())) - { - JSON_THROW(type_error::create(312, detail::concat("cannot use update() with ", type_name()), this)); - } + prepare_update(); // check if range iterators belong to the same JSON object if (JSON_HEDLEY_UNLIKELY(first.m_object != last.m_object)) @@ -4447,7 +4448,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_THROW(type_error::create(312, detail::concat("cannot use update() with ", first.m_object->type_name()), first.m_object)); } - update_members(first, last, merge_objects, 0); + // copy first: the range may belong to *this or one of its + // descendants, and is iterated (and moved from) below to update + // *this + basic_json source(first, last); + update_from(source, merge_objects); } private: @@ -4455,15 +4460,42 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// merge_patch_iteratively is merging into, and the members still to merge struct merge_frame { - merge_frame(basic_json* target_, const_iterator position_, const_iterator last_) noexcept + merge_frame(basic_json* target_, iterator position_, iterator last_) noexcept : target(target_), position(std::move(position_)), last(std::move(last_)) {} basic_json* target; - const_iterator position; - const_iterator last; + iterator position; + iterator last; }; + /// @brief converts a null value to an empty object and checks that this + /// value is an object; called first by both @ref update overloads + void prepare_update() + { + // implicitly convert a null value to an empty object; create the + // object before setting the type, so a throwing allocation leaves + // this value null + if (is_null()) + { + m_data.m_value.object = create(); + m_data.m_type = value_t::object; + assert_invariant(); + } + + if (JSON_HEDLEY_UNLIKELY(!is_object())) + { + JSON_THROW(type_error::create(312, detail::concat("cannot use update() with ", type_name()), this)); + } + } + + /// @brief starts the @ref update_members loop over an already-copied @a + /// source; called by both @ref update overloads + void update_from(basic_json& source, const bool merge_objects) + { + update_members(source.begin(), source.end(), merge_objects, 0); + } + /*! @brief the members loop of @ref update, for this object and range @@ -4476,7 +4508,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec @param[in] depth nesting level of this object, counted from the object @ref update was called on */ - void update_members(const const_iterator& first, const const_iterator& last, const bool merge_objects, const std::size_t depth) + void update_members(const iterator& first, const iterator& last, const bool merge_objects, const std::size_t depth) { if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) { @@ -4494,13 +4526,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // are overwritten as usual" behavior (see #5402). if (it2 != m_data.m_value.object->end() && it2->second.is_object()) { - it2->second.update_members(it.value().cbegin(), it.value().cend(), true, depth + 1); + it2->second.update_members(it.value().begin(), it.value().end(), true, depth + 1); continue; } } // set_parent() also repairs the other members, which ordered_json // relocates when adding a key makes its vector grow - set_parent(m_data.m_value.object->operator[](it.key()) = it.value()); + set_parent(m_data.m_value.object->operator[](it.key()) = std::move(it.value())); } } @@ -4514,7 +4546,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec version. Only reached for values nested deeper than @ref detail::recursion_depth_limit. */ - void update_members_iteratively(const_iterator first, const_iterator last) + void update_members_iteratively(iterator first, iterator last) { std::vector stack; @@ -4541,18 +4573,18 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec const auto it2 = target->m_data.m_value.object->find(first.key()); if (it2 != target->m_data.m_value.object->end() && it2->second.is_object()) { - const basic_json& source = first.value(); + basic_json& source = first.value(); ++first; stack.emplace_back(target, first, last); target = &it2->second; - first = source.cbegin(); - last = source.cend(); + first = source.begin(); + last = source.end(); continue; } } // set_parent() also repairs the other members, which ordered_json // relocates when adding a key makes its vector grow - target->set_parent(target->m_data.m_value.object->operator[](first.key()) = first.value()); + target->set_parent(target->m_data.m_value.object->operator[](first.key()) = std::move(first.value())); ++first; } } @@ -6561,7 +6593,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/merge_patch/ void merge_patch(const basic_json& apply_patch) { - apply_merge_patch(apply_patch, 0); + // copy first: apply_patch may be *this or one of its descendants, + // and is iterated (and moved from) below to patch *this + basic_json patch = apply_patch; + apply_merge_patch(patch, 0); } private: @@ -6574,7 +6609,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec detail::recursion_depth_limit levels have been entered, @ref merge_patch_iteratively applies what is left without the call stack. */ - void apply_merge_patch(const basic_json& apply_patch, const std::size_t depth) + void apply_merge_patch(basic_json& apply_patch, const std::size_t depth) { if (apply_patch.is_object()) { @@ -6602,7 +6637,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } else { - *this = apply_patch; + *this = std::move(apply_patch); } } @@ -6615,12 +6650,12 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec recursive version. Only reached for patches nested deeper than @ref detail::recursion_depth_limit. */ - void merge_patch_iteratively(const basic_json& apply_patch) + void merge_patch_iteratively(basic_json& apply_patch) { std::vector stack; // patch `target` with `patch`, or start patching it member by member - const auto apply = [&stack](basic_json & target, const basic_json & patch) + const auto apply = [&stack](basic_json & target, basic_json & patch) { if (patch.is_object()) { @@ -6628,11 +6663,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec { target = basic_json::object(); } - stack.emplace_back(&target, patch.cbegin(), patch.cend()); + stack.emplace_back(&target, patch.begin(), patch.end()); } else { - target = patch; + target = std::move(patch); } }; @@ -6648,7 +6683,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec continue; } - const const_iterator member = frame.position; + const iterator member = frame.position; ++stack.back().position; if (member.value().is_null()) { diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index f39e593b9..cacc8ea70 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -31162,25 +31162,26 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/update/ void update(const_reference j, bool merge_objects = false) { - update(j.begin(), j.end(), merge_objects); + prepare_update(); + + // passed value must be an object (checked here so a type_error names + // j, not the copy made below) + if (JSON_HEDLEY_UNLIKELY(!j.is_object())) + { + JSON_THROW(type_error::create(312, detail::concat("cannot use update() with ", j.type_name()), &j)); + } + + // copy first: j may be *this or one of its descendants, and is + // iterated (and moved from) below to update *this + basic_json source = j; + update_from(source, merge_objects); } /// @brief updates a JSON object from another object, overwriting existing keys /// @sa https://json.nlohmann.me/api/basic_json/update/ void update(const_iterator first, const_iterator last, bool merge_objects = false) // NOLINT(performance-unnecessary-value-param) { - // implicitly convert a null value to an empty object - if (is_null()) - { - m_data.m_type = value_t::object; - m_data.m_value.object = create(); - assert_invariant(); - } - - if (JSON_HEDLEY_UNLIKELY(!is_object())) - { - JSON_THROW(type_error::create(312, detail::concat("cannot use update() with ", type_name()), this)); - } + prepare_update(); // check if range iterators belong to the same JSON object if (JSON_HEDLEY_UNLIKELY(first.m_object != last.m_object)) @@ -31194,7 +31195,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_THROW(type_error::create(312, detail::concat("cannot use update() with ", first.m_object->type_name()), first.m_object)); } - update_members(first, last, merge_objects, 0); + // copy first: the range may belong to *this or one of its + // descendants, and is iterated (and moved from) below to update + // *this + basic_json source(first, last); + update_from(source, merge_objects); } private: @@ -31202,15 +31207,42 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// merge_patch_iteratively is merging into, and the members still to merge struct merge_frame { - merge_frame(basic_json* target_, const_iterator position_, const_iterator last_) noexcept + merge_frame(basic_json* target_, iterator position_, iterator last_) noexcept : target(target_), position(std::move(position_)), last(std::move(last_)) {} basic_json* target; - const_iterator position; - const_iterator last; + iterator position; + iterator last; }; + /// @brief converts a null value to an empty object and checks that this + /// value is an object; called first by both @ref update overloads + void prepare_update() + { + // implicitly convert a null value to an empty object; create the + // object before setting the type, so a throwing allocation leaves + // this value null + if (is_null()) + { + m_data.m_value.object = create(); + m_data.m_type = value_t::object; + assert_invariant(); + } + + if (JSON_HEDLEY_UNLIKELY(!is_object())) + { + JSON_THROW(type_error::create(312, detail::concat("cannot use update() with ", type_name()), this)); + } + } + + /// @brief starts the @ref update_members loop over an already-copied @a + /// source; called by both @ref update overloads + void update_from(basic_json& source, const bool merge_objects) + { + update_members(source.begin(), source.end(), merge_objects, 0); + } + /*! @brief the members loop of @ref update, for this object and range @@ -31223,7 +31255,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec @param[in] depth nesting level of this object, counted from the object @ref update was called on */ - void update_members(const const_iterator& first, const const_iterator& last, const bool merge_objects, const std::size_t depth) + void update_members(const iterator& first, const iterator& last, const bool merge_objects, const std::size_t depth) { if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) { @@ -31241,13 +31273,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // are overwritten as usual" behavior (see #5402). if (it2 != m_data.m_value.object->end() && it2->second.is_object()) { - it2->second.update_members(it.value().cbegin(), it.value().cend(), true, depth + 1); + it2->second.update_members(it.value().begin(), it.value().end(), true, depth + 1); continue; } } // set_parent() also repairs the other members, which ordered_json // relocates when adding a key makes its vector grow - set_parent(m_data.m_value.object->operator[](it.key()) = it.value()); + set_parent(m_data.m_value.object->operator[](it.key()) = std::move(it.value())); } } @@ -31261,7 +31293,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec version. Only reached for values nested deeper than @ref detail::recursion_depth_limit. */ - void update_members_iteratively(const_iterator first, const_iterator last) + void update_members_iteratively(iterator first, iterator last) { std::vector stack; @@ -31288,18 +31320,18 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec const auto it2 = target->m_data.m_value.object->find(first.key()); if (it2 != target->m_data.m_value.object->end() && it2->second.is_object()) { - const basic_json& source = first.value(); + basic_json& source = first.value(); ++first; stack.emplace_back(target, first, last); target = &it2->second; - first = source.cbegin(); - last = source.cend(); + first = source.begin(); + last = source.end(); continue; } } // set_parent() also repairs the other members, which ordered_json // relocates when adding a key makes its vector grow - target->set_parent(target->m_data.m_value.object->operator[](first.key()) = first.value()); + target->set_parent(target->m_data.m_value.object->operator[](first.key()) = std::move(first.value())); ++first; } } @@ -33308,7 +33340,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/merge_patch/ void merge_patch(const basic_json& apply_patch) { - apply_merge_patch(apply_patch, 0); + // copy first: apply_patch may be *this or one of its descendants, + // and is iterated (and moved from) below to patch *this + basic_json patch = apply_patch; + apply_merge_patch(patch, 0); } private: @@ -33321,7 +33356,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec detail::recursion_depth_limit levels have been entered, @ref merge_patch_iteratively applies what is left without the call stack. */ - void apply_merge_patch(const basic_json& apply_patch, const std::size_t depth) + void apply_merge_patch(basic_json& apply_patch, const std::size_t depth) { if (apply_patch.is_object()) { @@ -33349,7 +33384,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } else { - *this = apply_patch; + *this = std::move(apply_patch); } } @@ -33362,12 +33397,12 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec recursive version. Only reached for patches nested deeper than @ref detail::recursion_depth_limit. */ - void merge_patch_iteratively(const basic_json& apply_patch) + void merge_patch_iteratively(basic_json& apply_patch) { std::vector stack; // patch `target` with `patch`, or start patching it member by member - const auto apply = [&stack](basic_json & target, const basic_json & patch) + const auto apply = [&stack](basic_json & target, basic_json & patch) { if (patch.is_object()) { @@ -33375,11 +33410,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec { target = basic_json::object(); } - stack.emplace_back(&target, patch.cbegin(), patch.cend()); + stack.emplace_back(&target, patch.begin(), patch.end()); } else { - target = patch; + target = std::move(patch); } }; @@ -33395,7 +33430,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec continue; } - const const_iterator member = frame.position; + const iterator member = frame.position; ++stack.back().position; if (member.value().is_null()) { diff --git a/tests/src/unit-diagnostics.cpp b/tests/src/unit-diagnostics.cpp index ee7360eb1..62385551c 100644 --- a/tests/src/unit-diagnostics.cpp +++ b/tests/src/unit-diagnostics.cpp @@ -236,6 +236,31 @@ TEST_CASE("Regression tests for extended diagnostics") } } + SECTION("Regression test for issue #5641 - parent pointers after update()/merge_patch() with an aliasing argument") + { + // update()'s and merge_patch()'s argument may be *this or one of its + // descendants; the values moved out of the (temporary) copy must end + // up with their parent pointing at their new location in *this + { + json j = {{"a", {{"a", 1}, {"b", 2}}}}; + j.update(j["a"]); + CHECK(j == json({{"a", 1}, {"b", 2}})); + + // Must call operator[] on const element, otherwise m_parent gets updated. + auto const& constJ = j; + CHECK_THROWS_WITH_AS(constJ["a"].at(0), "[json.exception.type_error.304] (/a) cannot use at() with number", json::type_error); + } + + { + json j = {{"a", {{"a", nullptr}, {"b", 2}}}}; + j.merge_patch(j["a"]); + CHECK(j == json({{"b", 2}})); + + auto const& constJ = j; + CHECK_THROWS_WITH_AS(constJ["b"].at(0), "[json.exception.type_error.304] (/b) cannot use at() with number", json::type_error); + } + } + SECTION("Regression test for issue #3032 - Yet another assertion failure when inserting into arrays with JSON_DIAGNOSTICS set") { // reference operator[](size_type idx) diff --git a/tests/src/unit-merge_patch.cpp b/tests/src/unit-merge_patch.cpp index b1e0c431b..2e55a66e7 100644 --- a/tests/src/unit-merge_patch.cpp +++ b/tests/src/unit-merge_patch.cpp @@ -374,3 +374,52 @@ TEST_CASE("JSON Merge Patch and update on ordered_json") CHECK(target == ordered_json::parse(R"({"a": 1, "e": {"y": 5}, "g": 6})")); } } + +TEST_CASE("merge_patch() with an argument that aliases *this (#5641)") +{ + SECTION("j.merge_patch(j): erasing a member destroys the node the loop's iterator points to") + { + // reproduces issue #5641, case 1 + json j = {{"a", nullptr}, {"b", 1}}; + j.merge_patch(j); + CHECK(j == json({{"b", 1}})); + } + + SECTION("j.merge_patch(j[\"a\"]): removing \"a\" destroys the patch while it is iterated") + { + // reproduces issue #5641, case 2 + json j = {{"a", {{"a", nullptr}, {"b", 2}}}}; + j.merge_patch(j["a"]); + CHECK(j == json({{"b", 2}})); + } + + SECTION("a patch nested past the iterative descent bound aliases *this") + { + // every depth on either side of where the iterative version takes + // over (detail::recursion_depth_limit(), 128); patching *this with + // itself is idempotent, aliased or not + for (const std::size_t depth : + { + std::size_t{0}, std::size_t{127}, std::size_t{128}, std::size_t{300} + }) + { + CAPTURE(depth); + json j = json::parse(nested_objects(depth, 0)); + const json expected = j; + j.merge_patch(j); + CHECK(j == expected); + } + } + + SECTION("ordered_json") + { + using nlohmann::ordered_json; + + SECTION("merge_patch with a member of *this") + { + ordered_json j = {{"a", {{"a", nullptr}, {"b", 2}}}}; + j.merge_patch(j["a"]); + CHECK(j == ordered_json({{"b", 2}})); + } + } +} diff --git a/tests/src/unit-modifiers.cpp b/tests/src/unit-modifiers.cpp index d36ed5ad2..d4ea6982a 100644 --- a/tests/src/unit-modifiers.cpp +++ b/tests/src/unit-modifiers.cpp @@ -1116,3 +1116,72 @@ TEST_CASE("update() on deeply nested values") CHECK(p->at("y") == 2); } } + +TEST_CASE("update() with an argument that aliases *this (#5641)") +{ + SECTION("the target is checked before the argument, as before the copy") + { + json j = 1; + CHECK_THROWS_WITH_AS(j.update(json::array()), "[json.exception.type_error.312] cannot use update() with number", json::type_error&); + CHECK_THROWS_WITH_AS(j.update(j.cbegin(), j.cend()), "[json.exception.type_error.312] cannot use update() with number", json::type_error&); + + json k; + CHECK_THROWS_WITH_AS(k.update(json::array()), "[json.exception.type_error.312] cannot use update() with array", json::type_error&); + CHECK(k == json::object()); + } + + SECTION("const reference") + { + SECTION("j.update(j[\"a\"]): assigning into the argument's parent destroys it mid-iteration") + { + // reproduces issue #5641, case 3 + json j = {{"a", {{"a", 1}, {"b", 2}}}}; + j.update(j["a"]); + CHECK(j == json({{"a", 1}, {"b", 2}})); + } + + SECTION("merge_objects with an argument that is a member of *this") + { + json j = {{"defaults", {{"opts", {{"a", 1}}}}}, {"opts", {{"b", 2}}}}; + j.update(j["defaults"], true); + CHECK(j == json({{"defaults", {{"opts", {{"a", 1}}}}}, {"opts", {{"a", 1}, {"b", 2}}}})); + } + + SECTION("ordered_json: inserting a new key relocates the vector behind the argument") + { + // reproduces issue #5641, case 4 + using nlohmann::ordered_json; + ordered_json j = {{"a", {{"x", 1}, {"y", 2}, {"z", 3}}}}; + j.update(j["a"]); + CHECK(j == ordered_json({{"a", {{"x", 1}, {"y", 2}, {"z", 3}}}, {"x", 1}, {"y", 2}, {"z", 3}})); + } + } + + SECTION("iterator range") + { + SECTION("range that is a member of *this") + { + json j = {{"a", {{"a", 1}, {"b", 2}}}}; + j.update(j["a"].begin(), j["a"].end()); + CHECK(j == json({{"a", 1}, {"b", 2}})); + } + } + + SECTION("nested past the iterative descent bound aliases *this") + { + // every depth on either side of where the iterative version takes + // over (detail::recursion_depth_limit(), 128); merging *this into + // itself is idempotent, aliased or not + for (const std::size_t depth : + { + std::size_t{0}, std::size_t{127}, std::size_t{128}, std::size_t{300} + }) + { + CAPTURE(depth); + json j = json::parse(nested_objects(depth, 0)); + const json expected = j; + j.update(j, true); + CHECK(j == expected); + } + } +}