Compare commits

..
Author SHA1 Message Date
Niels Lohmann 43beef41b9 Merge branch 'develop' into bench/json-view-comparison-fairness
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 15:53:30 +01:00
Niels Lohmann b38c056204 json_view benchmarks: compare fairly and state what each row means
- bench_view: add "(editable)" rows for json_editable_document and a
  parse+dump workload, so the cost of lazy float conversion shows next to the
  parse row (the traverse workload already includes the parse). Check that
  all engines' dumps agree where outputs are comparable.
- compare.py: add --aligned, which also builds and runs with aligned code and
  reports both tables, so code-layout artifacts become visible. Off by
  default; the default output is unchanged. Not run end to end yet.
- README: say which engines are like-for-like peers (yyjson, simdjson DOM) and
  which are mutable reference points (Boost.JSON, json::parse, the editable
  json_view); explain the lazy float conversion and the eager prototype that
  was measured and dropped.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-25 10:54:04 +01:00
27 changed files with 320 additions and 1256 deletions

No files matched your search

+2 -3
View File
@@ -90,9 +90,8 @@ type `#!cpp binary_t*` must be dereferenced.
- MessagePack - MessagePack
- If a subtype is given and the binary array contains exactly 1, 2, 4, 8, or 16 elements, the fixext family (fixext1, - If a subtype is given and the binary array contains exactly 1, 2, 4, 8, or 16 elements, the fixext family (fixext1,
fixext2, fixext4, fixext8, fixext16) is used. For other sizes, the ext family (ext8, ext16, ext32) is used. The fixext2, fixext4, fixext8) is used. For other sizes, the ext family (ext8, ext16, ext32) is used. The subtype is
subtype is then written as the signed 8-bit ext type: subtypes 128 to 255 map to the negative (reserved) ext then added as a signed 8-bit integer.
types, and on reading the ext type is exposed as an unsigned subtype from 0 to 255.
- If no subtype is given, the bin family (bin8, bin16, bin32) is used. - If no subtype is given, the bin family (bin8, bin16, bin32) is used.
- BSON - BSON
@@ -15,13 +15,11 @@ error
: report a parse error in case of a tag (the `from_cbor` overloads throw a `parse_error` exception by default) : report a parse error in case of a tag (the `from_cbor` overloads throw a `parse_error` exception by default)
ignore ignore
: ignore tags (also in front of a text string map key) : ignore tags
store store
: store tagged byte strings (for bytes 0xd8..0xdb) as binary values with the tag as subtype; other tagged values are : store tagged byte strings (for bytes 0xd8..0xdb) as binary values with the tag as subtype; other tagged values are
read as if the tag were ignored. If several tags precede a byte string, only the tag directly in front of it is read as if the tag were ignored. If several tags precede a byte string, only the innermost one is stored.
stored, and only when it has the form 0xd8..0xdb (a tag 0..23 written as 0xc0..0xd7 is skipped). Tags in front of a
text string map key are skipped.
## Examples ## Examples
+1 -2
View File
@@ -80,8 +80,7 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va
- Throws [`parse_error.110`](../../home/exceptions.md#jsonexceptionparse_error110) if the given input ends prematurely or - Throws [`parse_error.110`](../../home/exceptions.md#jsonexceptionparse_error110) if the given input ends prematurely or
the end of the input was not reached when `strict` was set to true the end of the input was not reached when `strict` was set to true
- Throws [`parse_error.112`](../../home/exceptions.md#jsonexceptionparse_error112) if a parse error occurs (e.g., an - Throws [`parse_error.112`](../../home/exceptions.md#jsonexceptionparse_error112) if a parse error occurs (e.g., an
invalid string or byte array length, an invalid inner length of a binary value with subtype `0x02`, or a document size invalid string or byte array length)
that does not match the content; `strict` does not affect the document size checks)
- Throws [`parse_error.114`](../../home/exceptions.md#jsonexceptionparse_error114) if an unsupported BSON record type is - Throws [`parse_error.114`](../../home/exceptions.md#jsonexceptionparse_error114) if an unsupported BSON record type is
encountered encountered
- Throws [`parse_error.113`](../../home/exceptions.md#jsonexceptionparse_error113) if a string value or object key is - Throws [`parse_error.113`](../../home/exceptions.md#jsonexceptionparse_error113) if a string value or object key is
+4 -12
View File
@@ -32,10 +32,8 @@ The exact mapping and its limitations are described on a [dedicated page](../../
- an `std::istream` object - an `std::istream` object
- a `FILE` pointer - a `FILE` pointer
- a C-style array of characters (its terminating NUL is read as trailing data: `parse_error.110` unless `strict` is - a C-style array of characters
`false`) - a pointer to a null-terminated string of single byte characters
- a pointer to a null-terminated string of single byte characters (read up to the first 0x00 byte, which is a valid
CBOR byte)
- a container `obj` for which `begin(obj)` and `end(obj)` produce a valid pair of iterators - a container `obj` for which `begin(obj)` and `end(obj)` produce a valid pair of iterators
(as found via ADL or member functions, with semantics compatible to `std::begin` and `std::end`) (as found via ADL or member functions, with semantics compatible to `std::begin` and `std::end`)
@@ -91,14 +89,8 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va
- Throws [parse_error.112](../../home/exceptions.md#jsonexceptionparse_error112) if unsupported features from CBOR were - Throws [parse_error.112](../../home/exceptions.md#jsonexceptionparse_error112) if unsupported features from CBOR were
used in the given input or if the input is not valid CBOR used in the given input or if the input is not valid CBOR
- Throws [parse_error.113](../../home/exceptions.md#jsonexceptionparse_error113) if a map key is not a string (keys of - Throws [parse_error.113](../../home/exceptions.md#jsonexceptionparse_error113) if a map key is not a string (keys of
other types are not supported, as JSON object keys are always strings), if the head of a string or byte string is other types are not supported, as JSON object keys are always strings), or if a string value or object key is not
malformed (including a chunk of the wrong type or an indefinite-length string nested inside an indefinite-length valid UTF-8 and `error_handler` is `strict`
string), or if a string value or object key (or one chunk of an indefinite-length string) is not valid UTF-8 and
`error_handler` is `strict`
- Throws [out_of_range.408](../../home/exceptions.md#jsonexceptionout_of_range408) if an array or map announces
a length that `std::size_t` cannot hold, or that equals `std::size_t(-1)` (for example `9B FF FF FF FF FF FF FF FF`)
- Throws [out_of_range.406](../../home/exceptions.md#jsonexceptionout_of_range406) if a floating-point number does not
fit into `number_float_t`
## Complexity ## Complexity
+4 -11
View File
@@ -79,18 +79,11 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va
- Throws [parse_error.110](../../home/exceptions.md#jsonexceptionparse_error110) if the given input ends prematurely or - Throws [parse_error.110](../../home/exceptions.md#jsonexceptionparse_error110) if the given input ends prematurely or
the end of the file was not reached when `strict` was set to true the end of the file was not reached when `strict` was set to true
- Throws [parse_error.112](../../home/exceptions.md#jsonexceptionparse_error112) if an invalid type marker is read - Throws [parse_error.112](../../home/exceptions.md#jsonexceptionparse_error112) if a parse error occurs
(including a type marker after `$` of an optimized container, even if its count is 0), or if `$` is not followed by `#`
- Throws [parse_error.113](../../home/exceptions.md#jsonexceptionparse_error113) if a string could not be parsed - Throws [parse_error.113](../../home/exceptions.md#jsonexceptionparse_error113) if a string could not be parsed
successfully (negative or non-integer length, `C` followed by a byte above 0x7F), if the count of an optimized successfully, or if a string value or object key is not valid UTF-8 and `error_handler` is `strict`
container is negative, or if a string value or object key is not valid UTF-8 and `error_handler` is `strict` - Throws [out_of_range.408](../../home/exceptions.md#jsonexceptionout_of_range408) if the size of an optimized container
- Throws [parse_error.115](../../home/exceptions.md#jsonexceptionparse_error115) if the text of a high-precision number or n-dimensional array cannot be represented by `std::size_t`
(`H`) is not a valid JSON number
- 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`
- 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`), or if an optimized array of `Z`, `T`, or `F` has
more than 1,048,576 elements
## Complexity ## Complexity
+1 -1
View File
@@ -13,7 +13,7 @@ static void to_bson(const basic_json& j, detail::output_adapter<char> o,
``` ```
BSON (Binary JSON) is a binary format in which zero or more ordered key/value pairs are stored as a single entity (a BSON (Binary JSON) is a binary format in which zero or more ordered key/value pairs are stored as a single entity (a
so-called document). The members of an object are written in the order in which iteration visits them. so-called document).
1. Returns a byte vector containing the BSON serialization. 1. Returns a byte vector containing the BSON serialization.
2. Writes the BSON serialization to an output adapter. 2. Writes the BSON serialization to an output adapter.
+1 -2
View File
@@ -42,8 +42,7 @@ The exact mapping and its limitations are described on a [dedicated page](../../
## Exception safety ## Exception safety
Strong guarantee: if an exception is thrown, there are no changes in the JSON value. Bytes that were already written to Strong guarantee: if an exception is thrown, there are no changes in the JSON value.
the output adapter are not taken back.
## Exceptions ## Exceptions
@@ -53,7 +53,7 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va
value, array, or object exceeds 4294967295, the maximum MessagePack can store; example: value, array, or object exceeds 4294967295, the maximum MessagePack can store; example:
`"MessagePack length 4294967296 exceeds maximum of 4294967295"` `"MessagePack length 4294967296 exceeds maximum of 4294967295"`
- Throws [`out_of_range.415`](../../home/exceptions.md#jsonexceptionout_of_range415) if the subtype of a binary value - Throws [`out_of_range.415`](../../home/exceptions.md#jsonexceptionout_of_range415) if the subtype of a binary value
exceeds 255, the largest value the library can store in the one-byte MessagePack ext type; example: exceeds 255, the maximum of the MessagePack ext type; example:
`"subtype 70000 is too large for the MessagePack ext type (max 255)"` `"subtype 70000 is too large for the MessagePack ext type (max 255)"`
- Throws [type_error.316](../../home/exceptions.md#jsonexceptiontype_error316) if a string or object key in `j` is - Throws [type_error.316](../../home/exceptions.md#jsonexceptiontype_error316) if a string or object key in `j` is
not valid UTF-8 and `error_handler` is `strict` not valid UTF-8 and `error_handler` is `strict`
@@ -6,7 +6,7 @@ class byte_container_with_subtype : public BinaryType;
``` ```
This type extends the template parameter `BinaryType` provided to [`basic_json`](../basic_json/index.md) with a subtype This type extends the template parameter `BinaryType` provided to [`basic_json`](../basic_json/index.md) with a subtype
used by BSON, CBOR, and MessagePack. This type exists so that the user does not have to specify a type themselves with a used by BSON and MessagePack. This type exists so that the user does not have to specify a type themselves with a
specific naming scheme in order to override the binary type. specific naming scheme in order to override the binary type.
## Template parameters ## Template parameters
@@ -66,12 +66,7 @@ The library uses the following mapping from JSON values types to BSON types:
!!! warning "Incomplete mapping" !!! warning "Incomplete mapping"
The mapping is **incomplete**, since only JSON-objects (and things contained therein) can be serialized to BSON. The mapping is **incomplete**, since only JSON-objects (and things contained therein) can be serialized to BSON.
Also, keys may not contain U+0000, since they are serialized as zero-terminated C strings. Also, keys may not contain U+0000, since they are serialized a zero-terminated c-strings.
!!! info "Order of object members"
The members of an object are written in the order in which iteration visits them: sorted by key for
[`json`](../../api/json.md), in insertion order for [`ordered_json`](../../api/ordered_json.md).
!!! warning "BSON type 0x11 interoperability" !!! warning "BSON type 0x11 interoperability"
@@ -86,19 +81,6 @@ The library uses the following mapping from JSON values types to BSON types:
As a result, serializing and deserializing a JSON object containing such a value produces a different JSON object, As a result, serializing and deserializing a JSON object containing such a value produces a different JSON object,
even though the binary data is unchanged. even though the binary data is unchanged.
!!! info "Binary subtype 0x02 (old binary)"
The BSON specification defines the payload of the "binary (old)" subtype `0x02` as an int32 followed by that many
bytes, so the value is preceded by two lengths: the length of the binary element and, as the first four bytes of
the payload, the number of bytes that follow. A binary value with subtype `2` is therefore written with the outer
length 4 bytes larger than its size and the size as inner length: `binary({1, 2, 3}, 2)` becomes
`07 00 00 00 02 03 00 00 00 01 02 03`. When reading, the inner length is checked and removed, so the value
contains only the bytes that follow it. `from_bson()` throws
[`parse_error.112`](../../home/exceptions.md#jsonexceptionparse_error112) if the outer length is less than 4 or the
inner length is not the outer length minus 4. Versions before 3.13.0 did not write the inner length and returned
it as part of the payload; BSON data written by those versions with subtype 2 is not readable any more and must be
written again.
??? example "Example: serialize a JSON value to BSON" ??? example "Example: serialize a JSON value to BSON"
```cpp ```cpp
@@ -153,29 +135,12 @@ The library maps BSON record types to JSON value types as follows:
The BSON reader is lenient in a few areas where the BSON specification is more restrictive: The BSON reader is lenient in a few areas where the BSON specification is more restrictive:
- array element keys are not checked against the required decimal sequence (`0`, `1`, `2`, ...), - array element keys are not checked against the required decimal sequence (`0`, `1`, `2`, ...),
- any non-zero byte is accepted as `true` for the boolean type, - any non-zero byte is accepted as `true` for the boolean type, and
- the payloads of the binary subtypes `0x03` (UUID, old), `0x04` (UUID), and `0x05` (MD5) are not checked to have - the payload for binary subtype `0x02` is returned as-is, including its inner length prefix.
16 bytes,
- duplicate keys within a document are accepted; the last value wins (`{"a": 1, "a": 2}` is read as `{"a": 2}`),
while a SAX handler receives every occurrence,
- a `string` value may contain embedded `0x00` bytes, and
- with `strict` set to `false`, bytes after the end of the document are ignored.
If BSON input must be validated for strict specification compliance, validate it separately before passing it to If BSON input must be validated for strict specification compliance, validate it separately before passing it to
`from_bson()`. `from_bson()`.
!!! note "Document size"
Every document starts with its size. `from_bson()` rejects a document with
[`parse_error.112`](../../home/exceptions.md#jsonexceptionparse_error112) if the size is less than 5 (the size prefix and the terminator), if an embedded document does not end before the
end of the document that contains it, if the elements do not fit into the declared size, or if the size does not
match the number of bytes read when the terminating `0x00` byte is reached. A size that is too small is detected
before an element is read whose type byte and name no longer fit; an element that starts inside the declared size
but extends past it is read, and reported to a SAX handler, before the mismatch is found at the terminator. A size
that is too large is likewise found only at the terminator. Passing `strict` as `false` skips only the check for bytes after
the end of the document ([`parse_error.110`](../../home/exceptions.md#jsonexceptionparse_error110)), not the
document size checks.
!!! warning "Ill-formed UTF-8 in string values" !!! warning "Ill-formed UTF-8 in string values"
The BSON specification requires `string` values (type `0x02`) to be valid UTF-8, but this is not required of a The BSON specification requires `string` values (type `0x02`) to be valid UTF-8, but this is not required of a
@@ -189,9 +154,8 @@ The library maps BSON record types to JSON value types as follows:
ill-formed bytes. `to_bson()`'s own `error_handler` parameter defaults to `keep`, so such a string value or element ill-formed bytes. `to_bson()`'s own `error_handler` parameter defaults to `keep`, so such a string value or element
(key) name is written unchanged; with `strict` (the default if (key) name is written unchanged; with `strict` (the default if
[`JSON_STRICT_BINARY_UTF8`](../../api/macros/json_strict_binary_utf8.md) is enabled), it throws the same exception [`JSON_STRICT_BINARY_UTF8`](../../api/macros/json_strict_binary_utf8.md) is enabled), it throws the same exception
instead. Element (key) names are treated like string values: with `keep` they are read unchanged, with `strict`, instead. Element (key) names are never validated on read, since they are read byte-by-byte as a C string. `binary`
`replace`, or `ignore` they are checked and handled in the same way. `binary` values (type `0x05`) are unaffected, values (type `0x05`) are unaffected, since they are not required to hold text.
since they are not required to hold text.
??? example "Example: deserialize a JSON value from BSON" ??? example "Example: deserialize a JSON value from BSON"
@@ -11,7 +11,7 @@ extremely small code sizes, fairly small message size, and extensibility without
## Byte layout ## Byte layout
CBOR encodes every data item as a *head* followed by optional content. The first byte of the head holds the *major type* in its upper three bits (unsigned integer, negative integer, byte string, text string, array, map, tag, or simple/float) and an *additional information* value in its lower five bits. Additional information below 24 is the argument itself (for example, a small integer or a short length); the values 24 to 27 announce that the argument follows in 1, 2, 4, or 8 big-endian bytes. Strings, arrays, and maps are prefixed with their length (or count of elements/pairs), and content follows directly. A reader of definite-length items knows how many items to expect without a terminator; indefinite-length items (additional information 31) are instead closed by the "break" byte 0xFF, which `from_cbor` reads but `to_cbor` never writes. CBOR encodes every data item as a *head* followed by optional content. The first byte of the head holds the *major type* in its upper three bits (unsigned integer, negative integer, byte string, text string, array, map, tag, or simple/float) and an *additional information* value in its lower five bits. Additional information below 24 is the argument itself (for example, a small integer or a short length); the values 24 to 27 announce that the argument follows in 1, 2, 4, or 8 big-endian bytes. Strings, arrays, and maps are prefixed with their length (or count of elements/pairs), and content follows directly, so a reader always knows how many items to expect without a terminator.
Consider the following JSON value (shown formatted for readability): Consider the following JSON value (shown formatted for readability):
@@ -52,16 +52,16 @@ The library uses the following mapping from JSON values types to CBOR types acco
| null | `null` | Null | 0xF6 | | null | `null` | Null | 0xF6 |
| boolean | `true` | True | 0xF5 | | boolean | `true` | True | 0xF5 |
| boolean | `false` | False | 0xF4 | | boolean | `false` | False | 0xF4 |
| number_integer | -9223372036854775808..-4294967297 | Negative integer (8 bytes follow) | 0x3B | | number_integer | -9223372036854775808..-2147483649 | Negative integer (8 bytes follow) | 0x3B |
| number_integer | -4294967296..-65537 | Negative integer (4 bytes follow) | 0x3A | | number_integer | -2147483648..-32769 | Negative integer (4 bytes follow) | 0x3A |
| number_integer | -65536..-257 | Negative integer (2 bytes follow) | 0x39 | | number_integer | -32768..-129 | Negative integer (2 bytes follow) | 0x39 |
| number_integer | -256..-25 | Negative integer (1 byte follow) | 0x38 | | number_integer | -128..-25 | Negative integer (1 byte follow) | 0x38 |
| number_integer | -24..-1 | Negative integer | 0x20..0x37 | | number_integer | -24..-1 | Negative integer | 0x20..0x37 |
| number_integer | 0..23 | Integer | 0x00..0x17 | | number_integer | 0..23 | Integer | 0x00..0x17 |
| number_integer | 24..255 | Unsigned integer (1 byte follow) | 0x18 | | number_integer | 24..255 | Unsigned integer (1 byte follow) | 0x18 |
| number_integer | 256..65535 | Unsigned integer (2 bytes follow) | 0x19 | | number_integer | 256..65535 | Unsigned integer (2 bytes follow) | 0x19 |
| number_integer | 65536..4294967295 | Unsigned integer (4 bytes follow) | 0x1A | | number_integer | 65536..4294967295 | Unsigned integer (4 bytes follow) | 0x1A |
| number_integer | 4294967296..9223372036854775807 | Unsigned integer (8 bytes follow) | 0x1B | | number_integer | 4294967296..18446744073709551615 | Unsigned integer (8 bytes follow) | 0x1B |
| number_unsigned | 0..23 | Integer | 0x00..0x17 | | number_unsigned | 0..23 | Integer | 0x00..0x17 |
| number_unsigned | 24..255 | Unsigned integer (1 byte follow) | 0x18 | | number_unsigned | 24..255 | Unsigned integer (1 byte follow) | 0x18 |
| number_unsigned | 256..65535 | Unsigned integer (2 bytes follow) | 0x19 | | number_unsigned | 256..65535 | Unsigned integer (2 bytes follow) | 0x19 |
@@ -95,24 +95,7 @@ see "binary" cells in the table above.
!!! success "Complete mapping" !!! success "Complete mapping"
The mapping is **complete** in the sense that any JSON value type can be converted to a CBOR value, with one The mapping is **complete** in the sense that any JSON value type can be converted to a CBOR value.
exception: an integer outside the range of CBOR integers (only possible with a number type wider than 64 bits)
throws [`out_of_range.407`](../../home/exceptions.md#jsonexceptionout_of_range407).
!!! info "Not a deterministic encoding"
`to_cbor` does not produce the deterministic encoding of
[RFC 8949, Section 4.2.1](https://www.rfc-editor.org/rfc/rfc8949.html#section-4.2.1), and not always the preferred
serialization of Section 4.1:
- floating-point numbers are written as single precision (0xFA) when that is exact and as double precision (0xFB)
otherwise; half precision (0xF9) is used only for `NaN` and `Infinity`/`-Infinity` (`1.0` is `fa 3f 80 00 00`);
- a binary subtype 0..23 is written as `d8 xx` instead of the one-byte tag 0xC0..0xD7 (`d8 01 41 01`);
- object members are written in the order of `object_t` (for the default `std::map`: sorted by the key string, not
by the encoded key bytes).
Integers and lengths use the shortest form, and indefinite-length items are never written. Decoders must accept all
of this; the RFC does not require the preferred serialization.
!!! info "NaN/infinity handling" !!! info "NaN/infinity handling"
@@ -122,8 +105,9 @@ see "binary" cells in the table above.
!!! note !!! note
Prior to version 3.8.0, NaN and Infinity were instead serialized as a CBOR double-precision float (type 0xFB, Prior to version 3.13.0, NaN and Infinity were instead serialized as a CBOR double-precision float (type 0xFB,
9 bytes total). 9 bytes total), because the check used to select a smaller encoding compared magnitudes with NaN, which is
always `false` and caused the intended half-precision path to be skipped.
!!! info "Unused CBOR types" !!! info "Unused CBOR types"
@@ -140,6 +124,7 @@ see "binary" cells in the table above.
- expected conversions (0xD5..0xD7) - expected conversions (0xD5..0xD7)
- simple values (0xE0..0xF3, 0xF8) - simple values (0xE0..0xF3, 0xF8)
- undefined (0xF7) - undefined (0xF7)
- half-precision floats (0xF9)
- break (0xFF) - break (0xFF)
!!! info "Tagged items" !!! info "Tagged items"
@@ -241,20 +226,10 @@ string of the same major type, as required by [RFC 8949, Section 3.2.3](https://
([RFC 8392](https://www.rfc-editor.org/rfc/rfc8392.html)), cannot be read with this library and need a ([RFC 8392](https://www.rfc-editor.org/rfc/rfc8392.html)), cannot be read with this library and need a
general-purpose CBOR library instead. general-purpose CBOR library instead.
A tag in front of a text string key is handled like a tag in front of a value: it is skipped with
`cbor_tag_handler_t::ignore` or `cbor_tag_handler_t::store` and rejected with the default `error`. A tag does not
turn a key of another type into a text string.
A map with duplicate keys is well-formed, but invalid under
[RFC 8949, Section 5.3.1](https://www.rfc-editor.org/rfc/rfc8949.html#section-5.3.1). `from_cbor` accepts it and the
last value wins; the [SAX interface](../parsing/sax_interface.md) reports every occurrence.
!!! warning "Ill-formed UTF-8 in text strings" !!! warning "Ill-formed UTF-8 in text strings"
[RFC 8949, Section 3.1](https://www.rfc-editor.org/rfc/rfc8949.html#section-3.1) requires CBOR text strings (major [RFC 8949, Section 3.1](https://www.rfc-editor.org/rfc/rfc8949.html#section-3.1) requires CBOR text strings (major
type 3) to be valid UTF-8 (a string with ill-formed UTF-8 is "well-formed but invalid"), and type 3) to be valid UTF-8, but leaves it up to the decoder whether to enforce this, so checking is opt-in: with the
[Section 5.3.1](https://www.rfc-editor.org/rfc/rfc8949.html#section-5.3.1) leaves it up to the decoder whether to
verify this, so checking is opt-in: with the
[`error_handler`](../../api/basic_json/from_cbor.md) parameter left at `keep` (the default), `from_cbor()` accepts a [`error_handler`](../../api/basic_json/from_cbor.md) parameter left at `keep` (the default), `from_cbor()` accepts a
text string (object keys included) whose bytes are not valid UTF-8 and hands them back unchanged. Passing text string (object keys included) whose bytes are not valid UTF-8 and hands them back unchanged. Passing
`error_handler_t::strict` makes `from_cbor()` check and throw `error_handler_t::strict` makes `from_cbor()` check and throw
@@ -267,14 +242,9 @@ string of the same major type, as required by [RFC 8949, Section 3.2.3](https://
[`JSON_STRICT_BINARY_UTF8`](../../api/macros/json_strict_binary_utf8.md) is enabled), it throws the same exception [`JSON_STRICT_BINARY_UTF8`](../../api/macros/json_strict_binary_utf8.md) is enabled), it throws the same exception
instead. Byte strings (major type 2) are unaffected, since they are not required to hold text. instead. Byte strings (major type 2) are unaffected, since they are not required to hold text.
The chunks of an indefinite-length text string are checked one by one, as required by
[RFC 8949, Section 3.2.3](https://www.rfc-editor.org/rfc/rfc8949.html#section-3.2.3): a code point cannot be spread
over two chunks. With `error_handler_t::strict` such a string throws `parse_error.113`; `replace` and `ignore`
sanitize each chunk on its own.
!!! warning "Tagged items" !!! warning "Tagged items"
Tagged items (0xC0..0xDB) will throw a parse error by default. They can be ignored by passing `cbor_tag_handler_t::ignore` to function `from_cbor`, in which case the tag is skipped and the enclosed data item is parsed on its own. Passing `cbor_tag_handler_t::store` to function `from_cbor` stores tagged byte strings (for bytes 0xd8..0xdb) as binary values with the tag as subtype; other tagged values are read as if the tag were ignored. If several tags precede a byte string, only the tag directly in front of it is stored, and only when it has the form 0xD8..0xDB (a tag 0..23 written as 0xC0..0xD7 is skipped); for example, `D8 05 C1 41 AA` yields subtype `null`, whereas `C1 D8 05 41 AA` yields 5. Tags in front of a text string map key are skipped as well. Note that no tag is ever interpreted: for instance, a text string tagged with tag 0 (date/time) stays a string. Tagged items (0xC0..0xDB) will throw a parse error by default. They can be ignored by passing `cbor_tag_handler_t::ignore` to function `from_cbor`, in which case the tag is skipped and the enclosed data item is parsed on its own. Passing `cbor_tag_handler_t::store` to function `from_cbor` stores tagged byte strings (for bytes 0xd8..0xdb) as binary values with the tag as subtype; other tagged values are read as if the tag were ignored. If several tags precede a byte string, only the innermost one is stored. Note that no tag is ever interpreted: for instance, a text string tagged with tag 0 (date/time) stays a string.
??? example "Example: deserialize a JSON value from CBOR" ??? example "Example: deserialize a JSON value from CBOR"
@@ -22,7 +22,7 @@ to efficiently encode JSON values to byte vectors and to decode such vectors.
| BON8 | incomplete: no unsigned integers above int64 | complete | | BON8 | incomplete: no unsigned integers above int64 | complete |
| BSON | incomplete: top-level value must be an object | incomplete, but all JSON types are supported | | BSON | incomplete: top-level value must be an object | incomplete, but all JSON types are supported |
| CBOR | complete | incomplete, but all JSON types are supported | | CBOR | complete | incomplete, but all JSON types are supported |
| MessagePack | complete | incomplete, but all JSON types are supported | | MessagePack | complete | complete |
| UBJSON | complete | complete | | UBJSON | complete | complete |
### Binary values ### Binary values
@@ -11,7 +11,7 @@ extra byte in addition to the strings themselves.
## Byte layout ## Byte layout
MessagePack encodes each value as a one-byte *format marker*, optionally followed by a length or the payload. The marker range is partitioned to keep common values small: small non-negative integers (0 to 127) and small negative integers (-32 to -1) are the marker byte itself; short strings, arrays, and maps (up to 31 bytes for strings and up to 15 elements for arrays and maps) hide their length in the marker's lower bits. Larger values use dedicated markers that announce the width (8, 16, 32, or 64 bits) of the length or number that follows, in big-endian byte order. Like CBOR, containers are length-prefixed, so no terminator is needed. MessagePack encodes each value as a one-byte *format marker*, optionally followed by a length or the payload. The marker range is partitioned to keep common values small: small non-negative integers (0 to 127) and small negative integers (-32 to -1) are the marker byte itself; short strings, arrays, and maps (up to 31 or 15 elements) hide their length in the marker's lower bits. Larger values use dedicated markers that announce the width (8, 16, 32, or 64 bits) of the length or number that follows, in big-endian byte order. Like CBOR, containers are length-prefixed, so no terminator is needed.
Consider the following JSON value (shown formatted for readability): Consider the following JSON value (shown formatted for readability):
@@ -109,9 +109,8 @@ specification:
!!! note !!! note
Prior to version 3.13.0, NaN and Infinity were instead serialized as a MessagePack float 64 (type 0xCB, 9 bytes Prior to version 3.13.0, NaN and Infinity were instead serialized as a MessagePack float 64 (type 0xCB, 9 bytes
total), because the check used to select the smaller float 32 encoding failed for NaN (all comparisons are `false`) total), because the check used to select the smaller float 32 encoding compared magnitudes with NaN, which is
and for Infinity and -Infinity (outside the float range checked against `std::numeric_limits<float>::lowest()` and always `false` and caused the float 32 path to be skipped.
`max()`), so the float 32 path was skipped.
??? example "Example: serialize a JSON value to MessagePack" ??? example "Example: serialize a JSON value to MessagePack"
@@ -136,7 +135,6 @@ The library maps MessagePack types to JSON value types as follows:
| fixarray | array | 0x90..0x9F | | fixarray | array | 0x90..0x9F |
| fixstr | string | 0xA0..0xBF | | fixstr | string | 0xA0..0xBF |
| nil | `null` | 0xC0 | | nil | `null` | 0xC0 |
| (never used) | – | 0xC1 |
| false | `false` | 0xC2 | | false | `false` | 0xC2 |
| true | `true` | 0xC3 | | true | `true` | 0xC3 |
| float 32 | number_float | 0xCA | | float 32 | number_float | 0xCA |
@@ -169,30 +167,6 @@ The library maps MessagePack types to JSON value types as follows:
| fixext 16 | binary | 0xD8 | | fixext 16 | binary | 0xD8 |
| negative fixint | number_integer | 0xE0-0xFF | | negative fixint | number_integer | 0xE0-0xFF |
The byte 0xC1 is reserved as "never used" by the specification. As a value it is rejected with
[`parse_error.112`](../../home/exceptions.md#jsonexceptionparse_error112) (`invalid byte: 0xC1`); as a map key it is
rejected with [`parse_error.113`](../../home/exceptions.md#jsonexceptionparse_error113) and the generic "expected length
specification" message, because the key is read as a string.
Duplicate keys in a map are accepted; the last value wins.
### Extension types (ext, fixext)
MessagePack ext and fixext values are read as binary values, and the ext type byte is stored as the binary subtype.
Although the specification defines the type as a signed 8-bit integer (0 to 127 for applications, -128 to -1 reserved,
with -1 being the Timestamp extension), the library reads it as an unsigned byte: a negative ext type `n` becomes
subtype `256+n`, so ext type -1 is read as subtype 255 and -128 as subtype 128. The Timestamp extension is not
interpreted; it is returned as binary with subtype 255. Use
[`has_subtype()`](../../api/byte_container_with_subtype/has_subtype.md) to tell a `bin` value (no subtype) from an ext
value (subtype, even if it is 0).
When serializing, a binary value with a subtype is always written as fixext (1, 2, 4, 8, or 16 bytes) or ext, even
for subtype 0 (`d6 00 01 02 03 04`); a binary value without a subtype is written as bin. Subtypes 128 to 255 are
written as the reserved negative ext types (255 as -1), so `json::binary({1,2,3,4}, 255)` becomes
`d6 ff 01 02 03 04`, which other MessagePack libraries decode as a 32-bit timestamp. A subtype above 255 throws
[`out_of_range.415`](../../home/exceptions.md#jsonexceptionout_of_range415). The payload length is limited to
4294967295 bytes like all other lengths.
!!! info !!! info
Any MessagePack output created by `to_msgpack` can be successfully parsed by `from_msgpack`. Any MessagePack output created by `to_msgpack` can be successfully parsed by `from_msgpack`.
@@ -203,8 +177,7 @@ written as the reserved negative ext types (255 as -1), so `json::binary({1,2,3,
JSON-compatible [profile](https://github.com/msgpack/msgpack/blob/master/spec.md#profile) sketched in the JSON-compatible [profile](https://github.com/msgpack/msgpack/blob/master/spec.md#profile) sketched in the
MessagePack specification, this library restricts map keys to `str` values. Maps with keys of any other type are MessagePack specification, this library restricts map keys to `str` values. Maps with keys of any other type are
rejected with a [`parse_error.113`](../../home/exceptions.md#jsonexceptionparse_error113) exception (or, with rejected with a [`parse_error.113`](../../home/exceptions.md#jsonexceptionparse_error113) exception (or, with
`allow_exceptions` set to `false`, a discarded value). For most key types the message names the type that was `allow_exceptions` set to `false`, a discarded value) naming the type of the key that was found, for instance:
found, for instance:
``` ```
[json.exception.parse_error.113] parse error at byte 2: syntax error while parsing MessagePack object key: only string keys are supported, but found nil; last byte: 0xC0 [json.exception.parse_error.113] parse error at byte 2: syntax error while parsing MessagePack object key: only string keys are supported, but found nil; last byte: 0xC0
@@ -216,7 +189,7 @@ written as the reserved negative ext types (255 as -1), so `json::binary({1,2,3,
!!! warning "Ill-formed UTF-8 in string values" !!! warning "Ill-formed UTF-8 in string values"
The MessagePack specification explicitly allows a `str` value (`fixstr`, `str 8`, `str 16`, `str 32`) to contain The MessagePack specification explicitly allows a `str` value (`fixstr`, `str 8`, `str 16`, `str 32`) to contain
a byte sequence that is not valid UTF-8, and recommends that a deserializer provide a way to obtain the original bytes. a byte sequence that is not valid UTF-8, and expects a deserializer to hand the original bytes back unchanged.
This library follows that by default: with its This library follows that by default: with its
[`error_handler`](../../api/basic_json/from_msgpack.md) parameter left at `keep` (the default), [`error_handler`](../../api/basic_json/from_msgpack.md) parameter left at `keep` (the default),
`from_msgpack()` reads `str` bytes (object keys included) as-is, without validating them, so such a value `from_msgpack()` reads `str` bytes (object keys included) as-is, without validating them, so such a value
@@ -5,11 +5,11 @@ achieve the generality of JSON, combined with being much easier to process than
!!! abstract "References" !!! abstract "References"
- [UBJSON Website](https://ubjson.org) - [UBJSON Website](http://ubjson.org)
## Byte layout ## Byte layout
UBJSON is built from just a one-byte, human-readable ASCII *type marker* (for example, `i` for int8, `S` for string, `[` for array) followed by the payload of that type. Strings and the like are prefixed with their length, which is itself a UBJSON integer value including its marker. Numbers are stored in big-endian byte order. Containers can be written in two ways: as an *unoptimized* container that is opened with `[` or `{` and closed with `]` or `}`, or as an *optimized* container that declares a count (`#`) after the opening marker, optionally preceded by an element type (`$`); an element type is only allowed together with a count. An optimized container saves the closing marker and, with a type, the per-element markers. UBJSON is built from just a one-byte, human-readable ASCII *type marker* (for example, `i` for int8, `S` for string, `[` for array) followed by the payload of that type. Strings and the like are prefixed with their length, which is itself a UBJSON integer value including its marker. Numbers are stored in big-endian byte order. Containers can be written in two ways: as an *unoptimized* container that is opened with `[` or `{` and closed with `]` or `}`, or as an *optimized* container that declares an element type (`$`) and/or a count (`#`) after the opening marker, which saves per-element markers and (for a count) the closing marker.
Consider the following JSON value (shown formatted for readability): Consider the following JSON value (shown formatted for readability):
@@ -44,29 +44,29 @@ Consider the following JSON value (shown formatted for readability):
The library uses the following mapping from JSON values types to UBJSON types according to the UBJSON specification: The library uses the following mapping from JSON values types to UBJSON types according to the UBJSON specification:
| JSON value type | value/range | UBJSON type | marker | | JSON value type | value/range | UBJSON type | marker |
|-----------------|-------------------------------------------|--------------------------------------------------|-----------| |-----------------|-------------------------------------------|----------------|--------|
| null | `null` | null | `Z` | | null | `null` | null | `Z` |
| boolean | `true` | true | `T` | | boolean | `true` | true | `T` |
| boolean | `false` | false | `F` | | boolean | `false` | false | `F` |
| number_integer | -9223372036854775808..-2147483649 | int64 | `L` | | number_integer | -9223372036854775808..-2147483649 | int64 | `L` |
| number_integer | -2147483648..-32769 | int32 | `l` | | number_integer | -2147483648..-32769 | int32 | `l` |
| number_integer | -32768..-129 | int16 | `I` | | number_integer | -32768..-129 | int16 | `I` |
| number_integer | -128..127 | int8 | `i` | | number_integer | -128..127 | int8 | `i` |
| number_integer | 128..255 | uint8 | `U` | | number_integer | 128..255 | uint8 | `U` |
| number_integer | 256..32767 | int16 | `I` | | number_integer | 256..32767 | int16 | `I` |
| number_integer | 32768..2147483647 | int32 | `l` | | number_integer | 32768..2147483647 | int32 | `l` |
| number_integer | 2147483648..9223372036854775807 | int64 | `L` | | number_integer | 2147483648..9223372036854775807 | int64 | `L` |
| number_unsigned | 0..127 | int8 | `i` | | number_unsigned | 0..127 | int8 | `i` |
| number_unsigned | 128..255 | uint8 | `U` | | number_unsigned | 128..255 | uint8 | `U` |
| number_unsigned | 256..32767 | int16 | `I` | | number_unsigned | 256..32767 | int16 | `I` |
| number_unsigned | 32768..2147483647 | int32 | `l` | | number_unsigned | 32768..2147483647 | int32 | `l` |
| number_unsigned | 2147483648..9223372036854775807 | int64 | `L` | | number_unsigned | 2147483648..9223372036854775807 | int64 | `L` |
| number_unsigned | 9223372036854775808..18446744073709551615 | high-precision | `H` | | number_unsigned | 9223372036854775808..18446744073709551615 | high-precision | `H` |
| number_float | *any value* | float64 (float32 if `number_float_t` is `float`) | `D` (`d`) | | number_float | *any value* | float64 | `D` |
| string | *with shortest length indicator* | string | `S` | | string | *with shortest length indicator* | string | `S` |
| array | *see notes on optimized format* | array | `[` | | array | *see notes on optimized format* | array | `[` |
| object | *see notes on optimized format* | object | `{` | | object | *see notes on optimized format* | map | `{` |
!!! success "Complete mapping" !!! success "Complete mapping"
@@ -93,27 +93,20 @@ The library uses the following mapping from JSON values types to UBJSON types ac
The following markers are not used in the conversion: The following markers are not used in the conversion:
- `N`: no-op values are never written (they are skipped when reading). - `Z`: no-op values are not created.
- `C`: single-byte strings are serialized with `S` markers. - `C`: single-byte strings are serialized with `S` markers.
!!! info "NaN/infinity handling" !!! info "NaN/infinity handling"
NaN and infinity are written as IEEE 754 float64 values (`D`) and read back unchanged. This differs from If NaN or Infinity are stored inside a JSON number, they are serialized properly. This behavior differs from the
[`dump()`](../../api/basic_json/dump.md), which serializes them to `null`, and from the UBJSON specification, which [`dump()`](../../api/basic_json/dump.md) function which serializes NaN or Infinity to `null`.
encodes infinity as `null` (it does not mention NaN); other UBJSON readers may therefore not accept these values.
!!! info "Optimized formats" !!! info "Optimized formats"
The optimized formats for containers are supported: Parameter `use_size` adds size information to the beginning of a The optimized formats for containers are supported: Parameter `use_size` adds size information to the beginning of a
container and removes the closing marker. Parameter `use_type` further checks whether all elements of a container container and removes the closing marker. Parameter `use_type` further checks whether all elements of a container
have the same type and adds the type marker to the beginning of the container. The `use_type` parameter must only be have the same type and adds the type marker to the beginning of the container. The `use_type` parameter must only be
used together with `use_size = true`: combining `use_type` with `use_size = false` throws used together with `use_size = true`.
[`other_error.502`](../../home/exceptions.md#jsonexceptionother_error502) if the value contains a non-empty array,
object, or binary value.
Two elements have the same type if they are written with the same type marker. For example, `[1, 2]` is written with
the type `i`, but `[1, 200]` (`i`, `U`), `[true, false]` (`T`, `F`), and `[1, 2.5]` (`i`, `D`) are not; an empty
container is never given a `$` annotation of its own (an array of empty arrays is still typed `[`).
Note that `use_size = true` alone may result in larger representations - the benefit of this parameter is that the Note that `use_size = true` alone may result in larger representations - the benefit of this parameter is that the
receiving side is immediately informed on the number of elements of the container. receiving side is immediately informed on the number of elements of the container.
@@ -122,9 +115,8 @@ 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`), and `to_ubjson` writes longer arrays of these types in the counted but untyped form: the `$` type marker is (`1 << 20`), and `to_ubjson` writes longer arrays of these types without the annotation, so any value it produces
dropped, the `#` count is kept, and every element is written with its own marker (`[#l 1048577 Z Z ...`), so any value can be read back.
it produces can be read back. Objects and untyped counted arrays are not subject to this limit.
!!! info "Binary values" !!! info "Binary values"
@@ -148,54 +140,27 @@ The library uses the following mapping from JSON values types to UBJSON types ac
The library maps UBJSON types to JSON value types as follows: The library maps UBJSON types to JSON value types as follows:
| UBJSON type | JSON value type | marker | | UBJSON type | JSON value type | marker |
|----------------|--------------------------------------------------|--------| |-------------|-----------------------------------------|--------|
| no-op | *no value, next value is read* | `N` | | no-op | *no value, next value is read* | `N` |
| null | `null` | `Z` | | null | `null` | `Z` |
| false | `false` | `F` | | false | `false` | `F` |
| true | `true` | `T` | | true | `true` | `T` |
| float32 | number_float | `d` | | float32 | number_float | `d` |
| float64 | number_float | `D` | | float64 | number_float | `D` |
| uint8 | number_unsigned | `U` | | uint8 | number_unsigned | `U` |
| int8 | number_integer | `i` | | int8 | number_integer | `i` |
| int16 | number_integer | `I` | | int16 | number_integer | `I` |
| int32 | number_integer | `l` | | int32 | number_integer | `l` |
| int64 | number_integer | `L` | | int64 | number_integer | `L` |
| string | string | `S` | | string | string | `S` |
| char | string | `C` | | char | string | `C` |
| high-precision | number_unsigned, number_integer, or number_float | `H` | | array | array (optimized values are supported) | `[` |
| array | array (optimized values are supported) | `[` | | object | object (optimized values are supported) | `{` |
| object | object (optimized values are supported) | `{` |
!!! info "High-precision numbers"
The payload of `H` is a length-prefixed string that is parsed as a JSON number (surrounding whitespace is ignored).
Integer text in the range of `number_unsigned_t` (non-negative) or `number_integer_t` (negative) is stored as such;
any other valid number text (larger or smaller integers, fractions, and exponents such as `1e2`) is stored as
`number_float`, so precision may be lost. Text that is not a valid JSON number raises
[`parse_error.115`](../../home/exceptions.md#jsonexceptionparse_error115), a number outside the range of
`number_float_t` (`1e999`) raises [`out_of_range.406`](../../home/exceptions.md#jsonexceptionout_of_range406).
!!! success "Complete mapping" !!! success "Complete mapping"
Every well-formed UBJSON value can be converted to a JSON value, with these exceptions: a high-precision number The mapping is **complete** in the sense that any UBJSON value can be converted to a JSON value.
outside the range of `number_float_t` ([`out_of_range.406`](../../home/exceptions.md#jsonexceptionout_of_range406)),
an optimized array of `Z`, `T`, or `F` with more than 1,048,576 elements, and a count that exceeds the capacity of the
container ([`out_of_range.408`](../../home/exceptions.md#jsonexceptionout_of_range408)). High-precision numbers that
do not fit an integer type are stored as floating-point numbers and may lose precision. Ill-formed input is rejected
with a parse error.
!!! info "Duplicate object keys"
If an object contains the same key more than once, the last value is kept (the UBJSON specification does not
address duplicate keys).
!!! info "Type marker of optimized containers"
The type marker after `$` must be one of `Z`, `N`, `T`, `F`, `i`, `U`, `I`, `l`, `L`, `d`, `D`, `H`, `C`, `S`, `[`, or
`{`. It is checked even if the count is 0; any other marker raises
[`parse_error.112`](../../home/exceptions.md#jsonexceptionparse_error112). A counted object typed `N` (`{$N#i1 ...`)
is not supported.
!!! warning "Ill-formed UTF-8 in string values and object keys" !!! warning "Ill-formed UTF-8 in string values and object keys"
+3 -18
View File
@@ -227,10 +227,7 @@ array of integers.
### BSON ### BSON
[BSON](binary_formats/bson.md) supports binary values and subtypes. If a subtype is given, it is used and added as an [BSON](binary_formats/bson.md) supports binary values and subtypes. If a subtype is given, it is used and added as an
unsigned 8-bit integer; a subtype above 255 throws unsigned 8-bit integer. If no subtype is given, the generic binary subtype 0x00 is used.
[`out_of_range.415`](../home/exceptions.md#jsonexceptionout_of_range415). If no subtype is given, the generic binary
subtype 0x00 is used. The payload of the "old binary" subtype 0x02 is preceded by a second length, which the library
writes and removes again when reading; it is not part of the payload.
??? example "Example: serialize a binary value to BSON" ??? example "Example: serialize a binary value to BSON"
@@ -304,8 +301,6 @@ representation using the length of the byte array.
[`json::from_cbor`](../api/basic_json/from_cbor.md) (see [`json::from_cbor`](../api/basic_json/from_cbor.md) (see
[`cbor_tag_handler_t`](../api/basic_json/cbor_tag_handler_t.md)). [`cbor_tag_handler_t`](../api/basic_json/cbor_tag_handler_t.md)).
With `ignore`, the subtype is dropped (`"subtype": null`); with `store`, the tag is kept (`"subtype": 42`):
```json ```json
{ {
"binary": { "binary": {
@@ -315,21 +310,11 @@ representation using the length of the byte array.
} }
``` ```
```json
{
"binary": {
"bytes": [202, 254, 186, 190],
"subtype": 42
}
}
```
### MessagePack ### MessagePack
[MessagePack](binary_formats/messagepack.md) supports binary values and subtypes. If a subtype is given, the ext family [MessagePack](binary_formats/messagepack.md) supports binary values and subtypes. If a subtype is given, the ext family
is used. The library will choose the smallest representation among fixext1, fixext2, fixext4, fixext8, fixext16, ext8, is used. The library will choose the smallest representation among fixext1, fixext2, fixext4, fixext8, ext8, ext16, and
ext16, and ext32. The subtype is then written as the signed 8-bit ext type: subtypes 128 to 255 map to the negative ext32. The subtype is then added as a signed 8-bit integer.
(reserved) ext types, and on reading the ext type is exposed as an unsigned subtype from 0 to 255.
If no subtype is given, the bin family (bin8, bin16, bin32) is used. If no subtype is given, the bin family (bin8, bin16, bin32) is used.
@@ -368,7 +368,8 @@ using array_t = ArrayType<basic_json, AllocatorType<basic_json>>;
`#!cpp std::from_chars` is not available or declines the token, and `std::strtold` reads up to the null character. A type whose `data()` `#!cpp std::from_chars` is not available or declines the token, and `std::strtold` reads up to the null character. A type whose `data()`
is not null-terminated does not fail to compile; with such a `NumberFloatType` it can silently misparse is not null-terminated does not fail to compile; with such a `NumberFloatType` it can silently misparse
floating-point numbers. floating-point numbers.
- `append(const char*, size_type)`, used by [`dump`](../../api/basic_json/dump.md) and by the binary readers. The library's internal string concatenation additionally has - `append(const char*, size_type)`, used by [`dump`](../../api/basic_json/dump.md), and `append(const StringType&)`,
used by the CBOR reader for indefinite-length strings. The library's internal string concatenation additionally has
to append a `#!cpp char` and a `#!cpp const char*`; for each it selects between `append(arg)`, `#!cpp operator+=`, to append a `#!cpp char` and a `#!cpp const char*`; for each it selects between `append(arg)`, `#!cpp operator+=`,
`append(first, last)`, and `append(data, size)`. `append(first, last)`, and `append(data, size)`.
- The comparison operator `==` against another `StringType`, and `<` for use as a key of the chosen - The comparison operator `==` against another `StringType`, and `<` for use as a key of the chosen
@@ -677,7 +678,8 @@ using binary_t = nlohmann::byte_container_with_subtype<BinaryType>;
[`StringType`](#stringtype), whose non-`#!cpp const` `data()` is C++17 and later only. [`StringType`](#stringtype), whose non-`#!cpp const` `data()` is C++17 and later only.
- Default-constructible, copy-constructible, and move-constructible. - Default-constructible, copy-constructible, and move-constructible.
- Member functions `size()`, `empty()`, `data()`, `resize()`, `operator[]`, `back()`, `begin()`, `end()`, `cbegin()`, - Member functions `size()`, `empty()`, `data()`, `resize()`, `operator[]`, `back()`, `begin()`, `end()`, `cbegin()`,
and `cend()` with random-access iterators. `push_back()` is **not** required. and `cend()` with random-access iterators, and `insert(pos, first, last)`, which the CBOR reader uses to join the
chunks of an indefinite-length byte string. `push_back()` is **not** required.
- Comparison operators: `==` is used by - Comparison operators: `==` is used by
[`byte_container_with_subtype`](../../api/byte_container_with_subtype/index.md), the relational operators by [`byte_container_with_subtype`](../../api/byte_container_with_subtype/index.md), the relational operators by
[`basic_json`'s comparison operators](../../api/basic_json/operator_le.md). [`basic_json`'s comparison operators](../../api/basic_json/operator_le.md).
+6 -11
View File
@@ -330,7 +330,7 @@ where they cannot be resolved.
### json.exception.parse_error.110 ### json.exception.parse_error.110
When parsing a [binary format](../features/binary_formats/index.md), the byte vector ends before the complete value has When parsing a [binary format](../features/binary_formats/index.md), the byte vector ends before the complete value has
been read, or (with `strict = true`) further bytes follow the value. been read.
!!! failure "Example messages" !!! failure "Example messages"
@@ -1045,8 +1045,8 @@ or the encoded length for [CBOR](../features/binary_formats/cbor.md).
The exception is also thrown for a [UBJSON](../features/binary_formats/ubjson.md) array of a type that is encoded by its The exception is also thrown for a [UBJSON](../features/binary_formats/ubjson.md) array of a type that is encoded by its
marker alone (`Z`, `T` or `F`) whose declared count exceeds 1,048,576 (`1 << 20`). Such an array has no payload, so its marker alone (`Z`, `T` or `F`) whose declared count exceeds 1,048,576 (`1 << 20`). Such an array has no payload, so its
count alone decides how much memory is allocated, and a handful of bytes would otherwise describe billions of values. count alone decides how much memory is allocated, and a handful of bytes would otherwise describe billions of values.
[`to_ubjson`](../api/basic_json/to_ubjson.md) writes longer arrays of these types without the `$` type annotation (the `#` [`to_ubjson`](../api/basic_json/to_ubjson.md) writes longer arrays of these types without the size and type annotation,
count and one marker per element are kept), so any value it produces can still be read back. so any value it produces can still be read back.
!!! failure "Example messages" !!! failure "Example messages"
@@ -1153,18 +1153,14 @@ A JSON Patch `move` operation's `"from"` location is a proper prefix of its `"pa
### json.exception.out_of_range.415 ### json.exception.out_of_range.415
MessagePack's ext type and BSON's binary subtype are each stored in a single byte. This exception is thrown when serializing a MessagePack's ext type and BSON's binary subtype are each stored in a single byte. This exception is thrown when serializing a
[`byte_container_with_subtype`](../api/byte_container_with_subtype/index.md) whose subtype exceeds 255, the largest value the library can store in that byte. [`byte_container_with_subtype`](../api/byte_container_with_subtype/index.md) whose subtype exceeds 255.
!!! failure "Example messages" !!! failure "Example message"
``` ```
[json.exception.out_of_range.415] subtype 70000 is too large for the MessagePack ext type (max 255) [json.exception.out_of_range.415] subtype 70000 is too large for the MessagePack ext type (max 255)
``` ```
```
[json.exception.out_of_range.415] subtype 70000 is too large for the BSON binary subtype (max 255)
```
!!! note !!! note
This exception was added in version 3.13.0. Before that, subtypes above 255 were silently truncated modulo 256 instead of raising an error. This exception was added in version 3.13.0. Before that, subtypes above 255 were silently truncated modulo 256 instead of raising an error.
@@ -1233,8 +1229,7 @@ A JSON Patch operation 'test' failed. The unsuccessful operation is also printed
### json.exception.other_error.502 ### json.exception.other_error.502
[`to_ubjson`](../api/basic_json/to_ubjson.md) and [`to_bjdata`](../api/basic_json/to_bjdata.md) were called with [`to_ubjson`](../api/basic_json/to_ubjson.md) and [`to_bjdata`](../api/basic_json/to_bjdata.md) were called with
`use_type = true` but `use_size = false` for a value that contains a non-empty array, object, or binary value. UBJSON `use_type = true` but `use_size = false`. UBJSON requires a size marker (`#`) after a type marker (`$`).
requires a size marker (`#`) after a type marker (`$`); scalars and empty containers do not throw.
!!! failure "Example message" !!! failure "Example message"
+11 -192
View File
@@ -235,9 +235,6 @@ class binary_reader
/// UBJSON/BJData: the type marker of an optimized container, so that /// UBJSON/BJData: the type marker of an optimized container, so that
/// its elements are read without one of their own; 0 otherwise /// its elements are read without one of their own; 0 otherwise
char_int_type type_marker; char_int_type type_marker;
/// BSON: the position right after this document, according to the
/// size it declares
std::size_t end_position = 0;
/// BSON: the size this document declares, in bytes /// BSON: the size this document declares, in bytes
std::int32_t declared_size = 0; std::int32_t declared_size = 0;
/// whether to close this container with end_object() or end_array() /// whether to close this container with end_object() or end_array()
@@ -374,23 +371,6 @@ class binary_reader
return false; return false;
} }
// the smallest document is the size prefix and the terminator
if (JSON_HEDLEY_UNLIKELY(document_size < 5))
{
return bson_document_size_error("document size ", document_size, " is less than 5");
}
// the declared size bounds where the document ends: an embedded
// document must end before the terminator of the document that contains it
const auto unsigned_size = static_cast<std::size_t>(document_size);
const std::size_t document_end = unsigned_size > (std::numeric_limits<std::size_t>::max)() - document_start
? (std::numeric_limits<std::size_t>::max)()
: document_start + unsigned_size;
if (JSON_HEDLEY_UNLIKELY(!container_stack.empty() && document_end >= container_stack.back().end_position))
{
return bson_document_size_error("document size ", document_size, " exceeds the size of the enclosing document");
}
if (JSON_HEDLEY_UNLIKELY(!enter_container(is_object, detail::unknown_size()))) if (JSON_HEDLEY_UNLIKELY(!enter_container(is_object, detail::unknown_size())))
{ {
return false; return false;
@@ -398,7 +378,6 @@ class binary_reader
container_frame& frame = container_stack.back(); container_frame& frame = container_stack.back();
frame.start_position = document_start; frame.start_position = document_start;
frame.end_position = document_end;
frame.declared_size = document_size; frame.declared_size = document_size;
return true; return true;
} }
@@ -457,13 +436,6 @@ class binary_reader
return false; return false;
} }
// an element needs at least its name's terminator, and the
// document its own terminator, within the declared size
if (JSON_HEDLEY_UNLIKELY(chars_read + 2 > container_stack.back().end_position))
{
return bson_document_size_error("document size ", container_stack.back().declared_size, " is too small for its content");
}
const std::size_t element_type_parse_position = chars_read; const std::size_t element_type_parse_position = chars_read;
key.clear(); key.clear();
if (JSON_HEDLEY_UNLIKELY(!get_bson_cstr(key))) if (JSON_HEDLEY_UNLIKELY(!get_bson_cstr(key)))
@@ -612,46 +584,9 @@ class binary_reader
} }
result.set_subtype(subtype); result.set_subtype(subtype);
// the "old binary" subtype 0x02 starts with an int32 that repeats the
// size of the payload following it; the payload does not include it
if (subtype == 0x02)
{
return get_bson_old_binary(len, result);
}
return get_binary(len, result); return get_binary(len, result);
} }
/*!
@brief Parses the payload of a BSON binary value with subtype 0x02.
@param[in] len The length of the binary value, including the inner int32.
@param[in,out] result A reference to the binary variable where the payload
(without the inner int32) is to be stored.
@pre len >= 0
@return `true` if the inner length was present and matches @a len
*/
template<typename NumberType>
bool get_bson_old_binary(const NumberType len, binary_t& result)
{
if (JSON_HEDLEY_UNLIKELY(len < 4))
{
return bson_length_error("length of binary subtype 0x02 must be at least 4, is ", len, "binary");
}
std::int32_t inner_length{};
if (JSON_HEDLEY_UNLIKELY((!get_number<std::int32_t, true>(inner_length))))
{
return false;
}
if (JSON_HEDLEY_UNLIKELY(static_cast<std::int64_t>(inner_length) != static_cast<std::int64_t>(len) - 4))
{
return bson_length_error("inner length of binary subtype 0x02 does not match the payload size; inner length is ", inner_length, "binary");
}
return get_binary(inner_length, result);
}
/*! /*!
@brief Read a BSON document element of the given @a element_type. @brief Read a BSON document element of the given @a element_type.
@param[in] element_type The BSON element type, c.f. http://bsonspec.org/spec.html @param[in] element_type The BSON element type, c.f. http://bsonspec.org/spec.html
@@ -1283,10 +1218,6 @@ class binary_reader
// level as required by RFC 8949, Section 3.2.3 // level as required by RFC 8949, Section 3.2.3
bool indefinite = false; bool indefinite = false;
// the chunk currently being validated; only used for the chunks of an
// indefinite-length string when the UTF-8 check can do something
string_t chunk;
while (true) while (true)
{ {
if (JSON_HEDLEY_UNLIKELY(!unexpect_eof("string"))) if (JSON_HEDLEY_UNLIKELY(!unexpect_eof("string")))
@@ -1306,82 +1237,26 @@ class binary_reader
} }
// a break marker closes the indefinite-length string; outside // a break marker closes the indefinite-length string; outside
// of one it falls through to the error below. Every chunk has // of one it falls through to the error below
// been checked on its own, so there is nothing left to check.
if (indefinite && current == 0xFF) if (indefinite && current == 0xFF)
{ {
return true; return check_string_utf8(result, context);
}
if (JSON_HEDLEY_UNLIKELY(!get_cbor_string_chunk(result, indefinite)))
{
return false;
} }
if (!indefinite) if (!indefinite)
{ {
return get_cbor_string_chunk(result, false) && check_string_utf8(result, context); return check_string_utf8(result, context);
}
if (error_handler == error_handler_t::keep)
{
// nothing to check: read the chunk directly into the result
if (JSON_HEDLEY_UNLIKELY(!get_cbor_string_chunk(result, true)))
{
return false;
}
}
else
{
// RFC 8949, Section 3.2.3: each chunk is a text string of its
// own and must be valid UTF-8 by itself, i.e., a code point
// cannot be spread over two chunks
chunk.clear();
if (JSON_HEDLEY_UNLIKELY(!get_cbor_string_chunk(chunk, true) || !check_string_utf8(chunk, context)))
{
return false;
}
result.append(chunk.data(), chunk.size());
} }
get(); get();
} }
} }
/*!
@brief skips the tags in front of a CBOR object key
A tag may wrap any data item, so it may also wrap a key. Like in front of a
value, the tags are skipped for the handlers ignore and store; with the
handler error they are left to @ref get_cbor_object_key, which rejects the
key. The tag loop is iterative, so the number of tags is not limited by the
recursion depth.
@param[in] tag_handler how CBOR tags should be treated
@return whether the tags could be skipped (false on EOF in a tag argument)
@pre @a current is the first byte of the key
*/
bool skip_cbor_key_tags(const cbor_tag_handler_t tag_handler)
{
if (tag_handler == cbor_tag_handler_t::error)
{
return true;
}
// 0xC0..0xDB: tag with the tag number in the head (0xC0..0xD7) or in
// the 1, 2, 4, or 8 bytes that follow (0xD8..0xDB)
while (current >= 0xC0 && current <= 0xDB)
{
if (current >= 0xD8)
{
std::uint64_t tag_to_ignore{};
if (JSON_HEDLEY_UNLIKELY(!get_cbor_argument(tag_to_ignore)))
{
return false;
}
}
get();
}
return true;
}
/*! /*!
@brief reads a CBOR object key @brief reads a CBOR object key
@@ -1748,10 +1623,6 @@ class binary_reader
if (top.is_object) if (top.is_object)
{ {
if (JSON_HEDLEY_UNLIKELY(!skip_cbor_key_tags(tag_handler)))
{
return false;
}
if (JSON_HEDLEY_UNLIKELY(!read_object_key<&binary_reader::get_cbor_object_key>(key))) if (JSON_HEDLEY_UNLIKELY(!read_object_key<&binary_reader::get_cbor_object_key>(key)))
{ {
return false; return false;
@@ -3065,14 +2936,6 @@ class binary_reader
return false; return false;
} }
// validate the type marker up front: otherwise it would only be
// checked when an element is read, and a container with count 0
// would accept any marker
if (JSON_HEDLEY_UNLIKELY(!is_valid_optimized_type(result.second)))
{
return optimized_type_error();
}
get_ignore_noop(); get_ignore_noop();
if (JSON_HEDLEY_UNLIKELY(current != '#')) if (JSON_HEDLEY_UNLIKELY(current != '#'))
{ {
@@ -4267,9 +4130,9 @@ class binary_reader
// sequence), UBJSON, BJData, or BSON requires a decoder to reject // sequence), UBJSON, BJData, or BSON requires a decoder to reject
// ill-formed UTF-8. Checking (and, with @ref error_handler_t::strict, // ill-formed UTF-8. Checking (and, with @ref error_handler_t::strict,
// rejecting, or with `replace`/`ignore`, sanitizing) is opt-in via // rejecting, or with `replace`/`ignore`, sanitizing) is opt-in via
// @ref error_handler, applied by @ref check_string_utf8 at the call // @ref error_handler, applied once the whole string (all chunks of
// site to the whole string or, for an indefinite-length CBOR string, // an indefinite-length CBOR string included) has been assembled, by
// to each chunk on its own (RFC 8949, Section 3.2.3). // @ref check_string_utf8 at the call site.
return get_bytes(len, "string", result); return get_bytes(len, "string", result);
} }
@@ -4464,12 +4327,6 @@ class binary_reader
return last_byte_error(exception_id::unexpected_byte, concat("document size ", std::to_string(document_size), " does not match the number of bytes read (", std::to_string(bytes_read), ")"), "document"); return last_byte_error(exception_id::unexpected_byte, concat("document size ", std::to_string(document_size), " does not match the number of bytes read (", std::to_string(bytes_read), ")"), "document");
} }
JSON_HEDLEY_NEVER_INLINE
bool bson_document_size_error(const char* prefix, const std::int32_t document_size, const char* suffix) const
{
return last_byte_error(exception_id::unexpected_byte, concat(prefix, std::to_string(document_size), suffix), "document");
}
JSON_HEDLEY_NEVER_INLINE JSON_HEDLEY_NEVER_INLINE
bool bson_unsupported_type_error(const std::size_t position, const char_int_type element_type) const bool bson_unsupported_type_error(const std::size_t position, const char_int_type element_type) const
{ {
@@ -4575,44 +4432,6 @@ class binary_reader
std::size_t bon8_pushback_size = 0; std::size_t bon8_pushback_size = 0;
JSON_PRIVATE_UNLESS_TESTED: JSON_PRIVATE_UNLESS_TESTED:
/*!
@brief check whether @a marker is a type marker that may follow '$'
UBJSON: Z N T F i U I l L d D H C S [ {; BJData: the numeric markers and C
(BJData's excluded markers are rejected separately).
*/
bool is_valid_optimized_type(const char_int_type marker) const noexcept
{
switch (marker)
{
case 'i':
case 'U':
case 'I':
case 'l':
case 'L':
case 'd':
case 'D':
case 'C':
return true;
case 'Z':
case 'N':
case 'T':
case 'F':
case 'H':
case 'S':
case '[':
case '{':
return input_format != input_format_t::bjdata;
case 'B':
case 'u':
case 'm':
case 'M':
case 'h':
return input_format == input_format_t::bjdata;
default:
return false;
}
}
/*! /*!
@brief whether @a marker is excluded from BJData's optimized ND-array types @brief whether @a marker is excluded from BJData's optimized ND-array types
@return whether @a marker is one of 'F', 'H', 'N', 'S', 'T', 'Z', '[', '{' @return whether @a marker is one of 'F', 'H', 'N', 'S', 'T', 'Z', '[', '{'
@@ -1684,15 +1684,7 @@ class binary_writer
throw_subtype_out_of_range(value.subtype(), "the BSON binary subtype", j); throw_subtype_out_of_range(value.subtype(), "the BSON binary subtype", j);
} }
// the "old binary" subtype 0x02 repeats the payload size in an inner return sizeof(std::int32_t) + value.size() + 1ul;
// int32 in front of the payload
return sizeof(std::int32_t) + (is_bson_old_binary(value) ? sizeof(std::int32_t) : 0ul) + value.size() + 1ul;
}
/// @return whether @a value has the BSON subtype 0x02 ("binary (old)")
static bool is_bson_old_binary(const binary_t& value) noexcept
{
return value.has_subtype() && value.subtype() == 0x02;
} }
/*! /*!
@@ -1705,18 +1697,10 @@ class binary_writer
{ {
write_bson_entry_header(name, 0x05); write_bson_entry_header(name, 0x05);
const bool old_binary = is_bson_old_binary(value); write_number<std::int32_t>(to_bson_length(value.size()), true);
// the length field counts the inner int32 of the old binary subtype
write_number<std::int32_t>(to_bson_length(value.size() + (old_binary ? sizeof(std::int32_t) : 0ul)), true);
write_number(value.has_subtype() ? static_cast<std::uint8_t>(value.subtype()) : static_cast<std::uint8_t>(0x00)); write_number(value.has_subtype() ? static_cast<std::uint8_t>(value.subtype()) : static_cast<std::uint8_t>(0x00));
if (old_binary)
{
write_number<std::int32_t>(to_bson_length(value.size()), true);
}
write_bytes(value.data(), value.size()); write_bytes(value.data(), value.size());
} }
@@ -1876,8 +1860,7 @@ class binary_writer
(or the header of a string or binary value) in one reservation (or the header of a string or binary value) in one reservation
@return whether the entry was written; false for a discarded value, a @return whether the entry was written; false for a discarded value, a
string or binary value whose length does not fit the length string or binary value whose length does not fit the length
field, or a binary value with subtype 0x02, in which case nothing field, in which case nothing was written
was written
@pre @a name is at most bson_scalar_max_name_size characters long and is @pre @a name is at most bson_scalar_max_name_size characters long and is
not changed by the error handler not changed by the error handler
*/ */
@@ -1938,9 +1921,7 @@ class binary_writer
if (j.is_binary()) if (j.is_binary())
{ {
const binary_t& value = *j.m_data.m_value.binary; const binary_t& value = *j.m_data.m_value.binary;
// the old binary subtype 0x02 has an inner length that the single if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::int32_t>(value.size())))
// reservation has no room for; write_bson_binary writes it
if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::int32_t>(value.size()) || is_bson_old_binary(value)))
{ {
return false; return false;
} }
+15 -215
View File
@@ -17702,9 +17702,6 @@ class binary_reader
/// UBJSON/BJData: the type marker of an optimized container, so that /// UBJSON/BJData: the type marker of an optimized container, so that
/// its elements are read without one of their own; 0 otherwise /// its elements are read without one of their own; 0 otherwise
char_int_type type_marker; char_int_type type_marker;
/// BSON: the position right after this document, according to the
/// size it declares
std::size_t end_position = 0;
/// BSON: the size this document declares, in bytes /// BSON: the size this document declares, in bytes
std::int32_t declared_size = 0; std::int32_t declared_size = 0;
/// whether to close this container with end_object() or end_array() /// whether to close this container with end_object() or end_array()
@@ -17841,23 +17838,6 @@ class binary_reader
return false; return false;
} }
// the smallest document is the size prefix and the terminator
if (JSON_HEDLEY_UNLIKELY(document_size < 5))
{
return bson_document_size_error("document size ", document_size, " is less than 5");
}
// the declared size bounds where the document ends: an embedded
// document must end before the terminator of the document that contains it
const auto unsigned_size = static_cast<std::size_t>(document_size);
const std::size_t document_end = unsigned_size > (std::numeric_limits<std::size_t>::max)() - document_start
? (std::numeric_limits<std::size_t>::max)()
: document_start + unsigned_size;
if (JSON_HEDLEY_UNLIKELY(!container_stack.empty() && document_end >= container_stack.back().end_position))
{
return bson_document_size_error("document size ", document_size, " exceeds the size of the enclosing document");
}
if (JSON_HEDLEY_UNLIKELY(!enter_container(is_object, detail::unknown_size()))) if (JSON_HEDLEY_UNLIKELY(!enter_container(is_object, detail::unknown_size())))
{ {
return false; return false;
@@ -17865,7 +17845,6 @@ class binary_reader
container_frame& frame = container_stack.back(); container_frame& frame = container_stack.back();
frame.start_position = document_start; frame.start_position = document_start;
frame.end_position = document_end;
frame.declared_size = document_size; frame.declared_size = document_size;
return true; return true;
} }
@@ -17924,13 +17903,6 @@ class binary_reader
return false; return false;
} }
// an element needs at least its name's terminator, and the
// document its own terminator, within the declared size
if (JSON_HEDLEY_UNLIKELY(chars_read + 2 > container_stack.back().end_position))
{
return bson_document_size_error("document size ", container_stack.back().declared_size, " is too small for its content");
}
const std::size_t element_type_parse_position = chars_read; const std::size_t element_type_parse_position = chars_read;
key.clear(); key.clear();
if (JSON_HEDLEY_UNLIKELY(!get_bson_cstr(key))) if (JSON_HEDLEY_UNLIKELY(!get_bson_cstr(key)))
@@ -18079,46 +18051,9 @@ class binary_reader
} }
result.set_subtype(subtype); result.set_subtype(subtype);
// the "old binary" subtype 0x02 starts with an int32 that repeats the
// size of the payload following it; the payload does not include it
if (subtype == 0x02)
{
return get_bson_old_binary(len, result);
}
return get_binary(len, result); return get_binary(len, result);
} }
/*!
@brief Parses the payload of a BSON binary value with subtype 0x02.
@param[in] len The length of the binary value, including the inner int32.
@param[in,out] result A reference to the binary variable where the payload
(without the inner int32) is to be stored.
@pre len >= 0
@return `true` if the inner length was present and matches @a len
*/
template<typename NumberType>
bool get_bson_old_binary(const NumberType len, binary_t& result)
{
if (JSON_HEDLEY_UNLIKELY(len < 4))
{
return bson_length_error("length of binary subtype 0x02 must be at least 4, is ", len, "binary");
}
std::int32_t inner_length{};
if (JSON_HEDLEY_UNLIKELY((!get_number<std::int32_t, true>(inner_length))))
{
return false;
}
if (JSON_HEDLEY_UNLIKELY(static_cast<std::int64_t>(inner_length) != static_cast<std::int64_t>(len) - 4))
{
return bson_length_error("inner length of binary subtype 0x02 does not match the payload size; inner length is ", inner_length, "binary");
}
return get_binary(inner_length, result);
}
/*! /*!
@brief Read a BSON document element of the given @a element_type. @brief Read a BSON document element of the given @a element_type.
@param[in] element_type The BSON element type, c.f. http://bsonspec.org/spec.html @param[in] element_type The BSON element type, c.f. http://bsonspec.org/spec.html
@@ -18750,10 +18685,6 @@ class binary_reader
// level as required by RFC 8949, Section 3.2.3 // level as required by RFC 8949, Section 3.2.3
bool indefinite = false; bool indefinite = false;
// the chunk currently being validated; only used for the chunks of an
// indefinite-length string when the UTF-8 check can do something
string_t chunk;
while (true) while (true)
{ {
if (JSON_HEDLEY_UNLIKELY(!unexpect_eof("string"))) if (JSON_HEDLEY_UNLIKELY(!unexpect_eof("string")))
@@ -18773,82 +18704,26 @@ class binary_reader
} }
// a break marker closes the indefinite-length string; outside // a break marker closes the indefinite-length string; outside
// of one it falls through to the error below. Every chunk has // of one it falls through to the error below
// been checked on its own, so there is nothing left to check.
if (indefinite && current == 0xFF) if (indefinite && current == 0xFF)
{ {
return true; return check_string_utf8(result, context);
}
if (JSON_HEDLEY_UNLIKELY(!get_cbor_string_chunk(result, indefinite)))
{
return false;
} }
if (!indefinite) if (!indefinite)
{ {
return get_cbor_string_chunk(result, false) && check_string_utf8(result, context); return check_string_utf8(result, context);
}
if (error_handler == error_handler_t::keep)
{
// nothing to check: read the chunk directly into the result
if (JSON_HEDLEY_UNLIKELY(!get_cbor_string_chunk(result, true)))
{
return false;
}
}
else
{
// RFC 8949, Section 3.2.3: each chunk is a text string of its
// own and must be valid UTF-8 by itself, i.e., a code point
// cannot be spread over two chunks
chunk.clear();
if (JSON_HEDLEY_UNLIKELY(!get_cbor_string_chunk(chunk, true) || !check_string_utf8(chunk, context)))
{
return false;
}
result.append(chunk.data(), chunk.size());
} }
get(); get();
} }
} }
/*!
@brief skips the tags in front of a CBOR object key
A tag may wrap any data item, so it may also wrap a key. Like in front of a
value, the tags are skipped for the handlers ignore and store; with the
handler error they are left to @ref get_cbor_object_key, which rejects the
key. The tag loop is iterative, so the number of tags is not limited by the
recursion depth.
@param[in] tag_handler how CBOR tags should be treated
@return whether the tags could be skipped (false on EOF in a tag argument)
@pre @a current is the first byte of the key
*/
bool skip_cbor_key_tags(const cbor_tag_handler_t tag_handler)
{
if (tag_handler == cbor_tag_handler_t::error)
{
return true;
}
// 0xC0..0xDB: tag with the tag number in the head (0xC0..0xD7) or in
// the 1, 2, 4, or 8 bytes that follow (0xD8..0xDB)
while (current >= 0xC0 && current <= 0xDB)
{
if (current >= 0xD8)
{
std::uint64_t tag_to_ignore{};
if (JSON_HEDLEY_UNLIKELY(!get_cbor_argument(tag_to_ignore)))
{
return false;
}
}
get();
}
return true;
}
/*! /*!
@brief reads a CBOR object key @brief reads a CBOR object key
@@ -19215,10 +19090,6 @@ class binary_reader
if (top.is_object) if (top.is_object)
{ {
if (JSON_HEDLEY_UNLIKELY(!skip_cbor_key_tags(tag_handler)))
{
return false;
}
if (JSON_HEDLEY_UNLIKELY(!read_object_key<&binary_reader::get_cbor_object_key>(key))) if (JSON_HEDLEY_UNLIKELY(!read_object_key<&binary_reader::get_cbor_object_key>(key)))
{ {
return false; return false;
@@ -20532,14 +20403,6 @@ class binary_reader
return false; return false;
} }
// validate the type marker up front: otherwise it would only be
// checked when an element is read, and a container with count 0
// would accept any marker
if (JSON_HEDLEY_UNLIKELY(!is_valid_optimized_type(result.second)))
{
return optimized_type_error();
}
get_ignore_noop(); get_ignore_noop();
if (JSON_HEDLEY_UNLIKELY(current != '#')) if (JSON_HEDLEY_UNLIKELY(current != '#'))
{ {
@@ -21734,9 +21597,9 @@ class binary_reader
// sequence), UBJSON, BJData, or BSON requires a decoder to reject // sequence), UBJSON, BJData, or BSON requires a decoder to reject
// ill-formed UTF-8. Checking (and, with @ref error_handler_t::strict, // ill-formed UTF-8. Checking (and, with @ref error_handler_t::strict,
// rejecting, or with `replace`/`ignore`, sanitizing) is opt-in via // rejecting, or with `replace`/`ignore`, sanitizing) is opt-in via
// @ref error_handler, applied by @ref check_string_utf8 at the call // @ref error_handler, applied once the whole string (all chunks of
// site to the whole string or, for an indefinite-length CBOR string, // an indefinite-length CBOR string included) has been assembled, by
// to each chunk on its own (RFC 8949, Section 3.2.3). // @ref check_string_utf8 at the call site.
return get_bytes(len, "string", result); return get_bytes(len, "string", result);
} }
@@ -21931,12 +21794,6 @@ class binary_reader
return last_byte_error(exception_id::unexpected_byte, concat("document size ", std::to_string(document_size), " does not match the number of bytes read (", std::to_string(bytes_read), ")"), "document"); return last_byte_error(exception_id::unexpected_byte, concat("document size ", std::to_string(document_size), " does not match the number of bytes read (", std::to_string(bytes_read), ")"), "document");
} }
JSON_HEDLEY_NEVER_INLINE
bool bson_document_size_error(const char* prefix, const std::int32_t document_size, const char* suffix) const
{
return last_byte_error(exception_id::unexpected_byte, concat(prefix, std::to_string(document_size), suffix), "document");
}
JSON_HEDLEY_NEVER_INLINE JSON_HEDLEY_NEVER_INLINE
bool bson_unsupported_type_error(const std::size_t position, const char_int_type element_type) const bool bson_unsupported_type_error(const std::size_t position, const char_int_type element_type) const
{ {
@@ -22042,44 +21899,6 @@ class binary_reader
std::size_t bon8_pushback_size = 0; std::size_t bon8_pushback_size = 0;
JSON_PRIVATE_UNLESS_TESTED: JSON_PRIVATE_UNLESS_TESTED:
/*!
@brief check whether @a marker is a type marker that may follow '$'
UBJSON: Z N T F i U I l L d D H C S [ {; BJData: the numeric markers and C
(BJData's excluded markers are rejected separately).
*/
bool is_valid_optimized_type(const char_int_type marker) const noexcept
{
switch (marker)
{
case 'i':
case 'U':
case 'I':
case 'l':
case 'L':
case 'd':
case 'D':
case 'C':
return true;
case 'Z':
case 'N':
case 'T':
case 'F':
case 'H':
case 'S':
case '[':
case '{':
return input_format != input_format_t::bjdata;
case 'B':
case 'u':
case 'm':
case 'M':
case 'h':
return input_format == input_format_t::bjdata;
default:
return false;
}
}
/*! /*!
@brief whether @a marker is excluded from BJData's optimized ND-array types @brief whether @a marker is excluded from BJData's optimized ND-array types
@return whether @a marker is one of 'F', 'H', 'N', 'S', 'T', 'Z', '[', '{' @return whether @a marker is one of 'F', 'H', 'N', 'S', 'T', 'Z', '[', '{'
@@ -28112,15 +27931,7 @@ class binary_writer
throw_subtype_out_of_range(value.subtype(), "the BSON binary subtype", j); throw_subtype_out_of_range(value.subtype(), "the BSON binary subtype", j);
} }
// the "old binary" subtype 0x02 repeats the payload size in an inner return sizeof(std::int32_t) + value.size() + 1ul;
// int32 in front of the payload
return sizeof(std::int32_t) + (is_bson_old_binary(value) ? sizeof(std::int32_t) : 0ul) + value.size() + 1ul;
}
/// @return whether @a value has the BSON subtype 0x02 ("binary (old)")
static bool is_bson_old_binary(const binary_t& value) noexcept
{
return value.has_subtype() && value.subtype() == 0x02;
} }
/*! /*!
@@ -28133,18 +27944,10 @@ class binary_writer
{ {
write_bson_entry_header(name, 0x05); write_bson_entry_header(name, 0x05);
const bool old_binary = is_bson_old_binary(value); write_number<std::int32_t>(to_bson_length(value.size()), true);
// the length field counts the inner int32 of the old binary subtype
write_number<std::int32_t>(to_bson_length(value.size() + (old_binary ? sizeof(std::int32_t) : 0ul)), true);
write_number(value.has_subtype() ? static_cast<std::uint8_t>(value.subtype()) : static_cast<std::uint8_t>(0x00)); write_number(value.has_subtype() ? static_cast<std::uint8_t>(value.subtype()) : static_cast<std::uint8_t>(0x00));
if (old_binary)
{
write_number<std::int32_t>(to_bson_length(value.size()), true);
}
write_bytes(value.data(), value.size()); write_bytes(value.data(), value.size());
} }
@@ -28304,8 +28107,7 @@ class binary_writer
(or the header of a string or binary value) in one reservation (or the header of a string or binary value) in one reservation
@return whether the entry was written; false for a discarded value, a @return whether the entry was written; false for a discarded value, a
string or binary value whose length does not fit the length string or binary value whose length does not fit the length
field, or a binary value with subtype 0x02, in which case nothing field, in which case nothing was written
was written
@pre @a name is at most bson_scalar_max_name_size characters long and is @pre @a name is at most bson_scalar_max_name_size characters long and is
not changed by the error handler not changed by the error handler
*/ */
@@ -28366,9 +28168,7 @@ class binary_writer
if (j.is_binary()) if (j.is_binary())
{ {
const binary_t& value = *j.m_data.m_value.binary; const binary_t& value = *j.m_data.m_value.binary;
// the old binary subtype 0x02 has an inner length that the single if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::int32_t>(value.size())))
// reservation has no room for; write_bson_binary writes it
if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::int32_t>(value.size()) || is_bson_old_binary(value)))
{ {
return false; return false;
} }
+27 -1
View File
@@ -21,6 +21,10 @@ python3 tests/benchmarks/json_view/compare.py --data <json_test_data directory>
- The other libraries come from the system: pkg-config, or Homebrew (`brew install yyjson simdjson boost`). With - The other libraries come from the system: pkg-config, or Homebrew (`brew install yyjson simdjson boost`). With
`--download`, pinned releases are downloaded instead and checked against their SHA-256. Without Boost headers (or `--download`, pinned releases are downloaded instead and checked against their SHA-256. Without Boost headers (or
with `--no-boost`), the Boost.JSON columns are skipped, and the results say so. with `--no-boost`), the Boost.JSON columns are skipped, and the results say so.
- `--aligned` additionally builds and runs everything with all functions aligned to 64 bytes (`-mllvm
-align-all-functions=6` for Clang, `-falign-functions=64 -falign-loops=64` for GCC), and reports both result
tables as "default build" and "aligned build". Code layout alone can move a result by a few percent; a difference
that appears in only one of the two tables is a layout artifact. Off by default, which doubles the run time.
- `--corpus file...` adds files to the corpus benchmark, e.g. those of - `--corpus file...` adds files to the corpus benchmark, e.g. those of
[simdjson-data](https://github.com/simdjson/simdjson-data) or the [simdjson-data](https://github.com/simdjson/simdjson-data) or the
[yyjson benchmark](https://github.com/ibireme/yyjson_benchmark). [yyjson benchmark](https://github.com/ibireme/yyjson_benchmark).
@@ -39,7 +43,7 @@ where `json_view` stands on another architecture; they are not meant for publica
## What is measured ## What is measured
`bench_view.cpp` runs four workloads on twitter, citm_catalog, canada, jeopardy, a single tweet (`status`), and a `bench_view.cpp` runs these workloads on twitter, citm_catalog, canada, jeopardy, a single tweet (`status`), and a
JSON-RPC request (`rpc`): JSON-RPC request (`rpc`):
| workload | what it does | | workload | what it does |
@@ -48,6 +52,7 @@ JSON-RPC request (`rpc`):
| traverse | parse, then visit every value, convert every number, touch every string and key | | traverse | parse, then visit every value, convert every number, touch every string and key |
| select | parse, then read a few fields per record (e.g. id, user name, and retweet count of each tweet) | | select | parse, then read a few fields per record (e.g. id, user name, and retweet count of each tweet) |
| dump | serialize a parsed document (compact) | | dump | serialize a parsed document (compact) |
| parse+dump | build a fresh document, then serialize it (compact), in one timed call |
`bench_corpus.cpp` runs parse, traverse, and dump on any list of files, so that no library is tuned to a handful of `bench_corpus.cpp` runs parse, traverse, and dump on any list of files, so that no library is tuned to a handful of
documents. Its dump also writes the numbers as they are in the input: `json_view` with `number_format::source`, and documents. Its dump also writes the numbers as they are in the input: `json_view` with `number_format::source`, and
@@ -66,11 +71,32 @@ call follows an untimed call of the same engine: otherwise the engine after `jso
cleaning up the tens of thousands of nodes `json::parse` just freed (with glibc, this made `json_view` look 1.7 times cleaning up the tens of thousands of nodes `json::parse` just freed (with glibc, this made `json_view` look 1.7 times
slower on citm_catalog traverse). slower on citm_catalog traverse).
`json_view` is an index into the source text, not a tree of owning nodes. Its like-for-like peers are yyjson and
simdjson DOM (immutable, indexed, no per-node allocation). Boost.JSON and `json::parse` build mutable, owning DOMs and
are reference points for what that costs, not equals; simdjson On-Demand is lazy and forward-only, so it is the other
way round: it has no random access to pay for. All engines validate UTF-8 and syntax by default.
Number conversion differs: `json_view` converts integers while parsing but converts floating-point numbers lazily,
when they are read or dumped; yyjson (default flags) and simdjson DOM convert every number while parsing. The cost of
float conversion therefore appears in the traverse and dump rows for `json_view`, and in the parse row for the others.
For float-heavy documents (canada), the fair reading is a sum of parse and use: the `traverse` workload already
includes the parse, and `parse+dump` measures a fresh document and its dump in one timed call; the `dump` row alone
leaves the conversion in `json_view` and out of the others. Eager conversion at parse time was prototyped as an opt-in
and measured: canada traverse (with its parse) was 4-7 % faster, but parse+dump 11-30 % slower and parse 36-90 % slower. It was
dropped, so lazy conversion is the only mode.
`json_view` can also be edited: `json_editable_document` has the same read API, and the `(editable)` rows in
`bench_view` measure it on the same workloads. They are the fair counterpart of Boost.JSON and `json::parse`, which are
mutable too; the plain `json_view` rows are the counterpart of yyjson and simdjson DOM, which are not. `bench_edit` adds
the read-modify-write workloads. Read the tables with that in mind: an immutable-only library is not compared with an
editable `json_view` unless it says so.
The engines do not all offer the same features, which the numbers should be read with: The engines do not all offer the same features, which the numbers should be read with:
| engine | document | random access | editable | notes | | engine | document | random access | editable | notes |
|---|---|---|---|---| |---|---|---|---|---|
| `json_view` | immutable index into the text | yes | no | a fresh document per parse; "reused" parses into the same document | | `json_view` | immutable index into the text | yes | no | a fresh document per parse; "reused" parses into the same document |
| `json_view` (editable) | index into the text plus edits | yes | yes, in place | `json_editable_document`; keeps member order and number spellings |
| yyjson | immutable (`yyjson_read`) | yes | via a mutable copy | | | yyjson | immutable (`yyjson_read`) | yes | via a mutable copy | |
| simdjson DOM | immutable, parser reused | yes | no | "fresh" uses a new parser per parse | | simdjson DOM | immutable, parser reused | yes | no | "fresh" uses a new parser per parse |
| simdjson On-Demand | none: forward-only, lazy | no | no | only traverse and select | | simdjson On-Demand | none: forward-only, lazy | no | no | only traverse and select |
+92 -24
View File
@@ -8,8 +8,9 @@
// Same-feature-set benchmark: read-only JSON documents with random access. // Same-feature-set benchmark: read-only JSON documents with random access.
// //
// json_view nlohmann/json_view.hpp (fresh document per parse / reused) // json_view nlohmann/json_view.hpp (fresh document per parse / reused;
// yyjson yyjson_read(): immutable document, random access // "(editable)": json_editable_document, same read API)
// yyjson yyjson_read(): immutable document, random access
// simdjson DOM dom::parser (reused, as recommended; "fresh": a new parser // simdjson DOM dom::parser (reused, as recommended; "fresh": a new parser
// per parse): immutable, random access // per parse): immutable, random access
// references (different feature sets): // references (different feature sets):
@@ -19,7 +20,11 @@
// //
// Workloads: parse (build + free), traverse (visit everything, convert every // Workloads: parse (build + free), traverse (visit everything, convert every
// number, touch every string and key), select (a few fields per document), // number, touch every string and key), select (a few fields per document),
// dump (compact serialization of the parsed document). // dump (compact serialization of the parsed document), and parse+dump (a fresh
// document, then its dump, in one timed call). json_view converts floating-point
// numbers lazily (when read or dumped), yyjson and simdjson DOM while parsing:
// for float-heavy documents, traverse (which includes the parse) and parse+dump
// are the fair reading of the parse and dump rows.
// All engines run interleaved in every round, each timed call after an untimed // All engines run interleaved in every round, each timed call after an untimed
// one of the same engine; the best round is reported. // one of the same engine; the best round is reported.
#include <nlohmann/json_view.hpp> #include <nlohmann/json_view.hpp>
@@ -43,6 +48,8 @@
using nlohmann::json; using nlohmann::json;
using nlohmann::json_document; using nlohmann::json_document;
using nlohmann::json_editable_document;
using nlohmann::json_editable_view;
using nlohmann::json_view; using nlohmann::json_view;
static volatile double g_sink; static volatile double g_sink;
@@ -55,7 +62,8 @@ struct stats
// ---------------- traversal ---------------- // ---------------- traversal ----------------
static void walk(json_view v, stats& st) template<class V>
static void walk_view(V v, stats& st)
{ {
++st.nodes; ++st.nodes;
switch (v.type()) switch (v.type())
@@ -64,32 +72,42 @@ static void walk(json_view v, stats& st)
for (auto it = v.begin(); it != v.end(); ++it) for (auto it = v.begin(); it != v.end(); ++it)
{ {
st.str += it.key().size(); st.str += it.key().size();
walk(*it, st); walk_view<V>(*it, st);
} }
break; break;
case json::value_t::array: case json::value_t::array:
for (const json_view e : v) for (const V e : v)
{ {
walk(e, st); walk_view<V>(e, st);
} }
break; break;
case json::value_t::string: case json::value_t::string:
st.str += v.get_string().size(); st.str += v.get_string().size();
break; break;
case json::value_t::number_integer: case json::value_t::number_integer:
st.num += static_cast<double>(v.get<std::int64_t>()); st.num += static_cast<double>(v.template get<std::int64_t>());
break; break;
case json::value_t::number_unsigned: case json::value_t::number_unsigned:
st.num += static_cast<double>(v.get<std::uint64_t>()); st.num += static_cast<double>(v.template get<std::uint64_t>());
break; break;
case json::value_t::number_float: case json::value_t::number_float:
st.num += v.get<double>(); st.num += v.template get<double>();
break; break;
default: default:
break; break;
} }
} }
static void walk(json_view v, stats& st)
{
walk_view<json_view>(v, st);
}
static void walk(json_editable_view v, stats& st)
{
walk_view<json_editable_view>(v, st);
}
static void walk(const json& j, stats& st) static void walk(const json& j, stats& st)
{ {
++st.nodes; ++st.nodes;
@@ -300,20 +318,21 @@ static void walk(const boost::json::value& v, stats& st)
// status (one tweet): id, user.screen_name, retweet_count // status (one tweet): id, user.screen_name, retweet_count
// rpc: method, params.minuend, id // rpc: method, params.minuend, id
static double pick(const std::string& name, json_view r) template<class V>
static double pick_view(const std::string& name, V r)
{ {
double acc = 0; double acc = 0;
if (name == "twitter" || name == "status") if (name == "twitter" || name == "status")
{ {
auto one = [&](json_view s) auto one = [&](V s)
{ {
acc += static_cast<double>(s["id"].get<std::uint64_t>()); acc += static_cast<double>(s["id"].template get<std::uint64_t>());
acc += static_cast<double>(s["user"]["screen_name"].get_string().size()); acc += static_cast<double>(s["user"]["screen_name"].get_string().size());
acc += static_cast<double>(s["retweet_count"].get<std::int64_t>()); acc += static_cast<double>(s["retweet_count"].template get<std::int64_t>());
}; };
if (name == "twitter") if (name == "twitter")
{ {
for (const json_view s : r["statuses"]) for (const V s : r["statuses"])
{ {
one(s); one(s);
} }
@@ -325,20 +344,20 @@ static double pick(const std::string& name, json_view r)
} }
else if (name == "citm_catalog") else if (name == "citm_catalog")
{ {
for (const json_view p : r["performances"]) for (const V p : r["performances"])
{ {
acc += static_cast<double>(p["id"].get<std::uint64_t>() + p["eventId"].get<std::uint64_t>() + p["seatCategories"].size()); acc += static_cast<double>(p["id"].template get<std::uint64_t>() + p["eventId"].template get<std::uint64_t>() + p["seatCategories"].size());
} }
acc += static_cast<double>(r["events"].size()); acc += static_cast<double>(r["events"].size());
} }
else if (name == "canada") else if (name == "canada")
{ {
const json_view g = r["features"][0]["geometry"]; const V g = r["features"][0]["geometry"];
acc += static_cast<double>(r["type"].get_string().size() + g["type"].get_string().size() + g["coordinates"].size()); acc += static_cast<double>(r["type"].get_string().size() + g["type"].get_string().size() + g["coordinates"].size());
} }
else if (name == "jeopardy") else if (name == "jeopardy")
{ {
for (const json_view q : r) for (const V q : r)
{ {
acc += q["round"].get_string() == "Final Jeopardy!" ? 1 : 0; acc += q["round"].get_string() == "Final Jeopardy!" ? 1 : 0;
acc += static_cast<double>(q["category"].get_string().size()); acc += static_cast<double>(q["category"].get_string().size());
@@ -347,11 +366,21 @@ static double pick(const std::string& name, json_view r)
else if (name == "rpc") else if (name == "rpc")
{ {
acc += static_cast<double>(r["method"].get_string().size()); acc += static_cast<double>(r["method"].get_string().size());
acc += static_cast<double>(r["params"]["minuend"].get<std::int64_t>() + r["id"].get<std::int64_t>()); acc += static_cast<double>(r["params"]["minuend"].template get<std::int64_t>() + r["id"].template get<std::int64_t>());
} }
return acc; return acc;
} }
static double pick(const std::string& name, json_view r)
{
return pick_view<json_view>(name, r);
}
static double pick(const std::string& name, json_editable_view r)
{
return pick_view<json_editable_view>(name, r);
}
static double pick(const std::string& name, const json& r) static double pick(const std::string& name, const json& r)
{ {
double acc = 0; double acc = 0;
@@ -634,15 +663,35 @@ int main(int argc, char** argv)
walk(json::parse(dc.text), a); walk(json::parse(dc.text), a);
auto d = json_document::parse(dc.text); auto d = json_document::parse(dc.text);
walk(d.root(), b); walk(d.root(), b);
auto ed = json_editable_document::parse(dc.text);
stats e;
walk(ed.root(), e);
yyjson_doc* y = yyjson_read(dc.text.data(), dc.text.size(), 0); yyjson_doc* y = yyjson_read(dc.text.data(), dc.text.size(), 0);
walk(yyjson_doc_get_root(y), c); walk(yyjson_doc_get_root(y), c);
simdjson::dom::parser p; simdjson::dom::parser p;
walk(p.parse(dc.text).value(), dd); walk(p.parse(dc.text).value(), dd);
const bool ok = a.nodes == b.nodes && a.nodes == c.nodes && a.nodes == dd.nodes && a.str == b.str && a.str == c.str && a.str == dd.str const bool ok = a.nodes == b.nodes && a.nodes == e.nodes && a.str == e.str && std::fabs(a.num - e.num) <= 1e-9 * std::fabs(a.num) && a.nodes == c.nodes && a.nodes == dd.nodes && a.str == b.str && a.str == c.str && a.str == dd.str
&& std::fabs(a.num - b.num) <= 1e-9 * std::fabs(a.num) && std::fabs(a.num - c.num) <= 1e-9 * std::fabs(a.num); && std::fabs(a.num - b.num) <= 1e-9 * std::fabs(a.num) && std::fabs(a.num - c.num) <= 1e-9 * std::fabs(a.num);
const double pa = pick(dc.name, json::parse(dc.text)), pb = pick(dc.name, d.root()), pc = pick(dc.name, yyjson_doc_get_root(y)), pd = pick(dc.name, p.parse(dc.text).value()); const double pa = pick(dc.name, json::parse(dc.text)), pb = pick(dc.name, d.root()), pe = pick(dc.name, ed.root()), pc = pick(dc.name, yyjson_doc_get_root(y)), pd = pick(dc.name, p.parse(dc.text).value());
std::printf("check %-13s traverse %s select %s\n", dc.name.c_str(), ok ? "OK" : "MISMATCH", (pa == pb && pa == pc && pa == pd) ? "OK" : "MISMATCH"); std::printf("check %-13s traverse %s select %s\n", dc.name.c_str(), ok ? "OK" : "MISMATCH", (pa == pb && pa == pe && pa == pc && pa == pd) ? "OK" : "MISMATCH");
yyjson_doc_free(y); yyjson_doc_free(y);
// the dumps of the engines may format floats differently, so only: every dump is non-empty, and the
// dump of json_view (also editable) and of json::parse parse back to the same number of nodes
const std::string dj = json::parse(dc.text).dump();
const std::string dv = d.root().dump();
const std::string de = ed.root().dump();
std::size_t yn = 0;
yyjson_doc* y2 = yyjson_read(dc.text.data(), dc.text.size(), 0);
char* yo = yyjson_write(y2, 0, &yn);
const bool dumps_ok = !dj.empty() && !dv.empty() && !de.empty() && yo != nullptr && yn > 0 && !simdjson::to_string(p.parse(dc.text).value()).empty();
std::free(yo);
yyjson_doc_free(y2);
stats sj_nodes, sv_nodes, se_nodes;
walk(json::parse(dj), sj_nodes);
walk(json::parse(dv), sv_nodes);
walk(json::parse(de), se_nodes);
std::printf("check %-13s dump %s\n", dc.name.c_str(), (dumps_ok && sj_nodes.nodes == a.nodes && sv_nodes.nodes == a.nodes && se_nodes.nodes == a.nodes) ? "OK" : "MISMATCH");
} }
std::FILE* csv = std::fopen("bench_view.csv", "w"); std::FILE* csv = std::fopen("bench_view.csv", "w");
@@ -669,6 +718,7 @@ int main(int argc, char** argv)
workloads.push_back({"parse", { workloads.push_back({"parse", {
{"json_view", [&] { auto d = json_document::parse(s); g_sink = static_cast<double>(d.node_count()); }}, {"json_view", [&] { auto d = json_document::parse(s); g_sink = static_cast<double>(d.node_count()); }},
{"json_view (reused)", [&] { reused.read(s); g_sink = static_cast<double>(reused.node_count()); }}, {"json_view (reused)", [&] { reused.read(s); g_sink = static_cast<double>(reused.node_count()); }},
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); g_sink = static_cast<double>(d.root().size()); }},
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = static_cast<double>(yyjson_doc_get_val_count(d)); yyjson_doc_free(d); }}, {"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = static_cast<double>(yyjson_doc_get_val_count(d)); yyjson_doc_free(d); }},
{"simdjson DOM", [&] { auto e = sj.parse(ps).value_unsafe(); g_sink = e.is_object(); }}, {"simdjson DOM", [&] { auto e = sj.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
{"simdjson DOM (fresh)", [&] { simdjson::dom::parser p; auto e = p.parse(ps).value_unsafe(); g_sink = e.is_object(); }}, {"simdjson DOM (fresh)", [&] { simdjson::dom::parser p; auto e = p.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
@@ -680,6 +730,7 @@ int main(int argc, char** argv)
workloads.push_back({"traverse", { workloads.push_back({"traverse", {
{"json_view", [&] { auto d = json_document::parse(s); stats st; walk(d.root(), st); g_sink = st.num; }}, {"json_view", [&] { auto d = json_document::parse(s); stats st; walk(d.root(), st); g_sink = st.num; }},
{"json_view (reused)", [&] { reused.read(s); stats st; walk(reused.root(), st); g_sink = st.num; }}, {"json_view (reused)", [&] { reused.read(s); stats st; walk(reused.root(), st); g_sink = st.num; }},
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); stats st; walk(d.root(), st); g_sink = st.num; }},
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); stats st; walk(yyjson_doc_get_root(d), st); g_sink = st.num; yyjson_doc_free(d); }}, {"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); stats st; walk(yyjson_doc_get_root(d), st); g_sink = st.num; yyjson_doc_free(d); }},
{"simdjson DOM", [&] { stats st; walk(sj.parse(ps).value_unsafe(), st); g_sink = st.num; }}, {"simdjson DOM", [&] { stats st; walk(sj.parse(ps).value_unsafe(), st); g_sink = st.num; }},
{"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); stats st; walk_od(d.get_value().value_unsafe(), st); g_sink = st.num; }}, {"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); stats st; walk_od(d.get_value().value_unsafe(), st); g_sink = st.num; }},
@@ -691,15 +742,31 @@ int main(int argc, char** argv)
workloads.push_back({"select", { workloads.push_back({"select", {
{"json_view", [&] { auto d = json_document::parse(s); g_sink = pick(name, d.root()); }}, {"json_view", [&] { auto d = json_document::parse(s); g_sink = pick(name, d.root()); }},
{"json_view (reused)", [&] { reused.read(s); g_sink = pick(name, reused.root()); }}, {"json_view (reused)", [&] { reused.read(s); g_sink = pick(name, reused.root()); }},
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); g_sink = pick(name, d.root()); }},
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = pick(name, yyjson_doc_get_root(d)); yyjson_doc_free(d); }}, {"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = pick(name, yyjson_doc_get_root(d)); yyjson_doc_free(d); }},
{"simdjson DOM", [&] { g_sink = pick(name, sj.parse(ps).value_unsafe()); }}, {"simdjson DOM", [&] { g_sink = pick(name, sj.parse(ps).value_unsafe()); }},
{"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); g_sink = pick_od(name, d); }}, {"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); g_sink = pick_od(name, d); }},
{"json::parse", [&] { json j = json::parse(s); g_sink = pick(name, j); }}, {"json::parse", [&] { json j = json::parse(s); g_sink = pick(name, j); }},
}}); }});
// pipeline: a fresh document, then a dump of it, in one timed call
// (simdjson DOM reuses its parser, as in the parse row); the traverse
// workload above is already parse+traverse
workloads.push_back({"parse+dump", {
{"json_view", [&] { auto d = json_document::parse(s); std::string o = d.root().dump(); g_sink = static_cast<double>(o.size()); }},
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); std::string o = d.root().dump(); g_sink = static_cast<double>(o.size()); }},
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); std::size_t n = 0; char* o = yyjson_write(d, 0, &n); g_sink = static_cast<double>(n); std::free(o); yyjson_doc_free(d); }},
{"simdjson DOM", [&] { std::string o = simdjson::to_string(sj.parse(ps).value_unsafe()); g_sink = static_cast<double>(o.size()); }},
#if JSON_VIEW_BENCH_BOOST
{"Boost.JSON", [&] { boost::json::monotonic_resource mr; auto v = boost::json::parse(s, &mr); std::string o = boost::json::serialize(v); g_sink = static_cast<double>(o.size()); }},
#endif
{"json::parse", [&] { json j = json::parse(s); std::string o = j.dump(); g_sink = static_cast<double>(o.size()); }},
}});
{ {
// serialization of an already parsed document // serialization of an already parsed document
static json_document vd; static json_document vd;
vd.read(s); vd.read(s);
static json_editable_document ed = json_editable_document::parse(s);
ed = json_editable_document::parse(s);
static yyjson_doc* yd = nullptr; static yyjson_doc* yd = nullptr;
if (yd) if (yd)
{ {
@@ -713,6 +780,7 @@ int main(int argc, char** argv)
jd = json::parse(s); jd = json::parse(s);
workloads.push_back({"dump", { workloads.push_back({"dump", {
{"json_view", [&] { std::string o = vd.root().dump(); g_sink = static_cast<double>(o.size()); }}, {"json_view", [&] { std::string o = vd.root().dump(); g_sink = static_cast<double>(o.size()); }},
{"json_view (editable)", [&] { std::string o = ed.root().dump(); g_sink = static_cast<double>(o.size()); }},
{"yyjson", [&] { std::size_t n = 0; char* o = yyjson_write(yd, 0, &n); g_sink = static_cast<double>(n); std::free(o); }}, {"yyjson", [&] { std::size_t n = 0; char* o = yyjson_write(yd, 0, &n); g_sink = static_cast<double>(n); std::free(o); }},
{"simdjson DOM", [&] { std::string o = simdjson::to_string(se); g_sink = static_cast<double>(o.size()); }}, {"simdjson DOM", [&] { std::string o = simdjson::to_string(se); g_sink = static_cast<double>(o.size()); }},
{"json::parse", [&] { std::string o = jd.dump(); g_sink = static_cast<double>(o.size()); }}, {"json::parse", [&] { std::string o = jd.dump(); g_sink = static_cast<double>(o.size()); }},
@@ -740,7 +808,7 @@ int main(int argc, char** argv)
} }
} }
const double ref = best[0]; const double ref = best[0];
std::printf("%-13s %-9s", dc.name.c_str(), wl.first.c_str()); std::printf("%-13s %-14s", dc.name.c_str(), wl.first.c_str());
for (std::size_t k = 0; k < wl.second.size(); ++k) for (std::size_t k = 0; k < wl.second.size(); ++k)
{ {
const double us = best[k] / 1e3; const double us = best[k] / 1e3;
+45 -23
View File
@@ -239,6 +239,8 @@ def main():
ap.add_argument('--download', action='store_true', help='use pinned downloads instead of system libraries') ap.add_argument('--download', action='store_true', help='use pinned downloads instead of system libraries')
ap.add_argument('--no-boost', action='store_true', help='skip Boost.JSON') ap.add_argument('--no-boost', action='store_true', help='skip Boost.JSON')
ap.add_argument('--native', action='store_true', help='compile for this CPU (-march=native / -mcpu=native)') ap.add_argument('--native', action='store_true', help='compile for this CPU (-march=native / -mcpu=native)')
ap.add_argument('--aligned', action='store_true',
help='also build and run with all functions (and loops) aligned, to make code layout artifacts visible')
ap.add_argument('--rounds', type=int, default=30, help='rounds of bench_view (default: 30)') ap.add_argument('--rounds', type=int, default=30, help='rounds of bench_view (default: 30)')
ap.add_argument('--corpus', nargs='*', default=[], help='more files for bench_corpus') ap.add_argument('--corpus', nargs='*', default=[], help='more files for bench_corpus')
ap.add_argument('--build-dir', default=os.path.join(HERE, 'build'), help='where to build (default: build/ next to this script)') ap.add_argument('--build-dir', default=os.path.join(HERE, 'build'), help='where to build (default: build/ next to this script)')
@@ -282,30 +284,44 @@ def main():
+ ['-I' + d for d in lib.include]) + ['-I' + d for d in lib.include])
objects.append(obj) objects.append(obj)
binaries = {} cxx_version = output(cxx + ['--version'])
for bench in ['bench_view', 'bench_corpus', 'bench_edit']: is_clang = 'clang' in cxx_version.lower()
exe = os.path.join(args.build_dir, bench) # alignment flags: functions and (gcc) loops on 64-byte boundaries
run(cxx + flags + include + [os.path.join(HERE, bench + '.cpp')] + objects + link + ['-o', exe]) align_flags = ['-mllvm', '-align-all-functions=6'] if is_clang else ['-falign-functions=64', '-falign-loops=64']
binaries[bench] = exe
# the builds: the default one and, with --aligned, one with aligned code
variants = [('default build', args.build_dir, [])]
if args.aligned:
variants.append(('aligned build', os.path.join(args.build_dir, 'aligned'), align_flags))
# run: bench_view on its documents, bench_corpus on those and the given files
corpus = [os.path.join(args.data, f) for f in DEFAULT_CORPUS] + args.corpus corpus = [os.path.join(args.data, f) for f in DEFAULT_CORPUS] + args.corpus
outputs = {} # label -> {benchmark: output}
outputs['bench_view'] = run([binaries['bench_view'], args.data, str(args.rounds)], cwd=args.build_dir, all_outputs = {}
capture_output=True, text=True).stdout for label, build_dir, extra in variants:
outputs['bench_corpus'] = run([binaries['bench_corpus']] + corpus, cwd=args.build_dir, os.makedirs(build_dir, exist_ok=True)
capture_output=True, text=True).stdout binaries = {}
outputs['bench_edit'] = run([binaries['bench_edit'], args.data, str(max(1, args.rounds // 2))], cwd=args.build_dir, for bench in ['bench_view', 'bench_corpus', 'bench_edit']:
capture_output=True, text=True).stdout exe = os.path.join(build_dir, bench)
for name, text in outputs.items(): run(cxx + flags + extra + include + [os.path.join(HERE, bench + '.cpp')] + objects + link + ['-o', exe])
print(text) binaries[bench] = exe
# run: bench_view on its documents, bench_corpus on those and the given files
outputs = {}
outputs['bench_view'] = run([binaries['bench_view'], args.data, str(args.rounds)], cwd=build_dir,
capture_output=True, text=True).stdout
outputs['bench_corpus'] = run([binaries['bench_corpus']] + corpus, cwd=build_dir,
capture_output=True, text=True).stdout
outputs['bench_edit'] = run([binaries['bench_edit'], args.data, str(max(1, args.rounds // 2))], cwd=build_dir,
capture_output=True, text=True).stdout
for name, text in outputs.items():
print(text)
all_outputs[label] = outputs
# results with their metadata # results with their metadata
now = datetime.datetime.now() now = datetime.datetime.now()
host = re.sub(r'[^A-Za-z0-9-]+', '-', platform.node().split('.')[0]) or 'host' host = re.sub(r'[^A-Za-z0-9-]+', '-', platform.node().split('.')[0]) or 'host'
stem = os.path.join(HERE, 'results', f'{now:%Y-%m-%d}-{host}') stem = os.path.join(HERE, 'results', f'{now:%Y-%m-%d}-{host}')
os.makedirs(os.path.dirname(stem), exist_ok=True) os.makedirs(os.path.dirname(stem), exist_ok=True)
cxx_version = output(cxx + ['--version'])
compiler_version = cxx_version.splitlines()[0] if cxx_version else ' '.join(cxx) compiler_version = cxx_version.splitlines()[0] if cxx_version else ' '.join(cxx)
meta = [ meta = [
('date', f'{now:%Y-%m-%d %H:%M}'), ('date', f'{now:%Y-%m-%d %H:%M}'),
@@ -314,6 +330,7 @@ def main():
('OS', f'{platform.system()} {platform.release()} ({platform.machine()})'), ('OS', f'{platform.system()} {platform.release()} ({platform.machine()})'),
('compiler', compiler_version), ('compiler', compiler_version),
('flags', ' '.join(flags)), ('flags', ' '.join(flags)),
('aligned results', ('included (flags: ' + ' '.join(align_flags) + ')') if args.aligned else 'not included'),
('yyjson', libs['yyjson'].version), ('yyjson', libs['yyjson'].version),
('simdjson', libs['simdjson'].version), ('simdjson', libs['simdjson'].version),
('Boost.JSON', libs['boost'].version if with_boost else 'skipped (not found)'), ('Boost.JSON', libs['boost'].version if with_boost else 'skipped (not found)'),
@@ -326,15 +343,20 @@ def main():
f.write('| | |\n|---|---|\n') f.write('| | |\n|---|---|\n')
for key, value in meta: for key, value in meta:
f.write(f'| {key} | {value} |\n') f.write(f'| {key} | {value} |\n')
for name, text in outputs.items(): for label, outputs in all_outputs.items():
f.write(f'\n## {name}\n\n```\n{text.rstrip()}\n```\n') # (without --aligned there is only the default build, and no extra heading level)
prefix = f'{label}: ' if args.aligned else ''
for name, text in outputs.items():
f.write(f'\n## {prefix}{name}\n\n```\n{text.rstrip()}\n```\n')
with open(stem + '.csv', 'w', encoding='utf-8') as out: with open(stem + '.csv', 'w', encoding='utf-8') as out:
out.write(''.join(f'# {key}: {value}\n' for key, value in meta)) out.write(''.join(f'# {key}: {value}\n' for key, value in meta))
for name in ['bench_view', 'bench_corpus', 'bench_edit']: for label, build_dir, _ in variants:
path = os.path.join(args.build_dir, name + '.csv') for name in ['bench_view', 'bench_corpus', 'bench_edit']:
if os.path.isfile(path): path = os.path.join(build_dir, name + '.csv')
with open(path, encoding='utf-8') as f: if os.path.isfile(path):
out.write(f'# {name}\n' + f.read()) with open(path, encoding='utf-8') as f:
# (the default build keeps its plain section names)
out.write(f'# {name}' + (f' ({label})' if args.aligned else '') + '\n' + f.read())
print(f'results: {stem}.md, {stem}.csv') print(f'results: {stem}.md, {stem}.csv')
+2 -59
View File
@@ -3134,7 +3134,7 @@ TEST_CASE("BJData")
std::vector<uint8_t> const v1 = {'[', '#', 'i', 0xF1}; std::vector<uint8_t> const v1 = {'[', '#', 'i', 0xF1};
std::vector<uint8_t> const v2 = {'[', '$', 'I', '#', 'i', 0xF2}; std::vector<uint8_t> const v2 = {'[', '$', 'I', '#', 'i', 0xF2};
std::vector<uint8_t> const v3 = {'[', '$', 'I', '#', '[', 'i', 0xF4, 'i', 0x02, ']'}; std::vector<uint8_t> const v3 = {'[', '$', 'I', '#', '[', 'i', 0xF4, 'i', 0x02, ']'};
std::vector<uint8_t> const v4 = {'[', '$', 'l', '#', 'i', 0xF7}; std::vector<uint8_t> const v4 = {'[', '$', 0xF6, '#', 'i', 0xF7};
std::vector<uint8_t> const v5 = {'[', '$', 'I', '#', '[', 'i', 0xF5, 'i', 0xF1, ']'}; std::vector<uint8_t> const v5 = {'[', '$', 'I', '#', '[', 'i', 0xF5, 'i', 0xF1, ']'};
std::vector<uint8_t> const v6 = {'[', '#', '[', 'i', 0xF3, 'i', 0x02, ']'}; std::vector<uint8_t> const v6 = {'[', '#', '[', 'i', 0xF3, 'i', 0x02, ']'};
@@ -3455,7 +3455,7 @@ TEST_CASE("BJData")
CHECK(json::from_bjdata(v0, true, false).is_discarded()); CHECK(json::from_bjdata(v0, true, false).is_discarded());
std::vector<uint8_t> const vi = {'[', '$', '#'}; std::vector<uint8_t> const vi = {'[', '$', '#'};
CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vi), "[json.exception.parse_error.112] parse error at byte 3: syntax error while parsing BJData type: marker 0x23 is not a permitted optimized array type", json::parse_error&); CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vi), "[json.exception.parse_error.110] parse error at byte 4: syntax error while parsing BJData value: unexpected end of input", json::parse_error&);
CHECK(json::from_bjdata(vi, true, false).is_discarded()); CHECK(json::from_bjdata(vi, true, false).is_discarded());
std::vector<uint8_t> const vU = {'[', '$', 'U'}; std::vector<uint8_t> const vU = {'[', '$', 'U'};
@@ -4622,60 +4622,3 @@ TEST_CASE("BJData large strings and binaries (chunked reader)")
} }
} }
} }
TEST_CASE("BJData optimized container type marker is validated for count 0")
{
// the '$' type marker must be a legal marker even if no element follows
SECTION("invalid marker")
{
// 'Z', 'T', 'F', 'N', 'S', 'H', '[' and '{' are excluded in BJData
for (const char marker :
{'x', 'E', 'Z', 'T', 'F', 'N', 'S', 'H', '[', '{'
})
{
for (const char open :
{'[', '{'
})
{
for (const int count :
{
0, 1
})
{
CAPTURE(marker)
CAPTURE(open)
CAPTURE(count)
const std::vector<std::uint8_t> v = {static_cast<std::uint8_t>(open), '$', static_cast<std::uint8_t>(marker), '#', 'i', static_cast<std::uint8_t>(count)};
json _;
CHECK_THROWS_AS(_ = json::from_bjdata(v), json::parse_error&);
CHECK(json::from_bjdata(v, true, false).is_discarded());
}
}
}
}
SECTION("error message")
{
const std::vector<std::uint8_t> v = {'[', '$', 'x', '#', 'i', 0};
json _;
CHECK_THROWS_WITH_AS(_ = json::from_bjdata(v), "[json.exception.parse_error.112] parse error at byte 3: syntax error while parsing BJData type: marker 0x78 is not a permitted optimized array type", json::parse_error&);
}
SECTION("valid marker")
{
for (const char marker :
{'i', 'U', 'I', 'u', 'l', 'm', 'L', 'M', 'h', 'd', 'D', 'C'
})
{
CAPTURE(marker)
const std::vector<std::uint8_t> va = {'[', '$', static_cast<std::uint8_t>(marker), '#', 'i', 0};
const std::vector<std::uint8_t> vo = {'{', '$', static_cast<std::uint8_t>(marker), '#', 'i', 0};
CHECK(json::from_bjdata(va) == json::array());
CHECK(json::from_bjdata(vo) == json::object());
}
// an optimized array of bytes is read as a binary value
const std::vector<std::uint8_t> vb = {'[', '$', 'B', '#', 'i', 0};
CHECK(json::from_bjdata(vb) == json::binary({}));
}
}
+17 -249
View File
@@ -802,11 +802,12 @@ TEST_CASE("BSON")
CHECK(json::from_bson(result, true, false) == j); CHECK(json::from_bson(result, true, false) == j);
} }
SECTION("binary member with subtype 0x02 (old binary) carries an inner length") SECTION("binary member with subtype 0x02 (old binary) keeps its inner length prefix (lenient parsing)")
{ {
// the payload of binary subtype 0x02 ("old binary") starts with an // documented lenient behavior (see gh-5333): the payload for
// int32 that repeats the payload size; the bytes below are what // binary subtype 0x02 ("old binary") is returned as-is,
// pymongo and the bson-corpus (binary.json) use // including its own inner 4-byte length prefix; it is not
// stripped or reinterpreted
std::vector<std::uint8_t> const input = std::vector<std::uint8_t> const input =
{ {
0x17, 0x00, 0x00, 0x00, // size (little endian) 0x17, 0x00, 0x00, 0x00, // size (little endian)
@@ -814,87 +815,16 @@ TEST_CASE("BSON")
0x06, 0x00, 0x00, 0x00, // size of binary (little endian) 0x06, 0x00, 0x00, 0x00, // size of binary (little endian)
0x02, // "old binary" subtype 0x02, // "old binary" subtype
0x02, 0x00, 0x00, 0x00, // inner length 0x02, 0x00, 0x00, 0x00, // inner length prefix (part of the old-binary payload)
0x68, 0x69, // payload ('h', 'i') 0x68, 0x69, // payload ('h', 'i')
0x00 // end marker 0x00 // end marker
}; };
// the inner length is not part of the payload // the inner length prefix is part of the (unmodified) payload
const json expected = { { "entry", json::binary({0x68, 0x69}, 0x02) } }; const std::vector<std::uint8_t> expected_payload = {0x02, 0x00, 0x00, 0x00, 0x68, 0x69};
const json expected = { { "entry", json::binary(expected_payload, 0x02) } };
CHECK(json::from_bson(input) == expected); CHECK(json::from_bson(input) == expected);
CHECK(json::to_bson(expected) == input);
CHECK(json::from_bson(json::to_bson(expected)) == expected);
}
SECTION("binary member with subtype 0x02 (old binary) and empty payload")
{
std::vector<std::uint8_t> const input =
{
0x15, 0x00, 0x00, 0x00, // size (little endian)
0x05, 'e', 'n', 't', 'r', 'y', '\x00', // entry: binary
0x04, 0x00, 0x00, 0x00, // size of binary (little endian)
0x02, // "old binary" subtype
0x00, 0x00, 0x00, 0x00, // inner length
0x00 // end marker
};
const json expected = { { "entry", json::binary({}, 0x02) } };
CHECK(json::from_bson(input) == expected);
CHECK(json::to_bson(expected) == input);
}
SECTION("binary member with subtype 0x02 (old binary) with an invalid inner length")
{
std::vector<std::uint8_t> input =
{
0x17, 0x00, 0x00, 0x00, // size (little endian)
0x05, 'e', 'n', 't', 'r', 'y', '\x00', // entry: binary
0x06, 0x00, 0x00, 0x00, // size of binary (little endian)
0x02, // "old binary" subtype
0x02, 0x00, 0x00, 0x00, // inner length
0x68, 0x69, // payload ('h', 'i')
0x00 // end marker
};
SECTION("inner length too long")
{
input[19] = 0x03;
}
SECTION("inner length too short")
{
input[19] = 0x01;
}
SECTION("inner length negative")
{
input[19] = 0xFF;
input[20] = 0xFF;
input[21] = 0xFF;
input[22] = 0xFF;
}
CAPTURE(input)
json _;
CHECK_THROWS_AS(_ = json::from_bson(input), json::parse_error&);
}
SECTION("binary member with subtype 0x02 (old binary) without an inner length")
{
// the payload is shorter than the inner length field
std::vector<std::uint8_t> const input =
{
0x14, 0x00, 0x00, 0x00, // size (little endian)
0x05, 'e', 'n', 't', 'r', 'y', '\x00', // entry: binary
0x03, 0x00, 0x00, 0x00, // size of binary (little endian)
0x02, // "old binary" subtype
0x01, 0x02, 0x03, // payload without an inner length
0x00 // end marker
};
json _;
CHECK_THROWS_AS(_ = json::from_bson(input), json::parse_error&);
} }
SECTION("Some more complex document") SECTION("Some more complex document")
@@ -1248,7 +1178,7 @@ TEST_CASE("BSON document size mismatch")
SECTION("top-level document with a negative size") SECTION("top-level document with a negative size")
{ {
std::vector<std::uint8_t> const input = {0xFF, 0xFF, 0xFF, 0xFF, 0x00}; std::vector<std::uint8_t> const input = {0xFF, 0xFF, 0xFF, 0xFF, 0x00};
CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 4: syntax error while parsing BSON document: document size -1 is less than 5", json::parse_error&); CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 5: syntax error while parsing BSON document: document size -1 does not match the number of bytes read (5)", json::parse_error&);
CHECK(json::from_bson(input, true, false).is_discarded()); CHECK(json::from_bson(input, true, false).is_discarded());
} }
@@ -1260,7 +1190,7 @@ TEST_CASE("BSON document size mismatch")
// enclosing document instead of "d" // enclosing document instead of "d"
std::vector<std::uint8_t> const input = std::vector<std::uint8_t> const input =
{ {
0x14, 0x00, 0x00, 0x00, // outer size 0x00, 0x00, 0x00, 0x00, // outer size
0x03, 'd', 0x00, // entry: embedded document "d" 0x03, 'd', 0x00, // entry: embedded document "d"
0xFF, 0xFF, 0xFF, 0x7F, // embedded size 0x7FFFFFFF 0xFF, 0xFF, 0xFF, 0x7F, // embedded size 0x7FFFFFFF
0x0A, 'a', 0x00, // entry: null "a" 0x0A, 'a', 0x00, // entry: null "a"
@@ -1268,24 +1198,7 @@ TEST_CASE("BSON document size mismatch")
0x08, 'h', 0x00, 0x01, // entry: bool "h" = true 0x08, 'h', 0x00, 0x01, // entry: bool "h" = true
0x00 // outer end marker 0x00 // outer end marker
}; };
CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 11: syntax error while parsing BSON document: document size 2147483647 exceeds the size of the enclosing document", json::parse_error&); CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 15: syntax error while parsing BSON document: document size 2147483647 does not match the number of bytes read (8)", json::parse_error&);
CHECK(json::from_bson(input, true, false).is_discarded());
}
SECTION("embedded document whose size is smaller than its content")
{
// {"a":null} is 8 bytes, but the length prefix claims 7
std::vector<std::uint8_t> const input =
{
0x14, 0x00, 0x00, 0x00, // outer size
0x03, 'd', 0x00, // entry: embedded document "d"
0x07, 0x00, 0x00, 0x00, // embedded size 7 (real is 8)
0x0A, 'a', 0x00, // entry: null "a"
0x00, // embedded end marker
0x08, 'h', 0x00, 0x01, // entry: bool "h" = true
0x00 // outer end marker
};
CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 15: syntax error while parsing BSON document: document size 7 does not match the number of bytes read (8)", json::parse_error&);
CHECK(json::from_bson(input, true, false).is_discarded()); CHECK(json::from_bson(input, true, false).is_discarded());
} }
@@ -1294,163 +1207,18 @@ TEST_CASE("BSON document size mismatch")
// array [42] is 12 bytes, but the length prefix claims 13 // array [42] is 12 bytes, but the length prefix claims 13
std::vector<std::uint8_t> const input = std::vector<std::uint8_t> const input =
{ {
0x14, 0x00, 0x00, 0x00, // outer size 0x00, 0x00, 0x00, 0x00, // outer size
0x04, 'a', 0x00, // entry: array "a" 0x04, 'a', 0x00, // entry: array "a"
0x0D, 0x00, 0x00, 0x00, // array size 13 (real is 12) 0x0D, 0x00, 0x00, 0x00, // array size 13 (real is 12)
0x10, '0', 0x00, 0x2A, 0x00, 0x00, 0x00, // entry: int32 "0" = 42 0x10, '0', 0x00, 0x2A, 0x00, 0x00, 0x00, // entry: int32 "0" = 42
0x00, // array end marker 0x00, // array end marker
0x00 // outer end marker 0x00 // outer end marker
}; };
CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 11: syntax error while parsing BSON document: document size 13 exceeds the size of the enclosing document", json::parse_error&); CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 19: syntax error while parsing BSON document: document size 13 does not match the number of bytes read (12)", json::parse_error&);
CHECK(json::from_bson(input, true, false).is_discarded()); CHECK(json::from_bson(input, true, false).is_discarded());
} }
} }
TEST_CASE("BSON declared document size bounds the content")
{
// records the events that a SAX parser receives
struct recorder : nlohmann::json_sax<json>
{
std::string events;
bool null() override
{
events += "null ";
return true;
}
bool boolean(bool /*val*/) override
{
events += "boolean ";
return true;
}
bool number_integer(json::number_integer_t /*val*/) override
{
events += "integer ";
return true;
}
bool number_unsigned(json::number_unsigned_t /*val*/) override
{
events += "unsigned ";
return true;
}
bool number_float(json::number_float_t /*val*/, const std::string& /*s*/) override
{
events += "float ";
return true;
}
bool string(std::string& /*val*/) override
{
events += "string ";
return true;
}
bool binary(json::binary_t& /*val*/) override
{
events += "binary ";
return true;
}
bool start_object(std::size_t /*elements*/) override
{
events += "{ ";
return true;
}
bool key(std::string& /*val*/) override
{
events += "key ";
return true;
}
bool end_object() override
{
events += "} ";
return true;
}
bool start_array(std::size_t /*elements*/) override
{
events += "[ ";
return true;
}
bool end_array() override
{
events += "] ";
return true;
}
bool parse_error(std::size_t /*position*/, const std::string& /*last_token*/, const json::exception& /*ex*/) override
{
events += "error ";
return false;
}
};
SECTION("the content of a too small document is not reported")
{
// {"a":1} is 12 bytes; the length prefix claims 5, so the element
// does not fit
std::vector<std::uint8_t> const input =
{
0x05, 0x00, 0x00, 0x00,
0x10, 'a', 0x00, 0x01, 0x00, 0x00, 0x00,
0x00
};
recorder rec;
CHECK(!json::sax_parse(input, &rec, json::input_format_t::bson));
CHECK(rec.events == "{ error ");
// also without strict mode
recorder rec2;
CHECK(!json::sax_parse(input, &rec2, json::input_format_t::bson, false));
CHECK(rec2.events == "{ error ");
}
SECTION("an embedded document that does not fit its parent is not opened")
{
// the embedded document ends where the enclosing one does, so there
// is no room for the terminator of the enclosing document
std::vector<std::uint8_t> const input =
{
0x0C, 0x00, 0x00, 0x00,
0x03, 'd', 0x00,
0x05, 0x00, 0x00, 0x00, 0x00,
0x00
};
recorder rec;
CHECK(!json::sax_parse(input, &rec, json::input_format_t::bson));
CHECK(rec.events == "{ key error ");
}
SECTION("an embedded document that fits exactly")
{
std::vector<std::uint8_t> const input =
{
0x0D, 0x00, 0x00, 0x00,
0x03, 'd', 0x00,
0x05, 0x00, 0x00, 0x00, 0x00,
0x00
};
CHECK(json::from_bson(input) == json({{"d", json::object()}}));
}
SECTION("a document size that is less than 5")
{
for (const std::int32_t size :
{
0, 1, 4, -1, (std::numeric_limits<std::int32_t>::min)()
})
{
CAPTURE(size)
std::vector<std::uint8_t> input =
{
0x00, 0x00, 0x00, 0x00, 0x00
};
input[0] = static_cast<std::uint8_t>(size & 0xFF);
input[1] = static_cast<std::uint8_t>((size >> 8) & 0xFF);
input[2] = static_cast<std::uint8_t>((size >> 16) & 0xFF);
input[3] = static_cast<std::uint8_t>((size >> 24) & 0xFF);
json _;
CHECK_THROWS_AS(_ = json::from_bson(input), json::parse_error&);
CHECK(json::from_bson(input, true, false).is_discarded());
}
}
}
TEST_CASE("BSON nesting does not consume the call stack") TEST_CASE("BSON nesting does not consume the call stack")
{ {
// An embedded document or array used to be read by calling back into the // An embedded document or array used to be read by calling back into the
@@ -2155,7 +1923,7 @@ TEST_CASE("Invalid document size handling")
{ {
std::vector<std::uint8_t> const v = {0x04, 0x00, 0x00, 0x00, 0x00}; std::vector<std::uint8_t> const v = {0x04, 0x00, 0x00, 0x00, 0x00};
json _; json _;
CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 4: syntax error while parsing BSON document: document size 4 is less than 5", json::parse_error&); CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 5: syntax error while parsing BSON document: document size 4 does not match the number of bytes read (5)", json::parse_error&);
CHECK(json::from_bson(v, true, false).is_discarded()); CHECK(json::from_bson(v, true, false).is_discarded());
} }
@@ -2170,7 +1938,7 @@ TEST_CASE("Invalid document size handling")
0x00 0x00
}; };
json _; json _;
CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 5: syntax error while parsing BSON document: document size 5 is too small for its content", json::parse_error&); CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 16: syntax error while parsing BSON document: document size 5 does not match the number of bytes read (16)", json::parse_error&);
CHECK(json::from_bson(v, true, false).is_discarded()); CHECK(json::from_bson(v, true, false).is_discarded());
} }
@@ -2201,7 +1969,7 @@ TEST_CASE("Invalid document size handling")
0x00 // object terminator 0x00 // object terminator
}; };
json _; json _;
CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 12: syntax error while parsing BSON document: document size 5 is too small for its content", json::parse_error&); CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 19: syntax error while parsing BSON document: document size 5 does not match the number of bytes read (12)", json::parse_error&);
CHECK(json::from_bson(v, true, false).is_discarded()); CHECK(json::from_bson(v, true, false).is_discarded());
} }
+3 -148
View File
@@ -1807,93 +1807,6 @@ TEST_CASE("CBOR")
CHECK(json::from_cbor(std::vector<uint8_t>({0xa1, 0xff, 0x01}), true, false).is_discarded()); CHECK(json::from_cbor(std::vector<uint8_t>({0xa1, 0xff, 0x01}), true, false).is_discarded());
} }
SECTION("tag in front of a key")
{
const auto ignore = json::cbor_tag_handler_t::ignore;
const auto store = json::cbor_tag_handler_t::store;
const json expected = {{"a", 10}};
// the tags are skipped in front of a text string key, like in
// front of a value
const std::vector<std::vector<std::uint8_t>> tagged =
{
{0xA1, 0xC0, 0x61, 0x61, 0x0A}, // tag in the head
{0xA1, 0xD8, 0x05, 0x61, 0x61, 0x0A}, // 1-byte tag number
{0xA1, 0xD9, 0x01, 0x2C, 0x61, 0x61, 0x0A}, // 2-byte tag number
{0xA1, 0xDA, 0x00, 0x01, 0x00, 0x00, 0x61, 0x61, 0x0A}, // 4-byte tag number
{0xA1, 0xDB, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x61, 0x61, 0x0A}, // 8-byte tag number
{0xA1, 0xC0, 0xD8, 0x05, 0xC1, 0x61, 0x61, 0x0A}, // chained tags
{0xBF, 0xC0, 0x61, 0x61, 0x0A, 0xFF}, // indefinite-length map
{0xBF, 0x61, 0x62, 0x01, 0xC0, 0x61, 0x61, 0x0A, 0xFF}, // second key tagged
{0xA1, 0xC0, 0x7F, 0x61, 0x61, 0xFF, 0x0A}, // indefinite-length text key
{0xA1, 0xC0, 0x61, 0x61, 0xC0, 0x0A}, // tagged key and value
};
for (const auto& input : tagged)
{
CAPTURE(input)
json const j_ignore = json::from_cbor(input, true, true, ignore);
json const j_store = json::from_cbor(input, true, true, store);
CHECK(j_ignore.at("a") == 10);
CHECK(j_store.at("a") == 10);
CHECK(j_ignore == j_store);
}
CHECK(json::from_cbor(tagged[0], true, true, ignore) == expected);
CHECK(json::from_cbor(tagged[1], true, true, store) == expected);
// nested maps
CHECK(json::from_cbor(std::vector<std::uint8_t>({0xA1, 0xC0, 0x61, 0x61, 0xA1, 0xD8, 0x05, 0x61, 0x62, 0x01}), true, true, ignore) == json{{"a", {{"b", 1}}}});
// the tag handler error still rejects every tag
for (const auto& input : tagged)
{
CAPTURE(input)
json _;
CHECK_THROWS_AS(_ = json::from_cbor(input), json::parse_error&);
}
{
json _;
CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector<std::uint8_t>({0xA1, 0xD8, 0x05, 0x61, 0x61, 0x0A})), "[json.exception.parse_error.113] parse error at byte 2: syntax error while parsing CBOR object key: only string keys are supported, but found a tag; last byte: 0xD8", json::parse_error&);
}
// a tag does not turn a key of another type into a text string
{
json _;
CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector<std::uint8_t>({0xA1, 0xC0, 0x01, 0x01}), true, true, ignore), "[json.exception.parse_error.113] parse error at byte 3: syntax error while parsing CBOR object key: only string keys are supported, but found an unsigned integer; last byte: 0x01", json::parse_error&);
CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector<std::uint8_t>({0xA1, 0xD8, 0x05, 0x41, 0x61, 0x01}), true, true, store), "[json.exception.parse_error.113] parse error at byte 4: syntax error while parsing CBOR object key: only string keys are supported, but found a byte string; last byte: 0x41", json::parse_error&);
CHECK(json::from_cbor(std::vector<std::uint8_t>({0xA1, 0xC0, 0x01, 0x01}), true, false, ignore).is_discarded());
}
// end of input after the tag, or inside the tag number
{
json _;
CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector<std::uint8_t>({0xA1, 0xC0}), true, true, ignore), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing CBOR string: unexpected end of input", json::parse_error&);
CHECK_THROWS_AS(_ = json::from_cbor(std::vector<std::uint8_t>({0xA1, 0xD8}), true, true, ignore), json::parse_error&);
CHECK_THROWS_AS(_ = json::from_cbor(std::vector<std::uint8_t>({0xA1, 0xDB, 0x00, 0x00}), true, true, store), json::parse_error&);
CHECK_THROWS_AS(_ = json::from_cbor(std::vector<std::uint8_t>({0xBF, 0xC0}), true, true, store), json::parse_error&);
}
// SAX: the key event is reported without the tag (start_object,
// key, number_unsigned, end_object are four events)
{
const std::vector<std::uint8_t> input = {0xA1, 0xC0, 0x61, 0x61, 0x0A};
SaxCountdown four(4);
CHECK(json::sax_parse(input, &four, json::input_format_t::cbor, true, false, false, ignore));
SaxCountdown three(3);
CHECK(!json::sax_parse(input, &three, json::input_format_t::cbor, true, false, false, ignore));
SaxCountdown error_handler(4);
CHECK(!json::sax_parse(input, &error_handler, json::input_format_t::cbor));
}
// a long chain of tags needs no recursion
{
std::vector<std::uint8_t> input = {0xA1};
input.insert(input.end(), 100000, 0xC0);
input.insert(input.end(), {0x61, 0x61, 0x0A});
CHECK(json::from_cbor(input, true, true, ignore) == expected);
}
}
SECTION("non-string key (see #2766 and #3381)") SECTION("non-string key (see #2766 and #3381)")
{ {
// only text strings map to JSON object keys; any other key is // only text strings map to JSON object keys; any other key is
@@ -2002,10 +1915,9 @@ TEST_CASE("CBOR")
{ {
json _; json _;
// with the default error handler keep, the chunks are concatenated // the chunks are concatenated as is, without checking that each
// as is, without checking that each chunk is valid UTF-8 on its // chunk is valid UTF-8 on its own (RFC 8949, Section 3.2.3), so
// own (RFC 8949, Section 3.2.3), so a code point split across two // a code point split across two chunks yields a valid string
// chunks yields a valid string
CHECK_NOTHROW(_ = json::from_cbor(std::vector<uint8_t>({0x7f, 0x61, 0xc3, 0x61, 0xa9, 0xff}))); CHECK_NOTHROW(_ = json::from_cbor(std::vector<uint8_t>({0x7f, 0x61, 0xc3, 0x61, 0xa9, 0xff})));
CHECK(_ == "\xc3\xa9"); CHECK(_ == "\xc3\xa9");
CHECK(_.dump() == "\"\xc3\xa9\""); CHECK(_.dump() == "\"\xc3\xa9\"");
@@ -2025,63 +1937,6 @@ TEST_CASE("CBOR")
CHECK(json::from_cbor(std::vector<uint8_t>({0x7f, 0x62, 0xc3, 0xa9, 0x62, 0xc3, 0xb6, 0xff})) == "\xc3\xa9\xc3\xb6"); CHECK(json::from_cbor(std::vector<uint8_t>({0x7f, 0x62, 0xc3, 0xa9, 0x62, 0xc3, 0xb6, 0xff})) == "\xc3\xa9\xc3\xb6");
} }
SECTION("ill-formed UTF-8 in a chunk of an indefinite-length string (RFC 8949, Section 3.2.3)")
{
const auto strict = json::error_handler_t::strict;
const auto replace = json::error_handler_t::replace;
const auto ignore = json::error_handler_t::ignore;
const auto keep = json::error_handler_t::keep;
const auto tags = json::cbor_tag_handler_t::error;
// a code point split over two chunks
const std::vector<std::uint8_t> split = {0x7f, 0x61, 0xc3, 0x61, 0xa9, 0xff};
// a 4-byte code point split over two chunks
const std::vector<std::uint8_t> split_emoji = {0x7f, 0x62, 0xf0, 0x9f, 0x62, 0x98, 0x80, 0xff};
// the same in an object key
const std::vector<std::uint8_t> split_key = {0xa1, 0x7f, 0x61, 0xc3, 0x61, 0xa9, 0xff, 0x01};
// a valid chunk followed by one with a truncated code point
const std::vector<std::uint8_t> truncated = {0x7f, 0x62, 0x41, 0xc3, 0x61, 0xa9, 0xff};
// strict: the string is invalid
for (const auto& input :
{
split, split_emoji, split_key, truncated
})
{
CAPTURE(input)
json _;
CHECK_THROWS_AS(_ = json::from_cbor(input, true, true, tags, strict), json::parse_error&);
CHECK(json::from_cbor(input, true, false, tags, strict).is_discarded());
}
{
json _;
CHECK_THROWS_WITH_AS(_ = json::from_cbor(split, true, true, tags, strict), "[json.exception.parse_error.113] parse error at byte 3: syntax error while parsing CBOR string: invalid string: ill-formed UTF-8 byte", json::parse_error&);
CHECK_THROWS_WITH_AS(_ = json::from_cbor(split_key, true, true, tags, strict), "[json.exception.parse_error.113] parse error at byte 4: syntax error while parsing CBOR key: invalid string: ill-formed UTF-8 byte", json::parse_error&);
}
// replace and ignore: every chunk is sanitized on its own
CHECK(json::from_cbor(split, true, true, tags, replace) == "\xEF\xBF\xBD\xEF\xBF\xBD");
CHECK(json::from_cbor(split, true, true, tags, ignore) == "");
CHECK(json::from_cbor(truncated, true, true, tags, replace) == "A\xEF\xBF\xBD\xEF\xBF\xBD");
CHECK(json::from_cbor(truncated, true, true, tags, ignore) == "A");
CHECK(json::from_cbor(split_key, true, true, tags, replace) == json{{"\xEF\xBF\xBD\xEF\xBF\xBD", 1}});
// keep: unchanged, the chunks are concatenated as is
CHECK(json::from_cbor(split, true, true, tags, keep) == "\xc3\xa9");
CHECK(json::from_cbor(split_emoji, true, true, tags, keep) == "\xf0\x9f\x98\x80");
// conforming input is unaffected by the error handler
const std::vector<std::uint8_t> valid = {0x7f, 0x62, 0xc3, 0xa9, 0x64, 0xf0, 0x9f, 0x98, 0x80, 0x60, 0xff};
for (const auto handler :
{
strict, replace, ignore, keep
})
{
CHECK(json::from_cbor(valid, true, true, tags, handler) == "\xc3\xa9\xf0\x9f\x98\x80");
CHECK(json::from_cbor(std::vector<std::uint8_t>({0x7f, 0xff}), true, true, tags, handler) == "");
}
}
SECTION("many chunks in indefinite-length string") SECTION("many chunks in indefinite-length string")
{ {
constexpr std::size_t chunks = 100000; constexpr std::size_t chunks = 100000;
+1 -53
View File
@@ -2023,7 +2023,7 @@ TEST_CASE("UBJSON")
CHECK(json::from_ubjson(v0, true, false).is_discarded()); CHECK(json::from_ubjson(v0, true, false).is_discarded());
std::vector<uint8_t> const vi = {'[', '$', '#'}; std::vector<uint8_t> const vi = {'[', '$', '#'};
CHECK_THROWS_WITH_AS(_ = json::from_ubjson(vi), "[json.exception.parse_error.112] parse error at byte 3: syntax error while parsing UBJSON type: marker 0x23 is not a permitted optimized array type", json::parse_error&); CHECK_THROWS_WITH_AS(_ = json::from_ubjson(vi), "[json.exception.parse_error.110] parse error at byte 4: syntax error while parsing UBJSON value: unexpected end of input", json::parse_error&);
CHECK(json::from_ubjson(vi, true, false).is_discarded()); CHECK(json::from_ubjson(vi, true, false).is_discarded());
std::vector<uint8_t> const vT = {'[', '$', 'T'}; std::vector<uint8_t> const vT = {'[', '$', 'T'};
@@ -3348,55 +3348,3 @@ TEST_CASE("UBJSON large strings (chunked reader)")
} }
} }
} }
TEST_CASE("UBJSON optimized container type marker is validated for count 0")
{
// the '$' type marker must be a legal marker even if no element follows
SECTION("invalid marker")
{
for (const char marker :
{'x', 'E', 'B', 'u', 'm', 'M', 'h'
})
{
for (const char open :
{'[', '{'
})
{
for (const int count :
{
0, 1
})
{
CAPTURE(marker)
CAPTURE(open)
CAPTURE(count)
const std::vector<std::uint8_t> v = {static_cast<std::uint8_t>(open), '$', static_cast<std::uint8_t>(marker), '#', 'i', static_cast<std::uint8_t>(count)};
json _;
CHECK_THROWS_AS(_ = json::from_ubjson(v), json::parse_error&);
CHECK(json::from_ubjson(v, true, false).is_discarded());
}
}
}
}
SECTION("error message")
{
const std::vector<std::uint8_t> v = {'[', '$', 'x', '#', 'i', 0};
json _;
CHECK_THROWS_WITH_AS(_ = json::from_ubjson(v), "[json.exception.parse_error.112] parse error at byte 3: syntax error while parsing UBJSON type: marker 0x78 is not a permitted optimized array type", json::parse_error&);
}
SECTION("valid marker")
{
for (const char marker :
{'Z', 'N', 'T', 'F', 'i', 'U', 'I', 'l', 'L', 'd', 'D', 'H', 'C', 'S', '[', '{'
})
{
CAPTURE(marker)
const std::vector<std::uint8_t> va = {'[', '$', static_cast<std::uint8_t>(marker), '#', 'i', 0};
const std::vector<std::uint8_t> vo = {'{', '$', static_cast<std::uint8_t>(marker), '#', 'i', 0};
CHECK(json::from_ubjson(va) == json::array());
CHECK(json::from_ubjson(vo) == json::object());
}
}
}