Compare commits

..
Author SHA1 Message Date
Niels Lohmann 23f999547e Mark the parameters of the binary writers' throw helpers as used
With JSON_NOEXCEPTION, JSON_THROW() drops its argument, so the
parameters that only build the message were unused, which Clang's
-Wunused-parameter turned into an error in test-disabled_exceptions.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 11:45:57 +02:00
Niels Lohmann adceb69a77 Test that the writers' loops for arrays and objects keep the range checks
Runs of numbers and scalar object members are encoded by loops of their own
for the number types up to 64 bits. A number type wider than that has to take
the generic path, which throws out_of_range.407 for an integer that does not
fit. The test writes arrays and objects with such an integer in every format,
checks the partial output of a failing CBOR and BON8 write, and compares the
bytes of the values that do fit with those of the 64-bit number types.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:29:39 +02:00
Niels Lohmann b0a7d5f6ff Test that number runs, string heads and adapter flushing keep the output
The writers encode a run of numbers in an array, and a string with its head,
straight into space reserved from the sink, and the adapter sink buffers its
writes. These tests pin the output to what it was: an array is its head and
the encodings of its elements one by one for every boundary of the integer
encodings and every kind of float, strings and keys of every head length and
large arrays read back, and a write that throws leaves the same bytes in a
vector, a string and a stream.

The tests pass with and without the changes of this series.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:29:36 +02:00
Niels Lohmann 70fdb359f0 Describe the sinks of the binary writer in the architecture page
The page listed the output adapters only. The binary writer writes through a
sink: a non-virtual one that appends to the vector of the vector-returning
to_* functions, and one that collects the output for the adapters of the other
overloads and passes it on in blocks.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:29:34 +02:00
Niels Lohmann 7717853976 Write the digits of a high-precision UBJSON number without basic_json
An unsigned integer above the int64 range is a high-precision number in
UBJSON: its decimal digits as a string. They were found by building a
basic_json from the integer and calling dump() on it, an allocation and a
serializer for 20 digits.

An unsigned integer of at most 64 bits now writes its digits from a small
buffer. Other integer types (wider than 64 bits) keep dump().

Instructions per call, 3000 integers above the int64 range (Linux, GCC 13, thousands): to_ubjson 3968 -> 1825, to_bjdata 180 -> 180. (to_bjdata writes these as uint64 and is unchanged.)

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:29:31 +02:00
Niels Lohmann bc94203dbc Throw, and write high-precision numbers, from functions of their own
Functions that check something that almost never fails kept the code that
builds the exception in their own body: check_utf8() of BON8 (a std::string
for the message), the NUL check of a BSON key, the length checks of
MessagePack and BSON, the subtype checks, the size marker check of UBJSON, the
integer range check of BON8, and the high-precision number of UBJSON (a
basic_json and a string, for every integer that picks the marker `H`). That
gave them a frame with a stack protector under GCC, and made the compiler
spill registers for a path that is not taken.

Each of these is now thrown, or written, by a function that is not inlined.
The messages and exception ids are the same.

Instructions per call (Linux, GCC 13, thousands), to_bson: 20000 small objects 31714 -> 29614, twitter 4930 -> 4637; to_ubjson: citm_catalog 7020 -> 6597; to_bjdata: citm_catalog 7192 -> 6789; to_bon8: citm_catalog 7107 -> 6790.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:29:29 +02:00
Niels Lohmann b85d1ca043 Write the one-byte head of an array or object with a single sink write
The head of an array or object of up to 23 elements (15 for MessagePack) is a
single byte, but it was written through write_cbor_head()/write_number(),
which GCC keeps out of line, and a std::array.

The CBOR array and object writers now write that byte directly, and
MessagePack does the same in its array and map prefixes.

Instructions per call (Linux, GCC 13, thousands), to_cbor: citm_catalog 5588 -> 5020, canada 15840 -> 14499, 20000 small objects 13162 -> 12582; to_msgpack: citm_catalog 5920 -> 5676, canada 16344 -> 15676.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:29:26 +02:00
Niels Lohmann 26b4716740 Write the count of a UBJSON or BJData container in one reservation
With use_size, a container is announced by the marker `#` and its element
count as an integer, three small sink writes for the many containers of up to
127 elements (an empty array, an object with a few members).

A count of up to 127 is now written as `#`, `i` and one byte in a single
reservation.

Instructions per call (Linux, GCC 13, thousands), to_ubjson with use_size and use_type: canada 25813 -> 22060, citm_catalog 10656 -> 9192, 20000 small objects 18178 -> 16798; to_bjdata with use_size and use_type: canada 26341 -> 22588, citm_catalog 10904 -> 9440.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:29:24 +02:00
Niels Lohmann a3b9e9c712 Write the members of a BON8 object with scalar values without a call each
As for the other formats: a member with a null, boolean, number, or string
value is written by the loop over the members, without a call of
write_bon8_value() for its value. A number above the int64 range, which BON8
cannot write (also with a number type wider than 64 bits), still goes through
write_bon8_value(), which throws as before.

Instructions per call (Linux, GCC 13, thousands), to_bon8: 20000 small objects 20741 -> 18164, twitter 5526 -> 5123, citm_catalog 7335 -> 7107.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:29:21 +02:00
Niels Lohmann 985fa5cb2e Keep a cursor and an end pointer in the vector sink
The vector sink kept the vector and a position, so that every write or
reservation read the vector's begin and end, subtracted them and the position,
and added the position to the data pointer again for the store.

It now keeps the write pointer and the end of the vector. A capacity check is
a subtraction of two members, and the pointer is the one to store to. Both are
set again whenever the vector grows; the destructor still trims the vector to
what was written, also when a write throws.

Instructions per call (Linux, GCC 13, thousands), to_bon8: 20000 small objects 23279 -> 20741, citm_catalog 7974 -> 7335; to_msgpack: 20000 small objects 13641 -> 12702; to_ubjson: 20000 small objects 14620 -> 13620; to_bjdata: 20000 small objects 14641 -> 13641.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:29:19 +02:00
Niels Lohmann f41f6408eb Write a MessagePack float that is not in an array of numbers into reserved space
A float that was not part of a run in an array (an object member, a float in a
mixed array) went through write_compact_float(), with a sink call for its
marker and one through a std::array for its bytes.

It is now encoded by the function that encodes the floats of a run, into space
reserved from the sink.

CBOR keeps the previous code for these floats: giving them the same treatment
made GCC 13 stop inlining the encoder into the loop for the floats of an array,
which cost 4 percent more instructions for canada and citm_catalog.

Instructions per call (Linux, GCC 13, thousands), to_msgpack: 20000 small objects 14601 -> 13641.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:29:16 +02:00
Niels Lohmann ff2d59d3f1 Write the elements of a BJData ND-array in blocks
An ND-array annotation object is checked and then written from its _ArrayData_
in three passes. Every element went through basic_json::get<>() twice or three
times (the range check of the dry run, the conversion for the write) and
through write_number() with a std::array and a sink call of its own, with a
switch on the number kind for each.

The range check and the conversion read the value directly (the elements are
known to be integers or floats by then), and the real pass switches on the
number kind once and encodes its elements in blocks into space reserved from
the sink.

to_bjdata into a std::vector, instructions per call (Linux, GCC 13, thousands): an ND-array of 120000 doubles 14588 -> 9975, an ND-array of 120000 uint8 16583 -> 8790, an ND-array of 120000 doubles with use_size and use_type 14630 -> 10017.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:29:14 +02:00
Niels Lohmann ceb3c92709 Reserve at least 64 bytes for the result of a binary serialization
The result of to_cbor() and its siblings reserved the lower bound of
binary_reserve_hint(), for a small object a few bytes, and the first write of
the sink replaced it by an allocation of 64 bytes, as an empty or too small
vector is grown to at least that. A small document therefore cost two
allocations and a free.

The result now reserves at least min_vector_sink_capacity (64) characters at
once. binary_reserve_hint() itself, a lower bound that is tested as such, is
unchanged.

Instructions to serialize a small object (six members, a nested array and object), Linux, GCC 13: to_cbor 1700 -> 1500, to_msgpack 1800 -> 1600, to_ubjson 1800 -> 1700, to_bson 2700 -> 2500. Documents larger than 64 bytes of hint are unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:29:11 +02:00
Niels Lohmann 7241ba94c8 Encode runs of numbers in BON8 arrays without a call each
A number in a BON8 array went through write_bon8_value(), the ladder of
write_bon8_integer() and one sink write per byte of its encoding (through a
std::array for each).

An array that starts with a number now encodes its runs of integers and floats
straight into space reserved from the sink, as the other formats do. An
unsigned integer above the int64 range, which BON8 cannot write, is passed to
write_bon8_value(), which throws exactly as before after the same bytes. So
is an integer of a number type wider than 64 bits that does not fit.

Instructions per call (Linux, GCC 13, thousands), to_bon8: 20000 integers 2375 -> 1002, 20000 floats 3173 -> 1607, canada 22130 -> 15558.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:29:09 +02:00
Niels Lohmann f9e2302af9 Write the members of an object with scalar values without a call each
Every member of an object was a call of write_cbor()/write_msgpack()/
write_ubjson() for its value, however simple: a function with a large frame
that saves and restores many registers around a single sink write.

Objects are now written by a function of their own, like arrays, in which
members with a null, boolean, integer, or string value (and a float, in UBJSON
and BJData) are written in place; anything else is passed on to the general
function. The depth limit and the recursion for nested values are unchanged.

Instructions per call (Linux, GCC 13, thousands), to_ubjson: 20000 small objects 17861 -> 14661, twitter 3271 -> 2798; to_bjdata: 20000 small objects 17881 -> 14681, twitter 3277 -> 2804; to_msgpack: twitter 2948 -> 2646.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:29:06 +02:00
Niels Lohmann eb251df17c Size and write BSON arrays of numbers without a frame on the stack
Every array nested in a BSON document pushed a frame of about 100 bytes on a
vector, replaced the current one by moving, and popped it again, once for the
size pass and once for the write pass. For the many small arrays of numbers
(coordinates, vectors) that was more than the work on their entries.

An array whose elements are all booleans, numbers, or null is now sized by a
loop of its own, and written in the loop for fixed-size entries on a frame
that is not pushed. The entries of other arrays go to the fixed-size loop only
when the element is such a value, which also saves the call for an array of
strings. An array with an integer that does not fit 64 bits (possible with
number types wider than that) is left to the generic path, which throws.

Instructions per call (Linux, GCC 13, thousands), to_bson: canada 46833 -> 30575, 20000 integers 2680 -> 1839, 20000 floats 3050 -> 2209, citm_catalog 14536 -> 13235, 20000 small objects 33294 -> 31954.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:29:04 +02:00
Niels Lohmann dd3ed1ef86 Check and sanitize strings out of line
The string writers kept the string_t that holds a sanitized copy for the other
error handlers in the function that also has the fast path for the default
one, so that every string paid for its construction check, a stack frame with
a stack protector under GCC, and the registers saved for the slow path.

The slow path is now a function of its own that the fast path does not call.

Instructions per call (Linux, GCC 13, thousands), to_ubjson: twitter 3867 -> 3274, 20000 small objects 20981 -> 17901; to_bjdata: twitter 3873 -> 3281, 20000 small objects 21001 -> 17921; to_msgpack: 20000 small objects 18342 -> 15922.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:29:01 +02:00
Niels Lohmann a517b8ea8b Write a UBJSON, BJData or BSON string and its head in one reservation
Same as for CBOR and MessagePack: with the default error handler (keep) a
string or object key of up to 253 characters (UBJSON, BJData: the `S` marker,
the length as `i` or `U` and one byte, and the characters), or a BSON string
entry that fits the reservation limit (type, name, length, characters and null
byte), is written into space reserved from the sink in one go, instead of by
four to six sink writes, a string_t for the error handler, and a call of
sanitize_utf8_for_write(). The name of a BSON entry is copied with the short-
string copy of the sinks.

Instructions per call (Linux, GCC 13, thousands), to_ubjson: 20000 small objects 28241 -> 20981, twitter 5175 -> 3867, citm_catalog 10681 -> 8857; to_bjdata: 20000 small objects 28261 -> 21001, twitter 5181 -> 3873.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:59 +02:00
Niels Lohmann ecc2f511c7 Write a CBOR or MessagePack string with its head in one reservation
A string or object key took a string_t constructed for the UTF-8 error
handler, a call of the (out of line) sanitize_utf8_for_write() that returns
the string unchanged for the default handler, a call that builds the head from
a range-checked length and writes it with a std::array and a sink call of its
own, and a sink write for the characters, which copied it with a call of
memcpy() through a stub with its own dispatch on the length.

With the default error handler (keep) and a string of up to 254 characters,
the head and the characters are now written into space reserved from the sink
in one go. Longer strings and the other handlers take the previous code. The
sinks copy up to 32 characters with fixed-size loads and stores - two possibly
overlapping 16, 8 or 4 byte copies, or the first, middle and last character of
a string of 1 to 3 - instead of calling memcpy(); nothing outside of the
source or destination range is touched.

Instructions per call (Linux, GCC 13, thousands), to_msgpack: twitter 4414 -> 3383, 20000 small objects 23895 -> 18342, citm_catalog 8575 -> 7031; to_cbor: 20000 small objects 20695 -> 17182, twitter 3842 -> 3204.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:56 +02:00
Niels Lohmann 4ee05e4d1f Encode runs of floats in CBOR and MessagePack arrays without a call each
Integers in an array were already encoded in runs; a float went through
write_cbor()/write_msgpack(), the NaN/infinity checks, write_compact_float()
and, for the marker and the 4 or 8 bytes of the number, two sink writes, one
of them through a std::array that was reversed and copied.

A run of floats is now encoded straight into space reserved from the sink,
like a run of integers, by a function of its own so that the loop of integers
is unchanged. The float32 check is shared with write_compact_float(). Arrays
that are empty are returned from before the elements loop, which also saves
its prologue for the many empty arrays of documents like citm_catalog.json.

Instructions per call (Linux, GCC 13, thousands), to_cbor: 20000 floats 2753 -> 1202, canada 21529 -> 16232; to_msgpack: 20000 floats 2633 -> 1202, canada 21595 -> 16791.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:54 +02:00
Niels Lohmann 38536b6b4d Write the bytes of an unoptimized UBJSON and BON8 binary value in blocks
Without use_type, a binary value is an array of its bytes, and was written one
sink call per marker and per byte (UBJSON, BJData), or through the integer
ladder for each byte (BON8).

The bytes are now encoded in blocks into space reserved from the sink, by a
function that is not inlined. BON8 stores both bytes of an integer above 39
and keeps the second only if it is needed, so that the code does not branch on
the value.

Instructions per call for a 1 MB binary value (Linux, GCC 13, thousands): to_ubjson 34253 -> 9521, to_bjdata 34253 -> 9521, to_bon8 53088 -> 17333.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:51 +02:00
Niels Lohmann 7d3412a559 Encode runs of numbers in UBJSON and BJData arrays without a call each
An array of numbers was written one write_ubjson() call per element, each
picking the marker of an integer by a ladder of range checks and writing it
with a switch and several small sink writes.

An array that starts with a number now encodes its runs of numbers straight
into space reserved from the sink, as the CBOR and MessagePack writers do for
integers. The marker and size of an integer come from a table indexed by the
bit length of its magnitude, and its bytes are stored as one word, so that the
code does not branch on their size; floats are stored the same way. Arrays
that do not start with a number, and a single integer, keep the previous code.
An unsigned integer UBJSON can only write as a high-precision number is left
to write_ubjson().

Instructions per call (Linux, GCC 13, thousands), to_bjdata: 20000 integers 3278 -> 1086, 20000 floats 2193 -> 1000, canada 20829 -> 16805; to_ubjson: 20000 integers 3282 -> 1246, 20000 floats 2213 -> 1020.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:49 +02:00
Niels Lohmann e82ee7375a Collect the writes of the adapter sink and pass them on in blocks
to_cbor(j, o) and its siblings with a std::string, a std::vector or a
std::ostream go through output_adapter_sink, where every write is a virtual
call, and for a stream a call with a sentry on top. That costs more than
encoding the few bytes of most values, which is why those overloads were 1.8
to 3 times slower than the vector-returning ones.

The sink now keeps a 1 KiB buffer and passes it on when it is full, when a
write is larger than the space left (a payload of a KiB or more goes
directly), or when binary_writer::flush() is called at the end of the to_*
function. Everything reaches the adapter in the same order. The destructor
flushes what is left, so a value that throws (invalid UTF-8 with the strict
handler) leaves the same partial output as before; only flush() can report an
error of the adapter, as a destructor must not throw.

Instructions per call (Linux, GCC 13, thousands), to_*(j, std::string&): twitter cbor 4952 -> 3356, twitter ubjson 6762 -> 4647, twitter bson 5518 -> 4704, citm_catalog cbor 10404 -> 7090; to_*(j, std::ostream&): twitter cbor 9178 -> 3844, twitter ubjson 11974 -> 5129, citm_catalog cbor 18374 -> 7554.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:46 +02:00
Niels Lohmann ef2fee20d8 Write BSON entries into space reserved from the sink
An entry was written as up to five separate sink writes (type, name, null
byte, then the number as a std::array that was reversed and copied) and, in an
array, from a string_t built for the index. The size pass looked for U+0000 in
every key with std::string::find() (a call that calls memchr()), and built a
string_t and called sanitize_utf8_for_write() for the default error handler,
which returns the key unchanged.

Entries of booleans, numbers and null are now encoded straight into space
reserved from the sink, as the CBOR and MessagePack writers do for integer
runs. A run of such elements in an array keeps the write pointer, the index,
and the digits of its name in locals, so that nothing is reloaded after each
character store. The entry of an object member does the same for one entry,
and a string or binary value writes its header the same way. The name is
searched for U+0000 eight characters at a time, and the string_t for the other
error handlers is built in a function of its own.

Instructions per call (Linux, GCC 13, thousands), to_bson: 20000 integers 6486 -> 2680, 20000 floats 6562 -> 3050, canada 55282 -> 47186, twitter 5865 -> 5140, 20000 small objects 37171 -> 33559.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-22 09:28:44 +02:00
Niels Lohmann 135095d692 Name BSON array elements without a std::string per element
BSON stores an array as a document whose keys are the decimal indices. Both
passes built that key with std::to_string() and copied it into a string_t for
every element, and the size pass then validated it like an object key (a
search for U+0000 and the UTF-8 handler), though a decimal number cannot fail
either.

The index name now lives in the array's frame as digits that are incremented
in place, and the size pass adds 2 + the number of digits.

Instructions per call (Linux, GCC 13, thousands), to_bson: 20000 integers 16228 -> 6486, 20000 floats 16304 -> 6562, canada 109155 -> 55282, 20000 small objects 45513 -> 37171, citm_catalog 17269 -> 15026.

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

No files matched your search

+5
View File
@@ -339,6 +339,11 @@ The `serializer` (used by [`dump`](../api/basic_json/dump.md) and [`operator<<`]
- `output_stream_adapter` writes to a `std::ostream`.
- `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
Values are converted from and to other types with the `JSONSerializer` template parameter. The default,
+33 -232
View File
@@ -15,7 +15,7 @@
#include <cstddef> // size_t
#include <cstdint> // uint64_t
#include <cstdlib> // strtoull
#include <cstring> // memchr, memcmp, memcpy
#include <cstring> // memcmp, memcpy
#ifndef JSON_NO_IO
#include <iosfwd> // ostream
#endif // JSON_NO_IO
@@ -303,12 +303,6 @@ class json_pointer
exceeds_size_type ///< @a s converts to an integer that exceeds size_type
};
/// whether a reference token is "-" (a plain comparison with a string literal is not always inlined)
static bool is_dash(const string_t& s) noexcept
{
return s.size() == 1 && s[0] == '-';
}
/*!
@param[in] s reference token to be converted into an array index
@param[out] idx the integer representation of @a s if @ref array_index_status::ok
@@ -334,7 +328,7 @@ class json_pointer
// error condition (cf. RFC 6901, Sect. 4); this also covers single-
// character tokens, so "/x" and "/xy" fail alike; "-" and the empty
// token are left to the conversion below and are reported as unresolved
if (JSON_HEDLEY_UNLIKELY(!s.empty() && !is_dash(s) && !(s[0] >= '0' && s[0] <= '9')))
if (JSON_HEDLEY_UNLIKELY(!s.empty() && s != "-" && !(s[0] >= '0' && s[0] <= '9')))
{
return array_index_status::not_a_number;
}
@@ -481,40 +475,9 @@ class json_pointer
return pos == size;
}
/// reserve room for @a n elements of an array type that has reserve()
template<typename ArrayType>
static auto reserve_elements(ArrayType& array, const std::size_t n, detail::priority_tag<1> /*unused*/)
-> decltype(array.reserve(n), void())
{
array.reserve(n);
}
template<typename ArrayType>
static void reserve_elements(ArrayType& /*array*/, const std::size_t /*n*/, detail::priority_tag<0> /*unused*/)
{}
/// @brief object member access with a key that is only copied if it is inserted
///
/// Objects that can find the position of a key and insert there
/// (std::map) search once, others look the key up and add it if it is missing.
template<typename BasicJsonType>
static auto object_member_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<1> /*unused*/)
-> decltype(object.m_data.m_value.object->emplace_hint(object.m_data.m_value.object->lower_bound(key), key, nullptr), std::declval<BasicJsonType&>())
{
auto& members = *object.m_data.m_value.object;
auto it = members.lower_bound(key);
if (it != members.end() && !members.key_comp()(key, it->first))
{
return it->second;
}
const auto old_capacity = object.container_capacity();
it = members.emplace_hint(it, key, nullptr);
return object.set_parent_after_insert(it->second, old_capacity);
}
template<typename BasicJsonType>
static BasicJsonType& object_member_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<0> /*unused*/)
static BasicJsonType& object_member(BasicJsonType& object, const string_t& key)
{
auto& members = *object.m_data.m_value.object;
const auto it = members.find(key);
@@ -525,12 +488,6 @@ class json_pointer
return object.operator[](key);
}
template<typename BasicJsonType>
static BasicJsonType& object_member(BasicJsonType& object, const string_t& key)
{
return object_member_impl(object, key, detail::priority_tag<1> {});
}
JSON_PRIVATE_UNLESS_TESTED:
json_pointer top() const
{
@@ -763,103 +720,48 @@ class json_pointer
}
}
/*!
@brief the values along the pointer last resolved by @ref resolve_plain
A patch from @ref basic_json::diff lists its operations in document order,
so consecutive pointers share most of their reference tokens; the values
along the previous pointer are kept to resume from the common prefix.
`steps[k]` is the value after the first `k` reference tokens together with
the length of the prefix of `*path` that holds them. A value stays valid as
long as no operation changes its container: callers keep only values up to
the parent of the pointer they modify, or call `reset()`.
*/
template<typename BasicJsonType>
struct plain_steps
{
const string_t* path = nullptr;
std::vector<std::pair<std::size_t, BasicJsonType*>> steps{};
void reset() noexcept
{
path = nullptr;
steps.clear();
}
};
/*!
@brief resolve a plain JSON pointer without building its reference tokens
@param[in] root the value the pointer starts at
@param[in] ptr the value the pointer starts at
@param[in] path a JSON pointer string
@param[in] length the number of leading bytes of @a path to resolve; 0 or
the position of a slash
@param[in,out] token a string to hold the reference tokens, to reuse its storage
@param[in,out] cache the values along the previously resolved pointer;
@a path must outlive the next call
@return the pointed to value, or `nullptr` if the pointer does not denote
an existing value, or does so in a way that is not plain: an array
index that is malformed or out of range, or a "-", or a reference
token with an escape. No exception is
thrown for these; callers use @ref json_pointer instead, which
handles them.
an existing value, or does so in a way that is not plain: a
reference token with an escape, an array index that is malformed or
out of range, or a "-". No exception is thrown for these; callers
use @ref json_pointer instead, which handles them.
*/
template<typename BasicJsonType>
static BasicJsonType* resolve_plain(BasicJsonType* root, const string_t& path, const std::size_t length, string_t& token, plain_steps<BasicJsonType>& cache)
static BasicJsonType* resolve_plain(BasicJsonType* ptr, const string_t& path, const std::size_t length, string_t& token)
{
auto& steps = cache.steps;
// resume from the deepest step that is also a prefix of this path
std::size_t depth = 1;
if (cache.path != nullptr && !steps.empty() && steps[0].second == root)
{
const string_t& previous = *cache.path;
for (depth = steps.size(); depth > 1; --depth)
{
// a step ends at a token boundary of the previous path
const std::size_t prefix = steps[depth - 1].first;
if (prefix <= length && (prefix == length || path[prefix] == '/') && std::memcmp(path.data(), previous.data(), prefix) == 0)
{
break;
}
}
steps.resize(depth);
}
else
{
steps.assign(1, std::make_pair(std::size_t{0}, root));
}
cache.path = &path;
BasicJsonType* ptr = steps[depth - 1].second;
for (std::size_t pos = steps[depth - 1].first; pos < length;)
for (std::size_t pos = 0; pos < length;)
{
if (JSON_HEDLEY_UNLIKELY(path[pos] != '/'))
{
return nullptr;
}
// the reference token between the slash at pos and the next one,
// which must not have an escape
const char* const begin = path.data() + pos + 1;
const char* const limit = path.data() + length;
const char* end = begin;
while (end < limit && *end != '/')
// the reference token between the slash at pos and the next one
std::size_t end = path.find_first_of('/', pos + 1);
if (end == string_t::npos || end > length)
{
if (JSON_HEDLEY_UNLIKELY(*end == '~'))
{
return nullptr;
}
++end;
end = length;
}
token.clear();
token.append(path.data() + pos + 1, end - pos - 1);
if (JSON_HEDLEY_UNLIKELY(token.find_first_of('~') != string_t::npos))
{
return nullptr;
}
switch (ptr->type())
{
case detail::value_t::object:
{
token.clear();
token.append(begin, static_cast<std::size_t>(end - begin));
const auto it = ptr->m_data.m_value.object->find(token);
if (JSON_HEDLEY_UNLIKELY(it == ptr->m_data.m_value.object->end()))
{
@@ -871,24 +773,8 @@ class json_pointer
case detail::value_t::array:
{
// an index of up to 18 digits without a leading zero fits into size_type
// (9 digits for a 32-bit size_type); everything else is left to json_pointer
constexpr std::size_t max_digits = sizeof(typename BasicJsonType::size_type) >= 8 ? 18 : 9;
const std::size_t digits = static_cast<std::size_t>(end - begin);
if (JSON_HEDLEY_UNLIKELY(digits == 0 || digits > max_digits || (digits > 1 && *begin == '0')))
{
return nullptr;
}
typename BasicJsonType::size_type idx = 0;
for (const char* c = begin; c != end; ++c)
{
if (JSON_HEDLEY_UNLIKELY(*c < '0' || *c > '9'))
{
return nullptr;
}
idx = static_cast<typename BasicJsonType::size_type>((idx * 10) + static_cast<typename BasicJsonType::size_type>(*c - '0'));
}
if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size()))
typename BasicJsonType::size_type idx{};
if (JSON_HEDLEY_UNLIKELY(parse_array_index<BasicJsonType>(token, idx) != array_index_status::ok || idx >= ptr->m_data.m_value.array->size()))
{
return nullptr;
}
@@ -907,8 +793,7 @@ class json_pointer
default:
return nullptr;
}
pos = static_cast<std::size_t>(end - path.data());
steps.emplace_back(pos, ptr);
pos = end;
}
return ptr;
@@ -1022,7 +907,7 @@ class json_pointer
case detail::value_t::array:
{
if (JSON_HEDLEY_UNLIKELY(is_dash(reference_token)))
if (JSON_HEDLEY_UNLIKELY(reference_token == "-"))
{
// "-" always fails the range check
JSON_THROW(detail::out_of_range::create(detail::exception_id::pointer_past_the_end_index, detail::concat(
@@ -1093,7 +978,7 @@ class json_pointer
case detail::value_t::array:
{
if (JSON_HEDLEY_UNLIKELY(is_dash(reference_token)))
if (JSON_HEDLEY_UNLIKELY(reference_token == "-"))
{
// "-" cannot be used for const access
JSON_THROW(detail::out_of_range::create(detail::exception_id::pointer_past_the_end_index, detail::concat("array index '-' (", std::to_string(ptr->m_data.m_value.array->size()), ") is out of range"), ptr));
@@ -1155,7 +1040,7 @@ class json_pointer
case detail::value_t::array:
{
if (JSON_HEDLEY_UNLIKELY(is_dash(reference_token)))
if (JSON_HEDLEY_UNLIKELY(reference_token == "-"))
{
// "-" always fails the range check
return nullptr;
@@ -1232,7 +1117,7 @@ class json_pointer
case detail::value_t::array:
{
if (JSON_HEDLEY_UNLIKELY(is_dash(reference_token)))
if (JSON_HEDLEY_UNLIKELY(reference_token == "-"))
{
// "-" always fails the range check
return false;
@@ -1391,52 +1276,6 @@ class json_pointer
obj[key] = std::forward<ValueType>(v);
}
/// the number of entries that flattening @a value adds: its primitive
/// values and empty containers
template<typename BasicJsonType>
static std::size_t count_flattened(const BasicJsonType& value)
{
std::size_t count = 0;
std::vector<const BasicJsonType*> pending(1, &value);
while (!pending.empty())
{
const BasicJsonType* const v = pending.back();
pending.pop_back();
if (v->is_array() && !v->m_data.m_value.array->empty())
{
for (const auto& element : *v->m_data.m_value.array)
{
pending.push_back(&element);
}
}
else if (v->is_object() && !v->m_data.m_value.object->empty())
{
for (const auto& member : *v->m_data.m_value.object)
{
pending.push_back(&member.second);
}
}
else
{
++count;
}
}
return count;
}
// Reserve the storage for @a value of objects that have a reserve(), so
// that it does not have to be moved while it grows
template<typename ObjectType, typename BasicJsonType>
static auto reserve_flattened(ObjectType& obj, const BasicJsonType& value, detail::priority_tag<1> /*unused*/)
-> decltype(obj.reserve(std::size_t {}), void())
{
obj.reserve(count_flattened(value));
}
template<typename ObjectType, typename BasicJsonType>
static void reserve_flattened(ObjectType& /*obj*/, const BasicJsonType& /*value*/, detail::priority_tag<0> /*unused*/)
{}
/// append the decimal representation of @a i to @a out
static void append_index(string_t& out, std::size_t i)
{
@@ -1546,7 +1385,6 @@ class json_pointer
}
};
reserve_flattened(object, value, detail::priority_tag<1> {});
enter(value);
while (!stack.empty())
{
@@ -1554,9 +1392,8 @@ class json_pointer
// frame, which would invalidate a reference to it
const BasicJsonType* const container = stack.back().container;
// the path of the previous child is replaced by the path of the
// next one; for an array element in one resize
const std::size_t path_length = stack.back().path_length;
// drop the path of the previous child
path.resize(stack.back().path_length);
if (container->is_array())
{
@@ -1570,20 +1407,8 @@ class json_pointer
// iterate array and use index as a reference string
++stack.back().index;
std::array<char, 24> digits{};
std::size_t pos = digits.size();
std::size_t rest = i;
do
{
digits[--pos] = static_cast<char>('0' + rest % 10);
rest /= 10;
}
while (rest != 0);
const std::size_t count = digits.size() - pos;
path.resize(path_length + 1 + count);
char* const out = &path[path_length];
out[0] = '/';
std::memcpy(out + 1, digits.data() + pos, count);
path.push_back('/');
append_index(path, i);
enter(array[i]);
}
else
@@ -1597,7 +1422,6 @@ class json_pointer
// iterate object and use keys as reference string
++stack.back().member;
path.resize(path_length);
path.push_back('/');
detail::escape_into(path, it->first);
enter(it->second);
@@ -1693,28 +1517,8 @@ class json_pointer
{
end = key.size();
}
const std::size_t token_size = end - pos - 1;
// a token that is a plain index of up to 9 digits, which is
// the case for almost all array elements, is not copied
std::size_t index = 0;
bool plain_index = current->type() == detail::value_t::array && token_size != 0 && token_size <= 9 && (token_size == 1 || data[pos + 1] != '0');
for (std::size_t k = pos + 1; plain_index && k < end; ++k)
{
plain_index = data[k] >= '0' && data[k] <= '9';
index = (index * 10) + static_cast<std::size_t>(data[k] - '0');
}
if (plain_index)
{
current = &current->operator[](static_cast<typename BasicJsonType::size_type>(index));
values.push_back(current);
ends.push_back(end);
pos = end;
continue;
}
token.clear();
token.append(data + pos + 1, token_size);
token.append(data + pos + 1, end - pos - 1);
if (token.find_first_of('~') != string_t::npos)
{
detail::unescape(token);
@@ -1724,13 +1528,10 @@ class json_pointer
{
case detail::value_t::null:
{
// a first child 0 makes the value an array without a look-up
if ((token_size == 1 && data[pos + 1] == '0') || arrays.contains(data, pos))
if (arrays.contains(data, pos))
{
// some reference token below this position is 0, so the
// value is an array
*current = BasicJsonType(detail::value_t::array);
reserve_elements(*current->m_data.m_value.array, 2, detail::priority_tag<1> {});
current = &current->operator[](array_index<BasicJsonType>(token));
}
else
File diff suppressed because it is too large. Load diff
@@ -55,6 +55,66 @@ using output_adapter_t = std::shared_ptr<output_adapter_protocol<CharType>>;
/// output_vector_sink also appends writes of at least this size with insert()
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
///
/// This sink is not part of the virtual output_adapter_protocol hierarchy: it is
@@ -78,7 +138,7 @@ class output_vector_sink
{
public:
explicit output_vector_sink(std::vector<CharType, AllocatorType>& vec) noexcept
: v(&vec), position(vec.size())
: v(&vec), cursor(vec.data() + vec.size()), limit(cursor)
{}
output_vector_sink(const output_vector_sink&) = delete;
@@ -86,7 +146,7 @@ class output_vector_sink
output_vector_sink& operator=(output_vector_sink&&) = delete;
output_vector_sink(output_vector_sink&& other) noexcept
: v(other.v), position(other.position)
: v(other.v), cursor(other.cursor), limit(other.limit)
{
other.v = nullptr;
}
@@ -95,7 +155,7 @@ class output_vector_sink
{
if (v != nullptr)
{
v->resize(position);
v->resize(position());
}
}
@@ -104,11 +164,11 @@ class output_vector_sink
JSON_HEDLEY_ALWAYS_INLINE
void write_character(CharType c)
{
if (JSON_HEDLEY_UNLIKELY(position == v->size()))
if (JSON_HEDLEY_UNLIKELY(cursor == limit))
{
grow(1);
}
(*v)[position++] = c;
*cursor++ = c;
}
// no JSON_HEDLEY_NON_NULL here: binary_writer legitimately passes a null
@@ -117,7 +177,7 @@ class output_vector_sink
JSON_HEDLEY_ALWAYS_INLINE
void write_characters(const CharType* s, std::size_t length)
{
if (JSON_HEDLEY_UNLIKELY(v->size() - position < length))
if (JSON_HEDLEY_UNLIKELY(space() < length))
{
if (length >= max_reserve_size)
{
@@ -126,10 +186,15 @@ class output_vector_sink
}
grow(length);
}
if (length != 0)
if (length <= 32)
{
std::memcpy(v->data() + position, s, length * sizeof(CharType));
position += length;
copy_short(cursor, s, length, std::integral_constant<bool, sizeof(CharType) == 1>());
cursor += length;
}
else
{
std::memcpy(cursor, s, length * sizeof(CharType));
cursor += length;
}
}
@@ -144,21 +209,37 @@ class output_vector_sink
CharType* reserve(const std::size_t length)
{
JSON_ASSERT(length <= max_reserve_size);
if (JSON_HEDLEY_UNLIKELY(v->size() - position < length))
if (JSON_HEDLEY_UNLIKELY(space() < length))
{
grow(length);
}
return v->data() + position;
return cursor;
}
/// keep the first @a length characters written to the space from reserve()
JSON_HEDLEY_ALWAYS_INLINE
void commit(const std::size_t length) noexcept
{
position += length;
cursor += length;
}
/// nothing to pass on: the characters are in the vector (see output_adapter_sink)
void flush() noexcept
{}
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;
/// kept out of line so that the write functions stay small enough for
/// GCC to inline them into the writer
@@ -166,12 +247,15 @@ class output_vector_sink
void grow(const std::size_t length)
{
// use the capacity the vector already has, else at least double it;
// never less than 64 characters, so that a small value does not
// never less than min_vector_sink_capacity, so that a small value does not
// reallocate for every few bytes
const std::size_t needed = position + length;
v->resize((std::max)(std::size_t{64}, needed <= v->capacity()
const std::size_t written = position();
const std::size_t needed = written + length;
v->resize((std::max)(min_vector_sink_capacity, needed <= v->capacity()
? v->capacity()
: (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
@@ -181,20 +265,24 @@ class output_vector_sink
JSON_HEDLEY_NEVER_INLINE
void append(const CharType* s, const std::size_t length)
{
v->resize(position);
if (v->capacity() - position < length)
const std::size_t written = position();
v->resize(written);
if (v->capacity() - written < length)
{
// grow geometrically, with room for the writes that follow, so
// that the next grow() need not reallocate right away
v->reserve((std::max)(v->capacity() * 2, position + length + max_reserve_size));
v->reserve((std::max)(v->capacity() * 2, written + length + max_reserve_size));
}
v->insert(v->end(), s, s + length);
position = v->size();
cursor = v->data() + v->size();
limit = cursor;
}
std::vector<CharType, AllocatorType>* v;
/// number of characters of *v written so far
std::size_t position;
/// the write position: in *v, at the number of characters written so far
CharType* cursor;
/// the end of *v
CharType* limit;
};
/// output adapter for byte vectors
@@ -277,9 +365,17 @@ class output_string_adapter : public output_adapter_protocol<CharType>
///
/// Wraps the polymorphic output_adapter_t so the same binary_writer template can
/// also target arbitrary adapters (output streams, strings, user-provided
/// adapters) via the `output_adapter`-based overloads. Each write still goes
/// through one virtual call, exactly as before; only the concrete sinks above
/// avoid it.
/// adapters) via the `output_adapter`-based overloads.
///
/// A write through the adapter is a virtual call, and for a stream a call with
/// 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>
class output_adapter_sink
{
@@ -290,36 +386,121 @@ class output_adapter_sink
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)
{
oa->write_character(c);
if (JSON_HEDLEY_UNLIKELY(used == buffer_size))
{
flush();
}
buffer[used++] = c;
}
// no JSON_HEDLEY_NON_NULL: forwards (null, 0) for empty payloads, exactly as
// the type-erased path already did before this sink existed
JSON_HEDLEY_ALWAYS_INLINE
void write_characters(const CharType* s, std::size_t length)
{
oa->write_characters(s, length);
if (JSON_HEDLEY_UNLIKELY(buffer_size - used < 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
/// output_vector_sink::reserve)
CharType* reserve(const std::size_t length) noexcept
JSON_HEDLEY_ALWAYS_INLINE
CharType* reserve(const std::size_t length)
{
JSON_ASSERT(length <= max_reserve_size);
static_cast<void>(length);
return reserved.data();
if (JSON_HEDLEY_UNLIKELY(buffer_size - used < length))
{
flush();
}
return buffer.data() + used;
}
/// write the first @a length characters written to the space from reserve()
void commit(const std::size_t length)
/// keep the first @a length characters written to the space from reserve()
JSON_HEDLEY_ALWAYS_INLINE
void commit(const std::size_t length) noexcept
{
oa->write_characters(reserved.data(), length);
used += 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:
/// 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;
std::array<CharType, max_reserve_size> reserved{};
/// number of characters of @ref buffer not yet passed on
std::size_t used = 0;
std::array<CharType, buffer_size> buffer{};
};
/// @brief whether std::basic_string<CharType> has a non-deprecated std::char_traits
+74 -175
View File
@@ -6321,7 +6321,7 @@ public:
const error_handler_t error_handler = detail::binary_writer_default_error_handler())
{
std::vector<std::uint8_t> result;
result.reserve(detail::binary_reserve_hint(j));
result.reserve(detail::binary_initial_capacity(j));
vector_writer(result, error_handler).write_cbor(j);
return result;
}
@@ -6331,7 +6331,9 @@ public:
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())
{
binary_writer<std::uint8_t>(o, error_handler).write_cbor(j);
binary_writer<std::uint8_t> writer(o, error_handler);
writer.write_cbor(j);
writer.flush();
}
/// @brief create a CBOR serialization of a given JSON value
@@ -6339,7 +6341,9 @@ public:
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())
{
binary_writer<char>(o, error_handler).write_cbor(j);
binary_writer<char> writer(o, error_handler);
writer.write_cbor(j);
writer.flush();
}
/// @brief create a MessagePack serialization of a given JSON value
@@ -6348,7 +6352,7 @@ public:
const error_handler_t error_handler = error_handler_t::keep)
{
std::vector<std::uint8_t> result;
result.reserve(detail::binary_reserve_hint(j));
result.reserve(detail::binary_initial_capacity(j));
vector_writer(result, error_handler).write_msgpack(j);
return result;
}
@@ -6358,7 +6362,9 @@ public:
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)
{
binary_writer<std::uint8_t>(o, error_handler).write_msgpack(j);
binary_writer<std::uint8_t> writer(o, error_handler);
writer.write_msgpack(j);
writer.flush();
}
/// @brief create a MessagePack serialization of a given JSON value
@@ -6366,7 +6372,9 @@ public:
static void to_msgpack(const basic_json& j, detail::output_adapter<char> o,
const error_handler_t error_handler = error_handler_t::keep)
{
binary_writer<char>(o, error_handler).write_msgpack(j);
binary_writer<char> writer(o, error_handler);
writer.write_msgpack(j);
writer.flush();
}
/// @brief create a UBJSON serialization of a given JSON value
@@ -6377,7 +6385,7 @@ public:
const error_handler_t error_handler = detail::binary_writer_default_error_handler())
{
std::vector<std::uint8_t> result;
result.reserve(detail::binary_reserve_hint(j));
result.reserve(detail::binary_initial_capacity(j));
vector_writer(result, error_handler).write_ubjson(j, use_size, use_type);
return result;
}
@@ -6388,7 +6396,9 @@ public:
const bool use_size = false, const bool use_type = false,
const error_handler_t error_handler = detail::binary_writer_default_error_handler())
{
binary_writer<std::uint8_t>(o, error_handler).write_ubjson(j, use_size, use_type);
binary_writer<std::uint8_t> writer(o, error_handler);
writer.write_ubjson(j, use_size, use_type);
writer.flush();
}
/// @brief create a UBJSON serialization of a given JSON value
@@ -6397,7 +6407,9 @@ public:
const bool use_size = false, const bool use_type = false,
const error_handler_t error_handler = detail::binary_writer_default_error_handler())
{
binary_writer<char>(o, error_handler).write_ubjson(j, use_size, use_type);
binary_writer<char> writer(o, error_handler);
writer.write_ubjson(j, use_size, use_type);
writer.flush();
}
/// @brief create a BJData serialization of a given JSON value
@@ -6409,7 +6421,7 @@ public:
const error_handler_t error_handler = detail::binary_writer_default_error_handler())
{
std::vector<std::uint8_t> result;
result.reserve(detail::binary_reserve_hint(j));
result.reserve(detail::binary_initial_capacity(j));
vector_writer(result, error_handler).write_ubjson(j, use_size, use_type, true, true, version);
return result;
}
@@ -6421,7 +6433,9 @@ public:
const bjdata_version_t version = bjdata_version_t::draft2,
const error_handler_t error_handler = detail::binary_writer_default_error_handler())
{
binary_writer<std::uint8_t>(o, error_handler).write_ubjson(j, use_size, use_type, true, true, version);
binary_writer<std::uint8_t> writer(o, error_handler);
writer.write_ubjson(j, use_size, use_type, true, true, version);
writer.flush();
}
/// @brief create a BJData serialization of a given JSON value
@@ -6431,7 +6445,9 @@ public:
const bjdata_version_t version = bjdata_version_t::draft2,
const error_handler_t error_handler = detail::binary_writer_default_error_handler())
{
binary_writer<char>(o, error_handler).write_ubjson(j, use_size, use_type, true, true, version);
binary_writer<char> writer(o, error_handler);
writer.write_ubjson(j, use_size, use_type, true, true, version);
writer.flush();
}
/// @brief create a BSON serialization of a given JSON value
@@ -6440,7 +6456,7 @@ public:
const error_handler_t error_handler = detail::binary_writer_default_error_handler())
{
std::vector<std::uint8_t> result;
result.reserve(detail::binary_reserve_hint(j));
result.reserve(detail::binary_initial_capacity(j));
vector_writer(result, error_handler).write_bson(j);
return result;
}
@@ -6450,7 +6466,9 @@ public:
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())
{
binary_writer<std::uint8_t>(o, error_handler).write_bson(j);
binary_writer<std::uint8_t> writer(o, error_handler);
writer.write_bson(j);
writer.flush();
}
/// @brief create a BSON serialization of a given JSON value
@@ -6458,7 +6476,9 @@ public:
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())
{
binary_writer<char>(o, error_handler).write_bson(j);
binary_writer<char> writer(o, error_handler);
writer.write_bson(j);
writer.flush();
}
/// @brief create a BON8 serialization of a given JSON value
@@ -6466,7 +6486,7 @@ public:
static std::vector<std::uint8_t> to_bon8(const basic_json& j)
{
std::vector<std::uint8_t> result;
result.reserve(detail::binary_reserve_hint(j));
result.reserve(detail::binary_initial_capacity(j));
vector_writer(result).write_bon8(j);
return result;
}
@@ -6475,14 +6495,18 @@ public:
/// @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)
{
binary_writer<std::uint8_t>(o).write_bon8(j);
binary_writer<std::uint8_t> writer(o);
writer.write_bon8(j);
writer.flush();
}
/// @brief create a BON8 serialization of a given JSON value
/// @sa https://json.nlohmann.me/api/basic_json/to_bon8/
static void to_bon8(const basic_json& j, detail::output_adapter<char> o)
{
binary_writer<char>(o).write_bon8(j);
binary_writer<char> writer(o);
writer.write_bon8(j);
writer.flush();
}
/// @brief create a JSON value from an input in CBOR format
@@ -6878,72 +6902,6 @@ public:
/// @name JSON Patch functions
/// @{
private:
/// the members of an operation of a JSON patch that are of interest
struct patch_members
{
basic_json* op = nullptr;
basic_json* path = nullptr;
basic_json* value = nullptr;
basic_json* from = nullptr;
};
/// @brief find the members "op", "path", "value", and "from" of the object @a val in one pass
static patch_members find_patch_members(const basic_json& val, std::true_type /*key_type is string_t*/)
{
patch_members members;
for (auto& member : *val.m_data.m_value.object)
{
const string_t& key = member.first;
switch (key.size())
{
case 2:
if (key == "op")
{
members.op = &member.second;
}
break;
case 4:
if (key == "path")
{
members.path = &member.second;
}
else if (key == "from")
{
members.from = &member.second;
}
break;
case 5:
if (key == "value")
{
members.value = &member.second;
}
break;
default:
break;
}
}
return members;
}
/// @brief find the members by lookup for key types other than string_t
static patch_members find_patch_members(const basic_json& val, std::false_type /*key_type is not string_t*/)
{
const auto find_member = [&val](const char* name) -> basic_json *
{
const auto it = val.m_data.m_value.object->find(string_t(name));
return it != val.m_data.m_value.object->end() ? &it->second : nullptr;
};
patch_members members;
members.op = find_member("op");
members.path = find_member("path");
members.value = find_member("value");
members.from = find_member("from");
return members;
}
public:
/// @brief applies a JSON patch in-place without copying the object
/// @sa https://json.nlohmann.me/api/basic_json/patch/
void patch_inplace(const basic_json& json_patch)
@@ -7117,18 +7075,18 @@ public:
// a diff creates them: apply the operation and return true; return
// false, with nothing changed, for all others
string_t token;
typename json_pointer::template plain_steps<basic_json> steps;
const auto try_plain = [&result, &token, &steps](patch_operations operation, const string_t& path, const patch_members & members, const basic_json & val)
const string_t value_key = "value";
const auto try_plain = [&result, &token, &value_key](patch_operations operation, const string_t& path, const basic_json & val)
{
if (path.empty())
if (path.empty() || path.find_first_of('~') != string_t::npos)
{
return false;
}
// a missing "value" is reported below
const basic_json* const value_ptr = members.value;
const basic_json& value = value_ptr != nullptr ? *value_ptr : val;
if (operation != patch_operations::remove && value_ptr == nullptr)
const auto value_it = val.m_data.m_value.object->find(value_key);
const basic_json& value = value_it != val.m_data.m_value.object->end() ? value_it->second : val;
if (operation != patch_operations::remove && value_it == val.m_data.m_value.object->end())
{
return false;
}
@@ -7137,7 +7095,7 @@ public:
{
case patch_operations::replace:
{
basic_json* const target = json_pointer::resolve_plain(&result, path, path.size(), token, steps);
basic_json* const target = json_pointer::resolve_plain(&result, path, path.size(), token);
if (target == nullptr)
{
return false;
@@ -7148,7 +7106,7 @@ public:
case patch_operations::test:
{
const basic_json* const target = json_pointer::resolve_plain(&result, path, path.size(), token, steps);
const basic_json* const target = json_pointer::resolve_plain(&result, path, path.size(), token);
// a failing test is reported below
return target != nullptr && *target == value;
}
@@ -7168,17 +7126,13 @@ public:
}
--slash;
basic_json* const parent = json_pointer::resolve_plain(&result, path, slash, token, steps);
basic_json* const parent = json_pointer::resolve_plain(&result, path, slash, token);
if (parent == nullptr)
{
return false;
}
token.clear();
token.append(path.data() + slash + 1, path.size() - slash - 1);
if (token.find_first_of('~') != string_t::npos)
{
return false;
}
if (operation == patch_operations::add)
{
@@ -7234,32 +7188,31 @@ public:
}
};
// the name of the first member to check
const string_t op_member = "op";
// iterate and apply the operations
for (const auto& val : json_patch)
{
// wrapper to get a value for an operation
const auto get_value = [&val](basic_json * found,
const string_t& op,
const char* member,
const auto get_value = [&val](const string_t& op,
const string_t& member,
bool string_type) -> basic_json &
{
// find value
auto it = val.m_data.m_value.object->find(member);
// check if the desired value is present
if (JSON_HEDLEY_UNLIKELY(found == nullptr))
if (JSON_HEDLEY_UNLIKELY(it == val.m_data.m_value.object->end()))
{
detail::throw_patch_missing_member(op, string_t(member), false, val);
detail::throw_patch_missing_member(op, member, false, val);
}
// check if the result is of type string
if (JSON_HEDLEY_UNLIKELY(string_type && !found->is_string()))
if (JSON_HEDLEY_UNLIKELY(string_type && !it->second.is_string()))
{
detail::throw_patch_missing_member(op, string_t(member), true, val);
detail::throw_patch_missing_member(op, member, true, val);
}
// no error: return value
return *found;
return it->second;
};
// type check: every element of the array must be an object
@@ -7268,30 +7221,26 @@ public:
detail::throw_patch_not_an_array(val);
}
const patch_members members = find_patch_members(val, std::is_same<typename object_t::key_type, string_t> {});
// collect mandatory members
const string_t& op = *get_value(members.op, op_member, "op", true).m_data.m_value.string;
const string_t& path = *get_value(members.path, op, "path", true).m_data.m_value.string;
const string_t& op = *get_value("op", "op", true).m_data.m_value.string;
const string_t& path = *get_value(op, "path", true).m_data.m_value.string;
const patch_operations operation = get_op(op);
// the usual operations on a plain pointer are applied without
// building the pointer's reference tokens; anything else (and
// every error) is handled below
if (try_plain(operation, path, members, val))
if (try_plain(operation, path, val))
{
continue;
}
// the general operations may change any container
steps.reset();
json_pointer ptr(path);
switch (operation)
{
case patch_operations::add:
{
operation_add(ptr, get_value(members.value, "add", "value", false));
operation_add(ptr, get_value("add", "value", false));
break;
}
@@ -7304,13 +7253,13 @@ public:
case patch_operations::replace:
{
// the "path" location must exist - use at()
result.at(ptr) = get_value(members.value, "replace", "value", false);
result.at(ptr) = get_value("replace", "value", false);
break;
}
case patch_operations::move:
{
const string_t& from_path = *get_value(members.from, "move", "from", true).m_data.m_value.string;
const string_t& from_path = *get_value("move", "from", true).m_data.m_value.string;
json_pointer from_ptr(from_path);
if (JSON_HEDLEY_UNLIKELY(is_proper_prefix(from_ptr, ptr)))
@@ -7333,7 +7282,7 @@ public:
case patch_operations::copy:
{
const string_t& from_path = *get_value(members.from, "copy", "from", true).m_data.m_value.string;
const string_t& from_path = *get_value("copy", "from", true).m_data.m_value.string;
const json_pointer from_ptr(from_path);
// The copy is functionally identical to an "add"
@@ -7351,7 +7300,7 @@ public:
{
// check if "value" matches the one at "path"
// the "path" location must exist - use at()
success = (result.at(ptr) == get_value(members.value, "test", "value", false));
success = (result.at(ptr) == get_value("test", "value", false));
}
JSON_INTERNAL_CATCH (out_of_range&)
{
@@ -7443,18 +7392,13 @@ public:
/// @brief append an operation @a op for @a path to @a result, with @a value if it is not null
static void diff_operation(basic_json& result, const char* op, const string_t& path, const basic_json* value)
{
// the keys are distinct and in the order of std::map; the members are
// added without looking them up
basic_json entry(value_t::object);
auto& members = *entry.m_data.m_value.object;
typename object_t::iterator hint = members.end();
json_pointer::add_flattened(members, hint, string_t("op"), string_t(op), detail::priority_tag<2> {});
json_pointer::add_flattened(members, hint, string_t("path"), path, detail::priority_tag<2> {});
entry["op"] = op;
entry["path"] = path;
if (value != nullptr)
{
json_pointer::add_flattened(members, hint, string_t("value"), *value, detail::priority_tag<2> {});
entry["value"] = *value;
}
entry.set_parents();
result.push_back(std::move(entry));
}
@@ -7476,50 +7420,6 @@ public:
diff_operation(result, "add", path, &value);
}
/// @brief the values in the target of the members of a source object, for the
/// objects of up to 16 members without allocating
///
/// reserve() must be called with the number of values that are added.
class diff_target_values
{
public:
diff_target_values() = default;
diff_target_values(const diff_target_values&) = delete;
diff_target_values& operator=(const diff_target_values&) = delete;
~diff_target_values() = default;
void reserve(const std::size_t n)
{
if (n > inline_size)
{
large.resize(n);
items = large.data();
}
}
void push_back(const basic_json* value)
{
items[count++] = value;
}
std::size_t size() const noexcept
{
return count;
}
const basic_json* operator[](const std::size_t i) const noexcept
{
return items[i];
}
private:
static constexpr std::size_t inline_size = 16;
std::size_t count = 0;
const basic_json** items = inline_items;
std::vector<const basic_json*> large{}; // NOLINT(readability-redundant-member-init)
const basic_json* inline_items[inline_size]; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init)
};
/// @brief append "/" and the decimal representation of @a index to @a path
static void diff_append_index(string_t& path, std::size_t index)
{
@@ -7600,9 +7500,8 @@ public:
operations that remove every member of @a source and add every member of
@a target to @a result, and returns false.
*/
template<typename TargetValues>
static bool diff_object_keys(basic_json& result, const basic_json& source, const basic_json& target,
const string_t& path, TargetValues& target_values,
const string_t& path, std::vector<const basic_json*>& target_values,
std::vector<const_iterator>& added)
{
// first pass: one lookup per source key
@@ -7816,7 +7715,7 @@ public:
case value_t::object:
{
diff_target_values target_values;
std::vector<const basic_json*> target_values;
std::vector<const_iterator> added;
if (diff_object_keys(result, source, target, path, target_values, added))
{
+2040 -609
View File
File diff suppressed because it is too large. Load diff
+272
View File
@@ -492,3 +492,275 @@ TEST_CASE("binary_reserve_hint never over-reserves")
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,6 +285,55 @@ 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);
}
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")
{
// an ND-array element beyond 64 bits does not fit any dtype, so the