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 4330 additions and 544 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_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,
+27 -52
View File
@@ -8,7 +8,7 @@
#pragma once #pragma once
#include <algorithm> // all_of, copy #include <algorithm> // all_of
#include <array> // array #include <array> // array
#include <cctype> // isdigit #include <cctype> // isdigit
#include <cerrno> // errno, ERANGE #include <cerrno> // errno, ERANGE
@@ -82,34 +82,12 @@ class json_pointer
{ {
// one buffer: every token adds its escaped form (at least its length) and a slash // one buffer: every token adds its escaped form (at least its length) and a slash
std::size_t length = 0; std::size_t length = 0;
bool plain = true;
for (const auto& reference_token : reference_tokens) for (const auto& reference_token : reference_tokens)
{ {
length += reference_token.size() + 1; length += reference_token.size() + 1;
const char* const data = reference_token.data();
for (std::size_t i = 0, size = reference_token.size(); plain && i < size; ++i)
{
plain = data[i] != '~' && data[i] != '/';
}
} }
string_t result; string_t result;
if (JSON_HEDLEY_LIKELY(plain))
{
// nothing to escape: the tokens are copied into the string
result.resize(length);
if (length != 0)
{
char* out = &result[0];
for (const auto& reference_token : reference_tokens)
{
*out++ = '/';
out = std::copy(reference_token.data(), reference_token.data() + reference_token.size(), out);
}
}
return result;
}
result.reserve(length); result.reserve(length);
for (const auto& reference_token : reference_tokens) for (const auto& reference_token : reference_tokens)
{ {
@@ -1220,32 +1198,33 @@ class json_pointer
} }
result.reserve(token_count); result.reserve(token_count);
// extract the reference tokens: every one runs from after a slash to // extract the reference tokens:
// the next slash or the end of the string; a token without a '~' (the // - slash: position of the last read slash (or end of string)
// common case) is stored as it is, others are checked and unescaped // - start: position after the previous slash
const char* const data = reference_string.data(); for (
const char* const end = data + size; // search for the first slash after the first character
for (const char* start = data + 1;; ++start) std::size_t slash = reference_string.find_first_of('/', 1),
// set the beginning of the first reference token
start = 1;
// we can stop if start == 0 (if slash == string_t::npos)
start != 0;
// set the beginning of the next reference token
// (will eventually be 0 if slash == string_t::npos)
start = (slash == string_t::npos) ? 0 : slash + 1,
// find next slash
slash = reference_string.find_first_of('/', start))
{ {
const char* slash = start; // use the text between the beginning of the reference token
bool escaped = false; // (start) and the last slash (slash).
while (slash != end && *slash != '/') const auto count = (slash == string_t::npos ? reference_string.size() : slash) - start;
{ auto reference_token = string_t(reference_string.data() + start, count);
escaped = escaped || *slash == '~';
++slash;
}
const auto count = static_cast<std::size_t>(slash - start); // check reference tokens are properly escaped; a token without
if (JSON_HEDLEY_LIKELY(!escaped)) // a '~' (the common case) needs neither check nor unescaping
std::size_t pos = reference_token.find_first_of('~');
if (pos != string_t::npos)
{ {
result.emplace_back(start, count); for (; pos != string_t::npos; pos = reference_token.find_first_of('~', pos + 1))
}
else
{
auto reference_token = string_t(start, count);
// check reference tokens are properly escaped
for (std::size_t pos = reference_token.find_first_of('~'); pos != string_t::npos; pos = reference_token.find_first_of('~', pos + 1))
{ {
JSON_ASSERT(reference_token[pos] == '~'); JSON_ASSERT(reference_token[pos] == '~');
@@ -1259,14 +1238,10 @@ class json_pointer
} }
detail::unescape(reference_token); detail::unescape(reference_token);
result.push_back(std::move(reference_token));
} }
if (slash == end) // finally, store the reference token
{ result.push_back(std::move(reference_token));
break;
}
start = slash;
} }
return result; return result;
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() /// 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
@@ -78,7 +138,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), position(vec.size()) : v(&vec), cursor(vec.data() + vec.size()), limit(cursor)
{} {}
output_vector_sink(const output_vector_sink&) = delete; 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& operator=(output_vector_sink&&) = delete;
output_vector_sink(output_vector_sink&& other) noexcept 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; other.v = nullptr;
} }
@@ -95,7 +155,7 @@ class output_vector_sink
{ {
if (v != nullptr) if (v != nullptr)
{ {
v->resize(position); v->resize(position());
} }
} }
@@ -104,11 +164,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(position == v->size())) if (JSON_HEDLEY_UNLIKELY(cursor == limit))
{ {
grow(1); grow(1);
} }
(*v)[position++] = c; *cursor++ = 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
@@ -117,7 +177,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(v->size() - position < length)) if (JSON_HEDLEY_UNLIKELY(space() < length))
{ {
if (length >= max_reserve_size) if (length >= max_reserve_size)
{ {
@@ -126,10 +186,15 @@ class output_vector_sink
} }
grow(length); grow(length);
} }
if (length != 0) if (length <= 32)
{ {
std::memcpy(v->data() + position, s, length * sizeof(CharType)); copy_short(cursor, s, length, std::integral_constant<bool, sizeof(CharType) == 1>());
position += length; 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) CharType* reserve(const std::size_t length)
{ {
JSON_ASSERT(length <= max_reserve_size); JSON_ASSERT(length <= max_reserve_size);
if (JSON_HEDLEY_UNLIKELY(v->size() - position < length)) if (JSON_HEDLEY_UNLIKELY(space() < length))
{ {
grow(length); grow(length);
} }
return v->data() + position; return cursor;
} }
/// 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
{ {
position += length; cursor += 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
@@ -166,12 +247,15 @@ 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 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 // reallocate for every few bytes
const std::size_t needed = position + length; const std::size_t written = position();
v->resize((std::max)(std::size_t{64}, needed <= v->capacity() const std::size_t needed = written + length;
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
@@ -181,20 +265,24 @@ 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)
{ {
v->resize(position); const std::size_t written = position();
if (v->capacity() - position < length) v->resize(written);
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, position + length + max_reserve_size)); v->reserve((std::max)(v->capacity() * 2, written + length + max_reserve_size));
} }
v->insert(v->end(), s, s + length); v->insert(v->end(), s, s + length);
position = v->size(); cursor = v->data() + v->size();
limit = cursor;
} }
std::vector<CharType, AllocatorType>* v; std::vector<CharType, AllocatorType>* v;
/// number of characters of *v written so far /// the write position: in *v, at the number of characters written so far
std::size_t position; CharType* cursor;
/// the end of *v
CharType* limit;
}; };
/// output adapter for byte vectors /// 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 /// 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. Each write still goes /// adapters) via the `output_adapter`-based overloads.
/// through one virtual call, exactly as before; only the concrete sinks above ///
/// avoid it. /// 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> template<typename CharType>
class output_adapter_sink class output_adapter_sink
{ {
@@ -290,36 +386,121 @@ 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)
{ {
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 // 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)
{ {
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 /// space for writing up to @a length characters (see
/// output_vector_sink::reserve) /// 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); JSON_ASSERT(length <= max_reserve_size);
static_cast<void>(length); if (JSON_HEDLEY_UNLIKELY(buffer_size - used < length))
return reserved.data(); {
flush();
}
return buffer.data() + used;
} }
/// write the first @a length characters written to the space from reserve() /// keep the first @a length characters written to the space from reserve()
void commit(const std::size_t length) 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: 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;
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 /// @brief whether std::basic_string<CharType> has a non-deprecated std::char_traits
+42 -18
View File
@@ -6321,7 +6321,7 @@ public:
const error_handler_t error_handler = detail::binary_writer_default_error_handler()) const error_handler_t error_handler = detail::binary_writer_default_error_handler())
{ {
std::vector<std::uint8_t> result; std::vector<std::uint8_t> result;
result.reserve(detail::binary_reserve_hint(j)); result.reserve(detail::binary_initial_capacity(j));
vector_writer(result, error_handler).write_cbor(j); vector_writer(result, error_handler).write_cbor(j);
return result; return result;
} }
@@ -6331,7 +6331,9 @@ 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>(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 /// @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, 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>(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 /// @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) 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_reserve_hint(j)); result.reserve(detail::binary_initial_capacity(j));
vector_writer(result, error_handler).write_msgpack(j); vector_writer(result, error_handler).write_msgpack(j);
return result; return result;
} }
@@ -6358,7 +6362,9 @@ 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>(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 /// @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, 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>(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 /// @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()) 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_reserve_hint(j)); result.reserve(detail::binary_initial_capacity(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;
} }
@@ -6388,7 +6396,9 @@ 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>(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 /// @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 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>(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 /// @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()) 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_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); vector_writer(result, error_handler).write_ubjson(j, use_size, use_type, true, true, version);
return result; return result;
} }
@@ -6421,7 +6433,9 @@ 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>(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 /// @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 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>(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 /// @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()) 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_reserve_hint(j)); result.reserve(detail::binary_initial_capacity(j));
vector_writer(result, error_handler).write_bson(j); vector_writer(result, error_handler).write_bson(j);
return result; return result;
} }
@@ -6450,7 +6466,9 @@ 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>(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 /// @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, 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>(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 /// @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) 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_reserve_hint(j)); result.reserve(detail::binary_initial_capacity(j));
vector_writer(result).write_bon8(j); vector_writer(result).write_bon8(j);
return result; return result;
} }
@@ -6475,14 +6495,18 @@ 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>(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 /// @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>(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 /// @brief create a JSON value from an input in CBOR format
+2002 -272
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); 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); 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