diff --git a/include/nlohmann/detail/json_pointer.hpp b/include/nlohmann/detail/json_pointer.hpp index 247c5babb9..d9848c7770 100644 --- a/include/nlohmann/detail/json_pointer.hpp +++ b/include/nlohmann/detail/json_pointer.hpp @@ -876,58 +876,86 @@ class json_pointer const BasicJsonType& value, BasicJsonType& result) { - switch (value.type()) + struct flatten_task { - case detail::value_t::array: + string_t reference_string; + const BasicJsonType* value; + + flatten_task(string_t reference_string_, const BasicJsonType* value_) noexcept + : reference_string(std::move(reference_string_)), value(value_) + {} + }; + + std::vector stack; + stack.emplace_back(reference_string, &value); + + while (!stack.empty()) + { + auto current = std::move(stack.back()); + stack.pop_back(); + + switch (current.value->type()) { - if (value.m_data.m_value.array->empty()) - { - // flatten empty array as null - result[reference_string] = nullptr; - } - else + case detail::value_t::array: { - // iterate array and use index as a reference string - for (std::size_t i = 0; i < value.m_data.m_value.array->size(); ++i) + if (current.value->m_data.m_value.array->empty()) { - flatten(detail::concat(reference_string, '/', std::to_string(i)), - value.m_data.m_value.array->operator[](i), result); + // flatten empty array as null + result[current.reference_string] = nullptr; } + else + { + // iterate array and use index as a reference string + for (std::size_t i = current.value->m_data.m_value.array->size(); i > 0; --i) + { + const auto index = i - 1; + stack.emplace_back(detail::concat(current.reference_string, '/', std::to_string(index)), + ¤t.value->m_data.m_value.array->operator[](index)); + } + } + break; } - break; - } - case detail::value_t::object: - { - if (value.m_data.m_value.object->empty()) - { - // flatten empty object as null - result[reference_string] = nullptr; - } - else + case detail::value_t::object: { - // iterate object and use keys as reference string - for (const auto& element : *value.m_data.m_value.object) + if (current.value->m_data.m_value.object->empty()) { - flatten(detail::concat(reference_string, '/', detail::escape(element.first)), element.second, result); + // flatten empty object as null + result[current.reference_string] = nullptr; } + else + { + // iterate object and use keys as reference string + std::vector children; + children.reserve(current.value->m_data.m_value.object->size()); + for (const auto& element : *current.value->m_data.m_value.object) + { + children.emplace_back(detail::concat(current.reference_string, '/', detail::escape(element.first)), + &element.second); + } + // Push children in reverse so the LIFO stack preserves object iteration order. + for (auto it = children.rbegin(); it != children.rend(); ++it) + { + stack.emplace_back(std::move(*it)); + } + } + break; } - break; - } - case detail::value_t::null: - 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: - { - // add a primitive value with its reference string - result[reference_string] = value; - break; + case detail::value_t::null: + 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: + { + // add a primitive value with its reference string + result[current.reference_string] = *current.value; + break; + } } } } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index d3c8293a79..011d7c65e3 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -16660,58 +16660,86 @@ class json_pointer const BasicJsonType& value, BasicJsonType& result) { - switch (value.type()) + struct flatten_task { - case detail::value_t::array: + string_t reference_string; + const BasicJsonType* value; + + flatten_task(string_t reference_string_, const BasicJsonType* value_) noexcept + : reference_string(std::move(reference_string_)), value(value_) + {} + }; + + std::vector stack; + stack.emplace_back(reference_string, &value); + + while (!stack.empty()) + { + auto current = std::move(stack.back()); + stack.pop_back(); + + switch (current.value->type()) { - if (value.m_data.m_value.array->empty()) - { - // flatten empty array as null - result[reference_string] = nullptr; - } - else + case detail::value_t::array: { - // iterate array and use index as a reference string - for (std::size_t i = 0; i < value.m_data.m_value.array->size(); ++i) + if (current.value->m_data.m_value.array->empty()) + { + // flatten empty array as null + result[current.reference_string] = nullptr; + } + else { - flatten(detail::concat(reference_string, '/', std::to_string(i)), - value.m_data.m_value.array->operator[](i), result); + // iterate array and use index as a reference string + for (std::size_t i = current.value->m_data.m_value.array->size(); i > 0; --i) + { + const auto index = i - 1; + stack.emplace_back(detail::concat(current.reference_string, '/', std::to_string(index)), + ¤t.value->m_data.m_value.array->operator[](index)); + } } + break; } - break; - } - case detail::value_t::object: - { - if (value.m_data.m_value.object->empty()) - { - // flatten empty object as null - result[reference_string] = nullptr; - } - else + case detail::value_t::object: { - // iterate object and use keys as reference string - for (const auto& element : *value.m_data.m_value.object) + if (current.value->m_data.m_value.object->empty()) + { + // flatten empty object as null + result[current.reference_string] = nullptr; + } + else { - flatten(detail::concat(reference_string, '/', detail::escape(element.first)), element.second, result); + // iterate object and use keys as reference string + std::vector children; + children.reserve(current.value->m_data.m_value.object->size()); + for (const auto& element : *current.value->m_data.m_value.object) + { + children.emplace_back(detail::concat(current.reference_string, '/', detail::escape(element.first)), + &element.second); + } + // Push children in reverse so the LIFO stack preserves object iteration order. + for (auto it = children.rbegin(); it != children.rend(); ++it) + { + stack.emplace_back(std::move(*it)); + } } + break; } - break; - } - case detail::value_t::null: - 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: - { - // add a primitive value with its reference string - result[reference_string] = value; - break; + case detail::value_t::null: + 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: + { + // add a primitive value with its reference string + result[current.reference_string] = *current.value; + break; + } } } } diff --git a/tests/src/unit-regression2.cpp b/tests/src/unit-regression2.cpp index 6c30e35039..fcbff58e78 100644 --- a/tests/src/unit-regression2.cpp +++ b/tests/src/unit-regression2.cpp @@ -1571,4 +1571,14 @@ TEST_CASE("issue #5402 - update(merge_objects=true) overwrites a primitive with CHECK(mixed == json({{"keep", {{"a", 1}, {"b", 2}}}, {"replace", {{"x", 2}}}})); } +TEST_CASE("issue #5393 - flatten deeply nested values without overflowing the stack") +{ + constexpr std::size_t depth = 100000; + const auto deep = json::parse(std::string(depth, '[') + "0" + std::string(depth, ']')); + const auto flattened = deep.flatten(); + CHECK(flattened.size() == 1); + CHECK(flattened.begin().value() == 0); + CHECK(flattened.begin().key().size() == depth * 2); +} + DOCTEST_CLANG_SUPPRESS_WARNING_POP