Compare commits

..
Author SHA1 Message Date
Niels Lohmann f5cb977387 Rename the key index's match callback so Flawfinder doesn't read it as std::equal
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:42:44 +02:00
Niels Lohmann 4cde5e39b9 Collect the members of ordered_json objects on a stack until they are known
An ordered_json object keeps its members in a vector and appends every new
member to it, so reading an object of unknown size reallocated the vector at
1, 2, 4, 8, ... members, and ordered_map copies all keys on every
reallocation. The DOM SAX parser now collects the members of such objects on
one stack shared by all open objects, as it does the elements of arrays, and
moves them into a vector of exactly the right size when the closing brace
arrives. An object that gets past 32 members switches to appending to its
own vector (and, from 64, to the key index of the earlier commit). The
members are the same, in the same order, and a key that occurs twice keeps
the place of its first occurrence. std::map based objects, objects whose
size the input announces (binary formats), and builds with JSON_DIAGNOSTICS
or JSON_DIAGNOSTIC_POSITIONS are untouched.

What only ordered objects need (the collected members, the key indexes) is
a member of the parser only for them, and the code that stores a value tries
the array that takes its elements directly first. An array that outgrows
the stack now reserves 32 elements, the capacity a vector that grew by
itself has at that point; with the larger reserve before, parsing
floats.json (1 000 000 numbers) was 2 % slower on Apple M1 than before this
series.

ordered_json parse, Linux Ir against develop (GCC / Clang): citm -11.6 % /
-11.7 %, twitter -8.4 % / -8.4 %, jeopardy (12 000 objects of 7 keys)
-10.5 % / -8.0 %, canada -3.9 % / -5.2 %, 124-byte object -20.9 % / -16.5 %.
macOS time (M1, earlier base): citm -19 %, twitter -13 %, jeopardy -13 %,
canada -14 %, 124-byte object -26 %. json parse: steady-state twitter -0.1 %
(see the previous commits), citm -1.2 %.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:08 +02:00
Niels Lohmann 284ef38125 Do not convert floats that cannot overflow in accept()
accept() only has to find out whether a float token is finite: json_sax_acceptor
ignores the value, and the parser rejects only a float that overflows. A
float whose digits fit into 19 digits and whose decimal exponent is at most
max_exponent10 - 19 is below 10^max_exponent10 and cannot overflow, so the
lexer, told by accept() that the value is not needed, skips the conversion
for those. A token that may overflow, with more digits, or in another float
format is converted as before, so 1e309 and its neighbours are rejected as
they were (the new test compares accept() with parse() on tokens around the
limit).

json::accept(): canada.json Ir -8.6 % (GCC) / -10.9 % (Clang), 124-byte
object -3.2 %; cycles on the M1 (release benchmarks, LTO): canada -7.8 %,
floats.json -9.0 %. The check tests the accept() flag first, so the parser
that needs the value pays one load and branch: SaxParse canada +0.35 % Ir
(GCC and Clang), DOM canada +0.4 %.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:05 +02:00
Niels Lohmann e38bb5d6f8 Test parsing containers of the sizes where the DOM parser changes storage
The DOM parser collects the elements of an array on a stack (up to 16) and
the members of an ordered_json object (up to 32) before it moves them into
the container, and uses a hash table for keys from 64 members on. Parse
arrays and objects of the sizes around these limits, nested into each other,
with repeated keys, and as random trees that are built next to their text,
and compare json and ordered_json against the value built through the API.

The tests pass on the code from before these changes as well. The random
trees are what finds the mistakes that are easy to make here: while the
collecting change was being written, a collected array that outgrew the
stack at the moment its parent had just reached the limit left its elements
in the parent, and only these tests (142 failed assertions) showed it.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:03 +02:00
Niels Lohmann ffa9babc8d Trim the work the DOM parser does per container and per member
Opening and closing an array or object cost the DOM parser more than before
the exact-size array change, which showed on documents of many small
objects (jeopardy.json: +1.4 % Ir with GCC). This takes it back:

- the stack of open containers keeps its first eight entries in the parser
  object instead of in a vector, so it needs no capacity check beyond the
  push itself and no allocation for typical documents;
- a container is created in the place it ends up in, instead of as a
  temporary that is then moved there;
- a new object member, which is null, is constructed over the null instead
  of being swapped with it (only with the default base class of basic_json);
- the key index that big ordered_json objects use is only a part of the
  stack entries of objects that can have one;
- an array that outgrows the stack reserves twice what it has so far, as a
  vector that grew by itself would have.

DOM parse Ir of the series up to here against develop, Linux GCC / Clang:
canada -6.6 % / -7.3 %, citm +1.2 % / +0.2 %, jeopardy +0.4 % / -0.1 %,
124-byte object -6.5 % / -6.2 %. (twitter measures +6.8 % / +6.6 % at this
point in steady state, almost all of it in glibc's malloc/consolidate for the
new allocation sizes; with the last commit of the series it is -0.1 % / 0.0 %.)

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:00 +02:00
Niels Lohmann 5b08041308 Look up the keys of big ordered_json objects in a hash table
ordered_map keeps its elements in a vector and searches it linearly for
every key it adds, so parsing an object with n keys into ordered_json took
O(n^2) key comparisons: 33 billion instructions for one object with 30 000
keys, where json needs 245 million. From 64 keys on, the DOM parser now
keeps a hash table of the keys of the open object (hashes and positions
only; the elements stay in the vector) and finds duplicate keys there. The
result is the same, including the position of a key that occurs twice.
Smaller objects, std::map based objects and key types without std::hash
are untouched.

ordered_json parse, macOS instructions: object with 1 000 keys 64.8 M ->
14.6 M, 10 000 keys 5 104 M -> 38.7 M, 30 000 keys 33 358 M -> 89 M.
Linux Ir of an object with 2 000 keys: 43.0 M -> 1.9 M (GCC), 37.9 M ->
1.8 M (Clang).

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:27:58 +02:00
Niels Lohmann eef9aab5ba Remember where the DOM parser stores the next value
The DOM SAX parser looked up the innermost open container for every value:
the end of its frame stack, then the object or array pointer in the frame.
Compilers have to reload all of that after each value is stored, since a
store of a value may alias the frame. The parser now keeps the innermost
container's kind and, for an array that stores directly, the array itself
in members, updated when a container opens or closes.

Flat arrays of numbers are the case that mattered: parsing floats.json
(1 000 000 doubles) into a DOM took 2 % more cycles than before the
previous commit on macOS and now takes 0.4 % fewer than the code before it
(signed_ints.json -0.8 %). Linux Ir is unchanged (canada -6.1 % GCC /
-6.9 % Clang, citm -0.3 % / -0.8 %, twitter +0.2 % / 0.0 %).

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:27:55 +02:00
Niels Lohmann c27f770c07 Reuse one value for the key events of the callback parser
parse() with a callback built a new string value for every key it passed to
the callback (a heap allocation and release per key), and copied the key a
second time into the object. The callback parser now keeps one string value
for key events, which it assigns, and moves the key into the object.

Callback parse, Linux Ir (GCC / Clang): twitter -13.3 % / -11.7 %, citm
-9.5 % / -7.6 %. (Earlier macOS measurement: twitter -19.5 % instructions.)

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:27:53 +02:00
Niels Lohmann 2e454713d0 Build arrays of the DOM parser in one exact-size allocation
Reading an array of unknown length appended its elements to a std::vector
one at a time, so a small array reallocated (and moved its elements) at
sizes 1, 2, 4, ... The DOM SAX parser now collects the elements of arrays
whose length is unknown on one stack shared by all open arrays and moves
them into an array of exactly the right size when the closing bracket
arrives. An array that grows past 16 elements switches to appending to its
own vector, so large arrays pay no second copy. Binary formats, which
announce the length, and builds with JSON_DIAGNOSTICS or
JSON_DIAGNOSTIC_POSITIONS (whose values need their final place from the
start) keep storing into the array directly.

The stacks of open containers are reserved on first use instead of growing
from one element.

The binary readers get a DOM parser of their own, json_sax_dom_binary_parser:
it is the plain parser that stores every value directly in its container
(the code the readers used before). Their containers announce their size, so
the bookkeeping of the collecting parser (frame entries, the destination of a
finished container) was pure overhead for them: with the collecting parser
FromBinaryShape nested was 3-12 % and containers up to 5 % more instructions
(GCC) than before. With the plain parser Linux Ir of FromBinaryShape and
FromBinaryBuffer is within 0.8 % of develop (Clang: identical).

canada.json has 56 000 two-element arrays that took three allocations each
and now take two. DOM parse Ir on Linux against develop (GCC / Clang): canada
-7.0 % / -7.7 %, citm -0.8 % / -1.6 %, 124-byte object -5.4 % / -5.5 %;
accept and SAX are untouched.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:27:51 +02:00
Niels Lohmann 56ea5dd6bf Reserve the parser's nesting stack once
The stack of open arrays and objects was a std::vector<std::uint8_t> that
allocated at the first push_back and then reallocated at 1, 2, 4, 8 bytes,
so a document nested four levels deep made four allocations before it
parsed a value. A document that starts with a container now reserves 32
levels in one allocation; deeper documents grow as before. The loop that
reads the tokens is the code it was, so large documents are not affected
(a stack object with its own storage, and a reserve at every container,
both cost the token loop instructions or cycles).

Fixed cost of parsing a 124-byte object (macOS instructions): accept
-7.6 %, DOM -2.5 %; no change for a single number.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:27:48 +02:00
13 changed files with 2846 additions and 4324 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,
@@ -86,7 +86,7 @@ JSON_INLINE_VARIABLE constexpr std::size_t max_valueless_container_size = 1 << 2
/*! /*!
@brief deserialization of BJData, BON8, BSON, CBOR, MessagePack, and UBJSON values @brief deserialization of BJData, BON8, BSON, CBOR, MessagePack, and UBJSON values
*/ */
template<typename BasicJsonType, typename InputAdapterType, typename SAX = json_sax_dom_parser<BasicJsonType, InputAdapterType>> template<typename BasicJsonType, typename InputAdapterType, typename SAX = json_sax_dom_binary_parser<BasicJsonType, InputAdapterType>>
class binary_reader class binary_reader
{ {
using number_integer_t = typename BasicJsonType::number_integer_t; using number_integer_t = typename BasicJsonType::number_integer_t;
File diff suppressed because it is too large. Load diff
+10
View File
@@ -14,6 +14,7 @@
#include <cstdio> // snprintf #include <cstdio> // snprintf
#include <cstring> // memcmp #include <cstring> // memcmp
#include <initializer_list> // initializer_list #include <initializer_list> // initializer_list
#include <limits> // numeric_limits
#include <string> // char_traits, string #include <string> // char_traits, string
#include <utility> // move #include <utility> // move
#include <vector> // vector #include <vector> // vector
@@ -1816,6 +1817,15 @@ scan_number_done:
/// significand (see scan_number_bulk_contiguous()) /// significand (see scan_number_bulk_contiguous())
bool convert_accumulated_float(const float_significand& s, std::true_type /*native*/) noexcept bool convert_accumulated_float(const float_significand& s, std::true_type /*native*/) noexcept
{ {
// accept() only needs to know that the value is finite (see the parser):
// w < 10^19, so a value below 10^max_exponent10 cannot overflow, and a
// value that underflows is a finite number as well. Only a token that
// may overflow has to be converted to find out.
if (JSON_HEDLEY_UNLIKELY(discard_number_values && s.exponent + 19 <= std::numeric_limits<number_float_t>::max_exponent10))
{
value_float = 0;
return true;
}
value_float = decimal_to_float<number_float_t>(s); value_float = decimal_to_float<number_float_t>(s);
return true; return true;
} }
+7
View File
@@ -250,6 +250,13 @@ class parser
// value to avoid a goto (see comment where set to true) // value to avoid a goto (see comment where set to true)
bool skip_to_state_evaluation = false; bool skip_to_state_evaluation = false;
// a document that starts with a container pushes onto the stack at
// once: allocate it in one step instead of growing it from 1 byte
if (last_token == token_type::begin_object || last_token == token_type::begin_array)
{
states.reserve(32);
}
while (true) while (true)
{ {
if (!skip_to_state_evaluation) if (!skip_to_state_evaluation)
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
+22 -44
View File
@@ -161,6 +161,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
template<typename BasicJsonType, typename InputAdapterType> template<typename BasicJsonType, typename InputAdapterType>
friend class ::nlohmann::detail::json_sax_dom_parser; friend class ::nlohmann::detail::json_sax_dom_parser;
template<typename BasicJsonType, typename InputAdapterType> template<typename BasicJsonType, typename InputAdapterType>
friend class ::nlohmann::detail::json_sax_dom_binary_parser;
template<typename BasicJsonType, typename InputAdapterType>
friend class ::nlohmann::detail::json_sax_dom_callback_parser; friend class ::nlohmann::detail::json_sax_dom_callback_parser;
#if JSON_DIAGNOSTIC_POSITIONS #if JSON_DIAGNOSTIC_POSITIONS
friend struct ::nlohmann::detail::diagnostic_positions; friend struct ::nlohmann::detail::diagnostic_positions;
@@ -6287,7 +6289,7 @@ public:
from_ubjson/from_bjdata/from_bon8/from_bson overloads from_ubjson/from_bjdata/from_bon8/from_bson overloads
Building the @ref binary_reader as a named local, rather than as a Building the @ref binary_reader as a named local, rather than as a
temporary that @ref detail::json_sax_dom_parser::parse is called on in temporary that @ref detail::json_sax_dom_binary_parser::parse is called on in
the same expression, avoids a false-positive cppcheck accessMoved the same expression, avoids a false-positive cppcheck accessMoved
warning in each of the 16 callers. warning in each of the 16 callers.
*/ */
@@ -6298,7 +6300,7 @@ public:
const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error) const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error)
{ {
basic_json result; basic_json result;
detail::json_sax_dom_parser<basic_json, InputAdapterType> sdp(result, allow_exceptions); detail::json_sax_dom_binary_parser<basic_json, InputAdapterType> sdp(result, allow_exceptions);
binary_reader<InputAdapterType> reader(std::move(ia), format, error_handler); binary_reader<InputAdapterType> reader(std::move(ia), format, error_handler);
if (!reader.sax_parse(&sdp, strict, tag_handler)) if (!reader.sax_parse(&sdp, strict, tag_handler))
{ {
@@ -6321,7 +6323,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 +6333,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 +6341,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 +6350,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 +6360,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 +6368,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 +6379,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 +6390,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 +6399,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 +6411,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 +6423,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 +6433,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 +6442,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 +6452,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 +6460,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 +6468,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 +6477,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
+1300 -2025
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());
}
}
+29
View File
@@ -2952,3 +2952,32 @@ TEST_CASE("diagnostic positions: value lifetime, input adapters, and SAX")
} }
} }
#endif #endif
TEST_CASE("accept() agrees with parse() on floats around the overflow limit")
{
// accept() does not convert a float that cannot overflow; the tokens
// around the limit (and beyond it) must still be rejected exactly as
// parse() rejects them
const char* const numbers[] =
{
"1e0", "123.456e-5", "1e-400", "-1e-400", "1e307", "1e308", "1e309", "-1e309",
"10e307", "100e307", "0.1e309", "0.1e310", "1E+308", "1E+309",
"1.7976931348623157e308", "1.7976931348623159e308", "1.8e308", "-1.7976931348623157e308",
"12345678901234567e291", "12345678901234567e292", "123456789012345678e290", "123456789012345678e291",
"1234567890123456789e289", "1234567890123456789e290", "9999999999999999999e289", "9999999999999999999e290",
"0.00000000000000000001e328", "0.00000000000000000001e329"
};
for (const char* number : numbers)
{
for (const std::string text :
{
std::string(number), "[" + std::string(number) + "]", "{\"a\":" + std::string(number) + "}"
})
{
INFO(text);
const bool parses = !json::parse(text, nullptr, false).is_discarded();
CHECK(json::accept(text) == parses);
}
}
}
-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
+271
View File
@@ -237,3 +237,274 @@ TEST_CASE("regression test for issue #5673 - ordered_json::emplace with a non-rv
CHECK(oj.dump() == "{\"a\":1}"); CHECK(oj.dump() == "{\"a\":1}");
} }
} }
namespace
{
// the sizes around which the DOM parser changes how it stores the elements of
// an array (16) and the members of an object in a vector (32, and 64 for the
// key index)
const std::size_t container_sizes[] = {0, 1, 2, 3, 15, 16, 17, 18, 31, 32, 33, 34, 63, 64, 65, 66, 200};
template<typename Json>
std::string array_text(const std::size_t n, const std::size_t depth)
{
std::string text = "[";
for (std::size_t i = 0; i < n; ++i)
{
text += (i == 0 ? "" : ",");
// alternate scalars, empty containers and nested containers
switch (i % 4)
{
case 0:
text += std::to_string(i);
break;
case 1:
text += "\"s" + std::to_string(i) + "\"";
break;
case 2:
text += depth == 0 ? "[]" : array_text<Json>(n / 2, depth - 1);
break;
default:
text += depth == 0 ? "{}" : "{\"a\":" + array_text<Json>(n / 3, depth - 1) + ",\"b\":null}";
break;
}
}
return text + "]";
}
template<typename Json>
std::string object_text(const std::size_t n, const std::size_t depth, const bool duplicates)
{
std::string text = "{";
for (std::size_t i = 0; i < n; ++i)
{
text += (i == 0 ? "" : ",");
text += "\"key" + std::to_string(i) + "\":";
switch (i % 4)
{
case 0:
text += std::to_string(i);
break;
case 1:
text += depth == 0 ? "[]" : array_text<Json>(n / 2, depth - 1);
break;
case 2:
text += depth == 0 ? "{}" : object_text<Json>(n / 2, depth - 1, duplicates);
break;
default:
text += "\"value\"";
break;
}
}
if (duplicates)
{
// keys that are there already, the first and the last one, with other values
text += n == 0 ? "" : ",";
text += "\"key0\":[1,2],\"key" + std::to_string(n - 1) + "\":{\"again\":true}";
}
return text + "}";
}
template<typename Json>
void check_same_dump(const std::string& text)
{
// the text is what dump() writes for the parsed value, and the value
// survives a second round trip: arrays keep their elements in order, an
// ordered_json keeps its members in order of appearance
const Json parsed = Json::parse(text);
const Json again = Json::parse(parsed.dump());
CHECK(parsed == again);
CHECK(parsed.dump() == again.dump());
CHECK(Json::accept(text));
}
} // namespace
TEST_CASE("parsing containers of sizes around the limits of the DOM parser")
{
SECTION("arrays")
{
for (const std::size_t n : container_sizes)
{
const std::string text = array_text<json>(n, 2);
INFO("size ", n);
const json parsed = json::parse(text);
CHECK(parsed.is_array());
CHECK(parsed.size() == n);
for (std::size_t i = 0; i < n; i += 4)
{
CHECK(parsed[i] == i);
}
check_same_dump<json>(text);
check_same_dump<ordered_json>(text);
CHECK(ordered_json::parse(text).dump() == parsed.dump());
}
}
SECTION("objects")
{
for (const bool duplicates :
{
false, true
})
{
for (const std::size_t n : container_sizes)
{
const std::string text = object_text<json>(n, 2, duplicates);
INFO("size ", n, " duplicates ", duplicates);
const json parsed = json::parse(text);
const ordered_json ordered = ordered_json::parse(text);
CHECK(parsed.is_object());
CHECK(ordered.is_object());
CHECK(parsed.size() == ordered.size());
// the same members, in the order of their first appearance
std::size_t i = 0;
for (auto it = ordered.begin(); it != ordered.end(); ++it, ++i)
{
if (i < n)
{
CHECK(it.key() == "key" + std::to_string(i));
}
CHECK(parsed.at(it.key()) == json(it.value()));
}
if (duplicates && n > 1)
{
// the later value replaced the earlier one in its place
CHECK(ordered.at("key0") == ordered_json::parse("[1,2]"));
CHECK(ordered.at("key" + std::to_string(n - 1)) == ordered_json::parse("{\"again\":true}"));
CHECK(ordered.begin().key() == "key0");
CHECK(ordered.size() == n);
}
check_same_dump<json>(text);
check_same_dump<ordered_json>(text);
}
}
}
SECTION("objects with many different and repeated keys")
{
// 300 members of which a third repeat an earlier key
std::string text = "{";
for (std::size_t i = 0; i < 300; ++i)
{
text += (i == 0 ? "" : ",");
text += "\"k" + std::to_string(i % 200) + "\":" + std::to_string(i);
}
text += "}";
const ordered_json ordered = ordered_json::parse(text);
const json parsed = json::parse(text);
CHECK(ordered.size() == 200);
CHECK(parsed.size() == 200);
std::size_t i = 0;
for (auto it = ordered.begin(); it != ordered.end(); ++it, ++i)
{
CHECK(it.key() == "k" + std::to_string(i));
// the last value written
CHECK(it.value() == (i < 100 ? i + 200 : i));
CHECK(parsed.at(it.key()) == json(it.value()));
}
}
}
namespace
{
// a deterministic source of structure for parsing tests: builds a JSON text
// and, at the same time, the value it has to parse to
class tree_generator
{
public:
explicit tree_generator(std::uint32_t seed) : state(seed) {}
template<typename Json>
Json make(std::string& text, const std::size_t depth)
{
switch (next(depth == 0 ? 5 : 9))
{
case 0:
text += "null";
return Json(nullptr);
case 1:
text += "true";
return Json(true);
case 2:
{
const int n = static_cast<int>(next(2000)) - 1000;
text += std::to_string(n);
return Json(n);
}
case 3:
text += "\"text\"";
return Json("text");
case 4:
text += "2.5";
return Json(2.5);
case 5:
case 6:
case 7:
{
Json array = Json::array();
text += "[";
const std::size_t n = size();
for (std::size_t i = 0; i < n; ++i)
{
text += (i == 0 ? "" : ",");
array.push_back(make<Json>(text, depth - 1));
}
text += "]";
return array;
}
default:
{
Json object = Json::object();
text += "{";
const std::size_t n = size();
for (std::size_t i = 0; i < n; ++i)
{
const std::string key = "k" + std::to_string(i);
text += (i == 0 ? "\"" : ",\"") + key + "\":";
object[key] = make<Json>(text, depth - 1);
}
text += "}";
return object;
}
}
}
private:
std::uint32_t next(const std::uint32_t bound)
{
state = state * 1664525U + 1013904223U;
return (state >> 8) % bound;
}
std::size_t size()
{
return container_sizes[next(sizeof(container_sizes) / sizeof(*container_sizes))];
}
std::uint32_t state;
};
} // namespace
TEST_CASE("parsing random trees of arrays and objects")
{
for (std::uint32_t seed = 1; seed <= 150; ++seed)
{
INFO("seed ", seed);
std::string text;
std::string ordered_text;
const json expected = tree_generator(seed).make<json>(text, 3);
const ordered_json ordered_expected = tree_generator(seed).make<ordered_json>(ordered_text, 3);
CHECK(text == ordered_text);
CHECK(json::parse(text) == expected);
CHECK(json::accept(text));
const ordered_json ordered = ordered_json::parse(text);
CHECK(ordered == ordered_expected);
CHECK(ordered.dump() == ordered_expected.dump());
CHECK(json::parse(text).dump() == expected.dump());
}
}