diff --git a/include/nlohmann/detail/output/serializer.hpp b/include/nlohmann/detail/output/serializer.hpp index 565169e24..71566ace5 100644 --- a/include/nlohmann/detail/output/serializer.hpp +++ b/include/nlohmann/detail/output/serializer.hpp @@ -22,6 +22,7 @@ #include // setfill, setw #include // is_same #include // move +#include // vector #include #include @@ -88,8 +89,8 @@ class serializer This function is called by the public member function dump and organizes the serialization internally. The indentation level is propagated as - additional parameter. In case of arrays and objects, the function is - called recursively. + additional parameter. Arrays and objects are serialized without recursion, + however deeply they are nested. - strings and object keys are escaped using `escape_string()` - integer numbers are converted implicitly via `operator<<` @@ -117,23 +118,39 @@ class serializer JSON_PRIVATE_UNLESS_TESTED: /*! - @brief recursive worker for @ref dump + @brief worker for @ref dump Identical in behavior to the historical @ref dump, but writes into the serializer's internal @ref write_buffer instead of issuing a virtual call per token. The public @ref dump wraps this and flushes the buffer once the top-level value has been serialized. + + Serializing a container descends into its elements, so a value nested deeply + enough used to exhaust the call stack and terminate the process with no + exception to catch. The descent is bounded here: once @ref dump_depth_limit + levels have been entered, @ref dump_iteratively writes out what is left + without the call stack. A value nested less deeply than that - all but a + vanishing minority - is written by exactly the code that always wrote it. + + @sa https://github.com/nlohmann/json/issues/5387 */ void dump_internal(const BasicJsonType& val, const bool pretty_print, const bool ensure_ascii, const std::size_t indent_step, - const std::size_t current_indent = 0) + const std::size_t current_indent = 0, + const std::size_t depth = 0) { switch (val.m_data.m_type) { case value_t::object: { + if (JSON_HEDLEY_UNLIKELY(depth >= dump_depth_limit())) + { + dump_iteratively(val, pretty_print, ensure_ascii, indent_step, current_indent); + return; + } + if (val.m_data.m_value.object->empty()) { put_literal("{}"); @@ -155,7 +172,7 @@ class serializer put_char('\"'); dump_escaped(i->first, ensure_ascii); put_literal("\": "); - dump_internal(i->second, true, ensure_ascii, indent_step, new_indent); + dump_internal(i->second, true, ensure_ascii, indent_step, new_indent, depth + 1); put_literal(",\n"); } @@ -166,7 +183,7 @@ class serializer put_char('\"'); dump_escaped(i->first, ensure_ascii); put_literal("\": "); - dump_internal(i->second, true, ensure_ascii, indent_step, new_indent); + dump_internal(i->second, true, ensure_ascii, indent_step, new_indent, depth + 1); put_char('\n'); put_indent(current_indent); @@ -183,7 +200,7 @@ class serializer put_char('\"'); dump_escaped(i->first, ensure_ascii); put_literal("\":"); - dump_internal(i->second, false, ensure_ascii, indent_step, current_indent); + dump_internal(i->second, false, ensure_ascii, indent_step, current_indent, depth + 1); put_char(','); } @@ -193,7 +210,7 @@ class serializer put_char('\"'); dump_escaped(i->first, ensure_ascii); put_literal("\":"); - dump_internal(i->second, false, ensure_ascii, indent_step, current_indent); + dump_internal(i->second, false, ensure_ascii, indent_step, current_indent, depth + 1); put_char('}'); } @@ -203,6 +220,12 @@ class serializer case value_t::array: { + if (JSON_HEDLEY_UNLIKELY(depth >= dump_depth_limit())) + { + dump_iteratively(val, pretty_print, ensure_ascii, indent_step, current_indent); + return; + } + if (val.m_data.m_value.array->empty()) { put_literal("[]"); @@ -221,14 +244,14 @@ class serializer i != val.m_data.m_value.array->cend() - 1; ++i) { put_indent(new_indent); - dump_internal(*i, true, ensure_ascii, indent_step, new_indent); + dump_internal(*i, true, ensure_ascii, indent_step, new_indent, depth + 1); put_literal(",\n"); } // last element JSON_ASSERT(!val.m_data.m_value.array->empty()); put_indent(new_indent); - dump_internal(val.m_data.m_value.array->back(), true, ensure_ascii, indent_step, new_indent); + dump_internal(val.m_data.m_value.array->back(), true, ensure_ascii, indent_step, new_indent, depth + 1); put_char('\n'); put_indent(current_indent); @@ -242,13 +265,13 @@ class serializer for (auto i = val.m_data.m_value.array->cbegin(); i != val.m_data.m_value.array->cend() - 1; ++i) { - dump_internal(*i, false, ensure_ascii, indent_step, current_indent); + dump_internal(*i, false, ensure_ascii, indent_step, current_indent, depth + 1); put_char(','); } // last element JSON_ASSERT(!val.m_data.m_value.array->empty()); - dump_internal(val.m_data.m_value.array->back(), false, ensure_ascii, indent_step, current_indent); + dump_internal(val.m_data.m_value.array->back(), false, ensure_ascii, indent_step, current_indent, depth + 1); put_char(']'); } @@ -381,6 +404,358 @@ class serializer } } + private: + /// the number of levels @ref dump_internal descends into before it hands + /// over to @ref dump_iteratively + static constexpr std::size_t dump_depth_limit() + { + return 128; + } + + /*! + @brief write out @a val and everything below it without the call stack + + Emits the same bytes as @ref dump_internal, keeping the containers it has + entered on an explicit stack instead of descending into them. Only reached + for values nested deeper than @ref dump_depth_limit, which is why it is not + written for speed: walking every value this way measured up to 20% slower on + object-heavy documents than letting the compiler drive the descent. + */ + void dump_iteratively(const BasicJsonType& val, + const bool pretty_print, + const bool ensure_ascii, + const std::size_t indent_step, + const std::size_t current_indent = 0) + { + // Scalars, empty containers and binary values are written by dump_value + // alone, so nothing is allocated for them: only a container with + // elements is ever pushed. + std::vector stack; + + dump_value(val, pretty_print, ensure_ascii, indent_step, current_indent, stack); + + while (!stack.empty()) + { + dump_frame& frame = stack.back(); + + if (frame.value->m_data.m_type == value_t::object) + { + const auto* object = frame.value->m_data.m_value.object; + + if (frame.object_it == object->cend()) + { + if (pretty_print) + { + put_char('\n'); + put_indent(frame.current_indent); + } + + put_char('}'); + stack.pop_back(); + continue; + } + + // the separator goes in front of every element but the first, + // which puts exactly one between each pair and none at the end + if (frame.object_it != object->cbegin()) + { + if (pretty_print) + { + put_literal(",\n"); + } + else + { + put_char(','); + } + } + + if (pretty_print) + { + put_indent(frame.child_indent); + } + + put_char('\"'); + dump_escaped(frame.object_it->first, ensure_ascii); + + if (pretty_print) + { + put_literal("\": "); + } + else + { + put_literal("\":"); + } + + const BasicJsonType& element = frame.object_it->second; + ++frame.object_it; + + // read everything needed from the frame before this: entering a + // container pushes another one and can move them all + const std::size_t element_indent = frame.child_indent; + dump_value(element, pretty_print, ensure_ascii, indent_step, element_indent, stack); + } + else + { + const auto* array = frame.value->m_data.m_value.array; + + if (frame.array_it == array->cend()) + { + if (pretty_print) + { + put_char('\n'); + put_indent(frame.current_indent); + } + + put_char(']'); + stack.pop_back(); + continue; + } + + if (frame.array_it != array->cbegin()) + { + if (pretty_print) + { + put_literal(",\n"); + } + else + { + put_char(','); + } + } + + if (pretty_print) + { + put_indent(frame.child_indent); + } + + const BasicJsonType& element = *frame.array_it; + ++frame.array_it; + + // see above + const std::size_t element_indent = frame.child_indent; + dump_value(element, pretty_print, ensure_ascii, indent_step, element_indent, stack); + } + } + } + + private: + /// @brief a container that has been opened but not closed yet + struct dump_frame + { + dump_frame(const BasicJsonType* value_, const std::size_t current_indent_, + const std::size_t child_indent_) noexcept + : value(value_) + , current_indent(current_indent_) + , child_indent(child_indent_) + {} + + /// the object or array being serialized + const BasicJsonType* value; + /// the element to serialize next; which of the two is live follows from + /// the type of @a value. They are kept side by side rather than in a + /// union, which would need its special members written out by hand, see + /// detail/iterators/internal_iterator.hpp + typename BasicJsonType::object_t::const_iterator object_it{}; + typename BasicJsonType::array_t::const_iterator array_it{}; + /// the indentation of the container itself, used by its closing bracket + std::size_t current_indent; + /// the indentation of the container's elements + std::size_t child_indent; + }; + + /*! + @brief serialize the value @a val, but not the elements of a container + + An object or array with elements is opened and pushed onto @a stack for + @ref dump_internal to walk; everything else - including a binary value, + which looks like an object but has no elements to descend into - is written + out here in full. + */ + void dump_value(const BasicJsonType& val, + const bool pretty_print, + const bool ensure_ascii, + const std::size_t indent_step, + const std::size_t current_indent, + std::vector& stack) + { + switch (val.m_data.m_type) + { + case value_t::object: + { + if (val.m_data.m_value.object->empty()) + { + put_literal("{}"); + return; + } + + std::size_t child_indent = current_indent; + + if (pretty_print) + { + put_literal("{\n"); + child_indent = next_indent(current_indent, indent_step); + } + else + { + put_char('{'); + } + + stack.emplace_back(&val, current_indent, child_indent); + stack.back().object_it = val.m_data.m_value.object->cbegin(); + return; + } + + case value_t::array: + { + if (val.m_data.m_value.array->empty()) + { + put_literal("[]"); + return; + } + + std::size_t child_indent = current_indent; + + if (pretty_print) + { + put_literal("[\n"); + child_indent = next_indent(current_indent, indent_step); + } + else + { + put_char('['); + } + + stack.emplace_back(&val, current_indent, child_indent); + stack.back().array_it = val.m_data.m_value.array->cbegin(); + return; + } + + case value_t::string: + { + put_char('\"'); + dump_escaped(*val.m_data.m_value.string, ensure_ascii); + put_char('\"'); + return; + } + + case value_t::binary: + { + if (pretty_print) + { + put_literal("{\n"); + + // variable to hold indentation for the bytes + const auto new_indent = next_indent(current_indent, indent_step); + + put_indent(new_indent); + + put_literal("\"bytes\": ["); + + if (!val.m_data.m_value.binary->empty()) + { + for (auto i = val.m_data.m_value.binary->cbegin(); + i != val.m_data.m_value.binary->cend() - 1; ++i) + { + dump_integer(*i); + put_literal(", "); + } + dump_integer(val.m_data.m_value.binary->back()); + } + + put_literal("],\n"); + put_indent(new_indent); + + put_literal("\"subtype\": "); + if (val.m_data.m_value.binary->has_subtype()) + { + dump_integer(val.m_data.m_value.binary->subtype()); + } + else + { + put_literal("null"); + } + put_char('\n'); + put_indent(current_indent); + put_char('}'); + } + else + { + put_literal("{\"bytes\":["); + + if (!val.m_data.m_value.binary->empty()) + { + for (auto i = val.m_data.m_value.binary->cbegin(); + i != val.m_data.m_value.binary->cend() - 1; ++i) + { + dump_integer(*i); + put_char(','); + } + dump_integer(val.m_data.m_value.binary->back()); + } + + put_literal("],\"subtype\":"); + if (val.m_data.m_value.binary->has_subtype()) + { + dump_integer(val.m_data.m_value.binary->subtype()); + put_char('}'); + } + else + { + put_literal("null}"); + } + } + return; + } + + case value_t::boolean: + { + if (val.m_data.m_value.boolean) + { + put_literal("true"); + } + else + { + put_literal("false"); + } + return; + } + + case value_t::number_integer: + { + dump_integer(val.m_data.m_value.number_integer); + return; + } + + case value_t::number_unsigned: + { + dump_integer(val.m_data.m_value.number_unsigned); + return; + } + + case value_t::number_float: + { + dump_float(val.m_data.m_value.number_float); + return; + } + + case value_t::discarded: + { + put_literal(""); + return; + } + + case value_t::null: + { + put_literal("null"); + return; + } + + default: // LCOV_EXCL_LINE + JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE + } + } + + /*! @brief the indentation level to use for the children of the current value diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 6c57dcfed..8901a619c 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -19827,6 +19827,7 @@ NLOHMANN_JSON_NAMESPACE_END #include // setfill, setw #include // is_same #include // move +#include // vector // #include // __ _____ _____ _____ @@ -21021,8 +21022,8 @@ class serializer This function is called by the public member function dump and organizes the serialization internally. The indentation level is propagated as - additional parameter. In case of arrays and objects, the function is - called recursively. + additional parameter. Arrays and objects are serialized without recursion, + however deeply they are nested. - strings and object keys are escaped using `escape_string()` - integer numbers are converted implicitly via `operator<<` @@ -21050,23 +21051,39 @@ class serializer JSON_PRIVATE_UNLESS_TESTED: /*! - @brief recursive worker for @ref dump + @brief worker for @ref dump Identical in behavior to the historical @ref dump, but writes into the serializer's internal @ref write_buffer instead of issuing a virtual call per token. The public @ref dump wraps this and flushes the buffer once the top-level value has been serialized. + + Serializing a container descends into its elements, so a value nested deeply + enough used to exhaust the call stack and terminate the process with no + exception to catch. The descent is bounded here: once @ref dump_depth_limit + levels have been entered, @ref dump_iteratively writes out what is left + without the call stack. A value nested less deeply than that - all but a + vanishing minority - is written by exactly the code that always wrote it. + + @sa https://github.com/nlohmann/json/issues/5387 */ void dump_internal(const BasicJsonType& val, const bool pretty_print, const bool ensure_ascii, const std::size_t indent_step, - const std::size_t current_indent = 0) + const std::size_t current_indent = 0, + const std::size_t depth = 0) { switch (val.m_data.m_type) { case value_t::object: { + if (JSON_HEDLEY_UNLIKELY(depth >= dump_depth_limit())) + { + dump_iteratively(val, pretty_print, ensure_ascii, indent_step, current_indent); + return; + } + if (val.m_data.m_value.object->empty()) { put_literal("{}"); @@ -21088,7 +21105,7 @@ class serializer put_char('\"'); dump_escaped(i->first, ensure_ascii); put_literal("\": "); - dump_internal(i->second, true, ensure_ascii, indent_step, new_indent); + dump_internal(i->second, true, ensure_ascii, indent_step, new_indent, depth + 1); put_literal(",\n"); } @@ -21099,7 +21116,7 @@ class serializer put_char('\"'); dump_escaped(i->first, ensure_ascii); put_literal("\": "); - dump_internal(i->second, true, ensure_ascii, indent_step, new_indent); + dump_internal(i->second, true, ensure_ascii, indent_step, new_indent, depth + 1); put_char('\n'); put_indent(current_indent); @@ -21116,7 +21133,7 @@ class serializer put_char('\"'); dump_escaped(i->first, ensure_ascii); put_literal("\":"); - dump_internal(i->second, false, ensure_ascii, indent_step, current_indent); + dump_internal(i->second, false, ensure_ascii, indent_step, current_indent, depth + 1); put_char(','); } @@ -21126,7 +21143,7 @@ class serializer put_char('\"'); dump_escaped(i->first, ensure_ascii); put_literal("\":"); - dump_internal(i->second, false, ensure_ascii, indent_step, current_indent); + dump_internal(i->second, false, ensure_ascii, indent_step, current_indent, depth + 1); put_char('}'); } @@ -21136,6 +21153,12 @@ class serializer case value_t::array: { + if (JSON_HEDLEY_UNLIKELY(depth >= dump_depth_limit())) + { + dump_iteratively(val, pretty_print, ensure_ascii, indent_step, current_indent); + return; + } + if (val.m_data.m_value.array->empty()) { put_literal("[]"); @@ -21154,14 +21177,14 @@ class serializer i != val.m_data.m_value.array->cend() - 1; ++i) { put_indent(new_indent); - dump_internal(*i, true, ensure_ascii, indent_step, new_indent); + dump_internal(*i, true, ensure_ascii, indent_step, new_indent, depth + 1); put_literal(",\n"); } // last element JSON_ASSERT(!val.m_data.m_value.array->empty()); put_indent(new_indent); - dump_internal(val.m_data.m_value.array->back(), true, ensure_ascii, indent_step, new_indent); + dump_internal(val.m_data.m_value.array->back(), true, ensure_ascii, indent_step, new_indent, depth + 1); put_char('\n'); put_indent(current_indent); @@ -21175,13 +21198,13 @@ class serializer for (auto i = val.m_data.m_value.array->cbegin(); i != val.m_data.m_value.array->cend() - 1; ++i) { - dump_internal(*i, false, ensure_ascii, indent_step, current_indent); + dump_internal(*i, false, ensure_ascii, indent_step, current_indent, depth + 1); put_char(','); } // last element JSON_ASSERT(!val.m_data.m_value.array->empty()); - dump_internal(val.m_data.m_value.array->back(), false, ensure_ascii, indent_step, current_indent); + dump_internal(val.m_data.m_value.array->back(), false, ensure_ascii, indent_step, current_indent, depth + 1); put_char(']'); } @@ -21314,6 +21337,358 @@ class serializer } } + private: + /// the number of levels @ref dump_internal descends into before it hands + /// over to @ref dump_iteratively + static constexpr std::size_t dump_depth_limit() + { + return 128; + } + + /*! + @brief write out @a val and everything below it without the call stack + + Emits the same bytes as @ref dump_internal, keeping the containers it has + entered on an explicit stack instead of descending into them. Only reached + for values nested deeper than @ref dump_depth_limit, which is why it is not + written for speed: walking every value this way measured up to 20% slower on + object-heavy documents than letting the compiler drive the descent. + */ + void dump_iteratively(const BasicJsonType& val, + const bool pretty_print, + const bool ensure_ascii, + const std::size_t indent_step, + const std::size_t current_indent = 0) + { + // Scalars, empty containers and binary values are written by dump_value + // alone, so nothing is allocated for them: only a container with + // elements is ever pushed. + std::vector stack; + + dump_value(val, pretty_print, ensure_ascii, indent_step, current_indent, stack); + + while (!stack.empty()) + { + dump_frame& frame = stack.back(); + + if (frame.value->m_data.m_type == value_t::object) + { + const auto* object = frame.value->m_data.m_value.object; + + if (frame.object_it == object->cend()) + { + if (pretty_print) + { + put_char('\n'); + put_indent(frame.current_indent); + } + + put_char('}'); + stack.pop_back(); + continue; + } + + // the separator goes in front of every element but the first, + // which puts exactly one between each pair and none at the end + if (frame.object_it != object->cbegin()) + { + if (pretty_print) + { + put_literal(",\n"); + } + else + { + put_char(','); + } + } + + if (pretty_print) + { + put_indent(frame.child_indent); + } + + put_char('\"'); + dump_escaped(frame.object_it->first, ensure_ascii); + + if (pretty_print) + { + put_literal("\": "); + } + else + { + put_literal("\":"); + } + + const BasicJsonType& element = frame.object_it->second; + ++frame.object_it; + + // read everything needed from the frame before this: entering a + // container pushes another one and can move them all + const std::size_t element_indent = frame.child_indent; + dump_value(element, pretty_print, ensure_ascii, indent_step, element_indent, stack); + } + else + { + const auto* array = frame.value->m_data.m_value.array; + + if (frame.array_it == array->cend()) + { + if (pretty_print) + { + put_char('\n'); + put_indent(frame.current_indent); + } + + put_char(']'); + stack.pop_back(); + continue; + } + + if (frame.array_it != array->cbegin()) + { + if (pretty_print) + { + put_literal(",\n"); + } + else + { + put_char(','); + } + } + + if (pretty_print) + { + put_indent(frame.child_indent); + } + + const BasicJsonType& element = *frame.array_it; + ++frame.array_it; + + // see above + const std::size_t element_indent = frame.child_indent; + dump_value(element, pretty_print, ensure_ascii, indent_step, element_indent, stack); + } + } + } + + private: + /// @brief a container that has been opened but not closed yet + struct dump_frame + { + dump_frame(const BasicJsonType* value_, const std::size_t current_indent_, + const std::size_t child_indent_) noexcept + : value(value_) + , current_indent(current_indent_) + , child_indent(child_indent_) + {} + + /// the object or array being serialized + const BasicJsonType* value; + /// the element to serialize next; which of the two is live follows from + /// the type of @a value. They are kept side by side rather than in a + /// union, which would need its special members written out by hand, see + /// detail/iterators/internal_iterator.hpp + typename BasicJsonType::object_t::const_iterator object_it{}; + typename BasicJsonType::array_t::const_iterator array_it{}; + /// the indentation of the container itself, used by its closing bracket + std::size_t current_indent; + /// the indentation of the container's elements + std::size_t child_indent; + }; + + /*! + @brief serialize the value @a val, but not the elements of a container + + An object or array with elements is opened and pushed onto @a stack for + @ref dump_internal to walk; everything else - including a binary value, + which looks like an object but has no elements to descend into - is written + out here in full. + */ + void dump_value(const BasicJsonType& val, + const bool pretty_print, + const bool ensure_ascii, + const std::size_t indent_step, + const std::size_t current_indent, + std::vector& stack) + { + switch (val.m_data.m_type) + { + case value_t::object: + { + if (val.m_data.m_value.object->empty()) + { + put_literal("{}"); + return; + } + + std::size_t child_indent = current_indent; + + if (pretty_print) + { + put_literal("{\n"); + child_indent = next_indent(current_indent, indent_step); + } + else + { + put_char('{'); + } + + stack.emplace_back(&val, current_indent, child_indent); + stack.back().object_it = val.m_data.m_value.object->cbegin(); + return; + } + + case value_t::array: + { + if (val.m_data.m_value.array->empty()) + { + put_literal("[]"); + return; + } + + std::size_t child_indent = current_indent; + + if (pretty_print) + { + put_literal("[\n"); + child_indent = next_indent(current_indent, indent_step); + } + else + { + put_char('['); + } + + stack.emplace_back(&val, current_indent, child_indent); + stack.back().array_it = val.m_data.m_value.array->cbegin(); + return; + } + + case value_t::string: + { + put_char('\"'); + dump_escaped(*val.m_data.m_value.string, ensure_ascii); + put_char('\"'); + return; + } + + case value_t::binary: + { + if (pretty_print) + { + put_literal("{\n"); + + // variable to hold indentation for the bytes + const auto new_indent = next_indent(current_indent, indent_step); + + put_indent(new_indent); + + put_literal("\"bytes\": ["); + + if (!val.m_data.m_value.binary->empty()) + { + for (auto i = val.m_data.m_value.binary->cbegin(); + i != val.m_data.m_value.binary->cend() - 1; ++i) + { + dump_integer(*i); + put_literal(", "); + } + dump_integer(val.m_data.m_value.binary->back()); + } + + put_literal("],\n"); + put_indent(new_indent); + + put_literal("\"subtype\": "); + if (val.m_data.m_value.binary->has_subtype()) + { + dump_integer(val.m_data.m_value.binary->subtype()); + } + else + { + put_literal("null"); + } + put_char('\n'); + put_indent(current_indent); + put_char('}'); + } + else + { + put_literal("{\"bytes\":["); + + if (!val.m_data.m_value.binary->empty()) + { + for (auto i = val.m_data.m_value.binary->cbegin(); + i != val.m_data.m_value.binary->cend() - 1; ++i) + { + dump_integer(*i); + put_char(','); + } + dump_integer(val.m_data.m_value.binary->back()); + } + + put_literal("],\"subtype\":"); + if (val.m_data.m_value.binary->has_subtype()) + { + dump_integer(val.m_data.m_value.binary->subtype()); + put_char('}'); + } + else + { + put_literal("null}"); + } + } + return; + } + + case value_t::boolean: + { + if (val.m_data.m_value.boolean) + { + put_literal("true"); + } + else + { + put_literal("false"); + } + return; + } + + case value_t::number_integer: + { + dump_integer(val.m_data.m_value.number_integer); + return; + } + + case value_t::number_unsigned: + { + dump_integer(val.m_data.m_value.number_unsigned); + return; + } + + case value_t::number_float: + { + dump_float(val.m_data.m_value.number_float); + return; + } + + case value_t::discarded: + { + put_literal(""); + return; + } + + case value_t::null: + { + put_literal("null"); + return; + } + + default: // LCOV_EXCL_LINE + JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE + } + } + + /*! @brief the indentation level to use for the children of the current value diff --git a/tests/src/unit-serialization.cpp b/tests/src/unit-serialization.cpp index c565dcab9..4fcaee6bd 100644 --- a/tests/src/unit-serialization.cpp +++ b/tests/src/unit-serialization.cpp @@ -524,3 +524,90 @@ TEST_CASE("indentation is written straight into the write buffer") CHECK(j.dump(0) == "{\n\"a\": [\n1,\n2\n],\n\"b\": null\n}"); } } + +TEST_CASE("serialization of deeply nested values") +{ + // dump() descends into a bounded number of levels and writes out whatever + // is nested deeper than that without the call stack; see + // https://github.com/nlohmann/json/issues/5387 + + SECTION("nested deeper than the call stack could follow") + { + // parsing is iterative, so building these costs little + const std::size_t depth = 100000; + + const std::string array_text = std::string(depth, '[') + '0' + std::string(depth, ']'); + CHECK(json::parse(array_text).dump() == array_text); + + std::string object_text; + object_text.reserve(6 * depth + 1); + for (std::size_t i = 0; i < depth; ++i) + { + object_text += "{\"a\":"; + } + object_text += '1'; + object_text.append(depth, '}'); + CHECK(json::parse(object_text).dump() == object_text); + } + + SECTION("depths around the bound of the descent") + { + // Cover every depth around the bound, so that the two ways of writing a + // value are known to meet cleanly - wherever the bound is set. + for (std::size_t d = 1; d <= 300; ++d) + { + CAPTURE(d); + + const std::string array_text = std::string(d, '[') + '7' + std::string(d, ']'); + CHECK(json::parse(array_text).dump() == array_text); + + std::string object_text; + for (std::size_t i = 0; i < d; ++i) + { + object_text += "{\"k\":"; + } + object_text += '7'; + object_text.append(d, '}'); + CHECK(json::parse(object_text).dump() == object_text); + } + } + + SECTION("pretty-printing across the bound") + { + for (std::size_t d = 120; d <= 140; ++d) + { + CAPTURE(d); + + const json j = json::parse(std::string(d, '[') + '7' + std::string(d, ']')); + + std::string expected; + for (std::size_t i = 0; i < d; ++i) + { + expected += std::string(2 * i, ' ') + "[\n"; + } + expected += std::string(2 * d, ' ') + '7'; + for (std::size_t i = d; i > 0; --i) + { + expected += '\n' + std::string(2 * (i - 1), ' ') + ']'; + } + + CHECK(j.dump(2) == expected); + } + } + + SECTION("an empty container below the bound") + { + // an empty container is written out in full and never descended into, + // so it must not gain a newline when it is reached iteratively + for (std::size_t d = 125; d <= 135; ++d) + { + CAPTURE(d); + + const std::string compact = std::string(d, '[') + "[]" + std::string(d, ']'); + CHECK(json::parse(compact).dump() == compact); + + const std::string with_object = std::string(d, '[') + "{}" + std::string(d, ']'); + CHECK(json::parse(with_object).dump() == with_object); + } + } +}