Compare commits

..
Author SHA1 Message Date
Niels Lohmann ff8ba3cb74 Keep the serializer's buffers out of -Weffc++ and cpplint warnings
GCC's -Weffc++ asks for the three buffers in the member initialization
list, which would zero them again. They are now of a type with an empty
user-provided constructor (as the json_view builder's frame stack), so
they are listed there and stay uninitialized.

The 8-to-31-byte branch of the short-string writer no longer spans an
#if with a dangling else, which cpplint read as an else body without
braces.

dump() instructions (Apple clang, twitter/citm/canada, compact and
indented) are unchanged within noise (-0.3 % to +0.6 %).

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 10:51:40 +02:00
Niels Lohmann 0c3e83325a Test strings of 8 to 15 characters as one vector
The words of a string of 8 to 15 printable ASCII characters were each tested
with the word-at-a-time arithmetic. With SSE2 or NEON, the two overlapping
words are joined into one vector, which takes fewer operations to test; the
stores are unchanged. Targets without either keep the word arithmetic.

Apple M1, Apple clang, retired instructions per dump(), on top of the
previous changes:
  jeopardy 451M -> 430M (-5 %)   citm_catalog 5.60M -> 5.37M (-4 %)
  twitter 3.31M -> 3.25M (-2 %)  keys and values of 1..40 characters -1 %
Linux x86-64 (callgrind), gcc 13 / clang 18: jeopardy -15 % / -7 %,
citm_catalog -6 % / -5 %, twitter -6 % / -4 %.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:31 +02:00
Niels Lohmann ebcd4ae4ab Copy the short runs between escapes as a block of 16 bytes
The characters between two escapes (a quote, a backslash, a control
character) are copied to the write buffer by memcpy with a length known only
at run time. For a run of up to 16 characters that has 16 or more bytes in
the string behind its start and in the buffer behind its end, a copy of a
fixed block of 16 bytes does the same without the call; the surplus bytes in
the buffer are overwritten by what follows.

Apple M1, Apple clang, retired instructions per dump(), on top of the
previous changes:
  JSON in JSON 8.32M -> 7.52M (-10 %)   paths 8.17M -> 7.49M (-8 %)
  log lines 7.49M -> 7.14M (-5 %)       cjk, emoji text (no escapes): +-1 %
Linux x86-64 (callgrind), gcc 13 / clang 18:
  JSON in JSON -11 % / -10 %, paths -7 % / -6 %, log lines -5 % / -4 %.
The branch is marked unlikely: text without escapes (CJK, accented Latin)
has few short runs, and without the hint it paid up to 5 % for the check.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:28 +02:00
Niels Lohmann 47462527b2 Write a line break and the indentation of the next line together
A pretty-printed array or object wrote ",\n", checked the room for the
indentation, and stored it, for every element. The line break and the
usual indentation (up to 32 characters, with room in the write buffer) are
now written with one check and one run of stores, also after the opening
bracket and in front of the closing one. Other indentations take the
unchanged path.

Apple M1, Apple clang, retired instructions per dump(4), on top of the
previous changes:
  citm_catalog 7.28M -> 7.06M (-3 %)   canada 44.4M -> 43.3M (-3 %)
  twitter 3.91M -> 3.78M (-3 %)        jeopardy -1 %
Linux x86-64 (callgrind), gcc 13 / clang 18, dump(4) / dump(1, '\t'):
  citm_catalog dump(4) -3 % / -4 %, tab -4 % / -5 %, with the function
  inlined (without that GCC wrote the combined step out of line and was
  5 % slower than before the change).
Compact output is unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:26 +02:00
Niels Lohmann ae30ba55c3 Write strings and keys of up to 32 characters in one go with vectors
The previous change handles plain strings of up to 16 characters with
two overlapping 8-byte words. With SSE2 or NEON, two overlapping 16-byte
vectors do the same for 16 to 32 characters: one test for a quote,
backslash, control character, DEL or non-ASCII byte over both, and two
stores. Targets without either keep the limit of 16.

Apple M1, Apple clang, retired instructions per dump(), on top of the
previous changes (it replaces the table of #5878 for these strings):
  keys and values of 1..40 characters 7.07M -> 4.94M (-30 %)
  twitter 4.15M -> 3.30M (-21 %)   paths 9.09M -> 8.08M (-11 %)
  citm_catalog 5.76M -> 5.53M (-4 %)   jeopardy 461M -> 451M (-2 %)
Linux x86-64 (callgrind), gcc 13 / clang 18: twitter -11 % / -9 %,
keys and values of 1..40 characters -19 % / -17 %, paths -7 % / -7 %.
Documents without strings in that range are unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:23 +02:00
Niels Lohmann 731363a325 Write the usual indentation with four stores instead of memset
Every line of a pretty-printed document starts with its indentation, which
was written by calling memset with a length known only at run time.

An indentation of up to 32 characters, with room in the write buffer,
stores four words of the indentation character and claims the part it
needs. Wider indentations and a nearly full buffer take the unchanged
path.

Apple M1, Apple clang, retired instructions per dump(4), on top of the
previous changes:
  canada  50.7M -> 44.4M (-13 %)   citm_catalog 8.85M -> 7.48M (-16 %)
  twitter 5.13M -> 4.71M (-8 %)    jeopardy 564M -> 518M (-8 %)
  a 139-byte object 3277 -> 2924 (-11 %)
Linux x86-64 (callgrind), the whole series against develop, gcc 13 / clang 18:
  canada, indent 4: -13 % / -14 %.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:21 +02:00
Niels Lohmann fdcf3aa390 Do not zero the serializer's buffers
Constructing a serializer value-initialized its three buffers (64 + 512
+ 1024 bytes). Each is written before it is read and only the part that
was written is ever read, so the zeroing was never needed, but it took
about 170 instructions per dump(), more than half of the cost of dumping
a small value.

Apple M1, Apple clang, per dump():
  null                325 -> 153 instructions
  [1,2,3]             629 -> 458 instructions
  small object (134 B) 2795 -> 2624 instructions, 190 -> 165 ns (-13 %)
  a 139-byte object, compact: 2763 -> 2596 instructions (-6 %)
Large documents are unchanged. Checked with valgrind memcheck over the
differential corpus (no use of uninitialized values), and with GCC 13
-Wall -Wextra -Wmaybe-uninitialized at -O0 to -O3 and -Os.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:18 +02:00
Niels Lohmann 209348c151 Escape runs of multi-byte characters straight into the write buffer
With ensure_ascii, every non-ASCII character went through the UTF-8
state machine one byte at a time and its \uXXXX escape was assembled in
string_buffer and copied to the write buffer afterwards.

A run of well-formed multi-byte characters is now decoded and escaped
directly into the write buffer. The first byte that is ASCII or starts an
ill-formed or truncated sequence ends the run and is left to the
unchanged byte-at-a-time path, so error handling and its messages
(positions, replacement, ignore, keep) are as before.

Apple M1, Apple clang, retired instructions per dump(-1, ' ', true):
  cjk text      15.9M -> 6.94M (-56 %)   twitter 9.10M -> 6.16M (-32 %)
  emoji text    8.89M -> 6.08M (-32 %)   latin text 10.3M -> 9.23M (-10 %)
Pure ASCII documents with escapes (logs, JSON in JSON) and all modes
without ensure_ascii are unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:16 +02:00
Niels Lohmann 7c3888fe42 Write the scalar elements of a container without entering dump_internal()
Every element of an array or object went through dump_internal(), whose
frame holds the state for descending into a container (callee-saved
registers, indentation, depth). A scalar, which is what most elements
are, needs none of it, and paid for the entry and exit regardless.

The loops now call dump_scalar() directly for anything that is not an
object or an array, and dump_internal() for those only.

Apple M1, Apple clang, retired instructions per dump(), on top of the
previous change:
  small object 2997 -> 2712 (-10 %)   signed_ints 276M -> 252M (-9 %)
  floats 323M -> 301M (-7 %)          jeopardy 501M -> 462M (-8 %)
  citm_catalog 5.96M -> 5.61M (-6 %)  twitter 4.40M -> 4.12M (-6 %)
  canada 39.2M -> 37.0M (-5 %)

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:13 +02:00
Niels Lohmann 5bbc7a1098 Write short plain strings and keys in one go in dump()
Most strings in real documents are object keys and short values. For
those, dump_escaped() scanned the bytes, called memcpy with a length
known only at run time, and the surrounding quote, colon and space were
three more bounds-checked writes.

A string of up to 16 printable ASCII characters (no quote, backslash or
DEL) is now read with two overlapping loads, checked with one SWAR test
per word, and written together with its quotes and the colon with two
overlapping stores. Anything else, and a write buffer without room, takes
the general path unchanged. Little-endian targets only; others keep the
general path.

dump_scalar() held the code of every scalar type and saved and restored
all callee-saved registers on every call. It only dispatches now, and the
string, integer, float and binary writers are functions of their own, so
each pays only for what it uses (the string path keeps the check for
long strings in front of any attempt, which keeps documents of long
strings neutral).

Apple M1, Apple clang, retired instructions per dump(), on top of the
printable-ASCII table of #5878:
  citm_catalog  7.54M -> 5.96M (-21 %)   twitter 4.87M -> 4.40M (-10 %)
  jeopardy      603M -> 501M   (-17 %)   logs     8.65M -> 8.17M (-5 %)
  a 139-byte object 3497 -> 2997 (-14 %)   signed_ints -4 %, canada -3 %
Linux x86-64 (callgrind), the whole series against develop, gcc 13 / clang 18:
  citm_catalog -16 % / -19 % (compact), twitter -22 % / -24 %.
Documents of long strings (paths, latin and CJK text) are within 1 %.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:11 +02:00
10 changed files with 1605 additions and 4476 deletions

No files matched your search

-5
View File
@@ -339,11 +339,6 @@ The `serializer` (used by [`dump`](../api/basic_json/dump.md) and [`operator<<`]
- `output_stream_adapter` writes to a `std::ostream`. - `output_stream_adapter` writes to a `std::ostream`.
- `output_string_adapter` appends to a string. - `output_string_adapter` appends to a string.
The `binary_writer` does not call an adapter for every byte. The `to_*` functions that return a `std::vector` write
into the vector through a sink (`output_vector_sink`) that is not virtual. The functions that take an output adapter
write through a sink (`output_adapter_sink`) that collects the output in a buffer of one kibibyte and passes it to the
adapter in blocks, so that the adapter is called (and a stream locked) once per block instead of once per value.
## Value conversion ## Value conversion
Values are converted from and to other types with the `JSONSerializer` template parameter. The default, Values are converted from and to other types with the `JSONSerializer` template parameter. The default,
@@ -33,6 +33,7 @@
#undef JSON_SCAN_SSSE3_TARGET #undef JSON_SCAN_SSSE3_TARGET
#undef JSON_SCAN_VECTOR #undef JSON_SCAN_VECTOR
#undef JSON_SCAN_VECTOR_UTF8 #undef JSON_SCAN_VECTOR_UTF8
#undef JSON_DUMP_SHORT_STRINGS
#ifndef JSON_TEST_KEEP_MACROS #ifndef JSON_TEST_KEEP_MACROS
#undef JSON_CATCH #undef JSON_CATCH
File diff suppressed because it is too large. Load diff
@@ -55,66 +55,6 @@ using output_adapter_t = std::shared_ptr<output_adapter_protocol<CharType>>;
/// output_vector_sink also appends writes of at least this size with insert() /// output_vector_sink also appends writes of at least this size with insert()
constexpr std::size_t max_reserve_size = 256; constexpr std::size_t max_reserve_size = 256;
/// @brief copy @a length characters, at most 32, without a call of memcpy()
///
/// Short strings are the typical payload of a binary value, and a call of
/// memcpy() costs more than copying them. A copy of 16 to 32 characters is two
/// possibly overlapping 16-byte copies, one of 8 to 15 two 8-byte copies, and
/// so on down to a single character; each is a fixed-size load and store.
/// Nothing outside of [src, src + length) is read or written.
template<typename CharType>
inline void copy_short(CharType* dst, const CharType* src, const std::size_t length, std::true_type /*is_byte*/) noexcept
{
JSON_ASSERT(length <= 32);
if (length >= 16)
{
std::array<char, 16> first; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
std::array<char, 16> last; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
std::memcpy(first.data(), src, 16);
std::memcpy(last.data(), src + (length - 16), 16);
std::memcpy(dst, first.data(), 16);
std::memcpy(dst + (length - 16), last.data(), 16);
}
else if (length >= 8)
{
std::array<char, 8> first; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
std::array<char, 8> last; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
std::memcpy(first.data(), src, 8);
std::memcpy(last.data(), src + (length - 8), 8);
std::memcpy(dst, first.data(), 8);
std::memcpy(dst + (length - 8), last.data(), 8);
}
else if (length >= 4)
{
std::array<char, 4> first; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
std::array<char, 4> last; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
std::memcpy(first.data(), src, 4);
std::memcpy(last.data(), src + (length - 4), 4);
std::memcpy(dst, first.data(), 4);
std::memcpy(dst + (length - 4), last.data(), 4);
}
else if (length != 0)
{
// 1 to 3: the first, the middle, and the last character
const CharType a = src[0];
const CharType b = src[length >> 1];
const CharType c = src[length - 1];
dst[0] = a;
dst[length >> 1] = b;
dst[length - 1] = c;
}
}
/// @brief copy @a length characters, of a character type wider than a byte
template<typename CharType>
inline void copy_short(CharType* dst, const CharType* src, const std::size_t length, std::false_type /*is_byte*/) noexcept
{
std::memcpy(dst, src, length * sizeof(CharType));
}
/// the number of characters an empty vector is grown to by output_vector_sink
constexpr std::size_t min_vector_sink_capacity = 64;
/// @brief non-virtual output sink writing into a std::vector /// @brief non-virtual output sink writing into a std::vector
/// ///
/// This sink is not part of the virtual output_adapter_protocol hierarchy: it is /// This sink is not part of the virtual output_adapter_protocol hierarchy: it is
@@ -138,7 +78,7 @@ class output_vector_sink
{ {
public: public:
explicit output_vector_sink(std::vector<CharType, AllocatorType>& vec) noexcept explicit output_vector_sink(std::vector<CharType, AllocatorType>& vec) noexcept
: v(&vec), cursor(vec.data() + vec.size()), limit(cursor) : v(&vec), position(vec.size())
{} {}
output_vector_sink(const output_vector_sink&) = delete; output_vector_sink(const output_vector_sink&) = delete;
@@ -146,7 +86,7 @@ class output_vector_sink
output_vector_sink& operator=(output_vector_sink&&) = delete; output_vector_sink& operator=(output_vector_sink&&) = delete;
output_vector_sink(output_vector_sink&& other) noexcept output_vector_sink(output_vector_sink&& other) noexcept
: v(other.v), cursor(other.cursor), limit(other.limit) : v(other.v), position(other.position)
{ {
other.v = nullptr; other.v = nullptr;
} }
@@ -155,7 +95,7 @@ class output_vector_sink
{ {
if (v != nullptr) if (v != nullptr)
{ {
v->resize(position()); v->resize(position);
} }
} }
@@ -164,11 +104,11 @@ class output_vector_sink
JSON_HEDLEY_ALWAYS_INLINE JSON_HEDLEY_ALWAYS_INLINE
void write_character(CharType c) void write_character(CharType c)
{ {
if (JSON_HEDLEY_UNLIKELY(cursor == limit)) if (JSON_HEDLEY_UNLIKELY(position == v->size()))
{ {
grow(1); grow(1);
} }
*cursor++ = c; (*v)[position++] = c;
} }
// no JSON_HEDLEY_NON_NULL here: binary_writer legitimately passes a null // no JSON_HEDLEY_NON_NULL here: binary_writer legitimately passes a null
@@ -177,7 +117,7 @@ class output_vector_sink
JSON_HEDLEY_ALWAYS_INLINE JSON_HEDLEY_ALWAYS_INLINE
void write_characters(const CharType* s, std::size_t length) void write_characters(const CharType* s, std::size_t length)
{ {
if (JSON_HEDLEY_UNLIKELY(space() < length)) if (JSON_HEDLEY_UNLIKELY(v->size() - position < length))
{ {
if (length >= max_reserve_size) if (length >= max_reserve_size)
{ {
@@ -186,15 +126,10 @@ class output_vector_sink
} }
grow(length); grow(length);
} }
if (length <= 32) if (length != 0)
{ {
copy_short(cursor, s, length, std::integral_constant<bool, sizeof(CharType) == 1>()); std::memcpy(v->data() + position, s, length * sizeof(CharType));
cursor += length; position += length;
}
else
{
std::memcpy(cursor, s, length * sizeof(CharType));
cursor += length;
} }
} }
@@ -209,37 +144,21 @@ class output_vector_sink
CharType* reserve(const std::size_t length) CharType* reserve(const std::size_t length)
{ {
JSON_ASSERT(length <= max_reserve_size); JSON_ASSERT(length <= max_reserve_size);
if (JSON_HEDLEY_UNLIKELY(space() < length)) if (JSON_HEDLEY_UNLIKELY(v->size() - position < length))
{ {
grow(length); grow(length);
} }
return cursor; return v->data() + position;
} }
/// keep the first @a length characters written to the space from reserve() /// keep the first @a length characters written to the space from reserve()
JSON_HEDLEY_ALWAYS_INLINE JSON_HEDLEY_ALWAYS_INLINE
void commit(const std::size_t length) noexcept void commit(const std::size_t length) noexcept
{ {
cursor += length; position += length;
} }
/// nothing to pass on: the characters are in the vector (see output_adapter_sink)
void flush() noexcept
{}
private: private:
/// number of characters of *v written so far
std::size_t position() const noexcept
{
return static_cast<std::size_t>(cursor - v->data());
}
/// number of characters that fit the vector behind the write position
std::size_t space() const noexcept
{
return static_cast<std::size_t>(limit - cursor);
}
/// make room for at least @a length more characters after the position; /// make room for at least @a length more characters after the position;
/// kept out of line so that the write functions stay small enough for /// kept out of line so that the write functions stay small enough for
/// GCC to inline them into the writer /// GCC to inline them into the writer
@@ -247,15 +166,12 @@ class output_vector_sink
void grow(const std::size_t length) void grow(const std::size_t length)
{ {
// use the capacity the vector already has, else at least double it; // use the capacity the vector already has, else at least double it;
// never less than min_vector_sink_capacity, so that a small value does not // never less than 64 characters, so that a small value does not
// reallocate for every few bytes // reallocate for every few bytes
const std::size_t written = position(); const std::size_t needed = position + length;
const std::size_t needed = written + length; v->resize((std::max)(std::size_t{64}, needed <= v->capacity()
v->resize((std::max)(min_vector_sink_capacity, needed <= v->capacity()
? v->capacity() ? v->capacity()
: (std::max)(needed, v->capacity() * 2))); : (std::max)(needed, v->capacity() * 2)));
cursor = v->data() + written;
limit = v->data() + v->size();
} }
/// append @a length (at least max_reserve_size) characters that do not fit /// append @a length (at least max_reserve_size) characters that do not fit
@@ -265,24 +181,20 @@ class output_vector_sink
JSON_HEDLEY_NEVER_INLINE JSON_HEDLEY_NEVER_INLINE
void append(const CharType* s, const std::size_t length) void append(const CharType* s, const std::size_t length)
{ {
const std::size_t written = position(); v->resize(position);
v->resize(written); if (v->capacity() - position < length)
if (v->capacity() - written < length)
{ {
// grow geometrically, with room for the writes that follow, so // grow geometrically, with room for the writes that follow, so
// that the next grow() need not reallocate right away // that the next grow() need not reallocate right away
v->reserve((std::max)(v->capacity() * 2, written + length + max_reserve_size)); v->reserve((std::max)(v->capacity() * 2, position + length + max_reserve_size));
} }
v->insert(v->end(), s, s + length); v->insert(v->end(), s, s + length);
cursor = v->data() + v->size(); position = v->size();
limit = cursor;
} }
std::vector<CharType, AllocatorType>* v; std::vector<CharType, AllocatorType>* v;
/// the write position: in *v, at the number of characters written so far /// number of characters of *v written so far
CharType* cursor; std::size_t position;
/// the end of *v
CharType* limit;
}; };
/// output adapter for byte vectors /// output adapter for byte vectors
@@ -365,17 +277,9 @@ class output_string_adapter : public output_adapter_protocol<CharType>
/// ///
/// Wraps the polymorphic output_adapter_t so the same binary_writer template can /// Wraps the polymorphic output_adapter_t so the same binary_writer template can
/// also target arbitrary adapters (output streams, strings, user-provided /// also target arbitrary adapters (output streams, strings, user-provided
/// adapters) via the `output_adapter`-based overloads. /// adapters) via the `output_adapter`-based overloads. Each write still goes
/// /// through one virtual call, exactly as before; only the concrete sinks above
/// A write through the adapter is a virtual call, and for a stream a call with /// avoid it.
/// a sentry on top, which costs more than encoding the few bytes of most values.
/// The sink therefore collects what is written in a buffer and passes it on in
/// one piece when it is full, a write is too large for it, or @ref flush is
/// called. Everything written reaches the adapter in the same order and with
/// the same bytes; the destructor passes on what is left, so that a write that
/// throws leaves the adapter with exactly the bytes written before it. Only
/// @ref flush can report an error of the adapter (the destructor must not
/// throw), so it has to be called once the value is written.
template<typename CharType> template<typename CharType>
class output_adapter_sink class output_adapter_sink
{ {
@@ -386,121 +290,36 @@ class output_adapter_sink
JSON_ASSERT(oa); JSON_ASSERT(oa);
} }
output_adapter_sink(const output_adapter_sink&) = delete;
output_adapter_sink& operator=(const output_adapter_sink&) = delete;
output_adapter_sink& operator=(output_adapter_sink&&) = delete;
output_adapter_sink(output_adapter_sink&& other) noexcept
: oa(std::move(other.oa)), used(other.used)
{
std::memcpy(buffer.data(), other.buffer.data(), other.used * sizeof(CharType));
other.used = 0;
}
~output_adapter_sink()
{
// a normal return has flushed already, so this is for a write that threw
if (used != 0)
{
JSON_TRY
{
flush();
}
JSON_CATCH(...)
{
// nothing can be done about an adapter that throws here
}
}
}
JSON_HEDLEY_ALWAYS_INLINE
void write_character(CharType c) void write_character(CharType c)
{ {
if (JSON_HEDLEY_UNLIKELY(used == buffer_size)) oa->write_character(c);
{
flush();
}
buffer[used++] = c;
} }
// no JSON_HEDLEY_NON_NULL: forwards (null, 0) for empty payloads, exactly as // no JSON_HEDLEY_NON_NULL: forwards (null, 0) for empty payloads, exactly as
// the type-erased path already did before this sink existed // the type-erased path already did before this sink existed
JSON_HEDLEY_ALWAYS_INLINE
void write_characters(const CharType* s, std::size_t length) void write_characters(const CharType* s, std::size_t length)
{ {
if (JSON_HEDLEY_UNLIKELY(buffer_size - used < length)) oa->write_characters(s, length);
{
write_large(s, length);
return;
}
if (length <= 32)
{
copy_short(buffer.data() + used, s, length, std::integral_constant<bool, sizeof(CharType) == 1>());
used += length;
}
else
{
std::memcpy(buffer.data() + used, s, length * sizeof(CharType));
used += length;
}
} }
/// space for writing up to @a length characters (see /// space for writing up to @a length characters (see
/// output_vector_sink::reserve) /// output_vector_sink::reserve)
JSON_HEDLEY_ALWAYS_INLINE CharType* reserve(const std::size_t length) noexcept
CharType* reserve(const std::size_t length)
{ {
JSON_ASSERT(length <= max_reserve_size); JSON_ASSERT(length <= max_reserve_size);
if (JSON_HEDLEY_UNLIKELY(buffer_size - used < length)) static_cast<void>(length);
{ return reserved.data();
flush();
}
return buffer.data() + used;
} }
/// keep the first @a length characters written to the space from reserve() /// write the first @a length characters written to the space from reserve()
JSON_HEDLEY_ALWAYS_INLINE void commit(const std::size_t length)
void commit(const std::size_t length) noexcept
{ {
used += length; oa->write_characters(reserved.data(), length);
}
/// pass everything written so far on to the adapter
void flush()
{
if (used != 0)
{
// reset first: the characters count as written even if the adapter throws
const std::size_t length = used;
used = 0;
oa->write_characters(buffer.data(), length);
}
} }
private: private:
/// a write that does not fit the space left in the buffer
JSON_HEDLEY_NEVER_INLINE
void write_large(const CharType* s, const std::size_t length)
{
flush();
if (length >= buffer_size)
{
oa->write_characters(s, length);
}
else
{
std::memcpy(buffer.data(), s, length * sizeof(CharType));
used = length;
}
}
/// large enough for reserve() at any time
static constexpr std::size_t buffer_size = 4 * max_reserve_size;
output_adapter_t<CharType> oa; output_adapter_t<CharType> oa;
/// number of characters of @ref buffer not yet passed on std::array<CharType, max_reserve_size> reserved{};
std::size_t used = 0;
std::array<CharType, buffer_size> buffer{};
}; };
/// @brief whether std::basic_string<CharType> has a non-deprecated std::char_traits /// @brief whether std::basic_string<CharType> has a non-deprecated std::char_traits
+550 -114
View File
@@ -34,6 +34,14 @@
#include <nlohmann/detail/string_utils.hpp> #include <nlohmann/detail/string_utils.hpp>
#include <nlohmann/detail/value_t.hpp> #include <nlohmann/detail/value_t.hpp>
// the short-string fast path of the serializer assembles words from little-endian
// loads, see serializer::put_short_quoted()
#if defined(_MSC_VER) || defined(__x86_64__) || defined(__i386__) || (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
#define JSON_DUMP_SHORT_STRINGS 1
#else
#define JSON_DUMP_SHORT_STRINGS 0
#endif
NLOHMANN_JSON_NAMESPACE_BEGIN NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail namespace detail
{ {
@@ -80,11 +88,14 @@ class serializer
const std::size_t indent_step_ = 0, const std::size_t indent_step_ = 0,
error_handler_t error_handler_ = error_handler_t::strict) error_handler_t error_handler_ = error_handler_t::strict)
: o(&s) : o(&s)
, number_buffer() // NOLINT(readability-redundant-member-init)
, string_buffer() // NOLINT(readability-redundant-member-init)
, indent_char(ichar) , indent_char(ichar)
, pretty_print(pretty_print_) , pretty_print(pretty_print_)
, ensure_ascii(ensure_ascii_) , ensure_ascii(ensure_ascii_)
, indent_step(indent_step_) , indent_step(indent_step_)
, error_handler(error_handler_) , error_handler(error_handler_)
, write_buffer() // NOLINT(readability-redundant-member-init)
{} {}
// deleted because of pointer members // deleted because of pointer members
@@ -159,34 +170,27 @@ class serializer
if (pretty_print) if (pretty_print)
{ {
put_literal("{\n");
// variable to hold indentation for recursive calls // variable to hold indentation for recursive calls
const auto new_indent = next_indent(current_indent, indent_step); const auto new_indent = next_indent(current_indent, indent_step);
put_literal_indent("{\n", new_indent);
// first n-1 elements // first n-1 elements
auto i = val.m_data.m_value.object->cbegin(); 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) for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i)
{ {
put_indent(new_indent); put_quoted(i->first, "\": ");
put_char('"'); dump_element(i->second, new_indent, depth + 1);
dump_escaped(i->first); put_literal_indent(",\n", new_indent);
put_literal("\": ");
dump_internal(i->second, new_indent, depth + 1);
put_literal(",\n");
} }
// last element // last element
JSON_ASSERT(i != val.m_data.m_value.object->cend()); JSON_ASSERT(i != val.m_data.m_value.object->cend());
JSON_ASSERT(std::next(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_quoted(i->first, "\": ");
put_char('"'); dump_element(i->second, new_indent, depth + 1);
dump_escaped(i->first);
put_literal("\": ");
dump_internal(i->second, new_indent, depth + 1);
put_char('\n'); put_literal_indent("\n", current_indent);
put_indent(current_indent);
put_char('}'); put_char('}');
} }
else else
@@ -197,20 +201,16 @@ class serializer
auto i = val.m_data.m_value.object->cbegin(); 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) for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i)
{ {
put_char('"'); put_quoted(i->first, "\":");
dump_escaped(i->first); dump_element(i->second, current_indent, depth + 1);
put_literal("\":");
dump_internal(i->second, current_indent, depth + 1);
put_char(','); put_char(',');
} }
// last element // last element
JSON_ASSERT(i != val.m_data.m_value.object->cend()); JSON_ASSERT(i != val.m_data.m_value.object->cend());
JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend()); JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend());
put_char('"'); put_quoted(i->first, "\":");
dump_escaped(i->first); dump_element(i->second, current_indent, depth + 1);
put_literal("\":");
dump_internal(i->second, current_indent, depth + 1);
put_char('}'); put_char('}');
} }
@@ -234,27 +234,24 @@ class serializer
if (pretty_print) if (pretty_print)
{ {
put_literal("[\n");
// variable to hold indentation for recursive calls // variable to hold indentation for recursive calls
const auto new_indent = next_indent(current_indent, indent_step); const auto new_indent = next_indent(current_indent, indent_step);
put_literal_indent("[\n", new_indent);
// first n-1 elements // first n-1 elements
for (auto i = val.m_data.m_value.array->cbegin(); for (auto i = val.m_data.m_value.array->cbegin();
i != val.m_data.m_value.array->cend() - 1; ++i) i != val.m_data.m_value.array->cend() - 1; ++i)
{ {
put_indent(new_indent); dump_element(*i, new_indent, depth + 1);
dump_internal(*i, new_indent, depth + 1); put_literal_indent(",\n", new_indent);
put_literal(",\n");
} }
// last element // last element
JSON_ASSERT(!val.m_data.m_value.array->empty()); JSON_ASSERT(!val.m_data.m_value.array->empty());
put_indent(new_indent); dump_element(val.m_data.m_value.array->back(), new_indent, depth + 1);
dump_internal(val.m_data.m_value.array->back(), new_indent, depth + 1);
put_char('\n'); put_literal_indent("\n", current_indent);
put_indent(current_indent);
put_char(']'); put_char(']');
} }
else else
@@ -265,13 +262,13 @@ class serializer
for (auto i = val.m_data.m_value.array->cbegin(); for (auto i = val.m_data.m_value.array->cbegin();
i != val.m_data.m_value.array->cend() - 1; ++i) i != val.m_data.m_value.array->cend() - 1; ++i)
{ {
dump_internal(*i, current_indent, depth + 1); dump_element(*i, current_indent, depth + 1);
put_char(','); put_char(',');
} }
// last element // last element
JSON_ASSERT(!val.m_data.m_value.array->empty()); JSON_ASSERT(!val.m_data.m_value.array->empty());
dump_internal(val.m_data.m_value.array->back(), current_indent, depth + 1); dump_element(val.m_data.m_value.array->back(), current_indent, depth + 1);
put_char(']'); put_char(']');
} }
@@ -294,6 +291,27 @@ class serializer
} }
private: private:
/*!
@brief serialize an element of a container
Most elements are scalars, which need none of the state dump_internal() keeps
for a container; writing them from here spares each the entry and exit of
that function.
*/
JSON_HEDLEY_ALWAYS_INLINE void dump_element(const BasicJsonType& val,
const std::size_t current_indent,
const std::size_t depth)
{
if (val.m_data.m_type == value_t::object || val.m_data.m_type == value_t::array)
{
dump_internal(val, current_indent, depth);
}
else
{
dump_scalar(val, current_indent);
}
}
/*! /*!
@brief write out @a val and everything below it without the call stack @brief write out @a val and everything below it without the call stack
@@ -353,16 +371,13 @@ class serializer
put_indent(frame.child_indent); put_indent(frame.child_indent);
} }
put_char('"');
dump_escaped(frame.object_it->first);
if (pretty_print) if (pretty_print)
{ {
put_literal("\": "); put_quoted(frame.object_it->first, "\": ");
} }
else else
{ {
put_literal("\":"); put_quoted(frame.object_it->first, "\":");
} }
const BasicJsonType& element = frame.object_it->second; const BasicJsonType& element = frame.object_it->second;
@@ -537,78 +552,13 @@ class serializer
{ {
case value_t::string: case value_t::string:
{ {
put_char('"'); dump_string_value(*val.m_data.m_value.string);
dump_escaped(*val.m_data.m_value.string);
put_char('"');
return; return;
} }
case value_t::binary: case value_t::binary:
{ {
if (pretty_print) dump_binary_value(current_indent, *val.m_data.m_value.binary);
{
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; return;
} }
@@ -627,19 +577,19 @@ class serializer
case value_t::number_integer: case value_t::number_integer:
{ {
dump_integer(val.m_data.m_value.number_integer); dump_signed_value(val.m_data.m_value.number_integer);
return; return;
} }
case value_t::number_unsigned: case value_t::number_unsigned:
{ {
dump_integer(val.m_data.m_value.number_unsigned); dump_unsigned_value(val.m_data.m_value.number_unsigned);
return; return;
} }
case value_t::number_float: case value_t::number_float:
{ {
dump_float(val.m_data.m_value.number_float); dump_float_value(val.m_data.m_value.number_float);
return; return;
} }
@@ -662,6 +612,107 @@ class serializer
} }
} }
/// @brief write the string @a s in quotes; kept out of line, so that the
/// other scalars do not pay for its register needs (and the other way round)
JSON_HEDLEY_NEVER_INLINE void dump_string_value(const string_t& s)
{
#if JSON_DUMP_SHORT_STRINGS
if (s.size() <= short_string_max && put_short_quoted(s, "\""))
{
return;
}
#endif
put_char('"');
dump_escaped(s);
put_char('"');
}
// The numbers, and the rest of what dump_scalar() writes, are left to
// functions of their own as well: a single function holding all of them
// saves and restores every register any of them needs, on every call. Now
// dump_scalar() only dispatches, and each writer pays for what it uses.
JSON_HEDLEY_NEVER_INLINE void dump_signed_value(const number_integer_t x)
{
dump_integer(x);
}
JSON_HEDLEY_NEVER_INLINE void dump_unsigned_value(const number_unsigned_t x)
{
dump_integer(x);
}
JSON_HEDLEY_NEVER_INLINE void dump_float_value(const number_float_t x)
{
dump_float(x);
}
JSON_HEDLEY_NEVER_INLINE void dump_binary_value(const std::size_t current_indent, const typename BasicJsonType::binary_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 (!binary.empty())
{
for (auto i = binary.cbegin();
i != binary.cend() - 1; ++i)
{
dump_byte(*i);
put_literal(", ");
}
dump_byte(binary.back());
}
put_literal("],\n");
put_indent(new_indent);
put_literal("\"subtype\": ");
if (binary.has_subtype())
{
dump_integer(binary.subtype());
}
else
{
put_literal("null");
}
put_char('\n');
put_indent(current_indent);
put_char('}');
}
else
{
put_literal("{\"bytes\":[");
if (!binary.empty())
{
for (auto i = binary.cbegin();
i != binary.cend() - 1; ++i)
{
dump_byte(*i);
put_char(',');
}
dump_byte(binary.back());
}
put_literal("],\"subtype\":");
if (binary.has_subtype())
{
dump_integer(binary.subtype());
put_char('}');
}
else
{
put_literal("null}");
}
}
}
/*! /*!
@brief the indentation level to use for the children of the current value @brief the indentation level to use for the children of the current value
@@ -678,6 +729,217 @@ class serializer
return new_indent; return new_indent;
} }
private:
/*!
@brief append the string @a s in quotes, followed by @a tail
The quote, the string and @a tail (the closing quote, possibly with the
colon of an object key) are the bulk of what most documents consist of, and
most keys and many values are short and plain. Those are handled in one go
by put_short_quoted(); everything else takes the general path.
*/
template<std::size_t N>
void put_quoted(const string_t& s, const char (&tail)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
{
#if JSON_DUMP_SHORT_STRINGS
if (put_short_quoted(s, tail))
{
return;
}
#endif
put_char('"');
dump_escaped(s);
put_literal(tail);
}
#if JSON_DUMP_SHORT_STRINGS
/// the longest string put_short_quoted() takes
static constexpr std::size_t short_string_max = JSON_SCAN_VECTOR ? 32 : 16;
/// the high bit of every byte of @a v that is not printable ASCII or is a
/// quote, a backslash, or DEL (zero if the bytes can be copied as they are)
static std::uint64_t swar_not_plain(const std::uint64_t v) noexcept
{
constexpr std::uint64_t ones = 0x0101010101010101ull;
const std::uint64_t q = v ^ 0x2222222222222222ull; // '"'
const std::uint64_t b = v ^ 0x5C5C5C5C5C5C5C5Cull; // '\\'
return (((q - ones) & ~q) | ((b - ones) & ~b) // == '"', == '\\'
| ((v - 0x2020202020202020ull) & ~v) // < 0x20
| v | (v + ones)) // >= 0x7F
& 0x8080808080808080ull;
}
template<typename WordType>
static WordType load_word(const char* p) noexcept
{
WordType w;
std::memcpy(&w, p, sizeof(w));
return w;
}
template<typename WordType>
static void store_word(char* p, const WordType w) noexcept
{
std::memcpy(p, &w, sizeof(w));
}
/*!
@brief write `"`, @a s and @a tail if @a s is short and needs no escaping
A string of up to 16 (32 with vector instructions) printable ASCII
characters (no quote, backslash, or DEL) is read with two overlapping loads
and written with two overlapping stores, instead of scanning it and calling
memcpy with a length that is only known at run time. Returns false, having
changed nothing, if @a s does not qualify or the write buffer lacks the
room.
*/
template<std::size_t N>
bool put_short_quoted(const string_t& s, const char (&tail)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
{
const std::size_t n = s.size();
if (n > short_string_max || write_buffer_pos + n + N > write_buffer.size())
{
return false;
}
char* d = write_buffer.data() + write_buffer_pos + 1;
const char* const p = s.data();
if (n >= 8)
{
#if JSON_SCAN_VECTOR
if (!(n >= 16 ? copy_plain_vectors(d, p, n) : copy_plain_halves(d, p, n)))
{
return false;
}
#else
const auto a = load_word<std::uint64_t>(p);
const auto b = load_word<std::uint64_t>(p + n - 8);
if ((swar_not_plain(a) | swar_not_plain(b)) != 0)
{
return false;
}
store_word(d, a);
store_word(d + n - 8, b);
#endif
}
else if (n >= 4)
{
const auto a = load_word<std::uint32_t>(p);
const auto b = load_word<std::uint32_t>(p + n - 4);
// the bytes above the string are filled with a plain character
const std::uint64_t w = a | (static_cast<std::uint64_t>(b) << (8 * (n - 4)))
| (0x4141414141414141ull << (8 * n));
if (swar_not_plain(w) != 0)
{
return false;
}
store_word(d, a);
store_word(d + n - 4, b);
}
else if (n >= 2)
{
const auto a = load_word<std::uint16_t>(p);
const auto b = load_word<std::uint16_t>(p + n - 2);
const std::uint64_t w = a | (static_cast<std::uint64_t>(b) << (8 * (n - 2)))
| (0x4141414141414141ull << (8 * n));
if (swar_not_plain(w) != 0)
{
return false;
}
store_word(d, a);
store_word(d + n - 2, b);
}
else if (n == 1)
{
const std::uint64_t w = static_cast<std::uint64_t>(static_cast<unsigned char>(p[0])) | (0x4141414141414141ull << 8u);
if (swar_not_plain(w) != 0)
{
return false;
}
*d = p[0];
}
d[-1] = '"';
std::memcpy(d + n, tail, N - 1);
write_buffer_pos += n + N;
return true;
}
#if JSON_SCAN_VECTOR
/// copy the 8 to 15 bytes [@a p, @a p + @a n) to @a d with two overlapping
/// words, tested together as one vector, see copy_plain_vectors()
static bool copy_plain_halves(char* d, const char* p, const std::size_t n) noexcept
{
#if JSON_SCAN_NEON
const uint8x8_t lo = vld1_u8(reinterpret_cast<const std::uint8_t*>(p));
const uint8x8_t hi = vld1_u8(reinterpret_cast<const std::uint8_t*>(p + n - 8));
const uint8x16_t in = vcombine_u8(lo, hi);
const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))),
vorrq_u8(vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20)), vceqq_u8(in, vdupq_n_u8(0x7F))));
if (vmaxvq_u8(special) != 0)
{
return false;
}
vst1_u8(reinterpret_cast<std::uint8_t*>(d), lo);
vst1_u8(reinterpret_cast<std::uint8_t*>(d + n - 8), hi);
#else
const __m128i lo = _mm_loadl_epi64(static_cast<const __m128i*>(static_cast<const void*>(p))); // NOLINT(bugprone-casting-through-void)
const __m128i hi = _mm_loadl_epi64(static_cast<const __m128i*>(static_cast<const void*>(p + n - 8))); // NOLINT(bugprone-casting-through-void)
const __m128i in = _mm_unpacklo_epi64(lo, hi);
const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))),
_mm_or_si128(_mm_cmplt_epi8(in, _mm_set1_epi8(0x20)), _mm_cmpeq_epi8(in, _mm_set1_epi8(0x7F))));
if (_mm_movemask_epi8(special) != 0)
{
return false;
}
_mm_storel_epi64(static_cast<__m128i*>(static_cast<void*>(d)), lo); // NOLINT(bugprone-casting-through-void)
_mm_storel_epi64(static_cast<__m128i*>(static_cast<void*>(d + n - 8)), hi); // NOLINT(bugprone-casting-through-void)
#endif
return true;
}
/// copy the 16 to 32 bytes [@a p, @a p + @a n) to @a d with two overlapping
/// vectors, unless one of them is a quote, a backslash, a control
/// character, DEL or not ASCII
static bool copy_plain_vectors(char* d, const char* p, const std::size_t n) noexcept
{
#if JSON_SCAN_NEON
const uint8x16_t a = vld1q_u8(reinterpret_cast<const std::uint8_t*>(p));
const uint8x16_t b = vld1q_u8(reinterpret_cast<const std::uint8_t*>(p + n - 16));
const uint8x16_t special = vorrq_u8(vorrq_u8(vorrq_u8(vceqq_u8(a, vdupq_n_u8('"')), vceqq_u8(a, vdupq_n_u8('\\'))),
vorrq_u8(vceqq_u8(b, vdupq_n_u8('"')), vceqq_u8(b, vdupq_n_u8('\\')))),
vorrq_u8(vorrq_u8(vcltq_s8(vreinterpretq_s8_u8(a), vdupq_n_s8(0x20)), vcltq_s8(vreinterpretq_s8_u8(b), vdupq_n_s8(0x20))),
vorrq_u8(vceqq_u8(a, vdupq_n_u8(0x7F)), vceqq_u8(b, vdupq_n_u8(0x7F)))));
if (vmaxvq_u8(special) != 0)
{
return false;
}
vst1q_u8(reinterpret_cast<std::uint8_t*>(d), a);
vst1q_u8(reinterpret_cast<std::uint8_t*>(d + n - 16), b);
#else
const __m128i a = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(p))); // NOLINT(bugprone-casting-through-void)
const __m128i b = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(p + n - 16))); // NOLINT(bugprone-casting-through-void)
const __m128i quote = _mm_set1_epi8('"');
const __m128i backslash = _mm_set1_epi8('\\');
const __m128i space = _mm_set1_epi8(0x20);
const __m128i del = _mm_set1_epi8(0x7F);
const __m128i special = _mm_or_si128(_mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(a, quote), _mm_cmpeq_epi8(a, backslash)),
_mm_or_si128(_mm_cmpeq_epi8(b, quote), _mm_cmpeq_epi8(b, backslash))),
_mm_or_si128(_mm_or_si128(_mm_cmplt_epi8(a, space), _mm_cmplt_epi8(b, space)),
_mm_or_si128(_mm_cmpeq_epi8(a, del), _mm_cmpeq_epi8(b, del))));
if (_mm_movemask_epi8(special) != 0)
{
return false;
}
_mm_storeu_si128(static_cast<__m128i*>(static_cast<void*>(d)), a); // NOLINT(bugprone-casting-through-void)
_mm_storeu_si128(static_cast<__m128i*>(static_cast<void*>(d + n - 16)), b); // NOLINT(bugprone-casting-through-void)
#endif
return true;
}
#endif
#endif
JSON_PRIVATE_UNLESS_TESTED: JSON_PRIVATE_UNLESS_TESTED:
/*! /*!
@brief dump escaped string @brief dump escaped string
@@ -690,14 +952,23 @@ class serializer
@param[in] s the string to escape @param[in] s the string to escape
Complexity: Linear in the length of string @a s. Complexity: Linear in the length of string @a s.
@note kept out of line: with the short-string attempts in front of it gone
from here, compilers would otherwise inline it into @ref put_quoted
and then no longer into the loops that call that
*/ */
void dump_escaped(const string_t& s) JSON_HEDLEY_NEVER_INLINE void dump_escaped(const string_t& s)
{ {
// Short strings of printable ASCII - most object keys and many values - // Short strings of printable ASCII - most object keys and many values -
// are checked byte by byte against a table and then copied in one go. // are checked byte by byte against a table and then copied in one go.
// For them, this is cheaper than starting the vector scanner below, // For them, this is cheaper than starting the vector scanner below,
// whose setup costs more than such a string takes to scan. The same // whose setup costs more than such a string takes to scan. The same
// check serves both values of ensure_ascii. // check serves both values of ensure_ascii.
//
// Where put_short_quoted() exists and takes strings of up to 32 bytes,
// the callers have tried it already, and repeating the attempt only
// costs time for strings that need escaping or are not ASCII.
#if !(JSON_DUMP_SHORT_STRINGS && JSON_SCAN_VECTOR)
if (s.size() <= 32) if (s.size() <= 32)
{ {
const auto& copyable = ascii_copyable(); const auto& copyable = ascii_copyable();
@@ -719,6 +990,7 @@ class serializer
return; return;
} }
} }
#endif
// dispatch once here rather than test the flag inside the loop: it does // 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 // not change while a string is written, and folding it lets each of the
@@ -796,7 +1068,7 @@ class serializer
put_buffer(string_buffer, bytes); put_buffer(string_buffer, bytes);
bytes = 0; bytes = 0;
} }
put_string(s, i, i + run); put_run(s, i, run);
bytes_after_last_accept = 0; bytes_after_last_accept = 0;
undumped_chars = 0; undumped_chars = 0;
i += run; i += run;
@@ -836,6 +1108,27 @@ class serializer
const auto byte = static_cast<std::uint8_t>(s[i]); const auto byte = static_cast<std::uint8_t>(s[i]);
if (EnsureAscii && state == UTF8_ACCEPT && byte >= 0x80)
{
// a run of well-formed multi-byte characters is escaped
// straight into the write buffer; an ill-formed sequence
// ends the run and is left to the byte-at-a-time path
if (bytes != 0)
{
put_buffer(string_buffer, bytes);
bytes = 0;
bytes_after_last_accept = 0;
}
const std::size_t escaped = write_u_escapes(reinterpret_cast<const unsigned char*>(s.data()) + i, s.size() - i);
if (escaped != 0)
{
bytes_after_last_accept = 0;
undumped_chars = 0;
i += escaped - 1;
continue;
}
}
switch (decode(state, codepoint, byte)) switch (decode(state, codepoint, byte))
{ {
case UTF8_ACCEPT: // decode found a new code point case UTF8_ACCEPT: // decode found a new code point
@@ -1113,6 +1406,21 @@ class serializer
return; return;
} }
// the usual indentation: four words of the indentation character are
// stored unconditionally and only the part wanted is claimed, which
// spares the call of memset
if (JSON_HEDLEY_LIKELY(indent <= 32 && write_buffer_pos + 32 <= write_buffer.size()))
{
const std::uint64_t word = 0x0101010101010101ull * static_cast<unsigned char>(indent_char);
char* const d = write_buffer.data() + write_buffer_pos;
std::memcpy(d, &word, sizeof(word));
std::memcpy(d + 8, &word, sizeof(word));
std::memcpy(d + 16, &word, sizeof(word));
std::memcpy(d + 24, &word, sizeof(word));
write_buffer_pos += indent;
return;
}
const std::size_t capacity = write_buffer.size(); const std::size_t capacity = write_buffer.size();
// fill whatever room is left in the buffer; this is the whole job // fill whatever room is left in the buffer; this is the whole job
@@ -1146,6 +1454,32 @@ class serializer
write_buffer_pos = indent; write_buffer_pos = indent;
} }
/*!
@brief append a string literal and then @a indent indentation characters
The usual line break and indentation go into the buffer with one check of
its room and four stores of the indentation, see @ref put_indent.
*/
template<std::size_t N>
JSON_HEDLEY_ALWAYS_INLINE void put_literal_indent(const char (&s)[N], const std::size_t indent) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
{
constexpr std::size_t length = N - 1;
if (JSON_HEDLEY_LIKELY(indent <= 32 && write_buffer_pos + length + 32 <= write_buffer.size()))
{
const std::uint64_t word = 0x0101010101010101ull * static_cast<unsigned char>(indent_char);
char* const d = write_buffer.data() + write_buffer_pos;
std::memcpy(d, s, length);
std::memcpy(d + length, &word, sizeof(word));
std::memcpy(d + length + 8, &word, sizeof(word));
std::memcpy(d + length + 16, &word, sizeof(word));
std::memcpy(d + length + 24, &word, sizeof(word));
write_buffer_pos += length + indent;
return;
}
put_literal(s);
put_indent(indent);
}
/*! /*!
@brief append a string literal to the write buffer @brief append a string literal to the write buffer
@@ -1170,6 +1504,28 @@ class serializer
write_buffer_pos += length; write_buffer_pos += length;
} }
/*!
@brief append the characters of @a str in [@a start, @a start + @a length)
A run of up to 16 characters with 16 or more in the string from @a start on
and in the write buffer from its end on is copied as a whole block of 16,
which spares the call of memcpy with a length known only at run time; the
surplus characters in the buffer are overwritten by what follows.
*/
template<typename StringType>
void put_run(const StringType& str, const std::size_t start, const std::size_t length)
{
// a short run is the exception in text without escapes (CJK, accented
// Latin), for which this keeps the long path the fast one
if (JSON_HEDLEY_UNLIKELY(length <= 16 && start + 16 <= str.size() && write_buffer_pos + 16 <= write_buffer.size()))
{
std::memcpy(write_buffer.data() + write_buffer_pos, str.data() + start, 16);
write_buffer_pos += length;
return;
}
put_string(str, start, start + length);
}
/*! /*!
@brief append the characters of @a str in [@a start, @a end) @brief append the characters of @a str in [@a start, @a end)
@@ -1412,6 +1768,75 @@ class serializer
return escapes; return escapes;
} }
/*!
@brief escape the well-formed multi-byte characters at the start of @a data
Writes the "\\uXXXX" escape of each character (two of them for a character
above U+FFFF) straight into the write buffer, which is why the caller must
have written out what is pending in @ref string_buffer. Stops at the first
byte that is below 0x80 or starts an ill-formed or truncated sequence, which
the caller handles.
@return the number of bytes of @a data that have been escaped
*/
JSON_HEDLEY_NEVER_INLINE std::size_t write_u_escapes(const unsigned char* data, std::size_t n)
{
constexpr const char* nibble_to_hex = "0123456789abcdef";
std::size_t i = 0;
while (i < n && data[i] >= 0x80)
{
const std::size_t length = validate_one_utf8(data + i, n - i);
if (length == 0)
{
break;
}
const auto b0 = static_cast<std::uint32_t>(data[i]);
const auto b1 = static_cast<std::uint32_t>(data[i + 1]);
std::uint32_t codepoint = 0;
switch (length)
{
case 2:
codepoint = ((b0 & 0x1Fu) << 6u) | (b1 & 0x3Fu);
break;
case 3:
codepoint = ((b0 & 0x0Fu) << 12u) | ((b1 & 0x3Fu) << 6u) | (static_cast<std::uint32_t>(data[i + 2]) & 0x3Fu);
break;
default:
codepoint = ((b0 & 0x07u) << 18u) | ((b1 & 0x3Fu) << 12u) | ((static_cast<std::uint32_t>(data[i + 2]) & 0x3Fu) << 6u) | (static_cast<std::uint32_t>(data[i + 3]) & 0x3Fu);
break;
}
// at most two escapes of six bytes each
if (JSON_HEDLEY_UNLIKELY(write_buffer.size() - write_buffer_pos < 12))
{
flush();
}
char* d = write_buffer.data() + write_buffer_pos;
if (codepoint > 0xFFFFu)
{
d = put_u_escape(d, static_cast<std::uint16_t>(0xD7C0u + (codepoint >> 10u)), nibble_to_hex);
codepoint = 0xDC00u + (codepoint & 0x3FFu);
}
d = put_u_escape(d, static_cast<std::uint16_t>(codepoint), nibble_to_hex);
write_buffer_pos = static_cast<std::size_t>(d - write_buffer.data());
i += length;
}
return i;
}
static char* put_u_escape(char* d, const std::uint16_t codeunit, const char* nibble_to_hex) noexcept
{
const auto unit = static_cast<std::uint32_t>(codeunit);
d[0] = '\\';
d[1] = 'u';
d[2] = nibble_to_hex[(unit >> 12u) & 0x0Fu];
d[3] = nibble_to_hex[(unit >> 8u) & 0x0Fu];
d[4] = nibble_to_hex[(unit >> 4u) & 0x0Fu];
d[5] = nibble_to_hex[unit & 0x0Fu];
return d + 6;
}
/*! /*!
* @brief write a lowercase "\\uXXXX" escape sequence into @a string_buffer * @brief write a lowercase "\\uXXXX" escape sequence into @a string_buffer
* *
@@ -1769,11 +2194,22 @@ class serializer
/// the output of the serializer (non-owning; the adapter lives at the call site) /// the output of the serializer (non-owning; the adapter lives at the call site)
output_adapter_protocol<char>* o = nullptr; output_adapter_protocol<char>* o = nullptr;
/// a character buffer left uninitialized: the buffers below are written
/// before they are read (only the part written is ever read), and zeroing
/// 1.6 KiB took more than half of what dumping a small value costs (the
/// user-provided constructor keeps the member initialization, which
/// -Weffc++ asks for, from zeroing them)
template<std::size_t N>
struct uninitialized_buffer : std::array<char, N>
{
uninitialized_buffer() noexcept {} // NOLINT(modernize-use-equals-default,hicpp-use-equals-default,cppcoreguidelines-pro-type-member-init,hicpp-member-init): see above
};
/// a (hopefully) large enough character buffer /// a (hopefully) large enough character buffer
std::array<char, 64> number_buffer{{}}; uninitialized_buffer<64> number_buffer;
/// string buffer /// string buffer
std::array<char, 512> string_buffer{{}}; uninitialized_buffer<512> string_buffer;
/// the indentation character /// the indentation character
const char indent_char; const char indent_char;
@@ -1793,7 +2229,7 @@ class serializer
/// buffer collecting output before it is flushed to the output adapter, so /// buffer collecting output before it is flushed to the output adapter, so
/// that the many small structural writes become few bulk writes /// that the many small structural writes become few bulk writes
static constexpr std::size_t write_buffer_size = 1024; static constexpr std::size_t write_buffer_size = 1024;
std::array<char, write_buffer_size> write_buffer{{}}; uninitialized_buffer<write_buffer_size> write_buffer;
/// number of valid bytes currently held in @ref write_buffer /// number of valid bytes currently held in @ref write_buffer
std::size_t write_buffer_pos = 0; std::size_t write_buffer_pos = 0;
}; };
+18 -42
View File
@@ -6321,7 +6321,7 @@ public:
const error_handler_t error_handler = detail::binary_writer_default_error_handler()) const error_handler_t error_handler = detail::binary_writer_default_error_handler())
{ {
std::vector<std::uint8_t> result; std::vector<std::uint8_t> result;
result.reserve(detail::binary_initial_capacity(j)); result.reserve(detail::binary_reserve_hint(j));
vector_writer(result, error_handler).write_cbor(j); vector_writer(result, error_handler).write_cbor(j);
return result; return result;
} }
@@ -6331,9 +6331,7 @@ public:
static void to_cbor(const basic_json& j, detail::output_adapter<std::uint8_t> o, static void to_cbor(const basic_json& j, detail::output_adapter<std::uint8_t> o,
const error_handler_t error_handler = detail::binary_writer_default_error_handler()) const error_handler_t error_handler = detail::binary_writer_default_error_handler())
{ {
binary_writer<std::uint8_t> writer(o, error_handler); binary_writer<std::uint8_t>(o, error_handler).write_cbor(j);
writer.write_cbor(j);
writer.flush();
} }
/// @brief create a CBOR serialization of a given JSON value /// @brief create a CBOR serialization of a given JSON value
@@ -6341,9 +6339,7 @@ public:
static void to_cbor(const basic_json& j, detail::output_adapter<char> o, static void to_cbor(const basic_json& j, detail::output_adapter<char> o,
const error_handler_t error_handler = detail::binary_writer_default_error_handler()) const error_handler_t error_handler = detail::binary_writer_default_error_handler())
{ {
binary_writer<char> writer(o, error_handler); binary_writer<char>(o, error_handler).write_cbor(j);
writer.write_cbor(j);
writer.flush();
} }
/// @brief create a MessagePack serialization of a given JSON value /// @brief create a MessagePack serialization of a given JSON value
@@ -6352,7 +6348,7 @@ public:
const error_handler_t error_handler = error_handler_t::keep) const error_handler_t error_handler = error_handler_t::keep)
{ {
std::vector<std::uint8_t> result; std::vector<std::uint8_t> result;
result.reserve(detail::binary_initial_capacity(j)); result.reserve(detail::binary_reserve_hint(j));
vector_writer(result, error_handler).write_msgpack(j); vector_writer(result, error_handler).write_msgpack(j);
return result; return result;
} }
@@ -6362,9 +6358,7 @@ public:
static void to_msgpack(const basic_json& j, detail::output_adapter<std::uint8_t> o, static void to_msgpack(const basic_json& j, detail::output_adapter<std::uint8_t> o,
const error_handler_t error_handler = error_handler_t::keep) const error_handler_t error_handler = error_handler_t::keep)
{ {
binary_writer<std::uint8_t> writer(o, error_handler); binary_writer<std::uint8_t>(o, error_handler).write_msgpack(j);
writer.write_msgpack(j);
writer.flush();
} }
/// @brief create a MessagePack serialization of a given JSON value /// @brief create a MessagePack serialization of a given JSON value
@@ -6372,9 +6366,7 @@ public:
static void to_msgpack(const basic_json& j, detail::output_adapter<char> o, static void to_msgpack(const basic_json& j, detail::output_adapter<char> o,
const error_handler_t error_handler = error_handler_t::keep) const error_handler_t error_handler = error_handler_t::keep)
{ {
binary_writer<char> writer(o, error_handler); binary_writer<char>(o, error_handler).write_msgpack(j);
writer.write_msgpack(j);
writer.flush();
} }
/// @brief create a UBJSON serialization of a given JSON value /// @brief create a UBJSON serialization of a given JSON value
@@ -6385,7 +6377,7 @@ public:
const error_handler_t error_handler = detail::binary_writer_default_error_handler()) const error_handler_t error_handler = detail::binary_writer_default_error_handler())
{ {
std::vector<std::uint8_t> result; std::vector<std::uint8_t> result;
result.reserve(detail::binary_initial_capacity(j)); result.reserve(detail::binary_reserve_hint(j));
vector_writer(result, error_handler).write_ubjson(j, use_size, use_type); vector_writer(result, error_handler).write_ubjson(j, use_size, use_type);
return result; return result;
} }
@@ -6396,9 +6388,7 @@ public:
const bool use_size = false, const bool use_type = false, const bool use_size = false, const bool use_type = false,
const error_handler_t error_handler = detail::binary_writer_default_error_handler()) const error_handler_t error_handler = detail::binary_writer_default_error_handler())
{ {
binary_writer<std::uint8_t> writer(o, error_handler); binary_writer<std::uint8_t>(o, error_handler).write_ubjson(j, use_size, use_type);
writer.write_ubjson(j, use_size, use_type);
writer.flush();
} }
/// @brief create a UBJSON serialization of a given JSON value /// @brief create a UBJSON serialization of a given JSON value
@@ -6407,9 +6397,7 @@ public:
const bool use_size = false, const bool use_type = false, const bool use_size = false, const bool use_type = false,
const error_handler_t error_handler = detail::binary_writer_default_error_handler()) const error_handler_t error_handler = detail::binary_writer_default_error_handler())
{ {
binary_writer<char> writer(o, error_handler); binary_writer<char>(o, error_handler).write_ubjson(j, use_size, use_type);
writer.write_ubjson(j, use_size, use_type);
writer.flush();
} }
/// @brief create a BJData serialization of a given JSON value /// @brief create a BJData serialization of a given JSON value
@@ -6421,7 +6409,7 @@ public:
const error_handler_t error_handler = detail::binary_writer_default_error_handler()) const error_handler_t error_handler = detail::binary_writer_default_error_handler())
{ {
std::vector<std::uint8_t> result; std::vector<std::uint8_t> result;
result.reserve(detail::binary_initial_capacity(j)); result.reserve(detail::binary_reserve_hint(j));
vector_writer(result, error_handler).write_ubjson(j, use_size, use_type, true, true, version); vector_writer(result, error_handler).write_ubjson(j, use_size, use_type, true, true, version);
return result; return result;
} }
@@ -6433,9 +6421,7 @@ public:
const bjdata_version_t version = bjdata_version_t::draft2, const bjdata_version_t version = bjdata_version_t::draft2,
const error_handler_t error_handler = detail::binary_writer_default_error_handler()) const error_handler_t error_handler = detail::binary_writer_default_error_handler())
{ {
binary_writer<std::uint8_t> writer(o, error_handler); binary_writer<std::uint8_t>(o, error_handler).write_ubjson(j, use_size, use_type, true, true, version);
writer.write_ubjson(j, use_size, use_type, true, true, version);
writer.flush();
} }
/// @brief create a BJData serialization of a given JSON value /// @brief create a BJData serialization of a given JSON value
@@ -6445,9 +6431,7 @@ public:
const bjdata_version_t version = bjdata_version_t::draft2, const bjdata_version_t version = bjdata_version_t::draft2,
const error_handler_t error_handler = detail::binary_writer_default_error_handler()) const error_handler_t error_handler = detail::binary_writer_default_error_handler())
{ {
binary_writer<char> writer(o, error_handler); binary_writer<char>(o, error_handler).write_ubjson(j, use_size, use_type, true, true, version);
writer.write_ubjson(j, use_size, use_type, true, true, version);
writer.flush();
} }
/// @brief create a BSON serialization of a given JSON value /// @brief create a BSON serialization of a given JSON value
@@ -6456,7 +6440,7 @@ public:
const error_handler_t error_handler = detail::binary_writer_default_error_handler()) const error_handler_t error_handler = detail::binary_writer_default_error_handler())
{ {
std::vector<std::uint8_t> result; std::vector<std::uint8_t> result;
result.reserve(detail::binary_initial_capacity(j)); result.reserve(detail::binary_reserve_hint(j));
vector_writer(result, error_handler).write_bson(j); vector_writer(result, error_handler).write_bson(j);
return result; return result;
} }
@@ -6466,9 +6450,7 @@ public:
static void to_bson(const basic_json& j, detail::output_adapter<std::uint8_t> o, static void to_bson(const basic_json& j, detail::output_adapter<std::uint8_t> o,
const error_handler_t error_handler = detail::binary_writer_default_error_handler()) const error_handler_t error_handler = detail::binary_writer_default_error_handler())
{ {
binary_writer<std::uint8_t> writer(o, error_handler); binary_writer<std::uint8_t>(o, error_handler).write_bson(j);
writer.write_bson(j);
writer.flush();
} }
/// @brief create a BSON serialization of a given JSON value /// @brief create a BSON serialization of a given JSON value
@@ -6476,9 +6458,7 @@ public:
static void to_bson(const basic_json& j, detail::output_adapter<char> o, static void to_bson(const basic_json& j, detail::output_adapter<char> o,
const error_handler_t error_handler = detail::binary_writer_default_error_handler()) const error_handler_t error_handler = detail::binary_writer_default_error_handler())
{ {
binary_writer<char> writer(o, error_handler); binary_writer<char>(o, error_handler).write_bson(j);
writer.write_bson(j);
writer.flush();
} }
/// @brief create a BON8 serialization of a given JSON value /// @brief create a BON8 serialization of a given JSON value
@@ -6486,7 +6466,7 @@ public:
static std::vector<std::uint8_t> to_bon8(const basic_json& j) static std::vector<std::uint8_t> to_bon8(const basic_json& j)
{ {
std::vector<std::uint8_t> result; std::vector<std::uint8_t> result;
result.reserve(detail::binary_initial_capacity(j)); result.reserve(detail::binary_reserve_hint(j));
vector_writer(result).write_bon8(j); vector_writer(result).write_bon8(j);
return result; return result;
} }
@@ -6495,18 +6475,14 @@ public:
/// @sa https://json.nlohmann.me/api/basic_json/to_bon8/ /// @sa https://json.nlohmann.me/api/basic_json/to_bon8/
static void to_bon8(const basic_json& j, detail::output_adapter<std::uint8_t> o) static void to_bon8(const basic_json& j, detail::output_adapter<std::uint8_t> o)
{ {
binary_writer<std::uint8_t> writer(o); binary_writer<std::uint8_t>(o).write_bon8(j);
writer.write_bon8(j);
writer.flush();
} }
/// @brief create a BON8 serialization of a given JSON value /// @brief create a BON8 serialization of a given JSON value
/// @sa https://json.nlohmann.me/api/basic_json/to_bon8/ /// @sa https://json.nlohmann.me/api/basic_json/to_bon8/
static void to_bon8(const basic_json& j, detail::output_adapter<char> o) static void to_bon8(const basic_json& j, detail::output_adapter<char> o)
{ {
binary_writer<char> writer(o); binary_writer<char>(o).write_bon8(j);
writer.write_bon8(j);
writer.flush();
} }
/// @brief create a JSON value from an input in CBOR format /// @brief create a JSON value from an input in CBOR format
+757 -2075
View File
File diff suppressed because it is too large. Load diff
-272
View File
@@ -492,275 +492,3 @@ TEST_CASE("binary_reserve_hint never over-reserves")
CHECK(nlohmann::detail::binary_reserve_hint(json({{"a", 1}, {"b", 2}})) == 5); CHECK(nlohmann::detail::binary_reserve_hint(json({{"a", 1}, {"b", 2}})) == 5);
} }
} }
namespace
{
// numbers around every boundary of the integer encodings, and floats of each
// kind (exact in float, needing double, special values)
std::vector<json> boundary_numbers()
{
std::vector<json> numbers;
for (int k = 0; k < 64; ++k)
{
const std::uint64_t p = std::uint64_t{1} << k;
for (const std::uint64_t d :
{
p - 1, p, p + 1
})
{
numbers.emplace_back(d);
if (d <= static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()))
{
numbers.emplace_back(static_cast<std::int64_t>(d));
numbers.emplace_back(-static_cast<std::int64_t>(d));
numbers.emplace_back(-static_cast<std::int64_t>(d) - 1);
}
}
}
numbers.emplace_back((std::numeric_limits<std::int64_t>::min)());
numbers.emplace_back((std::numeric_limits<std::uint64_t>::max)());
for (const double f :
{
0.0, -0.0, 1.0, -1.0, 0.5, 0.1, 3.25, 1e300, -1e-300, 3.4028234663852886e38, 1e39,
std::numeric_limits<double>::infinity(), -std::numeric_limits<double>::infinity(),
std::numeric_limits<double>::quiet_NaN()
})
{
numbers.emplace_back(f);
}
return numbers;
}
// the concatenation of the encodings of the elements of @a numbers[first, first + count)
template<typename Single>
std::vector<std::uint8_t> concat_singles(const std::vector<json>& numbers, const std::size_t first, const std::size_t count, Single single)
{
std::vector<std::uint8_t> bytes;
for (std::size_t i = first; i < first + count; ++i)
{
const auto one = single(numbers[i]);
bytes.insert(bytes.end(), one.begin(), one.end());
}
return bytes;
}
} // namespace
// Runs of numbers in arrays are encoded straight into space reserved from the
// sink, by code that is separate from the encoding of a single value. Both have
// to agree on every number: an array is its head and the encodings of its
// elements, one after the other.
TEST_CASE("runs of numbers in arrays are encoded as the numbers one by one")
{
const std::vector<json> numbers = boundary_numbers();
// arrays of every length that a one-byte head covers, so that runs are as
// long as, and longer and shorter than, what fits one reservation
for (const std::size_t count :
{
1u, 2u, 3u, 5u, 14u, 15u, 23u
})
{
for (std::size_t first = 0; first + count <= numbers.size(); first += 7)
{
const json array(std::vector<json>(numbers.begin() + static_cast<std::ptrdiff_t>(first),
numbers.begin() + static_cast<std::ptrdiff_t>(first + count)));
CAPTURE(array.dump(-1, ' ', false, json::error_handler_t::replace))
std::vector<std::uint8_t> expected{static_cast<std::uint8_t>(0x80 + count)};
auto elements = concat_singles(numbers, first, count, [](const json & j)
{
return json::to_cbor(j);
});
expected.insert(expected.end(), elements.begin(), elements.end());
CHECK(json::to_cbor(array) == expected);
expected.assign(1, static_cast<std::uint8_t>(count <= 15 ? 0x90 + count : 0xDC));
if (count > 15)
{
expected.push_back(0);
expected.push_back(static_cast<std::uint8_t>(count));
}
elements = concat_singles(numbers, first, count, [](const json & j)
{
return json::to_msgpack(j);
});
expected.insert(expected.end(), elements.begin(), elements.end());
CHECK(json::to_msgpack(array) == expected);
for (const bool bjdata :
{
false, true
})
{
expected.assign(1, static_cast<std::uint8_t>('['));
elements = concat_singles(numbers, first, count, [bjdata](const json & j)
{
return bjdata ? json::to_bjdata(j) : json::to_ubjson(j);
});
expected.insert(expected.end(), elements.begin(), elements.end());
expected.push_back(static_cast<std::uint8_t>(']'));
CHECK((bjdata ? json::to_bjdata(array) : json::to_ubjson(array)) == expected);
}
// BON8 has no integers above the int64 range
bool representable = true;
for (std::size_t i = first; i < first + count; ++i)
{
representable = representable && bon8_representable(numbers[i]);
}
if (representable)
{
expected.assign(1, static_cast<std::uint8_t>(count <= 4 ? 0x80 + count : 0x85));
elements = concat_singles(numbers, first, count, [](const json & j)
{
return json::to_bon8(j);
});
expected.insert(expected.end(), elements.begin(), elements.end());
if (count > 4)
{
expected.push_back(0xFE);
}
CHECK(json::to_bon8(array) == expected);
}
else
{
CHECK_THROWS_AS(json::to_bon8(array), json::out_of_range&);
}
}
}
}
// An array that is written in more than one reservation (more numbers than fit
// one), and arrays of an index with more digits, round trip for every format.
TEST_CASE("large arrays and many digits of indices round trip")
{
json array = json::array();
for (int i = 0; i < 12000; ++i)
{
switch (i % 4)
{
case 0:
array.push_back(i);
break;
case 1:
array.push_back(-i);
break;
case 2:
array.push_back(i * 0.25);
break;
default:
array.push_back(i % 8 == 3);
break;
}
}
json document = json::object();
document["numbers"] = array;
document["nested"] = json::array({array, "text", array});
CHECK(json::from_cbor(json::to_cbor(document)) == document);
CHECK(json::from_msgpack(json::to_msgpack(document)) == document);
CHECK(json::from_ubjson(json::to_ubjson(document)) == document);
CHECK(json::from_ubjson(json::to_ubjson(document, true, true)) == document);
CHECK(json::from_bjdata(json::to_bjdata(document)) == document);
CHECK(json::from_bson(json::to_bson(document)) == document);
CHECK(json::from_bon8(json::to_bon8(document)) == document);
}
// Strings and keys are written with their head in one reservation up to a
// length; around that length, and with every kind of head, they must read back.
TEST_CASE("strings of every head length round trip")
{
json document = json::object();
json array = json::array();
for (const std::size_t length :
{
0u, 1u, 2u, 3u, 4u, 7u, 8u, 15u, 16u, 17u, 23u, 24u, 25u, 31u, 32u, 33u, 63u, 127u, 128u, 129u, 200u, 245u, 246u, 247u, 248u, 249u, 250u, 251u, 252u, 253u, 254u, 255u, 256u, 257u, 300u, 1000u, 65535u, 65536u, 70000u
})
{
const std::string text(length, 'x');
array.push_back(text);
document[std::string(length, 'k')] = text;
}
document["array"] = array;
CHECK(json::from_cbor(json::to_cbor(document)) == document);
CHECK(json::from_msgpack(json::to_msgpack(document)) == document);
CHECK(json::from_ubjson(json::to_ubjson(document)) == document);
CHECK(json::from_ubjson(json::to_ubjson(document, true, true)) == document);
CHECK(json::from_bjdata(json::to_bjdata(document)) == document);
CHECK(json::from_bson(json::to_bson(document)) == document);
CHECK(json::from_bon8(json::to_bon8(document)) == document);
}
// What a write that throws leaves behind in an output adapter is what was written
// before it, whatever the adapter and however the adapter sink buffers.
TEST_CASE("a write that throws leaves the same bytes in every kind of output")
{
json array = json::array();
for (int i = 0; i < 700; ++i)
{
array.push_back(i * 1000);
array.push_back("text " + std::to_string(i));
}
array.push_back("bad \xFF byte");
json document = json::object();
document["first"] = array;
document["second"] = 1;
const auto strict = json::error_handler_t::strict;
SECTION("cbor")
{
std::vector<std::uint8_t> as_vector;
std::string as_string;
std::ostringstream as_stream;
CHECK_THROWS_AS(json::to_cbor(document, as_vector, strict), json::type_error&);
CHECK_THROWS_AS(json::to_cbor(document, as_string, strict), json::type_error&);
CHECK_THROWS_AS(json::to_cbor(document, as_stream, strict), json::type_error&);
CHECK(!as_vector.empty());
CHECK(std::string(as_vector.begin(), as_vector.end()) == as_string);
CHECK(as_stream.str() == as_string);
}
SECTION("msgpack")
{
std::vector<std::uint8_t> as_vector;
std::string as_string;
std::ostringstream as_stream;
CHECK_THROWS_AS(json::to_msgpack(document, as_vector, strict), json::type_error&);
CHECK_THROWS_AS(json::to_msgpack(document, as_string, strict), json::type_error&);
CHECK_THROWS_AS(json::to_msgpack(document, as_stream, strict), json::type_error&);
CHECK(!as_vector.empty());
CHECK(std::string(as_vector.begin(), as_vector.end()) == as_string);
CHECK(as_stream.str() == as_string);
}
SECTION("ubjson")
{
std::vector<std::uint8_t> as_vector;
std::string as_string;
std::ostringstream as_stream;
CHECK_THROWS_AS(json::to_ubjson(document, as_vector, false, false, strict), json::type_error&);
CHECK_THROWS_AS(json::to_ubjson(document, as_string, false, false, strict), json::type_error&);
CHECK_THROWS_AS(json::to_ubjson(document, as_stream, false, false, strict), json::type_error&);
CHECK(!as_vector.empty());
CHECK(std::string(as_vector.begin(), as_vector.end()) == as_string);
CHECK(as_stream.str() == as_string);
}
SECTION("bson")
{
// the size pass finds the string before anything is written
std::vector<std::uint8_t> as_vector;
std::string as_string;
std::ostringstream as_stream;
CHECK_THROWS_AS(json::to_bson(document, as_vector, strict), json::type_error&);
CHECK_THROWS_AS(json::to_bson(document, as_string, strict), json::type_error&);
CHECK_THROWS_AS(json::to_bson(document, as_stream, strict), json::type_error&);
CHECK(as_vector.empty());
CHECK(as_string.empty());
CHECK(as_stream.str().empty());
}
}
-49
View File
@@ -285,55 +285,6 @@ TEST_CASE("custom number types: integers beyond 64 bits")
CHECK(wide_json::to_bjdata(wide_json::array({uint_big, uint_big}), true, true) == expected_bjdata_array); CHECK(wide_json::to_bjdata(wide_json::array({uint_big, uint_big}), true, true) == expected_bjdata_array);
} }
SECTION("integers in arrays and objects")
{
// runs of numbers and scalar object members are encoded by loops of
// their own for the number types up to 64 bits; a wider number type
// takes the generic path, which checks the range of every integer
const wide_json run = wide_json::array({make_int(1), make_int(-3), make_uint(7), wide_json(1.5), int_big, make_int(5)});
const wide_json run_negative = wide_json::array({make_int(1), make_int(-3), int_big_negative});
const wide_json run_unsigned = wide_json::array({make_uint(1), make_uint(7), uint_big});
const wide_json members = wide_json::object({{"a", make_int(1)}, {"b", wide_json(1.5)}, {"c", int_big}});
const std::vector<wide_json> failing = {run, run_negative, run_unsigned, members, wrap(run), wrap(members)};
for (const auto& value : failing)
{
CHECK_THROWS_AS(_ = wide_json::to_cbor(value), out_of_range&);
CHECK_THROWS_AS(_ = wide_json::to_msgpack(value), out_of_range&);
CHECK_THROWS_AS(_ = wide_json::to_bson(wrap(value)), out_of_range&);
CHECK_THROWS_AS(_ = wide_json::to_bon8(value), out_of_range&);
}
// a failing write leaves no more than the elements before the failing one
std::vector<std::uint8_t> out;
CHECK_THROWS_AS(wide_json::to_cbor(run, out), out_of_range&);
// (the head of the array, 1, -3, 7, and 1.5 as a single-precision float)
CHECK(out == std::vector<std::uint8_t>({0x86, 0x01, 0x22, 0x07, 0xFA, 0x3F, 0xC0, 0x00, 0x00}));
out.clear();
CHECK_THROWS_AS(wide_json::to_bon8(run, out), out_of_range&);
CHECK(!out.empty());
// the same values that fit are written exactly as with 64-bit number types
const wide_json fits = wide_json::array({make_int(1), make_int(-3), make_uint(7), wide_json(1.5), make_int(min_int64), make_int(max_int64), make_int(200), make_int(-70000)});
const nlohmann::json fits64 = nlohmann::json::array({1, -3, 7, 1.5, (std::numeric_limits<std::int64_t>::min)(), (std::numeric_limits<std::int64_t>::max)(), 200, -70000});
CHECK(wide_json::to_cbor(fits) == nlohmann::json::to_cbor(fits64));
CHECK(wide_json::to_msgpack(fits) == nlohmann::json::to_msgpack(fits64));
CHECK(wide_json::to_ubjson(fits) == nlohmann::json::to_ubjson(fits64));
CHECK(wide_json::to_ubjson(fits, true, true) == nlohmann::json::to_ubjson(fits64, true, true));
CHECK(wide_json::to_bjdata(fits) == nlohmann::json::to_bjdata(fits64));
CHECK(wide_json::to_bjdata(fits, true, true) == nlohmann::json::to_bjdata(fits64, true, true));
CHECK(wide_json::to_bon8(fits) == nlohmann::json::to_bon8(fits64));
CHECK(wide_json::to_bson(wrap(fits)) == nlohmann::json::to_bson(nlohmann::json::object({{"v", fits64}})));
const wide_json fit_members = wide_json::object({{"a", make_int(1)}, {"b", make_int(-300)}, {"c", make_uint(max_uint64)}, {"d", wide_json(2.5)}});
const nlohmann::json fit_members64 = nlohmann::json::object({{"a", 1}, {"b", -300}, {"c", (std::numeric_limits<std::uint64_t>::max)()}, {"d", 2.5}});
CHECK(wide_json::to_cbor(fit_members) == nlohmann::json::to_cbor(fit_members64));
CHECK(wide_json::to_msgpack(fit_members) == nlohmann::json::to_msgpack(fit_members64));
CHECK(wide_json::to_ubjson(fit_members) == nlohmann::json::to_ubjson(fit_members64));
CHECK(wide_json::to_bjdata(fit_members) == nlohmann::json::to_bjdata(fit_members64));
CHECK(wide_json::to_bson(fit_members) == nlohmann::json::to_bson(fit_members64));
}
SECTION("BJData ND-array") SECTION("BJData ND-array")
{ {
// an ND-array element beyond 64 bits does not fit any dtype, so the // an ND-array element beyond 64 bits does not fit any dtype, so the
+91
View File
@@ -1580,3 +1580,94 @@ TEST_CASE("invalid UTF-8 at every position is handled like the reference")
} }
} }
} }
TEST_CASE("short strings are written like the reference, as values and as keys")
{
// strings of up to 16 characters (32 with vector instructions) that need no
// escaping are written as a whole (together with the quotes and, for a key,
// the colon); everything else, including a string that is merely short,
// takes the general path.
// Every length around the edges of the size classes of the fast path is
// combined with each kind of byte that must send a string to the general
// path, at each position, and compared with the reference for values and,
// via the value, for keys.
const std::vector<std::string> specials =
{
"\"", "\\", "\n", "\x1f", "\x7f", "\xc3\xa9", "\xe2\x82\xac", "\xf0\x9f\x98\x80", "\xff", "\x80", "\xc3"
};
for (std::size_t length = 0; length <= 36; ++length)
{
const std::string plain(length, 'k');
check_dump_against_reference(plain, false);
for (const auto& special : specials)
{
for (std::size_t pos = 0; pos <= length; ++pos)
{
std::string s = plain;
s.insert(pos, special);
check_dump_against_reference(s, false);
for (const bool ensure_ascii :
{
false, true
})
{
// a key and a value are written alike, in compact and
// in pretty-printed form
const json value = s;
const json object = json::object({{s, 1}});
for (const auto error_handler :
{
json::error_handler_t::replace, json::error_handler_t::keep
})
{
const std::string quoted = value.dump(-1, ' ', ensure_ascii, error_handler);
CHECK(object.dump(-1, ' ', ensure_ascii, error_handler) == "{" + quoted + ":1}");
CHECK(object.dump(2, ' ', ensure_ascii, error_handler) == "{\n " + quoted + ": 1\n}");
}
}
}
}
}
}
TEST_CASE("runs of multi-byte characters are escaped like the reference under ensure_ascii")
{
// a run of well-formed multi-byte characters is escaped straight into the
// write buffer; it has to flush that mid-run, stop at an ASCII character or
// an ill-formed sequence and leave those to the general path, and pick up
// behind what the general path has pending
const std::vector<std::string> characters =
{
"\xc2\x80", "\xdf\xbf", "\xe0\xa0\x80", "\xe2\x82\xac", "\xed\x9f\xbf", "\xef\xbf\xbf",
"\xf0\x90\x80\x80", "\xf0\x9f\x98\x80", "\xf4\x8f\xbf\xbf"
};
const std::vector<std::string> interruptions =
{
"", "a", "\n", "\"", "\x7f", "\xff", "\x80", "\xc3", "\xe2\x82", "\xf0\x9f\x98", "\xed\xa0\x80"
};
for (std::size_t shift = 0; shift <= 13; ++shift)
{
for (const auto& interruption : interruptions)
{
for (const std::size_t count :
{
std::size_t{1}, std::size_t{2}, std::size_t{85}, std::size_t{200}
})
{
std::string s(shift, 'x');
for (std::size_t i = 0; i < count; ++i)
{
s += characters[(i + shift) % characters.size()];
}
s += interruption;
s += characters[shift % characters.size()];
s += characters[(shift + 3) % characters.size()];
check_dump_against_reference(s, shift < 2);
}
}
}
}