Compare commits

...
Author SHA1 Message Date
Niels Lohmann 2638f276d0 docs: format tables, use the Oxford comma
Reformat the tables changed by this PR (BJData serialization and
deserialization mappings, completeness and binary-value tables) so all
rows are aligned, and add the Oxford comma to lists of three or more
items in the added text.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 06:53:55 +01:00
Niels Lohmann 66d4f3602b Address review of the BJData documentation
The $ type restriction exists since Draft 2 (Draft 3 added B), $ requires a count, ND char above 127 is rejected on read, and the new API page is listed in alphabetical order in the nav.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 06:53:54 +01:00
Niels Lohmann 612e9ade2c Regenerate single_include and apply astyle
Result of make amalgamate (including make pretty) for the preceding changes.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 06:53:53 +01:00
Niels Lohmann b3ab23e11c docs: correct and complete the BJData documentation
Verify the BJData chapter against the library and the Draft 2/3/4
specifications and fix what did not match:

- bjdata.md: correct the list of unused markers (N, not Z), the ND-array
  example (3x2, not 2x3), the conditions under which an object is packed
  as an ND-array (exactly three keys, row vectors, float-only elements for
  single/double, char 0..127, byte only in Draft 3), the binary table row
  and notes (which flag/draft combinations write [$B), the "$" type
  restriction (Draft 3, Z is excluded too), the H marker in the
  deserialization table, and the claim of a complete deserialization
  mapping. Document the supported drafts, that Draft 4 (SoA containers,
  extension type E) and column-major ND-arrays are rejected, ND-arrays of
  half floats, duplicate object keys, and link the Draft 3 specifications
  instead of master.
- Add an API page for basic_json::bjdata_version_t with an example, and
  link it from the nav, the API index and to_bjdata.
- Update binary_formats/index.md and binary_values.md, which still claimed
  that BJData has no binary type, and fix a wrong byte comment there.
- Document parse_error.115 and the out_of_range.408 cases for from_bjdata,
  the precise other_error.502 condition for to_bjdata, and reword the note
  on truncated unsigned integers in exceptions.md (only ND-array elements
  were truncated before 3.13.0).

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 06:53:52 +01:00
Niels Lohmann 31538a91ed Read ND-arrays of half floats and enforce the char limit in ND-arrays
BJData N-dimensional arrays with element type 'h' (float16) failed to
read with a misleading parse_error.112, because the reader's dtype name
table lacked 'h'. It now maps to "half", the JData name. The writer still
does not pack "half"; such an object is written as a plain object and
reads back equal.

BJData limits 'C' (char) to 0..127 in Draft 2 and Draft 3, and the 1-D
reader enforces this, but ND-arrays with _ArrayType_ "char" wrote values
up to 255 as 'C' elements and read them back without a check. The writer
now only packs values in 0..127 (others fall back to a plain object that
round-trips), and the reader rejects 'C' elements above 127 with
parse_error.113 like the 1-D reader.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 06:53:51 +01:00
Niels Lohmann bf8dca5c16 Fix -Weffc++ in the BSON recorder test and a clang-tidy use-auto hit
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 06:53:50 +01:00
Niels Lohmann d19e030bf1 docs: use the Oxford comma, drop discarded-value remarks
Add the serial comma in a list added by this PR and remove the BSON
example of type_error.321 (with the type_error.317 remark) so the docs do
not mention discarded values for BSON; restore the original singular
heading of that section. No tables were changed by this PR.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 06:53:09 +01:00
Niels Lohmann e359852b76 Address review of the BSON documentation
Describe exactly when a too small document size is detected and note that subtype 2 data written by earlier versions is not readable.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 06:53:08 +01:00
Niels Lohmann 734912bed6 Regenerate single_include and apply astyle
Result of make amalgamate (including make pretty) for the preceding changes.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 06:53:07 +01:00
Niels Lohmann 91cc42b302 Document BSON subtype 0x02, document size checks, and lenient reads
Describe the inner length of binary subtype 0x02 that to_bson now writes and
from_bson checks and removes. Add the document size checks (parse_error.112,
not affected by strict=false) to bson.md and from_bson.md, and extend the
list of lenient reads with the UUID and MD5 payload lengths (not checked),
duplicate keys (last value wins), embedded NUL bytes in strings, and the
ignored trailing bytes with strict=false. State that the members of an object
are written in iteration order.

Correct the statement that element names are never validated on read: they
are checked and sanitized like string values by the error handler. Add the
BSON message to the examples of out_of_range.415 and type_error.321, the
subtype limit of 255 to the binary values page, CBOR to the formats that use
byte_container_with_subtype, and fix a typo in the incomplete mapping box.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 06:53:06 +01:00
Niels Lohmann 95412a36d0 Check the declared BSON document size before reading its content
The BSON reader compared a document's declared size with the number of bytes
read only when it reached the terminator. The declared size never bounded
the reads, so a SAX handler saw the keys and values of a document whose size
was too small before parse error 112 was reported.

The container frame now stores the position where the document ends
according to its declared size. open_bson_document rejects a size below 5
(size prefix plus terminator) and an embedded document that does not end
before the terminator of its enclosing document; the element loop rejects an
element whose type byte leaves no room for the name terminator and the
document terminator. The terminator check stays and still catches a declared
size that is too large. No input that was accepted before is rejected now;
documents with a wrong size are reported earlier, with fewer SAX events, a
different byte position and, in three cases, a different message. The error
id (parse_error.112) is unchanged and strict=false still does not skip the
check.

The tests that asserted the old messages and positions are updated, with the
outer sizes of two inputs corrected so that they still exercise the embedded
document. New tests cover the SAX events, a size below 5, and an embedded
document that fits exactly.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 06:53:05 +01:00
Niels Lohmann d2309300e9 Write and read the inner length of BSON binary subtype 0x02
The BSON specification defines the payload of the "binary (old)" subtype
0x02 as an int32 followed by that many bytes. to_bson wrote the plain
payload instead, so decoders such as pymongo and libbson-based readers
rejected the bytes, and from_bson returned the inner length as part of the
payload (documented as lenient behavior in gh-5333).

The writer now emits the outer length (payload + 4), the subtype, the inner
int32 holding the payload size, and the payload. The reader requires an
outer length of at least 4 and an inner length equal to the outer length
minus 4 (the cases the bson-corpus lists as decode errors), reports parse
error 112 otherwise, and returns only the payload. Both directions change
together so that the round trip keeps working.

The single-reservation fast path for scalar entries declines subtype 0x02
because it has no room for the inner length.

This is a deliberate exception to the opt-in rule for behavior changes:
the previous output was invalid BSON.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 06:53:04 +01:00
Niels Lohmann d3d39a9c10 Fix narrowing of the optimized-container count in BJData and UBJSON tests
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 06:52:50 +01:00
Niels Lohmann 24ff6eaa43 docs: format tables, use the Oxford comma, drop discarded-value remarks
Reformat the two UBJSON mapping tables that this PR changed (uniform column
widths, padded cells), use the serial comma in lists of three or more in the
added text, and restore the original type_error.321 bullet in to_ubjson.md.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-25 20:05:29 +01:00
Niels Lohmann 9d9bf927ff Address review of the UBJSON documentation
Clarify that empty containers carry no $ annotation of their own and note that a counted N object is not supported.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-25 20:05:29 +01:00
Niels Lohmann cc39e1f8da Regenerate single_include and apply astyle
Result of make amalgamate (including make pretty) for the preceding changes.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-25 20:05:28 +01:00
Niels Lohmann 5ac95db5a9 Correct and complete the UBJSON documentation
Fix statements that disagree with the behavior (checked with probes):
- `Z` is null; the unused marker is `N`.
- `$` requires `#`; use_type with use_size=false throws other_error.502 only
  for non-empty arrays, objects and binary values.
- NaN and infinity are written as IEEE 754 float64 and read back unchanged;
  the UBJSON specification encodes infinity as null.
- `H` is documented (semantics, parse_error.115, out_of_range.406); the
  "complete mapping" claim is qualified.
- use_type "same type" means same marker.
- Long Z/T/F arrays lose only the `$` annotation; the `#` count and the
  element markers are kept.
- from_ubjson lists the 112, 113, 115, 406 and 408 causes that occur for
  UBJSON, without the BJData-only ND-array wording.
- Duplicate object keys keep the last value; the `$` type marker is
  validated even for count 0.
- Smaller fixes: float32 row, "map" -> "object", https link, exception 110
  strict case, to_ubjson history of discarded values.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-25 20:05:27 +01:00
Niels Lohmann d1d142633a Validate the optimized-container type marker for count 0 in UBJSON and BJData
The '$' type marker of an optimized container was only checked when an
element was read, so containers with count 0 accepted any marker:
`[$x#i0`, `{$x#i0` and `[$E#i0` parsed as empty containers although a
non-empty container with the same marker is rejected and the UBJSON
specification requires a legal marker after '$'.

get_ubjson_size_type() now checks the marker right after '$' against the
allowed set (UBJSON: Z N T F i U I l L d D H C S [ {; BJData: B U i u I m
l M L h d D C, with the existing exclusion of T F Z N S H [ {) and reports
parse_error.112 "marker ... is not a permitted optimized array type".

Input that was accepted and is now rejected: an optimized container with
count 0 and an invalid '$' type marker. Valid input is unaffected. Two
existing tests that used an invalid marker incidentally ("[$#" and a
negative-count case) were adapted.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-25 20:05:26 +01:00
Niels Lohmann 49e9eaac4f Address review of the MessagePack documentation
Name parse_error.113 for a 0xC1 map key, move the duplicate-key sentence out of the ext section, and keep the 0xC1 row in marker order.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-25 20:05:25 +01:00
Niels Lohmann 4b35877ae5 Document MessagePack ext types and fix related doc errors
Add an "Extension types (ext, fixext)" section to the MessagePack page:
ext/fixext are read as binary, the signed ext type is exposed as an
unsigned subtype 0..255 (-1 reads as 255), Timestamp is not interpreted,
subtypes are always written as ext/fixext, and subtypes above 255 throw
out_of_range.415. Add the 0xC1 row to the deserialization table, note
that duplicate keys keep the last value, and correct the NaN/Infinity
explanation, the "31 bytes / 15 elements" wording, the ill-formed UTF-8
sentence, and the generic 0xC1-key error message.

Add fixext16 and the unsigned subtype mapping to binary_t.md and
binary_values.md, reword "maximum of the MessagePack ext type" in
to_msgpack.md and exceptions.md, and mark MessagePack deserialization
as incomplete (non-string keys) in the binary formats overview.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-25 20:05:24 +01:00
26 changed files with 1116 additions and 122 deletions

No files matched your search

+3 -2
View File
@@ -90,8 +90,9 @@ type `#!cpp binary_t*` must be dereferenced.
- MessagePack
- 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) is used. For other sizes, the ext family (ext8, ext16, ext32) is used. The subtype is
then added as a signed 8-bit integer.
fixext2, fixext4, fixext8, fixext16) is used. For other sizes, the ext family (ext8, ext16, ext32) is used. The
subtype is then written as the signed 8-bit ext type: subtypes 128 to 255 map to the negative (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.
- BSON
@@ -0,0 +1,48 @@
# <small>nlohmann::basic_json::</small>bjdata_version_t
```cpp
enum class bjdata_version_t
{
draft2,
draft3
};
```
This enumeration is used in [`to_bjdata`](to_bjdata.md) to choose the version of the BJData specification to write:
draft2
: BJData Draft 2 (the default). Binary values are written as arrays of `U` integers, and ND-arrays with
`"_ArrayType_": "byte"` are written as regular objects, since the `B` marker is not part of Draft 2.
draft3
: BJData Draft 3. Binary values are written as `B` elements: as an optimized binary array (`[$B#`) if both `use_size`
and `use_type` are set, and as an array of `B`-prefixed elements otherwise. ND-arrays with
`"_ArrayType_": "byte"` are written as packed arrays (`[$B#[...]`).
Only the writer is affected by the version. [`from_bjdata`](from_bjdata.md) reads the markers of both drafts.
## Examples
??? example
The example below shows how the different values of the `bjdata_version_t` influence the serialization of a binary
value with [`to_bjdata`](to_bjdata.md).
```cpp
--8<-- "examples/bjdata_version_t.cpp"
```
Output:
```json
--8<-- "examples/bjdata_version_t.output"
```
## See also
- [to_bjdata](to_bjdata.md) serializes a JSON value to BJData
- [BJData](../../features/binary_formats/bjdata.md) - the article on the BJData format
## Version history
- Added in version 3.12.0.
@@ -82,8 +82,11 @@ 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 a parse error occurs
- Throws [parse_error.113](../../home/exceptions.md#jsonexceptionparse_error113) if a string could not be parsed
successfully, or if a string value or object key is not valid UTF-8 and `error_handler` is `strict`
- Throws [parse_error.115](../../home/exceptions.md#jsonexceptionparse_error115) if the text of a high-precision
number (`H`) is not a valid number
- Throws [out_of_range.408](../../home/exceptions.md#jsonexceptionout_of_range408) if the size of an optimized container
or n-dimensional array cannot be represented by `std::size_t`
cannot be represented by `std::size_t` or exceeds the container's `max_size()`, or if the product of the dimensions of
an n-dimensional array overflows
## Complexity
+2 -1
View File
@@ -80,7 +80,8 @@ 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
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
invalid string or byte array length)
invalid string or byte array length, an invalid inner length of a binary value with subtype `0x02`, or a document size
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
encountered
- Throws [`parse_error.113`](../../home/exceptions.md#jsonexceptionparse_error113) if a string value or object key is
+11 -4
View File
@@ -79,11 +79,18 @@ 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
the end of the file was not reached when `strict` was set to true
- Throws [parse_error.112](../../home/exceptions.md#jsonexceptionparse_error112) if a parse error occurs
- Throws [parse_error.112](../../home/exceptions.md#jsonexceptionparse_error112) if an invalid type marker is read
(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
successfully, 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
or n-dimensional array cannot be represented by `std::size_t`
successfully (negative or non-integer length, `C` followed by a byte above 0x7F), if the count of an optimized
container is negative, or if a string value or object key is not valid UTF-8 and `error_handler` is `strict`
- Throws [parse_error.115](../../home/exceptions.md#jsonexceptionparse_error115) if the text of a high-precision number
(`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
+1
View File
@@ -121,6 +121,7 @@ The class satisfies the following concept requirements:
- [**json_pointer**](../json_pointer/index.md) - JSON Pointer implementation
- [**json_serializer**](json_serializer.md) - type of the serializer to for conversions from/to JSON
- [**error_handler_t**](error_handler_t.md) - type to choose behavior on decoding errors
- [**bjdata_version_t**](bjdata_version_t.md) - type to choose the BJData version to write
- [**cbor_tag_handler_t**](cbor_tag_handler_t.md) - type to choose how to handle CBOR tags
- **initializer_list_t** - type for initializer lists of `basic_json` values
- [**input_format_t**](input_format_t.md) - type to choose the format to parse
+5 -2
View File
@@ -43,7 +43,8 @@ The exact mapping and its limitations are described on a [dedicated page](../../
`#!cpp false` by default.
`version` (in)
: which version of BJData to use (see note on "Binary values" on [BJData](../../features/binary_formats/bjdata.md));
: which version of BJData to use, see [`bjdata_version_t`](bjdata_version_t.md) and the note on "Binary values" on
[BJData](../../features/binary_formats/bjdata.md);
optional, `#!cpp bjdata_version_t::draft2` by default.
`error_handler` (in)
@@ -64,7 +65,9 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va
## Exceptions
- Throws [`other_error.502`](../../home/exceptions.md#jsonexceptionother_error502) if `use_type` is true and `use_size`
is false, and `j` contains a non-empty array, object, or binary value.
is false, and `j` contains a non-empty array or object, or a non-empty binary value (with
`version = bjdata_version_t::draft3`, any binary value, including an empty one).
Scalars, empty arrays, and empty objects do not throw.
- 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` (the default only if
[`JSON_STRICT_BINARY_UTF8`](../macros/json_strict_binary_utf8.md) is enabled)
+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
so-called document).
so-called document). The members of an object are written in the order in which iteration visits them.
1. Returns a byte vector containing the BSON serialization.
2. Writes the BSON serialization to an output adapter.
@@ -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:
`"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
exceeds 255, the maximum of the MessagePack ext type; example:
exceeds 255, the largest value the library can store in the one-byte MessagePack ext type; example:
`"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
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
used by BSON and MessagePack. This type exists so that the user does not have to specify a type themselves with a
used by BSON, CBOR, 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.
## Template parameters
@@ -0,0 +1,33 @@
#include <iomanip>
#include <iostream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
void print_hex(const char* label, const std::vector<std::uint8_t>& v)
{
std::cout << label << ':';
for (auto b : v)
{
std::cout << ' ' << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(b);
}
std::cout << std::dec << std::setfill(' ') << '\n';
}
int main()
{
// a binary value
json j = json::binary({0xCA, 0xFE, 0xBA, 0xBE});
// Draft 2 (the default) writes an array of integers
auto v2 = json::to_bjdata(j, true, true, json::bjdata_version_t::draft2);
print_hex("draft2", v2);
// Draft 3 writes an optimized binary array
auto v3 = json::to_bjdata(j, true, true, json::bjdata_version_t::draft3);
print_hex("draft3", v3);
// reading yields an array for Draft 2 and a binary value for Draft 3
std::cout << json::from_bjdata(v2) << '\n';
std::cout << json::from_bjdata(v3) << '\n';
}
@@ -0,0 +1,4 @@
draft2: 5b 24 55 23 69 04 ca fe ba be
draft3: 5b 24 42 23 69 04 ca fe ba be
[202,254,186,190]
{"bytes":[202,254,186,190],"subtype":null}
@@ -2,9 +2,9 @@
The [BJData format](https://neurojson.org) was derived from and improved upon
[Universal Binary JSON(UBJSON)](https://ubjson.org) specification (Draft 12). Specifically, it introduces an optimized
array container for efficient storage of N-dimensional packed arrays (**ND-arrays**); it also adds 5 new type markers -
`[u] - uint16`, `[m] - uint32`, `[M] - uint64`, `[h] - float16` and `[B] - byte` - to unambiguously map common binary
numeric types; furthermore, it uses little-endian (LE) to store all numerics instead of big-endian (BE) as in UBJSON to
array container for efficient storage of N-dimensional packed arrays (**ND-arrays**); it also adds four new type markers -
`[u] - uint16`, `[m] - uint32`, `[M] - uint64`, and `[h] - float16` - to unambiguously map common binary numeric types
(Draft 3 adds `[B] - byte`); furthermore, it uses little-endian (LE) to store all numerics instead of big-endian (BE) as in UBJSON to
avoid unnecessary conversions on commonly available platforms.
Compared to other binary JSON-like formats such as MessagePack and CBOR, both BJData and UBJSON demonstrate a rare
@@ -15,11 +15,30 @@ simple processing.
!!! abstract "References"
- [BJData Specification](https://neurojson.org/bjdata/draft2)
- [BJData Specification (Draft 3)](https://neurojson.org/bjdata/draft3)
- [BJData Specification (Draft 2)](https://neurojson.org/bjdata/draft2)
!!! info "Supported drafts"
The library supports BJData Draft 2 (the default) and Draft 3, except for column-major N-dimensional arrays (see
below). The draft used for writing is selected with the
[`version` parameter](../../api/basic_json/bjdata_version_t.md) of
[`to_bjdata`](../../api/basic_json/to_bjdata.md); [`from_bjdata`](../../api/basic_json/from_bjdata.md) has no such
parameter and reads the markers of both drafts.
!!! warning "Draft 4 and column-major ND-arrays are not supported"
BJData Draft 4 (April 2026) adds structure-of-arrays (SoA) containers (`[$` or `{$` followed by a schema object)
and the extension type `E`. `from_bjdata` does not implement them and throws
[`parse_error.112`](../../home/exceptions.md#jsonexceptionparse_error112) for such input; `to_bjdata` never writes
them.
Likewise, column-major ND-arrays of Draft 3 (the `#` marker followed by an array that wraps the dimension vector)
are rejected with [`parse_error.113`](../../home/exceptions.md#jsonexceptionparse_error113).
## Byte layout
BJData follows the structure of UBJSON: a one-byte, human-readable ASCII *type marker* followed by the payload of that type, with strings prefixed by a length that is itself an integer value. In contrast to UBJSON, numbers are stored in little-endian byte order, and additional markers cover unsigned 16/32/64-bit integers, half-precision floats, and bytes. Containers can be written unoptimized (opened with `[` or `{`, closed with `]` or `}`) or optimized with a declared element type (`$`) and/or count (`#`). BJData extends the optimized form: the count can be a list of dimensions, which stores an N-dimensional packed array as a flat sequence of values without any per-element markers.
BJData follows the structure of UBJSON: a one-byte, human-readable ASCII *type marker* followed by the payload of that type, with strings prefixed by a length that is itself an integer value. In contrast to UBJSON, numbers are stored in little-endian byte order, and additional markers cover unsigned 16/32/64-bit integers, half-precision floats, and bytes. Containers can be written unoptimized (opened with `[` or `{`, closed with `]` or `}`) or optimized with a count (`#`), optionally preceded by a declared element type (`$`). BJData extends the optimized form: the count can be a list of dimensions, which stores an N-dimensional packed array as a flat sequence of values without any per-element markers.
Consider the following JSON value (shown formatted for readability):
@@ -55,7 +74,7 @@ Consider the following JSON value (shown formatted for readability):
The library uses the following mapping from JSON values types to BJData types according to the BJData specification:
| JSON value type | value/range | BJData type | marker |
|-----------------|-------------------------------------------|----------------|--------|
|-----------------|-------------------------------------------|-------------|-------------------------------------------------------------------------|
| null | `null` | null | `Z` |
| boolean | `true` | true | `T` |
| boolean | `false` | false | `F` |
@@ -82,7 +101,7 @@ The library uses the following mapping from JSON values types to BJData types ac
| string | *with shortest length indicator* | string | `S` |
| array | *see notes on optimized format/ND-array* | array | `[` |
| object | *see notes on optimized format* | map | `{` |
| binary | *see notes on binary values* | array | `[$B` |
| binary | *see notes on binary values* | array | `[$B` (Draft 3 with `use_size` and `use_type`), `[U`, or `[B` otherwise |
!!! success "Complete mapping"
@@ -107,10 +126,16 @@ The library uses the following mapping from JSON values types to BJData types ac
!!! info "Unused BJData markers"
The following markers are not used in the conversion:
The following markers are not written:
- `Z`: no-op values are not created.
- `C`: single-byte strings are serialized with `S` markers.
- `N`: no-op values are not created (they are skipped when reading).
- `C`: single-character strings are serialized with `S` markers (`C` is read, and written for the elements of
ND-arrays with `"_ArrayType_": "char"`).
- `h`: floating-point numbers are written as `D`, not as float16 (`h` is read).
- `B`: only written in Draft 3 mode.
- `d` is only written for `single` ND-arrays or if `number_float_t` is `float`; `H` is only written for integers
wider than 64 bits (custom `number_integer_t`/`number_unsigned_t`). An unsigned integer above 9223372036854775807
is written as `M`.
!!! info "NaN/infinity handling"
@@ -142,7 +167,7 @@ The library uses the following mapping from JSON values types to BJData types ac
BJData extends UBJSON's optimized array **size** marker to support ND-arrays of uniform numerical data types
(referred to as *packed arrays*). For example, the 2-D `uint8` integer array `[[1,2],[3,4],[5,6]]`, stored as nested
optimized array in UBJSON `[ [$U#i2 1 2 [$U#i2 3 4 [$U#i2 5 6 ]`, can be further compressed in BJData to
`[$U#[$i#i2 2 3 1 2 3 4 5 6` or `[$U#[i2 i3] 1 2 3 4 5 6`.
`[$U#[$i#i2 3 2 1 2 3 4 5 6` or `[$U#[i3 i2] 1 2 3 4 5 6`.
To maintain type and size information, ND-arrays are converted to JSON objects following the **annotated array
format** (defined in the [JData specification (Draft 3)][JDataAAFmt]), when parsed using
@@ -152,11 +177,14 @@ The library uses the following mapping from JSON values types to BJData types ac
```json
{
"_ArrayType_": "uint8",
"_ArraySize_": [2,3],
"_ArraySize_": [3,2],
"_ArrayData_": [1,2,3,4,5,6]
}
```
ND-arrays with the element type `h` (float16) are returned with `"_ArrayType_": "half"`. `to_bjdata` does not pack
`"half"` objects; they are written as regular objects, which read back as an equal value.
Likewise, when a JSON object in the above form is serialized using
[`to_bjdata`](../../api/basic_json/to_bjdata.md), it is automatically converted into a compact BJData ND-array.
@@ -167,34 +195,40 @@ The library uses the following mapping from JSON values types to BJData types ac
object; otherwise it is serialized as a regular JSON object, so the annotation is never lost in a round trip. This requires
all of the following:
- the object has exactly the three keys `"_ArrayType_"`, `"_ArraySize_"`, and `"_ArrayData_"` (any other key, e.g.,
`"_ArrayOrder_"`, makes it a regular object),
- `"_ArrayType_"` is one of `uint8`, `int8`, `uint16`, `int16`, `uint32`, `int32`, `uint64`, `int64`, `single`,
`double`, `char`, or `byte`,
`double`, `char`, or (only with `version = bjdata_version_t::draft3`, since the `B` marker is not part of Draft 2)
`byte`; the names are case-sensitive,
- `"_ArraySize_"` is an array, since the dimensions are written as the ND-array header's length,
- `"_ArraySize_"` has at least two entries and is not a 1×N row vector (first entry 1), since other shapes are
parsed back as a regular array,
- `"_ArraySize_"` has at least two entries and is not a two-entry row vector (`[1,N]`), since other shapes are
parsed back as a regular array (`[1,2,2]` is packed, `[1,4]` is not),
- every entry of `"_ArraySize_"` is a positive integer, and their product is representable as a `std::size_t`,
- `"_ArrayData_"` is an array holding exactly that many elements, and
- every element of `"_ArrayData_"` is a number of the kind named by `"_ArrayType_"`: for the integer types, a
value that fits the named width; for `double`, any value; for `single`, a value that survives narrowing to
`float` and back without change (for instance, `0.1` does not, since it is not exactly representable as
`float`).
- every element of `"_ArrayData_"` is a number of the kind named by `"_ArrayType_"`: for the integer types, an
integer (a floating-point number such as `1.0` does not qualify) that fits the named width (for `char`, 0..127);
for `double`, a floating-point number (an integer such as `1` does not qualify); for `single`, a floating-point
number that survives narrowing to `float` and back without change (for instance, `0.1` does not, since it is not
exactly representable as `float`).
An annotated object is always read back with its keys in the order shown above, `"_ArrayType_"`, `"_ArraySize_"`,
`"_ArrayData_"`, regardless of the order the ND-array's header stores them in on the wire. This matters for
`ordered_json`, whose comparison takes key order into account.
With `ordered_json`, an annotated object read from BJData has its keys in the order `"_ArrayType_"`,
`"_ArraySize_"`, `"_ArrayData_"`; with `json`, they are sorted like those of any other object.
The current version of this library does not yet support automatic detection of and conversion from a nested JSON
array input to a BJData ND-array.
[JDataAAFmt]: https://github.com/NeuroJSON/jdata/blob/master/JData_specification.md#annotated-storage-of-n-d-arrays
[JDataAAFmt]: https://github.com/NeuroJSON/jdata/blob/Draft-3/JData_specification.md#annotated-storage-of-n-d-arrays
!!! info "Restrictions in optimized data types for arrays and objects"
Due to diminished space saving, hampered readability, and increased security risks, in BJData, the allowed data
types following the `$` marker in an optimized array and object container are restricted to
Due to diminished space saving, hampered readability, and increased security risks, in BJData, the
allowed data types following the `$` marker in an optimized array and object container are restricted to
**non-zero-fixed-length** data types. Therefore, the valid optimized type markers can only be one of
`UiuImlMLhdDCB`. This also means other variable (`[{SH`) or zero-length types (`TFN`) can not be used in an
optimized array or object in BJData.
`UiuImlMLhdDC`, plus `B` since Draft 3 (the reader accepts `B` in both drafts). This also means other variable (`[{SH`) or zero-length types (`TFZN`) can not be used in an
optimized array or object in BJData. The library enforces this restriction for both drafts. A `C` (char) element above 127 is rejected on read with
[`parse_error.113`](../../home/exceptions.md#jsonexceptionparse_error113), in 1-D and in N-dimensional arrays. With `use_type`, the
elements must have the same marker (`[1,200,3]` is not optimized, since it mixes `i` and `U`); booleans, null,
strings, arrays, and objects are never optimized with `$`.
!!! info "Binary values"
@@ -202,14 +236,16 @@ The library uses the following mapping from JSON values types to BJData types ac
in optimized arrays to designate binary data. This means that, unlike UBJSON, binary data can be both serialized and
deserialized.
To preserve compatibility with BJData Draft 2, the Draft 3 optimized binary array must be explicitly enabled using
the `version` parameter of [`to_bjdata`](../../api/basic_json/to_bjdata.md).
To preserve compatibility with BJData Draft 2, the Draft 3 optimized binary array must be explicitly enabled: pass
`version = bjdata_version_t::draft3` together with `use_size = true` and `use_type = true` to
[`to_bjdata`](../../api/basic_json/to_bjdata.md). With Draft 3 and only `use_size` (or neither), a binary value is
written as an array of `B` elements.
In Draft2 mode (default), if the JSON data contains the binary type, the value stored as a list of integers, as
In Draft 2 mode (default), if the JSON data contains the binary type, the value is stored as a list of integers, as
suggested by the BJData documentation. In particular, this means that the serialization and the deserialization of
JSON containing binary values into BJData and back will result in a different JSON object.
[BJDataBinArr]: https://github.com/NeuroJSON/bjdata/blob/master/Binary_JData_Specification.md#optimized-binary-array
[BJDataBinArr]: https://github.com/NeuroJSON/bjdata/blob/Draft-3/Binary_JData_Specification.md#optimized-binary-array
??? example "Example: serialize JSON values to BJData, with and without size/type optimization"
@@ -228,7 +264,7 @@ The library uses the following mapping from JSON values types to BJData types ac
The library maps BJData types to JSON value types as follows:
| BJData type | JSON value type | marker |
|-------------|------------------------------------------|----------|
|-----------------------|--------------------------------------------------|----------|
| no-op | *no value, next value is read* | `N` |
| null | `null` | `Z` |
| false | `false` | `F` |
@@ -236,6 +272,7 @@ The library maps BJData types to JSON value types as follows:
| float16 | number_float | `h` |
| float32 | number_float | `d` |
| float64 | number_float | `D` |
| high-precision number | number_integer, number_unsigned, or number_float | `H` |
| uint8 | number_unsigned | `U` |
| int8 | number_integer | `i` |
| uint16 | number_unsigned | `u` |
@@ -254,7 +291,18 @@ The library maps BJData types to JSON value types as follows:
!!! success "Complete mapping"
The mapping is **complete** in the sense that any BJData value can be converted to a JSON value.
The mapping is **complete** for the BJData types of Draft 2 and Draft 3 with one exception: column-major
N-dimensional arrays are rejected with [`parse_error.113`](../../home/exceptions.md#jsonexceptionparse_error113).
The Draft 4 constructs (structure-of-arrays containers and the extension type `E`) are not supported either.
The digits of an `H` value are parsed like a JSON number: integers that fit 64 bits become `number_integer` or
`number_unsigned`, anything else a `number_float`; invalid text throws
[`parse_error.115`](../../home/exceptions.md#jsonexceptionparse_error115).
!!! info "Duplicate object keys"
If an object contains the same key more than once, `from_bjdata` accepts it and keeps the last value; with
`ordered_json`, the key keeps the position of its first occurrence.
!!! warning "Ill-formed UTF-8 in string values and object keys"
@@ -66,7 +66,12 @@ The library uses the following mapping from JSON values types to BSON types:
!!! warning "Incomplete mapping"
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 a zero-terminated c-strings.
Also, keys may not contain U+0000, since they are serialized as 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"
@@ -81,6 +86,19 @@ 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,
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"
```cpp
@@ -135,12 +153,29 @@ 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:
- 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, and
- the payload for binary subtype `0x02` is returned as-is, including its inner length prefix.
- any non-zero byte is accepted as `true` for the boolean type,
- the payloads of the binary subtypes `0x03` (UUID, old), `0x04` (UUID), and `0x05` (MD5) are not checked to have
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
`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"
The BSON specification requires `string` values (type `0x02`) to be valid UTF-8, but this is not required of a
@@ -154,8 +189,9 @@ 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
(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
instead. Element (key) names are never validated on read, since they are read byte-by-byte as a C string. `binary`
values (type `0x05`) are unaffected, since they are not required to hold text.
instead. Element (key) names are treated like string values: with `keep` they are read unchanged, with `strict`,
`replace`, or `ignore` they are checked and handled in the same way. `binary` values (type `0x05`) are unaffected,
since they are not required to hold text.
??? example "Example: deserialize a JSON value from BSON"
@@ -17,19 +17,19 @@ to efficiently encode JSON values to byte vectors and to decode such vectors.
### Completeness
| Format | Serialization | Deserialization |
|-------------|-----------------------------------------------|----------------------------------------------|
| BJData | complete | complete |
|-------------|-----------------------------------------------|-----------------------------------------------------------------------|
| BJData | complete | incomplete: no column-major ND-arrays, no Draft 4 SoA/extension types |
| BON8 | incomplete: no unsigned integers above int64 | complete |
| 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 |
| MessagePack | complete | complete |
| MessagePack | complete | incomplete, but all JSON types are supported |
| UBJSON | complete | complete |
### Binary values
| Format | Binary values | Binary subtypes |
|-------------|---------------|-----------------|
| BJData | not supported | not supported |
|-------------|--------------------------|-----------------|
| BJData | supported (Draft 3 only) | not supported |
| BON8 | not supported | not supported |
| BSON | supported | supported |
| CBOR | supported | supported |
@@ -11,7 +11,7 @@ extra byte in addition to the strings themselves.
## 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 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.
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.
Consider the following JSON value (shown formatted for readability):
@@ -109,8 +109,9 @@ specification:
!!! note
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 compared magnitudes with NaN, which is
always `false` and caused the float 32 path to be skipped.
total), because the check used to select the smaller float 32 encoding failed for NaN (all comparisons are `false`)
and for Infinity and -Infinity (outside the float range checked against `std::numeric_limits<float>::lowest()` and
`max()`), so the float 32 path was skipped.
??? example "Example: serialize a JSON value to MessagePack"
@@ -135,6 +136,7 @@ The library maps MessagePack types to JSON value types as follows:
| fixarray | array | 0x90..0x9F |
| fixstr | string | 0xA0..0xBF |
| nil | `null` | 0xC0 |
| (never used) | – | 0xC1 |
| false | `false` | 0xC2 |
| true | `true` | 0xC3 |
| float 32 | number_float | 0xCA |
@@ -167,6 +169,30 @@ The library maps MessagePack types to JSON value types as follows:
| fixext 16 | binary | 0xD8 |
| 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
Any MessagePack output created by `to_msgpack` can be successfully parsed by `from_msgpack`.
@@ -177,7 +203,8 @@ The library maps MessagePack types to JSON value types as follows:
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
rejected with a [`parse_error.113`](../../home/exceptions.md#jsonexceptionparse_error113) exception (or, with
`allow_exceptions` set to `false`, a discarded value) naming the type of the key that was found, for instance:
`allow_exceptions` set to `false`, a discarded value). For most key types the message names the type that was
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
@@ -189,7 +216,7 @@ The library maps MessagePack types to JSON value types as follows:
!!! 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
a byte sequence that is not valid UTF-8, and expects a deserializer to hand the original bytes back unchanged.
a byte sequence that is not valid UTF-8, and recommends that a deserializer provide a way to obtain the original bytes.
This library follows that by default: with its
[`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
@@ -5,11 +5,11 @@ achieve the generality of JSON, combined with being much easier to process than
!!! abstract "References"
- [UBJSON Website](http://ubjson.org)
- [UBJSON Website](https://ubjson.org)
## 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 an element type (`$`) and/or a count (`#`) after the opening marker, which saves per-element markers and (for a count) the closing marker.
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.
Consider the following JSON value (shown formatted for readability):
@@ -45,7 +45,7 @@ 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:
| JSON value type | value/range | UBJSON type | marker |
|-----------------|-------------------------------------------|----------------|--------|
|-----------------|-------------------------------------------|--------------------------------------------------|-----------|
| null | `null` | null | `Z` |
| boolean | `true` | true | `T` |
| boolean | `false` | false | `F` |
@@ -63,10 +63,10 @@ The library uses the following mapping from JSON values types to UBJSON types ac
| number_unsigned | 32768..2147483647 | int32 | `l` |
| number_unsigned | 2147483648..9223372036854775807 | int64 | `L` |
| number_unsigned | 9223372036854775808..18446744073709551615 | high-precision | `H` |
| number_float | *any value* | float64 | `D` |
| number_float | *any value* | float64 (float32 if `number_float_t` is `float`) | `D` (`d`) |
| string | *with shortest length indicator* | string | `S` |
| array | *see notes on optimized format* | array | `[` |
| object | *see notes on optimized format* | map | `{` |
| object | *see notes on optimized format* | object | `{` |
!!! success "Complete mapping"
@@ -93,20 +93,27 @@ The library uses the following mapping from JSON values types to UBJSON types ac
The following markers are not used in the conversion:
- `Z`: no-op values are not created.
- `N`: no-op values are never written (they are skipped when reading).
- `C`: single-byte strings are serialized with `S` markers.
!!! info "NaN/infinity handling"
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) function which serializes NaN or Infinity to `null`.
NaN and infinity are written as IEEE 754 float64 values (`D`) and read back unchanged. This differs from
[`dump()`](../../api/basic_json/dump.md), which serializes them to `null`, and from the UBJSON specification, which
encodes infinity as `null` (it does not mention NaN); other UBJSON readers may therefore not accept these values.
!!! info "Optimized formats"
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
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`.
used together with `use_size = true`: combining `use_type` with `use_size = false` throws
[`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
receiving side is immediately informed on the number of elements of the container.
@@ -115,8 +122,9 @@ The library uses the following mapping from JSON values types to UBJSON types ac
already is the value. Its declared count is therefore the only thing that decides how much memory the receiving side
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
(`1 << 20`), and `to_ubjson` writes longer arrays of these types without the annotation, so any value it produces
can be read back.
(`1 << 20`), and `to_ubjson` writes longer arrays of these types in the counted but untyped form: the `$` type marker is
dropped, the `#` count is kept, and every element is written with its own marker (`[#l 1048577 Z Z ...`), so any value
it produces can be read back. Objects and untyped counted arrays are not subject to this limit.
!!! info "Binary values"
@@ -141,7 +149,7 @@ 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:
| UBJSON type | JSON value type | marker |
|-------------|-----------------------------------------|--------|
|----------------|--------------------------------------------------|--------|
| no-op | *no value, next value is read* | `N` |
| null | `null` | `Z` |
| false | `false` | `F` |
@@ -155,12 +163,39 @@ The library maps UBJSON types to JSON value types as follows:
| int64 | number_integer | `L` |
| string | string | `S` |
| char | string | `C` |
| high-precision | number_unsigned, number_integer, or number_float | `H` |
| array | array (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"
The mapping is **complete** in the sense that any UBJSON value can be converted to a JSON value.
Every well-formed UBJSON value can be converted to a JSON value, with these exceptions: a high-precision number
outside the range of `number_float_t` ([`out_of_range.406`](../../home/exceptions.md#jsonexceptionout_of_range406)),
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"
+14 -7
View File
@@ -132,8 +132,11 @@ is an integer or `null`.
### BJData
[BJData](binary_formats/bjdata.md) neither supports binary values nor subtypes and proposes to serialize binary values
as an array of uint8 values. The library implements this translation.
[BJData](binary_formats/bjdata.md) Draft 3 supports binary values as optimized arrays of `B` (byte) elements, but no
subtypes. With `version = bjdata_version_t::draft3` and both `use_size` and `use_type` set, `to_bjdata` writes a binary
value as `[$B#...`, and `from_bjdata` reads it back as a binary value (without subtype). In all other cases, including
the default Draft 2 mode, the library follows the Draft 2 proposal and writes an array of integers (`U` in Draft 2, `B`
in Draft 3), which is read back as an array.
??? example "Example: serialize a binary value to BJData"
@@ -171,7 +174,7 @@ as an array of uint8 values. The library implements this translation.
```c
0x7B // '{'
0x23 0x69 0x01 // '#' 'i' type of the array elements: unsigned integers
0x23 0x69 0x01 // '#' 'i' 1 number of object members
0x69 0x06 // i 6 (length of the key)
0x62 0x69 0x6E 0x61 0x72 0x79 // "binary"
0x5B // '[' array
@@ -180,7 +183,7 @@ as an array of uint8 values. The library implements this translation.
0xCA 0xFE 0xBA 0xBE // content
```
Note that subtype (42) is **not** serialized and that BJData has **no binary type**, and deserializing `v` would
Note that subtype (42) is **not** serialized and that, in Draft 2 mode, BJData has **no binary type**, and deserializing `v` would
yield the following value:
```json
@@ -227,7 +230,10 @@ array of integers.
### BSON
[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. If no subtype is given, the generic binary subtype 0x00 is used.
unsigned 8-bit integer; a subtype above 255 throws
[`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"
@@ -324,8 +330,9 @@ representation using the length of the byte array.
### MessagePack
[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, ext8, ext16, and
ext32. The subtype is then added as a signed 8-bit integer.
is used. The library will choose the smallest representation among fixext1, fixext2, fixext4, fixext8, fixext16, ext8,
ext16, and ext32. The subtype is then written as the signed 8-bit ext type: subtypes 128 to 255 map to the negative
(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.
+18 -9
View File
@@ -330,7 +330,7 @@ where they cannot be resolved.
### json.exception.parse_error.110
When parsing a [binary format](../features/binary_formats/index.md), the byte vector ends before the complete value has
been read.
been read, or (with `strict = true`) further bytes follow the value.
!!! failure "Example messages"
@@ -418,13 +418,16 @@ The parsing of the corresponding BSON record type is not implemented (yet).
### json.exception.parse_error.115
A UBJSON high-precision number could not be parsed.
A UBJSON or BJData high-precision number could not be parsed.
!!! failure "Example message"
!!! failure "Example messages"
```
[json.exception.parse_error.115] parse error at byte 5: syntax error while parsing UBJSON high-precision number: invalid number text: 1A
```
```
[json.exception.parse_error.115] parse error at byte 5: syntax error while parsing BJData high-precision number: invalid number text: 1A
```
### json.exception.parse_error.116
@@ -1025,7 +1028,7 @@ exception, because they serialize integers beyond 64 bits as high-precision numb
- Since version 3.12.0, integer numbers beyond int64 are serialized as uint64 BSON numbers.
Before version 3.13.0, CBOR, MessagePack, and BSON silently truncated integers wider than 64 bits, and BJData
truncated unsigned integers wider than 64 bits.
truncated unsigned integers wider than 64 bits that were elements of ND-arrays with `"_ArrayType_": "uint64"`.
!!! failure "Example messages"
@@ -1045,8 +1048,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
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.
[`to_ubjson`](../api/basic_json/to_ubjson.md) writes longer arrays of these types without the size and type annotation,
so any value it produces can still be read back.
[`to_ubjson`](../api/basic_json/to_ubjson.md) writes longer arrays of these types without the `$` type annotation (the `#`
count and one marker per element are kept), so any value it produces can still be read back.
!!! failure "Example messages"
@@ -1153,14 +1156,18 @@ A JSON Patch `move` operation's `"from"` location is a proper prefix of its `"pa
### 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
[`byte_container_with_subtype`](../api/byte_container_with_subtype/index.md) whose subtype exceeds 255.
[`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.
!!! failure "Example message"
!!! failure "Example messages"
```
[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
This exception was added in version 3.13.0. Before that, subtypes above 255 were silently truncated modulo 256 instead of raising an error.
@@ -1229,7 +1236,9 @@ A JSON Patch operation 'test' failed. The unsuccessful operation is also printed
### 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
`use_type = true` but `use_size = false`. UBJSON requires a size marker (`#`) after a type marker (`$`).
`use_type = true` but `use_size = false`, and the value contains a non-empty array or object (or, with BJData Draft 3,
any binary value). UBJSON requires a size marker (`#`) after a type marker (`$`); scalars and empty arrays or objects
do not throw.
!!! failure "Example message"
+1
View File
@@ -173,6 +173,7 @@ nav:
- 'begin': api/basic_json/begin.md
- 'binary': api/basic_json/binary.md
- 'binary_t': api/basic_json/binary_t.md
- 'bjdata_version_t': api/basic_json/bjdata_version_t.md
- 'boolean_t': api/basic_json/boolean_t.md
- 'cbegin': api/basic_json/cbegin.md
- 'cbor_tag_handler_t': api/basic_json/cbor_tag_handler_t.md
@@ -235,6 +235,9 @@ class binary_reader
/// UBJSON/BJData: the type marker of an optimized container, so that
/// its elements are read without one of their own; 0 otherwise
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
std::int32_t declared_size = 0;
/// whether to close this container with end_object() or end_array()
@@ -371,6 +374,23 @@ class binary_reader
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())))
{
return false;
@@ -378,6 +398,7 @@ class binary_reader
container_frame& frame = container_stack.back();
frame.start_position = document_start;
frame.end_position = document_end;
frame.declared_size = document_size;
return true;
}
@@ -436,6 +457,13 @@ class binary_reader
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;
key.clear();
if (JSON_HEDLEY_UNLIKELY(!get_bson_cstr(key)))
@@ -584,9 +612,46 @@ class binary_reader
}
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);
}
/*!
@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.
@param[in] element_type The BSON element type, c.f. http://bsonspec.org/spec.html
@@ -3000,6 +3065,14 @@ class binary_reader
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();
if (JSON_HEDLEY_UNLIKELY(current != '#'))
{
@@ -3204,6 +3277,8 @@ class binary_reader
// get_ubjson_size_value() (the type marker is known before the dimension vector
// that determines size_and_type.first is read, so it is emitted first there to
// match the documented _ArrayType_, _ArraySize_, _ArrayData_ key order)
// 'C' elements are read as unsigned bytes, but BJData limits a char to 0..127
const bool nd_is_char = size_and_type.second == 'C';
if (size_and_type.second == 'C' || size_and_type.second == 'B')
{
size_and_type.second = 'U';
@@ -3217,6 +3292,24 @@ class binary_reader
for (std::size_t i = 0; i < size_and_type.first; ++i)
{
if (nd_is_char)
{
get();
if (JSON_HEDLEY_UNLIKELY(!unexpect_eof("char")))
{
return false;
}
if (JSON_HEDLEY_UNLIKELY(current > 127))
{
return last_byte_error(exception_id::invalid_string_or_size, "byte after 'C' must be in range 0x00..0x7F", "char", "");
}
if (JSON_HEDLEY_UNLIKELY(!sax->number_unsigned(static_cast<number_unsigned_t>(current))))
{
return false;
}
continue;
}
if (JSON_HEDLEY_UNLIKELY(!get_ubjson_value(size_and_type.second)))
{
return false;
@@ -4391,6 +4484,12 @@ 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");
}
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
bool bson_unsupported_type_error(const std::size_t position, const char_int_type element_type) const
{
@@ -4496,6 +4595,44 @@ class binary_reader
std::size_t bon8_pushback_size = 0;
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
@return whether @a marker is one of 'F', 'H', 'N', 'S', 'T', 'Z', '[', '{'
@@ -4538,6 +4675,8 @@ class binary_reader
return "uint8";
case 'd':
return "single";
case 'h':
return "half";
case 'i':
return "int8";
case 'l':
@@ -1684,7 +1684,15 @@ class binary_writer
throw_subtype_out_of_range(value.subtype(), "the BSON binary subtype", j);
}
return sizeof(std::int32_t) + value.size() + 1ul;
// the "old binary" subtype 0x02 repeats the payload size in an inner
// 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;
}
/*!
@@ -1697,10 +1705,18 @@ class binary_writer
{
write_bson_entry_header(name, 0x05);
write_number<std::int32_t>(to_bson_length(value.size()), true);
const bool old_binary = is_bson_old_binary(value);
// 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));
if (old_binary)
{
write_number<std::int32_t>(to_bson_length(value.size()), true);
}
write_bytes(value.data(), value.size());
}
@@ -1860,7 +1876,8 @@ class binary_writer
(or the header of a string or binary value) in one reservation
@return whether the entry was written; false for a discarded value, a
string or binary value whose length does not fit the length
field, in which case nothing was written
field, or a binary value with subtype 0x02, in which case nothing
was written
@pre @a name is at most bson_scalar_max_name_size characters long and is
not changed by the error handler
*/
@@ -1921,7 +1938,9 @@ class binary_writer
if (j.is_binary())
{
const binary_t& value = *j.m_data.m_value.binary;
if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::int32_t>(value.size())))
// the old binary subtype 0x02 has an inner length that the single
// 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;
}
@@ -3829,7 +3848,7 @@ class binary_writer
A C++11 `constexpr` function cannot contain a `switch`, so this is a plain
comparison chain instead; it is only reached once per ND-array candidate
object. Keep in sync with binary_reader's `bjd_type_name()`, which maps
the other way.
the other way (it also reads "half", which is never written).
*/
static CharType bjdata_ndarray_type_marker(const string_t& name)
{
@@ -4037,10 +4056,13 @@ class binary_writer
switch (dtype)
{
case 'U':
case 'C':
case 'B':
ok = write_bjdata_ndarray_element<std::uint8_t>(el, dry_run);
break;
case 'C':
// BJData limits a char to 0..127 (the intersection of the int8 and uint8 ranges)
ok = bjdata_ndarray_value_in_range<std::int8_t>(el) && bjdata_ndarray_value_in_range<std::uint8_t>(el);
break;
case 'i':
ok = write_bjdata_ndarray_element<std::int8_t>(el, dry_run);
break;
+167 -6
View File
@@ -17702,6 +17702,9 @@ class binary_reader
/// UBJSON/BJData: the type marker of an optimized container, so that
/// its elements are read without one of their own; 0 otherwise
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
std::int32_t declared_size = 0;
/// whether to close this container with end_object() or end_array()
@@ -17838,6 +17841,23 @@ class binary_reader
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())))
{
return false;
@@ -17845,6 +17865,7 @@ class binary_reader
container_frame& frame = container_stack.back();
frame.start_position = document_start;
frame.end_position = document_end;
frame.declared_size = document_size;
return true;
}
@@ -17903,6 +17924,13 @@ class binary_reader
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;
key.clear();
if (JSON_HEDLEY_UNLIKELY(!get_bson_cstr(key)))
@@ -18051,9 +18079,46 @@ class binary_reader
}
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);
}
/*!
@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.
@param[in] element_type The BSON element type, c.f. http://bsonspec.org/spec.html
@@ -20467,6 +20532,14 @@ class binary_reader
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();
if (JSON_HEDLEY_UNLIKELY(current != '#'))
{
@@ -20671,6 +20744,8 @@ class binary_reader
// get_ubjson_size_value() (the type marker is known before the dimension vector
// that determines size_and_type.first is read, so it is emitted first there to
// match the documented _ArrayType_, _ArraySize_, _ArrayData_ key order)
// 'C' elements are read as unsigned bytes, but BJData limits a char to 0..127
const bool nd_is_char = size_and_type.second == 'C';
if (size_and_type.second == 'C' || size_and_type.second == 'B')
{
size_and_type.second = 'U';
@@ -20684,6 +20759,24 @@ class binary_reader
for (std::size_t i = 0; i < size_and_type.first; ++i)
{
if (nd_is_char)
{
get();
if (JSON_HEDLEY_UNLIKELY(!unexpect_eof("char")))
{
return false;
}
if (JSON_HEDLEY_UNLIKELY(current > 127))
{
return last_byte_error(exception_id::invalid_string_or_size, "byte after 'C' must be in range 0x00..0x7F", "char", "");
}
if (JSON_HEDLEY_UNLIKELY(!sax->number_unsigned(static_cast<number_unsigned_t>(current))))
{
return false;
}
continue;
}
if (JSON_HEDLEY_UNLIKELY(!get_ubjson_value(size_and_type.second)))
{
return false;
@@ -21858,6 +21951,12 @@ 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");
}
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
bool bson_unsupported_type_error(const std::size_t position, const char_int_type element_type) const
{
@@ -21963,6 +22062,44 @@ class binary_reader
std::size_t bon8_pushback_size = 0;
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
@return whether @a marker is one of 'F', 'H', 'N', 'S', 'T', 'Z', '[', '{'
@@ -22005,6 +22142,8 @@ class binary_reader
return "uint8";
case 'd':
return "single";
case 'h':
return "half";
case 'i':
return "int8";
case 'l':
@@ -27995,7 +28134,15 @@ class binary_writer
throw_subtype_out_of_range(value.subtype(), "the BSON binary subtype", j);
}
return sizeof(std::int32_t) + value.size() + 1ul;
// the "old binary" subtype 0x02 repeats the payload size in an inner
// 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;
}
/*!
@@ -28008,10 +28155,18 @@ class binary_writer
{
write_bson_entry_header(name, 0x05);
write_number<std::int32_t>(to_bson_length(value.size()), true);
const bool old_binary = is_bson_old_binary(value);
// 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));
if (old_binary)
{
write_number<std::int32_t>(to_bson_length(value.size()), true);
}
write_bytes(value.data(), value.size());
}
@@ -28171,7 +28326,8 @@ class binary_writer
(or the header of a string or binary value) in one reservation
@return whether the entry was written; false for a discarded value, a
string or binary value whose length does not fit the length
field, in which case nothing was written
field, or a binary value with subtype 0x02, in which case nothing
was written
@pre @a name is at most bson_scalar_max_name_size characters long and is
not changed by the error handler
*/
@@ -28232,7 +28388,9 @@ class binary_writer
if (j.is_binary())
{
const binary_t& value = *j.m_data.m_value.binary;
if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::int32_t>(value.size())))
// the old binary subtype 0x02 has an inner length that the single
// 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;
}
@@ -30140,7 +30298,7 @@ class binary_writer
A C++11 `constexpr` function cannot contain a `switch`, so this is a plain
comparison chain instead; it is only reached once per ND-array candidate
object. Keep in sync with binary_reader's `bjd_type_name()`, which maps
the other way.
the other way (it also reads "half", which is never written).
*/
static CharType bjdata_ndarray_type_marker(const string_t& name)
{
@@ -30348,10 +30506,13 @@ class binary_writer
switch (dtype)
{
case 'U':
case 'C':
case 'B':
ok = write_bjdata_ndarray_element<std::uint8_t>(el, dry_run);
break;
case 'C':
// BJData limits a char to 0..127 (the intersection of the int8 and uint8 ranges)
ok = bjdata_ndarray_value_in_range<std::int8_t>(el) && bjdata_ndarray_value_in_range<std::uint8_t>(el);
break;
case 'i':
ok = write_bjdata_ndarray_element<std::int8_t>(el, dry_run);
break;
+126 -2
View File
@@ -2916,6 +2916,35 @@ TEST_CASE("BJData")
CHECK(json::from_bjdata(out_single_ok) == json({{"_ArrayType_", "single"}, {"_ArraySize_", {2, 1}}, {"_ArrayData_", {1.5f, -1.5f}}}));
}
SECTION("ndarray with _ArrayType_ \"char\" is limited to 0..127")
{
// both BJData drafts limit a char to 0..127; larger values must not
// be written as 'C' elements (the reader rejects them), so such an
// object stays a plain object that round-trips
for (const auto version :
{
json::bjdata_version_t::draft2, json::bjdata_version_t::draft3
})
{
json const j_ok = json({{"_ArrayType_", "char"}, {"_ArraySize_", {2, 2}}, {"_ArrayData_", {0, 97, 98, 127}}});
const auto out_ok = json::to_bjdata(j_ok, true, true, version);
CHECK(out_ok == std::vector<uint8_t>({'[', '$', 'C', '#', '[', '$', 'i', '#', 'i', 2, 2, 2, 0, 97, 98, 127}));
CHECK(json::from_bjdata(out_ok) == j_ok);
for (const int bad :
{
128, 200, 255, 256, -1
})
{
CAPTURE(bad)
json const j_bad = json({{"_ArrayType_", "char"}, {"_ArraySize_", {2, 2}}, {"_ArrayData_", {97, 98, 99, bad}}});
const auto out_bad = json::to_bjdata(j_bad, true, true, version);
CHECK(out_bad.at(0) == '{');
CHECK(json::from_bjdata(out_bad) == j_bad);
}
}
}
SECTION("ndarray annotation keys are read back in the documented order")
{
// from_bjdata() must emit the annotation object's keys in the order
@@ -3134,7 +3163,7 @@ TEST_CASE("BJData")
std::vector<uint8_t> const v1 = {'[', '#', 'i', 0xF1};
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 v4 = {'[', '$', 0xF6, '#', 'i', 0xF7};
std::vector<uint8_t> const v4 = {'[', '$', 'l', '#', 'i', 0xF7};
std::vector<uint8_t> const v5 = {'[', '$', 'I', '#', '[', 'i', 0xF5, 'i', 0xF1, ']'};
std::vector<uint8_t> const v6 = {'[', '#', '[', 'i', 0xF3, 'i', 0x02, ']'};
@@ -3324,6 +3353,44 @@ TEST_CASE("BJData")
}
}
SECTION("ndarray of half-precision floats can be read")
{
// the 'h' element type is read as _ArrayType_ "half"
json _;
const std::vector<uint8_t> half = {'[', '$', 'h', '#', '[', '$', 'U', '#', 'U', 2, 2, 2, 0x00, 0x3C, 0x00, 0x40, 0x00, 0x42, 0x00, 0x44};
const json j_half = json::parse(R"({"_ArrayType_":"half","_ArraySize_":[2,2],"_ArrayData_":[1.0,2.0,3.0,4.0]})");
CHECK(json::from_bjdata(half) == j_half);
// the writer does not pack "half": it writes a plain object that reads back equal
CHECK(json::from_bjdata(json::to_bjdata(j_half)) == j_half);
CHECK(json::from_bjdata(json::to_bjdata(j_half, true, true, json::bjdata_version_t::draft3)) == j_half);
// truncated half data
CHECK_THROWS_AS(_ = json::from_bjdata(std::vector<uint8_t>(half.begin(), half.end() - 1)), json::parse_error&);
}
SECTION("ndarray of char rejects values above 127")
{
json _;
const std::vector<uint8_t> ok = {'[', '$', 'C', '#', '[', '$', 'U', '#', 'U', 2, 2, 2, 'a', 'b', 'c', 0x7F};
CHECK(json::from_bjdata(ok) == json::parse(R"({"_ArrayType_":"char","_ArraySize_":[2,2],"_ArrayData_":[97,98,99,127]})"));
std::vector<uint8_t> bad = ok;
bad.back() = 0xC8;
CHECK_THROWS_AS(_ = json::from_bjdata(bad), json::parse_error&);
CHECK(json::from_bjdata(bad, true, false).is_discarded());
// the same restriction holds for 1-D arrays
CHECK_THROWS_AS(_ = json::from_bjdata(std::vector<uint8_t>({'[', '$', 'C', '#', 'i', 1, 0xC8})), json::parse_error&);
// truncated input
CHECK_THROWS_AS(_ = json::from_bjdata(std::vector<uint8_t>(ok.begin(), ok.end() - 1)), json::parse_error&);
// 'B' (byte) elements may use the full range
const std::vector<uint8_t> byte_nd = {'[', '$', 'B', '#', '[', '$', 'U', '#', 'U', 2, 2, 2, 1, 2, 3, 0xC8};
CHECK(json::from_bjdata(byte_nd) == json::parse(R"({"_ArrayType_":"byte","_ArraySize_":[2,2],"_ArrayData_":[1,2,3,200]})"));
}
SECTION("do not accept NTFZ markers in ndarray optimized type (with count)")
{
json _;
@@ -3455,7 +3522,7 @@ TEST_CASE("BJData")
CHECK(json::from_bjdata(v0, true, false).is_discarded());
std::vector<uint8_t> const vi = {'[', '$', '#'};
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_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(json::from_bjdata(vi, true, false).is_discarded());
std::vector<uint8_t> const vU = {'[', '$', 'U'};
@@ -4622,3 +4689,60 @@ 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({}));
}
}
+249 -17
View File
@@ -802,12 +802,11 @@ TEST_CASE("BSON")
CHECK(json::from_bson(result, true, false) == j);
}
SECTION("binary member with subtype 0x02 (old binary) keeps its inner length prefix (lenient parsing)")
SECTION("binary member with subtype 0x02 (old binary) carries an inner length")
{
// documented lenient behavior (see gh-5333): the payload for
// binary subtype 0x02 ("old binary") is returned as-is,
// including its own inner 4-byte length prefix; it is not
// stripped or reinterpreted
// the payload of binary subtype 0x02 ("old binary") starts with an
// int32 that repeats the payload size; the bytes below are what
// pymongo and the bson-corpus (binary.json) use
std::vector<std::uint8_t> const input =
{
0x17, 0x00, 0x00, 0x00, // size (little endian)
@@ -815,16 +814,87 @@ TEST_CASE("BSON")
0x06, 0x00, 0x00, 0x00, // size of binary (little endian)
0x02, // "old binary" subtype
0x02, 0x00, 0x00, 0x00, // inner length prefix (part of the old-binary payload)
0x02, 0x00, 0x00, 0x00, // inner length
0x68, 0x69, // payload ('h', 'i')
0x00 // end marker
};
// the inner length prefix is part of the (unmodified) payload
const std::vector<std::uint8_t> expected_payload = {0x02, 0x00, 0x00, 0x00, 0x68, 0x69};
const json expected = { { "entry", json::binary(expected_payload, 0x02) } };
// the inner length is not part of the payload
const json expected = { { "entry", json::binary({0x68, 0x69}, 0x02) } };
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")
@@ -1178,7 +1248,7 @@ TEST_CASE("BSON document size mismatch")
SECTION("top-level document with a negative size")
{
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 5: syntax error while parsing BSON document: document size -1 does not match the number of bytes read (5)", json::parse_error&);
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(json::from_bson(input, true, false).is_discarded());
}
@@ -1190,7 +1260,7 @@ TEST_CASE("BSON document size mismatch")
// enclosing document instead of "d"
std::vector<std::uint8_t> const input =
{
0x00, 0x00, 0x00, 0x00, // outer size
0x14, 0x00, 0x00, 0x00, // outer size
0x03, 'd', 0x00, // entry: embedded document "d"
0xFF, 0xFF, 0xFF, 0x7F, // embedded size 0x7FFFFFFF
0x0A, 'a', 0x00, // entry: null "a"
@@ -1198,7 +1268,24 @@ TEST_CASE("BSON document size mismatch")
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 2147483647 does not match the number of bytes read (8)", json::parse_error&);
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(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());
}
@@ -1207,18 +1294,163 @@ TEST_CASE("BSON document size mismatch")
// array [42] is 12 bytes, but the length prefix claims 13
std::vector<std::uint8_t> const input =
{
0x00, 0x00, 0x00, 0x00, // outer size
0x14, 0x00, 0x00, 0x00, // outer size
0x04, 'a', 0x00, // entry: array "a"
0x0D, 0x00, 0x00, 0x00, // array size 13 (real is 12)
0x10, '0', 0x00, 0x2A, 0x00, 0x00, 0x00, // entry: int32 "0" = 42
0x00, // array end marker
0x00 // outer end marker
};
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_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(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")
{
// An embedded document or array used to be read by calling back into the
@@ -1923,7 +2155,7 @@ TEST_CASE("Invalid document size handling")
{
std::vector<std::uint8_t> const v = {0x04, 0x00, 0x00, 0x00, 0x00};
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 4 does not match the number of bytes read (5)", json::parse_error&);
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(json::from_bson(v, true, false).is_discarded());
}
@@ -1938,7 +2170,7 @@ TEST_CASE("Invalid document size handling")
0x00
};
json _;
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_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(json::from_bson(v, true, false).is_discarded());
}
@@ -1969,7 +2201,7 @@ TEST_CASE("Invalid document size handling")
0x00 // object terminator
};
json _;
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_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(json::from_bson(v, true, false).is_discarded());
}
+53 -1
View File
@@ -2023,7 +2023,7 @@ TEST_CASE("UBJSON")
CHECK(json::from_ubjson(v0, true, false).is_discarded());
std::vector<uint8_t> const vi = {'[', '$', '#'};
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_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(json::from_ubjson(vi, true, false).is_discarded());
std::vector<uint8_t> const vT = {'[', '$', 'T'};
@@ -3348,3 +3348,55 @@ 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());
}
}
}