diff --git a/docs/mkdocs/docs/api/macros/index.md b/docs/mkdocs/docs/api/macros/index.md index 507c04932..edee545eb 100644 --- a/docs/mkdocs/docs/api/macros/index.md +++ b/docs/mkdocs/docs/api/macros/index.md @@ -22,6 +22,7 @@ header. See also the [macro overview page](../../features/macros.md). - [**JSON_HAS_STD_FORMAT**](json_has_std_format.md) - control `std::format`/`std::formatter` support - [**JSON_HAS_THREE_WAY_COMPARISON**](json_has_three_way_comparison.md) - control 3-way comparison support - [**JSON_NO_IO**](json_no_io.md) - switch off functions relying on certain C++ I/O headers +- [**JSON_NO_THREAD_LOCAL**](json_no_thread_local.md) - switch off the use of `thread_local` storage - [**JSON_SKIP_UNSUPPORTED_COMPILER_CHECK**](json_skip_unsupported_compiler_check.md) - do not warn about unsupported compilers - [**JSON_USE_GLOBAL_UDLS**](json_use_global_udls.md) - place user-defined string literals (UDLs) into the global namespace diff --git a/docs/mkdocs/docs/api/macros/json_no_thread_local.md b/docs/mkdocs/docs/api/macros/json_no_thread_local.md new file mode 100644 index 000000000..6d6460811 --- /dev/null +++ b/docs/mkdocs/docs/api/macros/json_no_thread_local.md @@ -0,0 +1,42 @@ +# JSON_NO_THREAD_LOCAL + +```cpp +#define JSON_NO_THREAD_LOCAL +``` + +When defined, the library does not use `#!cpp thread_local` storage. This is relevant for the few environments whose +toolchain does not support it. + +The copy constructor copies the first levels of a value by copying the containers, which copy their elements, and +completes whatever is nested deeper than that without the call stack, so that copying a value cannot exhaust the stack +however deeply it is nested. It counts the levels it has descended into in a `#!cpp thread_local` variable, as a counter +shared between threads would be raced. + +Without that counter, no descent can be bounded safely, so objects and arrays are copied without the call stack right +away. Copying keeps working exactly as it does otherwise - the same values come out, and deeply nested values are copied +just as safely - but copying is measurably slower, as the containers no longer copy themselves. + +## Default definition + +By default, `#!cpp JSON_NO_THREAD_LOCAL` is not defined. + +```cpp +#undef JSON_NO_THREAD_LOCAL +``` + +## Examples + +??? example + + The code below forces the library not to use `#!cpp thread_local` storage. + + ```cpp + #define JSON_NO_THREAD_LOCAL 1 + #include + + ... + ``` + +## Version history + +- Added in version 3.12.1. diff --git a/docs/mkdocs/docs/features/macros.md b/docs/mkdocs/docs/features/macros.md index 1d169fdeb..8296eb05e 100644 --- a/docs/mkdocs/docs/features/macros.md +++ b/docs/mkdocs/docs/features/macros.md @@ -91,6 +91,13 @@ security reasons (e.g., Intel Software Guard Extensions (SGX)). See [full documentation of `JSON_NO_IO`](../api/macros/json_no_io.md). +## `JSON_NO_THREAD_LOCAL` + +When defined, the library does not use `#!cpp thread_local` storage. Copying a value then always avoids the call stack +rather than descending into a bounded number of levels first, which is slower but yields the same values. + +See [full documentation of `JSON_NO_THREAD_LOCAL`](../api/macros/json_no_thread_local.md). + ## `JSON_SKIP_LIBRARY_VERSION_CHECK` When defined, the library will not create a compiler warning when a different version of the library was already diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml index 2e1337f47..3f0031afe 100644 --- a/docs/mkdocs/mkdocs.yml +++ b/docs/mkdocs/mkdocs.yml @@ -291,6 +291,7 @@ nav: - 'JSON_HAS_THREE_WAY_COMPARISON': api/macros/json_has_three_way_comparison.md - 'JSON_NOEXCEPTION': api/macros/json_noexception.md - 'JSON_NO_IO': api/macros/json_no_io.md + - 'JSON_NO_THREAD_LOCAL': api/macros/json_no_thread_local.md - 'JSON_SKIP_LIBRARY_VERSION_CHECK': api/macros/json_skip_library_version_check.md - 'JSON_SKIP_UNSUPPORTED_COMPILER_CHECK': api/macros/json_skip_unsupported_compiler_check.md - 'JSON_USE_GLOBAL_UDLS': api/macros/json_use_global_udls.md diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 235e6b737..f981f7945 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -28,14 +28,14 @@ #pragma GCC diagnostic ignored "-Wignored-attributes" #endif -#include // all_of, find, for_each +#include // all_of, find, for_each, none_of #include // nullptr_t, ptrdiff_t, size_t #include // hash, less #include // initializer_list #ifndef JSON_NO_IO #include // istream, ostream #endif // JSON_NO_IO -#include // random_access_iterator_tag +#include // make_move_iterator, random_access_iterator_tag #include // unique_ptr #include // string, stoi, to_string #include // declval, forward, move, pair, swap @@ -821,6 +821,307 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec return j; } +#ifndef JSON_NO_THREAD_LOCAL + /// the number of levels the copy constructor descends into before it + /// finishes the value below without the call stack + static constexpr std::size_t copy_depth_limit() + { + return 128; + } + + /// @brief how many levels the copy going on in this thread has descended into + static std::size_t& copy_depth() noexcept + { + static thread_local std::size_t depth = 0; // NOLINT(misc-use-internal-linkage) + return depth; + } +#endif + +#ifndef JSON_NO_THREAD_LOCAL + /// @brief counts one level of @ref copy_structured for as long as it runs + class copy_depth_guard + { + public: + explicit copy_depth_guard(std::size_t& depth) noexcept + : m_depth(depth) + { + ++m_depth; + } + + ~copy_depth_guard() noexcept + { + --m_depth; + } + + copy_depth_guard(const copy_depth_guard&) = delete; + copy_depth_guard& operator=(const copy_depth_guard&) = delete; + copy_depth_guard(copy_depth_guard&&) = delete; + copy_depth_guard& operator=(copy_depth_guard&&) = delete; + + private: + std::size_t& m_depth; + }; +#endif + + /// an entry of the iterative deep copy's worklist: a structured value and + /// the value that is to become its copy + using copy_worklist_t = std::vector>; + + /// scratch space to build the key skeleton of an object copy in one go + using copy_scratch_t = std::vector>; + + /// @brief copy everything of @a src into @a dst but its type and value + static void copy_metadata(const basic_json& src, basic_json& dst) + { + // a custom base class is only required to be copy-constructible and + // move-assignable, so the copy has to go through a temporary + static_cast(dst) = json_base_class_t(static_cast(src)); + +#if JSON_DIAGNOSTIC_POSITIONS + dst.start_position = src.start_position; + dst.end_position = src.end_position; +#else + static_cast(src); + static_cast(dst); +#endif + } + + /*! + @brief copy everything of @a src into the null value @a dst but the children + + Objects and arrays are not copied here; they are appended to @a worklist to + be created later by @ref copy_iteratively. Until that happens, @a dst remains + a null value, so that a partially built copy can be destroyed at any point + without ever violating the class invariants. + */ + static void copy_shallow(const basic_json& src, basic_json& dst, copy_worklist_t& worklist) + { + copy_metadata(src, dst); + + switch (src.m_data.m_type) + { + case value_t::object: + case value_t::array: + { + // defer: dst stays a null value until its container exists + worklist.emplace_back(&src, &dst); + return; + } + + case value_t::string: + { + dst.m_data.m_value = *src.m_data.m_value.string; + break; + } + + case value_t::binary: + { + dst.m_data.m_value = *src.m_data.m_value.binary; + break; + } + + case value_t::boolean: + { + dst.m_data.m_value = src.m_data.m_value.boolean; + break; + } + + case value_t::number_integer: + { + dst.m_data.m_value = src.m_data.m_value.number_integer; + break; + } + + case value_t::number_unsigned: + { + dst.m_data.m_value = src.m_data.m_value.number_unsigned; + break; + } + + case value_t::number_float: + { + dst.m_data.m_value = src.m_data.m_value.number_float; + break; + } + + case value_t::null: + case value_t::discarded: + default: + break; + } + + // only now that the value exists may the type be set: had the creation + // of the value thrown, dst would have been left as a valid null value + dst.m_data.m_type = src.m_data.m_type; + } + + /// @brief create the copy of the array @a src in @a dst + /// @note structured elements are appended to @a worklist instead + static void copy_array_level(const basic_json& src, basic_json& dst, copy_worklist_t& worklist) + { + const array_t& src_array = *src.m_data.m_value.array; + + // create all elements up front: growing the array afterwards could + // invalidate the pointers that are handed to the worklist + dst.m_data.m_value.array = create(src_array.size(), basic_json()); + + auto dst_it = dst.m_data.m_value.array->begin(); + for (auto src_it = src_array.cbegin(); src_it != src_array.cend(); ++src_it, ++dst_it) + { + copy_shallow(*src_it, *dst_it, worklist); + } + } + + /// @brief create the copy of the object @a src in @a dst + /// @note structured values are appended to @a worklist instead + static void copy_object_level(const basic_json& src, basic_json& dst, + copy_worklist_t& worklist, copy_scratch_t& scratch) + { + const object_t& src_object = *src.m_data.m_value.object; + + // build the complete key skeleton and hand it to the object's range + // constructor: adding the keys one by one would be quadratic for object + // types that are backed by a vector, such as nlohmann::ordered_map + scratch.clear(); + scratch.reserve(src_object.size()); + for (const auto& element : src_object) + { + scratch.emplace_back(element.first, basic_json()); + } + + dst.m_data.m_value.object = create(std::make_move_iterator(scratch.begin()), + std::make_move_iterator(scratch.end())); + scratch.clear(); + + // pair every value of the copy with its counterpart in the original; + // both are enumerated in the same order for every object type with a + // deterministic order, so the lookup is only needed for exotic ones + auto src_it = src_object.cbegin(); + for (auto& element : *dst.m_data.m_value.object) + { + if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && src_it->first == element.first)) + { + copy_shallow(src_it->second, element.second, worklist); + ++src_it; + } + else + { + const auto found = src_object.find(element.first); + JSON_ASSERT(found != src_object.cend()); + copy_shallow(found->second, element.second, worklist); + } + } + } + + /*! + @brief deep-copy the object or array @a src into this value without recursing + + The values whose copy has not been created yet are kept on an explicit + worklist rather than on the call stack. This is only reached for values + nested deeper than @ref copy_depth_limit levels, which is why it copies + every container by hand instead of letting the container do it: the fast + ways of doing so would descend into the elements and defeat the purpose. + */ + void copy_iteratively(const basic_json& src) + { + copy_worklist_t worklist; + copy_scratch_t scratch; + + const basic_json* src_value = &src; + basic_json* dst_value = this; + + for (;;) + { + if (src_value->m_data.m_type == value_t::array) + { + copy_array_level(*src_value, *dst_value, worklist); + } + else + { + copy_object_level(*src_value, *dst_value, worklist, scratch); + } + + // the container is complete and will not be modified again + dst_value->set_parents(); + + if (worklist.empty()) + { + break; + } + + src_value = worklist.back().first; + dst_value = worklist.back().second; + worklist.pop_back(); + + // the value stops being a null value exactly here + dst_value->m_data.m_type = src_value->m_data.m_type; + } + } + +#ifndef JSON_NO_THREAD_LOCAL + /*! + @brief copy one level of the object or array @a src into this value + + The container copies its own elements, which is the fastest way to fill it. + Every element that is structured itself comes back to @ref copy_structured. + */ + void copy_level(const basic_json& src) + { + if (m_data.m_type == value_t::object) + { + m_data.m_value = *src.m_data.m_value.object; + } + else + { + m_data.m_value = *src.m_data.m_value.array; + } + + set_parents(); + } +#endif + + /*! + @brief deep-copy the object or array @a src into this value + + Copying a container copies its elements, so a value nested deeply enough + used to exhaust the call stack. The descent is bounded here: the first + @ref copy_depth_limit levels are copied by the containers themselves, just + as they always were, and anything below that is copied without the call + stack by @ref copy_iteratively. Copying a value can therefore no longer + exhaust the stack, however deeply it is nested, just like destroying one + cannot since #1436. + + Nothing has to be scanned or built by hand to reach that: a value that is + not nested deeper than the limit - all but a vanishing minority - is copied + exactly as it was before, and this whole detour costs it one counter. + + @sa https://github.com/nlohmann/json/issues/5387 + */ + void copy_structured(const basic_json& src) + { +#ifdef JSON_NO_THREAD_LOCAL + // without a counter of its own per thread, the descent cannot be + // bounded without racing another one, so none is made + copy_iteratively(src); +#else + std::size_t& depth = copy_depth(); + + if (JSON_HEDLEY_UNLIKELY(depth >= copy_depth_limit())) + { + // Finish this value without descending any further. It is completed + // before this returns, so a copy made by a custom base class - or by + // anything else that runs while a copy is going on - is unaffected + // by the copy it is nested in. + copy_iteratively(src); + return; + } + + const copy_depth_guard guard(depth); + copy_level(src); +#endif + } + + public: ////////////////////////// // JSON parser callback // @@ -1203,14 +1504,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec switch (m_data.m_type) { case value_t::object: - { - m_data.m_value = *other.m_data.m_value.object; - break; - } - case value_t::array: { - m_data.m_value = *other.m_data.m_value.array; + // copying the container directly would call this constructor + // again for every element, once per nesting level + copy_structured(other); break; } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 0ea563fb9..4ab4e4a92 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -28,14 +28,14 @@ #pragma GCC diagnostic ignored "-Wignored-attributes" #endif -#include // all_of, find, for_each +#include // all_of, find, for_each, none_of #include // nullptr_t, ptrdiff_t, size_t #include // hash, less #include // initializer_list #ifndef JSON_NO_IO #include // istream, ostream #endif // JSON_NO_IO -#include // random_access_iterator_tag +#include // make_move_iterator, random_access_iterator_tag #include // unique_ptr #include // string, stoi, to_string #include // declval, forward, move, pair, swap @@ -22166,6 +22166,307 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec return j; } +#ifndef JSON_NO_THREAD_LOCAL + /// the number of levels the copy constructor descends into before it + /// finishes the value below without the call stack + static constexpr std::size_t copy_depth_limit() + { + return 128; + } + + /// @brief how many levels the copy going on in this thread has descended into + static std::size_t& copy_depth() noexcept + { + static thread_local std::size_t depth = 0; // NOLINT(misc-use-internal-linkage) + return depth; + } +#endif + +#ifndef JSON_NO_THREAD_LOCAL + /// @brief counts one level of @ref copy_structured for as long as it runs + class copy_depth_guard + { + public: + explicit copy_depth_guard(std::size_t& depth) noexcept + : m_depth(depth) + { + ++m_depth; + } + + ~copy_depth_guard() noexcept + { + --m_depth; + } + + copy_depth_guard(const copy_depth_guard&) = delete; + copy_depth_guard& operator=(const copy_depth_guard&) = delete; + copy_depth_guard(copy_depth_guard&&) = delete; + copy_depth_guard& operator=(copy_depth_guard&&) = delete; + + private: + std::size_t& m_depth; + }; +#endif + + /// an entry of the iterative deep copy's worklist: a structured value and + /// the value that is to become its copy + using copy_worklist_t = std::vector>; + + /// scratch space to build the key skeleton of an object copy in one go + using copy_scratch_t = std::vector>; + + /// @brief copy everything of @a src into @a dst but its type and value + static void copy_metadata(const basic_json& src, basic_json& dst) + { + // a custom base class is only required to be copy-constructible and + // move-assignable, so the copy has to go through a temporary + static_cast(dst) = json_base_class_t(static_cast(src)); + +#if JSON_DIAGNOSTIC_POSITIONS + dst.start_position = src.start_position; + dst.end_position = src.end_position; +#else + static_cast(src); + static_cast(dst); +#endif + } + + /*! + @brief copy everything of @a src into the null value @a dst but the children + + Objects and arrays are not copied here; they are appended to @a worklist to + be created later by @ref copy_iteratively. Until that happens, @a dst remains + a null value, so that a partially built copy can be destroyed at any point + without ever violating the class invariants. + */ + static void copy_shallow(const basic_json& src, basic_json& dst, copy_worklist_t& worklist) + { + copy_metadata(src, dst); + + switch (src.m_data.m_type) + { + case value_t::object: + case value_t::array: + { + // defer: dst stays a null value until its container exists + worklist.emplace_back(&src, &dst); + return; + } + + case value_t::string: + { + dst.m_data.m_value = *src.m_data.m_value.string; + break; + } + + case value_t::binary: + { + dst.m_data.m_value = *src.m_data.m_value.binary; + break; + } + + case value_t::boolean: + { + dst.m_data.m_value = src.m_data.m_value.boolean; + break; + } + + case value_t::number_integer: + { + dst.m_data.m_value = src.m_data.m_value.number_integer; + break; + } + + case value_t::number_unsigned: + { + dst.m_data.m_value = src.m_data.m_value.number_unsigned; + break; + } + + case value_t::number_float: + { + dst.m_data.m_value = src.m_data.m_value.number_float; + break; + } + + case value_t::null: + case value_t::discarded: + default: + break; + } + + // only now that the value exists may the type be set: had the creation + // of the value thrown, dst would have been left as a valid null value + dst.m_data.m_type = src.m_data.m_type; + } + + /// @brief create the copy of the array @a src in @a dst + /// @note structured elements are appended to @a worklist instead + static void copy_array_level(const basic_json& src, basic_json& dst, copy_worklist_t& worklist) + { + const array_t& src_array = *src.m_data.m_value.array; + + // create all elements up front: growing the array afterwards could + // invalidate the pointers that are handed to the worklist + dst.m_data.m_value.array = create(src_array.size(), basic_json()); + + auto dst_it = dst.m_data.m_value.array->begin(); + for (auto src_it = src_array.cbegin(); src_it != src_array.cend(); ++src_it, ++dst_it) + { + copy_shallow(*src_it, *dst_it, worklist); + } + } + + /// @brief create the copy of the object @a src in @a dst + /// @note structured values are appended to @a worklist instead + static void copy_object_level(const basic_json& src, basic_json& dst, + copy_worklist_t& worklist, copy_scratch_t& scratch) + { + const object_t& src_object = *src.m_data.m_value.object; + + // build the complete key skeleton and hand it to the object's range + // constructor: adding the keys one by one would be quadratic for object + // types that are backed by a vector, such as nlohmann::ordered_map + scratch.clear(); + scratch.reserve(src_object.size()); + for (const auto& element : src_object) + { + scratch.emplace_back(element.first, basic_json()); + } + + dst.m_data.m_value.object = create(std::make_move_iterator(scratch.begin()), + std::make_move_iterator(scratch.end())); + scratch.clear(); + + // pair every value of the copy with its counterpart in the original; + // both are enumerated in the same order for every object type with a + // deterministic order, so the lookup is only needed for exotic ones + auto src_it = src_object.cbegin(); + for (auto& element : *dst.m_data.m_value.object) + { + if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && src_it->first == element.first)) + { + copy_shallow(src_it->second, element.second, worklist); + ++src_it; + } + else + { + const auto found = src_object.find(element.first); + JSON_ASSERT(found != src_object.cend()); + copy_shallow(found->second, element.second, worklist); + } + } + } + + /*! + @brief deep-copy the object or array @a src into this value without recursing + + The values whose copy has not been created yet are kept on an explicit + worklist rather than on the call stack. This is only reached for values + nested deeper than @ref copy_depth_limit levels, which is why it copies + every container by hand instead of letting the container do it: the fast + ways of doing so would descend into the elements and defeat the purpose. + */ + void copy_iteratively(const basic_json& src) + { + copy_worklist_t worklist; + copy_scratch_t scratch; + + const basic_json* src_value = &src; + basic_json* dst_value = this; + + for (;;) + { + if (src_value->m_data.m_type == value_t::array) + { + copy_array_level(*src_value, *dst_value, worklist); + } + else + { + copy_object_level(*src_value, *dst_value, worklist, scratch); + } + + // the container is complete and will not be modified again + dst_value->set_parents(); + + if (worklist.empty()) + { + break; + } + + src_value = worklist.back().first; + dst_value = worklist.back().second; + worklist.pop_back(); + + // the value stops being a null value exactly here + dst_value->m_data.m_type = src_value->m_data.m_type; + } + } + +#ifndef JSON_NO_THREAD_LOCAL + /*! + @brief copy one level of the object or array @a src into this value + + The container copies its own elements, which is the fastest way to fill it. + Every element that is structured itself comes back to @ref copy_structured. + */ + void copy_level(const basic_json& src) + { + if (m_data.m_type == value_t::object) + { + m_data.m_value = *src.m_data.m_value.object; + } + else + { + m_data.m_value = *src.m_data.m_value.array; + } + + set_parents(); + } +#endif + + /*! + @brief deep-copy the object or array @a src into this value + + Copying a container copies its elements, so a value nested deeply enough + used to exhaust the call stack. The descent is bounded here: the first + @ref copy_depth_limit levels are copied by the containers themselves, just + as they always were, and anything below that is copied without the call + stack by @ref copy_iteratively. Copying a value can therefore no longer + exhaust the stack, however deeply it is nested, just like destroying one + cannot since #1436. + + Nothing has to be scanned or built by hand to reach that: a value that is + not nested deeper than the limit - all but a vanishing minority - is copied + exactly as it was before, and this whole detour costs it one counter. + + @sa https://github.com/nlohmann/json/issues/5387 + */ + void copy_structured(const basic_json& src) + { +#ifdef JSON_NO_THREAD_LOCAL + // without a counter of its own per thread, the descent cannot be + // bounded without racing another one, so none is made + copy_iteratively(src); +#else + std::size_t& depth = copy_depth(); + + if (JSON_HEDLEY_UNLIKELY(depth >= copy_depth_limit())) + { + // Finish this value without descending any further. It is completed + // before this returns, so a copy made by a custom base class - or by + // anything else that runs while a copy is going on - is unaffected + // by the copy it is nested in. + copy_iteratively(src); + return; + } + + const copy_depth_guard guard(depth); + copy_level(src); +#endif + } + + public: ////////////////////////// // JSON parser callback // @@ -22548,14 +22849,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec switch (m_data.m_type) { case value_t::object: - { - m_data.m_value = *other.m_data.m_value.object; - break; - } - case value_t::array: { - m_data.m_value = *other.m_data.m_value.array; + // copying the container directly would call this constructor + // again for every element, once per nesting level + copy_structured(other); break; } diff --git a/tests/src/unit-large_json.cpp b/tests/src/unit-large_json.cpp index 98d16e336..4e3bdfa8d 100644 --- a/tests/src/unit-large_json.cpp +++ b/tests/src/unit-large_json.cpp @@ -12,6 +12,7 @@ using nlohmann::json; #include +#include TEST_CASE("tests on very large JSONs") { @@ -27,3 +28,153 @@ TEST_CASE("tests on very large JSONs") } } +namespace +{ + +// Descend a chain of single-element containers and return the value at its end, +// reporting the number of levels traversed in @a depth. +// +// The values in the test case below are nested far deeper than the call stack +// can follow, so they must not be inspected with operator== or dump(): both are +// still recursive and would overflow the stack themselves. +const json* innermost_value(const json& j, std::size_t& depth) +{ + const json* current = &j; + depth = 0; + + while ((current->is_array() || current->is_object()) && !current->empty()) + { + current = current->is_array() + ? ¤t->front() + : ¤t->begin().value(); + ++depth; + } + + return current; +} + +} // namespace + +TEST_CASE("tests on deeply nested JSONs") +{ + // deep enough to exhaust the call stack, but small enough to stay cheap: + // parsing is iterative, so building the values below costs little + const std::size_t depth = 100000; + + SECTION("issue #5387 - stack overflow in the copy constructor") + { + SECTION("array") + { + const json j = json::parse(std::string(depth, '[') + '0' + std::string(depth, ']')); + + const json copy(j); + + std::size_t copy_depth = 0; + CHECK(*innermost_value(copy, copy_depth) == 0); + CHECK(copy_depth == depth); + } + + SECTION("object") + { + std::string s; + s.reserve(6 * depth + 1); + for (std::size_t i = 0; i < depth; ++i) + { + s += "{\"a\":"; + } + s += '1'; + s.append(depth, '}'); + + const json j = json::parse(s); + + const json copy(j); + + std::size_t copy_depth = 0; + CHECK(*innermost_value(copy, copy_depth) == 1); + CHECK(copy_depth == depth); + } + + SECTION("copy assignment") + { + // operator=(basic_json) takes its argument by value, so the deep + // copy happens in the copy constructor + const json j = json::parse(std::string(depth, '[') + '0' + std::string(depth, ']')); + + json target; + target = j; + + std::size_t target_depth = 0; + CHECK(*innermost_value(target, target_depth) == 0); + CHECK(target_depth == depth); + } + + SECTION("depths around the bound of the recursive descent") + { + // The copy constructor descends into a bounded number of levels and + // completes whatever is below that without the call stack. Cover + // every depth around that bound, so that the two ways of copying + // are known to meet cleanly - wherever the bound is set. + for (std::size_t d = 1; d <= 300; ++d) + { + CAPTURE(d); + + const json array = json::parse(std::string(d, '[') + '0' + std::string(d, ']')); + const json array_copy(array); + std::size_t array_depth = 0; + CHECK(*innermost_value(array_copy, array_depth) == 0); + CHECK(array_depth == d); + + std::string object_text; + for (std::size_t i = 0; i < d; ++i) + { + object_text += "{\"a\":"; + } + object_text += '1'; + object_text.append(d, '}'); + + const json object = json::parse(object_text); + const json object_copy(object); + std::size_t object_depth = 0; + CHECK(*innermost_value(object_copy, object_depth) == 1); + CHECK(object_depth == d); + } + } + + SECTION("a value that is deep in one place only") + { + json j = json::object(); + j["shallow"] = 1; + j["deep"] = json::parse(std::string(depth, '[') + '0' + std::string(depth, ']')); + j["also_shallow"] = json::array({1, 2, 3}); + + const json copy(j); + + CHECK(copy["shallow"] == 1); + CHECK(copy["also_shallow"] == json::array({1, 2, 3})); + + std::size_t deep_depth = 0; + CHECK(*innermost_value(copy["deep"], deep_depth) == 0); + CHECK(deep_depth == depth); + } + + SECTION("the copy is independent of the original") + { + const json j = json::parse(std::string(depth, '[') + '0' + std::string(depth, ']')); + + json copy(j); + + // reach the innermost value without recursing and replace it + json* current = © + while (current->is_array() && !current->empty()) + { + current = ¤t->front(); + } + *current = 42; + + std::size_t unused = 0; + CHECK(*innermost_value(copy, unused) == 42); + CHECK(*innermost_value(j, unused) == 0); + } + } +} + diff --git a/tests/src/unit-ordered_json.cpp b/tests/src/unit-ordered_json.cpp index a38a1a2b8..4396aac06 100644 --- a/tests/src/unit-ordered_json.cpp +++ b/tests/src/unit-ordered_json.cpp @@ -81,3 +81,37 @@ TEST_CASE("regression test for issue #3732 - iteration_proxy_value(fn); } + +TEST_CASE("copying an ordered_json with nested values") +{ + // ordered_map is backed by a vector, so copying an object that has + // structured values takes a different route than copying a std::map-backed + // one; see https://github.com/nlohmann/json/issues/5387 + ordered_json oj; + oj["z"] = 1; + oj["a"]["y"] = 2; + oj["a"]["b"]["x"] = 3; + oj["m"] = {1, 2, {{"w", 4}}}; + + const ordered_json copy(oj); + + SECTION("the copy is equal to the original") + { + CHECK(copy == oj); + CHECK(copy.dump() == oj.dump()); + } + + SECTION("the key order is preserved at every level") + { + CHECK(copy.dump() == R"({"z":1,"a":{"y":2,"b":{"x":3}},"m":[1,2,{"w":4}]})"); + } + + SECTION("the copy is independent of the original") + { + ordered_json mutated(oj); + mutated["a"]["b"]["x"] = 99; + + CHECK(oj["a"]["b"]["x"] == 3); + CHECK(mutated["a"]["b"]["x"] == 99); + } +}