mirror of
https://github.com/nlohmann/json.git
synced 2026-08-21 16:43:17 +00:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user