mirror of
https://github.com/nlohmann/json.git
synced 2026-10-15 18:57:13 +00:00
* Escape characters below 0x60 from a table straight into the write buffer When the bulk scanner in dump_escaped_impl() stops at a control character, a quote or a backslash, that byte used to go through the UTF-8 decoder and a switch that wrote the escape byte by byte into string_buffer, which was then copied into write_buffer at the next run. A byte below 0x60 at a character boundary is a complete code point, so it now skips the decoder: its escape sequence comes from a 96-entry table of 8-byte rows (sequence padded with zeros, length in the last byte), and all 8 bytes are copied straight into write_buffer with one room check. The check sits inside the existing character-boundary block, so it runs once per scanner stop and not once per byte; testing every byte cost about 6% on twitter.json with ensure_ascii. The switch in the decoder's accept branch lost its cases for characters below 0x60, which can no longer reach it. Output is byte-identical. The string-buffer boundary tests now use \u-escaped non-ASCII characters, since simple escapes no longer reach string_buffer, and a new test case checks every ASCII character at every position against a reference escaper. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Avoid raw string literals with backslashes in CHECK macros MSVC's traditional preprocessor mis-lexes raw string literals inside macro arguments and reports illegal escape sequences (C2017, C3850). Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Sweep invalid UTF-8 at every position through dump() Compare dump() of strings with ill-formed UTF-8 (stray continuation bytes, invalid lead bytes, overlong encodings, surrogates, code points above U+10FFFF, and sequences cut short by ASCII, a quote, a backslash, a lead byte, or the end of the string) with an independent reference serializer, for every error handler and with ensure_ascii on and off. The pattern is put at every position of short strings to cross the strides of the bulk scanners, and of longer ones to cross the end of the 512-byte string buffer. Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me>
1739 lines
67 KiB
C++
1739 lines
67 KiB
C++
// __ _____ _____ _____
|
|
// __| | __| | | | JSON for Modern C++
|
|
// | | |__ | | | | | | version 3.12.0
|
|
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
|
//
|
|
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#pragma once
|
|
|
|
#include <algorithm> // remove, fill, find, none_of, min
|
|
#include <array> // array
|
|
#include <clocale> // localeconv, lconv
|
|
#include <cmath> // isfinite
|
|
#include <cstddef> // size_t, ptrdiff_t
|
|
#include <cstdint> // uint8_t
|
|
#include <cstdio> // snprintf
|
|
#include <cstring> // memcpy, memset
|
|
#include <iterator> // next
|
|
#include <limits> // numeric_limits
|
|
#include <string> // string, char_traits
|
|
#include <type_traits> // is_same
|
|
#include <vector> // vector
|
|
|
|
#include <nlohmann/detail/conversions/to_chars.hpp>
|
|
#include <nlohmann/detail/exceptions.hpp>
|
|
#include <nlohmann/detail/input/string_scan.hpp>
|
|
#include <nlohmann/detail/macro_scope.hpp>
|
|
#include <nlohmann/detail/meta/cpp_future.hpp>
|
|
#include <nlohmann/detail/output/error_handler.hpp>
|
|
#include <nlohmann/detail/output/output_adapters.hpp>
|
|
#include <nlohmann/detail/recursion_depth_limit.hpp>
|
|
#include <nlohmann/detail/string_concat.hpp>
|
|
#include <nlohmann/detail/string_utils.hpp>
|
|
#include <nlohmann/detail/value_t.hpp>
|
|
|
|
NLOHMANN_JSON_NAMESPACE_BEGIN
|
|
namespace detail
|
|
{
|
|
|
|
///////////////////
|
|
// serialization //
|
|
///////////////////
|
|
|
|
template<typename BasicJsonType>
|
|
class serializer
|
|
{
|
|
using string_t = typename BasicJsonType::string_t;
|
|
using number_float_t = typename BasicJsonType::number_float_t;
|
|
using number_integer_t = typename BasicJsonType::number_integer_t;
|
|
using number_unsigned_t = typename BasicJsonType::number_unsigned_t;
|
|
/// type of the absolute value of an integer: number_unsigned_t, or its
|
|
/// unsigned counterpart if it is signed (it may be number_integer_t, #2573)
|
|
using number_magnitude_t = typename std::conditional <
|
|
std::is_integral<number_unsigned_t>::value && std::is_signed<number_unsigned_t>::value,
|
|
std::make_unsigned<number_unsigned_t>,
|
|
std::enable_if<true, number_unsigned_t >>::type::type;
|
|
using binary_char_t = typename BasicJsonType::binary_t::value_type;
|
|
|
|
public:
|
|
/*!
|
|
@param[in] s output adapter to serialize to; not owned by the serializer,
|
|
so it must outlive it (it lives at the call site)
|
|
@param[in] ichar indentation character to use
|
|
@param[in] pretty_print_ whether the output shall be pretty-printed
|
|
@param[in] ensure_ascii_ If @a ensure_ascii_ is true, all non-ASCII
|
|
characters in the output are escaped with `\\uXXXX` sequences, and the
|
|
result consists of ASCII characters only.
|
|
@param[in] indent_step_ the indent level
|
|
@param[in] error_handler_ how to react on decoding errors
|
|
|
|
None of @a pretty_print_, @a ensure_ascii_ and @a indent_step_ change over
|
|
the life of the serializer, so they are captured once here instead of
|
|
being threaded through every call to @ref dump, @ref dump_internal and
|
|
@ref dump_iteratively.
|
|
*/
|
|
serializer(output_adapter_protocol<char>& s, const char ichar,
|
|
const bool pretty_print_ = false,
|
|
const bool ensure_ascii_ = false,
|
|
const std::size_t indent_step_ = 0,
|
|
error_handler_t error_handler_ = error_handler_t::strict)
|
|
: o(&s)
|
|
, indent_char(ichar)
|
|
, pretty_print(pretty_print_)
|
|
, ensure_ascii(ensure_ascii_)
|
|
, indent_step(indent_step_)
|
|
, error_handler(error_handler_)
|
|
{}
|
|
|
|
// deleted because of pointer members
|
|
serializer(const serializer&) = delete;
|
|
serializer& operator=(const serializer&) = delete;
|
|
serializer(serializer&&) = delete;
|
|
serializer& operator=(serializer&&) = delete;
|
|
~serializer() = default;
|
|
|
|
/*!
|
|
@brief internal implementation of the serialization function
|
|
|
|
This function is called by the public member function dump and organizes
|
|
the serialization internally. The indentation level is propagated as
|
|
additional parameter. Arrays and objects are serialized without recursion,
|
|
however deeply they are nested.
|
|
|
|
- strings and object keys are escaped using @ref dump_escaped
|
|
- integer numbers are converted using a digit-pair lookup table (@ref dump_integer)
|
|
- floating-point numbers are converted to a string using @ref dump_float, which
|
|
uses `to_chars` for IEEE-754 types and `snprintf` otherwise
|
|
- binary values are serialized as objects containing the subtype and the
|
|
byte array
|
|
|
|
@param[in] val value to serialize
|
|
@param[in] current_indent the current indent level (only used internally)
|
|
*/
|
|
void dump(const BasicJsonType& val,
|
|
const std::size_t current_indent = 0)
|
|
{
|
|
dump_internal(val, current_indent);
|
|
flush();
|
|
}
|
|
|
|
JSON_PRIVATE_UNLESS_TESTED:
|
|
/*!
|
|
@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 recursion_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 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 >= recursion_depth_limit()))
|
|
{
|
|
dump_iteratively(val, current_indent);
|
|
return;
|
|
}
|
|
|
|
if (val.m_data.m_value.object->empty())
|
|
{
|
|
put_literal("{}");
|
|
return;
|
|
}
|
|
|
|
if (pretty_print)
|
|
{
|
|
put_literal("{\n");
|
|
|
|
// variable to hold indentation for recursive calls
|
|
const auto new_indent = next_indent(current_indent, indent_step);
|
|
|
|
// first n-1 elements
|
|
auto i = val.m_data.m_value.object->cbegin();
|
|
for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i)
|
|
{
|
|
put_indent(new_indent);
|
|
put_char('"');
|
|
dump_escaped(i->first);
|
|
put_literal("\": ");
|
|
dump_internal(i->second, new_indent, depth + 1);
|
|
put_literal(",\n");
|
|
}
|
|
|
|
// last element
|
|
JSON_ASSERT(i != val.m_data.m_value.object->cend());
|
|
JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend());
|
|
put_indent(new_indent);
|
|
put_char('"');
|
|
dump_escaped(i->first);
|
|
put_literal("\": ");
|
|
dump_internal(i->second, new_indent, depth + 1);
|
|
|
|
put_char('\n');
|
|
put_indent(current_indent);
|
|
put_char('}');
|
|
}
|
|
else
|
|
{
|
|
put_char('{');
|
|
|
|
// first n-1 elements
|
|
auto i = val.m_data.m_value.object->cbegin();
|
|
for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i)
|
|
{
|
|
put_char('"');
|
|
dump_escaped(i->first);
|
|
put_literal("\":");
|
|
dump_internal(i->second, current_indent, depth + 1);
|
|
put_char(',');
|
|
}
|
|
|
|
// last element
|
|
JSON_ASSERT(i != val.m_data.m_value.object->cend());
|
|
JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend());
|
|
put_char('"');
|
|
dump_escaped(i->first);
|
|
put_literal("\":");
|
|
dump_internal(i->second, current_indent, depth + 1);
|
|
|
|
put_char('}');
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
case value_t::array:
|
|
{
|
|
if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit()))
|
|
{
|
|
dump_iteratively(val, current_indent);
|
|
return;
|
|
}
|
|
|
|
if (val.m_data.m_value.array->empty())
|
|
{
|
|
put_literal("[]");
|
|
return;
|
|
}
|
|
|
|
if (pretty_print)
|
|
{
|
|
put_literal("[\n");
|
|
|
|
// variable to hold indentation for recursive calls
|
|
const auto new_indent = next_indent(current_indent, indent_step);
|
|
|
|
// first n-1 elements
|
|
for (auto i = val.m_data.m_value.array->cbegin();
|
|
i != val.m_data.m_value.array->cend() - 1; ++i)
|
|
{
|
|
put_indent(new_indent);
|
|
dump_internal(*i, 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(), new_indent, depth + 1);
|
|
|
|
put_char('\n');
|
|
put_indent(current_indent);
|
|
put_char(']');
|
|
}
|
|
else
|
|
{
|
|
put_char('[');
|
|
|
|
// first n-1 elements
|
|
for (auto i = val.m_data.m_value.array->cbegin();
|
|
i != val.m_data.m_value.array->cend() - 1; ++i)
|
|
{
|
|
dump_internal(*i, 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(), current_indent, depth + 1);
|
|
|
|
put_char(']');
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
case value_t::string:
|
|
case value_t::binary:
|
|
case value_t::boolean:
|
|
case value_t::number_integer:
|
|
case value_t::number_unsigned:
|
|
case value_t::number_float:
|
|
case value_t::discarded:
|
|
case value_t::null:
|
|
default:
|
|
dump_scalar(val, current_indent);
|
|
return;
|
|
}
|
|
}
|
|
|
|
private:
|
|
/*!
|
|
@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 recursion_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 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, 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);
|
|
|
|
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, 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, 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_iteratively to walk; everything else - including a binary value,
|
|
which looks like an object but has no elements to descend into - is written
|
|
out in full by @ref dump_scalar.
|
|
*/
|
|
void dump_value(const BasicJsonType& val,
|
|
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:
|
|
case value_t::binary:
|
|
case value_t::boolean:
|
|
case value_t::number_integer:
|
|
case value_t::number_unsigned:
|
|
case value_t::number_float:
|
|
case value_t::discarded:
|
|
case value_t::null:
|
|
default:
|
|
dump_scalar(val, current_indent);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/*!
|
|
@brief serialize the value @a val, which is neither an object nor an array
|
|
|
|
Shared by @ref dump_internal and @ref dump_value, so that a value is written
|
|
the same way however deeply it is nested. A binary value is written out here
|
|
in full: it looks like an object, but has no elements to descend into.
|
|
|
|
@param[in] val value to serialize; not an object or array
|
|
@param[in] current_indent the indentation of @a val, used for a
|
|
pretty-printed binary value
|
|
*/
|
|
void dump_scalar(const BasicJsonType& val, const std::size_t current_indent)
|
|
{
|
|
switch (val.m_data.m_type)
|
|
{
|
|
case value_t::string:
|
|
{
|
|
put_char('"');
|
|
dump_escaped(*val.m_data.m_value.string);
|
|
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_byte(*i);
|
|
put_literal(", ");
|
|
}
|
|
dump_byte(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_byte(*i);
|
|
put_char(',');
|
|
}
|
|
dump_byte(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;
|
|
}
|
|
|
|
case value_t::object: // LCOV_EXCL_LINE
|
|
case value_t::array: // LCOV_EXCL_LINE
|
|
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
|
|
|
|
A very large @a indent_step can wrap the unsigned accumulation on deep
|
|
nesting, which would silently truncate the indentation. Far harder to reach
|
|
now that the accumulator is a std::size_t, but still reachable where that is
|
|
32 bits wide.
|
|
*/
|
|
static std::size_t next_indent(const std::size_t current_indent, const std::size_t indent_step)
|
|
{
|
|
const std::size_t new_indent = current_indent + indent_step;
|
|
JSON_ASSERT(new_indent >= current_indent);
|
|
return new_indent;
|
|
}
|
|
|
|
JSON_PRIVATE_UNLESS_TESTED:
|
|
/*!
|
|
@brief dump escaped string
|
|
|
|
Escape a string by replacing certain special characters by a sequence of an
|
|
escape character (backslash) and another character and other control
|
|
characters by a sequence of "\u" followed by a four-digit hex
|
|
representation. The escaped string is appended to @ref write_buffer.
|
|
|
|
@param[in] s the string to escape
|
|
|
|
Complexity: Linear in the length of string @a s.
|
|
*/
|
|
void dump_escaped(const string_t& s)
|
|
{
|
|
// dispatch once here rather than test the flag inside the loop: it does
|
|
// not change while a string is written, and folding it lets each of the
|
|
// two scanners be inlined into a loop of its own
|
|
if (ensure_ascii)
|
|
{
|
|
dump_escaped_impl<true>(s);
|
|
}
|
|
else
|
|
{
|
|
dump_escaped_impl<false>(s);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
@brief worker for @ref dump_escaped
|
|
|
|
@a ensure_ascii is a template parameter here so that the branch on it is
|
|
resolved once, outside the loop; see @ref dump_escaped.
|
|
*/
|
|
#ifdef JSON_HEDLEY_MSVC_VERSION
|
|
#pragma warning(push)
|
|
// EnsureAscii is a template parameter; C++11 has no if constexpr
|
|
#pragma warning(disable : 4127) // conditional expression is constant
|
|
#endif
|
|
template<bool EnsureAscii>
|
|
void dump_escaped_impl(const string_t& s)
|
|
{
|
|
std::uint32_t codepoint{};
|
|
std::uint8_t state = UTF8_ACCEPT;
|
|
std::size_t bytes = 0; // number of bytes written to string_buffer
|
|
|
|
// number of bytes written at the point of the last valid byte
|
|
std::size_t bytes_after_last_accept = 0;
|
|
std::size_t undumped_chars = 0;
|
|
|
|
for (std::size_t i = 0; i < s.size(); ++i)
|
|
{
|
|
// Fast path: at a character boundary (state == UTF8_ACCEPT),
|
|
// bulk-copy the longest run of bytes that need no escaping using a
|
|
// SWAR scanner shared with the lexer's contiguous path. The scanner
|
|
// stops exactly at the first byte dump_escaped would handle
|
|
// individually, so that byte is left to the byte-at-a-time path
|
|
// below, keeping escaping output and error diagnostics unchanged.
|
|
//
|
|
// - EnsureAscii == false: string_bulk_run() copies ordinary bytes
|
|
// and complete well-formed UTF-8, stopping at a quote, backslash,
|
|
// control character (< 0x20), or ill-formed/truncated sequence.
|
|
// - EnsureAscii == true: only printable ASCII may be copied
|
|
// verbatim; find_ascii_copyable_run() additionally stops at 0x7F
|
|
// and every non-ASCII byte (>= 0x80), which must be \u-escaped.
|
|
if (state == UTF8_ACCEPT)
|
|
{
|
|
const auto* const data = reinterpret_cast<const unsigned char*>(s.data());
|
|
// A run can only be non-empty when the very first byte is one
|
|
// the scanner may copy, so test that single byte before paying
|
|
// for the scan. Without it, text whose characters all have to be
|
|
// escaped - CJK under ensure_ascii, where every byte is >= 0x80 -
|
|
// runs the scanner once per character only to be told zero.
|
|
std::size_t run = 0;
|
|
if (!EnsureAscii)
|
|
{
|
|
run = string_bulk_run(data + i, s.size() - i);
|
|
}
|
|
else if (is_ascii_copyable(data[i]))
|
|
{
|
|
run = find_ascii_copyable_run(data + i, s.size() - i);
|
|
}
|
|
if (run != 0)
|
|
{
|
|
// emit any bytes still pending in string_buffer first to
|
|
// preserve output order, then write the run directly
|
|
if (bytes != 0)
|
|
{
|
|
put_buffer(string_buffer, bytes);
|
|
bytes = 0;
|
|
}
|
|
put_string(s, i, i + run);
|
|
bytes_after_last_accept = 0;
|
|
undumped_chars = 0;
|
|
i += run;
|
|
if (i >= s.size())
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// A byte below 0x60 is a complete code point and needs no
|
|
// UTF-8 decoding. The scanner stops only at a control
|
|
// character, a quote, or a backslash in that range, so look up
|
|
// its escape sequence and write all 8 bytes of the table entry
|
|
// straight into the write buffer; only the length stored in the
|
|
// last byte of the entry is kept.
|
|
if (data[i] < 0x60)
|
|
{
|
|
// emit any bytes still pending in string_buffer first to
|
|
// preserve output order
|
|
if (bytes != 0)
|
|
{
|
|
put_buffer(string_buffer, bytes);
|
|
bytes = 0;
|
|
}
|
|
const auto& escape = ascii_escapes()[data[i]];
|
|
if (JSON_HEDLEY_UNLIKELY(write_buffer.size() - write_buffer_pos < escape.size()))
|
|
{
|
|
flush();
|
|
}
|
|
std::memcpy(write_buffer.data() + write_buffer_pos, escape.data(), escape.size());
|
|
write_buffer_pos += static_cast<std::size_t>(escape[7]);
|
|
bytes_after_last_accept = 0;
|
|
undumped_chars = 0;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const auto byte = static_cast<std::uint8_t>(s[i]);
|
|
|
|
switch (decode(state, codepoint, byte))
|
|
{
|
|
case UTF8_ACCEPT: // decode found a new code point
|
|
{
|
|
// the code point is at least 0x60 (smaller ones are handled
|
|
// above) and is not printable ASCII (which the scanner
|
|
// copies): it is DEL (0x7F) under EnsureAscii or a non-ASCII
|
|
// character
|
|
if (EnsureAscii)
|
|
{
|
|
JSON_ASSERT(codepoint >= 0x7F);
|
|
if (error_handler == error_handler_t::keep)
|
|
{
|
|
// this character was buffered as raw bytes below
|
|
// in case it turned out to be part of an
|
|
// ill-formed sequence (which is kept as is); now
|
|
// that it decoded to a well-formed code point,
|
|
// undo that and \u-escape it like any other
|
|
// character instead
|
|
bytes = bytes_after_last_accept;
|
|
}
|
|
if (codepoint <= 0xFFFF)
|
|
{
|
|
write_u_escape(bytes, static_cast<std::uint16_t>(codepoint));
|
|
}
|
|
else
|
|
{
|
|
write_u_escape(bytes, static_cast<std::uint16_t>(0xD7C0u + (codepoint >> 10u)));
|
|
write_u_escape(bytes, static_cast<std::uint16_t>(0xDC00u + (codepoint & 0x3FFu)));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// copy byte to buffer (all previous bytes of the
|
|
// sequence have been copied in the default case below)
|
|
string_buffer[bytes++] = s[i];
|
|
}
|
|
|
|
// write buffer and reset index; there must be 13 bytes
|
|
// left, as this is the maximal number of bytes to be
|
|
// written ("\uxxxx\uxxxx\0") for one code point
|
|
if (string_buffer.size() - bytes < 13)
|
|
{
|
|
put_buffer(string_buffer, bytes);
|
|
bytes = 0;
|
|
}
|
|
|
|
// remember the byte position of this accept
|
|
bytes_after_last_accept = bytes;
|
|
undumped_chars = 0;
|
|
break;
|
|
}
|
|
|
|
case UTF8_REJECT: // decode found invalid UTF-8 byte
|
|
{
|
|
switch (error_handler)
|
|
{
|
|
case error_handler_t::strict:
|
|
{
|
|
JSON_THROW(type_error::create(exception_id::invalid_utf8, concat("invalid UTF-8 byte at index ", std::to_string(i), ": 0x", detail::hex_byte(byte)), nullptr));
|
|
}
|
|
|
|
case error_handler_t::ignore:
|
|
case error_handler_t::replace:
|
|
{
|
|
// in case we saw this character the first time, we
|
|
// would like to read it again, because the byte
|
|
// may be OK for itself, but just not OK for the
|
|
// previous sequence
|
|
if (undumped_chars > 0)
|
|
{
|
|
--i;
|
|
}
|
|
|
|
// reset length buffer to the last accepted index;
|
|
// thus removing/ignoring the invalid characters
|
|
bytes = bytes_after_last_accept;
|
|
|
|
if (error_handler == error_handler_t::replace)
|
|
{
|
|
// add a replacement character
|
|
if (EnsureAscii)
|
|
{
|
|
string_buffer[bytes++] = '\\';
|
|
string_buffer[bytes++] = 'u';
|
|
string_buffer[bytes++] = 'f';
|
|
string_buffer[bytes++] = 'f';
|
|
string_buffer[bytes++] = 'f';
|
|
string_buffer[bytes++] = 'd';
|
|
}
|
|
else
|
|
{
|
|
string_buffer[bytes++] = '\xEF';
|
|
string_buffer[bytes++] = '\xBF';
|
|
string_buffer[bytes++] = '\xBD';
|
|
}
|
|
|
|
// write buffer and reset index; there must be 13 bytes
|
|
// left, as this is the maximal number of bytes to be
|
|
// written ("\uxxxx\uxxxx\0") for one code point
|
|
if (string_buffer.size() - bytes < 13)
|
|
{
|
|
put_buffer(string_buffer, bytes);
|
|
bytes = 0;
|
|
}
|
|
|
|
bytes_after_last_accept = bytes;
|
|
}
|
|
|
|
undumped_chars = 0;
|
|
|
|
// continue processing the string
|
|
state = UTF8_ACCEPT;
|
|
break;
|
|
}
|
|
|
|
case error_handler_t::keep:
|
|
{
|
|
// the bytes of this (now abandoned) ill-formed
|
|
// sequence seen so far are already buffered below
|
|
// and are kept unchanged in the output
|
|
if (undumped_chars > 0)
|
|
{
|
|
// the byte that ended the sequence may be OK
|
|
// for itself (e.g., a quote that must still be
|
|
// escaped, or the lead byte of a well-formed
|
|
// code point), so read it again
|
|
--i;
|
|
}
|
|
else
|
|
{
|
|
// a byte that cannot start a sequence (e.g.,
|
|
// 0xFF or a stray continuation byte) is kept
|
|
// as well
|
|
string_buffer[bytes++] = s[i];
|
|
}
|
|
|
|
// write buffer and reset index; there must be 13 bytes
|
|
// left, as this is the maximal number of bytes to be
|
|
// written ("\uxxxx\uxxxx\0") for one code point
|
|
if (string_buffer.size() - bytes < 13)
|
|
{
|
|
put_buffer(string_buffer, bytes);
|
|
bytes = 0;
|
|
}
|
|
|
|
bytes_after_last_accept = bytes;
|
|
undumped_chars = 0;
|
|
|
|
// continue processing the string
|
|
state = UTF8_ACCEPT;
|
|
break;
|
|
}
|
|
|
|
default: // LCOV_EXCL_LINE
|
|
JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE
|
|
}
|
|
break;
|
|
}
|
|
|
|
default: // decode found yet incomplete multibyte code point
|
|
{
|
|
if (!EnsureAscii || error_handler == error_handler_t::keep)
|
|
{
|
|
// code point will not be escaped (or will be kept as
|
|
// is if it turns out to be ill-formed) - copy byte to
|
|
// buffer; dropped again above if it decodes to a
|
|
// well-formed code point that needs \u-escaping
|
|
string_buffer[bytes++] = s[i];
|
|
}
|
|
++undumped_chars;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// we finished processing the string
|
|
if (JSON_HEDLEY_LIKELY(state == UTF8_ACCEPT))
|
|
{
|
|
// write buffer
|
|
if (bytes > 0)
|
|
{
|
|
put_buffer(string_buffer, bytes);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// we finish reading, but do not accept: string was incomplete
|
|
switch (error_handler)
|
|
{
|
|
case error_handler_t::strict:
|
|
{
|
|
JSON_THROW(type_error::create(exception_id::invalid_utf8, concat("incomplete UTF-8 string; last byte: 0x", detail::hex_byte(static_cast<std::uint8_t>(s[s.size() - 1]))), nullptr));
|
|
}
|
|
|
|
case error_handler_t::ignore:
|
|
{
|
|
// write all accepted bytes
|
|
put_buffer(string_buffer, bytes_after_last_accept);
|
|
break;
|
|
}
|
|
|
|
case error_handler_t::replace:
|
|
{
|
|
// write all accepted bytes
|
|
put_buffer(string_buffer, bytes_after_last_accept);
|
|
// add a replacement character
|
|
if (EnsureAscii)
|
|
{
|
|
put_literal("\\ufffd");
|
|
}
|
|
else
|
|
{
|
|
put_literal("\xEF\xBF\xBD");
|
|
}
|
|
break;
|
|
}
|
|
|
|
case error_handler_t::keep:
|
|
{
|
|
// write the ill-formed trailing bytes as is; they were
|
|
// buffered above regardless of EnsureAscii
|
|
put_buffer(string_buffer, bytes);
|
|
break;
|
|
}
|
|
|
|
default: // LCOV_EXCL_LINE
|
|
JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE
|
|
}
|
|
}
|
|
}
|
|
#ifdef JSON_HEDLEY_MSVC_VERSION
|
|
#pragma warning(pop)
|
|
#endif
|
|
|
|
private:
|
|
/*!
|
|
@brief append a single character to the write buffer
|
|
|
|
Structural characters ('{', '"', ',', ...) previously went straight to the
|
|
output adapter, one virtual call each. Buffering them and flushing in bulk
|
|
turns those many indirect calls into a single memcpy plus an occasional
|
|
flush, which dominates the cost of serializing object/array-heavy values.
|
|
*/
|
|
void put_char(char c)
|
|
{
|
|
if (JSON_HEDLEY_UNLIKELY(write_buffer_pos == write_buffer.size()))
|
|
{
|
|
flush();
|
|
}
|
|
write_buffer[write_buffer_pos++] = c;
|
|
}
|
|
|
|
/*!
|
|
@brief append @a indent indentation characters to the write buffer
|
|
|
|
Writes the indentation straight into the buffer instead of copying it out of
|
|
a pre-grown indentation string, so no auxiliary string has to be sized,
|
|
resized, or kept in sync with the deepest nesting level reached.
|
|
|
|
An indentation wider than the buffer is emitted by filling the buffer with
|
|
the indentation character once and flushing that same content repeatedly:
|
|
flushing does not disturb what the buffer holds, so re-filling it between
|
|
flushes would be redundant work.
|
|
*/
|
|
void put_indent(std::size_t indent)
|
|
{
|
|
// closing braces at the outermost level ask for no indentation at all
|
|
if (indent == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const std::size_t capacity = write_buffer.size();
|
|
|
|
// fill whatever room is left in the buffer; this is the whole job
|
|
// whenever the indentation is narrower than the buffer, which is the
|
|
// case for every sane indent_step
|
|
const std::size_t head = (std::min)(indent, capacity - write_buffer_pos);
|
|
std::memset(write_buffer.data() + write_buffer_pos, indent_char, head);
|
|
write_buffer_pos += head;
|
|
indent -= head;
|
|
|
|
if (JSON_HEDLEY_LIKELY(indent == 0))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// the buffer is full and the remainder spans whole buffer-fulls: flush
|
|
// what is pending, then fill the buffer with the indentation character
|
|
// exactly once and hand the same bytes to the adapter as often as needed
|
|
flush();
|
|
std::memset(write_buffer.data(), indent_char, capacity);
|
|
|
|
while (indent >= capacity)
|
|
{
|
|
write_buffer_pos = capacity;
|
|
flush();
|
|
indent -= capacity;
|
|
}
|
|
|
|
// the buffer still holds indentation characters throughout, so the tail
|
|
// only has to be claimed, not written again
|
|
write_buffer_pos = indent;
|
|
}
|
|
|
|
/*!
|
|
@brief append a string literal to the write buffer
|
|
|
|
The length comes from the array bound rather than a hand-written count, so
|
|
it cannot drift out of sync with the literal. A literal always fits into the
|
|
buffer (checked at compile time), so unlike @ref put_string this needs no
|
|
write-through path for oversized runs.
|
|
*/
|
|
template<std::size_t N>
|
|
void put_literal(const char (&s)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
|
|
{
|
|
static_assert(N >= 2, "put_literal expects a non-empty string literal");
|
|
// the array bound counts the terminating NUL, which is not written
|
|
constexpr std::size_t length = N - 1;
|
|
static_assert(length < write_buffer_size, "string literal must fit into the write buffer");
|
|
|
|
if (JSON_HEDLEY_UNLIKELY(write_buffer_pos + length > write_buffer.size()))
|
|
{
|
|
flush();
|
|
}
|
|
std::memcpy(write_buffer.data() + write_buffer_pos, s, length);
|
|
write_buffer_pos += length;
|
|
}
|
|
|
|
/*!
|
|
@brief append the characters of @a str in [@a start, @a end)
|
|
|
|
The only way to append a run of characters: @a str carries its own bound,
|
|
so the range can be checked against it, which a bare pointer plus a count
|
|
could not do. Runs that do not fit the buffer are written straight through
|
|
the output adapter (after flushing what is pending), so large string and
|
|
number payloads are not copied an extra time.
|
|
*/
|
|
template<typename StringType>
|
|
void put_string(const StringType& str, std::size_t start, std::size_t end)
|
|
{
|
|
JSON_ASSERT(start <= end);
|
|
JSON_ASSERT(end <= str.size());
|
|
|
|
const char* const s = str.data() + start;
|
|
const std::size_t length = end - start;
|
|
|
|
if (JSON_HEDLEY_UNLIKELY(length >= write_buffer.size()))
|
|
{
|
|
flush();
|
|
o->write_characters(s, length);
|
|
return;
|
|
}
|
|
if (JSON_HEDLEY_UNLIKELY(write_buffer_pos + length > write_buffer.size()))
|
|
{
|
|
flush();
|
|
}
|
|
std::memcpy(write_buffer.data() + write_buffer_pos, s, length);
|
|
write_buffer_pos += length;
|
|
}
|
|
|
|
/*!
|
|
@brief append the first @a length characters of a fixed-size buffer
|
|
*/
|
|
template<std::size_t N>
|
|
void put_buffer(const std::array<char, N>& buffer, std::size_t length)
|
|
{
|
|
put_string(buffer, 0, length);
|
|
}
|
|
|
|
JSON_PRIVATE_UNLESS_TESTED:
|
|
/*!
|
|
@brief flush the write buffer to the output adapter
|
|
|
|
Writing zero characters is a well-defined no-op for every output adapter, so
|
|
the buffered length is passed through unconditionally (no empty-guard branch
|
|
to leave uncovered).
|
|
|
|
@note dump_escaped() and dump_integer()/dump_float() write into the internal
|
|
write buffer; callers that invoke them directly (rather than through the
|
|
public dump()) must call flush() before inspecting the output.
|
|
*/
|
|
void flush()
|
|
{
|
|
o->write_characters(write_buffer.data(), write_buffer_pos);
|
|
write_buffer_pos = 0;
|
|
}
|
|
|
|
private:
|
|
/*!
|
|
@brief count digits
|
|
|
|
Count the number of decimal (base 10) digits for an input unsigned integer.
|
|
|
|
@param[in] x unsigned integer number to count its digits
|
|
@return number of decimal digits
|
|
*/
|
|
unsigned int count_digits(number_magnitude_t x) noexcept
|
|
{
|
|
unsigned int n_digits = 1;
|
|
for (;;)
|
|
{
|
|
if (x < 10)
|
|
{
|
|
return n_digits;
|
|
}
|
|
if (x < 100)
|
|
{
|
|
return n_digits + 1;
|
|
}
|
|
if (x < 1000)
|
|
{
|
|
return n_digits + 2;
|
|
}
|
|
if (x < 10000)
|
|
{
|
|
return n_digits + 3;
|
|
}
|
|
x = x / 10000u;
|
|
n_digits += 4;
|
|
}
|
|
}
|
|
|
|
/*!
|
|
@brief the JSON form of each character below 0x60
|
|
|
|
Entry c holds the escape sequence of character c - the short form for
|
|
\\b, \\t, \\n, \\f, \\r, \\" and \\\\, \\u00xx for the other control characters -
|
|
or c itself if it needs no escaping, padded with zeros to 7 bytes. The last
|
|
byte is the length of the sequence. The 8 bytes of an entry are copied at
|
|
once, so writing an escape needs neither a branch nor a byte-wise loop.
|
|
*/
|
|
static const std::array<std::array<char, 8>, 0x60>& ascii_escapes() noexcept
|
|
{
|
|
static const std::array<std::array<char, 8>, 0x60> escapes =
|
|
{
|
|
{
|
|
{{'\\', 'u', '0', '0', '0', '0', 0, 6}}, // 0x00
|
|
{{'\\', 'u', '0', '0', '0', '1', 0, 6}}, // 0x01
|
|
{{'\\', 'u', '0', '0', '0', '2', 0, 6}}, // 0x02
|
|
{{'\\', 'u', '0', '0', '0', '3', 0, 6}}, // 0x03
|
|
{{'\\', 'u', '0', '0', '0', '4', 0, 6}}, // 0x04
|
|
{{'\\', 'u', '0', '0', '0', '5', 0, 6}}, // 0x05
|
|
{{'\\', 'u', '0', '0', '0', '6', 0, 6}}, // 0x06
|
|
{{'\\', 'u', '0', '0', '0', '7', 0, 6}}, // 0x07
|
|
{{'\\', 'b', 0, 0, 0, 0, 0, 2}}, // 0x08
|
|
{{'\\', 't', 0, 0, 0, 0, 0, 2}}, // 0x09
|
|
{{'\\', 'n', 0, 0, 0, 0, 0, 2}}, // 0x0A
|
|
{{'\\', 'u', '0', '0', '0', 'b', 0, 6}}, // 0x0B
|
|
{{'\\', 'f', 0, 0, 0, 0, 0, 2}}, // 0x0C
|
|
{{'\\', 'r', 0, 0, 0, 0, 0, 2}}, // 0x0D
|
|
{{'\\', 'u', '0', '0', '0', 'e', 0, 6}}, // 0x0E
|
|
{{'\\', 'u', '0', '0', '0', 'f', 0, 6}}, // 0x0F
|
|
{{'\\', 'u', '0', '0', '1', '0', 0, 6}}, // 0x10
|
|
{{'\\', 'u', '0', '0', '1', '1', 0, 6}}, // 0x11
|
|
{{'\\', 'u', '0', '0', '1', '2', 0, 6}}, // 0x12
|
|
{{'\\', 'u', '0', '0', '1', '3', 0, 6}}, // 0x13
|
|
{{'\\', 'u', '0', '0', '1', '4', 0, 6}}, // 0x14
|
|
{{'\\', 'u', '0', '0', '1', '5', 0, 6}}, // 0x15
|
|
{{'\\', 'u', '0', '0', '1', '6', 0, 6}}, // 0x16
|
|
{{'\\', 'u', '0', '0', '1', '7', 0, 6}}, // 0x17
|
|
{{'\\', 'u', '0', '0', '1', '8', 0, 6}}, // 0x18
|
|
{{'\\', 'u', '0', '0', '1', '9', 0, 6}}, // 0x19
|
|
{{'\\', 'u', '0', '0', '1', 'a', 0, 6}}, // 0x1A
|
|
{{'\\', 'u', '0', '0', '1', 'b', 0, 6}}, // 0x1B
|
|
{{'\\', 'u', '0', '0', '1', 'c', 0, 6}}, // 0x1C
|
|
{{'\\', 'u', '0', '0', '1', 'd', 0, 6}}, // 0x1D
|
|
{{'\\', 'u', '0', '0', '1', 'e', 0, 6}}, // 0x1E
|
|
{{'\\', 'u', '0', '0', '1', 'f', 0, 6}}, // 0x1F
|
|
{{' ', 0, 0, 0, 0, 0, 0, 1}}, // 0x20
|
|
{{'!', 0, 0, 0, 0, 0, 0, 1}}, // 0x21
|
|
{{'\\', '"', 0, 0, 0, 0, 0, 2}}, // 0x22
|
|
{{'#', 0, 0, 0, 0, 0, 0, 1}}, // 0x23
|
|
{{'$', 0, 0, 0, 0, 0, 0, 1}}, // 0x24
|
|
{{'%', 0, 0, 0, 0, 0, 0, 1}}, // 0x25
|
|
{{'&', 0, 0, 0, 0, 0, 0, 1}}, // 0x26
|
|
{{'\'', 0, 0, 0, 0, 0, 0, 1}}, // 0x27
|
|
{{'(', 0, 0, 0, 0, 0, 0, 1}}, // 0x28
|
|
{{')', 0, 0, 0, 0, 0, 0, 1}}, // 0x29
|
|
{{'*', 0, 0, 0, 0, 0, 0, 1}}, // 0x2A
|
|
{{'+', 0, 0, 0, 0, 0, 0, 1}}, // 0x2B
|
|
{{',', 0, 0, 0, 0, 0, 0, 1}}, // 0x2C
|
|
{{'-', 0, 0, 0, 0, 0, 0, 1}}, // 0x2D
|
|
{{'.', 0, 0, 0, 0, 0, 0, 1}}, // 0x2E
|
|
{{'/', 0, 0, 0, 0, 0, 0, 1}}, // 0x2F
|
|
{{'0', 0, 0, 0, 0, 0, 0, 1}}, // 0x30
|
|
{{'1', 0, 0, 0, 0, 0, 0, 1}}, // 0x31
|
|
{{'2', 0, 0, 0, 0, 0, 0, 1}}, // 0x32
|
|
{{'3', 0, 0, 0, 0, 0, 0, 1}}, // 0x33
|
|
{{'4', 0, 0, 0, 0, 0, 0, 1}}, // 0x34
|
|
{{'5', 0, 0, 0, 0, 0, 0, 1}}, // 0x35
|
|
{{'6', 0, 0, 0, 0, 0, 0, 1}}, // 0x36
|
|
{{'7', 0, 0, 0, 0, 0, 0, 1}}, // 0x37
|
|
{{'8', 0, 0, 0, 0, 0, 0, 1}}, // 0x38
|
|
{{'9', 0, 0, 0, 0, 0, 0, 1}}, // 0x39
|
|
{{':', 0, 0, 0, 0, 0, 0, 1}}, // 0x3A
|
|
{{';', 0, 0, 0, 0, 0, 0, 1}}, // 0x3B
|
|
{{'<', 0, 0, 0, 0, 0, 0, 1}}, // 0x3C
|
|
{{'=', 0, 0, 0, 0, 0, 0, 1}}, // 0x3D
|
|
{{'>', 0, 0, 0, 0, 0, 0, 1}}, // 0x3E
|
|
{{'?', 0, 0, 0, 0, 0, 0, 1}}, // 0x3F
|
|
{{'@', 0, 0, 0, 0, 0, 0, 1}}, // 0x40
|
|
{{'A', 0, 0, 0, 0, 0, 0, 1}}, // 0x41
|
|
{{'B', 0, 0, 0, 0, 0, 0, 1}}, // 0x42
|
|
{{'C', 0, 0, 0, 0, 0, 0, 1}}, // 0x43
|
|
{{'D', 0, 0, 0, 0, 0, 0, 1}}, // 0x44
|
|
{{'E', 0, 0, 0, 0, 0, 0, 1}}, // 0x45
|
|
{{'F', 0, 0, 0, 0, 0, 0, 1}}, // 0x46
|
|
{{'G', 0, 0, 0, 0, 0, 0, 1}}, // 0x47
|
|
{{'H', 0, 0, 0, 0, 0, 0, 1}}, // 0x48
|
|
{{'I', 0, 0, 0, 0, 0, 0, 1}}, // 0x49
|
|
{{'J', 0, 0, 0, 0, 0, 0, 1}}, // 0x4A
|
|
{{'K', 0, 0, 0, 0, 0, 0, 1}}, // 0x4B
|
|
{{'L', 0, 0, 0, 0, 0, 0, 1}}, // 0x4C
|
|
{{'M', 0, 0, 0, 0, 0, 0, 1}}, // 0x4D
|
|
{{'N', 0, 0, 0, 0, 0, 0, 1}}, // 0x4E
|
|
{{'O', 0, 0, 0, 0, 0, 0, 1}}, // 0x4F
|
|
{{'P', 0, 0, 0, 0, 0, 0, 1}}, // 0x50
|
|
{{'Q', 0, 0, 0, 0, 0, 0, 1}}, // 0x51
|
|
{{'R', 0, 0, 0, 0, 0, 0, 1}}, // 0x52
|
|
{{'S', 0, 0, 0, 0, 0, 0, 1}}, // 0x53
|
|
{{'T', 0, 0, 0, 0, 0, 0, 1}}, // 0x54
|
|
{{'U', 0, 0, 0, 0, 0, 0, 1}}, // 0x55
|
|
{{'V', 0, 0, 0, 0, 0, 0, 1}}, // 0x56
|
|
{{'W', 0, 0, 0, 0, 0, 0, 1}}, // 0x57
|
|
{{'X', 0, 0, 0, 0, 0, 0, 1}}, // 0x58
|
|
{{'Y', 0, 0, 0, 0, 0, 0, 1}}, // 0x59
|
|
{{'Z', 0, 0, 0, 0, 0, 0, 1}}, // 0x5A
|
|
{{'[', 0, 0, 0, 0, 0, 0, 1}}, // 0x5B
|
|
{{'\\', '\\', 0, 0, 0, 0, 0, 2}}, // 0x5C
|
|
{{']', 0, 0, 0, 0, 0, 0, 1}}, // 0x5D
|
|
{{'^', 0, 0, 0, 0, 0, 0, 1}}, // 0x5E
|
|
{{'_', 0, 0, 0, 0, 0, 0, 1}}, // 0x5F
|
|
}
|
|
};
|
|
return escapes;
|
|
}
|
|
|
|
/*!
|
|
* @brief write a lowercase "\\uXXXX" escape sequence into @a string_buffer
|
|
*
|
|
* Branch-free replacement for `snprintf(buf, 7, "\\u%04x", codeunit)` in the
|
|
* string escaping hot path. It writes exactly six characters ('\\', 'u' and
|
|
* four hex digits) at position @a pos of @a string_buffer via a nibble
|
|
* lookup table, avoiding the format-string parsing and locale machinery of
|
|
* `snprintf`. Advances @a pos by the number of bytes written (6).
|
|
*
|
|
* @param[in] pos position in @a string_buffer to write at; there must
|
|
* be at least 6 bytes of headroom
|
|
* @param[in] codeunit 16-bit value to encode
|
|
*/
|
|
void write_u_escape(std::size_t& pos, std::uint16_t codeunit) noexcept
|
|
{
|
|
JSON_ASSERT(string_buffer.size() - pos >= 6);
|
|
constexpr const char* nibble_to_hex = "0123456789abcdef";
|
|
string_buffer[pos + 0] = '\\';
|
|
string_buffer[pos + 1] = 'u';
|
|
string_buffer[pos + 2] = nibble_to_hex[(codeunit >> 12u) & 0x0Fu];
|
|
string_buffer[pos + 3] = nibble_to_hex[(codeunit >> 8u) & 0x0Fu];
|
|
string_buffer[pos + 4] = nibble_to_hex[(codeunit >> 4u) & 0x0Fu];
|
|
string_buffer[pos + 5] = nibble_to_hex[codeunit & 0x0Fu];
|
|
pos += 6;
|
|
}
|
|
|
|
/*!
|
|
@brief convert a single element of a binary value to its byte value
|
|
|
|
The elements of a binary value are dumped as the numbers 0..255, regardless
|
|
of the value type of the configured BinaryType: that type may be signed
|
|
(`char`), unsigned (`std::uint8_t`), or not an integer at all
|
|
(`std::byte`), none of which @ref dump_integer can handle uniformly.
|
|
*/
|
|
static std::uint8_t to_byte_value(binary_char_t x) noexcept
|
|
{
|
|
return static_cast<std::uint8_t>(x);
|
|
}
|
|
|
|
// templates to avoid warnings about useless casts
|
|
template <typename NumberType, enable_if_t<std::is_signed<NumberType>::value, int> = 0>
|
|
bool is_negative_number(NumberType x)
|
|
{
|
|
return x < 0;
|
|
}
|
|
|
|
template < typename NumberType, enable_if_t <std::is_unsigned<NumberType>::value, int > = 0 >
|
|
bool is_negative_number(NumberType /*unused*/)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/*!
|
|
@brief write the decimal representation of the byte @a value
|
|
|
|
A binary value's bytes are always in [0, 255], so writing one needs neither
|
|
the digit counting nor the 64-bit arithmetic that @ref dump_integer does for
|
|
an arbitrary number, and the three digits it takes at most are written
|
|
straight into the write buffer.
|
|
|
|
Any byte type that is not a plain unsigned byte is converted to its
|
|
@ref to_byte_value "byte value" and left to @ref dump_integer, so a signed
|
|
or non-integral BinaryType::value_type (`char`, `std::byte`, ...) still
|
|
dumps as 0..255.
|
|
*/
|
|
template<typename ByteType>
|
|
void dump_byte(const ByteType value)
|
|
{
|
|
dump_byte(value, std::integral_constant < bool,
|
|
std::is_unsigned<ByteType>::value && sizeof(ByteType) == 1
|
|
&& !std::is_same<ByteType, bool>::value > {});
|
|
}
|
|
|
|
template<typename ByteType>
|
|
void dump_byte(const ByteType value, std::false_type /*is_plain_byte*/)
|
|
{
|
|
dump_integer(to_byte_value(value));
|
|
}
|
|
|
|
template<typename ByteType>
|
|
void dump_byte(const ByteType value, std::true_type /*is_plain_byte*/)
|
|
{
|
|
if (JSON_HEDLEY_UNLIKELY(write_buffer_pos + 3 > write_buffer.size()))
|
|
{
|
|
flush();
|
|
}
|
|
|
|
const auto byte = static_cast<unsigned>(value);
|
|
// Accumulate the offset in a local and store it back once. Writing
|
|
// through write_buffer[] is a char write, which may alias any object,
|
|
// so with the member updated in place the compiler has to reload and
|
|
// store it around every digit - measured 2.4x slower on a dump of a
|
|
// multi-megabyte binary value.
|
|
std::size_t pos = write_buffer_pos;
|
|
|
|
if (byte >= 100)
|
|
{
|
|
write_buffer[pos++] = static_cast<char>('0' + (byte / 100));
|
|
write_buffer[pos++] = static_cast<char>('0' + ((byte / 10) % 10));
|
|
}
|
|
else if (byte >= 10)
|
|
{
|
|
write_buffer[pos++] = static_cast<char>('0' + (byte / 10));
|
|
}
|
|
|
|
write_buffer[pos++] = static_cast<char>('0' + (byte % 10));
|
|
|
|
write_buffer_pos = pos;
|
|
}
|
|
|
|
/*!
|
|
@brief dump an integer
|
|
|
|
Dump a given integer, appending it to @ref write_buffer (directly: copying
|
|
the digits from another buffer right after writing them waits until the
|
|
stores are done).
|
|
|
|
@param[in] x integer number (signed or unsigned) to dump
|
|
@tparam NumberType either @a number_integer_t or @a number_unsigned_t
|
|
*/
|
|
template < typename NumberType, detail::enable_if_t <
|
|
std::is_integral<NumberType>::value ||
|
|
std::is_same<NumberType, number_unsigned_t>::value ||
|
|
std::is_same<NumberType, number_integer_t>::value,
|
|
int > = 0 >
|
|
void dump_integer(NumberType x)
|
|
{
|
|
static constexpr std::array<std::array<char, 2>, 100> digits_to_99
|
|
{
|
|
{
|
|
{{'0', '0'}}, {{'0', '1'}}, {{'0', '2'}}, {{'0', '3'}}, {{'0', '4'}}, {{'0', '5'}}, {{'0', '6'}}, {{'0', '7'}}, {{'0', '8'}}, {{'0', '9'}},
|
|
{{'1', '0'}}, {{'1', '1'}}, {{'1', '2'}}, {{'1', '3'}}, {{'1', '4'}}, {{'1', '5'}}, {{'1', '6'}}, {{'1', '7'}}, {{'1', '8'}}, {{'1', '9'}},
|
|
{{'2', '0'}}, {{'2', '1'}}, {{'2', '2'}}, {{'2', '3'}}, {{'2', '4'}}, {{'2', '5'}}, {{'2', '6'}}, {{'2', '7'}}, {{'2', '8'}}, {{'2', '9'}},
|
|
{{'3', '0'}}, {{'3', '1'}}, {{'3', '2'}}, {{'3', '3'}}, {{'3', '4'}}, {{'3', '5'}}, {{'3', '6'}}, {{'3', '7'}}, {{'3', '8'}}, {{'3', '9'}},
|
|
{{'4', '0'}}, {{'4', '1'}}, {{'4', '2'}}, {{'4', '3'}}, {{'4', '4'}}, {{'4', '5'}}, {{'4', '6'}}, {{'4', '7'}}, {{'4', '8'}}, {{'4', '9'}},
|
|
{{'5', '0'}}, {{'5', '1'}}, {{'5', '2'}}, {{'5', '3'}}, {{'5', '4'}}, {{'5', '5'}}, {{'5', '6'}}, {{'5', '7'}}, {{'5', '8'}}, {{'5', '9'}},
|
|
{{'6', '0'}}, {{'6', '1'}}, {{'6', '2'}}, {{'6', '3'}}, {{'6', '4'}}, {{'6', '5'}}, {{'6', '6'}}, {{'6', '7'}}, {{'6', '8'}}, {{'6', '9'}},
|
|
{{'7', '0'}}, {{'7', '1'}}, {{'7', '2'}}, {{'7', '3'}}, {{'7', '4'}}, {{'7', '5'}}, {{'7', '6'}}, {{'7', '7'}}, {{'7', '8'}}, {{'7', '9'}},
|
|
{{'8', '0'}}, {{'8', '1'}}, {{'8', '2'}}, {{'8', '3'}}, {{'8', '4'}}, {{'8', '5'}}, {{'8', '6'}}, {{'8', '7'}}, {{'8', '8'}}, {{'8', '9'}},
|
|
{{'9', '0'}}, {{'9', '1'}}, {{'9', '2'}}, {{'9', '3'}}, {{'9', '4'}}, {{'9', '5'}}, {{'9', '6'}}, {{'9', '7'}}, {{'9', '8'}}, {{'9', '9'}},
|
|
}
|
|
};
|
|
|
|
// special case for "0"
|
|
if (x == 0)
|
|
{
|
|
put_char('0');
|
|
return;
|
|
}
|
|
|
|
// use a pointer to fill the buffer (room for as much as number_buffer holds)
|
|
if (JSON_HEDLEY_UNLIKELY(write_buffer_pos + number_buffer.size() > write_buffer.size()))
|
|
{
|
|
flush();
|
|
}
|
|
auto* buffer_ptr = write_buffer.data() + write_buffer_pos;
|
|
|
|
number_magnitude_t abs_value;
|
|
|
|
// one byte for the minus sign
|
|
unsigned int n_chars = 0;
|
|
|
|
if (is_negative_number(x))
|
|
{
|
|
*buffer_ptr = '-';
|
|
abs_value = remove_sign(static_cast<number_integer_t>(x));
|
|
n_chars = 1;
|
|
}
|
|
else
|
|
{
|
|
abs_value = static_cast<number_magnitude_t>(x);
|
|
}
|
|
|
|
// up to 16 digits: eight at a time (as the digits of floats), written
|
|
// without leading zeros
|
|
if (abs_value < 10000000000000000u)
|
|
{
|
|
const std::uint64_t value = abs_value;
|
|
const std::uint64_t upper = value / 100000000u;
|
|
const std::uint64_t first = dtoa_impl::eight_digit_bytes(upper != 0 ? upper : value);
|
|
const auto leading = static_cast<unsigned>(count_leading_zeros(first) / 8); // (first is not 0)
|
|
char* const p = buffer_ptr + n_chars;
|
|
dtoa_impl::store_msb_first(p, (first << (8 * leading)) + 0x3030303030303030u);
|
|
n_chars += 8 - leading;
|
|
if (upper != 0)
|
|
{
|
|
dtoa_impl::store_msb_first(p + 8 - leading, dtoa_impl::eight_digit_bytes(value - (upper * 100000000u)) + 0x3030303030303030u);
|
|
n_chars += 8;
|
|
}
|
|
write_buffer_pos += n_chars;
|
|
return;
|
|
}
|
|
|
|
n_chars += count_digits(abs_value);
|
|
|
|
// spare 1 byte for '\0'
|
|
JSON_ASSERT(n_chars < number_buffer.size() - 1);
|
|
|
|
// jump to the end to generate the string from backward,
|
|
// so we later avoid reversing the result
|
|
buffer_ptr += n_chars;
|
|
|
|
// Fast int2ascii implementation inspired by "Fastware" talk by Andrei Alexandrescu
|
|
// See: https://www.youtube.com/watch?v=o4-CwDo2zpg
|
|
while (abs_value >= 100)
|
|
{
|
|
const auto digits_index = static_cast<unsigned>((abs_value % 100));
|
|
abs_value /= 100;
|
|
*(--buffer_ptr) = digits_to_99[digits_index][1];
|
|
*(--buffer_ptr) = digits_to_99[digits_index][0];
|
|
}
|
|
|
|
if (abs_value >= 10)
|
|
{
|
|
const auto digits_index = static_cast<unsigned>(abs_value);
|
|
*(--buffer_ptr) = digits_to_99[digits_index][1];
|
|
*(--buffer_ptr) = digits_to_99[digits_index][0];
|
|
}
|
|
else
|
|
{
|
|
*(--buffer_ptr) = static_cast<char>('0' + abs_value);
|
|
}
|
|
|
|
write_buffer_pos += n_chars;
|
|
}
|
|
|
|
/*!
|
|
@brief dump a floating-point number
|
|
|
|
Dump a given floating-point number, appending it to @ref write_buffer.
|
|
|
|
@param[in] x floating-point number to dump
|
|
*/
|
|
void dump_float(number_float_t x)
|
|
{
|
|
// NaN / inf
|
|
if (!std::isfinite(x))
|
|
{
|
|
put_literal("null");
|
|
return;
|
|
}
|
|
|
|
// If number_float_t is an IEEE-754 single or double precision number,
|
|
// use the Grisu2 algorithm to produce short numbers which are
|
|
// guaranteed to round-trip, using strtof and strtod, resp.
|
|
//
|
|
// NB: The test below works if <long double> == <double>.
|
|
static constexpr bool is_ieee_single_or_double
|
|
= (std::numeric_limits<number_float_t>::is_iec559 && std::numeric_limits<number_float_t>::digits == 24 && std::numeric_limits<number_float_t>::max_exponent == 128) ||
|
|
(std::numeric_limits<number_float_t>::is_iec559 && std::numeric_limits<number_float_t>::digits == 53 && std::numeric_limits<number_float_t>::max_exponent == 1024);
|
|
|
|
dump_float(x, std::integral_constant<bool, is_ieee_single_or_double>());
|
|
}
|
|
|
|
void dump_float(number_float_t x, std::true_type /*is_ieee_single_or_double*/)
|
|
{
|
|
// directly into the write buffer: copying the text from number_buffer
|
|
// right after to_chars() wrote it waits until its stores are done
|
|
if (JSON_HEDLEY_UNLIKELY(write_buffer_pos + number_buffer.size() > write_buffer.size()))
|
|
{
|
|
flush();
|
|
}
|
|
auto* begin = write_buffer.data() + write_buffer_pos;
|
|
auto* end = ::nlohmann::detail::to_chars(begin, begin + number_buffer.size(), x);
|
|
write_buffer_pos += static_cast<std::size_t>(end - begin);
|
|
}
|
|
|
|
JSON_HEDLEY_NON_NULL(1)
|
|
static int snprintf_float(char* buf, std::size_t size, int d, double x)
|
|
{
|
|
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg)
|
|
return (std::snprintf)(buf, size, "%.*g", d, x);
|
|
}
|
|
|
|
JSON_HEDLEY_NON_NULL(1)
|
|
static int snprintf_float(char* buf, std::size_t size, int d, long double x)
|
|
{
|
|
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg)
|
|
return (std::snprintf)(buf, size, "%.*Lg", d, x);
|
|
}
|
|
|
|
void dump_float(number_float_t x, std::false_type /*is_ieee_single_or_double*/)
|
|
{
|
|
// get the number of digits for a float -> text -> float round-trip
|
|
static constexpr auto d = std::numeric_limits<number_float_t>::max_digits10;
|
|
|
|
// the actual conversion
|
|
std::ptrdiff_t len = snprintf_float(number_buffer.data(), number_buffer.size(), d, x);
|
|
|
|
// negative value indicates an error
|
|
JSON_ASSERT(len > 0);
|
|
// check if the buffer was large enough
|
|
JSON_ASSERT(static_cast<std::size_t>(len) < number_buffer.size());
|
|
|
|
// look up the locale's thousands separator and decimal point now,
|
|
// matching what snprintf_float() just used (see lexer::get_decimal_point())
|
|
const auto* loc = std::localeconv();
|
|
JSON_ASSERT(loc != nullptr);
|
|
const char thousands_sep = (loc->thousands_sep == nullptr) ? '\0' : *loc->thousands_sep;
|
|
const char decimal_point = (loc->decimal_point == nullptr) ? '\0' : *loc->decimal_point;
|
|
|
|
// erase thousands separators
|
|
if (thousands_sep != '\0')
|
|
{
|
|
// NOLINTNEXTLINE(readability-qualified-auto,llvm-qualified-auto): std::remove returns an iterator, see https://github.com/nlohmann/json/issues/3081
|
|
const auto end = std::remove(number_buffer.begin(), number_buffer.begin() + len, thousands_sep);
|
|
std::fill(end, number_buffer.end(), '\0');
|
|
JSON_ASSERT((end - number_buffer.begin()) <= len);
|
|
len = (end - number_buffer.begin());
|
|
}
|
|
|
|
// convert decimal point to '.'
|
|
if (decimal_point != '\0' && decimal_point != '.')
|
|
{
|
|
// NOLINTNEXTLINE(readability-qualified-auto,llvm-qualified-auto): std::find returns an iterator, see https://github.com/nlohmann/json/issues/3081
|
|
const auto dec_pos = std::find(number_buffer.begin(), number_buffer.end(), decimal_point);
|
|
if (dec_pos != number_buffer.end())
|
|
{
|
|
*dec_pos = '.';
|
|
}
|
|
}
|
|
|
|
put_buffer(number_buffer, static_cast<std::size_t>(len));
|
|
|
|
// determine if we need to append ".0"
|
|
const bool value_is_int_like =
|
|
std::none_of(number_buffer.begin(), number_buffer.begin() + len + 1,
|
|
[](char c)
|
|
{
|
|
return c == '.' || c == 'e';
|
|
});
|
|
|
|
if (value_is_int_like)
|
|
{
|
|
put_literal(".0");
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Helper function for dump_integer
|
|
*
|
|
* This function takes a negative signed integer and returns its absolute
|
|
* value as an unsigned integer. The plus/minus shuffling is necessary as we
|
|
* cannot directly remove the sign of an arbitrary signed integer as the
|
|
* absolute values of INT_MIN and INT_MAX are usually not the same. See
|
|
* #1708 for details.
|
|
*/
|
|
number_magnitude_t remove_sign(number_integer_t x) noexcept
|
|
{
|
|
JSON_ASSERT(x < 0);
|
|
return static_cast<number_magnitude_t>(-(x + 1)) + 1;
|
|
}
|
|
|
|
private:
|
|
/// the output of the serializer (non-owning; the adapter lives at the call site)
|
|
output_adapter_protocol<char>* o = nullptr;
|
|
|
|
/// a (hopefully) large enough character buffer
|
|
std::array<char, 64> number_buffer{{}};
|
|
|
|
/// string buffer
|
|
std::array<char, 512> string_buffer{{}};
|
|
|
|
/// the indentation character
|
|
const char indent_char;
|
|
|
|
/// whether to pretty-print the output
|
|
const bool pretty_print;
|
|
|
|
/// whether to escape non-ASCII characters with \\uXXXX sequences
|
|
const bool ensure_ascii;
|
|
|
|
/// the indent level
|
|
const std::size_t indent_step;
|
|
|
|
/// error_handler how to react on decoding errors
|
|
const error_handler_t error_handler;
|
|
|
|
/// buffer collecting output before it is flushed to the output adapter, so
|
|
/// that the many small structural writes become few bulk writes
|
|
static constexpr std::size_t write_buffer_size = 1024;
|
|
std::array<char, write_buffer_size> write_buffer{{}};
|
|
/// number of valid bytes currently held in @ref write_buffer
|
|
std::size_t write_buffer_pos = 0;
|
|
};
|
|
|
|
} // namespace detail
|
|
NLOHMANN_JSON_NAMESPACE_END
|