From 0c3570d44a95f096b2c5131c0b952899084261c9 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 26 Oct 2026 18:25:45 +0100 Subject: [PATCH] Validate the UBJSON/BJData container type marker for count 0; fix UBJSON docs (#5929) * 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 * 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 * Regenerate single_include and apply astyle Result of make amalgamate (including make pretty) for the preceding changes. Signed-off-by: Niels Lohmann * 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 * 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 * Fix narrowing of the optimized-container count in BJData and UBJSON tests Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- .../mkdocs/docs/api/basic_json/from_ubjson.md | 15 +- .../docs/features/binary_formats/ubjson.md | 133 +++++++++++------- docs/mkdocs/docs/home/exceptions.md | 9 +- .../nlohmann/detail/input/binary_reader.hpp | 46 ++++++ single_include/nlohmann/json.hpp | 46 ++++++ tests/src/unit-bjdata.cpp | 61 +++++++- tests/src/unit-ubjson.cpp | 54 ++++++- 7 files changed, 304 insertions(+), 60 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/from_ubjson.md b/docs/mkdocs/docs/api/basic_json/from_ubjson.md index 3881f9ea4..ca947ef05 100644 --- a/docs/mkdocs/docs/api/basic_json/from_ubjson.md +++ b/docs/mkdocs/docs/api/basic_json/from_ubjson.md @@ -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 diff --git a/docs/mkdocs/docs/features/binary_formats/ubjson.md b/docs/mkdocs/docs/features/binary_formats/ubjson.md index ba12ee4f6..0e7ea2ca1 100644 --- a/docs/mkdocs/docs/features/binary_formats/ubjson.md +++ b/docs/mkdocs/docs/features/binary_formats/ubjson.md @@ -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): @@ -44,29 +44,29 @@ Consider the following JSON value (shown formatted for readability): The library uses the following mapping from JSON values types to UBJSON types according to the UBJSON specification: -| JSON value type | value/range | UBJSON type | marker | -|-----------------|-------------------------------------------|----------------|--------| -| null | `null` | null | `Z` | -| boolean | `true` | true | `T` | -| boolean | `false` | false | `F` | -| number_integer | -9223372036854775808..-2147483649 | int64 | `L` | -| number_integer | -2147483648..-32769 | int32 | `l` | -| number_integer | -32768..-129 | int16 | `I` | -| number_integer | -128..127 | int8 | `i` | -| number_integer | 128..255 | uint8 | `U` | -| number_integer | 256..32767 | int16 | `I` | -| number_integer | 32768..2147483647 | int32 | `l` | -| number_integer | 2147483648..9223372036854775807 | int64 | `L` | -| number_unsigned | 0..127 | int8 | `i` | -| number_unsigned | 128..255 | uint8 | `U` | -| number_unsigned | 256..32767 | int16 | `I` | -| 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` | -| string | *with shortest length indicator* | string | `S` | -| array | *see notes on optimized format* | array | `[` | -| object | *see notes on optimized format* | map | `{` | +| JSON value type | value/range | UBJSON type | marker | +|-----------------|-------------------------------------------|--------------------------------------------------|-----------| +| null | `null` | null | `Z` | +| boolean | `true` | true | `T` | +| boolean | `false` | false | `F` | +| number_integer | -9223372036854775808..-2147483649 | int64 | `L` | +| number_integer | -2147483648..-32769 | int32 | `l` | +| number_integer | -32768..-129 | int16 | `I` | +| number_integer | -128..127 | int8 | `i` | +| number_integer | 128..255 | uint8 | `U` | +| number_integer | 256..32767 | int16 | `I` | +| number_integer | 32768..2147483647 | int32 | `l` | +| number_integer | 2147483648..9223372036854775807 | int64 | `L` | +| number_unsigned | 0..127 | int8 | `i` | +| number_unsigned | 128..255 | uint8 | `U` | +| number_unsigned | 256..32767 | int16 | `I` | +| number_unsigned | 32768..2147483647 | int32 | `l` | +| number_unsigned | 2147483648..9223372036854775807 | int64 | `L` | +| number_unsigned | 9223372036854775808..18446744073709551615 | high-precision | `H` | +| 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* | 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" @@ -140,27 +148,54 @@ 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` | -| true | `true` | `T` | -| float32 | number_float | `d` | -| float64 | number_float | `D` | -| uint8 | number_unsigned | `U` | -| int8 | number_integer | `i` | -| int16 | number_integer | `I` | -| int32 | number_integer | `l` | -| int64 | number_integer | `L` | -| string | string | `S` | -| char | string | `C` | -| array | array (optimized values are supported) | `[` | -| object | object (optimized values are supported) | `{` | +| UBJSON type | JSON value type | marker | +|----------------|--------------------------------------------------|--------| +| no-op | *no value, next value is read* | `N` | +| null | `null` | `Z` | +| false | `false` | `F` | +| true | `true` | `T` | +| float32 | number_float | `d` | +| float64 | number_float | `D` | +| uint8 | number_unsigned | `U` | +| int8 | number_integer | `i` | +| int16 | number_integer | `I` | +| int32 | number_integer | `l` | +| 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" diff --git a/docs/mkdocs/docs/home/exceptions.md b/docs/mkdocs/docs/home/exceptions.md index aedfe504e..ddf9d69cd 100644 --- a/docs/mkdocs/docs/home/exceptions.md +++ b/docs/mkdocs/docs/home/exceptions.md @@ -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" @@ -1045,8 +1045,8 @@ or the encoded length for [CBOR](../features/binary_formats/cbor.md). The exception is also thrown for a [UBJSON](../features/binary_formats/ubjson.md) array of a type that is encoded by its 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" @@ -1236,7 +1236,8 @@ 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` for a value that contains a non-empty array, object, or binary value. UBJSON +requires a size marker (`#`) after a type marker (`$`); scalars and empty containers do not throw. !!! failure "Example message" diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index 929ac7573..4c8d6ed0f 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -3000,6 +3000,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 != '#')) { @@ -4496,6 +4504,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', '[', '{' diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 5db064c63..c4b7f159a 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -20467,6 +20467,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 != '#')) { @@ -21963,6 +21971,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', '[', '{' diff --git a/tests/src/unit-bjdata.cpp b/tests/src/unit-bjdata.cpp index 67e9e0a05..4a34125ba 100644 --- a/tests/src/unit-bjdata.cpp +++ b/tests/src/unit-bjdata.cpp @@ -3134,7 +3134,7 @@ TEST_CASE("BJData") std::vector const v1 = {'[', '#', 'i', 0xF1}; std::vector const v2 = {'[', '$', 'I', '#', 'i', 0xF2}; std::vector const v3 = {'[', '$', 'I', '#', '[', 'i', 0xF4, 'i', 0x02, ']'}; - std::vector const v4 = {'[', '$', 0xF6, '#', 'i', 0xF7}; + std::vector const v4 = {'[', '$', 'l', '#', 'i', 0xF7}; std::vector const v5 = {'[', '$', 'I', '#', '[', 'i', 0xF5, 'i', 0xF1, ']'}; std::vector const v6 = {'[', '#', '[', 'i', 0xF3, 'i', 0x02, ']'}; @@ -3455,7 +3455,7 @@ TEST_CASE("BJData") CHECK(json::from_bjdata(v0, true, false).is_discarded()); std::vector 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 const vU = {'[', '$', 'U'}; @@ -4622,3 +4622,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 v = {static_cast(open), '$', static_cast(marker), '#', 'i', static_cast(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 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 va = {'[', '$', static_cast(marker), '#', 'i', 0}; + const std::vector vo = {'{', '$', static_cast(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 vb = {'[', '$', 'B', '#', 'i', 0}; + CHECK(json::from_bjdata(vb) == json::binary({})); + } +} diff --git a/tests/src/unit-ubjson.cpp b/tests/src/unit-ubjson.cpp index 4cce74dff..5c6378216 100644 --- a/tests/src/unit-ubjson.cpp +++ b/tests/src/unit-ubjson.cpp @@ -2023,7 +2023,7 @@ TEST_CASE("UBJSON") CHECK(json::from_ubjson(v0, true, false).is_discarded()); std::vector 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 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 v = {static_cast(open), '$', static_cast(marker), '#', 'i', static_cast(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 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 va = {'[', '$', static_cast(marker), '#', 'i', 0}; + const std::vector vo = {'{', '$', static_cast(marker), '#', 'i', 0}; + CHECK(json::from_ubjson(va) == json::array()); + CHECK(json::from_ubjson(vo) == json::object()); + } + } +}