Compare commits

..
Author SHA1 Message Date
Niels Lohmann 964b433dc7 Force inlining of the serializer helpers only in optimized builds
The seven serializer helpers marked JSON_HEDLEY_ALWAYS_INLINE are inlined
at -O0 as well, and GCC and Clang give every inlined copy its own stack
slots there. That made the recursive frame of dump_container() 2304 bytes
with Clang 21 (2048 with GCC 16), against 480 bytes in 3.12.0, so dump()
of a value nested beyond the default recursion depth limit needed 288 KiB
of stack in an unoptimized build and crashed in a 128 KiB musl thread.

Define JSON_INTERNAL_SERIALIZER_INLINE as JSON_HEDLEY_ALWAYS_INLINE when
__OPTIMIZE__ (or _MSC_VER) is defined and as plain inline otherwise, and
use it for these helpers. The frame drops to 320 bytes and dump() needs
48 KiB at the default limit. The -O2 object code is unchanged with Clang 21
and GCC 16.

Update the stack tables in json_recursion_depth_limit.md and
untrusted_input.md: copying and conversion are now the most demanding
operations at -O0.

Fixes #6116.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-13 10:25:10 +01:00
13 changed files with 122 additions and 657 deletions

No files matched your search

@@ -89,10 +89,8 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va
- Throws [out_of_range.406](../../home/exceptions.md#jsonexceptionout_of_range406) if a high-precision number, or a - Throws [out_of_range.406](../../home/exceptions.md#jsonexceptionout_of_range406) if a high-precision number, or a
float64 when `number_float_t` is `float`, does not fit into `number_float_t` float64 when `number_float_t` is `float`, does not fit into `number_float_t`
- Throws [out_of_range.408](../../home/exceptions.md#jsonexceptionout_of_range408) if the count of an optimized - Throws [out_of_range.408](../../home/exceptions.md#jsonexceptionout_of_range408) if the count of an optimized
container exceeds the capacity of the container (or of `std::size_t`; an array of type `N` is empty, so only the container exceeds the capacity of the container (or of `std::size_t`), or if an optimized array of `Z`, `T`, or `F` has
latter applies to it), if an optimized array of `Z`, `T`, or `F` has more than 1,048,576 elements, or if the more than 1,048,576 elements
optimized arrays of `Z`, `T`, and `F` in the input together have more than 1,048,576 elements plus 4 for every byte
read so far
## Complexity ## Complexity
@@ -134,9 +132,6 @@ Linear in the size of the input.
- Extended container support (1) to include types with lvalue-only ADL `begin`/`end` (matching `std::begin`/`std::end` semantics) in version 3.13.0. - Extended container support (1) to include types with lvalue-only ADL `begin`/`end` (matching `std::begin`/`std::end` semantics) in version 3.13.0.
- Extended overload (2) to accept heterogeneous iterator+sentinel pairs (C++20 ranges support) in version 3.13.0. - Extended overload (2) to accept heterogeneous iterator+sentinel pairs (C++20 ranges support) in version 3.13.0.
- Added `error_handler` parameter in version 3.13.0. - Added `error_handler` parameter in version 3.13.0.
- Report `start_array(0)` to a SAX handler for an optimized array of type `N` (`[$N#n`), whatever its count, in version
3.13.0.
- Limited the total number of elements of optimized arrays of `Z`, `T`, and `F` in one input in version 3.13.0.
!!! warning "Deprecation" !!! warning "Deprecation"
@@ -117,8 +117,5 @@ Linear in the size of the JSON value `j`.
- Added `error_handler` parameter in version 3.13.0. Its default, `keep`, writes the bytes of a string or object key - Added `error_handler` parameter in version 3.13.0. Its default, `keep`, writes the bytes of a string or object key
that is not valid UTF-8 unchanged, as before; `strict` (the default if that is not valid UTF-8 unchanged, as before; `strict` (the default if
[`JSON_STRICT_BINARY_UTF8`](../macros/json_strict_binary_utf8.md) is enabled) throws `type_error.316`. [`JSON_STRICT_BINARY_UTF8`](../macros/json_strict_binary_utf8.md) is enabled) throws `type_error.316`.
- Writes arrays of `Z`, `T`, or `F` without the `$` type annotation (`[#n Z Z ...`) once their elements in one call
add up to more than 1,048,576, in version 3.13.0, so that `from_ubjson` can read the output back. Such output is
larger: 270,000 rows of 100 `null` values take 26.8 MB instead of 1.35 MB.
- Throws `type_error.321` for a discarded value since version 3.13.0; previously, a discarded value nested in an - Throws `type_error.321` for a discarded value since version 3.13.0; previously, a discarded value nested in an
array or object was silently skipped, producing invalid UBJSON. array or object was silently skipped, producing invalid UBJSON.
@@ -27,7 +27,7 @@ comparison of two [`json_view`](../basic_json_view/index.md) documents has a fix
and does not depend on it either. and does not depend on it either.
The call stack that these operations need grows with the limit: roughly *a few kilobytes plus a cost per level times the The call stack that these operations need grows with the limit: roughly *a few kilobytes plus a cost per level times the
limit*, where the cost per level is up to about 2.3 KiB in an unoptimized and up to about 0.5 KiB in an optimized build limit*, where the cost per level is up to about 1.8 KiB in an unoptimized and up to about 0.5 KiB in an optimized build
(see [Stack needed](#stack-needed)). The default of `128` levels is tuned for threads with ordinary stacks of a few (see [Stack needed](#stack-needed)). The default of `128` levels is tuned for threads with ordinary stacks of a few
hundred kilobytes or more. Set a lower limit if your threads have small stacks (RTOS tasks, fibers, threads of the musl hundred kilobytes or more. Set a lower limit if your threads have small stacks (RTOS tasks, fibers, threads of the musl
C C
@@ -67,7 +67,7 @@ When the macro is not defined, the library defines it to its default value.
the levels that are descended into are counted, so the count never exceeds the limit, not even when user code (a the levels that are descended into are counted, so the count never exceeds the limit, not even when user code (a
copy constructor of a custom base class, say) copies a value while a copy is going on. A byte holds `255` at copy constructor of a custom base class, say) copies a value while a copy is going on. A byte holds `255` at
most, and the limit stays below that, so that the count cannot wrap around. (More levels would hardly be most, and the limit stays below that, so that the count cannot wrap around. (More levels would hardly be
useful: `254` levels need up to 576 KiB of stack in an unoptimized build already, twice as much as the useful: `254` levels need up to 448 KiB of stack in an unoptimized build already, twice as much as the
default.) default.)
### Stack needed ### Stack needed
@@ -83,7 +83,7 @@ Unoptimized (`-O0`):
| Operation | limit 16 | limit 32 | limit 64 | limit 128 | limit 254 | | Operation | limit 16 | limit 32 | limit 64 | limit 128 | limit 254 |
|---|---:|---:|---:|---:|---:| |---|---:|---:|---:|---:|---:|
| `dump()` and `dump(indent)`, `operator<<` | 48 / 32 | 80 / 64 | 144 / 128 | 288 / 256 | 576 / 512 | | `dump()` and `dump(indent)`, `operator<<` | 16 / 16 | 16 / 16 | 32 / 32 | 48 / 48 | 96 / 96 |
| copy construction and assignment | 32 / 16 | 48 / 32 | 112 / 80 | 224 / 144 | 432 / 304 | | copy construction and assignment | 32 / 16 | 48 / 32 | 112 / 80 | 224 / 144 | 432 / 304 |
| `==` | 16 / 16 | 16 / 16 | 16 / 16 | 48 / 32 | 96 / 48 | | `==` | 16 / 16 | 16 / 16 | 16 / 16 | 48 / 32 | 96 / 48 |
| `<=>` (C++20) | 16 / 16 | 16 / 16 | 32 / 32 | 64 / 64 | 144 / 128 | | `<=>` (C++20) | 16 / 16 | 16 / 16 | 32 / 32 | 64 / 64 | 144 / 128 |
@@ -123,23 +123,24 @@ The rule of thumb is
> stack needed ≈ a few KiB + cost per level × limit > stack needed ≈ a few KiB + cost per level × limit
with a cost per level of up to 2.3 KiB (`dump` at `-O0` with Clang; 2.1 KiB with GCC), 1.7 KiB for copying and merging with a cost per level of up to 1.8 KiB (conversion to another `basic_json` type at `-O0` with Clang; 1.3 KiB with
(1.3 KiB with GCC), and 0.1 to 0.5 KiB for most other operations at `-O0`; and 0.5 KiB or less at `-O2`. The numbers GCC), 1.7 KiB for copying and merging (1.3 KiB with GCC), 0.4 KiB for `dump`, and 0.1 to 0.5 KiB for most other
come from the difference between the limits `128` and `254` in the tables, so they are rough: other compilers, operations at `-O0`; and 0.5 KiB or less at `-O2`. The numbers come from the difference between the limits `128` and
other targets (the frames of x86-64 code are about as large), other standard libraries, `JSON_DIAGNOSTICS`, and `254` in the tables, so they are rough: other compilers, other targets (the frames of x86-64 code are about as large),
sanitizers change them. Measure with your own tool chain (see the example below) rather than relying on the tables. other standard libraries, `JSON_DIAGNOSTICS`, and sanitizers change them. Measure with your own tool chain (see the
The stack that your own code and the operating system need comes on top of these numbers. example below) rather than relying on the tables. The stack that your own code and the operating system need comes on
top of these numbers.
Which limit to choose, for the stack that you can spare for the library (the largest limit of those measured Which limit to choose, for the stack that you can spare for the library (the largest limit of those measured
for which the slowest operation needs at most that much): for which the slowest operation needs at most that much):
| Stack for the library | optimized (`-O2`) | unoptimized (`-O0`) | | Stack for the library | optimized (`-O2`) | unoptimized (`-O0`) |
|---|---:|---:| |---|---:|---:|
| 32 KiB | 64 | 0 | | 32 KiB | 64 | 16 |
| 64 KiB | 128 | 16 | | 64 KiB | 128 | 32 |
| 128 KiB (the default stack of threads on musl) | 254 | 32 | | 128 KiB (the default stack of threads on musl) | 254 | 64 |
| 256 KiB | 254 | 64 | | 256 KiB | 254 | 128 |
| 512 KiB | 254 | 128 | | 512 KiB | 254 | 254 |
| 1 MiB or more | 254 | 254 | | 1 MiB or more | 254 | 254 |
### Speed ### Speed
@@ -122,18 +122,9 @@ The library uses the following mapping from JSON values types to UBJSON types ac
already is the value. Its declared count is therefore the only thing that decides how much memory the receiving side already is the value. Its declared count is therefore the only thing that decides how much memory the receiving side
allocates, and a handful of bytes can describe billions of elements. `from_ubjson` rejects such an array with allocates, and a handful of bytes can describe billions of elements. `from_ubjson` rejects such an array with
[`out_of_range.408`](../../home/exceptions.md#jsonexceptionout_of_range408) when the count exceeds 1,048,576 [`out_of_range.408`](../../home/exceptions.md#jsonexceptionout_of_range408) when the count exceeds 1,048,576
(`1 << 20`), or when the counts of all such arrays in the input add up to more than 1,048,576 plus 4 for every byte (`1 << 20`), and `to_ubjson` writes longer arrays of these types in the counted but untyped form: the `$` type marker is
read so far. `to_ubjson` therefore writes an array of these types in the counted but untyped form when it is longer dropped, the `#` count is kept, and every element is written with its own marker (`[#l 1048577 Z Z ...`), so any value
than that, or when it would bring the total of the typed arrays written so far above 1,048,576: the `$` type marker it produces can be read back. Objects and untyped counted arrays are not subject to this limit.
is dropped, the `#` count is kept, and every element is written with its own marker (`[#l 1048577 Z Z ...`), so
everything it produces can be read back. Only input from another encoder, or written by version 3.12.0 and earlier of
this library, can exceed the limit: for example, 1,100 rows of 1,000 `null` values or 210,000 rows of 25, each with
a `$Z` annotation. Version 3.12.0 writes such a row in 5 bytes (6 above 255 elements), so rows of 20 or fewer such
values are never rejected. Objects and untyped counted arrays are not subject to this limit.
An optimized array of type `N` (no-op) is empty whatever its count is, because a no-op is not a value. Its count is
not passed on: `sax_parse` reports `start_array(0)` (versions before 3.13.0 reported the declared count), and the DOM
parser reserves nothing for it.
!!! info "Binary values" !!! info "Binary values"
@@ -189,11 +180,10 @@ The library maps UBJSON types to JSON value types as follows:
Every well-formed UBJSON value can be converted to a JSON value, with these exceptions: a high-precision number Every well-formed UBJSON value can be converted to a JSON value, with these exceptions: a high-precision number
outside the range of `number_float_t` ([`out_of_range.406`](../../home/exceptions.md#jsonexceptionout_of_range406)), outside the range of `number_float_t` ([`out_of_range.406`](../../home/exceptions.md#jsonexceptionout_of_range406)),
optimized arrays of `Z`, `T`, or `F` with more elements than the limit allows (1,048,576 plus 4 per byte read), an optimized array of `Z`, `T`, or `F` with more than 1,048,576 elements, and a count that exceeds the capacity of the
and a count that exceeds the capacity of the container container ([`out_of_range.408`](../../home/exceptions.md#jsonexceptionout_of_range408)). High-precision numbers that
([`out_of_range.408`](../../home/exceptions.md#jsonexceptionout_of_range408)). High-precision numbers that do not do not fit an integer type are stored as floating-point numbers and may lose precision. Ill-formed input is rejected
fit an integer type are stored as floating-point numbers and may lose precision. Ill-formed input is rejected with a with a parse error.
parse error.
!!! info "Duplicate object keys" !!! info "Duplicate object keys"
@@ -64,13 +64,13 @@ stack for the nesting.)
The call stack still needs room for the levels that are recursed into, and the default limit assumes a stack of ordinary The call stack still needs room for the levels that are recursed into, and the default limit assumes a stack of ordinary
size. The stack that an operation needs is roughly proportional to the limit; with the default of 128 levels, the most size. The stack that an operation needs is roughly proportional to the limit; with the default of 128 levels, the most
demanding operation (`dump` in an unoptimized build) needed up to 288 KiB, and an optimized build needed 48 KiB or less, demanding operations (copying a value and converting it to another `basic_json` type, in an unoptimized build) needed
in measurements on macOS with arm64. The most demanding operation needed this much stack at the limits `0`, `32`, up to 224 KiB, and an optimized build needed 48 KiB or less, in measurements on macOS with arm64. The most demanding
`128`, and `254`: operation needed this much stack at the limits `0`, `32`, `128`, and `254`:
| Stack needed, in KiB | limit 0 | limit 32 | limit 128 (default) | limit 254 | | Stack needed, in KiB | limit 0 | limit 32 | limit 128 (default) | limit 254 |
|---|---:|---:|---:|---:| |---|---:|---:|---:|---:|
| unoptimized build (`-O0`) | 16 or less | 80 | 288 | 576 | | unoptimized build (`-O0`) | 16 or less | 64 | 224 | 448 |
| optimized build (`-O2`) | 16 or less | 16 or less | 48 | 112 | | optimized build (`-O2`) | 16 or less | 16 or less | 48 | 112 |
A thread with a smaller stack (a task of a real-time operating system, a fiber, or a thread of the musl C library, whose A thread with a smaller stack (a task of a real-time operating system, a fiber, or a thread of the musl C library, whose
@@ -94,22 +94,10 @@ For the binary formats, an announced size is never trusted outright:
(reported as a parse error) instead of triggering an oversized allocation. (reported as a parse error) instead of triggering an oversized allocation.
- When an array announces its number of elements and the array container supports `reserve()` (as `#!cpp std::vector`, - When an array announces its number of elements and the array container supports `reserve()` (as `#!cpp std::vector`,
the default, does), the library reserves storage for at most 16384 of them upfront, regardless of how large the the default, does), the library reserves storage for at most 16384 of them upfront, regardless of how large the
announced count is; further elements still grow the container normally as they are read. A truncated input whose announced count is; further elements still grow the container normally as they are read.
count is larger than its remaining bytes therefore reserves at most that much before the parse error is reported.
Every array element takes at least one byte of input, except in the [UBJSON](../binary_formats/ubjson.md) arrays of
type `Z`, `T` or `F` described below.
- An announced array or object size that exceeds what the target container could ever hold (its `max_size()`) is - An announced array or object size that exceeds what the target container could ever hold (its `max_size()`) is
rejected immediately as [`out_of_range.408`](../../home/exceptions.md#jsonexceptionout_of_range408), without rejected immediately as [`out_of_range.408`](../../home/exceptions.md#jsonexceptionout_of_range408), without
attempting to allocate anything. attempting to allocate anything.
- A UBJSON array of the no-op type (`[$N#<n>`) is empty whatever count it declares: a SAX handler is told
`start_array(0)` and the DOM parser reserves nothing.
- A UBJSON array of type `Z`, `T` or `F` has no payload, so its announced count is not bounded by the input. One array
may announce at most 1,048,576 elements, and all such arrays of one input together may contain at most 1,048,576
elements plus 4 for every byte read so far (checked when each array's header has been read); beyond that the input is
rejected as [`out_of_range.408`](../../home/exceptions.md#jsonexceptionout_of_range408). For a DOM that bounds the memory these
arrays can need to about 80 bytes per byte of input plus about 33 MB for the first 1,048,576 elements; a SAX handler
receives at most that many events. Every other optimized array element, and every UBJSON object member (it has a
key), takes at least one byte of input.
## Strings ## Strings
+5 -14
View File
@@ -1050,17 +1050,11 @@ The size of an array or object in a [binary format](../features/binary_formats/i
the size following `#` for [UBJSON](../features/binary_formats/ubjson.md)/[BJData](../features/binary_formats/bjdata.md), the size following `#` for [UBJSON](../features/binary_formats/ubjson.md)/[BJData](../features/binary_formats/bjdata.md),
or the encoded length for [CBOR](../features/binary_formats/cbor.md). or the encoded length for [CBOR](../features/binary_formats/cbor.md).
The exception is also thrown for [UBJSON](../features/binary_formats/ubjson.md) arrays of a type that is encoded by The exception is also thrown for a [UBJSON](../features/binary_formats/ubjson.md) array of a type that is encoded by its
its marker alone (`Z`, `T` or `F`). Such an array has no payload, so its count alone decides how much memory is marker alone (`Z`, `T` or `F`) whose declared count exceeds 1,048,576 (`1 << 20`). Such an array has no payload, so its
allocated, and a handful of bytes would otherwise describe billions of values. It is thrown if the declared count of one count alone decides how much memory is allocated, and a handful of bytes would otherwise describe billions of values.
array exceeds 1,048,576 (`1 << 20`), and if the counts of all such arrays read so far in one input add up to more than [`to_ubjson`](../api/basic_json/to_ubjson.md) writes longer arrays of these types without the `$` type annotation (the `#`
1,048,576 plus 4 for every byte of the input read when the header of the last array has been read. count and one marker per element are kept), so any value it produces can still be read back.
[`to_ubjson`](../api/basic_json/to_ubjson.md) writes an array of these types without the `$` type annotation (the `#`
count and one marker per element are kept) when it is longer, or when it would bring the total of the typed arrays
written so far above 1,048,576, so everything it produces can be read back. Only input written by another encoder, or
by version 3.12.0 and earlier of this library, can exceed the limit: for example, 1,100 rows of 1,000 `null` values or
210,000 rows of 25, each with a `$Z` annotation. Version 3.12.0 writes such a row in 5 bytes (6 above 255 elements),
so rows of 20 or fewer such values are never rejected.
!!! failure "Example messages" !!! failure "Example messages"
@@ -1076,9 +1070,6 @@ so rows of 20 or fewer such values are never rejected.
``` ```
[json.exception.out_of_range.408] syntax error while parsing UBJSON size: excessive array size [json.exception.out_of_range.408] syntax error while parsing UBJSON size: excessive array size
``` ```
```
[json.exception.out_of_range.408] syntax error while parsing UBJSON size: excessive number of elements without payload
```
### json.exception.out_of_range.409 ### json.exception.out_of_range.409
+13 -84
View File
@@ -73,46 +73,13 @@ the input.
This is a sanity bound rather than a security boundary, and it is far above This is a sanity bound rather than a security boundary, and it is far above
any container met in practice. @ref binary_writer falls back to the any container met in practice. @ref binary_writer falls back to the
unoptimized encoding for longer containers, and for containers that together unoptimized encoding for longer containers, so that a value serialized by
with the ones before them would exceed @ref valueless_elements_base, so that a this library can always be read back.
value serialized by this library can always be read back.
@sa https://github.com/nlohmann/json/issues/2793 @sa https://github.com/nlohmann/json/issues/2793
*/ */
JSON_INLINE_VARIABLE constexpr std::size_t max_valueless_container_size = 1 << 20; JSON_INLINE_VARIABLE constexpr std::size_t max_valueless_container_size = 1 << 20;
/*!
@brief number of elements of valueless UBJSON containers accepted on top of
@ref valueless_elements_per_byte
The limit @ref max_valueless_container_size applies to each container on its
own, so on its own it lets 9 bytes announce 1048576 values, and every further
9 bytes another 1048576. The elements of all containers of a valueless type
in one input are therefore added up, and the sum must stay below this base
plus @ref valueless_elements_per_byte for every byte read so far. The base is
the per-container limit, so a container that was accepted on its own before
the sum was introduced is still accepted at the start of an input.
*/
JSON_INLINE_VARIABLE constexpr std::size_t valueless_elements_base = max_valueless_container_size;
/*!
@brief number of elements of valueless UBJSON containers accepted per byte of
input, in addition to @ref valueless_elements_base
An element of a value that has a payload costs at least one byte of input.
Valueless elements are accepted at a rate of 4 per byte. A value takes 16
bytes of memory in a DOM, so these arrays add about 80 bytes of memory per
byte of input (measured: 77; one `Z` per byte needs about 32), beyond what the
same bytes cost anyway, plus the base. This bounds the arrays of valueless
type, not the memory of a whole document. Containers written by
@ref binary_writer never need more than the base, because it writes an array
in the unoptimized form once the typed arrays written so far and this one add
up to more than that. The rate matters for input written by other encoders and
by versions before 3.13.0 only; in the data this was measured on, no input has
more than 0.17 elements per byte.
*/
JSON_INLINE_VARIABLE constexpr std::size_t valueless_elements_per_byte = 4;
/////////////////// ///////////////////
// binary reader // // binary reader //
/////////////////// ///////////////////
@@ -3733,37 +3700,6 @@ class binary_reader
return emit_unsigned(number); return emit_unsigned(number);
} }
/*!
@brief add the elements of a container of a valueless type to the total of
the input and check the total against the input read so far
A container of type 'Z', 'T' or 'F' has no payload, so its elements are the
only ones for which the input does not bound the count. The check is made
once per container, not per element.
@param[in] count number of elements the container declares; at most
@ref max_valueless_container_size
@return false if the total is more than @ref valueless_elements_base plus
@ref valueless_elements_per_byte for every byte read so far; the
total is not changed then
*/
bool charge_valueless_elements(const std::size_t count) noexcept
{
// the limit saturates instead of overflowing when size_t is 32 bits
// wide and a very large input has been read; valueless_elements
// never exceeds the limit, so the difference cannot underflow
constexpr std::size_t max_size = (std::numeric_limits<std::size_t>::max)();
const std::size_t limit = chars_read <= (max_size - valueless_elements_base) / valueless_elements_per_byte
? valueless_elements_base + (valueless_elements_per_byte * chars_read)
: max_size;
if (JSON_HEDLEY_UNLIKELY(count > limit - valueless_elements))
{
return false;
}
valueless_elements += count;
return true;
}
/*! /*!
@return whether array creation completed @return whether array creation completed
*/ */
@@ -3849,27 +3785,24 @@ class binary_reader
// declared count alone decides how much is allocated; the check is // declared count alone decides how much is allocated; the check is
// made before the start event so that no container is opened that // made before the start event so that no container is opened that
// is then abandoned. See @ref max_valueless_container_size. // is then abandoned. See @ref max_valueless_container_size.
if (JSON_HEDLEY_UNLIKELY(size_and_type.second == 'Z' || size_and_type.second == 'T' || size_and_type.second == 'F')) if (JSON_HEDLEY_UNLIKELY((size_and_type.second == 'Z' || size_and_type.second == 'T' || size_and_type.second == 'F')
&& size_and_type.first > max_valueless_container_size))
{ {
if (JSON_HEDLEY_UNLIKELY(size_and_type.first > max_valueless_container_size)) return out_of_range_error(exception_id::container_too_large, "excessive array size", "size");
{
return out_of_range_error(exception_id::container_too_large, "excessive array size", "size");
}
if (JSON_HEDLEY_UNLIKELY(!charge_valueless_elements(size_and_type.first)))
{
return out_of_range_error(exception_id::container_too_large, "excessive number of elements without payload", "size");
}
} }
// a no-op is not a value, so an array of them is empty whatever if (JSON_HEDLEY_UNLIKELY(!enter_array(size_and_type.first, size_and_type.second)))
// count it declares: the SAX parser is told so, instead of being
// made to reserve storage for a count that no input backs
const std::size_t announced = size_and_type.second == 'N' ? 0 : size_and_type.first;
if (JSON_HEDLEY_UNLIKELY(!enter_array(announced, size_and_type.second)))
{ {
return false; return false;
} }
if (size_and_type.second == 'N')
{
// a no-op is not a value, so a container of them holds none;
// the declared size has already been passed to the SAX parser
container_stack.back().remaining = 0;
}
return size_and_type.second == 0 return size_and_type.second == 0
|| parse_ubjson_typed_run(container_stack.back().remaining, size_and_type.second, std::integral_constant<bool, bulk_scan> {}); || parse_ubjson_typed_run(container_stack.back().remaining, size_and_type.second, std::integral_constant<bool, bulk_scan> {});
} }
@@ -5181,10 +5114,6 @@ class binary_reader
/// the number of characters read /// the number of characters read
std::size_t chars_read = 0; std::size_t chars_read = 0;
/// UBJSON: the sum of the element counts of the containers of a valueless
/// type read so far; see @ref charge_valueless_elements
std::size_t valueless_elements = 0;
/// whether we can assume little endianness /// whether we can assume little endianness
const bool is_little_endian = little_endianness(); const bool is_little_endian = little_endianness();
+10
View File
@@ -230,6 +230,16 @@
// detail/recursion_depth_limit.hpp. Like JSON_NO_THREAD_LOCAL it is neither part // detail/recursion_depth_limit.hpp. Like JSON_NO_THREAD_LOCAL it is neither part
// of the ABI tag nor undefined at the end of json.hpp. // of the ABI tag nor undefined at the end of json.hpp.
// forced inlining of the serializer's helpers only in optimized builds: at -O0,
// GCC and Clang give every inlined copy its own stack slots, which made the
// recursive frame of serializer::dump_container() about 2.3 KB (MSVC does not
// expand __forceinline at /Ob0, so it keeps it)
#if defined(__OPTIMIZE__) || defined(_MSC_VER)
#define JSON_INTERNAL_SERIALIZER_INLINE JSON_HEDLEY_ALWAYS_INLINE
#else
#define JSON_INTERNAL_SERIALIZER_INLINE inline
#endif
// allow disabling exceptions // allow disabling exceptions
#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) #if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION)
#define JSON_THROW(exception) throw exception #define JSON_THROW(exception) throw exception
@@ -11,6 +11,7 @@
// clean up // clean up
#undef JSON_ASSERT #undef JSON_ASSERT
#undef JSON_INTERNAL_CATCH #undef JSON_INTERNAL_CATCH
#undef JSON_INTERNAL_SERIALIZER_INLINE
#undef JSON_THROW #undef JSON_THROW
#undef JSON_PRIVATE_UNLESS_TESTED #undef JSON_PRIVATE_UNLESS_TESTED
#undef NLOHMANN_BASIC_JSON_TPL_DECLARATION #undef NLOHMANN_BASIC_JSON_TPL_DECLARATION
@@ -1169,14 +1169,6 @@ class binary_writer
write_container_iterative<msgpack_walk>(root); write_container_iterative<msgpack_walk>(root);
} }
// a function, not an inline condition: for an object the type is never
// valueless and MSVC would warn about a constant expression (C4127)
static constexpr bool valueless_array_fits(const bool valueless_type, const std::size_t size,
const std::size_t written) noexcept
{
return !valueless_type || size <= detail::valueless_elements_base - written;
}
/*! /*!
@brief write the start of an array (@a IsArray) or object: its opening @brief write the start of an array (@a IsArray) or object: its opening
marker, the optimized type, and the count marker, the optimized type, and the count
@@ -1212,27 +1204,20 @@ class binary_writer
// an optimized array of a valueless type carries no payload, so a // an optimized array of a valueless type carries no payload, so a
// reader has nothing but the declared count to bound the allocation // reader has nothing but the declared count to bound the allocation
// by. It refuses one array of more than max_valueless_container_size // by and refuses an excessive one. Write the unoptimized form for
// elements, and all such arrays of one input together when they add // those, at one byte per element, so the result can be read back.
// up to more than valueless_elements_base plus a number per byte // Objects are not affected: every element is preceded by its key.
// read. Write the unoptimized form, at one byte per element, for const bool valueless_type = (first_prefix == 'Z' || first_prefix == 'T' || first_prefix == 'F');
// an array that would exceed the base with the arrays written so const bool excessive_valueless = IsArray
// far, so that the result can be read back: the limit of a reader && valueless_type
// is never below the base. Objects are not affected: every element && size > detail::max_valueless_container_size;
// is preceded by its key.
if (same_prefix && !(use_bjdata && is_bjdata_excluded_type_marker(first_prefix))) if (same_prefix && !excessive_valueless
&& !(use_bjdata && is_bjdata_excluded_type_marker(first_prefix)))
{ {
const bool valueless_type = IsArray && (first_prefix == 'Z' || first_prefix == 'T' || first_prefix == 'F'); prefix_required = false;
if (valueless_array_fits(valueless_type, size, valueless_elements_written)) oa.write_character(to_char_type('$'));
{ oa.write_character(first_prefix);
prefix_required = false;
oa.write_character(to_char_type('$'));
oa.write_character(first_prefix);
if (valueless_type)
{
valueless_elements_written += size;
}
}
} }
} }
@@ -5456,10 +5441,6 @@ class binary_writer
/// how to treat a string value or object key that is not valid UTF-8 /// how to treat a string value or object key that is not valid UTF-8
/// (CBOR, MessagePack, UBJSON, BJData, and BSON; not BON8) /// (CBOR, MessagePack, UBJSON, BJData, and BSON; not BON8)
const error_handler_t error_handler = binary_writer_default_error_handler(); const error_handler_t error_handler = binary_writer_default_error_handler();
/// UBJSON: the number of elements written so far in optimized arrays of a
/// valueless type, which a reader adds up and bounds
std::size_t valueless_elements_written = 0;
}; };
} // namespace detail } // namespace detail
@@ -265,7 +265,7 @@ class serializer
for a container; writing them from here spares each the entry and exit of for a container; writing them from here spares each the entry and exit of
that function. that function.
*/ */
JSON_HEDLEY_ALWAYS_INLINE void dump_element(const BasicJsonType& val, JSON_INTERNAL_SERIALIZER_INLINE void dump_element(const BasicJsonType& val,
const std::size_t current_indent, const std::size_t current_indent,
const std::size_t depth) const std::size_t depth)
{ {
@@ -971,7 +971,7 @@ class serializer
} }
/// write out the bytes pending in @ref string_buffer and reset @a bytes /// write out the bytes pending in @ref string_buffer and reset @a bytes
JSON_HEDLEY_ALWAYS_INLINE void emit_pending(std::size_t& bytes) JSON_INTERNAL_SERIALIZER_INLINE void emit_pending(std::size_t& bytes)
{ {
if (bytes != 0) if (bytes != 0)
{ {
@@ -983,7 +983,7 @@ class serializer
/// write buffer and reset index if fewer than 13 bytes are left, as this /// write buffer and reset index if fewer than 13 bytes are left, as this
/// is the maximal number of bytes to be written ("\uxxxx\uxxxx\0") for one /// is the maximal number of bytes to be written ("\uxxxx\uxxxx\0") for one
/// code point /// code point
JSON_HEDLEY_ALWAYS_INLINE void reserve_string_buffer(std::size_t& bytes) JSON_INTERNAL_SERIALIZER_INLINE void reserve_string_buffer(std::size_t& bytes)
{ {
if (string_buffer.size() - bytes < 13) if (string_buffer.size() - bytes < 13)
{ {
@@ -1344,7 +1344,7 @@ class serializer
inlining both into the pretty-printing loops once @ref dump_escaped inlining both into the pretty-printing loops once @ref dump_escaped
has grown, which made indented output up to 13% slower has grown, which made indented output up to 13% slower
*/ */
JSON_HEDLEY_ALWAYS_INLINE void put_indent(std::size_t indent) JSON_INTERNAL_SERIALIZER_INLINE void put_indent(std::size_t indent)
{ {
// closing braces at the outermost level ask for no indentation at all // closing braces at the outermost level ask for no indentation at all
if (indent == 0) if (indent == 0)
@@ -1398,7 +1398,7 @@ class serializer
its room and four stores of the indentation, see @ref put_short_literal_indent. its room and four stores of the indentation, see @ref put_short_literal_indent.
*/ */
template<std::size_t N> template<std::size_t N>
JSON_HEDLEY_ALWAYS_INLINE void put_literal_indent(const char (&s)[N], const std::size_t indent) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) JSON_INTERNAL_SERIALIZER_INLINE void put_literal_indent(const char (&s)[N], const std::size_t indent) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
{ {
if (JSON_HEDLEY_LIKELY(put_short_literal_indent(s, indent))) if (JSON_HEDLEY_LIKELY(put_short_literal_indent(s, indent)))
{ {
@@ -1418,7 +1418,7 @@ class serializer
@a indent is wider or the write buffer lacks the room. @a indent is wider or the write buffer lacks the room.
*/ */
template<std::size_t N> template<std::size_t N>
JSON_HEDLEY_ALWAYS_INLINE bool put_short_literal_indent(const char (&s)[N], const std::size_t indent) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) JSON_INTERNAL_SERIALIZER_INLINE bool put_short_literal_indent(const char (&s)[N], const std::size_t indent) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
{ {
constexpr std::size_t length = N - 1; constexpr std::size_t length = N - 1;
if (indent > 32 || write_buffer_pos + length + 32 > write_buffer.size()) if (indent > 32 || write_buffer_pos + length + 32 > write_buffer.size())
@@ -1445,7 +1445,7 @@ class serializer
write-through path for oversized runs. write-through path for oversized runs.
*/ */
template<std::size_t N> template<std::size_t N>
JSON_HEDLEY_ALWAYS_INLINE void put_literal(const char (&s)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) JSON_INTERNAL_SERIALIZER_INLINE void put_literal(const char (&s)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
{ {
static_assert(N >= 2, "put_literal expects a non-empty string literal"); static_assert(N >= 2, "put_literal expects a non-empty string literal");
// the array bound counts the terminating NUL, which is not written // the array bound counts the terminating NUL, which is not written
+44 -123
View File
@@ -2603,6 +2603,16 @@ JSON_HEDLEY_DIAGNOSTIC_POP
// detail/recursion_depth_limit.hpp. Like JSON_NO_THREAD_LOCAL it is neither part // detail/recursion_depth_limit.hpp. Like JSON_NO_THREAD_LOCAL it is neither part
// of the ABI tag nor undefined at the end of json.hpp. // of the ABI tag nor undefined at the end of json.hpp.
// forced inlining of the serializer's helpers only in optimized builds: at -O0,
// GCC and Clang give every inlined copy its own stack slots, which made the
// recursive frame of serializer::dump_container() about 2.3 KB (MSVC does not
// expand __forceinline at /Ob0, so it keeps it)
#if defined(__OPTIMIZE__) || defined(_MSC_VER)
#define JSON_INTERNAL_SERIALIZER_INLINE JSON_HEDLEY_ALWAYS_INLINE
#else
#define JSON_INTERNAL_SERIALIZER_INLINE inline
#endif
// allow disabling exceptions // allow disabling exceptions
#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) #if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION)
#define JSON_THROW(exception) throw exception #define JSON_THROW(exception) throw exception
@@ -18464,46 +18474,13 @@ the input.
This is a sanity bound rather than a security boundary, and it is far above This is a sanity bound rather than a security boundary, and it is far above
any container met in practice. @ref binary_writer falls back to the any container met in practice. @ref binary_writer falls back to the
unoptimized encoding for longer containers, and for containers that together unoptimized encoding for longer containers, so that a value serialized by
with the ones before them would exceed @ref valueless_elements_base, so that a this library can always be read back.
value serialized by this library can always be read back.
@sa https://github.com/nlohmann/json/issues/2793 @sa https://github.com/nlohmann/json/issues/2793
*/ */
JSON_INLINE_VARIABLE constexpr std::size_t max_valueless_container_size = 1 << 20; JSON_INLINE_VARIABLE constexpr std::size_t max_valueless_container_size = 1 << 20;
/*!
@brief number of elements of valueless UBJSON containers accepted on top of
@ref valueless_elements_per_byte
The limit @ref max_valueless_container_size applies to each container on its
own, so on its own it lets 9 bytes announce 1048576 values, and every further
9 bytes another 1048576. The elements of all containers of a valueless type
in one input are therefore added up, and the sum must stay below this base
plus @ref valueless_elements_per_byte for every byte read so far. The base is
the per-container limit, so a container that was accepted on its own before
the sum was introduced is still accepted at the start of an input.
*/
JSON_INLINE_VARIABLE constexpr std::size_t valueless_elements_base = max_valueless_container_size;
/*!
@brief number of elements of valueless UBJSON containers accepted per byte of
input, in addition to @ref valueless_elements_base
An element of a value that has a payload costs at least one byte of input.
Valueless elements are accepted at a rate of 4 per byte. A value takes 16
bytes of memory in a DOM, so these arrays add about 80 bytes of memory per
byte of input (measured: 77; one `Z` per byte needs about 32), beyond what the
same bytes cost anyway, plus the base. This bounds the arrays of valueless
type, not the memory of a whole document. Containers written by
@ref binary_writer never need more than the base, because it writes an array
in the unoptimized form once the typed arrays written so far and this one add
up to more than that. The rate matters for input written by other encoders and
by versions before 3.13.0 only; in the data this was measured on, no input has
more than 0.17 elements per byte.
*/
JSON_INLINE_VARIABLE constexpr std::size_t valueless_elements_per_byte = 4;
/////////////////// ///////////////////
// binary reader // // binary reader //
/////////////////// ///////////////////
@@ -22124,37 +22101,6 @@ class binary_reader
return emit_unsigned(number); return emit_unsigned(number);
} }
/*!
@brief add the elements of a container of a valueless type to the total of
the input and check the total against the input read so far
A container of type 'Z', 'T' or 'F' has no payload, so its elements are the
only ones for which the input does not bound the count. The check is made
once per container, not per element.
@param[in] count number of elements the container declares; at most
@ref max_valueless_container_size
@return false if the total is more than @ref valueless_elements_base plus
@ref valueless_elements_per_byte for every byte read so far; the
total is not changed then
*/
bool charge_valueless_elements(const std::size_t count) noexcept
{
// the limit saturates instead of overflowing when size_t is 32 bits
// wide and a very large input has been read; valueless_elements
// never exceeds the limit, so the difference cannot underflow
constexpr std::size_t max_size = (std::numeric_limits<std::size_t>::max)();
const std::size_t limit = chars_read <= (max_size - valueless_elements_base) / valueless_elements_per_byte
? valueless_elements_base + (valueless_elements_per_byte * chars_read)
: max_size;
if (JSON_HEDLEY_UNLIKELY(count > limit - valueless_elements))
{
return false;
}
valueless_elements += count;
return true;
}
/*! /*!
@return whether array creation completed @return whether array creation completed
*/ */
@@ -22240,27 +22186,24 @@ class binary_reader
// declared count alone decides how much is allocated; the check is // declared count alone decides how much is allocated; the check is
// made before the start event so that no container is opened that // made before the start event so that no container is opened that
// is then abandoned. See @ref max_valueless_container_size. // is then abandoned. See @ref max_valueless_container_size.
if (JSON_HEDLEY_UNLIKELY(size_and_type.second == 'Z' || size_and_type.second == 'T' || size_and_type.second == 'F')) if (JSON_HEDLEY_UNLIKELY((size_and_type.second == 'Z' || size_and_type.second == 'T' || size_and_type.second == 'F')
&& size_and_type.first > max_valueless_container_size))
{ {
if (JSON_HEDLEY_UNLIKELY(size_and_type.first > max_valueless_container_size)) return out_of_range_error(exception_id::container_too_large, "excessive array size", "size");
{
return out_of_range_error(exception_id::container_too_large, "excessive array size", "size");
}
if (JSON_HEDLEY_UNLIKELY(!charge_valueless_elements(size_and_type.first)))
{
return out_of_range_error(exception_id::container_too_large, "excessive number of elements without payload", "size");
}
} }
// a no-op is not a value, so an array of them is empty whatever if (JSON_HEDLEY_UNLIKELY(!enter_array(size_and_type.first, size_and_type.second)))
// count it declares: the SAX parser is told so, instead of being
// made to reserve storage for a count that no input backs
const std::size_t announced = size_and_type.second == 'N' ? 0 : size_and_type.first;
if (JSON_HEDLEY_UNLIKELY(!enter_array(announced, size_and_type.second)))
{ {
return false; return false;
} }
if (size_and_type.second == 'N')
{
// a no-op is not a value, so a container of them holds none;
// the declared size has already been passed to the SAX parser
container_stack.back().remaining = 0;
}
return size_and_type.second == 0 return size_and_type.second == 0
|| parse_ubjson_typed_run(container_stack.back().remaining, size_and_type.second, std::integral_constant<bool, bulk_scan> {}); || parse_ubjson_typed_run(container_stack.back().remaining, size_and_type.second, std::integral_constant<bool, bulk_scan> {});
} }
@@ -23572,10 +23515,6 @@ class binary_reader
/// the number of characters read /// the number of characters read
std::size_t chars_read = 0; std::size_t chars_read = 0;
/// UBJSON: the sum of the element counts of the containers of a valueless
/// type read so far; see @ref charge_valueless_elements
std::size_t valueless_elements = 0;
/// whether we can assume little endianness /// whether we can assume little endianness
const bool is_little_endian = little_endianness(); const bool is_little_endian = little_endianness();
@@ -30727,14 +30666,6 @@ class binary_writer
write_container_iterative<msgpack_walk>(root); write_container_iterative<msgpack_walk>(root);
} }
// a function, not an inline condition: for an object the type is never
// valueless and MSVC would warn about a constant expression (C4127)
static constexpr bool valueless_array_fits(const bool valueless_type, const std::size_t size,
const std::size_t written) noexcept
{
return !valueless_type || size <= detail::valueless_elements_base - written;
}
/*! /*!
@brief write the start of an array (@a IsArray) or object: its opening @brief write the start of an array (@a IsArray) or object: its opening
marker, the optimized type, and the count marker, the optimized type, and the count
@@ -30770,27 +30701,20 @@ class binary_writer
// an optimized array of a valueless type carries no payload, so a // an optimized array of a valueless type carries no payload, so a
// reader has nothing but the declared count to bound the allocation // reader has nothing but the declared count to bound the allocation
// by. It refuses one array of more than max_valueless_container_size // by and refuses an excessive one. Write the unoptimized form for
// elements, and all such arrays of one input together when they add // those, at one byte per element, so the result can be read back.
// up to more than valueless_elements_base plus a number per byte // Objects are not affected: every element is preceded by its key.
// read. Write the unoptimized form, at one byte per element, for const bool valueless_type = (first_prefix == 'Z' || first_prefix == 'T' || first_prefix == 'F');
// an array that would exceed the base with the arrays written so const bool excessive_valueless = IsArray
// far, so that the result can be read back: the limit of a reader && valueless_type
// is never below the base. Objects are not affected: every element && size > detail::max_valueless_container_size;
// is preceded by its key.
if (same_prefix && !(use_bjdata && is_bjdata_excluded_type_marker(first_prefix))) if (same_prefix && !excessive_valueless
&& !(use_bjdata && is_bjdata_excluded_type_marker(first_prefix)))
{ {
const bool valueless_type = IsArray && (first_prefix == 'Z' || first_prefix == 'T' || first_prefix == 'F'); prefix_required = false;
if (valueless_array_fits(valueless_type, size, valueless_elements_written)) oa.write_character(to_char_type('$'));
{ oa.write_character(first_prefix);
prefix_required = false;
oa.write_character(to_char_type('$'));
oa.write_character(first_prefix);
if (valueless_type)
{
valueless_elements_written += size;
}
}
} }
} }
@@ -35014,10 +34938,6 @@ class binary_writer
/// how to treat a string value or object key that is not valid UTF-8 /// how to treat a string value or object key that is not valid UTF-8
/// (CBOR, MessagePack, UBJSON, BJData, and BSON; not BON8) /// (CBOR, MessagePack, UBJSON, BJData, and BSON; not BON8)
const error_handler_t error_handler = binary_writer_default_error_handler(); const error_handler_t error_handler = binary_writer_default_error_handler();
/// UBJSON: the number of elements written so far in optimized arrays of a
/// valueless type, which a reader adds up and bounds
std::size_t valueless_elements_written = 0;
}; };
} // namespace detail } // namespace detail
@@ -37075,7 +36995,7 @@ class serializer
for a container; writing them from here spares each the entry and exit of for a container; writing them from here spares each the entry and exit of
that function. that function.
*/ */
JSON_HEDLEY_ALWAYS_INLINE void dump_element(const BasicJsonType& val, JSON_INTERNAL_SERIALIZER_INLINE void dump_element(const BasicJsonType& val,
const std::size_t current_indent, const std::size_t current_indent,
const std::size_t depth) const std::size_t depth)
{ {
@@ -37781,7 +37701,7 @@ class serializer
} }
/// write out the bytes pending in @ref string_buffer and reset @a bytes /// write out the bytes pending in @ref string_buffer and reset @a bytes
JSON_HEDLEY_ALWAYS_INLINE void emit_pending(std::size_t& bytes) JSON_INTERNAL_SERIALIZER_INLINE void emit_pending(std::size_t& bytes)
{ {
if (bytes != 0) if (bytes != 0)
{ {
@@ -37793,7 +37713,7 @@ class serializer
/// write buffer and reset index if fewer than 13 bytes are left, as this /// write buffer and reset index if fewer than 13 bytes are left, as this
/// is the maximal number of bytes to be written ("\uxxxx\uxxxx\0") for one /// is the maximal number of bytes to be written ("\uxxxx\uxxxx\0") for one
/// code point /// code point
JSON_HEDLEY_ALWAYS_INLINE void reserve_string_buffer(std::size_t& bytes) JSON_INTERNAL_SERIALIZER_INLINE void reserve_string_buffer(std::size_t& bytes)
{ {
if (string_buffer.size() - bytes < 13) if (string_buffer.size() - bytes < 13)
{ {
@@ -38154,7 +38074,7 @@ class serializer
inlining both into the pretty-printing loops once @ref dump_escaped inlining both into the pretty-printing loops once @ref dump_escaped
has grown, which made indented output up to 13% slower has grown, which made indented output up to 13% slower
*/ */
JSON_HEDLEY_ALWAYS_INLINE void put_indent(std::size_t indent) JSON_INTERNAL_SERIALIZER_INLINE void put_indent(std::size_t indent)
{ {
// closing braces at the outermost level ask for no indentation at all // closing braces at the outermost level ask for no indentation at all
if (indent == 0) if (indent == 0)
@@ -38208,7 +38128,7 @@ class serializer
its room and four stores of the indentation, see @ref put_short_literal_indent. its room and four stores of the indentation, see @ref put_short_literal_indent.
*/ */
template<std::size_t N> template<std::size_t N>
JSON_HEDLEY_ALWAYS_INLINE void put_literal_indent(const char (&s)[N], const std::size_t indent) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) JSON_INTERNAL_SERIALIZER_INLINE void put_literal_indent(const char (&s)[N], const std::size_t indent) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
{ {
if (JSON_HEDLEY_LIKELY(put_short_literal_indent(s, indent))) if (JSON_HEDLEY_LIKELY(put_short_literal_indent(s, indent)))
{ {
@@ -38228,7 +38148,7 @@ class serializer
@a indent is wider or the write buffer lacks the room. @a indent is wider or the write buffer lacks the room.
*/ */
template<std::size_t N> template<std::size_t N>
JSON_HEDLEY_ALWAYS_INLINE bool put_short_literal_indent(const char (&s)[N], const std::size_t indent) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) JSON_INTERNAL_SERIALIZER_INLINE bool put_short_literal_indent(const char (&s)[N], const std::size_t indent) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
{ {
constexpr std::size_t length = N - 1; constexpr std::size_t length = N - 1;
if (indent > 32 || write_buffer_pos + length + 32 > write_buffer.size()) if (indent > 32 || write_buffer_pos + length + 32 > write_buffer.size())
@@ -38255,7 +38175,7 @@ class serializer
write-through path for oversized runs. write-through path for oversized runs.
*/ */
template<std::size_t N> template<std::size_t N>
JSON_HEDLEY_ALWAYS_INLINE void put_literal(const char (&s)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) JSON_INTERNAL_SERIALIZER_INLINE void put_literal(const char (&s)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
{ {
static_assert(N >= 2, "put_literal expects a non-empty string literal"); static_assert(N >= 2, "put_literal expects a non-empty string literal");
// the array bound counts the terminating NUL, which is not written // the array bound counts the terminating NUL, which is not written
@@ -47975,6 +47895,7 @@ struct formatter<nlohmann::NLOHMANN_BASIC_JSON_TPL, char> // NOLINT(cert-dcl58-c
// clean up // clean up
#undef JSON_ASSERT #undef JSON_ASSERT
#undef JSON_INTERNAL_CATCH #undef JSON_INTERNAL_CATCH
#undef JSON_INTERNAL_SERIALIZER_INLINE
#undef JSON_THROW #undef JSON_THROW
#undef JSON_PRIVATE_UNLESS_TESTED #undef JSON_PRIVATE_UNLESS_TESTED
#undef NLOHMANN_BASIC_JSON_TPL_DECLARATION #undef NLOHMANN_BASIC_JSON_TPL_DECLARATION
-339
View File
@@ -19,12 +19,8 @@
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
using nlohmann::json; using nlohmann::json;
#include <cstdio>
#include <iostream> #include <iostream>
#include <sstream>
#include <utility>
#include <fstream> #include <fstream>
#include <limits>
#include <list> #include <list>
#include <set> #include <set>
#include "make_test_data_available.hpp" #include "make_test_data_available.hpp"
@@ -2440,341 +2436,6 @@ TEST_CASE("UBJSON optimized arrays of a valueless type are bounded")
} }
} }
namespace
{
/// SAX consumer that counts the values and keeps the id of a parse error
struct ValuelessEventCounter : public nlohmann::json_sax<json>
{
bool null() override
{
return ++values < stop_after;
}
bool boolean(bool /*val*/) override
{
return ++values < stop_after;
}
bool number_integer(json::number_integer_t /*val*/) override
{
++values;
return true;
}
bool number_unsigned(json::number_unsigned_t /*val*/) override
{
++values;
return true;
}
bool number_float(json::number_float_t /*val*/, const std::string& /*s*/) override
{
++values;
return true;
}
bool string(std::string& /*val*/) override
{
++values;
return true;
}
bool binary(json::binary_t& /*val*/) override
{
++values;
return true;
}
bool start_object(std::size_t /*elements*/) override
{
return true;
}
bool key(std::string& /*val*/) override
{
return true;
}
bool end_object() override
{
return true;
}
bool start_array(std::size_t /*elements*/) override
{
return true;
}
bool end_array() override
{
return true;
}
bool parse_error(std::size_t /*position*/, const std::string& /*last_token*/, const json::exception& ex) override
{
error_id = ex.id;
return false;
}
/// the handler refuses the value that makes this many
std::size_t stop_after = (std::numeric_limits<std::size_t>::max)();
std::size_t values = 0;
int error_id = 0;
};
/// appends `[$<marker>#l<count>` (nine bytes) to @a v
void append_valueless_array(std::vector<std::uint8_t>& v, const char marker, const std::uint32_t count)
{
v.push_back('[');
v.push_back('$');
v.push_back(static_cast<std::uint8_t>(marker));
v.push_back('#');
v.push_back('l');
v.push_back(static_cast<std::uint8_t>(count >> 24));
v.push_back(static_cast<std::uint8_t>(count >> 16));
v.push_back(static_cast<std::uint8_t>(count >> 8));
v.push_back(static_cast<std::uint8_t>(count));
}
/// `[` followed by one `[$<marker>#l<count>` per entry of @a counts and `]`
std::vector<std::uint8_t> valueless_arrays(const char marker, const std::vector<std::uint32_t>& counts)
{
std::vector<std::uint8_t> v = {'['};
for (const auto count : counts)
{
append_valueless_array(v, marker, count);
}
v.push_back(']');
return v;
}
} // namespace
TEST_CASE("UBJSON the valueless elements of a whole input are bounded")
{
// The per-container limit alone let nine bytes (`[$Z#l` and a count of
// 1048576) announce a million values, and every further nine bytes another
// million. The elements of all containers of type 'Z', 'T' or 'F' in one
// input are now added up: they may not exceed 1048576 plus 4 for every
// byte read when the container's header has been read.
//
// The tests count events with a SAX handler where they can: building and
// comparing values of a million elements is what makes them slow under
// Valgrind, so there is one rejection and one round trip with a DOM.
json _;
const char* const budget_message = "[json.exception.out_of_range.408] syntax error while parsing UBJSON size: excessive number of elements without payload";
// two arrays that exceed the limit: the second one, with its header read
// at byte 19, may hold 4 * 19 = 76 values on top of the base of 1048576
const std::uint32_t over_first = 524288;
const std::uint32_t over_second = 1048576 + 76 - over_first + 1;
SECTION("the input of the original report is rejected")
{
// 362 bytes (40 arrays of 1048576 values) used to allocate 661 MB; the
// second array is the one that is refused
const auto input = valueless_arrays('Z', {over_first, over_second});
CHECK_THROWS_WITH_AS(_ = json::from_ubjson(input), budget_message, json::out_of_range&);
}
SECTION("every input adapter applies the budget")
{
const auto input = valueless_arrays('Z', {over_first, over_second});
// stream; not strict, which does not matter
std::stringstream stream;
stream.write(reinterpret_cast<const char*>(input.data()), static_cast<std::streamsize>(input.size()));
ValuelessEventCounter from_stream;
CHECK(!json::sax_parse(stream, &from_stream, json::input_format_t::ubjson, false));
CHECK(from_stream.error_id == 408);
// a container that is not contiguous; the counts of arrays of
// different types are added up
std::vector<std::uint8_t> mixed = {'['};
append_valueless_array(mixed, 'T', over_first);
append_valueless_array(mixed, 'F', over_second);
mixed.push_back(']');
const std::list<std::uint8_t> as_list(mixed.begin(), mixed.end());
ValuelessEventCounter from_list;
CHECK(!json::sax_parse(as_list.begin(), as_list.end(), &from_list, json::input_format_t::ubjson));
CHECK(from_list.error_id == 408);
CHECK(from_list.values == over_first);
// FILE*
FILE* const file = std::tmpfile(); // NOLINT(cppcoreguidelines-owning-memory)
REQUIRE(file != nullptr);
CHECK(std::fwrite(input.data(), 1, input.size(), file) == input.size());
CHECK(std::fseek(file, 0, SEEK_SET) == 0);
ValuelessEventCounter from_file;
CHECK(!json::sax_parse(file, &from_file, json::input_format_t::ubjson));
CHECK(from_file.error_id == 408);
std::fclose(file); // NOLINT(cert-err33-c,cppcoreguidelines-owning-memory)
}
SECTION("the limit is exact and the bytes read raise it")
{
// [ [$Z#l 524288] [$U#l 4096 + 4096 bytes] [$Z#l count] ]: chars_read
// is 1 + 9 + (9 + 4096) + 9 = 4124 when the last header has been read,
// so the last array may hold 4 * 4124 = 16496 values on top of the
// base of 1048576 less the 524288 of the first array
const std::uint32_t last_allowed = 1048576 + 16496 - 524288;
for (const std::uint32_t count :
{
last_allowed, last_allowed + 1
})
{
CAPTURE(count)
std::vector<std::uint8_t> input = {'['};
append_valueless_array(input, 'Z', 524288);
input.insert(input.end(), {'[', '$', 'U', '#', 'l', 0x00, 0x00, 0x10, 0x00});
input.insert(input.end(), 4096, 0x01);
append_valueless_array(input, 'Z', count);
input.push_back(']');
ValuelessEventCounter sax;
const bool accepted = count == last_allowed;
CHECK(json::sax_parse(input, &sax, json::input_format_t::ubjson) == accepted);
CHECK(sax.error_id == (accepted ? 0 : 408));
CHECK(sax.values == 524288u + 4096u + (accepted ? count : 0u));
}
// an array of 1048576 values on its own is accepted as before
const std::vector<std::uint8_t> single = {'[', '$', 'Z', '#', 'l', 0x00, 0x10, 0x00, 0x00};
// (the handler ends the parse at the first value, so that the million
// values are not read)
ValuelessEventCounter sax;
sax.stop_after = 1;
CHECK(!json::sax_parse(single, &sax, json::input_format_t::ubjson));
CHECK(sax.error_id == 0);
CHECK(sax.values == 1);
}
SECTION("to_ubjson writes the typed form up to the base only")
{
// 1048 rows of 1000 values fit under the base of 1048576, the 1049th
// would not: its elements are written one by one. Written all typed,
// 1100 rows of 6 bytes would be refused by a reader (1075 rows are the
// most it accepts). The rows hold null, true or false, and the
// fallback does not depend on which
std::vector<std::uint8_t> encoded;
{
json table = json::array();
for (int row = 0; row < 1100; ++row)
{
const json cell = row % 3 == 0 ? json(nullptr) : json(row % 3 == 1);
table.push_back(json(1000, cell));
}
encoded = json::to_ubjson(table, true, true);
}
// outer array: `[$[#I` and the count (7 bytes), then 1048 typed rows of 6
// bytes (`$Z#I` and two bytes) and 52 rows of `#I`, two bytes, and 1000 `Z`
CHECK(encoded.size() == 7 + (1048 * 6) + (52 * (4 + 1000)));
// the output is read back
ValuelessEventCounter sax;
CHECK(json::sax_parse(encoded, &sax, json::input_format_t::ubjson));
CHECK(sax.error_id == 0);
CHECK(sax.values == 1100u * 1000u);
}
SECTION("BJData is not affected")
{
// BJData has no optimized arrays of these types, and the writer does
// not charge them
json table = json::array();
for (int row = 0; row < 3; ++row)
{
table.push_back(json(10, nullptr));
}
const auto encoded = json::to_bjdata(table, true, true);
CHECK(json::from_bjdata(encoded) == table);
const std::vector<std::uint8_t> input = {'[', '$', 'Z', '#', 'l', 0x00, 0x10, 0x00, 0x00};
CHECK_THROWS_AS(_ = json::from_bjdata(input), json::parse_error&);
}
SECTION("an object of a valueless type needs a key for every value")
{
// an object member costs a key, so the count is bounded by the input
// and is not charged to the budget
const std::vector<std::uint8_t> input = {'{', '$', 'Z', '#', 'l', 0x00, 0x10, 0x00, 0x00};
ValuelessEventCounter sax;
CHECK(!json::sax_parse(input, &sax, json::input_format_t::ubjson));
CHECK(sax.error_id == 110);
CHECK(sax.values == 0);
}
}
TEST_CASE("UBJSON an optimized array of no-ops is empty whatever its count")
{
// A no-op ('N') is not a value, so `[$N#<n>` is an array without elements
// for every n. The count used to be passed on to the SAX parser, which
// told it to expect n elements, and the DOM parser reserved storage for
// them: 16384 values (256 KB) for a count of 2^31 in 9 bytes of input.
SECTION("the SAX parser is told that the array is empty")
{
for (const std::vector<std::uint8_t>& input :
{
std::vector<std::uint8_t> {'[', '$', 'N', '#', 'i', 2},
std::vector<std::uint8_t> {'[', '$', 'N', '#', 'I', 0x7F, 0xFF},
std::vector<std::uint8_t> {'[', '$', 'N', '#', 'l', 0x7F, 0xFF, 0xFF, 0xFF},
std::vector<std::uint8_t> {'[', '$', 'N', '#', 'i', 0}
})
{
SaxEventLogger logger;
CHECK(json::sax_parse(input, &logger, json::input_format_t::ubjson));
CHECK(logger.events == std::vector<std::string>({"start_array(0)", "end_array()"}));
}
// between other values, the next value is read as usual
const std::vector<std::uint8_t> nested = {'[', '[', '$', 'N', '#', 'l', 0x7F, 0xFF, 0xFF, 0xFF, 'T', '[', '$', 'N', '#', 'i', 3, ']'};
SaxEventLogger logger;
CHECK(json::sax_parse(nested, &logger, json::input_format_t::ubjson));
CHECK(logger.events == std::vector<std::string>({"start_array()", "start_array(0)", "end_array()", "boolean(true)", "start_array(0)", "end_array()", "end_array()"}));
}
SECTION("the DOM parser reserves nothing")
{
// 2000 arrays of 9 bytes that announce 2^31 elements each: with a
// reservation of 16384 values per array this took about 500 MB
std::vector<std::uint8_t> input = {'['};
for (int i = 0; i < 2000; ++i)
{
input.insert(input.end(), {'[', '$', 'N', '#', 'l', 0x7F, 0xFF, 0xFF, 0xFF});
}
input.push_back(']');
const json j = json::from_ubjson(input);
REQUIRE(j.size() == 2000);
std::size_t capacity = 0;
for (const auto& element : j)
{
CHECK(element.is_array());
CHECK(element.empty());
capacity += element.get_ref<const json::array_t&>().capacity();
}
CHECK(capacity == 0);
// the 7-byte form inside an optimized array of arrays
std::vector<std::uint8_t> typed = {'[', '$', '[', '#', 'I', 0x07, 0xD0};
for (int i = 0; i < 2000; ++i)
{
typed.insert(typed.end(), {'$', 'N', '#', 'I', 0x7F, 0xFF});
}
const json k = json::from_ubjson(typed);
REQUIRE(k.size() == 2000);
capacity = 0;
for (const auto& element : k)
{
CHECK(element.empty());
capacity += element.get_ref<const json::array_t&>().capacity();
}
CHECK(capacity == 0);
}
SECTION("a count that no container could hold is no longer an error")
{
// the array is empty, so nothing has to fit: 0x7FFFFFFF used to be
// reported as out_of_range.408 where it exceeds the maximum size of
// an array, and is accepted now
const std::vector<std::uint8_t> input = {'[', '$', 'N', '#', 'l', 0x7F, 0xFF, 0xFF, 0xFF};
CHECK(json::from_ubjson(input) == json::array());
// a count that does not fit into std::size_t still is
#if SIZE_MAX == 0xffffffff
const std::vector<std::uint8_t> too_large = {'[', '$', 'N', '#', 'L', 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00};
json _;
CHECK_THROWS_AS(_ = json::from_ubjson(too_large), json::out_of_range&);
#endif
}
}
TEST_CASE("issue #5405 - array reserve for definite-length UBJSON arrays") TEST_CASE("issue #5405 - array reserve for definite-length UBJSON arrays")
{ {
#if !defined(JSON_NOEXCEPTION) #if !defined(JSON_NOEXCEPTION)