Bound the descent of dump()

Serializing a container serializes its elements, so dump() descended into one
call per nesting level. A value nested deeply enough exhausted the call stack
and terminated the process with a segmentation fault - no exception, nothing
the caller could catch. Parsing such a value works, as the parser is
iterative, and so does destroying one, as #1436 made destruction iterative.

Bound how far the descent goes rather than take the call stack away from it.
The first 128 levels are written by exactly the code that always wrote them,
and only below that does dump_iteratively write out what is left, keeping the
containers it has entered on an explicit stack. Serializing can therefore no
longer exhaust the stack, however deeply a value is nested, while a value
nested less deeply than the bound pays only for one comparison per container.

Writing every value that way instead measured between 2% and 20% slower - 20%
on object-heavy documents - which is why the descent is kept for all but the
values that cannot afford it. The bound costs nothing measurable: between
-1.4% and +1.2% across compact and pretty output of number, integer, string,
object-heavy, wide-object and deeply nested documents.

The output is unchanged for every value. Both ways of writing a container
emit the separator in front of every element but the first, rather than
after every element but the last, which puts exactly one between each pair
and none at the end.

This fixes #5387 for dump(). The copy constructor is fixed in #5389.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann
2026-08-21 04:51:22 +02:00
parent cb8be7f76c
commit 5bf19dc28d
3 changed files with 861 additions and 24 deletions
+387 -12
View File
@@ -22,6 +22,7 @@
#include <iomanip> // setfill, setw
#include <type_traits> // is_same
#include <utility> // move
#include <vector> // vector
#include <nlohmann/detail/conversions/to_chars.hpp>
#include <nlohmann/detail/exceptions.hpp>
@@ -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<dump_frame> 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<dump_frame>& 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("<discarded>");
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
+387 -12
View File
@@ -19827,6 +19827,7 @@ NLOHMANN_JSON_NAMESPACE_END
#include <iomanip> // setfill, setw
#include <type_traits> // is_same
#include <utility> // move
#include <vector> // vector
// #include <nlohmann/detail/conversions/to_chars.hpp>
// __ _____ _____ _____
@@ -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<dump_frame> 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<dump_frame>& 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("<discarded>");
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
+87
View File
@@ -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);
}
}
}