diff --git a/.github/labeler.yml b/.github/labeler.yml index b4c176960..5708f84f5 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -37,13 +37,13 @@ labels: files: - "include/nlohmann/detail/input/binary_reader\\.hpp" - "include/nlohmann/detail/output/binary_writer\\.hpp" - - "tests/src/unit-(bson|cbor|msgpack|ubjson|bjdata|binary_formats)" - - "tests/src/fuzzer-parse_(bson|cbor|msgpack|ubjson|bjdata)" + - "tests/src/unit-(bson|cbor|msgpack|ubjson|bjdata|bon8|binary_formats)" + - "tests/src/fuzzer-parse_(bson|cbor|msgpack|ubjson|bjdata|bon8)" - "docs/mkdocs/docs/features/binary_formats/" - - "docs/mkdocs/docs/(api/basic_json|examples)/(to|from)_(bson|cbor|msgpack|ubjson|bjdata)" + - "docs/mkdocs/docs/(api/basic_json|examples)/(to|from)_(bson|cbor|msgpack|ubjson|bjdata|bon8)" - label: "aspect: binary formats" - title: "(?i)(bson|cbor|msgpack|messagepack|ubjson|bjdata|binary format)" + title: "(?i)(bson|cbor|msgpack|messagepack|ubjson|bjdata|bon8|binary format)" - label: "python" files: diff --git a/Makefile b/Makefile index 871ea7995..196f87243 100644 --- a/Makefile +++ b/Makefile @@ -36,6 +36,7 @@ all: @echo "clean - remove built files" @echo "doctest - compile example files and check their output" @echo "fuzz_testing - prepare fuzz testing of the JSON parser" + @echo "fuzz_testing_bon8 - prepare fuzz testing of the BON8 parser" @echo "fuzz_testing_bson - prepare fuzz testing of the BSON parser" @echo "fuzz_testing_cbor - prepare fuzz testing of the CBOR parser" @echo "fuzz_testing_msgpack - prepare fuzz testing of the MessagePack parser" @@ -71,6 +72,14 @@ fuzz_testing: find tests/data/json_tests -size -5k -name *json | xargs -I{} cp "{}" fuzz-testing/testcases @echo "Execute: afl-fuzz -i fuzz-testing/testcases -o fuzz-testing/out fuzz-testing/fuzzer" +fuzz_testing_bon8: + rm -fr fuzz-testing + mkdir -p fuzz-testing fuzz-testing/testcases fuzz-testing/out + $(MAKE) parse_bon8_fuzzer -C tests CXX=afl-clang++ + mv tests/parse_bon8_fuzzer fuzz-testing/fuzzer + find tests/data -size -5k -name *.bon8 | xargs -I{} cp "{}" fuzz-testing/testcases + @echo "Execute: afl-fuzz -i fuzz-testing/testcases -o fuzz-testing/out fuzz-testing/fuzzer" + fuzz_testing_bson: rm -fr fuzz-testing mkdir -p fuzz-testing fuzz-testing/testcases fuzz-testing/out diff --git a/README.md b/README.md index 412d05fd4..cc3546686 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ - [Implicit conversions](#implicit-conversions) - [Conversions to/from arbitrary types](#arbitrary-types-conversions) - [Specializing enum conversion](#specializing-enum-conversion) - - [Binary formats (BSON, CBOR, MessagePack, UBJSON, and BJData)](#binary-formats-bson-cbor-messagepack-ubjson-and-bjdata) + - [Binary formats (BSON, CBOR, MessagePack, UBJSON, BJData, and BON8)](#binary-formats-bson-cbor-messagepack-ubjson-bjdata-and-bon8) - [Customers](#customers) - [Ecosystem](#ecosystem) - [Supported compilers](#supported-compilers) @@ -128,7 +128,7 @@ There is also a [**docset**](https://github.com/Kapeli/Dash-User-Contributions/t - **JSON Pointer functions**: [flatten](https://json.nlohmann.me/api/basic_json/flatten), [unflatten](https://json.nlohmann.me/api/basic_json/unflatten) - **JSON Patch functions**: [patch](https://json.nlohmann.me/api/basic_json/patch), [patch_inplace](https://json.nlohmann.me/api/basic_json/patch_inplace), [diff](https://json.nlohmann.me/api/basic_json/diff), [merge_patch](https://json.nlohmann.me/api/basic_json/merge_patch) - **Static functions**: [meta](https://json.nlohmann.me/api/basic_json/meta), [get_allocator](https://json.nlohmann.me/api/basic_json/get_allocator) -- **Binary formats**: [from_bjdata](https://json.nlohmann.me/api/basic_json/from_bjdata), [from_bson](https://json.nlohmann.me/api/basic_json/from_bson), [from_cbor](https://json.nlohmann.me/api/basic_json/from_cbor), [from_msgpack](https://json.nlohmann.me/api/basic_json/from_msgpack), [from_ubjson](https://json.nlohmann.me/api/basic_json/from_ubjson), [to_bjdata](https://json.nlohmann.me/api/basic_json/to_bjdata), [to_bson](https://json.nlohmann.me/api/basic_json/to_bson), [to_cbor](https://json.nlohmann.me/api/basic_json/to_cbor), [to_msgpack](https://json.nlohmann.me/api/basic_json/to_msgpack), [to_ubjson](https://json.nlohmann.me/api/basic_json/to_ubjson) +- **Binary formats**: [from_bjdata](https://json.nlohmann.me/api/basic_json/from_bjdata), [from_bon8](https://json.nlohmann.me/api/basic_json/from_bon8), [from_bson](https://json.nlohmann.me/api/basic_json/from_bson), [from_cbor](https://json.nlohmann.me/api/basic_json/from_cbor), [from_msgpack](https://json.nlohmann.me/api/basic_json/from_msgpack), [from_ubjson](https://json.nlohmann.me/api/basic_json/from_ubjson), [to_bjdata](https://json.nlohmann.me/api/basic_json/to_bjdata), [to_bon8](https://json.nlohmann.me/api/basic_json/to_bon8), [to_bson](https://json.nlohmann.me/api/basic_json/to_bson), [to_cbor](https://json.nlohmann.me/api/basic_json/to_cbor), [to_msgpack](https://json.nlohmann.me/api/basic_json/to_msgpack), [to_ubjson](https://json.nlohmann.me/api/basic_json/to_ubjson) - **Non-member functions**: [operator<<](https://json.nlohmann.me/api/operator_ltlt/), [operator>>](https://json.nlohmann.me/api/operator_gtgt/), [to_string](https://json.nlohmann.me/api/basic_json/to_string) - **Literals**: [operator""_json](https://json.nlohmann.me/api/operator_literal_json) - **Helper classes**: [std::hash<basic_json>](https://json.nlohmann.me/api/basic_json/std_hash), [std::swap<basic_json>](https://json.nlohmann.me/api/basic_json/std_swap) @@ -1110,9 +1110,9 @@ Other Important points: - When using `get()`, undefined JSON values will default to the first pair specified in your map. Select this default pair carefully. If you desire an exception in this circumstance use `NLOHMANN_JSON_SERIALIZE_ENUM_STRICT()` which behaves identically except for throwing an exception on unrecognized values. - If an enum or JSON value is specified more than once in your map, the first matching occurrence from the top of the map will be returned when converting to or from JSON. -### Binary formats (BSON, CBOR, MessagePack, UBJSON, and BJData) +### Binary formats (BSON, CBOR, MessagePack, UBJSON, BJData, and BON8) -Though JSON is a ubiquitous data format, it is not a very compact format suitable for data exchange, for instance over a network. Hence, the library supports [BSON](https://bsonspec.org) (Binary JSON), [CBOR](https://cbor.io) (Concise Binary Object Representation), [MessagePack](https://msgpack.org), [UBJSON](https://ubjson.org) (Universal Binary JSON Specification) and [BJData](https://neurojson.org/bjdata) (Binary JData) to efficiently encode JSON values to byte vectors and to decode such vectors. +Though JSON is a ubiquitous data format, it is not a very compact format suitable for data exchange, for instance over a network. Hence, the library supports [BSON](https://bsonspec.org) (Binary JSON), [CBOR](https://cbor.io) (Concise Binary Object Representation), [MessagePack](https://msgpack.org), [UBJSON](https://ubjson.org) (Universal Binary JSON Specification), [BJData](https://neurojson.org/bjdata) (Binary JData), and [BON8](https://github.com/hikoworks/hikogui/blob/main/docs/BON8.md) (Binary Object Notation 8) to efficiently encode JSON values to byte vectors and to decode such vectors. ```cpp // create a JSON value @@ -1149,6 +1149,14 @@ std::vector v_ubjson = json::to_ubjson(j); // roundtrip json j_from_ubjson = json::from_ubjson(v_ubjson); + +// serialize to BON8 +std::vector v_bon8 = json::to_bon8(j); + +// 0x88, 0x63, 0x6F, 0x6D, 0x70, 0x61, 0x63, 0x74, 0xF9, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x90 + +// roundtrip +json j_from_bon8 = json::from_bon8(v_bon8); ``` The library also supports binary types from BSON, CBOR (byte strings), and MessagePack (bin, ext, fixext). They are stored by default as `std::vector` to be processed outside the library. diff --git a/cmake/ci.cmake b/cmake/ci.cmake index 573a8e0e0..f7695c36e 100644 --- a/cmake/ci.cmake +++ b/cmake/ci.cmake @@ -542,7 +542,7 @@ add_custom_target(ci_infer add_custom_target(ci_offline_testdata COMMAND mkdir -p ${PROJECT_BINARY_DIR}/build_offline_testdata/test_data - COMMAND cd ${PROJECT_BINARY_DIR}/build_offline_testdata/test_data && ${GIT_TOOL} clone -c advice.detachedHead=false --branch v3.1.0 https://github.com/nlohmann/json_test_data.git --quiet --depth 1 + COMMAND cd ${PROJECT_BINARY_DIR}/build_offline_testdata/test_data && ${GIT_TOOL} clone -c advice.detachedHead=false --branch v3.2.0 https://github.com/nlohmann/json_test_data.git --quiet --depth 1 COMMAND ${CMAKE_COMMAND} -DCMAKE_BUILD_TYPE=Debug -GNinja -DJSON_BuildTests=ON -DJSON_FastTests=ON -DJSON_TestDataDirectory=${PROJECT_BINARY_DIR}/build_offline_testdata/test_data/json_test_data diff --git a/cmake/download_test_data.cmake b/cmake/download_test_data.cmake index 3b6f9a395..984c445be 100644 --- a/cmake/download_test_data.cmake +++ b/cmake/download_test_data.cmake @@ -1,5 +1,5 @@ set(JSON_TEST_DATA_URL https://github.com/nlohmann/json_test_data) -set(JSON_TEST_DATA_VERSION 3.1.0) +set(JSON_TEST_DATA_VERSION 3.2.0) include(ExternalProject) diff --git a/docs/docset/docSet.sql b/docs/docset/docSet.sql index 38b203d38..3a97e405f 100644 --- a/docs/docset/docSet.sql +++ b/docs/docset/docSet.sql @@ -48,6 +48,7 @@ INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::from_bjdata', 'Fu INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::from_bson', 'Function', 'api/basic_json/from_bson/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::from_cbor', 'Function', 'api/basic_json/from_cbor/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::from_msgpack', 'Function', 'api/basic_json/from_msgpack/index.html'); +INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::from_bon8', 'Function', 'api/basic_json/from_bon8/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::from_ubjson', 'Function', 'api/basic_json/from_ubjson/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::front', 'Method', 'api/basic_json/front/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::get', 'Method', 'api/basic_json/get/index.html'); @@ -121,6 +122,7 @@ INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::to_bjdata', 'Func INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::to_bson', 'Function', 'api/basic_json/to_bson/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::to_cbor', 'Function', 'api/basic_json/to_cbor/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::to_msgpack', 'Function', 'api/basic_json/to_msgpack/index.html'); +INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::to_bon8', 'Function', 'api/basic_json/to_bon8/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::to_string', 'Method', 'api/basic_json/to_string/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::to_ubjson', 'Function', 'api/basic_json/to_ubjson/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::value', 'Method', 'api/basic_json/value/index.html'); @@ -171,6 +173,7 @@ INSERT INTO searchIndex(name, type, path) VALUES ('Binary Formats: BJData', 'Gui INSERT INTO searchIndex(name, type, path) VALUES ('Binary Formats: BSON', 'Guide', 'features/binary_formats/bson/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('Binary Formats: CBOR', 'Guide', 'features/binary_formats/cbor/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('Binary Formats: MessagePack', 'Guide', 'features/binary_formats/messagepack/index.html'); +INSERT INTO searchIndex(name, type, path) VALUES ('Binary Formats: BON8', 'Guide', 'features/binary_formats/bon8/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('Binary Formats: UBJSON', 'Guide', 'features/binary_formats/ubjson/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('Binary Values', 'Guide', 'features/binary_values/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('Comments', 'Guide', 'features/comments/index.html'); diff --git a/docs/mkdocs/docs/api/basic_json/cbor_tag_handler_t.md b/docs/mkdocs/docs/api/basic_json/cbor_tag_handler_t.md index e19c3edd9..cea009e4e 100644 --- a/docs/mkdocs/docs/api/basic_json/cbor_tag_handler_t.md +++ b/docs/mkdocs/docs/api/basic_json/cbor_tag_handler_t.md @@ -18,7 +18,7 @@ ignore : ignore tags store -: store tagged values as binary container with subtype (for bytes 0xd8..0xdb) +: store tagged byte strings (for bytes 0xd8..0xdb) as binary values with the tag as subtype; other tagged values are read as if the tag were ignored. If several tags precede a byte string, only the innermost one is stored. ## Examples diff --git a/docs/mkdocs/docs/api/basic_json/from_bjdata.md b/docs/mkdocs/docs/api/basic_json/from_bjdata.md index ac60bb58b..9df21ab30 100644 --- a/docs/mkdocs/docs/api/basic_json/from_bjdata.md +++ b/docs/mkdocs/docs/api/basic_json/from_bjdata.md @@ -104,6 +104,7 @@ Linear in the size of the input. - [from_msgpack](from_msgpack.md) create a JSON value from an input in MessagePack format - [from_bson](from_bson.md) create a JSON value from an input in BSON format - [from_ubjson](from_ubjson.md) create a JSON value from an input in UBJSON format +- [from_bon8](from_bon8.md) create a JSON value from an input in BON8 format ## Version history diff --git a/docs/mkdocs/docs/api/basic_json/from_bon8.md b/docs/mkdocs/docs/api/basic_json/from_bon8.md new file mode 100644 index 000000000..2f9d1e749 --- /dev/null +++ b/docs/mkdocs/docs/api/basic_json/from_bon8.md @@ -0,0 +1,108 @@ +# nlohmann::basic_json::from_bon8 + +```cpp +// (1) +template +static basic_json from_bon8(InputType&& i, + const bool strict = true, + const bool allow_exceptions = true); +// (2) +template +static basic_json from_bon8(IteratorType first, SentinelType last, + const bool strict = true, + const bool allow_exceptions = true); +``` + +Deserializes a given input to a JSON value using the BON8 (Binary Object Notation 8) serialization format. + +1. Reads from a compatible input. +2. Reads from an iterator range, or an iterator and a sentinel of a different type (C++20 ranges support). + +The exact mapping and its limitations are described on a [dedicated page](../../features/binary_formats/bon8.md). + +## Template parameters + +`InputType` +: A compatible input, for instance: + + - an `std::istream` object + - a `FILE` pointer + - a C-style array of characters + - a pointer to a null-terminated string of single byte characters + - a container `obj` for which `begin(obj)` and `end(obj)` produce a valid pair of iterators + (as found via ADL or member functions, with semantics compatible to `std::begin` and `std::end`) + +`IteratorType` +: a compatible iterator type + +`SentinelType` +: defaults to `IteratorType`; may be a different type comparable to `IteratorType` via `operator!=`, for instance. + + - a custom sentinel type for C++20 ranges + - `std::default_sentinel_t`, when `IteratorType` is `std::counted_iterator` + +## Parameters + +`i` (in) +: an input in BON8 format convertible to an input adapter + +`first` (in) +: iterator to the start of the input + +`last` (in) +: iterator to the end of the input, or a sentinel value that compares equal to the end iterator with `operator!=` + +`strict` (in) +: whether to expect the input to be consumed until EOF (`#!cpp true` by default) + +`allow_exceptions` (in) +: whether to throw exceptions in case of a parse error (optional, `#!cpp true` by default) + +## Return value + +deserialized JSON value; in case of a parse error and `allow_exceptions` set to `#!cpp false`, the return value will be +`value_t::discarded`. The latter can be checked with [`is_discarded`](is_discarded.md). + +## Exception safety + +Strong guarantee: if an exception is thrown, there are no changes in the JSON value. + +## Exceptions + +- 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, for instance + an invalid byte, a string that is not valid UTF-8, or an object key that is not a string + +## Complexity + +Linear in the size of the input. + +## Examples + +??? example + + The example shows the deserialization of a byte vector in BON8 format to a JSON value. + + ```cpp + --8<-- "examples/from_bon8.cpp" + ``` + + Output: + + ```json + --8<-- "examples/from_bon8.output" + ``` + +## See also + +- [to_bon8](to_bon8.md) create a BON8 serialization of a JSON value +- [from_cbor](from_cbor.md) create a JSON value from an input in CBOR format +- [from_msgpack](from_msgpack.md) create a JSON value from an input in MessagePack format +- [from_bson](from_bson.md) create a JSON value from an input in BSON format +- [from_ubjson](from_ubjson.md) create a JSON value from an input in UBJSON format +- [from_bjdata](from_bjdata.md) create a JSON value from an input in BJData format + +## Version history + +- Added in version 3.13.0. diff --git a/docs/mkdocs/docs/api/basic_json/from_bson.md b/docs/mkdocs/docs/api/basic_json/from_bson.md index ce093a0d8..cf022aeb6 100644 --- a/docs/mkdocs/docs/api/basic_json/from_bson.md +++ b/docs/mkdocs/docs/api/basic_json/from_bson.md @@ -104,6 +104,7 @@ Linear in the size of the input. - [from_msgpack](from_msgpack.md) for the related MessagePack format - [from_ubjson](from_ubjson.md) for the related UBJSON format - [from_bjdata](from_bjdata.md) for the related BJData format +- [from_bon8](from_bon8.md) for the related BON8 format ## Version history diff --git a/docs/mkdocs/docs/api/basic_json/from_cbor.md b/docs/mkdocs/docs/api/basic_json/from_cbor.md index fb28edd3b..b72f55280 100644 --- a/docs/mkdocs/docs/api/basic_json/from_cbor.md +++ b/docs/mkdocs/docs/api/basic_json/from_cbor.md @@ -110,6 +110,7 @@ Linear in the size of the input. - [from_bson](from_bson.md) create a JSON value from an input in BSON format - [from_ubjson](from_ubjson.md) create a JSON value from an input in UBJSON format - [from_bjdata](from_bjdata.md) create a JSON value from an input in BJData format +- [from_bon8](from_bon8.md) create a JSON value from an input in BON8 format ## Version history diff --git a/docs/mkdocs/docs/api/basic_json/from_msgpack.md b/docs/mkdocs/docs/api/basic_json/from_msgpack.md index 12834e2c3..2f4b7bb3b 100644 --- a/docs/mkdocs/docs/api/basic_json/from_msgpack.md +++ b/docs/mkdocs/docs/api/basic_json/from_msgpack.md @@ -103,6 +103,7 @@ Linear in the size of the input. - [from_bson](from_bson.md) create a JSON value from an input in BSON format - [from_ubjson](from_ubjson.md) create a JSON value from an input in UBJSON format - [from_bjdata](from_bjdata.md) create a JSON value from an input in BJData format +- [from_bon8](from_bon8.md) create a JSON value from an input in BON8 format ## Version history diff --git a/docs/mkdocs/docs/api/basic_json/from_ubjson.md b/docs/mkdocs/docs/api/basic_json/from_ubjson.md index a0f8944aa..0d060c750 100644 --- a/docs/mkdocs/docs/api/basic_json/from_ubjson.md +++ b/docs/mkdocs/docs/api/basic_json/from_ubjson.md @@ -104,6 +104,7 @@ Linear in the size of the input. - [from_msgpack](from_msgpack.md) create a JSON value from an input in MessagePack format - [from_bson](from_bson.md) create a JSON value from an input in BSON format - [from_bjdata](from_bjdata.md) create a JSON value from an input in BJData format +- [from_bon8](from_bon8.md) create a JSON value from an input in BON8 format ## Version history diff --git a/docs/mkdocs/docs/api/basic_json/index.md b/docs/mkdocs/docs/api/basic_json/index.md index c5556ea3b..866bda67a 100644 --- a/docs/mkdocs/docs/api/basic_json/index.md +++ b/docs/mkdocs/docs/api/basic_json/index.md @@ -290,11 +290,13 @@ Access to the JSON value ### Binary formats - [**from_bjdata**](from_bjdata.md) (_static_) - create a JSON value from an input in BJData format +- [**from_bon8**](from_bon8.md) (_static_) - create a JSON value from an input in BON8 format - [**from_bson**](from_bson.md) (_static_) - create a JSON value from an input in BSON format - [**from_cbor**](from_cbor.md) (_static_) - create a JSON value from an input in CBOR format - [**from_msgpack**](from_msgpack.md) (_static_) - create a JSON value from an input in MessagePack format - [**from_ubjson**](from_ubjson.md) (_static_) - create a JSON value from an input in UBJSON format - [**to_bjdata**](to_bjdata.md) (_static_) - create a BJData serialization of a given JSON value +- [**to_bon8**](to_bon8.md) (_static_) - create a BON8 serialization of a given JSON value - [**to_bson**](to_bson.md) (_static_) - create a BSON serialization of a given JSON value - [**to_cbor**](to_cbor.md) (_static_) - create a CBOR serialization of a given JSON value - [**to_msgpack**](to_msgpack.md) (_static_) - create a MessagePack serialization of a given JSON value diff --git a/docs/mkdocs/docs/api/basic_json/input_format_t.md b/docs/mkdocs/docs/api/basic_json/input_format_t.md index a3baabab8..407b43427 100644 --- a/docs/mkdocs/docs/api/basic_json/input_format_t.md +++ b/docs/mkdocs/docs/api/basic_json/input_format_t.md @@ -7,7 +7,8 @@ enum class input_format_t { msgpack, ubjson, bson, - bjdata + bjdata, + bon8 }; ``` @@ -31,6 +32,9 @@ bson bjdata : BJData (Binary JData) +bon8 +: BON8 (Binary Object Notation 8) + ## Examples ??? example diff --git a/docs/mkdocs/docs/api/basic_json/parse_error.md b/docs/mkdocs/docs/api/basic_json/parse_error.md index 9931d5463..74e16f41a 100644 --- a/docs/mkdocs/docs/api/basic_json/parse_error.md +++ b/docs/mkdocs/docs/api/basic_json/parse_error.md @@ -5,7 +5,7 @@ class parse_error : public exception; ``` The library throws this exception when a parse error occurs. Parse errors can occur during the deserialization of -JSON text, BSON, CBOR, MessagePack, UBJSON, as well as when using JSON Patch. +JSON text, BJData, BON8, BSON, CBOR, MessagePack, UBJSON, as well as when using JSON Patch. Member `byte` holds the byte index of the last read character in the input file (see note below). diff --git a/docs/mkdocs/docs/api/basic_json/sax_parse.md b/docs/mkdocs/docs/api/basic_json/sax_parse.md index bf61ea9eb..d6ce688a7 100644 --- a/docs/mkdocs/docs/api/basic_json/sax_parse.md +++ b/docs/mkdocs/docs/api/basic_json/sax_parse.md @@ -65,7 +65,8 @@ The SAX event lister must follow the interface of [`json_sax`](../json_sax/index : SAX event listener (must not be null) `format` (in) -: the format to parse (JSON, CBOR, MessagePack, or UBJSON) (optional, `input_format_t::json` by default), see +: the format to parse (JSON, BJData, BON8, BSON, CBOR, MessagePack, or UBJSON) (optional, `input_format_t::json` by + default), see [`input_format_t`](input_format_t.md) for more information `strict` (in) diff --git a/docs/mkdocs/docs/api/basic_json/to_bjdata.md b/docs/mkdocs/docs/api/basic_json/to_bjdata.md index 2cf936a51..b066e6852 100644 --- a/docs/mkdocs/docs/api/basic_json/to_bjdata.md +++ b/docs/mkdocs/docs/api/basic_json/to_bjdata.md @@ -84,6 +84,7 @@ Linear in the size of the JSON value `j`. - [to_msgpack](to_msgpack.md) create a MessagePack serialization of a JSON value - [to_bson](to_bson.md) create a BSON serialization of a JSON value - [to_ubjson](to_ubjson.md) create a UBJSON serialization of a JSON value +- [to_bon8](to_bon8.md) create a BON8 serialization of a JSON value ## Version history diff --git a/docs/mkdocs/docs/api/basic_json/to_bon8.md b/docs/mkdocs/docs/api/basic_json/to_bon8.md new file mode 100644 index 000000000..26d552e8a --- /dev/null +++ b/docs/mkdocs/docs/api/basic_json/to_bon8.md @@ -0,0 +1,76 @@ +# nlohmann::basic_json::to_bon8 + +```cpp +// (1) +static std::vector to_bon8(const basic_json& j); + +// (2) +static void to_bon8(const basic_json& j, detail::output_adapter o); +static void to_bon8(const basic_json& j, detail::output_adapter o); +``` + +Serializes a given JSON value `j` to a byte vector using the BON8 (Binary Object Notation 8) serialization format. BON8 +is a compact binary serialization format that stores strings as UTF-8 without a length prefix. + +1. Returns a byte vector containing the BON8 serialization. +2. Writes the BON8 serialization to an output adapter. + +The exact mapping and its limitations are described on a [dedicated page](../../features/binary_formats/bon8.md). + +## Parameters + +`j` (in) +: JSON value to serialize + +`o` (in) +: output adapter to write serialization to + +## Return value + +1. BON8 serialization as a byte vector +2. (none) + +## Exception safety + +Strong guarantee: if an exception is thrown, there are no changes in the JSON value `j`, which is never modified. +With (2), the bytes written before the exception remain in the output adapter. + +## Exceptions + +- Throws [out_of_range.407](../../home/exceptions.md#jsonexceptionout_of_range407) if `j` contains an unsigned integer + above 9223372036854775807, which BON8 cannot represent +- Throws [type_error.316](../../home/exceptions.md#jsonexceptiontype_error316) if `j` contains a string that is not + valid UTF-8 + +## Complexity + +Linear in the size of the JSON value `j`. + +## Examples + +??? example + + The example shows the serialization of a JSON value to a byte vector in BON8 format. + + ```cpp + --8<-- "examples/to_bon8.cpp" + ``` + + Output: + + ```json + --8<-- "examples/to_bon8.output" + ``` + +## See also + +- [from_bon8](from_bon8.md) create a JSON value from an input in BON8 format +- [to_cbor](to_cbor.md) create a CBOR serialization of a JSON value +- [to_msgpack](to_msgpack.md) create a MessagePack serialization of a JSON value +- [to_bson](to_bson.md) create a BSON serialization of a JSON value +- [to_ubjson](to_ubjson.md) create a UBJSON serialization of a JSON value +- [to_bjdata](to_bjdata.md) create a BJData serialization of a JSON value + +## Version history + +- Added in version 3.13.0. diff --git a/docs/mkdocs/docs/api/basic_json/to_bson.md b/docs/mkdocs/docs/api/basic_json/to_bson.md index 4cd45a57d..fb02c51e1 100644 --- a/docs/mkdocs/docs/api/basic_json/to_bson.md +++ b/docs/mkdocs/docs/api/basic_json/to_bson.md @@ -72,6 +72,7 @@ pass before anything is written. - [to_msgpack](to_msgpack.md) create a MessagePack serialization of a JSON value - [to_ubjson](to_ubjson.md) create a UBJSON serialization of a JSON value - [to_bjdata](to_bjdata.md) create a BJData serialization of a JSON value +- [to_bon8](to_bon8.md) create a BON8 serialization of a JSON value ## Version history diff --git a/docs/mkdocs/docs/api/basic_json/to_cbor.md b/docs/mkdocs/docs/api/basic_json/to_cbor.md index 93facfca0..3bbd9c7d3 100644 --- a/docs/mkdocs/docs/api/basic_json/to_cbor.md +++ b/docs/mkdocs/docs/api/basic_json/to_cbor.md @@ -62,6 +62,7 @@ Linear in the size of the JSON value `j`. - [to_bson](to_bson.md) create a BSON serialization of a JSON value - [to_ubjson](to_ubjson.md) create a UBJSON serialization of a JSON value - [to_bjdata](to_bjdata.md) create a BJData serialization of a JSON value +- [to_bon8](to_bon8.md) create a BON8 serialization of a JSON value ## Version history diff --git a/docs/mkdocs/docs/api/basic_json/to_msgpack.md b/docs/mkdocs/docs/api/basic_json/to_msgpack.md index 66b104f52..007fb1914 100644 --- a/docs/mkdocs/docs/api/basic_json/to_msgpack.md +++ b/docs/mkdocs/docs/api/basic_json/to_msgpack.md @@ -34,6 +34,15 @@ The exact mapping and its limitations are described on a [dedicated page](../../ Strong guarantee: if an exception is thrown, there are no changes in the JSON value. +## Exceptions + +- Throws [`out_of_range.412`](../../home/exceptions.md#jsonexceptionout_of_range412) if the length of a string, binary + 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: + `"subtype 70000 is too large for the MessagePack ext type (max 255)"` + ## Complexity Linear in the size of the JSON value `j`. @@ -61,7 +70,9 @@ Linear in the size of the JSON value `j`. - [to_bson](to_bson.md) create a BSON serialization of a JSON value - [to_ubjson](to_ubjson.md) create a UBJSON serialization of a JSON value - [to_bjdata](to_bjdata.md) create a BJData serialization of a JSON value +- [to_bon8](to_bon8.md) create a BON8 serialization of a JSON value ## Version history - Added in version 2.0.9. +- Throws `out_of_range.412` and `out_of_range.415` since version 3.13.0. diff --git a/docs/mkdocs/docs/api/basic_json/to_ubjson.md b/docs/mkdocs/docs/api/basic_json/to_ubjson.md index 694fbbba6..6437ba3e5 100644 --- a/docs/mkdocs/docs/api/basic_json/to_ubjson.md +++ b/docs/mkdocs/docs/api/basic_json/to_ubjson.md @@ -77,6 +77,7 @@ Linear in the size of the JSON value `j`. - [to_msgpack](to_msgpack.md) create a MessagePack serialization of a JSON value - [to_bson](to_bson.md) create a BSON serialization of a JSON value - [to_bjdata](to_bjdata.md) create a BJData serialization of a JSON value +- [to_bon8](to_bon8.md) create a BON8 serialization of a JSON value ## Version history diff --git a/docs/mkdocs/docs/api/macros/json_strict_nul_handling.md b/docs/mkdocs/docs/api/macros/json_strict_nul_handling.md index a50ce2645..f22395d3b 100644 --- a/docs/mkdocs/docs/api/macros/json_strict_nul_handling.md +++ b/docs/mkdocs/docs/api/macros/json_strict_nul_handling.md @@ -11,9 +11,9 @@ The macro only affects the JSON text parser ([`parse`](../basic_json/parse.md), [`sax_parse`](../basic_json/sax_parse.md), and [`operator>>`](../operator_gtgt.md)). There are three cases where a NUL byte is still not rejected: -- The binary formats ([`from_bjdata`](../basic_json/from_bjdata.md), [`from_bson`](../basic_json/from_bson.md), - [`from_cbor`](../basic_json/from_cbor.md), [`from_msgpack`](../basic_json/from_msgpack.md), - [`from_ubjson`](../basic_json/from_ubjson.md)) are never affected: there, `0x00` is ordinary data. +- The binary formats ([`from_bjdata`](../basic_json/from_bjdata.md), [`from_bon8`](../basic_json/from_bon8.md), + [`from_bson`](../basic_json/from_bson.md), [`from_cbor`](../basic_json/from_cbor.md), + [`from_msgpack`](../basic_json/from_msgpack.md), [`from_ubjson`](../basic_json/from_ubjson.md)) are never affected: there, `0x00` is ordinary data. - A bare `const char*` pointer has no length of its own, so its length is still determined with `strlen()`. The first NUL byte therefore still marks the end of the input, and nothing after it is read. - One trailing `'\0'` at the end of a `char` array (e.g., a string literal) is trimmed; see the warning below. diff --git a/docs/mkdocs/docs/examples/from_bon8.cpp b/docs/mkdocs/docs/examples/from_bon8.cpp new file mode 100644 index 000000000..83ab82060 --- /dev/null +++ b/docs/mkdocs/docs/examples/from_bon8.cpp @@ -0,0 +1,21 @@ +#include +#include +#include + +using json = nlohmann::json; + +int main() +{ + // create byte vector + std::vector v = {0x89, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, + 0xf9, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0xff, + 0x42, 0x4f, 0x4e, 0x38, 0xff, 0x73, 0x63, 0x68, + 0x65, 0x6d, 0x61, 0x90 + }; + + // deserialize it with BON8 + json j = json::from_bon8(v); + + // print the deserialized JSON value + std::cout << std::setw(2) << j << std::endl; +} diff --git a/docs/mkdocs/docs/examples/from_bon8.output b/docs/mkdocs/docs/examples/from_bon8.output new file mode 100644 index 000000000..78c64c2b4 --- /dev/null +++ b/docs/mkdocs/docs/examples/from_bon8.output @@ -0,0 +1,5 @@ +{ + "compact": true, + "format": "BON8", + "schema": 0 +} diff --git a/docs/mkdocs/docs/examples/to_bon8.cpp b/docs/mkdocs/docs/examples/to_bon8.cpp new file mode 100644 index 000000000..a40130903 --- /dev/null +++ b/docs/mkdocs/docs/examples/to_bon8.cpp @@ -0,0 +1,22 @@ +#include +#include +#include + +using json = nlohmann::json; +using namespace nlohmann::literals; + +int main() +{ + // create a JSON value + json j = R"({"compact": true, "format": "BON8", "schema": 0})"_json; + + // serialize it to BON8 + std::vector v = json::to_bon8(j); + + // print the vector content + for (auto& byte : v) + { + std::cout << "0x" << std::hex << std::setw(2) << std::setfill('0') << (int)byte << " "; + } + std::cout << std::endl; +} diff --git a/docs/mkdocs/docs/examples/to_bon8.output b/docs/mkdocs/docs/examples/to_bon8.output new file mode 100644 index 000000000..05481d51f --- /dev/null +++ b/docs/mkdocs/docs/examples/to_bon8.output @@ -0,0 +1 @@ +0x89 0x63 0x6f 0x6d 0x70 0x61 0x63 0x74 0xf9 0x66 0x6f 0x72 0x6d 0x61 0x74 0xff 0x42 0x4f 0x4e 0x38 0xff 0x73 0x63 0x68 0x65 0x6d 0x61 0x90 diff --git a/docs/mkdocs/docs/features/binary_formats/bon8.md b/docs/mkdocs/docs/features/binary_formats/bon8.md new file mode 100644 index 000000000..9b5fa1dda --- /dev/null +++ b/docs/mkdocs/docs/features/binary_formats/bon8.md @@ -0,0 +1,159 @@ +# BON8 + +BON8 (Binary Object Notation 8) is a compact binary serialization format for JSON values. It uses the byte values that +cannot begin a UTF-8 character as type markers, so strings are stored as plain UTF-8 without a length prefix: a string +ends at the first byte that cannot continue it. Integers from -10 to 39, `true`, `false`, `null`, and the floating-point +values -1.0, 0.0, and 1.0 take a single byte, and arrays and objects with up to four elements need no terminator. + +!!! abstract "References" + + - [BON8 specification](https://github.com/hikoworks/hikogui/blob/main/docs/BON8.md) + - [Reference implementation](https://github.com/hikoworks/hikogui/blob/main/src/hikogui/codec/BON8.hpp) in HikoGUI + +## Serialization + +The library uses the following mapping from JSON values types to BON8 types according to the BON8 specification: + +| JSON value type | value/range | BON8 type | first byte | +|-----------------|----------------------------------------------|-------------------------------|------------| +| null | `null` | null | 0xFA | +| boolean | `true` | true | 0xF9 | +| boolean | `false` | false | 0xF8 | +| number_integer | -9223372036854775808..-2147483649 | int64 | 0x8D | +| number_integer | -2147483648..-33818507 | int32 | 0x8C | +| number_integer | -33818506..-264075 | 4-byte negative integer | 0xF0..0xF7 | +| number_integer | -264074..-1931 | 3-byte negative integer | 0xE0..0xEF | +| number_integer | -1930..-11 | 2-byte negative integer | 0xC2..0xDF | +| number_integer | -10..-1 | 1-byte negative integer | 0xB8..0xC1 | +| number_integer | 0..39 | 1-byte positive integer | 0x90..0xB7 | +| number_integer | 40..3879 | 2-byte positive integer | 0xC2..0xDF | +| number_integer | 3880..528167 | 3-byte positive integer | 0xE0..0xEF | +| number_integer | 528168..67637031 | 4-byte positive integer | 0xF0..0xF7 | +| number_integer | 67637032..2147483647 | int32 | 0x8C | +| number_integer | 2147483648..9223372036854775807 | int64 | 0x8D | +| number_unsigned | 0..39 | 1-byte positive integer | 0x90..0xB7 | +| number_unsigned | 40..3879 | 2-byte positive integer | 0xC2..0xDF | +| number_unsigned | 3880..528167 | 3-byte positive integer | 0xE0..0xEF | +| number_unsigned | 528168..67637031 | 4-byte positive integer | 0xF0..0xF7 | +| number_unsigned | 67637032..2147483647 | int32 | 0x8C | +| number_unsigned | 2147483648..9223372036854775807 | int64 | 0x8D | +| number_float | `-1.0` | -1.0 | 0xFB | +| number_float | `0.0` | 0.0 | 0xFC | +| number_float | `1.0` | 1.0 | 0xFD | +| number_float | *any other value representable by a float* | binary32 | 0x8E | +| number_float | *any value NOT representable by a float* | binary64 | 0x8F | +| string | *empty* | end of string | 0xFF | +| string | *non-empty* | UTF-8 string | 0x00..0x7F, 0xC2..0xF4 | +| array | *size*: 0..4 | array with count | 0x80..0x84 | +| array | *size*: 5 or more | array (terminated by 0xFE) | 0x85 | +| object | *size*: 0..4 | object with count | 0x86..0x8A | +| object | *size*: 5 or more | object (terminated by 0xFE) | 0x8B | +| binary | *size*: 0..4 | array with count | 0x80..0x84 | +| binary | *size*: 5 or more | array (terminated by 0xFE) | 0x85 | + +An integer that takes 2 to 4 bytes starts with a UTF-8 lead byte (0xC2..0xF7) that is followed by a byte that cannot +continue a UTF-8 character: 0x00..0x7F for positive and 0xC0..0xFF for negative integers. A string is terminated by +0xFF only if it is empty, if another string follows it, or if it is the last value of the message; otherwise, the first +byte of the next value ends it. + +!!! success "Complete mapping" + + Except for the values listed below, any JSON value can be converted to a BON8 value. + + Any BON8 output created by `to_bon8` can be successfully parsed by `from_bon8`. + +!!! warning "Unsupported values" + + The following values can **not** be converted to a BON8 value: + + - unsigned integers above 9223372036854775807, because BON8 has no unsigned 64-bit integer type + ([out_of_range.407](../../home/exceptions.md#jsonexceptionout_of_range407)) + - strings that are not valid UTF-8, because the end of a string is determined from its encoding + ([type_error.316](../../home/exceptions.md#jsonexceptiontype_error316)) + +!!! info "NaN/infinity handling" + + `-0.0`, `Infinity`, and `-Infinity` are serialized as binary32 (type 0x8E, 5 bytes total). `NaN` is serialized as + the binary32 value 0x7F800001 that the specification recommends. This is in contrast to the + [dump](../../api/basic_json/dump.md) function which serializes NaN or Infinity to `null`. + +!!! warning "Binary values" + + BON8 has no binary type. Binary values are serialized as arrays of integers (0..255), so they are read back as + arrays. The subtype is not serialized. + +!!! info "Canonical representation" + + The output follows the specification's canonical representation rules: every value uses the shortest encoding, + floating-point numbers use binary32 whenever that loses no precision, and object keys are sorted by their UTF-8 + code units. There are two exceptions: + + - Strings are not normalized to Unicode Normalization Form C (NFC). + - Object keys are written in the order of the object type, which is sorted for `json`, but not for + [`ordered_json`](../../api/ordered_json.md). + +??? example + + ```cpp + --8<-- "examples/to_bon8.cpp" + ``` + + Output: + + ```c + --8<-- "examples/to_bon8.output" + ``` + +## Deserialization + +The library maps BON8 types to JSON value types as follows: + +| BON8 type | JSON value type | first byte | +|-------------------------------|-----------------|------------------------| +| UTF-8 string | string | 0x00..0x7F | +| array with count | array | 0x80..0x84 | +| array (terminated by 0xFE) | array | 0x85 | +| object with count | object | 0x86..0x8A | +| object (terminated by 0xFE) | object | 0x8B | +| int32 | number_unsigned or number_integer | 0x8C | +| int64 | number_unsigned or number_integer | 0x8D | +| binary32 | number_float | 0x8E | +| binary64 | number_float | 0x8F | +| 1-byte positive integer | number_unsigned | 0x90..0xB7 | +| 1-byte negative integer | number_integer | 0xB8..0xC1 | +| UTF-8 string | string | 0xC2..0xF4, followed by 0x80..0xBF | +| 2- to 4-byte positive integer | number_unsigned | 0xC2..0xF7, followed by 0x00..0x7F | +| 2- to 4-byte negative integer | number_integer | 0xC2..0xF7, followed by 0xC0..0xFF | +| false | `false` | 0xF8 | +| true | `true` | 0xF9 | +| null | `null` | 0xFA | +| -1.0 | number_float | 0xFB | +| 0.0 | number_float | 0xFC | +| 1.0 | number_float | 0xFD | +| empty string | string | 0xFF | + +Non-negative integers are read as number_unsigned, negative integers as number_integer. + +!!! info + + Values that do not use the canonical representation, such as integers with a longer encoding than necessary, + arrays and objects with up to four elements that are terminated by 0xFE, unsorted object keys, or a 0xFF after a + string that would also end without it, are accepted. A second 0xFF is not a terminator but an empty string. + + Strings must be valid UTF-8, and the last string of a message must be terminated by 0xFF. + +!!! info + + Any BON8 output created by `to_bon8` can be successfully parsed by `from_bon8`. + +??? example + + ```cpp + --8<-- "examples/from_bon8.cpp" + ``` + + Output: + + ```json + --8<-- "examples/from_bon8.output" + ``` diff --git a/docs/mkdocs/docs/features/binary_formats/cbor.md b/docs/mkdocs/docs/features/binary_formats/cbor.md index e4c257e27..8e6acf0fb 100644 --- a/docs/mkdocs/docs/features/binary_formats/cbor.md +++ b/docs/mkdocs/docs/features/binary_formats/cbor.md @@ -188,7 +188,7 @@ The library maps CBOR types to JSON value types as follows: !!! warning "Tagged items" - Tagged items (0xC0..0xDB) will throw a parse error by default. They can be ignored by passing `cbor_tag_handler_t::ignore` to function `from_cbor`, in which case the tag is skipped and the enclosed data item is parsed on its own. They can be stored by passing `cbor_tag_handler_t::store` to function `from_cbor`. Note that no tag is ever interpreted: for instance, a text string tagged with tag 0 (date/time) stays a string. + Tagged items (0xC0..0xDB) will throw a parse error by default. They can be ignored by passing `cbor_tag_handler_t::ignore` to function `from_cbor`, in which case the tag is skipped and the enclosed data item is parsed on its own. Passing `cbor_tag_handler_t::store` to function `from_cbor` stores tagged byte strings (for bytes 0xd8..0xdb) as binary values with the tag as subtype; other tagged values are read as if the tag were ignored. If several tags precede a byte string, only the innermost one is stored. Note that no tag is ever interpreted: for instance, a text string tagged with tag 0 (date/time) stays a string. ??? example diff --git a/docs/mkdocs/docs/features/binary_formats/index.md b/docs/mkdocs/docs/features/binary_formats/index.md index ef79e2ef3..2c45995a6 100644 --- a/docs/mkdocs/docs/features/binary_formats/index.md +++ b/docs/mkdocs/docs/features/binary_formats/index.md @@ -4,6 +4,7 @@ Though JSON is a ubiquitous data format, it is not a very compact format suitabl a network. Hence, the library supports - [BJData](bjdata.md) (Binary JData), +- [BON8](bon8.md) (Binary Object Notation 8), - [BSON](bson.md) (Binary JSON), - [CBOR](cbor.md) (Concise Binary Object Representation), - [MessagePack](messagepack.md), and @@ -18,6 +19,7 @@ to efficiently encode JSON values to byte vectors and to decode such vectors. | Format | Serialization | Deserialization | |-------------|-----------------------------------------------|----------------------------------------------| | BJData | complete | complete | +| BON8 | incomplete: no unsigned integers above int64 | complete | | BSON | incomplete: top-level value must be an object | incomplete, but all JSON types are supported | | CBOR | complete | incomplete, but all JSON types are supported | | MessagePack | complete | complete | @@ -28,6 +30,7 @@ to efficiently encode JSON values to byte vectors and to decode such vectors. | Format | Binary values | Binary subtypes | |-------------|---------------|-----------------| | BJData | not supported | not supported | +| BON8 | not supported | not supported | | BSON | supported | supported | | CBOR | supported | supported | | MessagePack | supported | supported | @@ -42,6 +45,7 @@ See [binary values](../binary_values.md) for more information. | BJData | 53.2 % | 91.1 % | 78.1 % | 96.6 % | | BJData (size) | 58.6 % | 92.1 % | 86.7 % | 97.4 % | | BJData (size+type) | 58.6 % | 92.1 % | 86.5 % | 97.4 % | +| BON8 | 50.5 % | 83.8 % | 63.5 % | 87.5 % | | BSON | 85.8 % | 95.2 % | 95.8 % | 106.7 % | | CBOR | 50.5 % | 86.3 % | 68.4 % | 88.0 % | | MessagePack | 50.5 % | 86.0 % | 68.5 % | 87.9 % | diff --git a/docs/mkdocs/docs/features/binary_formats/messagepack.md b/docs/mkdocs/docs/features/binary_formats/messagepack.md index a434909c4..0ca82c145 100644 --- a/docs/mkdocs/docs/features/binary_formats/messagepack.md +++ b/docs/mkdocs/docs/features/binary_formats/messagepack.md @@ -65,6 +65,8 @@ specification: - arrays with more than 4294967295 elements - objects with more than 4294967295 elements + Serializing such a value throws [`out_of_range.412`](../../home/exceptions.md#jsonexceptionout_of_range412). + !!! info "NaN/infinity handling" `NaN`, `Infinity`, and `-Infinity` are serialized as a MessagePack float 32 (type 0xCA, 5 bytes total), diff --git a/docs/mkdocs/docs/features/binary_values.md b/docs/mkdocs/docs/features/binary_values.md index 79629383f..aa3185c2a 100644 --- a/docs/mkdocs/docs/features/binary_values.md +++ b/docs/mkdocs/docs/features/binary_values.md @@ -187,6 +187,41 @@ as an array of uint8 values. The library implements this translation. } ``` +### BON8 + +[BON8](binary_formats/bon8.md) neither supports binary values nor subtypes. The library serializes binary values as an +array of integers. + +??? example + + Code: + + ```cpp + // create a binary value of subtype 42 (will be ignored in BON8) + json j; + j["binary"] = json::binary({0xCA, 0xFE, 0xBA, 0xBE}, 42); + + // convert to BON8 + auto v = json::to_bon8(j); + ``` + + `v` is a `std::vector` with the following 16 elements: + + ```c + 0x87 // object with 1 member + 0x62 0x69 0x6E 0x61 0x72 0x79 // "binary" + 0x84 // array with 4 elements + 0xC3 0x22 0xC3 0x56 0xC3 0x12 0xC3 0x16 // content (each byte as a 2-byte integer) + ``` + + Note that the subtype is lost, and deserializing `v` would yield the following value: + + ```json + { + "binary": [202, 254, 186, 190] + } + ``` + ### BSON [BSON](binary_formats/bson.md) supports binary values and subtypes. If a subtype is given, it is used and added as an diff --git a/docs/mkdocs/docs/features/index.md b/docs/mkdocs/docs/features/index.md index 0bbb2dadd..33403a58c 100644 --- a/docs/mkdocs/docs/features/index.md +++ b/docs/mkdocs/docs/features/index.md @@ -35,8 +35,8 @@ C++ types, and finally serialize it again. - [Serialization](serialization.md) — turn a value back into JSON text with [`dump`](../api/basic_json/dump.md), including pretty-printing and handling of non-ASCII and invalid UTF-8. - [Binary formats](binary_formats/index.md) — encode values more compactly as - [BJData](binary_formats/bjdata.md), [BSON](binary_formats/bson.md), [CBOR](binary_formats/cbor.md), - [MessagePack](binary_formats/messagepack.md), or [UBJSON](binary_formats/ubjson.md). + [BJData](binary_formats/bjdata.md), [BON8](binary_formats/bon8.md), [BSON](binary_formats/bson.md), + [CBOR](binary_formats/cbor.md), [MessagePack](binary_formats/messagepack.md), or [UBJSON](binary_formats/ubjson.md). - [Binary values](binary_values.md) — store and exchange raw byte sequences. ## How values are stored and configured diff --git a/docs/mkdocs/docs/features/serialization.md b/docs/mkdocs/docs/features/serialization.md index a875fcdde..5b916e6c8 100644 --- a/docs/mkdocs/docs/features/serialization.md +++ b/docs/mkdocs/docs/features/serialization.md @@ -117,7 +117,7 @@ For the [{fmt}](https://github.com/fmtlib/fmt) library, the library ships a ## Serializing to other formats Besides JSON text, a value can also be serialized to the more compact [binary formats](binary_formats/index.md) -(BJData, BSON, CBOR, MessagePack, UBJSON). +(BJData, BON8, BSON, CBOR, MessagePack, UBJSON). ## See also diff --git a/docs/mkdocs/docs/features/types/template_parameters.md b/docs/mkdocs/docs/features/types/template_parameters.md index 2a670f81c..8ed23e156 100644 --- a/docs/mkdocs/docs/features/types/template_parameters.md +++ b/docs/mkdocs/docs/features/types/template_parameters.md @@ -547,7 +547,7 @@ Grisu2 algorithm, which produces the shortest representation that round-trips. O ### Required for the binary formats `NumberFloatType` must be `#!cpp float` or `#!cpp double`. The writers for -[CBOR, MessagePack, UBJSON, BJData, and BSON](../binary_formats/index.md) map a floating-point value onto an IEEE 754 +[CBOR, MessagePack, UBJSON, BJData, BON8, and BSON](../binary_formats/index.md) map a floating-point value onto an IEEE 754 binary32 or binary64 field and have no encoding for `#!cpp long double`. ### Compatible types diff --git a/docs/mkdocs/docs/home/exceptions.md b/docs/mkdocs/docs/home/exceptions.md index ee76596f6..bf18baab1 100644 --- a/docs/mkdocs/docs/home/exceptions.md +++ b/docs/mkdocs/docs/home/exceptions.md @@ -932,19 +932,25 @@ A JSON Patch `add` operation cannot be applied because the target location's par ### json.exception.out_of_range.412 -BSON stores the length of documents, arrays, strings, and binary values in a signed 32-bit integer. This exception is thrown when a value is too large to be described by such a length field. +BSON stores the length of documents, arrays, strings, and binary values in a signed 32-bit integer, and MessagePack +stores the length of strings, binary values, arrays, and objects in at most an unsigned 32-bit integer. This exception +is thrown when a value is too large to be described by such a length field. -!!! failure "Example message" +!!! failure "Example messages" ``` BSON length 2147483661 exceeds maximum of 2147483647 ``` + ``` + MessagePack length 4294967296 exceeds maximum of 4294967295 + ``` !!! note - This exception was added in version 3.13.0. Before that, the length was silently truncated, and + This exception was added in version 3.13.0. Before that, the BSON length was silently truncated, and [`to_bson`](../api/basic_json/to_bson.md) produced documents with negative length prefixes that - [`from_bson`](../api/basic_json/from_bson.md) rejected. + [`from_bson`](../api/basic_json/from_bson.md) rejected; [`to_msgpack`](../api/basic_json/to_msgpack.md) wrote such + a value without any length, producing output that could not be read back. ### json.exception.out_of_range.413 diff --git a/docs/mkdocs/includes/glossary.md b/docs/mkdocs/includes/glossary.md index afc7ebc6a..9f2c6329d 100644 --- a/docs/mkdocs/includes/glossary.md +++ b/docs/mkdocs/includes/glossary.md @@ -5,6 +5,7 @@ *[ASCII]: American Standard Code for Information Interchange *[BDFL]: Benevolent Dictator for Life *[BJData]: Binary JData +*[BON8]: Binary Object Notation 8 *[BSON]: Binary JSON *[CBOR]: Concise Binary Object Representation *[CC0]: Creative Commons Zero diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml index a05ce2dff..70537bd27 100644 --- a/docs/mkdocs/mkdocs.yml +++ b/docs/mkdocs/mkdocs.yml @@ -63,6 +63,7 @@ nav: - Binary Formats: - features/binary_formats/index.md - features/binary_formats/bjdata.md + - features/binary_formats/bon8.md - features/binary_formats/bson.md - features/binary_formats/cbor.md - features/binary_formats/messagepack.md @@ -142,6 +143,7 @@ nav: - 'flatten': api/basic_json/flatten.md - 'format_as': api/basic_json/format_as.md - 'from_bjdata': api/basic_json/from_bjdata.md + - 'from_bon8': api/basic_json/from_bon8.md - 'from_bson': api/basic_json/from_bson.md - 'from_cbor': api/basic_json/from_cbor.md - 'from_msgpack': api/basic_json/from_msgpack.md @@ -213,6 +215,7 @@ nav: - 'swap': api/basic_json/swap.md - 'std::swap<basic_json>': api/basic_json/std_swap.md - 'to_bjdata': api/basic_json/to_bjdata.md + - 'to_bon8': api/basic_json/to_bon8.md - 'to_bson': api/basic_json/to_bson.md - 'to_cbor': api/basic_json/to_cbor.md - 'to_msgpack': api/basic_json/to_msgpack.md @@ -412,7 +415,7 @@ plugins: markdown_description: > JSON for Modern C++ is a C++11 header-only library implementing a JSON value type with an STL-like API, JSON Pointer/Patch, CBOR/MessagePack/ - BSON/UBJSON/BJData binary format support, and a SAX-style parser interface. + BSON/UBJSON/BJData/BON8 binary format support, and a SAX-style parser interface. sections: Home: - index.md diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index 4132c03ca..b0675c626 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -44,7 +45,7 @@ enum class cbor_tag_handler_t { error, ///< throw a parse_error exception in case of a tag ignore, ///< ignore tags - store ///< store tags as binary type + store ///< store tagged byte strings (for bytes 0xd8..0xdb) as binary values with the tag as subtype; other tagged values are read as if the tag were ignored }; /*! @@ -84,7 +85,7 @@ JSON_INLINE_VARIABLE constexpr std::size_t max_valueless_container_size = 1 << 2 /////////////////// /*! -@brief deserialization of CBOR, MessagePack, and UBJSON values +@brief deserialization of BJData, BON8, BSON, CBOR, MessagePack, and UBJSON values */ template> class binary_reader @@ -98,6 +99,11 @@ class binary_reader using char_type = typename InputAdapterType::char_type; using char_int_type = typename char_traits::int_type; + /// whether the input is a contiguous block of bytes that can be inspected + /// and consumed in bulk (as in the lexer); used by @ref get_bon8_string_bulk + static constexpr bool bulk_scan = + input_adapter_supports_bulk_scan(is_detected {}); + public: /*! @brief create a binary reader @@ -132,6 +138,7 @@ class binary_reader { sax = sax_; container_stack.clear(); + bon8_pushback_size = 0; bool result = false; switch (format) @@ -153,6 +160,10 @@ class binary_reader result = parse_ubjson_internal(); break; + case input_format_t::bon8: + result = parse_bon8_internal(); + break; + case input_format_t::json: // LCOV_EXCL_LINE default: // LCOV_EXCL_LINE JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE @@ -165,6 +176,11 @@ class binary_reader { get_ignore_noop(); } + else if (input_format == input_format_t::bon8) + { + // a string that ends a container hands back the byte after it + get_bon8(); + } else { get(); @@ -396,6 +412,11 @@ class binary_reader */ bool get_bson_cstr(string_t& result) { + if (get_bson_cstr_bulk(result, std::integral_constant {})) + { + return true; + } + auto out = std::back_inserter(result); while (true) { @@ -412,6 +433,46 @@ class binary_reader } } + /*! + @brief read a C-style string from contiguous input in one step + + @param[in,out] result the string to append to + @return whether the string was read; if the input has no \x00-byte, nothing + is read, and @ref get_bson_cstr reports the end of the input + */ + bool get_bson_cstr_bulk(string_t& result, std::true_type /*bulk*/) + { + const std::size_t remaining = ia.bulk_remaining(); + if (remaining == 0) + { + return false; + } + const auto* const data = reinterpret_cast(ia.bulk_data()); + // a plain loop rather than std::memchr: most keys are short (array + // indices are keys, too), and the call would cost more than it saves + std::size_t length = 0; + while (length < remaining && data[length] != 0x00) + { + ++length; + } + if (length == remaining) + { + return false; + } + result.append(reinterpret_cast(data), length); + // consume the string and its \x00-byte, as the byte-wise path does + ia.bulk_skip(length + 1); + chars_read += length + 1; + current = 0x00; + return true; + } + + /// input that is not contiguous: C-style strings are read byte by byte + bool get_bson_cstr_bulk(string_t& /*result*/, std::false_type /*bulk*/) const noexcept + { + return false; + } + /*! @brief Parses a zero-terminated string of length @a len from the BSON input. @@ -592,14 +653,18 @@ class binary_reader input (true) or whether the last read character should be considered instead (false) @param[in] tag_handler how CBOR tags should be treated + @param[out] tag_pending whether a tag was parsed and its value follows + @param[out] item_read whether the tagged value's initial byte is already in current @return whether a valid CBOR value was passed to the SAX parser */ bool parse_cbor_value(const bool get_char, const cbor_tag_handler_t tag_handler, - bool& tag_pending) + bool& tag_pending, + bool& item_read) { tag_pending = false; + item_read = false; switch (get_char ? get() : current) { @@ -1021,7 +1086,17 @@ class binary_reader } } get(); - return get_cbor_binary(b) && sax->binary(b); + // a byte string (the heads accepted by get_cbor_binary) keeps the tag as subtype + if ((current >= 0x40 && current <= 0x5B) || current == 0x5F) + { + return get_cbor_binary(b) && sax->binary(b); + } + + // not a byte string: the tagged value, whose first byte + // was just read, is read by the caller like for ignore + tag_pending = true; + item_read = true; + return true; } default: // LCOV_EXCL_LINE @@ -1503,13 +1578,14 @@ class binary_reader // a tag is not a value of its own: read on until the tagged value bool tag_pending = false; + bool item_read = false; do { - if (JSON_HEDLEY_UNLIKELY(!parse_cbor_value(fetch, tag_handler, tag_pending))) + if (JSON_HEDLEY_UNLIKELY(!parse_cbor_value(fetch, tag_handler, tag_pending, item_read))) { return false; } - fetch = true; + fetch = !item_read; } while (tag_pending); @@ -3184,6 +3260,549 @@ class binary_reader } } + ////////// + // BON8 // + ////////// + + /*! + @brief get the next byte of a BON8 value + + A BON8 string has no length prefix and no mandatory terminator: it ends at + the first byte that cannot continue it, which is already the first byte (or, + for an integer that begins with a UTF-8 lead byte, the first two bytes) of + whatever follows. The string reader hands those bytes back with + @ref unget_bon8, and every BON8 read goes through this function so that + they are seen again. + + @return character read from the input + */ + char_int_type get_bon8() + { + if (bon8_pushback_size != 0) + { + ++chars_read; + return current = bon8_pushback[--bon8_pushback_size]; + } + return get(); + } + + /*! + @brief hand a byte back so that the next @ref get_bon8 returns it again + + @param[in] c the byte to hand back; bytes handed back are returned in + reverse order + */ + void unget_bon8(const char_int_type c) + { + // At most two bytes are ever handed back: a byte is only handed back + // right after it was read with get_bon8(), and the only place that + // hands back two bytes (a lead byte and the byte after it) read both + // of them in a row, which emptied the buffer first. This is an + // invariant of the reader rather than a property of the input, so + // an assertion suffices (the fuzzers are built with assertions). + JSON_ASSERT(bon8_pushback_size < bon8_pushback.size()); + bon8_pushback[bon8_pushback_size++] = c; + --chars_read; + } + + /*! + @param[in] c a byte + @return whether @a c is a UTF-8 continuation byte (0x80..0xBF) + */ + static constexpr bool is_bon8_continuation(const char_int_type c) noexcept + { + return 0x80 <= c && c <= 0xBF; + } + + /*! + @brief report a parse error at the last read byte + + @param[in] detail a detailed error message + @param[in] context further context information + @return false + */ + bool bon8_error(const std::string& detail, const char* context) + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, + exception_message(input_format_t::bon8, concat(detail, ": 0x", last_token), context), nullptr)); + } + + /*! + @brief read a BON8 value and everything nested inside it + + Reads values until the one that was begun here is complete, resuming the + enclosing container after each element, so that the nesting depth of the + input costs heap rather than native stack (see #5104). + + @return whether reading the value succeeded + */ + bool parse_bon8_internal() + { + // the key currently being read; hoisted out of the loop so that its + // capacity is reused across elements and across nesting levels + string_t key; + + while (true) + { + if (!container_stack.empty()) + { + // a copy, not a reference: it must stay valid across the + // pop_back() below, which destroys the container_stack element + // it would otherwise alias + const container_frame top = container_stack.back(); + bool at_end = false; + + if (top.remaining != npos) + { + // counted container (0x80..0x84, 0x86..0x8A): it ends once + // its elements have been read + at_end = (top.remaining == 0); + if (!at_end) + { + // claim the element about to be read + --container_stack.back().remaining; + } + } + else + { + // container 0x85 or 0x8B: it ends at an end-of-container + // marker (0xFE); any other byte begins the next element + at_end = (get_bon8() == 0xFE); + if (!at_end) + { + unget_bon8(current); + } + } + + if (at_end) + { + container_stack.pop_back(); + if (JSON_HEDLEY_UNLIKELY(top.is_object ? !sax->end_object() : !sax->end_array())) + { + return false; + } + // the value begun here is complete once its container is + if (container_stack.empty()) + { + return true; + } + continue; + } + + if (top.is_object) + { + key.clear(); + if (JSON_HEDLEY_UNLIKELY(!get_bon8_key(key) || !sax->key(key))) + { + return false; + } + } + } + + if (JSON_HEDLEY_UNLIKELY(!parse_bon8_value())) + { + return false; + } + + // a value that opened a container left it on the stack; one that + // did not, and that was not inside a container, was the whole value + if (container_stack.empty()) + { + return true; + } + } + } + + /*! + @brief read one BON8 value + + Reads a single value and passes it to the SAX parser. A value that begins + a container is not read to its end: the container is opened with + @ref enter_container and its elements are read by + @ref parse_bon8_internal, so that nesting does not consume native stack. + + @return whether reading the value succeeded + */ + bool parse_bon8_value() + { + const auto byte = get_bon8(); + + if (byte == char_traits::eof()) + { + return unexpect_eof(input_format_t::bon8, "value"); + } + + // string: ASCII character + if (byte <= 0x7F) + { + string_t s; + unget_bon8(byte); + return get_bon8_string(s) && sax->string(s); + } + + // array with 0..4 elements + if (byte <= 0x84) + { + return enter_array(static_cast(byte - 0x80)); + } + + // array terminated by 0xFE + if (byte == 0x85) + { + return enter_array(npos); + } + + // object with 0..4 members + if (byte <= 0x8A) + { + return enter_object(static_cast(byte - 0x86)); + } + + switch (byte) + { + case 0x8B: // object terminated by 0xFE + return enter_object(npos); + + case 0x8C: // int32 + { + std::int32_t number{}; + return get_number(input_format_t::bon8, number) && emit_bon8_integer(number); + } + + case 0x8D: // int64 + { + std::int64_t number{}; + return get_number(input_format_t::bon8, number) && emit_bon8_integer(number); + } + + case 0x8E: // binary32 + { + float number{}; + return get_number(input_format_t::bon8, number) && sax->number_float(static_cast(number), ""); + } + + case 0x8F: // binary64 + { + double number{}; + return get_number(input_format_t::bon8, number) && sax->number_float(static_cast(number), ""); + } + + case 0xF8: + return sax->boolean(false); + + case 0xF9: + return sax->boolean(true); + + case 0xFA: + return sax->null(); + + case 0xFB: + return sax->number_float(static_cast(-1.0), ""); + + case 0xFC: + return sax->number_float(static_cast(0.0), ""); + + case 0xFD: + return sax->number_float(static_cast(1.0), ""); + + case 0xFF: // empty string + { + string_t s; + return sax->string(s); + } + + default: + break; + } + + // integer 0..39 + if (byte <= 0xB7) + { + return sax->number_unsigned(static_cast(byte - 0x90)); + } + + // integer -1..-10 + if (byte <= 0xC1) + { + return sax->number_integer(-1 - static_cast(byte - 0xB8)); + } + + // 0xC2..0xF7: a UTF-8 lead byte begins a string if a continuation + // byte follows and an integer otherwise + if (byte <= 0xF7) + { + const auto second = get_bon8(); + if (is_bon8_continuation(second)) + { + string_t s; + unget_bon8(second); + unget_bon8(byte); + return get_bon8_string(s) && sax->string(s); + } + return get_bon8_integer(byte, second); + } + + // 0xFE: end of container where a value is expected + return bon8_error("invalid byte", "value"); + } + + /*! + @brief pass an integer to the SAX parser + + Non-negative integers are passed as unsigned, negative integers as signed + numbers, like the other binary formats do. + + @param[in] number the integer + @return whether the SAX parser accepted the value + */ + bool emit_bon8_integer(const std::int64_t number) + { + if (number >= 0) + { + return sax->number_unsigned(static_cast(number)); + } + return sax->number_integer(static_cast(number)); + } + + /*! + @brief read an integer encoded in 2..4 bytes + + The first byte is a UTF-8 lead byte (0xC2..0xF7) that is followed by a + byte that is not a continuation byte: 0x00..0x7F for positive and + 0xC0..0xFF for negative integers. The lead byte's low bits and the second + byte's low 7 (positive) or 6 (negative) bits are the most significant bits + of the value; 3- and 4-byte integers add one or two full bytes. Each range + starts where the shorter one ends, so no value has two encodings of the + same length. + + @param[in] lead the first byte (0xC2..0xF7) + @param[in] second the second byte + @return whether reading the integer succeeded + */ + bool get_bon8_integer(const char_int_type lead, const char_int_type second) + { + if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::bon8, "number"))) + { + return false; + } + + const bool negative = second >= 0xC0; + auto value = static_cast(negative ? (second & 0x3F) : second); + std::int64_t offset = 0; + int extra_bytes = 0; + + if (lead <= 0xDF) + { + value |= static_cast(lead - 0xC2) << (negative ? 6 : 7); + offset = negative ? 11 : 40; + } + else if (lead <= 0xEF) + { + value |= static_cast(lead & 0x0F) << (negative ? 6 : 7); + offset = negative ? 1931 : 3880; + extra_bytes = 1; + } + else + { + value |= static_cast(lead & 0x07) << (negative ? 6 : 7); + offset = negative ? 264075 : 528168; + extra_bytes = 2; + } + + for (int i = 0; i < extra_bytes; ++i) + { + if (JSON_HEDLEY_UNLIKELY(get_bon8() == char_traits::eof())) + { + return unexpect_eof(input_format_t::bon8, "number"); + } + value = (value << 8) | static_cast(current); + } + + return negative ? sax->number_integer(static_cast(-(value + offset))) + : sax->number_unsigned(static_cast(value + offset)); + } + + /*! + @brief read an object key + + A key must be a string, so its first byte must be an ASCII character, a + UTF-8 lead byte followed by a continuation byte, or 0xFF (empty string). + + @param[out] result the key + @return whether reading the key succeeded + */ + bool get_bon8_key(string_t& result) + { + const auto byte = get_bon8(); + + if (byte == char_traits::eof()) + { + return unexpect_eof(input_format_t::bon8, "key"); + } + + if (byte == 0xFF) + { + return true; + } + + if (byte <= 0x7F) + { + unget_bon8(byte); + return get_bon8_string(result); + } + + if (0xC2 <= byte && byte <= 0xF7) + { + const auto second = get_bon8(); + unget_bon8(second); + if (is_bon8_continuation(second)) + { + unget_bon8(byte); + return get_bon8_string(result); + } + // an integer: report its first byte rather than the one after it + current = byte; + } + + return bon8_error("expected a string; last byte", "key"); + } + + /*! + @brief append the run of valid UTF-8 at the read position to a string + + For contiguous input, the ASCII characters and complete well-formed UTF-8 + sequences at the read position are appended to @a result in one step. The + byte that stops the run (an end-of-string marker, the first byte of the + next value, or an ill-formed byte) is left for @ref get_bon8_string, so + that strings end and errors are reported exactly as without this step. + + @param[in,out] result the string to append to + */ + void get_bon8_string_bulk(string_t& result, std::true_type /*bulk*/) + { + // bytes handed back must be read through get_bon8() first + if (bon8_pushback_size != 0) + { + return; + } + const std::size_t remaining = ia.bulk_remaining(); + if (remaining == 0) + { + return; + } + const auto* const data = reinterpret_cast(ia.bulk_data()); + const std::size_t length = valid_utf8_prefix(data, remaining); + if (length != 0) + { + result.append(reinterpret_cast(data), length); + ia.bulk_skip(length); + chars_read += length; + } + } + + /// input that is not contiguous: strings are read byte by byte + void get_bon8_string_bulk(string_t& /*result*/, std::false_type /*bulk*/) const noexcept {} + + /*! + @brief read a string + + Reads UTF-8 characters until an end-of-string marker (0xFF), which is + consumed, or a byte that cannot continue the string, which is handed back + to be read as the start of the next value. The string must be valid UTF-8, + and it must not end at the end of the input: the last string of a message + is always terminated by 0xFF. + + @param[out] result the string + @return whether reading the string succeeded + */ + bool get_bon8_string(string_t& result) + { + while (true) + { + get_bon8_string_bulk(result, std::integral_constant {}); + + const auto byte = get_bon8(); + + if (byte == char_traits::eof()) + { + return unexpect_eof(input_format_t::bon8, "string"); + } + + // end of string + if (byte == 0xFF) + { + return true; + } + + // ASCII character + if (byte <= 0x7F) + { + result.push_back(static_cast(byte)); + continue; + } + + // a byte that cannot begin a character ends the string and begins + // the next value + if (byte < 0xC2 || byte > 0xF7) + { + unget_bon8(byte); + return true; + } + + // a lead byte ends the string if no continuation byte follows: it + // is then the first byte of an integer + const auto second = get_bon8(); + if (!is_bon8_continuation(second)) + { + unget_bon8(second); + unget_bon8(byte); + return true; + } + + // the valid range of the second byte excludes overlong forms, + // surrogates, and code points above U+10FFFF + // (RFC 3629, section 4) + int continuation_bytes = 0; + bool valid_second = true; + if (byte <= 0xDF) + { + continuation_bytes = 1; + } + else if (byte <= 0xEF) + { + continuation_bytes = 2; + valid_second = (byte != 0xE0 || second >= 0xA0) && (byte != 0xED || second <= 0x9F); + } + else + { + continuation_bytes = 3; + valid_second = byte <= 0xF4 && (byte != 0xF0 || second >= 0x90) && (byte != 0xF4 || second <= 0x8F); + } + + if (JSON_HEDLEY_UNLIKELY(!valid_second)) + { + return bon8_error("invalid UTF-8 byte", "string"); + } + + result.push_back(static_cast(byte)); + result.push_back(static_cast(second)); + + for (int i = 1; i < continuation_bytes; ++i) + { + if (JSON_HEDLEY_UNLIKELY(get_bon8() == char_traits::eof())) + { + return unexpect_eof(input_format_t::bon8, "string"); + } + if (JSON_HEDLEY_UNLIKELY(!is_bon8_continuation(current))) + { + return bon8_error("invalid UTF-8 byte", "string"); + } + result.push_back(static_cast(current)); + } + } + } + /////////////////////// // Utility functions // /////////////////////// @@ -3482,6 +4101,10 @@ class binary_reader error_msg += "BJData"; break; + case input_format_t::bon8: + error_msg += "BON8"; + break; + case input_format_t::json: // LCOV_EXCL_LINE default: // LCOV_EXCL_LINE JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE @@ -3514,6 +4137,11 @@ class binary_reader /// the containers that have been opened and not closed yet; see @ref container_frame std::vector container_stack{}; + /// BON8: bytes read past the end of a string, returned again by @ref get_bon8 + std::array bon8_pushback{{}}; + /// BON8: number of bytes in @ref bon8_pushback + std::size_t bon8_pushback_size = 0; + // excluded markers in bjdata optimized type #define JSON_BINARY_READER_MAKE_BJD_OPTIMIZED_TYPE_MARKERS_ \ make_array('F', 'H', 'N', 'S', 'T', 'Z', '[', '{') diff --git a/include/nlohmann/detail/input/input_adapters.hpp b/include/nlohmann/detail/input/input_adapters.hpp index 317b38806..e174775c5 100644 --- a/include/nlohmann/detail/input/input_adapters.hpp +++ b/include/nlohmann/detail/input/input_adapters.hpp @@ -34,7 +34,7 @@ namespace detail { /// the supported input formats -enum class input_format_t { json, cbor, msgpack, ubjson, bson, bjdata }; +enum class input_format_t { json, cbor, msgpack, ubjson, bson, bjdata, bon8 }; //////////////////// // input adapters // diff --git a/include/nlohmann/detail/input/string_scan.hpp b/include/nlohmann/detail/input/string_scan.hpp index 6af0e6c5d..8a6f03122 100644 --- a/include/nlohmann/detail/input/string_scan.hpp +++ b/include/nlohmann/detail/input/string_scan.hpp @@ -201,6 +201,43 @@ inline std::size_t validate_one_utf8(const unsigned char* data, std::size_t avai return 0; // invalid, incomplete, or must be diagnosed by the byte path } +// Return the length of the longest prefix of [data, data+n) that consists of +// ASCII characters and complete well-formed UTF-8 sequences; n if all of it is +// valid UTF-8. Unlike scalar_string_bulk_run(), quotes, escapes, and control +// characters are ordinary characters here. ASCII is skipped 8 bytes at a time. +inline std::size_t valid_utf8_prefix(const unsigned char* data, std::size_t n) noexcept +{ + constexpr std::uint64_t high = 0x8080808080808080ull; + std::size_t pos = 0; + while (pos < n) + { + if (pos + 8 <= n) + { + std::uint64_t word = 0; + std::memcpy(&word, data + pos, sizeof(word)); + if ((word & high) == 0) + { + pos += 8; + continue; + } + } + + if (data[pos] < 0x80u) + { + ++pos; + continue; + } + + const std::size_t seq = validate_one_utf8(data + pos, n - pos); + if (seq == 0) + { + break; // ill-formed or truncated + } + pos += seq; + } + return pos; +} + // Scalar (C++11) computation of the bulk run length: the number of leading // bytes in [data, data+n) that are ordinary ASCII or complete well-formed UTF-8 // sequences, stopping before the first byte that needs individual handling (the diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index 5a0722a18..9da4269a8 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -25,6 +25,7 @@ #endif #include +#include #include #include #include @@ -76,7 +77,7 @@ std::size_t binary_reserve_hint(const BasicJsonType& j) } /*! -@brief serialization to CBOR and MessagePack values +@brief serialization to BJData, BON8, BSON, CBOR, MessagePack, and UBJSON values */ template> class binary_writer @@ -168,92 +169,20 @@ class binary_writer if (j.m_data.m_value.number_integer >= 0) { // CBOR does not differentiate between positive signed - // integers and unsigned integers. Therefore, we used the - // code from the value_t::number_unsigned case here. - if (j.m_data.m_value.number_integer <= 0x17) - { - write_number(static_cast(j.m_data.m_value.number_integer)); - } - else if (j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x18)); - write_number(static_cast(j.m_data.m_value.number_integer)); - } - else if (j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x19)); - write_number(static_cast(j.m_data.m_value.number_integer)); - } - else if (j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x1A)); - write_number(static_cast(j.m_data.m_value.number_integer)); - } - else - { - oa.write_character(to_char_type(0x1B)); - write_number(static_cast(j.m_data.m_value.number_integer)); - } + // integers and unsigned integers + write_cbor_head(0x00, static_cast(j.m_data.m_value.number_integer)); } else { - // The conversions below encode the sign in the first - // byte, and the value is converted to a positive number. - const auto positive_number = -1 - j.m_data.m_value.number_integer; - if (j.m_data.m_value.number_integer >= -24) - { - write_number(static_cast(0x20 + positive_number)); - } - else if (positive_number <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x38)); - write_number(static_cast(positive_number)); - } - else if (positive_number <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x39)); - write_number(static_cast(positive_number)); - } - else if (positive_number <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x3A)); - write_number(static_cast(positive_number)); - } - else - { - oa.write_character(to_char_type(0x3B)); - write_number(static_cast(positive_number)); - } + // a negative integer n is encoded as -1 - n + write_cbor_head(0x20, static_cast(-1 - j.m_data.m_value.number_integer)); } break; } case value_t::number_unsigned: { - if (j.m_data.m_value.number_unsigned <= 0x17) - { - write_number(static_cast(j.m_data.m_value.number_unsigned)); - } - else if (j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x18)); - write_number(static_cast(j.m_data.m_value.number_unsigned)); - } - else if (j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x19)); - write_number(static_cast(j.m_data.m_value.number_unsigned)); - } - else if (j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x1A)); - write_number(static_cast(j.m_data.m_value.number_unsigned)); - } - else - { - oa.write_character(to_char_type(0x1B)); - write_number(static_cast(j.m_data.m_value.number_unsigned)); - } + write_cbor_head(0x00, j.m_data.m_value.number_unsigned); break; } @@ -283,33 +212,7 @@ class binary_writer case value_t::string: { // step 1: write control byte and the string length - const auto N = j.m_data.m_value.string->size(); - if (N <= 0x17) - { - write_number(static_cast(0x60 + N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x78)); - write_number(static_cast(N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x79)); - write_number(static_cast(N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x7A)); - write_number(static_cast(N)); - } - // LCOV_EXCL_START - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x7B)); - write_number(static_cast(N)); - } - // LCOV_EXCL_STOP + write_cbor_head(0x60, j.m_data.m_value.string->size()); // step 2: write the string oa.write_characters( @@ -321,33 +224,7 @@ class binary_writer case value_t::array: { // step 1: write control byte and the array size - const auto N = j.m_data.m_value.array->size(); - if (N <= 0x17) - { - write_number(static_cast(0x80 + N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x98)); - write_number(static_cast(N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x99)); - write_number(static_cast(N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x9A)); - write_number(static_cast(N)); - } - // LCOV_EXCL_START - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x9B)); - write_number(static_cast(N)); - } - // LCOV_EXCL_STOP + write_cbor_head(0x80, j.m_data.m_value.array->size()); // step 2: write each element for (const auto& el : *j.m_data.m_value.array) @@ -376,7 +253,7 @@ class binary_writer write_number(static_cast(0xda)); write_number(static_cast(j.m_data.m_value.binary->subtype())); } - else if (j.m_data.m_value.binary->subtype() <= (std::numeric_limits::max)()) + else { write_number(static_cast(0xdb)); write_number(static_cast(j.m_data.m_value.binary->subtype())); @@ -385,32 +262,7 @@ class binary_writer // step 1: write control byte and the binary array size const auto N = j.m_data.m_value.binary->size(); - if (N <= 0x17) - { - write_number(static_cast(0x40 + N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x58)); - write_number(static_cast(N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x59)); - write_number(static_cast(N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x5A)); - write_number(static_cast(N)); - } - // LCOV_EXCL_START - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x5B)); - write_number(static_cast(N)); - } - // LCOV_EXCL_STOP + write_cbor_head(0x40, N); // step 2: write each element oa.write_characters( @@ -423,33 +275,7 @@ class binary_writer case value_t::object: { // step 1: write control byte and the object size - const auto N = j.m_data.m_value.object->size(); - if (N <= 0x17) - { - write_number(static_cast(0xA0 + N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0xB8)); - write_number(static_cast(N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0xB9)); - write_number(static_cast(N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0xBA)); - write_number(static_cast(N)); - } - // LCOV_EXCL_START - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0xBB)); - write_number(static_cast(N)); - } - // LCOV_EXCL_STOP + write_cbor_head(0xA0, j.m_data.m_value.object->size()); // step 2: write each element for (const auto& el : *j.m_data.m_value.object) @@ -466,6 +292,23 @@ class binary_writer } } + /*! + @brief check that @a length fits into the 32 bits that MessagePack stores + the length of a string, binary value, array, or object in + @return the length as an unsigned 32-bit integer + @throw out_of_range.412 if @a length exceeds the range of std::uint32_t + */ + static std::uint32_t to_msgpack_length(const std::size_t length, const BasicJsonType& j) + { + if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(length))) + { + JSON_THROW(out_of_range::create(412, concat("MessagePack length ", std::to_string(length), " exceeds maximum of ", std::to_string((std::numeric_limits::max)())), &j)); + } + + static_cast(j); + return static_cast(length); + } + /*! @param[in] j JSON value to serialize */ @@ -517,7 +360,7 @@ class binary_writer oa.write_character(to_char_type(0xCE)); write_number(static_cast(j.m_data.m_value.number_integer)); } - else if (j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) + else { // uint 64 oa.write_character(to_char_type(0xCF)); @@ -552,8 +395,7 @@ class binary_writer oa.write_character(to_char_type(0xD2)); write_number(static_cast(j.m_data.m_value.number_integer)); } - else if (j.m_data.m_value.number_integer >= (std::numeric_limits::min)() && - j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) + else { // int 64 oa.write_character(to_char_type(0xD3)); @@ -588,7 +430,7 @@ class binary_writer oa.write_character(to_char_type(0xCE)); write_number(static_cast(j.m_data.m_value.number_integer)); } - else if (j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) + else { // uint 64 oa.write_character(to_char_type(0xCF)); @@ -606,7 +448,7 @@ class binary_writer case value_t::string: { // step 1: write control byte and the string length - const auto N = j.m_data.m_value.string->size(); + const auto N = to_msgpack_length(j.m_data.m_value.string->size(), j); if (N <= 31) { // fixstr @@ -624,7 +466,7 @@ class binary_writer oa.write_character(to_char_type(0xDA)); write_number(static_cast(N)); } - else if (N <= (std::numeric_limits::max)()) + else { // str 32 oa.write_character(to_char_type(0xDB)); @@ -641,7 +483,7 @@ class binary_writer case value_t::array: { // step 1: write control byte and the array size - const auto N = j.m_data.m_value.array->size(); + const auto N = to_msgpack_length(j.m_data.m_value.array->size(), j); if (N <= 15) { // fixarray @@ -653,7 +495,7 @@ class binary_writer oa.write_character(to_char_type(0xDC)); write_number(static_cast(N)); } - else if (N <= (std::numeric_limits::max)()) + else { // array 32 oa.write_character(to_char_type(0xDD)); @@ -675,7 +517,7 @@ class binary_writer const bool use_ext = j.m_data.m_value.binary->has_subtype(); // step 1: write control byte and the byte string length - const auto N = j.m_data.m_value.binary->size(); + const auto N = to_msgpack_length(j.m_data.m_value.binary->size(), j); if (N <= (std::numeric_limits::max)()) { std::uint8_t output_type{}; @@ -727,7 +569,7 @@ class binary_writer oa.write_character(to_char_type(output_type)); write_number(static_cast(N)); } - else if (N <= (std::numeric_limits::max)()) + else { const std::uint8_t output_type = use_ext ? 0xC9 // ext 32 @@ -759,7 +601,7 @@ class binary_writer case value_t::object: { // step 1: write control byte and the object size - const auto N = j.m_data.m_value.object->size(); + const auto N = to_msgpack_length(j.m_data.m_value.object->size(), j); if (N <= 15) { // fixmap @@ -771,7 +613,7 @@ class binary_writer oa.write_character(to_char_type(0xDE)); write_number(static_cast(N)); } - else if (N <= (std::numeric_limits::max)()) + else { // map 32 oa.write_character(to_char_type(0xDF)); @@ -1032,6 +874,21 @@ class binary_writer } } + /*! + @param[in] j JSON value to serialize + */ + void write_bon8(const BasicJsonType& j) + { + bool string_open = false; + write_bon8_value(j, string_open); + + // the last string of a message must be terminated + if (string_open) + { + oa.write_character(to_char_type(0xFF)); + } + } + private: ////////// // BSON // @@ -1526,6 +1383,46 @@ class binary_writer // CBOR // ////////// + /*! + @brief write the head of a CBOR data item + + The head is the major type in the upper three bits of the first byte and + an argument - an unsigned integer, the length of a string, the number of + elements of a container - in the shortest of its encodings: in the lower + five bits of the first byte itself if it is at most 23, otherwise in the + 1, 2, 4, or 8 bytes that follow (RFC 8949, section 3). + + @param[in] major_type the major type, shifted into the upper three bits + @param[in] argument the argument of the data item + */ + void write_cbor_head(const std::uint8_t major_type, const std::uint64_t argument) + { + if (argument <= 0x17) + { + write_number(static_cast(major_type + argument)); + } + else if (argument <= (std::numeric_limits::max)()) + { + oa.write_character(to_char_type(static_cast(major_type + 0x18))); + write_number(static_cast(argument)); + } + else if (argument <= (std::numeric_limits::max)()) + { + oa.write_character(to_char_type(static_cast(major_type + 0x19))); + write_number(static_cast(argument)); + } + else if (argument <= (std::numeric_limits::max)()) + { + oa.write_character(to_char_type(static_cast(major_type + 0x1A))); + write_number(static_cast(argument)); + } + else + { + oa.write_character(to_char_type(static_cast(major_type + 0x1B))); + write_number(argument); + } + } + static constexpr CharType get_cbor_float_prefix(float /*unused*/) { return to_char_type(0xFA); // Single-Precision Float @@ -1550,6 +1447,28 @@ class binary_writer return to_char_type(0xCB); // float 64 } + /// @return the BON8 type marker for binary32 (float) or binary64 (double) + template + static constexpr CharType get_bon8_float_prefix() + { + return to_char_type(std::is_same::value ? 0x8E : 0x8F); + } + + /// @return the type marker for a FloatType value in @a format (CBOR, MessagePack, or BON8) + template + static CharType get_compact_float_prefix(const detail::input_format_t format) + { + if (format == detail::input_format_t::cbor) + { + return get_cbor_float_prefix(FloatType{}); + } + if (format == detail::input_format_t::bon8) + { + return get_bon8_float_prefix(); + } + return get_msgpack_float_prefix(FloatType{}); + } + //////////// // UBJSON // //////////// @@ -1631,7 +1550,7 @@ class binary_writer } write_number(static_cast(n), use_bjdata); } - else if (use_bjdata && n <= (std::numeric_limits::max)()) + else if (use_bjdata) { if (add_prefix) { @@ -1711,30 +1630,59 @@ class binary_writer } write_number(static_cast(n), use_bjdata); } - else if ((std::numeric_limits::min)() <= n && n <= (std::numeric_limits::max)()) - { - if (add_prefix) - { - oa.write_character(to_char_type('L')); // int64 - } - write_number(static_cast(n), use_bjdata); - } - // LCOV_EXCL_START else { - if (add_prefix) - { - oa.write_character(to_char_type('H')); // high-precision number - } - - const auto number = BasicJsonType(n).dump(); - write_number_with_ubjson_prefix(number.size(), true, use_bjdata); - for (std::size_t i = 0; i < number.size(); ++i) - { - oa.write_character(to_char_type(static_cast(number[i]))); - } + // every value of an integer type of at most 64 bits fits into an + // int64; only a wider type needs a range check + write_ubjson_int64_or_high_precision(n, add_prefix, use_bjdata, + std::integral_constant < bool, std::numeric_limits::digits <= std::numeric_limits::digits > {}); } - // LCOV_EXCL_STOP + } + + template + void write_ubjson_int64_or_high_precision(const NumberType n, const bool add_prefix, const bool use_bjdata, std::true_type /*fits_int64*/) + { + if (add_prefix) + { + oa.write_character(to_char_type('L')); // int64 + } + write_number(static_cast(n), use_bjdata); + } + + template + void write_ubjson_int64_or_high_precision(const NumberType n, const bool add_prefix, const bool use_bjdata, std::false_type /*fits_int64*/) + { + if ((std::numeric_limits::min)() <= n && n <= (std::numeric_limits::max)()) + { + write_ubjson_int64_or_high_precision(n, add_prefix, use_bjdata, std::true_type {}); + return; + } + + if (add_prefix) + { + oa.write_character(to_char_type('H')); // high-precision number + } + + const auto number = BasicJsonType(n).dump(); + write_number_with_ubjson_prefix(number.size(), true, use_bjdata); + for (std::size_t i = 0; i < number.size(); ++i) + { + oa.write_character(to_char_type(static_cast(number[i]))); + } + } + + template + static constexpr CharType ubjson_int64_or_high_precision_prefix(const NumberType /*n*/, std::true_type /*fits_int64*/) noexcept + { + return 'L'; + } + + template + static CharType ubjson_int64_or_high_precision_prefix(const NumberType n, std::false_type /*fits_int64*/) noexcept + { + // anything outside of the range of an int64 is treated as a + // high-precision number + return ((std::numeric_limits::min)() <= n && n <= (std::numeric_limits::max)()) ? 'L' : 'H'; } /*! @@ -1776,12 +1724,10 @@ class binary_writer { return 'm'; } - if ((std::numeric_limits::min)() <= j.m_data.m_value.number_integer && j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) - { - return 'L'; - } - // anything else is treated as a high-precision number - return 'H'; // LCOV_EXCL_LINE + // every value of an integer type of at most 64 bits fits into + // an int64; only a wider type needs a range check + return ubjson_int64_or_high_precision_prefix(j.m_data.m_value.number_integer, + std::integral_constant < bool, std::numeric_limits::digits <= std::numeric_limits::digits > {}); } case value_t::number_unsigned: @@ -1814,12 +1760,12 @@ class binary_writer { return 'L'; } - if (use_bjdata && j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) + if (use_bjdata) { return 'M'; } // anything else is treated as a high-precision number - return 'H'; // LCOV_EXCL_LINE + return 'H'; } case value_t::number_float: @@ -2142,6 +2088,322 @@ class binary_writer return false; } + ////////// + // BON8 // + ////////// + + /*! + @brief write a BON8 value + + A string is written without length or terminator: it ends at the first + byte that cannot continue it, which is the first byte of any non-string + value and of the end-of-container marker 0xFE. It only needs an explicit + end-of-string marker (0xFF) when it is empty, when another string follows, + or when it is the last thing in the message. + + @param[in] j JSON value to serialize + @param[in,out] string_open whether the output ends with a non-empty + string that has not been terminated with 0xFF + */ + void write_bon8_value(const BasicJsonType& j, bool& string_open) + { + switch (j.type()) + { + case value_t::null: + { + write_bon8_marker(0xFA, string_open); + break; + } + + case value_t::boolean: + { + write_bon8_marker(j.m_data.m_value.boolean ? 0xF9 : 0xF8, string_open); + break; + } + + case value_t::number_unsigned: + { + if (j.m_data.m_value.number_unsigned > static_cast((std::numeric_limits::max)())) + { + JSON_THROW(out_of_range::create(407, concat("integer number ", std::to_string(j.m_data.m_value.number_unsigned), " cannot be represented by BON8 as it does not fit int64"), &j)); + } + write_bon8_integer(static_cast(j.m_data.m_value.number_unsigned)); + string_open = false; + break; + } + + case value_t::number_integer: + { + write_bon8_integer(static_cast(j.m_data.m_value.number_integer)); + string_open = false; + break; + } + + case value_t::number_float: + { + write_bon8_float(j.m_data.m_value.number_float); + string_open = false; + break; + } + + case value_t::string: + { + write_bon8_string(*j.m_data.m_value.string, string_open, j); + break; + } + + case value_t::array: + { + const auto N = j.m_data.m_value.array->size(); + // 0x80..0x84: array with 0..4 elements; 0x85: array ended by 0xFE + write_bon8_marker(static_cast(N <= 4 ? 0x80 + N : 0x85), string_open); + + for (const auto& el : *j.m_data.m_value.array) + { + write_bon8_value(el, string_open); + } + + if (N > 4) + { + write_bon8_marker(0xFE, string_open); + } + break; + } + + case value_t::object: + { + const auto N = j.m_data.m_value.object->size(); + // 0x86..0x8A: object with 0..4 members; 0x8B: object ended by 0xFE + write_bon8_marker(static_cast(N <= 4 ? 0x86 + N : 0x8B), string_open); + + for (const auto& el : *j.m_data.m_value.object) + { + write_bon8_string(el.first, string_open, j); + write_bon8_value(el.second, string_open); + } + + if (N > 4) + { + write_bon8_marker(0xFE, string_open); + } + break; + } + + case value_t::binary: + { + // BON8 has no binary type: write the bytes as an array of + // integers, like UBJSON and BJData do + const auto N = j.m_data.m_value.binary->size(); + write_bon8_marker(static_cast(N <= 4 ? 0x80 + N : 0x85), string_open); + + for (std::size_t i = 0; i < N; ++i) + { + // the cast is needed for binary types whose value type + // is not an integer (e.g., std::byte) + write_bon8_integer(static_cast(j.m_data.m_value.binary->data()[i])); + } + + if (N > 4) + { + write_bon8_marker(0xFE, string_open); + } + break; + } + + case value_t::discarded: + default: + break; + } + } + + /*! + @brief write a single byte that is not part of a string + + @param[in] marker the byte to write + @param[out] string_open set to false, because the output no longer ends + with a string; see @ref write_bon8_value + */ + void write_bon8_marker(const std::uint8_t marker, bool& string_open) + { + oa.write_character(to_char_type(marker)); + string_open = false; + } + + /*! + @brief write a string + + @param[in] s the string to write + @param[in,out] string_open see @ref write_bon8_value + @param[in] context the value the string belongs to (for diagnostics) + + @throw type_error.316 if @a s is not valid UTF-8, because the end of a + string is determined from its encoding + */ + void write_bon8_string(const string_t& s, bool& string_open, const BasicJsonType& context) + { + check_bon8_utf8(s, context); + + // a string that follows another string terminates it + if (string_open) + { + oa.write_character(to_char_type(0xFF)); + } + + if (s.empty()) + { + // the empty string is just the end-of-string marker + oa.write_character(to_char_type(0xFF)); + string_open = false; + } + else + { + oa.write_characters(reinterpret_cast(s.data()), s.size()); + string_open = true; + } + } + + /*! + @brief check that a string is valid UTF-8 (RFC 3629) + + @param[in] s the string to check + @param[in] context the value the string belongs to (for diagnostics) + + @throw type_error.316 if @a s is not valid UTF-8; the message names the + first byte of the first invalid or incomplete sequence + */ + static void check_bon8_utf8(const string_t& s, const BasicJsonType& context) + { + static_cast(context); // only used when exceptions are enabled + const auto* data = reinterpret_cast(s.data()); + const std::size_t valid = valid_utf8_prefix(data, s.size()); + if (JSON_HEDLEY_UNLIKELY(valid != s.size())) + { + JSON_THROW(type_error::create(316, concat("invalid UTF-8 byte at index ", std::to_string(valid), ": 0x", hex_byte(data[valid])), &context)); + } + } + + /// @return a byte as two uppercase hexadecimal digits + static std::string hex_byte(const std::uint8_t byte) + { + std::string result = "00"; + constexpr const char* nibble_to_hex = "0123456789ABCDEF"; + result[0] = nibble_to_hex[byte / 16]; + result[1] = nibble_to_hex[byte % 16]; + return result; + } + + /*! + @brief write an integer in the shortest encoding + + Integers from -10 to 39 take one byte. Up to -33818506 and 67637031, an + integer takes 2 to 4 bytes that begin with a UTF-8 lead byte (0xC2..0xF7) + followed by a byte that is not a continuation byte: 0x00..0x7F for + positive and 0xC0..0xFF for negative integers. Each range starts where the + shorter one ends. Larger integers are written as int32 (0x8C) or int64 + (0x8D) in big-endian byte order. + + @param[in] value the integer to write + */ + void write_bon8_integer(std::int64_t value) + { + if (value < (std::numeric_limits::min)() || value > (std::numeric_limits::max)()) + { + oa.write_character(to_char_type(0x8D)); + write_number(value); + } + else if (value < -33818506 || value > 67637031) + { + oa.write_character(to_char_type(0x8C)); + write_number(static_cast(value)); + } + else if (value <= -264075) + { + value = -(value + 264075); + write_bon8_bytes(0xF0 + ((value >> 22) & 0x07), 0xC0 + ((value >> 16) & 0x3F), value >> 8, value); + } + else if (value <= -1931) + { + value = -(value + 1931); + write_bon8_bytes(0xE0 + ((value >> 14) & 0x0F), 0xC0 + ((value >> 8) & 0x3F), value); + } + else if (value <= -11) + { + value = -(value + 11); + write_bon8_bytes(0xC2 + ((value >> 6) & 0x1F), 0xC0 + (value & 0x3F)); + } + else if (value <= -1) + { + write_bon8_bytes(0xB8 - (value + 1)); + } + else if (value <= 39) + { + write_bon8_bytes(0x90 + value); + } + else if (value <= 3879) + { + value -= 40; + write_bon8_bytes(0xC2 + ((value >> 7) & 0x1F), value & 0x7F); + } + else if (value <= 528167) + { + value -= 3880; + write_bon8_bytes(0xE0 + ((value >> 15) & 0x0F), (value >> 8) & 0x7F, value); + } + else + { + value -= 528168; + write_bon8_bytes(0xF0 + ((value >> 23) & 0x07), (value >> 16) & 0x7F, value >> 8, value); + } + } + + /// write the low byte of each argument + template + void write_bon8_bytes(const Bytes... bytes) + { + const std::array buffer{{to_char_type(static_cast(bytes & 0xFF))...}}; + oa.write_characters(buffer.data(), buffer.size()); + } + + /*! + @brief write a floating-point number + + -1.0, +0.0, and 1.0 take one byte. Other numbers are written as binary32 + (0x8E) if that loses no precision, and as binary64 (0x8F) otherwise; -0.0, + infinities, and NaN are always written as binary32, NaN as 0x7F800001. + + @param[in] n the number to write + */ + void write_bon8_float(const number_float_t n) + { +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_PUSH + JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") +#endif + if (n == static_cast(-1)) + { + oa.write_character(to_char_type(0xFB)); + } + else if (n == static_cast(0) && !std::signbit(n)) + { + oa.write_character(to_char_type(0xFC)); + } + else if (n == static_cast(1)) + { + oa.write_character(to_char_type(0xFD)); + } + else if (std::isnan(n)) + { + write_bon8_bytes(0x8E, 0x7F, 0x80, 0x00, 0x01); + } + else + { + write_compact_float(n, detail::input_format_t::bon8); + } +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_POP +#endif + } + /////////////////////// // Utility functions // /////////////////////// @@ -2276,16 +2538,12 @@ class binary_writer static_cast(n) <= static_cast((std::numeric_limits::max)()) && static_cast(static_cast(n)) == static_cast(n)))) { - oa.write_character(format == detail::input_format_t::cbor - ? get_cbor_float_prefix(static_cast(n)) - : get_msgpack_float_prefix(static_cast(n))); + oa.write_character(get_compact_float_prefix(format)); write_number(static_cast(n)); } else { - oa.write_character(format == detail::input_format_t::cbor - ? get_cbor_float_prefix(n) - : get_msgpack_float_prefix(n)); + oa.write_character(get_compact_float_prefix(format)); write_number(n); } #ifdef __GNUC__ diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index f63ea6108..24c1fb587 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -1492,13 +1492,28 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec compare_keys(current.lhs_object_it->first, current.rhs_object_it->first, std::integral_constant {}); - if (key_result != compare_result::equal) - { - return key_result; - } - left = &(current.lhs_object_it->second); right = &(current.rhs_object_it->second); + + if (key_result != compare_result::equal) + { + // An object type without a fixed order of its entries - + // std::unordered_map, say - may enumerate two equal + // objects differently, and its operator== does not care. + // Equality then finds the entry by its key; an ordering, + // or an object type that compares its entries in + // sequence (ordered_map), is decided by the key itself. + const auto* rhs_object = current.rhs_value->m_data.m_value.object; + const auto found = (!Ordered && !detail::is_ordered_map::value) + ? rhs_object->find(current.lhs_object_it->first) + : rhs_object->cend(); + if (found == rhs_object->cend()) + { + return key_result; + } + right = &(found->second); + } + ++current.lhs_object_it; ++current.rhs_object_it; } @@ -2157,17 +2172,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // value access // ////////////////// - /// get a boolean (explicit) - boolean_t get_impl(boolean_t* /*unused*/) const - { - if (JSON_HEDLEY_LIKELY(is_boolean())) - { - return m_data.m_value.boolean; - } - - JSON_THROW(type_error::create(302, detail::concat("type must be boolean, but is ", type_name()), this)); - } - /// get a pointer to the value (object) object_t* get_impl_ptr(object_t* /*unused*/) noexcept { @@ -5286,6 +5290,30 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec binary_writer(o).write_bson(j); } + /// @brief create a BON8 serialization of a given JSON value + /// @sa https://json.nlohmann.me/api/basic_json/to_bon8/ + static std::vector to_bon8(const basic_json& j) + { + std::vector result; + result.reserve(detail::binary_reserve_hint(j)); + vector_writer(result).write_bon8(j); + return result; + } + + /// @brief create a BON8 serialization of a given JSON value + /// @sa https://json.nlohmann.me/api/basic_json/to_bon8/ + static void to_bon8(const basic_json& j, detail::output_adapter o) + { + binary_writer(o).write_bon8(j); + } + + /// @brief create a BON8 serialization of a given JSON value + /// @sa https://json.nlohmann.me/api/basic_json/to_bon8/ + static void to_bon8(const basic_json& j, detail::output_adapter o) + { + binary_writer(o).write_bon8(j); + } + /// @brief create a JSON value from an input in CBOR format /// @sa https://json.nlohmann.me/api/basic_json/from_cbor/ template @@ -5519,6 +5547,43 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec return result; } + /// @brief create a JSON value from an input in BON8 format + /// @sa https://json.nlohmann.me/api/basic_json/from_bon8/ + template + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json from_bon8(InputType&& i, + const bool strict = true, + const bool allow_exceptions = true) + { + basic_json result; + auto ia = detail::input_adapter(std::forward(i)); + detail::json_sax_dom_parser sdp(result, allow_exceptions); + if (!binary_reader(std::move(ia), input_format_t::bon8).sax_parse(input_format_t::bon8, &sdp, strict)) // cppcheck-suppress[accessMoved] + { + result = value_t::discarded; + } + return result; + } + + /// @brief create a JSON value from an input in BON8 format (iterator pair, or iterator+sentinel pair for C++20 ranges support) + /// @sa https://json.nlohmann.me/api/basic_json/from_bon8/ + template::value, int> = 0> + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json from_bon8(IteratorType first, SentinelType last, + const bool strict = true, + const bool allow_exceptions = true) + { + basic_json result; + auto ia = detail::input_adapter(std::move(first), std::move(last)); + detail::json_sax_dom_parser sdp(result, allow_exceptions); + if (!binary_reader(std::move(ia), input_format_t::bon8).sax_parse(input_format_t::bon8, &sdp, strict)) // cppcheck-suppress[accessMoved] + { + result = value_t::discarded; + } + return result; + } + /// @brief create a JSON value from an input in BSON format /// @sa https://json.nlohmann.me/api/basic_json/from_bson/ template diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 0f85bc61c..d8bc675e0 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -7578,7 +7578,7 @@ namespace detail { /// the supported input formats -enum class input_format_t { json, cbor, msgpack, ubjson, bson, bjdata }; +enum class input_format_t { json, cbor, msgpack, ubjson, bson, bjdata, bon8 }; //////////////////// // input adapters // @@ -8995,6 +8995,43 @@ inline std::size_t validate_one_utf8(const unsigned char* data, std::size_t avai return 0; // invalid, incomplete, or must be diagnosed by the byte path } +// Return the length of the longest prefix of [data, data+n) that consists of +// ASCII characters and complete well-formed UTF-8 sequences; n if all of it is +// valid UTF-8. Unlike scalar_string_bulk_run(), quotes, escapes, and control +// characters are ordinary characters here. ASCII is skipped 8 bytes at a time. +inline std::size_t valid_utf8_prefix(const unsigned char* data, std::size_t n) noexcept +{ + constexpr std::uint64_t high = 0x8080808080808080ull; + std::size_t pos = 0; + while (pos < n) + { + if (pos + 8 <= n) + { + std::uint64_t word = 0; + std::memcpy(&word, data + pos, sizeof(word)); + if ((word & high) == 0) + { + pos += 8; + continue; + } + } + + if (data[pos] < 0x80u) + { + ++pos; + continue; + } + + const std::size_t seq = validate_one_utf8(data + pos, n - pos); + if (seq == 0) + { + break; // ill-formed or truncated + } + pos += seq; + } + return pos; +} + // Scalar (C++11) computation of the bulk run length: the number of leading // bytes in [data, data+n) that are ordinary ASCII or complete well-formed UTF-8 // sequences, stopping before the first byte that needs individual handling (the @@ -12557,6 +12594,8 @@ NLOHMANN_JSON_NAMESPACE_END // #include +// #include + // #include // #include @@ -12741,7 +12780,7 @@ enum class cbor_tag_handler_t { error, ///< throw a parse_error exception in case of a tag ignore, ///< ignore tags - store ///< store tags as binary type + store ///< store tagged byte strings (for bytes 0xd8..0xdb) as binary values with the tag as subtype; other tagged values are read as if the tag were ignored }; /*! @@ -12781,7 +12820,7 @@ JSON_INLINE_VARIABLE constexpr std::size_t max_valueless_container_size = 1 << 2 /////////////////// /*! -@brief deserialization of CBOR, MessagePack, and UBJSON values +@brief deserialization of BJData, BON8, BSON, CBOR, MessagePack, and UBJSON values */ template> class binary_reader @@ -12795,6 +12834,11 @@ class binary_reader using char_type = typename InputAdapterType::char_type; using char_int_type = typename char_traits::int_type; + /// whether the input is a contiguous block of bytes that can be inspected + /// and consumed in bulk (as in the lexer); used by @ref get_bon8_string_bulk + static constexpr bool bulk_scan = + input_adapter_supports_bulk_scan(is_detected {}); + public: /*! @brief create a binary reader @@ -12829,6 +12873,7 @@ class binary_reader { sax = sax_; container_stack.clear(); + bon8_pushback_size = 0; bool result = false; switch (format) @@ -12850,6 +12895,10 @@ class binary_reader result = parse_ubjson_internal(); break; + case input_format_t::bon8: + result = parse_bon8_internal(); + break; + case input_format_t::json: // LCOV_EXCL_LINE default: // LCOV_EXCL_LINE JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE @@ -12862,6 +12911,11 @@ class binary_reader { get_ignore_noop(); } + else if (input_format == input_format_t::bon8) + { + // a string that ends a container hands back the byte after it + get_bon8(); + } else { get(); @@ -13093,6 +13147,11 @@ class binary_reader */ bool get_bson_cstr(string_t& result) { + if (get_bson_cstr_bulk(result, std::integral_constant {})) + { + return true; + } + auto out = std::back_inserter(result); while (true) { @@ -13109,6 +13168,46 @@ class binary_reader } } + /*! + @brief read a C-style string from contiguous input in one step + + @param[in,out] result the string to append to + @return whether the string was read; if the input has no \x00-byte, nothing + is read, and @ref get_bson_cstr reports the end of the input + */ + bool get_bson_cstr_bulk(string_t& result, std::true_type /*bulk*/) + { + const std::size_t remaining = ia.bulk_remaining(); + if (remaining == 0) + { + return false; + } + const auto* const data = reinterpret_cast(ia.bulk_data()); + // a plain loop rather than std::memchr: most keys are short (array + // indices are keys, too), and the call would cost more than it saves + std::size_t length = 0; + while (length < remaining && data[length] != 0x00) + { + ++length; + } + if (length == remaining) + { + return false; + } + result.append(reinterpret_cast(data), length); + // consume the string and its \x00-byte, as the byte-wise path does + ia.bulk_skip(length + 1); + chars_read += length + 1; + current = 0x00; + return true; + } + + /// input that is not contiguous: C-style strings are read byte by byte + bool get_bson_cstr_bulk(string_t& /*result*/, std::false_type /*bulk*/) const noexcept + { + return false; + } + /*! @brief Parses a zero-terminated string of length @a len from the BSON input. @@ -13289,14 +13388,18 @@ class binary_reader input (true) or whether the last read character should be considered instead (false) @param[in] tag_handler how CBOR tags should be treated + @param[out] tag_pending whether a tag was parsed and its value follows + @param[out] item_read whether the tagged value's initial byte is already in current @return whether a valid CBOR value was passed to the SAX parser */ bool parse_cbor_value(const bool get_char, const cbor_tag_handler_t tag_handler, - bool& tag_pending) + bool& tag_pending, + bool& item_read) { tag_pending = false; + item_read = false; switch (get_char ? get() : current) { @@ -13718,7 +13821,17 @@ class binary_reader } } get(); - return get_cbor_binary(b) && sax->binary(b); + // a byte string (the heads accepted by get_cbor_binary) keeps the tag as subtype + if ((current >= 0x40 && current <= 0x5B) || current == 0x5F) + { + return get_cbor_binary(b) && sax->binary(b); + } + + // not a byte string: the tagged value, whose first byte + // was just read, is read by the caller like for ignore + tag_pending = true; + item_read = true; + return true; } default: // LCOV_EXCL_LINE @@ -14200,13 +14313,14 @@ class binary_reader // a tag is not a value of its own: read on until the tagged value bool tag_pending = false; + bool item_read = false; do { - if (JSON_HEDLEY_UNLIKELY(!parse_cbor_value(fetch, tag_handler, tag_pending))) + if (JSON_HEDLEY_UNLIKELY(!parse_cbor_value(fetch, tag_handler, tag_pending, item_read))) { return false; } - fetch = true; + fetch = !item_read; } while (tag_pending); @@ -15881,6 +15995,549 @@ class binary_reader } } + ////////// + // BON8 // + ////////// + + /*! + @brief get the next byte of a BON8 value + + A BON8 string has no length prefix and no mandatory terminator: it ends at + the first byte that cannot continue it, which is already the first byte (or, + for an integer that begins with a UTF-8 lead byte, the first two bytes) of + whatever follows. The string reader hands those bytes back with + @ref unget_bon8, and every BON8 read goes through this function so that + they are seen again. + + @return character read from the input + */ + char_int_type get_bon8() + { + if (bon8_pushback_size != 0) + { + ++chars_read; + return current = bon8_pushback[--bon8_pushback_size]; + } + return get(); + } + + /*! + @brief hand a byte back so that the next @ref get_bon8 returns it again + + @param[in] c the byte to hand back; bytes handed back are returned in + reverse order + */ + void unget_bon8(const char_int_type c) + { + // At most two bytes are ever handed back: a byte is only handed back + // right after it was read with get_bon8(), and the only place that + // hands back two bytes (a lead byte and the byte after it) read both + // of them in a row, which emptied the buffer first. This is an + // invariant of the reader rather than a property of the input, so + // an assertion suffices (the fuzzers are built with assertions). + JSON_ASSERT(bon8_pushback_size < bon8_pushback.size()); + bon8_pushback[bon8_pushback_size++] = c; + --chars_read; + } + + /*! + @param[in] c a byte + @return whether @a c is a UTF-8 continuation byte (0x80..0xBF) + */ + static constexpr bool is_bon8_continuation(const char_int_type c) noexcept + { + return 0x80 <= c && c <= 0xBF; + } + + /*! + @brief report a parse error at the last read byte + + @param[in] detail a detailed error message + @param[in] context further context information + @return false + */ + bool bon8_error(const std::string& detail, const char* context) + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, + exception_message(input_format_t::bon8, concat(detail, ": 0x", last_token), context), nullptr)); + } + + /*! + @brief read a BON8 value and everything nested inside it + + Reads values until the one that was begun here is complete, resuming the + enclosing container after each element, so that the nesting depth of the + input costs heap rather than native stack (see #5104). + + @return whether reading the value succeeded + */ + bool parse_bon8_internal() + { + // the key currently being read; hoisted out of the loop so that its + // capacity is reused across elements and across nesting levels + string_t key; + + while (true) + { + if (!container_stack.empty()) + { + // a copy, not a reference: it must stay valid across the + // pop_back() below, which destroys the container_stack element + // it would otherwise alias + const container_frame top = container_stack.back(); + bool at_end = false; + + if (top.remaining != npos) + { + // counted container (0x80..0x84, 0x86..0x8A): it ends once + // its elements have been read + at_end = (top.remaining == 0); + if (!at_end) + { + // claim the element about to be read + --container_stack.back().remaining; + } + } + else + { + // container 0x85 or 0x8B: it ends at an end-of-container + // marker (0xFE); any other byte begins the next element + at_end = (get_bon8() == 0xFE); + if (!at_end) + { + unget_bon8(current); + } + } + + if (at_end) + { + container_stack.pop_back(); + if (JSON_HEDLEY_UNLIKELY(top.is_object ? !sax->end_object() : !sax->end_array())) + { + return false; + } + // the value begun here is complete once its container is + if (container_stack.empty()) + { + return true; + } + continue; + } + + if (top.is_object) + { + key.clear(); + if (JSON_HEDLEY_UNLIKELY(!get_bon8_key(key) || !sax->key(key))) + { + return false; + } + } + } + + if (JSON_HEDLEY_UNLIKELY(!parse_bon8_value())) + { + return false; + } + + // a value that opened a container left it on the stack; one that + // did not, and that was not inside a container, was the whole value + if (container_stack.empty()) + { + return true; + } + } + } + + /*! + @brief read one BON8 value + + Reads a single value and passes it to the SAX parser. A value that begins + a container is not read to its end: the container is opened with + @ref enter_container and its elements are read by + @ref parse_bon8_internal, so that nesting does not consume native stack. + + @return whether reading the value succeeded + */ + bool parse_bon8_value() + { + const auto byte = get_bon8(); + + if (byte == char_traits::eof()) + { + return unexpect_eof(input_format_t::bon8, "value"); + } + + // string: ASCII character + if (byte <= 0x7F) + { + string_t s; + unget_bon8(byte); + return get_bon8_string(s) && sax->string(s); + } + + // array with 0..4 elements + if (byte <= 0x84) + { + return enter_array(static_cast(byte - 0x80)); + } + + // array terminated by 0xFE + if (byte == 0x85) + { + return enter_array(npos); + } + + // object with 0..4 members + if (byte <= 0x8A) + { + return enter_object(static_cast(byte - 0x86)); + } + + switch (byte) + { + case 0x8B: // object terminated by 0xFE + return enter_object(npos); + + case 0x8C: // int32 + { + std::int32_t number{}; + return get_number(input_format_t::bon8, number) && emit_bon8_integer(number); + } + + case 0x8D: // int64 + { + std::int64_t number{}; + return get_number(input_format_t::bon8, number) && emit_bon8_integer(number); + } + + case 0x8E: // binary32 + { + float number{}; + return get_number(input_format_t::bon8, number) && sax->number_float(static_cast(number), ""); + } + + case 0x8F: // binary64 + { + double number{}; + return get_number(input_format_t::bon8, number) && sax->number_float(static_cast(number), ""); + } + + case 0xF8: + return sax->boolean(false); + + case 0xF9: + return sax->boolean(true); + + case 0xFA: + return sax->null(); + + case 0xFB: + return sax->number_float(static_cast(-1.0), ""); + + case 0xFC: + return sax->number_float(static_cast(0.0), ""); + + case 0xFD: + return sax->number_float(static_cast(1.0), ""); + + case 0xFF: // empty string + { + string_t s; + return sax->string(s); + } + + default: + break; + } + + // integer 0..39 + if (byte <= 0xB7) + { + return sax->number_unsigned(static_cast(byte - 0x90)); + } + + // integer -1..-10 + if (byte <= 0xC1) + { + return sax->number_integer(-1 - static_cast(byte - 0xB8)); + } + + // 0xC2..0xF7: a UTF-8 lead byte begins a string if a continuation + // byte follows and an integer otherwise + if (byte <= 0xF7) + { + const auto second = get_bon8(); + if (is_bon8_continuation(second)) + { + string_t s; + unget_bon8(second); + unget_bon8(byte); + return get_bon8_string(s) && sax->string(s); + } + return get_bon8_integer(byte, second); + } + + // 0xFE: end of container where a value is expected + return bon8_error("invalid byte", "value"); + } + + /*! + @brief pass an integer to the SAX parser + + Non-negative integers are passed as unsigned, negative integers as signed + numbers, like the other binary formats do. + + @param[in] number the integer + @return whether the SAX parser accepted the value + */ + bool emit_bon8_integer(const std::int64_t number) + { + if (number >= 0) + { + return sax->number_unsigned(static_cast(number)); + } + return sax->number_integer(static_cast(number)); + } + + /*! + @brief read an integer encoded in 2..4 bytes + + The first byte is a UTF-8 lead byte (0xC2..0xF7) that is followed by a + byte that is not a continuation byte: 0x00..0x7F for positive and + 0xC0..0xFF for negative integers. The lead byte's low bits and the second + byte's low 7 (positive) or 6 (negative) bits are the most significant bits + of the value; 3- and 4-byte integers add one or two full bytes. Each range + starts where the shorter one ends, so no value has two encodings of the + same length. + + @param[in] lead the first byte (0xC2..0xF7) + @param[in] second the second byte + @return whether reading the integer succeeded + */ + bool get_bon8_integer(const char_int_type lead, const char_int_type second) + { + if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::bon8, "number"))) + { + return false; + } + + const bool negative = second >= 0xC0; + auto value = static_cast(negative ? (second & 0x3F) : second); + std::int64_t offset = 0; + int extra_bytes = 0; + + if (lead <= 0xDF) + { + value |= static_cast(lead - 0xC2) << (negative ? 6 : 7); + offset = negative ? 11 : 40; + } + else if (lead <= 0xEF) + { + value |= static_cast(lead & 0x0F) << (negative ? 6 : 7); + offset = negative ? 1931 : 3880; + extra_bytes = 1; + } + else + { + value |= static_cast(lead & 0x07) << (negative ? 6 : 7); + offset = negative ? 264075 : 528168; + extra_bytes = 2; + } + + for (int i = 0; i < extra_bytes; ++i) + { + if (JSON_HEDLEY_UNLIKELY(get_bon8() == char_traits::eof())) + { + return unexpect_eof(input_format_t::bon8, "number"); + } + value = (value << 8) | static_cast(current); + } + + return negative ? sax->number_integer(static_cast(-(value + offset))) + : sax->number_unsigned(static_cast(value + offset)); + } + + /*! + @brief read an object key + + A key must be a string, so its first byte must be an ASCII character, a + UTF-8 lead byte followed by a continuation byte, or 0xFF (empty string). + + @param[out] result the key + @return whether reading the key succeeded + */ + bool get_bon8_key(string_t& result) + { + const auto byte = get_bon8(); + + if (byte == char_traits::eof()) + { + return unexpect_eof(input_format_t::bon8, "key"); + } + + if (byte == 0xFF) + { + return true; + } + + if (byte <= 0x7F) + { + unget_bon8(byte); + return get_bon8_string(result); + } + + if (0xC2 <= byte && byte <= 0xF7) + { + const auto second = get_bon8(); + unget_bon8(second); + if (is_bon8_continuation(second)) + { + unget_bon8(byte); + return get_bon8_string(result); + } + // an integer: report its first byte rather than the one after it + current = byte; + } + + return bon8_error("expected a string; last byte", "key"); + } + + /*! + @brief append the run of valid UTF-8 at the read position to a string + + For contiguous input, the ASCII characters and complete well-formed UTF-8 + sequences at the read position are appended to @a result in one step. The + byte that stops the run (an end-of-string marker, the first byte of the + next value, or an ill-formed byte) is left for @ref get_bon8_string, so + that strings end and errors are reported exactly as without this step. + + @param[in,out] result the string to append to + */ + void get_bon8_string_bulk(string_t& result, std::true_type /*bulk*/) + { + // bytes handed back must be read through get_bon8() first + if (bon8_pushback_size != 0) + { + return; + } + const std::size_t remaining = ia.bulk_remaining(); + if (remaining == 0) + { + return; + } + const auto* const data = reinterpret_cast(ia.bulk_data()); + const std::size_t length = valid_utf8_prefix(data, remaining); + if (length != 0) + { + result.append(reinterpret_cast(data), length); + ia.bulk_skip(length); + chars_read += length; + } + } + + /// input that is not contiguous: strings are read byte by byte + void get_bon8_string_bulk(string_t& /*result*/, std::false_type /*bulk*/) const noexcept {} + + /*! + @brief read a string + + Reads UTF-8 characters until an end-of-string marker (0xFF), which is + consumed, or a byte that cannot continue the string, which is handed back + to be read as the start of the next value. The string must be valid UTF-8, + and it must not end at the end of the input: the last string of a message + is always terminated by 0xFF. + + @param[out] result the string + @return whether reading the string succeeded + */ + bool get_bon8_string(string_t& result) + { + while (true) + { + get_bon8_string_bulk(result, std::integral_constant {}); + + const auto byte = get_bon8(); + + if (byte == char_traits::eof()) + { + return unexpect_eof(input_format_t::bon8, "string"); + } + + // end of string + if (byte == 0xFF) + { + return true; + } + + // ASCII character + if (byte <= 0x7F) + { + result.push_back(static_cast(byte)); + continue; + } + + // a byte that cannot begin a character ends the string and begins + // the next value + if (byte < 0xC2 || byte > 0xF7) + { + unget_bon8(byte); + return true; + } + + // a lead byte ends the string if no continuation byte follows: it + // is then the first byte of an integer + const auto second = get_bon8(); + if (!is_bon8_continuation(second)) + { + unget_bon8(second); + unget_bon8(byte); + return true; + } + + // the valid range of the second byte excludes overlong forms, + // surrogates, and code points above U+10FFFF + // (RFC 3629, section 4) + int continuation_bytes = 0; + bool valid_second = true; + if (byte <= 0xDF) + { + continuation_bytes = 1; + } + else if (byte <= 0xEF) + { + continuation_bytes = 2; + valid_second = (byte != 0xE0 || second >= 0xA0) && (byte != 0xED || second <= 0x9F); + } + else + { + continuation_bytes = 3; + valid_second = byte <= 0xF4 && (byte != 0xF0 || second >= 0x90) && (byte != 0xF4 || second <= 0x8F); + } + + if (JSON_HEDLEY_UNLIKELY(!valid_second)) + { + return bon8_error("invalid UTF-8 byte", "string"); + } + + result.push_back(static_cast(byte)); + result.push_back(static_cast(second)); + + for (int i = 1; i < continuation_bytes; ++i) + { + if (JSON_HEDLEY_UNLIKELY(get_bon8() == char_traits::eof())) + { + return unexpect_eof(input_format_t::bon8, "string"); + } + if (JSON_HEDLEY_UNLIKELY(!is_bon8_continuation(current))) + { + return bon8_error("invalid UTF-8 byte", "string"); + } + result.push_back(static_cast(current)); + } + } + } + /////////////////////// // Utility functions // /////////////////////// @@ -16179,6 +16836,10 @@ class binary_reader error_msg += "BJData"; break; + case input_format_t::bon8: + error_msg += "BON8"; + break; + case input_format_t::json: // LCOV_EXCL_LINE default: // LCOV_EXCL_LINE JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE @@ -16211,6 +16872,11 @@ class binary_reader /// the containers that have been opened and not closed yet; see @ref container_frame std::vector container_stack{}; + /// BON8: bytes read past the end of a string, returned again by @ref get_bon8 + std::array bon8_pushback{{}}; + /// BON8: number of bytes in @ref bon8_pushback + std::size_t bon8_pushback_size = 0; + // excluded markers in bjdata optimized type #define JSON_BINARY_READER_MAKE_BJD_OPTIMIZED_TYPE_MARKERS_ \ make_array('F', 'H', 'N', 'S', 'T', 'Z', '[', '{') @@ -19267,6 +19933,8 @@ NLOHMANN_JSON_NAMESPACE_END // #include +// #include + // #include // #include @@ -19541,7 +20209,7 @@ std::size_t binary_reserve_hint(const BasicJsonType& j) } /*! -@brief serialization to CBOR and MessagePack values +@brief serialization to BJData, BON8, BSON, CBOR, MessagePack, and UBJSON values */ template> class binary_writer @@ -19633,92 +20301,20 @@ class binary_writer if (j.m_data.m_value.number_integer >= 0) { // CBOR does not differentiate between positive signed - // integers and unsigned integers. Therefore, we used the - // code from the value_t::number_unsigned case here. - if (j.m_data.m_value.number_integer <= 0x17) - { - write_number(static_cast(j.m_data.m_value.number_integer)); - } - else if (j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x18)); - write_number(static_cast(j.m_data.m_value.number_integer)); - } - else if (j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x19)); - write_number(static_cast(j.m_data.m_value.number_integer)); - } - else if (j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x1A)); - write_number(static_cast(j.m_data.m_value.number_integer)); - } - else - { - oa.write_character(to_char_type(0x1B)); - write_number(static_cast(j.m_data.m_value.number_integer)); - } + // integers and unsigned integers + write_cbor_head(0x00, static_cast(j.m_data.m_value.number_integer)); } else { - // The conversions below encode the sign in the first - // byte, and the value is converted to a positive number. - const auto positive_number = -1 - j.m_data.m_value.number_integer; - if (j.m_data.m_value.number_integer >= -24) - { - write_number(static_cast(0x20 + positive_number)); - } - else if (positive_number <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x38)); - write_number(static_cast(positive_number)); - } - else if (positive_number <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x39)); - write_number(static_cast(positive_number)); - } - else if (positive_number <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x3A)); - write_number(static_cast(positive_number)); - } - else - { - oa.write_character(to_char_type(0x3B)); - write_number(static_cast(positive_number)); - } + // a negative integer n is encoded as -1 - n + write_cbor_head(0x20, static_cast(-1 - j.m_data.m_value.number_integer)); } break; } case value_t::number_unsigned: { - if (j.m_data.m_value.number_unsigned <= 0x17) - { - write_number(static_cast(j.m_data.m_value.number_unsigned)); - } - else if (j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x18)); - write_number(static_cast(j.m_data.m_value.number_unsigned)); - } - else if (j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x19)); - write_number(static_cast(j.m_data.m_value.number_unsigned)); - } - else if (j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x1A)); - write_number(static_cast(j.m_data.m_value.number_unsigned)); - } - else - { - oa.write_character(to_char_type(0x1B)); - write_number(static_cast(j.m_data.m_value.number_unsigned)); - } + write_cbor_head(0x00, j.m_data.m_value.number_unsigned); break; } @@ -19748,33 +20344,7 @@ class binary_writer case value_t::string: { // step 1: write control byte and the string length - const auto N = j.m_data.m_value.string->size(); - if (N <= 0x17) - { - write_number(static_cast(0x60 + N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x78)); - write_number(static_cast(N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x79)); - write_number(static_cast(N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x7A)); - write_number(static_cast(N)); - } - // LCOV_EXCL_START - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x7B)); - write_number(static_cast(N)); - } - // LCOV_EXCL_STOP + write_cbor_head(0x60, j.m_data.m_value.string->size()); // step 2: write the string oa.write_characters( @@ -19786,33 +20356,7 @@ class binary_writer case value_t::array: { // step 1: write control byte and the array size - const auto N = j.m_data.m_value.array->size(); - if (N <= 0x17) - { - write_number(static_cast(0x80 + N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x98)); - write_number(static_cast(N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x99)); - write_number(static_cast(N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x9A)); - write_number(static_cast(N)); - } - // LCOV_EXCL_START - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x9B)); - write_number(static_cast(N)); - } - // LCOV_EXCL_STOP + write_cbor_head(0x80, j.m_data.m_value.array->size()); // step 2: write each element for (const auto& el : *j.m_data.m_value.array) @@ -19841,7 +20385,7 @@ class binary_writer write_number(static_cast(0xda)); write_number(static_cast(j.m_data.m_value.binary->subtype())); } - else if (j.m_data.m_value.binary->subtype() <= (std::numeric_limits::max)()) + else { write_number(static_cast(0xdb)); write_number(static_cast(j.m_data.m_value.binary->subtype())); @@ -19850,32 +20394,7 @@ class binary_writer // step 1: write control byte and the binary array size const auto N = j.m_data.m_value.binary->size(); - if (N <= 0x17) - { - write_number(static_cast(0x40 + N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x58)); - write_number(static_cast(N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x59)); - write_number(static_cast(N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x5A)); - write_number(static_cast(N)); - } - // LCOV_EXCL_START - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0x5B)); - write_number(static_cast(N)); - } - // LCOV_EXCL_STOP + write_cbor_head(0x40, N); // step 2: write each element oa.write_characters( @@ -19888,33 +20407,7 @@ class binary_writer case value_t::object: { // step 1: write control byte and the object size - const auto N = j.m_data.m_value.object->size(); - if (N <= 0x17) - { - write_number(static_cast(0xA0 + N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0xB8)); - write_number(static_cast(N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0xB9)); - write_number(static_cast(N)); - } - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0xBA)); - write_number(static_cast(N)); - } - // LCOV_EXCL_START - else if (N <= (std::numeric_limits::max)()) - { - oa.write_character(to_char_type(0xBB)); - write_number(static_cast(N)); - } - // LCOV_EXCL_STOP + write_cbor_head(0xA0, j.m_data.m_value.object->size()); // step 2: write each element for (const auto& el : *j.m_data.m_value.object) @@ -19931,6 +20424,23 @@ class binary_writer } } + /*! + @brief check that @a length fits into the 32 bits that MessagePack stores + the length of a string, binary value, array, or object in + @return the length as an unsigned 32-bit integer + @throw out_of_range.412 if @a length exceeds the range of std::uint32_t + */ + static std::uint32_t to_msgpack_length(const std::size_t length, const BasicJsonType& j) + { + if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(length))) + { + JSON_THROW(out_of_range::create(412, concat("MessagePack length ", std::to_string(length), " exceeds maximum of ", std::to_string((std::numeric_limits::max)())), &j)); + } + + static_cast(j); + return static_cast(length); + } + /*! @param[in] j JSON value to serialize */ @@ -19982,7 +20492,7 @@ class binary_writer oa.write_character(to_char_type(0xCE)); write_number(static_cast(j.m_data.m_value.number_integer)); } - else if (j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) + else { // uint 64 oa.write_character(to_char_type(0xCF)); @@ -20017,8 +20527,7 @@ class binary_writer oa.write_character(to_char_type(0xD2)); write_number(static_cast(j.m_data.m_value.number_integer)); } - else if (j.m_data.m_value.number_integer >= (std::numeric_limits::min)() && - j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) + else { // int 64 oa.write_character(to_char_type(0xD3)); @@ -20053,7 +20562,7 @@ class binary_writer oa.write_character(to_char_type(0xCE)); write_number(static_cast(j.m_data.m_value.number_integer)); } - else if (j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) + else { // uint 64 oa.write_character(to_char_type(0xCF)); @@ -20071,7 +20580,7 @@ class binary_writer case value_t::string: { // step 1: write control byte and the string length - const auto N = j.m_data.m_value.string->size(); + const auto N = to_msgpack_length(j.m_data.m_value.string->size(), j); if (N <= 31) { // fixstr @@ -20089,7 +20598,7 @@ class binary_writer oa.write_character(to_char_type(0xDA)); write_number(static_cast(N)); } - else if (N <= (std::numeric_limits::max)()) + else { // str 32 oa.write_character(to_char_type(0xDB)); @@ -20106,7 +20615,7 @@ class binary_writer case value_t::array: { // step 1: write control byte and the array size - const auto N = j.m_data.m_value.array->size(); + const auto N = to_msgpack_length(j.m_data.m_value.array->size(), j); if (N <= 15) { // fixarray @@ -20118,7 +20627,7 @@ class binary_writer oa.write_character(to_char_type(0xDC)); write_number(static_cast(N)); } - else if (N <= (std::numeric_limits::max)()) + else { // array 32 oa.write_character(to_char_type(0xDD)); @@ -20140,7 +20649,7 @@ class binary_writer const bool use_ext = j.m_data.m_value.binary->has_subtype(); // step 1: write control byte and the byte string length - const auto N = j.m_data.m_value.binary->size(); + const auto N = to_msgpack_length(j.m_data.m_value.binary->size(), j); if (N <= (std::numeric_limits::max)()) { std::uint8_t output_type{}; @@ -20192,7 +20701,7 @@ class binary_writer oa.write_character(to_char_type(output_type)); write_number(static_cast(N)); } - else if (N <= (std::numeric_limits::max)()) + else { const std::uint8_t output_type = use_ext ? 0xC9 // ext 32 @@ -20224,7 +20733,7 @@ class binary_writer case value_t::object: { // step 1: write control byte and the object size - const auto N = j.m_data.m_value.object->size(); + const auto N = to_msgpack_length(j.m_data.m_value.object->size(), j); if (N <= 15) { // fixmap @@ -20236,7 +20745,7 @@ class binary_writer oa.write_character(to_char_type(0xDE)); write_number(static_cast(N)); } - else if (N <= (std::numeric_limits::max)()) + else { // map 32 oa.write_character(to_char_type(0xDF)); @@ -20497,6 +21006,21 @@ class binary_writer } } + /*! + @param[in] j JSON value to serialize + */ + void write_bon8(const BasicJsonType& j) + { + bool string_open = false; + write_bon8_value(j, string_open); + + // the last string of a message must be terminated + if (string_open) + { + oa.write_character(to_char_type(0xFF)); + } + } + private: ////////// // BSON // @@ -20991,6 +21515,46 @@ class binary_writer // CBOR // ////////// + /*! + @brief write the head of a CBOR data item + + The head is the major type in the upper three bits of the first byte and + an argument - an unsigned integer, the length of a string, the number of + elements of a container - in the shortest of its encodings: in the lower + five bits of the first byte itself if it is at most 23, otherwise in the + 1, 2, 4, or 8 bytes that follow (RFC 8949, section 3). + + @param[in] major_type the major type, shifted into the upper three bits + @param[in] argument the argument of the data item + */ + void write_cbor_head(const std::uint8_t major_type, const std::uint64_t argument) + { + if (argument <= 0x17) + { + write_number(static_cast(major_type + argument)); + } + else if (argument <= (std::numeric_limits::max)()) + { + oa.write_character(to_char_type(static_cast(major_type + 0x18))); + write_number(static_cast(argument)); + } + else if (argument <= (std::numeric_limits::max)()) + { + oa.write_character(to_char_type(static_cast(major_type + 0x19))); + write_number(static_cast(argument)); + } + else if (argument <= (std::numeric_limits::max)()) + { + oa.write_character(to_char_type(static_cast(major_type + 0x1A))); + write_number(static_cast(argument)); + } + else + { + oa.write_character(to_char_type(static_cast(major_type + 0x1B))); + write_number(argument); + } + } + static constexpr CharType get_cbor_float_prefix(float /*unused*/) { return to_char_type(0xFA); // Single-Precision Float @@ -21015,6 +21579,28 @@ class binary_writer return to_char_type(0xCB); // float 64 } + /// @return the BON8 type marker for binary32 (float) or binary64 (double) + template + static constexpr CharType get_bon8_float_prefix() + { + return to_char_type(std::is_same::value ? 0x8E : 0x8F); + } + + /// @return the type marker for a FloatType value in @a format (CBOR, MessagePack, or BON8) + template + static CharType get_compact_float_prefix(const detail::input_format_t format) + { + if (format == detail::input_format_t::cbor) + { + return get_cbor_float_prefix(FloatType{}); + } + if (format == detail::input_format_t::bon8) + { + return get_bon8_float_prefix(); + } + return get_msgpack_float_prefix(FloatType{}); + } + //////////// // UBJSON // //////////// @@ -21096,7 +21682,7 @@ class binary_writer } write_number(static_cast(n), use_bjdata); } - else if (use_bjdata && n <= (std::numeric_limits::max)()) + else if (use_bjdata) { if (add_prefix) { @@ -21176,30 +21762,59 @@ class binary_writer } write_number(static_cast(n), use_bjdata); } - else if ((std::numeric_limits::min)() <= n && n <= (std::numeric_limits::max)()) - { - if (add_prefix) - { - oa.write_character(to_char_type('L')); // int64 - } - write_number(static_cast(n), use_bjdata); - } - // LCOV_EXCL_START else { - if (add_prefix) - { - oa.write_character(to_char_type('H')); // high-precision number - } - - const auto number = BasicJsonType(n).dump(); - write_number_with_ubjson_prefix(number.size(), true, use_bjdata); - for (std::size_t i = 0; i < number.size(); ++i) - { - oa.write_character(to_char_type(static_cast(number[i]))); - } + // every value of an integer type of at most 64 bits fits into an + // int64; only a wider type needs a range check + write_ubjson_int64_or_high_precision(n, add_prefix, use_bjdata, + std::integral_constant < bool, std::numeric_limits::digits <= std::numeric_limits::digits > {}); } - // LCOV_EXCL_STOP + } + + template + void write_ubjson_int64_or_high_precision(const NumberType n, const bool add_prefix, const bool use_bjdata, std::true_type /*fits_int64*/) + { + if (add_prefix) + { + oa.write_character(to_char_type('L')); // int64 + } + write_number(static_cast(n), use_bjdata); + } + + template + void write_ubjson_int64_or_high_precision(const NumberType n, const bool add_prefix, const bool use_bjdata, std::false_type /*fits_int64*/) + { + if ((std::numeric_limits::min)() <= n && n <= (std::numeric_limits::max)()) + { + write_ubjson_int64_or_high_precision(n, add_prefix, use_bjdata, std::true_type {}); + return; + } + + if (add_prefix) + { + oa.write_character(to_char_type('H')); // high-precision number + } + + const auto number = BasicJsonType(n).dump(); + write_number_with_ubjson_prefix(number.size(), true, use_bjdata); + for (std::size_t i = 0; i < number.size(); ++i) + { + oa.write_character(to_char_type(static_cast(number[i]))); + } + } + + template + static constexpr CharType ubjson_int64_or_high_precision_prefix(const NumberType /*n*/, std::true_type /*fits_int64*/) noexcept + { + return 'L'; + } + + template + static CharType ubjson_int64_or_high_precision_prefix(const NumberType n, std::false_type /*fits_int64*/) noexcept + { + // anything outside of the range of an int64 is treated as a + // high-precision number + return ((std::numeric_limits::min)() <= n && n <= (std::numeric_limits::max)()) ? 'L' : 'H'; } /*! @@ -21241,12 +21856,10 @@ class binary_writer { return 'm'; } - if ((std::numeric_limits::min)() <= j.m_data.m_value.number_integer && j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) - { - return 'L'; - } - // anything else is treated as a high-precision number - return 'H'; // LCOV_EXCL_LINE + // every value of an integer type of at most 64 bits fits into + // an int64; only a wider type needs a range check + return ubjson_int64_or_high_precision_prefix(j.m_data.m_value.number_integer, + std::integral_constant < bool, std::numeric_limits::digits <= std::numeric_limits::digits > {}); } case value_t::number_unsigned: @@ -21279,12 +21892,12 @@ class binary_writer { return 'L'; } - if (use_bjdata && j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) + if (use_bjdata) { return 'M'; } // anything else is treated as a high-precision number - return 'H'; // LCOV_EXCL_LINE + return 'H'; } case value_t::number_float: @@ -21607,6 +22220,322 @@ class binary_writer return false; } + ////////// + // BON8 // + ////////// + + /*! + @brief write a BON8 value + + A string is written without length or terminator: it ends at the first + byte that cannot continue it, which is the first byte of any non-string + value and of the end-of-container marker 0xFE. It only needs an explicit + end-of-string marker (0xFF) when it is empty, when another string follows, + or when it is the last thing in the message. + + @param[in] j JSON value to serialize + @param[in,out] string_open whether the output ends with a non-empty + string that has not been terminated with 0xFF + */ + void write_bon8_value(const BasicJsonType& j, bool& string_open) + { + switch (j.type()) + { + case value_t::null: + { + write_bon8_marker(0xFA, string_open); + break; + } + + case value_t::boolean: + { + write_bon8_marker(j.m_data.m_value.boolean ? 0xF9 : 0xF8, string_open); + break; + } + + case value_t::number_unsigned: + { + if (j.m_data.m_value.number_unsigned > static_cast((std::numeric_limits::max)())) + { + JSON_THROW(out_of_range::create(407, concat("integer number ", std::to_string(j.m_data.m_value.number_unsigned), " cannot be represented by BON8 as it does not fit int64"), &j)); + } + write_bon8_integer(static_cast(j.m_data.m_value.number_unsigned)); + string_open = false; + break; + } + + case value_t::number_integer: + { + write_bon8_integer(static_cast(j.m_data.m_value.number_integer)); + string_open = false; + break; + } + + case value_t::number_float: + { + write_bon8_float(j.m_data.m_value.number_float); + string_open = false; + break; + } + + case value_t::string: + { + write_bon8_string(*j.m_data.m_value.string, string_open, j); + break; + } + + case value_t::array: + { + const auto N = j.m_data.m_value.array->size(); + // 0x80..0x84: array with 0..4 elements; 0x85: array ended by 0xFE + write_bon8_marker(static_cast(N <= 4 ? 0x80 + N : 0x85), string_open); + + for (const auto& el : *j.m_data.m_value.array) + { + write_bon8_value(el, string_open); + } + + if (N > 4) + { + write_bon8_marker(0xFE, string_open); + } + break; + } + + case value_t::object: + { + const auto N = j.m_data.m_value.object->size(); + // 0x86..0x8A: object with 0..4 members; 0x8B: object ended by 0xFE + write_bon8_marker(static_cast(N <= 4 ? 0x86 + N : 0x8B), string_open); + + for (const auto& el : *j.m_data.m_value.object) + { + write_bon8_string(el.first, string_open, j); + write_bon8_value(el.second, string_open); + } + + if (N > 4) + { + write_bon8_marker(0xFE, string_open); + } + break; + } + + case value_t::binary: + { + // BON8 has no binary type: write the bytes as an array of + // integers, like UBJSON and BJData do + const auto N = j.m_data.m_value.binary->size(); + write_bon8_marker(static_cast(N <= 4 ? 0x80 + N : 0x85), string_open); + + for (std::size_t i = 0; i < N; ++i) + { + // the cast is needed for binary types whose value type + // is not an integer (e.g., std::byte) + write_bon8_integer(static_cast(j.m_data.m_value.binary->data()[i])); + } + + if (N > 4) + { + write_bon8_marker(0xFE, string_open); + } + break; + } + + case value_t::discarded: + default: + break; + } + } + + /*! + @brief write a single byte that is not part of a string + + @param[in] marker the byte to write + @param[out] string_open set to false, because the output no longer ends + with a string; see @ref write_bon8_value + */ + void write_bon8_marker(const std::uint8_t marker, bool& string_open) + { + oa.write_character(to_char_type(marker)); + string_open = false; + } + + /*! + @brief write a string + + @param[in] s the string to write + @param[in,out] string_open see @ref write_bon8_value + @param[in] context the value the string belongs to (for diagnostics) + + @throw type_error.316 if @a s is not valid UTF-8, because the end of a + string is determined from its encoding + */ + void write_bon8_string(const string_t& s, bool& string_open, const BasicJsonType& context) + { + check_bon8_utf8(s, context); + + // a string that follows another string terminates it + if (string_open) + { + oa.write_character(to_char_type(0xFF)); + } + + if (s.empty()) + { + // the empty string is just the end-of-string marker + oa.write_character(to_char_type(0xFF)); + string_open = false; + } + else + { + oa.write_characters(reinterpret_cast(s.data()), s.size()); + string_open = true; + } + } + + /*! + @brief check that a string is valid UTF-8 (RFC 3629) + + @param[in] s the string to check + @param[in] context the value the string belongs to (for diagnostics) + + @throw type_error.316 if @a s is not valid UTF-8; the message names the + first byte of the first invalid or incomplete sequence + */ + static void check_bon8_utf8(const string_t& s, const BasicJsonType& context) + { + static_cast(context); // only used when exceptions are enabled + const auto* data = reinterpret_cast(s.data()); + const std::size_t valid = valid_utf8_prefix(data, s.size()); + if (JSON_HEDLEY_UNLIKELY(valid != s.size())) + { + JSON_THROW(type_error::create(316, concat("invalid UTF-8 byte at index ", std::to_string(valid), ": 0x", hex_byte(data[valid])), &context)); + } + } + + /// @return a byte as two uppercase hexadecimal digits + static std::string hex_byte(const std::uint8_t byte) + { + std::string result = "00"; + constexpr const char* nibble_to_hex = "0123456789ABCDEF"; + result[0] = nibble_to_hex[byte / 16]; + result[1] = nibble_to_hex[byte % 16]; + return result; + } + + /*! + @brief write an integer in the shortest encoding + + Integers from -10 to 39 take one byte. Up to -33818506 and 67637031, an + integer takes 2 to 4 bytes that begin with a UTF-8 lead byte (0xC2..0xF7) + followed by a byte that is not a continuation byte: 0x00..0x7F for + positive and 0xC0..0xFF for negative integers. Each range starts where the + shorter one ends. Larger integers are written as int32 (0x8C) or int64 + (0x8D) in big-endian byte order. + + @param[in] value the integer to write + */ + void write_bon8_integer(std::int64_t value) + { + if (value < (std::numeric_limits::min)() || value > (std::numeric_limits::max)()) + { + oa.write_character(to_char_type(0x8D)); + write_number(value); + } + else if (value < -33818506 || value > 67637031) + { + oa.write_character(to_char_type(0x8C)); + write_number(static_cast(value)); + } + else if (value <= -264075) + { + value = -(value + 264075); + write_bon8_bytes(0xF0 + ((value >> 22) & 0x07), 0xC0 + ((value >> 16) & 0x3F), value >> 8, value); + } + else if (value <= -1931) + { + value = -(value + 1931); + write_bon8_bytes(0xE0 + ((value >> 14) & 0x0F), 0xC0 + ((value >> 8) & 0x3F), value); + } + else if (value <= -11) + { + value = -(value + 11); + write_bon8_bytes(0xC2 + ((value >> 6) & 0x1F), 0xC0 + (value & 0x3F)); + } + else if (value <= -1) + { + write_bon8_bytes(0xB8 - (value + 1)); + } + else if (value <= 39) + { + write_bon8_bytes(0x90 + value); + } + else if (value <= 3879) + { + value -= 40; + write_bon8_bytes(0xC2 + ((value >> 7) & 0x1F), value & 0x7F); + } + else if (value <= 528167) + { + value -= 3880; + write_bon8_bytes(0xE0 + ((value >> 15) & 0x0F), (value >> 8) & 0x7F, value); + } + else + { + value -= 528168; + write_bon8_bytes(0xF0 + ((value >> 23) & 0x07), (value >> 16) & 0x7F, value >> 8, value); + } + } + + /// write the low byte of each argument + template + void write_bon8_bytes(const Bytes... bytes) + { + const std::array buffer{{to_char_type(static_cast(bytes & 0xFF))...}}; + oa.write_characters(buffer.data(), buffer.size()); + } + + /*! + @brief write a floating-point number + + -1.0, +0.0, and 1.0 take one byte. Other numbers are written as binary32 + (0x8E) if that loses no precision, and as binary64 (0x8F) otherwise; -0.0, + infinities, and NaN are always written as binary32, NaN as 0x7F800001. + + @param[in] n the number to write + */ + void write_bon8_float(const number_float_t n) + { +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_PUSH + JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") +#endif + if (n == static_cast(-1)) + { + oa.write_character(to_char_type(0xFB)); + } + else if (n == static_cast(0) && !std::signbit(n)) + { + oa.write_character(to_char_type(0xFC)); + } + else if (n == static_cast(1)) + { + oa.write_character(to_char_type(0xFD)); + } + else if (std::isnan(n)) + { + write_bon8_bytes(0x8E, 0x7F, 0x80, 0x00, 0x01); + } + else + { + write_compact_float(n, detail::input_format_t::bon8); + } +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_POP +#endif + } + /////////////////////// // Utility functions // /////////////////////// @@ -21741,16 +22670,12 @@ class binary_writer static_cast(n) <= static_cast((std::numeric_limits::max)()) && static_cast(static_cast(n)) == static_cast(n)))) { - oa.write_character(format == detail::input_format_t::cbor - ? get_cbor_float_prefix(static_cast(n)) - : get_msgpack_float_prefix(static_cast(n))); + oa.write_character(get_compact_float_prefix(format)); write_number(static_cast(n)); } else { - oa.write_character(format == detail::input_format_t::cbor - ? get_cbor_float_prefix(n) - : get_msgpack_float_prefix(n)); + oa.write_character(get_compact_float_prefix(format)); write_number(n); } #ifdef __GNUC__ @@ -26450,13 +27375,28 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec compare_keys(current.lhs_object_it->first, current.rhs_object_it->first, std::integral_constant {}); - if (key_result != compare_result::equal) - { - return key_result; - } - left = &(current.lhs_object_it->second); right = &(current.rhs_object_it->second); + + if (key_result != compare_result::equal) + { + // An object type without a fixed order of its entries - + // std::unordered_map, say - may enumerate two equal + // objects differently, and its operator== does not care. + // Equality then finds the entry by its key; an ordering, + // or an object type that compares its entries in + // sequence (ordered_map), is decided by the key itself. + const auto* rhs_object = current.rhs_value->m_data.m_value.object; + const auto found = (!Ordered && !detail::is_ordered_map::value) + ? rhs_object->find(current.lhs_object_it->first) + : rhs_object->cend(); + if (found == rhs_object->cend()) + { + return key_result; + } + right = &(found->second); + } + ++current.lhs_object_it; ++current.rhs_object_it; } @@ -27115,17 +28055,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // value access // ////////////////// - /// get a boolean (explicit) - boolean_t get_impl(boolean_t* /*unused*/) const - { - if (JSON_HEDLEY_LIKELY(is_boolean())) - { - return m_data.m_value.boolean; - } - - JSON_THROW(type_error::create(302, detail::concat("type must be boolean, but is ", type_name()), this)); - } - /// get a pointer to the value (object) object_t* get_impl_ptr(object_t* /*unused*/) noexcept { @@ -30244,6 +31173,30 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec binary_writer(o).write_bson(j); } + /// @brief create a BON8 serialization of a given JSON value + /// @sa https://json.nlohmann.me/api/basic_json/to_bon8/ + static std::vector to_bon8(const basic_json& j) + { + std::vector result; + result.reserve(detail::binary_reserve_hint(j)); + vector_writer(result).write_bon8(j); + return result; + } + + /// @brief create a BON8 serialization of a given JSON value + /// @sa https://json.nlohmann.me/api/basic_json/to_bon8/ + static void to_bon8(const basic_json& j, detail::output_adapter o) + { + binary_writer(o).write_bon8(j); + } + + /// @brief create a BON8 serialization of a given JSON value + /// @sa https://json.nlohmann.me/api/basic_json/to_bon8/ + static void to_bon8(const basic_json& j, detail::output_adapter o) + { + binary_writer(o).write_bon8(j); + } + /// @brief create a JSON value from an input in CBOR format /// @sa https://json.nlohmann.me/api/basic_json/from_cbor/ template @@ -30477,6 +31430,43 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec return result; } + /// @brief create a JSON value from an input in BON8 format + /// @sa https://json.nlohmann.me/api/basic_json/from_bon8/ + template + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json from_bon8(InputType&& i, + const bool strict = true, + const bool allow_exceptions = true) + { + basic_json result; + auto ia = detail::input_adapter(std::forward(i)); + detail::json_sax_dom_parser sdp(result, allow_exceptions); + if (!binary_reader(std::move(ia), input_format_t::bon8).sax_parse(input_format_t::bon8, &sdp, strict)) // cppcheck-suppress[accessMoved] + { + result = value_t::discarded; + } + return result; + } + + /// @brief create a JSON value from an input in BON8 format (iterator pair, or iterator+sentinel pair for C++20 ranges support) + /// @sa https://json.nlohmann.me/api/basic_json/from_bon8/ + template::value, int> = 0> + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json from_bon8(IteratorType first, SentinelType last, + const bool strict = true, + const bool allow_exceptions = true) + { + basic_json result; + auto ia = detail::input_adapter(std::move(first), std::move(last)); + detail::json_sax_dom_parser sdp(result, allow_exceptions); + if (!binary_reader(std::move(ia), input_format_t::bon8).sax_parse(input_format_t::bon8, &sdp, strict)) // cppcheck-suppress[accessMoved] + { + result = value_t::discarded; + } + return result; + } + /// @brief create a JSON value from an input in BSON format /// @sa https://json.nlohmann.me/api/basic_json/from_bson/ template diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ea6da107c..8c81b9123 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -112,7 +112,7 @@ endif() if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") # avoid stack overflow, see https://github.com/nlohmann/json/issues/2955 - json_test_set_test_options("test-cbor;test-msgpack;test-ubjson;test-bjdata;test-binary_formats" LINK_OPTIONS /STACK:4000000) + json_test_set_test_options("test-bon8;test-cbor;test-msgpack;test-ubjson;test-bjdata;test-binary_formats" LINK_OPTIONS /STACK:4000000) endif() # disable exceptions for test-disabled_exceptions diff --git a/tests/Makefile b/tests/Makefile index 3a11ce7dd..6bfa14397 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -10,7 +10,7 @@ CXXFLAGS += -std=c++11 CPPFLAGS += -I ../single_include FUZZER_ENGINE = src/fuzzer-driver_afl.cpp -FUZZERS = parse_afl_fuzzer parse_bson_fuzzer parse_cbor_fuzzer parse_msgpack_fuzzer parse_ubjson_fuzzer parse_bjdata_fuzzer +FUZZERS = parse_afl_fuzzer parse_bson_fuzzer parse_cbor_fuzzer parse_msgpack_fuzzer parse_ubjson_fuzzer parse_bjdata_fuzzer parse_bon8_fuzzer fuzzers: $(FUZZERS) parse_afl_fuzzer: @@ -30,3 +30,6 @@ parse_ubjson_fuzzer: parse_bjdata_fuzzer: $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(FUZZER_ENGINE) src/fuzzer-parse_bjdata.cpp -o $@ + +parse_bon8_fuzzer: + $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(FUZZER_ENGINE) src/fuzzer-parse_bon8.cpp -o $@ diff --git a/tests/benchmarks/src/benchmarks.cpp b/tests/benchmarks/src/benchmarks.cpp index 613ca1baa..2ad28a57a 100644 --- a/tests/benchmarks/src/benchmarks.cpp +++ b/tests/benchmarks/src/benchmarks.cpp @@ -274,7 +274,8 @@ enum class binary_format ubjson_optimized, bjdata, bjdata_optimized, - bson + bson, + bon8 }; static std::vector to_binary(const json& j, const binary_format format) @@ -293,6 +294,8 @@ static std::vector to_binary(const json& j, const binary_format fo return json::to_bjdata(j); case binary_format::bjdata_optimized: return json::to_bjdata(j, true, true); + case binary_format::bon8: + return json::to_bon8(j); case binary_format::bson: default: return json::to_bson(j); @@ -313,6 +316,8 @@ static json from_binary(const std::vector& bytes, const binary_for case binary_format::bjdata: case binary_format::bjdata_optimized: return json::from_bjdata(bytes); + case binary_format::bon8: + return json::from_bon8(bytes); case binary_format::bson: default: return json::from_bson(bytes); @@ -333,6 +338,8 @@ static json from_binary(std::FILE* file, const binary_format format) case binary_format::bjdata: case binary_format::bjdata_optimized: return json::from_bjdata(file); + case binary_format::bon8: + return json::from_bon8(file); case binary_format::bson: default: return json::from_bson(file); @@ -407,6 +414,10 @@ BENCHMARK_CAPTURE(FromBinaryBuffer, bjdata / canada, TEST_DATA_DIRECTORY "/nativ BENCHMARK_CAPTURE(FromBinaryBuffer, bjdata / twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json", binary_format::bjdata); BENCHMARK_CAPTURE(FromBinaryBuffer, bjdata_optimized / canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json", binary_format::bjdata_optimized); BENCHMARK_CAPTURE(FromBinaryBuffer, bjdata_optimized / twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json", binary_format::bjdata_optimized); +BENCHMARK_CAPTURE(FromBinaryBuffer, bon8 / jeopardy, TEST_DATA_DIRECTORY "/jeopardy/jeopardy.json", binary_format::bon8); +BENCHMARK_CAPTURE(FromBinaryBuffer, bon8 / canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json", binary_format::bon8); +BENCHMARK_CAPTURE(FromBinaryBuffer, bon8 / citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json", binary_format::bon8); +BENCHMARK_CAPTURE(FromBinaryBuffer, bon8 / twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json", binary_format::bon8); // BSON requires an object at the top level, so the array-rooted test files // (jeopardy and the regression files) cannot be captured here BENCHMARK_CAPTURE(FromBinaryBuffer, bson / canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json", binary_format::bson); @@ -450,6 +461,8 @@ BENCHMARK_CAPTURE(FromBinaryFile, cbor / twitter, TEST_DATA_DIRECTORY "/nativejs BENCHMARK_CAPTURE(FromBinaryFile, ubjson / canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json", binary_format::ubjson); BENCHMARK_CAPTURE(FromBinaryFile, ubjson / twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json", binary_format::ubjson); BENCHMARK_CAPTURE(FromBinaryFile, bjdata / twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json", binary_format::bjdata); +BENCHMARK_CAPTURE(FromBinaryFile, bon8 / canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json", binary_format::bon8); +BENCHMARK_CAPTURE(FromBinaryFile, bon8 / twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json", binary_format::bon8); BENCHMARK_CAPTURE(FromBinaryFile, bson / twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json", binary_format::bson); ////////////////////////////////////////////////////////////////////////////// @@ -530,18 +543,21 @@ BENCHMARK_CAPTURE(FromBinaryShape, nested / msgpack, make_nested, binary_format: BENCHMARK_CAPTURE(FromBinaryShape, nested / ubjson, make_nested, binary_format::ubjson); BENCHMARK_CAPTURE(FromBinaryShape, nested / bjdata, make_nested, binary_format::bjdata); BENCHMARK_CAPTURE(FromBinaryShape, nested / bson, make_nested, binary_format::bson); +BENCHMARK_CAPTURE(FromBinaryShape, nested / bon8, make_nested, binary_format::bon8); BENCHMARK_CAPTURE(FromBinaryShape, containers / cbor, make_containers, binary_format::cbor); BENCHMARK_CAPTURE(FromBinaryShape, containers / msgpack, make_containers, binary_format::msgpack); BENCHMARK_CAPTURE(FromBinaryShape, containers / ubjson, make_containers, binary_format::ubjson); BENCHMARK_CAPTURE(FromBinaryShape, containers / ubjson_optimized, make_containers, binary_format::ubjson_optimized); BENCHMARK_CAPTURE(FromBinaryShape, containers / bjdata, make_containers, binary_format::bjdata); BENCHMARK_CAPTURE(FromBinaryShape, containers / bson, make_containers, binary_format::bson); +BENCHMARK_CAPTURE(FromBinaryShape, containers / bon8, make_containers, binary_format::bon8); // BSON names every array element, so a large array measures key generation // rather than scalar decoding and is left out here BENCHMARK_CAPTURE(FromBinaryShape, scalars / cbor, make_scalars, binary_format::cbor); BENCHMARK_CAPTURE(FromBinaryShape, scalars / msgpack, make_scalars, binary_format::msgpack); BENCHMARK_CAPTURE(FromBinaryShape, scalars / ubjson, make_scalars, binary_format::ubjson); BENCHMARK_CAPTURE(FromBinaryShape, scalars / bjdata, make_scalars, binary_format::bjdata); +BENCHMARK_CAPTURE(FromBinaryShape, scalars / bon8, make_scalars, binary_format::bon8); /*! @brief parse an indefinite-length CBOR string diff --git a/tests/fuzzing.md b/tests/fuzzing.md index b3bf90d5d..49ab7b575 100644 --- a/tests/fuzzing.md +++ b/tests/fuzzing.md @@ -1,6 +1,6 @@ # Fuzz testing -Each parser of the library (JSON, BJData, BSON, CBOR, MessagePack, and UBJSON) can be fuzz tested. Currently, +Each parser of the library (JSON, BJData, BON8, BSON, CBOR, MessagePack, and UBJSON) can be fuzz tested. Currently, [libFuzzer](https://llvm.org/docs/LibFuzzer.html) and [afl++](https://github.com/AFLplusplus/AFLplusplus) are supported. ## Corpus creation @@ -10,11 +10,11 @@ directory with some simple input files that cover several features of the parser for mutations. ```shell -TEST_DATA_VERSION=3.1.0 +TEST_DATA_VERSION=3.2.0 wget https://github.com/nlohmann/json_test_data/archive/refs/tags/v$TEST_DATA_VERSION.zip unzip v$TEST_DATA_VERSION.zip rm v$TEST_DATA_VERSION.zip -for FORMAT in json bjdata bson cbor msgpack ubjson +for FORMAT in json bjdata bon8 bson cbor msgpack ubjson do rm -fr corpus_$FORMAT mkdir corpus_$FORMAT diff --git a/tests/src/fuzzer-parse_bon8.cpp b/tests/src/fuzzer-parse_bon8.cpp new file mode 100644 index 000000000..e97d4f17b --- /dev/null +++ b/tests/src/fuzzer-parse_bon8.cpp @@ -0,0 +1,103 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +/* +This file implements a parser test suitable for fuzz testing. Given a byte +array data, it performs the following steps: + +- j1 = from_bon8(data) +- vec = to_bon8(j1) +- j2 = from_bon8(vec) +- assert(j1 == j2) + +It also checks that reading the data from a stream, which reads strings byte by +byte, gives the same value or error as reading it from contiguous memory, which +copies strings in bulk. + +The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer +drivers. +*/ + +#include +#include +#include +#include + +// the round-trip checks below are assertions; NDEBUG would compile them away +#ifdef NDEBUG + #error "the fuzzer drivers must be built without NDEBUG" +#endif + +using json = nlohmann::json; + +namespace +{ +// the serialization of the value read from @a input, or the error message +template +std::string read_bon8(InputType&& input) +{ + try + { + const auto vec = json::to_bon8(json::from_bon8(std::forward(input))); + return {vec.begin(), vec.end()}; + } + catch (const json::exception& e) + { + return e.what(); + } +} +} // namespace + +// see http://llvm.org/docs/LibFuzzer.html +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + // contiguous and stream input must be read alike + { + std::istringstream stream(std::string(reinterpret_cast(data), size)); + assert(read_bon8(std::vector(data, data + size)) == read_bon8(stream)); + } + + try + { + // step 1: parse input + std::vector const vec1(data, data + size); + json const j1 = json::from_bon8(vec1); + + try + { + // step 2: round trip + std::vector const vec2 = json::to_bon8(j1); + + // parse serialization + json const j2 = json::from_bon8(vec2); + + // serializations must match + assert(json::to_bon8(j2) == vec2); + } + catch (const json::parse_error&) + { + // parsing a BON8 serialization must not fail + assert(false); + } + } + catch (const json::parse_error&) + { + // parse errors are ok, because input may be random bytes + } + catch (const json::type_error&) + { + // type errors can occur during parsing, too + } + catch (const json::out_of_range&) + { + // out of range errors may happen if provided sizes are excessive + } + + // return 0 - non-zero return values are reserved for future use + return 0; +} diff --git a/tests/src/test_utils.hpp b/tests/src/test_utils.hpp index 4c81a8ef4..b2a381fb0 100644 --- a/tests/src/test_utils.hpp +++ b/tests/src/test_utils.hpp @@ -9,6 +9,7 @@ #pragma once #include // uint8_t +#include // size_t #include // ifstream, istreambuf_iterator, ios #include // vector @@ -24,6 +25,23 @@ namespace utils template inline void ignore_return_value(T&& /*unused*/) noexcept {} +// Advance i toward last (inclusive) by stride, always visiting last. +// stride 7 is coprime to 256, so every low-byte residue is still hit. +template +T next_integer_sample(T i, T last, T stride) +{ + if (i >= last) + { + return static_cast(last + 1); + } + if (stride > 0 && i > static_cast(last - stride)) + { + return last; + } + const T n = static_cast(i + stride); + return n < last ? n : last; +} + inline std::vector read_binary_file(const std::string& filename) { std::ifstream file(filename, std::ios::binary); diff --git a/tests/src/unit-allocator.cpp b/tests/src/unit-allocator.cpp index cdc532e3a..9dde143c2 100644 --- a/tests/src/unit-allocator.cpp +++ b/tests/src/unit-allocator.cpp @@ -37,6 +37,12 @@ struct bad_allocator : std::allocator }; } // namespace +TEST_CASE("get_allocator") +{ + const auto alloc = nlohmann::json::get_allocator(); + CHECK(alloc == std::allocator()); +} + TEST_CASE("bad_alloc") { SECTION("bad_alloc") diff --git a/tests/src/unit-alt-string.cpp b/tests/src/unit-alt-string.cpp index ec2ac146b..cddaadb7e 100644 --- a/tests/src/unit-alt-string.cpp +++ b/tests/src/unit-alt-string.cpp @@ -185,6 +185,7 @@ TEST_CASE("alternative string type") CHECK(alt_json::from_cbor(alt_json::to_cbor(doc)) == doc); CHECK(alt_json::from_msgpack(alt_json::to_msgpack(doc)) == doc); + CHECK(alt_json::from_bon8(alt_json::to_bon8(doc)) == doc); // BSON is not covered: it additionally needs string_t::find(value_type), // which alt_string does not provide CHECK(alt_json::from_ubjson(alt_json::to_ubjson(doc)) == doc); diff --git a/tests/src/unit-binary_formats.cpp b/tests/src/unit-binary_formats.cpp index 0c084b720..ed6d89911 100644 --- a/tests/src/unit-binary_formats.cpp +++ b/tests/src/unit-binary_formats.cpp @@ -25,6 +25,7 @@ TEST_CASE("Binary Formats" * doctest::skip()) const auto bjdata_1_size = json::to_bjdata(j).size(); const auto bjdata_2_size = json::to_bjdata(j, true).size(); const auto bjdata_3_size = json::to_bjdata(j, true, true).size(); + const auto bon8_size = json::to_bon8(j).size(); const auto bson_size = json::to_bson(j).size(); const auto cbor_size = json::to_cbor(j).size(); const auto msgpack_size = json::to_msgpack(j).size(); @@ -36,6 +37,7 @@ TEST_CASE("Binary Formats" * doctest::skip()) CHECK(bjdata_1_size == 1112030); CHECK(bjdata_2_size == 1224148); CHECK(bjdata_3_size == 1224148); + CHECK(bon8_size == 1055792); CHECK(bson_size == 1794522); CHECK(cbor_size == 1055552); CHECK(msgpack_size == 1056145); @@ -47,6 +49,7 @@ TEST_CASE("Binary Formats" * doctest::skip()) CHECK((100.0 * double(bjdata_1_size) / double(json_size)) == Approx(53.199)); CHECK((100.0 * double(bjdata_2_size) / double(json_size)) == Approx(58.563)); CHECK((100.0 * double(bjdata_3_size) / double(json_size)) == Approx(58.563)); + CHECK((100.0 * double(bon8_size) / double(json_size)) == Approx(50.509)); CHECK((100.0 * double(bson_size) / double(json_size)) == Approx(85.849)); CHECK((100.0 * double(cbor_size) / double(json_size)) == Approx(50.497)); CHECK((100.0 * double(msgpack_size) / double(json_size)) == Approx(50.526)); @@ -64,6 +67,7 @@ TEST_CASE("Binary Formats" * doctest::skip()) const auto bjdata_1_size = json::to_bjdata(j).size(); const auto bjdata_2_size = json::to_bjdata(j, true).size(); const auto bjdata_3_size = json::to_bjdata(j, true, true).size(); + const auto bon8_size = json::to_bon8(j).size(); const auto bson_size = json::to_bson(j).size(); const auto cbor_size = json::to_cbor(j).size(); const auto msgpack_size = json::to_msgpack(j).size(); @@ -75,6 +79,7 @@ TEST_CASE("Binary Formats" * doctest::skip()) CHECK(bjdata_1_size == 425342); CHECK(bjdata_2_size == 429970); CHECK(bjdata_3_size == 429970); + CHECK(bon8_size == 391396); CHECK(bson_size == 444568); CHECK(cbor_size == 402814); CHECK(msgpack_size == 401510); @@ -86,6 +91,7 @@ TEST_CASE("Binary Formats" * doctest::skip()) CHECK((100.0 * double(bjdata_1_size) / double(json_size)) == Approx(91.097)); CHECK((100.0 * double(bjdata_2_size) / double(json_size)) == Approx(92.089)); CHECK((100.0 * double(bjdata_3_size) / double(json_size)) == Approx(92.089)); + CHECK((100.0 * double(bon8_size) / double(json_size)) == Approx(83.828)); CHECK((100.0 * double(bson_size) / double(json_size)) == Approx(95.215)); CHECK((100.0 * double(cbor_size) / double(json_size)) == Approx(86.273)); CHECK((100.0 * double(msgpack_size) / double(json_size)) == Approx(85.993)); @@ -103,6 +109,7 @@ TEST_CASE("Binary Formats" * doctest::skip()) const auto bjdata_1_size = json::to_bjdata(j).size(); const auto bjdata_2_size = json::to_bjdata(j, true).size(); const auto bjdata_3_size = json::to_bjdata(j, true, true).size(); + const auto bon8_size = json::to_bon8(j).size(); const auto bson_size = json::to_bson(j).size(); const auto cbor_size = json::to_cbor(j).size(); const auto msgpack_size = json::to_msgpack(j).size(); @@ -114,6 +121,7 @@ TEST_CASE("Binary Formats" * doctest::skip()) CHECK(bjdata_1_size == 390781); CHECK(bjdata_2_size == 433557); CHECK(bjdata_3_size == 432964); + CHECK(bon8_size == 317879); CHECK(bson_size == 479430); CHECK(cbor_size == 342373); CHECK(msgpack_size == 342473); @@ -125,6 +133,7 @@ TEST_CASE("Binary Formats" * doctest::skip()) CHECK((100.0 * double(bjdata_1_size) / double(json_size)) == Approx(78.109)); CHECK((100.0 * double(bjdata_2_size) / double(json_size)) == Approx(86.659)); CHECK((100.0 * double(bjdata_3_size) / double(json_size)) == Approx(86.541)); + CHECK((100.0 * double(bon8_size) / double(json_size)) == Approx(63.538)); CHECK((100.0 * double(bson_size) / double(json_size)) == Approx(95.828)); CHECK((100.0 * double(cbor_size) / double(json_size)) == Approx(68.433)); CHECK((100.0 * double(msgpack_size) / double(json_size)) == Approx(68.453)); @@ -142,6 +151,7 @@ TEST_CASE("Binary Formats" * doctest::skip()) const auto bjdata_1_size = json::to_bjdata(j).size(); const auto bjdata_2_size = json::to_bjdata(j, true).size(); const auto bjdata_3_size = json::to_bjdata(j, true, true).size(); + const auto bon8_size = json::to_bon8(j).size(); const auto bson_size = json::to_bson({{"", j}}).size(); // wrap array in object for BSON const auto cbor_size = json::to_cbor(j).size(); const auto msgpack_size = json::to_msgpack(j).size(); @@ -153,6 +163,7 @@ TEST_CASE("Binary Formats" * doctest::skip()) CHECK(bjdata_1_size == 50710965); CHECK(bjdata_2_size == 51144830); CHECK(bjdata_3_size == 51144830); + CHECK(bon8_size == 45942080); CHECK(bson_size == 56008520); CHECK(cbor_size == 46187320); CHECK(msgpack_size == 46158575); @@ -164,6 +175,7 @@ TEST_CASE("Binary Formats" * doctest::skip()) CHECK((100.0 * double(bjdata_1_size) / double(json_size)) == Approx(96.576)); CHECK((100.0 * double(bjdata_2_size) / double(json_size)) == Approx(97.402)); CHECK((100.0 * double(bjdata_3_size) / double(json_size)) == Approx(97.402)); + CHECK((100.0 * double(bon8_size) / double(json_size)) == Approx(87.494)); CHECK((100.0 * double(bson_size) / double(json_size)) == Approx(106.665)); CHECK((100.0 * double(cbor_size) / double(json_size)) == Approx(87.961)); CHECK((100.0 * double(msgpack_size) / double(json_size)) == Approx(87.906)); @@ -181,6 +193,7 @@ TEST_CASE("Binary Formats" * doctest::skip()) const auto bjdata_1_size = json::to_bjdata(j).size(); const auto bjdata_2_size = json::to_bjdata(j, true).size(); const auto bjdata_3_size = json::to_bjdata(j, true, true).size(); + const auto bon8_size = json::to_bon8(j).size(); // BSON cannot process the file as it contains code point U+0000 const auto cbor_size = json::to_cbor(j).size(); const auto msgpack_size = json::to_msgpack(j).size(); @@ -192,6 +205,7 @@ TEST_CASE("Binary Formats" * doctest::skip()) CHECK(bjdata_1_size == 148695); CHECK(bjdata_2_size == 150569); CHECK(bjdata_3_size == 150569); + CHECK(bon8_size == 144477); CHECK(cbor_size == 147095); CHECK(msgpack_size == 147017); CHECK(ubjson_1_size == 148695); @@ -202,6 +216,7 @@ TEST_CASE("Binary Formats" * doctest::skip()) CHECK((100.0 * double(bjdata_1_size) / double(json_size)) == Approx(88.153)); CHECK((100.0 * double(bjdata_2_size) / double(json_size)) == Approx(89.264)); CHECK((100.0 * double(bjdata_3_size) / double(json_size)) == Approx(89.264)); + CHECK((100.0 * double(bon8_size) / double(json_size)) == Approx(85.653)); CHECK((100.0 * double(cbor_size) / double(json_size)) == Approx(87.205)); CHECK((100.0 * double(msgpack_size) / double(json_size)) == Approx(87.158)); CHECK((100.0 * double(ubjson_1_size) / double(json_size)) == Approx(88.153)); diff --git a/tests/src/unit-binary_writer_sinks.cpp b/tests/src/unit-binary_writer_sinks.cpp index f60e1bf51..d13f5e5ac 100644 --- a/tests/src/unit-binary_writer_sinks.cpp +++ b/tests/src/unit-binary_writer_sinks.cpp @@ -12,6 +12,7 @@ using nlohmann::json; #include +#include #include #include @@ -49,6 +50,12 @@ std::vector test_values() }; } +// BON8 has no integers above the int64 range, so to_bon8() rejects them +bool bon8_representable(const json& j) +{ + return !j.is_number_unsigned() || j.get() <= static_cast((std::numeric_limits::max)()); +} + // values to_bson() accepts: the document must be an object std::vector bson_values() { @@ -92,6 +99,13 @@ TEST_CASE("binary writer output sinks") json::to_msgpack(j, msgpack); CHECK(json::to_msgpack(j) == msgpack); + if (bon8_representable(j)) + { + std::vector bon8; + json::to_bon8(j, bon8); + CHECK(json::to_bon8(j) == bon8); + } + for (const bool use_size : { false, true @@ -172,6 +186,10 @@ TEST_CASE("binary_reserve_hint never over-reserves") CHECK(hint <= json::to_ubjson(j).size()); CHECK(hint <= json::to_ubjson(j, true, true).size()); CHECK(hint <= json::to_bjdata(j).size()); + if (bon8_representable(j)) + { + CHECK(hint <= json::to_bon8(j).size()); + } } for (const auto& j : bson_values()) diff --git a/tests/src/unit-bjdata.cpp b/tests/src/unit-bjdata.cpp index ebbbbfaf6..a58507c15 100644 --- a/tests/src/unit-bjdata.cpp +++ b/tests/src/unit-bjdata.cpp @@ -418,7 +418,7 @@ TEST_CASE("BJData") SECTION("-32768..-129 (int16)") { - for (int32_t i = -32768; i <= -129; ++i) + for (int32_t i = -32768; i <= -129; i = utils::next_integer_sample(i, -129, 7)) { CAPTURE(i) @@ -578,7 +578,7 @@ TEST_CASE("BJData") SECTION("256..32767 (int16)") { - for (size_t i = 256; i <= 32767; ++i) + for (size_t i = 256; i <= 32767; i = utils::next_integer_sample(i, static_cast(32767), static_cast(7))) { CAPTURE(i) @@ -911,7 +911,7 @@ TEST_CASE("BJData") SECTION("256..32767 (int16)") { - for (size_t i = 256; i <= 32767; ++i) + for (size_t i = 256; i <= 32767; i = utils::next_integer_sample(i, static_cast(32767), static_cast(7))) { CAPTURE(i) @@ -3763,6 +3763,49 @@ TEST_CASE("BJData") } } +TEST_CASE("BJData input that cannot be read is discarded by every overload") +{ + std::vector input = json::to_bjdata(json({{"a", {1, 2}}})); + input.pop_back(); + + json _; + CHECK_THROWS_AS(_ = json::from_bjdata(input.begin(), input.end()), json::parse_error&); + CHECK(json::from_bjdata(input, true, false).is_discarded()); + CHECK(json::from_bjdata(input.begin(), input.end(), true, false).is_discarded()); +} + +TEST_CASE("BJData SAX parsing stops at every event") +{ + // Containers are opened and closed by the loop that reads them; a SAX + // handler that rejects any event - including the end of a nested + // container - must stop the parse right there. + const auto count_events = [](const std::vector& input) + { + int events = 0; + while (true) + { + SaxCountdown scp(events); + if (json::sax_parse(input, &scp, json::input_format_t::bjdata)) + { + return events; + } + ++events; + REQUIRE(events < 1000); + } + }; + + // 20 events: every container kind closes inside another one + const json j = json::parse(R"({"a": [1, {"b": []}], "c": {"d": [[2]]}})"); + CHECK(count_events(json::to_bjdata(j)) == 20); + CHECK(count_events(json::to_bjdata(j, true)) == 20); + CHECK(count_events(json::to_bjdata(j, true, true)) == 20); + + // an ND-array is announced as an annotated object: start_object, then + // _ArrayType_, _ArraySize_ and _ArrayData_ with its elements + const json ndarray = json::parse(R"({"_ArrayType_": "uint8", "_ArraySize_": [2, 2], "_ArrayData_": [1, 2, 3, 4]})"); + CHECK(count_events(json::to_bjdata(ndarray, true, true)) == 16); +} + TEST_CASE("issue #5405 - array reserve for definite-length BJData arrays") { #if !defined(JSON_NOEXCEPTION) @@ -4247,6 +4290,65 @@ TEST_CASE("all BJData first bytes") } #endif +TEST_CASE("BJData and UBJSON can be written to a string") +{ + const std::vector values = + { + {{"a", {1, 2.5, "x", nullptr}}, {"b", json::binary({1, 2})}}, + // an annotated ND-array, and objects that only look like one + json::parse(R"({"_ArrayType_": "uint8", "_ArraySize_": [2, 2], "_ArrayData_": [1, 2, 3, 4]})"), + json::parse(R"({"_ArrayType_": 1, "_ArraySize_": [2, 2], "_ArrayData_": [1, 2, 3, 4]})"), + json::parse(R"({"_ArrayType_": "uint8", "_ArraySize_": 4, "_ArrayData_": [1, 2, 3, 4]})"), + json::parse(R"({"_ArrayType_": "uint8", "_ArraySize_": [2, -2], "_ArrayData_": [1, 2, 3, 4]})"), + json::parse(R"({"_ArrayType_": "uint8", "_ArraySize_": [2, 2], "_ArrayData_": [1, 2, 3]})"), + json::parse(R"({"_ArrayType_": "uint8", "_ArraySize_": [2, 2], "_ArrayData_": 1})"), + }; + + // compared byte by byte: building a std::string from the bytes would + // convert them implicitly, which -fsanitize=integer reports for bytes of + // 0x80 and above + const auto same_bytes = [](const std::vector& bytes, const std::string & text) + { + return bytes.size() == text.size() && std::equal(bytes.begin(), bytes.end(), text.begin(), [](std::uint8_t byte, char c) + { + return byte == static_cast(c); + }); + }; + + for (const auto& j : values) + { + CAPTURE(j.dump()); + for (const bool use_size : + { + false, true + }) + { + for (const bool use_type : + { + false, true + }) + { + if (use_type && !use_size) + { + continue; + } + CAPTURE(use_size); + CAPTURE(use_type); + + const auto bjdata = json::to_bjdata(j, use_size, use_type); + std::string bjdata_string; + json::to_bjdata(j, bjdata_string, use_size, use_type); + CHECK(same_bytes(bjdata, bjdata_string)); + + const auto ubjson = json::to_ubjson(j, use_size, use_type); + std::string ubjson_string; + json::to_ubjson(j, ubjson_string, use_size, use_type); + CHECK(same_bytes(ubjson, ubjson_string)); + } + } + } +} + TEST_CASE("BJData use_type requires use_size") { SECTION("non-empty object throws other_error.502") @@ -4265,6 +4367,17 @@ TEST_CASE("BJData use_type requires use_size") json::other_error&); } + SECTION("non-empty binary value throws other_error.502") + { + const json j = json::binary({1, 2, 3}); + CHECK_THROWS_WITH_AS(json::to_bjdata(j, false, true), + "[json.exception.other_error.502] use_type requires use_size = true", + json::other_error&); + CHECK_THROWS_WITH_AS(json::to_ubjson(j, false, true), + "[json.exception.other_error.502] use_type requires use_size = true", + json::other_error&); + } + SECTION("scalars do not throw with use_type=true, use_count=false") { CHECK_NOTHROW(json::to_bjdata(42, false, true)); @@ -4428,45 +4541,27 @@ TEST_CASE("BJData roundtrips" * doctest::skip()) { CAPTURE(filename) + std::ifstream f_json(filename); + const json j1 = json::parse(f_json); + auto packed = utils::read_binary_file(filename + ".bjdata"); + { INFO_WITH_TEMP(filename + ": std::vector"); - // parse JSON file - std::ifstream f_json(filename); - const json j1 = json::parse(f_json); - - // parse BJData file - auto packed = utils::read_binary_file(filename + ".bjdata"); json j2; CHECK_NOTHROW(j2 = json::from_bjdata(packed)); - - // compare parsed JSON values CHECK(j1 == j2); } { INFO_WITH_TEMP(filename + ": std::ifstream"); - // parse JSON file - std::ifstream f_json(filename); - const json j1 = json::parse(f_json); - - // parse BJData file std::ifstream f_bjdata(filename + ".bjdata", std::ios::binary); json j2; CHECK_NOTHROW(j2 = json::from_bjdata(f_bjdata)); - - // compare parsed JSON values CHECK(j1 == j2); } { INFO_WITH_TEMP(filename + ": output to output adapters"); - // parse JSON file - std::ifstream f_json(filename); - json const j1 = json::parse(f_json); - - // parse BJData file - auto packed = utils::read_binary_file(filename + ".bjdata"); - { INFO_WITH_TEMP(filename + ": output adapters: std::vector"); std::vector vec; diff --git a/tests/src/unit-bon8.cpp b/tests/src/unit-bon8.cpp new file mode 100644 index 000000000..030e1d5c3 --- /dev/null +++ b/tests/src/unit-bon8.cpp @@ -0,0 +1,1050 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; +#ifdef JSON_TEST_NO_GLOBAL_UDLS + using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) +#endif + +#include +#include +#include +#include +#include +#include +#include "make_test_data_available.hpp" +#include "test_utils.hpp" + +namespace +{ +class SaxCountdown +{ + public: + explicit SaxCountdown(const int count) : events_left(count) + {} + + bool null() + { + return events_left-- > 0; + } + + bool boolean(bool /*unused*/) + { + return events_left-- > 0; + } + + bool number_integer(json::number_integer_t /*unused*/) + { + return events_left-- > 0; + } + + bool number_unsigned(json::number_unsigned_t /*unused*/) + { + return events_left-- > 0; + } + + bool number_float(json::number_float_t /*unused*/, const std::string& /*unused*/) + { + return events_left-- > 0; + } + + bool string(std::string& /*unused*/) + { + return events_left-- > 0; + } + + bool binary(std::vector& /*unused*/) + { + return events_left-- > 0; + } + + bool start_object(std::size_t /*unused*/) + { + return events_left-- > 0; + } + + bool key(std::string& /*unused*/) + { + return events_left-- > 0; + } + + bool end_object() + { + return events_left-- > 0; + } + + bool start_array(std::size_t /*unused*/) + { + return events_left-- > 0; + } + + bool end_array() + { + return events_left-- > 0; + } + + bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, const json::exception& /*unused*/) // NOLINT(readability-convert-member-functions-to-static) + { + return false; + } + + private: + int events_left = 0; +}; + +using bytes = std::vector; + +/// @return the string with the given bytes +std::string str(const bytes& b) +{ + // converted one by one: constructing the string from the byte range + // converts implicitly, which -fsanitize=integer reports for bytes >= 0x80 + std::string result; + result.reserve(b.size()); + for (const auto c : b) + { + result.push_back(static_cast(c)); + } + return result; +} + +/// check that @a j is serialized to @a expected and that @a expected is read back as @a j +void check_bon8(const json& j, const bytes& expected) +{ + CAPTURE(j) + CHECK(json::to_bon8(j) == expected); + + const json decoded = json::from_bon8(expected); + CHECK(decoded == j); + // integers are not read back as floats and vice versa + CHECK(decoded.type() == j.type()); + + // a stream is read byte by byte rather than in bulk + std::istringstream stream(str(expected)); + CHECK(json::from_bon8(stream) == decoded); +} + +/// @return @a b followed by @a tail +bytes concat(bytes b, const bytes& tail) +{ + b.insert(b.end(), tail.begin(), tail.end()); + return b; +} +} // namespace + +TEST_CASE("BON8") +{ + SECTION("individual values") + { + SECTION("discarded") + { + // discarded values are not serialized + const json j = json::value_t::discarded; + CHECK(json::to_bon8(j).empty()); + } + + SECTION("null") + { + check_bon8(nullptr, {0xFA}); + } + + SECTION("boolean") + { + check_bon8(true, {0xF9}); + check_bon8(false, {0xF8}); + } + + SECTION("integers") + { + // test vectors from HikoGUI (src/hikogui/codec/BON8_tests.cpp, + // Copyright Take Vos 2021, Boost Software License 1.0), which + // cover the first and last value of each encoding + SECTION("positive") + { + check_bon8(0u, {0x90}); + check_bon8(39u, {0xB7}); + check_bon8(40u, {0xC2, 0x00}); + check_bon8(3879u, {0xDF, 0x7F}); + check_bon8(3880u, {0xE0, 0x00, 0x00}); + check_bon8(528167u, {0xEF, 0x7F, 0xFF}); + check_bon8(528168u, {0xF0, 0x00, 0x00, 0x00}); + check_bon8(67637031u, {0xF7, 0x7F, 0xFF, 0xFF}); + check_bon8(67637032u, {0x8C, 0x04, 0x08, 0x0F, 0x28}); + check_bon8(2147483647u, {0x8C, 0x7F, 0xFF, 0xFF, 0xFF}); + check_bon8(2147483648u, {0x8D, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00}); + check_bon8(9223372036854775807u, {0x8D, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + } + + SECTION("negative") + { + check_bon8(-1, {0xB8}); + check_bon8(-10, {0xC1}); + check_bon8(-11, {0xC2, 0xC0}); + check_bon8(-1930, {0xDF, 0xFF}); + check_bon8(-1931, {0xE0, 0xC0, 0x00}); + check_bon8(-264074, {0xEF, 0xFF, 0xFF}); + check_bon8(-264075, {0xF0, 0xC0, 0x00, 0x00}); + check_bon8(-33818506, {0xF7, 0xFF, 0xFF, 0xFF}); + check_bon8(-33818507, {0x8C, 0xFD, 0xFB, 0xF8, 0x75}); + check_bon8(-2147483648LL, {0x8C, 0x80, 0x00, 0x00, 0x00}); + check_bon8(-2147483649LL, {0x8D, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF}); + check_bon8((std::numeric_limits::min)(), {0x8D, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + } + + SECTION("values inside the ranges") + { + check_bon8(1u, {0x91}); + check_bon8(100u, {0xC2, 0x3C}); + check_bon8(1000u, {0xC9, 0x40}); + check_bon8(100000u, {0xE2, 0x77, 0x78}); + check_bon8(1000000u, {0xF0, 0x07, 0x33, 0x18}); + check_bon8(-9, {0xC0}); + check_bon8(-100, {0xC3, 0xD9}); + check_bon8(-100000, {0xE5, 0xFF, 0x15}); + check_bon8(-1000000, {0xF0, 0xCB, 0x3A, 0xB5}); + } + + SECTION("every integer from -300000 to 600000") + { + for (std::int64_t i = -300000; i <= 600000; ++i) + { + const json j = i; + const auto packed = json::to_bon8(j); + CHECK(packed.size() == (i >= -10 && i <= 39 ? 1u : i >= -1930 && i <= 3879 ? 2u : i >= -264074 && i <= 528167 ? 3u : 4u)); + const json decoded = json::from_bon8(packed); + if (decoded != j) + { + CAPTURE(i) + CHECK(decoded == j); + } + } + } + + SECTION("signed values are read back as unsigned when not negative") + { + const json j = json::from_bon8(json::to_bon8(json(static_cast(1000)))); + CHECK(j.is_number_unsigned()); + CHECK(j == 1000); + } + + SECTION("unsigned integers above int64") + { + json _; + const json j = 9223372036854775808u; + CHECK_THROWS_WITH_AS(_ = json::to_bon8(j), "[json.exception.out_of_range.407] integer number 9223372036854775808 cannot be represented by BON8 as it does not fit int64", json::out_of_range&); + } + } + + SECTION("floating-point numbers") + { + SECTION("one-byte values") + { + check_bon8(-1.0, {0xFB}); + check_bon8(0.0, {0xFC}); + check_bon8(1.0, {0xFD}); + } + + SECTION("binary32") + { + check_bon8(2.0, {0x8E, 0x40, 0x00, 0x00, 0x00}); + check_bon8(0.5, {0x8E, 0x3F, 0x00, 0x00, 0x00}); + check_bon8(-1.5, {0x8E, 0xBF, 0xC0, 0x00, 0x00}); + check_bon8(3.4028234663852886e38, {0x8E, 0x7F, 0x7F, 0xFF, 0xFF}); + } + + SECTION("binary64") + { + check_bon8(100000000.1, {0x8F, 0x41, 0x97, 0xD7, 0x84, 0x00, 0x66, 0x66, 0x66}); + check_bon8(3.14159, {0x8F, 0x40, 0x09, 0x21, 0xF9, 0xF0, 0x1B, 0x86, 0x6E}); + check_bon8(1e300, {0x8F, 0x7E, 0x37, 0xE4, 0x3C, 0x88, 0x00, 0x75, 0x9C}); + } + + SECTION("-0.0 is written as binary32") + { + const json j = -0.0; + CHECK(json::to_bon8(j) == bytes{0x8E, 0x80, 0x00, 0x00, 0x00}); + const json decoded = json::from_bon8(json::to_bon8(j)); + CHECK(decoded.get() == 0.0); + CHECK(std::signbit(decoded.get())); + } + + SECTION("infinity is written as binary32") + { + check_bon8(std::numeric_limits::infinity(), {0x8E, 0x7F, 0x80, 0x00, 0x00}); + check_bon8(-std::numeric_limits::infinity(), {0x8E, 0xFF, 0x80, 0x00, 0x00}); + } + + SECTION("NaN is written as binary32 0x7F800001") + { + const json j = std::numeric_limits::quiet_NaN(); + CHECK(json::to_bon8(j) == bytes{0x8E, 0x7F, 0x80, 0x00, 0x01}); + CHECK(std::isnan(json::from_bon8(json::to_bon8(j)).get())); + } + } + + SECTION("strings") + { + SECTION("empty string") + { + check_bon8("", {0xFF}); + } + + SECTION("ASCII") + { + check_bon8("a", {'a', 0xFF}); + check_bon8("This is a string.", concat(bytes{'T', 'h', 'i', 's', ' ', 'i', 's', ' ', 'a', ' ', 's', 't', 'r', 'i', 'n', 'g', '.'}, {0xFF})); + check_bon8(str({0x00}), {0x00, 0xFF}); + check_bon8(str({0x7F}), {0x7F, 0xFF}); + } + + SECTION("multi-byte UTF-8") + { + check_bon8("\xC2\xA3", {0xC2, 0xA3, 0xFF}); // U+00A3 + check_bon8("\xEF\xB8\xBB", {0xEF, 0xB8, 0xBB, 0xFF}); // U+FE3B + check_bon8("\xF0\x9F\x80\x84", {0xF0, 0x9F, 0x80, 0x84, 0xFF}); // U+1F004 + check_bon8("\xC2\x80", {0xC2, 0x80, 0xFF}); // U+0080 + check_bon8("\xDF\xBF", {0xDF, 0xBF, 0xFF}); // U+07FF + check_bon8("\xE0\xA0\x80", {0xE0, 0xA0, 0x80, 0xFF}); // U+0800 + check_bon8("\xED\x9F\xBF", {0xED, 0x9F, 0xBF, 0xFF}); // U+D7FF + check_bon8("\xEE\x80\x80", {0xEE, 0x80, 0x80, 0xFF}); // U+E000 + check_bon8("\xF0\x90\x80\x80", {0xF0, 0x90, 0x80, 0x80, 0xFF}); // U+10000 + check_bon8("\xF4\x8F\xBF\xBF", {0xF4, 0x8F, 0xBF, 0xBF, 0xFF}); // U+10FFFF + check_bon8("a\xC2\xA3" "b", {'a', 0xC2, 0xA3, 'b', 0xFF}); + } + + SECTION("invalid UTF-8 cannot be written") + { + json _; + CHECK_THROWS_WITH_AS(_ = json::to_bon8(str({0x80})), "[json.exception.type_error.316] invalid UTF-8 byte at index 0: 0x80", json::type_error&); + CHECK_THROWS_WITH_AS(_ = json::to_bon8(str({'a', 0xC0, 0x80})), "[json.exception.type_error.316] invalid UTF-8 byte at index 1: 0xC0", json::type_error&); + CHECK_THROWS_WITH_AS(_ = json::to_bon8(str({0xE0, 0x80, 0x80})), "[json.exception.type_error.316] invalid UTF-8 byte at index 0: 0xE0", json::type_error&); + CHECK_THROWS_WITH_AS(_ = json::to_bon8(str({0xED, 0xA0, 0x80})), "[json.exception.type_error.316] invalid UTF-8 byte at index 0: 0xED", json::type_error&); + CHECK_THROWS_WITH_AS(_ = json::to_bon8(str({0xF4, 0x90, 0x80, 0x80})), "[json.exception.type_error.316] invalid UTF-8 byte at index 0: 0xF4", json::type_error&); + CHECK_THROWS_WITH_AS(_ = json::to_bon8(str({0xF5, 0x80, 0x80, 0x80})), "[json.exception.type_error.316] invalid UTF-8 byte at index 0: 0xF5", json::type_error&); + CHECK_THROWS_WITH_AS(_ = json::to_bon8(str({0xC2, 'a'})), "[json.exception.type_error.316] invalid UTF-8 byte at index 0: 0xC2", json::type_error&); + CHECK_THROWS_WITH_AS(_ = json::to_bon8(str({'a', 0xE2, 0x82})), "[json.exception.type_error.316] invalid UTF-8 byte at index 1: 0xE2", json::type_error&); + CHECK_THROWS_WITH_AS(_ = json::to_bon8(json::object({{str({0xFF}), 1}})), "[json.exception.type_error.316] invalid UTF-8 byte at index 0: 0xFF", json::type_error&); + // after a run of ASCII characters that is checked 8 bytes at a time + CHECK_THROWS_WITH_AS(_ = json::to_bon8(std::string(17, 'a') + str({0xC0})), "[json.exception.type_error.316] invalid UTF-8 byte at index 17: 0xC0", json::type_error&); + CHECK_THROWS_WITH_AS(_ = json::to_bon8(std::string(8, 'a') + "\xC3\xA4" + std::string(8, 'a') + str({0xE2, 0x82})), "[json.exception.type_error.316] invalid UTF-8 byte at index 18: 0xE2", json::type_error&); + } + } + + SECTION("arrays") + { + check_bon8(json::array(), {0x80}); + check_bon8({false}, {0x81, 0xF8}); + check_bon8({false, nullptr}, {0x82, 0xF8, 0xFA}); + check_bon8({false, nullptr, true}, {0x83, 0xF8, 0xFA, 0xF9}); + check_bon8({false, nullptr, true, 1.0}, {0x84, 0xF8, 0xFA, 0xF9, 0xFD}); + check_bon8({false, nullptr, true, 1.0, json::array(), 0.0}, {0x85, 0xF8, 0xFA, 0xF9, 0xFD, 0x80, 0xFC, 0xFE}); + check_bon8({{{1}}}, {0x81, 0x81, 0x81, 0x91}); + check_bon8({{{"foo"}}}, {0x81, 0x81, 0x81, 'f', 'o', 'o', 0xFF}); + check_bon8({{{""}}}, {0x81, 0x81, 0x81, 0xFF}); + + SECTION("large array") + { + json j = json::array(); + bytes expected = {0x85}; + for (int i = 0; i < 1000; ++i) + { + j.push_back(nullptr); + expected.push_back(0xFA); + } + expected.push_back(0xFE); + check_bon8(j, expected); + } + } + + SECTION("objects") + { + check_bon8(json::object(), {0x86}); + check_bon8({{"foo", nullptr}}, {0x87, 'f', 'o', 'o', 0xFA}); + check_bon8({{"", true}, {"foo", nullptr}}, {0x88, 0xFF, 0xF9, 'f', 'o', 'o', 0xFA}); + check_bon8({{"a", 1}, {"b", 2}, {"c", 3}}, {0x89, 'a', 0x91, 'b', 0x92, 'c', 0x93}); + check_bon8({{"a", 1}, {"b", 2}, {"c", 3}, {"d", 4}}, {0x8A, 'a', 0x91, 'b', 0x92, 'c', 0x93, 'd', 0x94}); + const json five = {{"one", 1}, {"two", 2}, {"three", 3}, {"four", 4}, {"five", 5}}; + check_bon8(five, {0x8B, 'f', 'i', 'v', 'e', 0x95, 'f', 'o', 'u', 'r', 0x94, 'o', 'n', 'e', 0x91, 't', 'h', 'r', 'e', 'e', 0x93, 't', 'w', 'o', 0x92, 0xFE}); + } + + SECTION("binary values are written as arrays of integers") + { + CHECK(json::to_bon8(json::binary({})) == bytes{0x80}); + CHECK(json::to_bon8(json::binary({0x00, 0x27, 0x28, 0xFF})) == bytes{0x84, 0x90, 0xB7, 0xC2, 0x00, 0xC3, 0x57}); + CHECK(json::to_bon8(json::binary({1, 2, 3, 4, 5}, 42)) == bytes{0x85, 0x91, 0x92, 0x93, 0x94, 0x95, 0xFE}); + CHECK(json::to_bon8({"a", json::binary({1})}) == bytes{0x82, 'a', 0x81, 0x91}); + CHECK(json::from_bon8(json::to_bon8(json::binary({1, 2, 3, 4, 5}))) == json({1, 2, 3, 4, 5})); + } + } + + SECTION("examples from the specification") + { + check_bon8("ab", {'a', 'b', 0xFF}); + check_bon8({"ab", "bc"}, {0x82, 'a', 'b', 0xFF, 'b', 'c', 0xFF}); + check_bon8({"a", "b", "c", "d", "e"}, {0x85, 'a', 0xFF, 'b', 0xFF, 'c', 0xFF, 'd', 0xFF, 'e', 0xFE}); + check_bon8({{"ab", 1}, {"bc", 2}}, {0x88, 'a', 'b', 0x91, 'b', 'c', 0x92}); + // the specification prints this example without the end-of-string + // markers after "b" and "c", which its own rules require: without + // them, the bytes read as the single string "bcd" + check_bon8({{"a", {"b", "c"}}, {"d", 1}}, {0x88, 'a', 0x82, 'b', 0xFF, 'c', 0xFF, 'd', 0x91}); + check_bon8({{"", 1}, {"a", 2}}, {0x88, 0xFF, 0x91, 'a', 0x92}); + } + + SECTION("examples from the discussion of the specification (#2980)") + { + check_bon8({{"a", ""}, {"c", "d"}}, {0x88, 'a', 0xFF, 0xFF, 'c', 0xFF, 'd', 0xFF}); + } + + SECTION("end of strings") + { + SECTION("a string ends at the first byte of an integer") + { + check_bon8({{"a", 100}}, {0x87, 'a', 0xC2, 0x3C}); + check_bon8({{"a", -9}}, {0x87, 'a', 0xC0}); + check_bon8({{"a", -10}}, {0x87, 'a', 0xC1}); + check_bon8({{"a", 5}}, {0x87, 'a', 0x95}); + check_bon8({{"a", 100000}}, {0x87, 'a', 0xE2, 0x77, 0x78}); + check_bon8({{"a", -1000000}}, {0x87, 'a', 0xF0, 0xCB, 0x3A, 0xB5}); + check_bon8({{"a", 67637032}}, {0x87, 'a', 0x8C, 0x04, 0x08, 0x0F, 0x28}); + check_bon8({{"a", 2147483648}}, {0x87, 'a', 0x8D, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00}); + check_bon8({"\xC2\xA3", 100}, {0x82, 0xC2, 0xA3, 0xC2, 0x3C}); + check_bon8({"\xF0\x9F\x80\x84", -1000000}, {0x82, 0xF0, 0x9F, 0x80, 0x84, 0xF0, 0xCB, 0x3A, 0xB5}); + } + + SECTION("a string ends at the first byte of other values") + { + check_bon8({"a", 2.0}, {0x82, 'a', 0x8E, 0x40, 0x00, 0x00, 0x00}); + check_bon8({"a", 0.1}, {0x82, 'a', 0x8F, 0x3F, 0xB9, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9A}); + check_bon8({"a", nullptr, "b", true, "c", false}, {0x85, 'a', 0xFA, 'b', 0xF9, 'c', 0xF8, 0xFE}); + check_bon8({"a", -1.0, "b", 0.0, "c", 1.0}, {0x85, 'a', 0xFB, 'b', 0xFC, 'c', 0xFD, 0xFE}); + check_bon8({"a", json::array()}, {0x82, 'a', 0x80}); + check_bon8({"a", json::object()}, {0x82, 'a', 0x86}); + check_bon8({{"a", {1, 2, 3, 4, 5}}}, {0x87, 'a', 0x85, 0x91, 0x92, 0x93, 0x94, 0x95, 0xFE}); + check_bon8({{"a", {{"b", 1}, {"c", 2}, {"d", 3}, {"e", 4}, {"f", 5}}}}, {0x87, 'a', 0x8B, 'b', 0x91, 'c', 0x92, 'd', 0x93, 'e', 0x94, 'f', 0x95, 0xFE}); + } + + SECTION("a string ends at the end of its container") + { + check_bon8({1, 2, 3, 4, "e"}, {0x85, 0x91, 0x92, 0x93, 0x94, 'e', 0xFE}); + check_bon8({{"a", 1}, {"b", 2}, {"c", 3}, {"d", 4}, {"e", "f"}}, {0x8B, 'a', 0x91, 'b', 0x92, 'c', 0x93, 'd', 0x94, 'e', 0xFF, 'f', 0xFE}); + check_bon8({{1, 2, 3, 4, "e"}, 1}, {0x82, 0x85, 0x91, 0x92, 0x93, 0x94, 'e', 0xFE, 0x91}); + check_bon8({{1, 2, 3, 4, "\xC2\xA3"}, 100}, {0x82, 0x85, 0x91, 0x92, 0x93, 0x94, 0xC2, 0xA3, 0xFE, 0xC2, 0x3C}); + } + + SECTION("a string that another string follows is terminated") + { + check_bon8({"s", "s"}, {0x82, 's', 0xFF, 's', 0xFF}); + check_bon8({"", "s"}, {0x82, 0xFF, 's', 0xFF}); + check_bon8({"s", ""}, {0x82, 's', 0xFF, 0xFF}); + check_bon8({"", ""}, {0x82, 0xFF, 0xFF}); + check_bon8({{"a", "b"}, {"c", "d"}}, {0x88, 'a', 0xFF, 'b', 0xFF, 'c', 0xFF, 'd', 0xFF}); + check_bon8({{"a", ""}}, {0x87, 'a', 0xFF, 0xFF}); + check_bon8({{"", ""}}, {0x87, 0xFF, 0xFF}); + } + + SECTION("a container that ends with a string and that a string follows") + { + check_bon8({{"a"}, "b"}, {0x82, 0x81, 'a', 0xFF, 'b', 0xFF}); + check_bon8({{{{"a"}}}, "b"}, {0x82, 0x81, 0x81, 0x81, 'a', 0xFF, 'b', 0xFF}); + check_bon8({{"a", {"x"}}, {"b", 1}}, {0x88, 'a', 0x81, 'x', 0xFF, 'b', 0x91}); + check_bon8({{"a", {{"b", "c"}}}, {"d", 1}}, {0x88, 'a', 0x87, 'b', 0xFF, 'c', 0xFF, 'd', 0x91}); + check_bon8({{"a"}, {"b"}}, {0x82, 0x81, 'a', 0x81, 'b', 0xFF}); + } + + SECTION("a string that ends the message is terminated") + { + check_bon8({"a"}, {0x81, 'a', 0xFF}); + check_bon8({{"a", "b"}}, {0x87, 'a', 0xFF, 'b', 0xFF}); + check_bon8({{{"a"}}}, {0x81, 0x81, 0x81, 'a', 0xFF}); + } + } + + SECTION("non-canonical input is accepted") + { + // an integer with a longer encoding than necessary + CHECK(json::from_bon8(bytes{0x8C, 0x00, 0x00, 0x00, 0x01}) == 1); + CHECK(json::from_bon8(bytes{0x8D, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) == -1); + // a float with a longer encoding than necessary + CHECK(json::from_bon8(bytes{0x8F, 0x3F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) == 1.0); + CHECK(json::from_bon8(bytes{0x8E, 0x00, 0x00, 0x00, 0x00}) == 0.0); + // an unsized container with few elements + CHECK(json::from_bon8(bytes{0x85, 0x91, 0x92, 0xFE}) == json({1, 2})); + CHECK(json::from_bon8(bytes{0x85, 0xFE}) == json::array()); + CHECK(json::from_bon8(bytes{0x8B, 'a', 0x91, 0xFE}) == json({{"a", 1}})); + // unsorted keys + CHECK(json::from_bon8(bytes{0x88, 'b', 0x91, 'a', 0x92}) == json({{"a", 2}, {"b", 1}})); + // an end-of-string marker that is not needed + CHECK(json::from_bon8(bytes{0x82, 'a', 0xFF, 0x91}) == json({"a", 1})); + CHECK(json::from_bon8(bytes{0x87, 'a', 0xFF, 0x91}) == json({{"a", 1}})); + } + + SECTION("types of values read") + { + CHECK(json::from_bon8(bytes{0x90}).is_number_unsigned()); + CHECK(json::from_bon8(bytes{0xC2, 0x00}).is_number_unsigned()); + CHECK(json::from_bon8(bytes{0x8C, 0x00, 0x00, 0x00, 0x00}).is_number_unsigned()); + CHECK(json::from_bon8(bytes{0xB8}).is_number_integer()); + CHECK(!json::from_bon8(bytes{0xB8}).is_number_unsigned()); + CHECK(json::from_bon8(bytes{0xC2, 0xC0}).is_number_integer()); + CHECK(json::from_bon8(bytes{0xFC}).is_number_float()); + CHECK(json::from_bon8(bytes{0x8E, 0x40, 0x00, 0x00, 0x00}).is_number_float()); + CHECK(json::from_bon8(bytes{0xFF}).is_string()); + } + + SECTION("errors") + { + json _; + + SECTION("empty input") + { + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes()), "[json.exception.parse_error.110] parse error at byte 1: syntax error while parsing BON8 value: unexpected end of input", json::parse_error&); + CHECK(json::from_bon8(bytes(), true, false).is_discarded()); + } + + SECTION("too short numbers") + { + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x8C}), "[json.exception.parse_error.110] parse error at byte 2: syntax error while parsing BON8 number: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x8C, 0x00, 0x00, 0x00}), "[json.exception.parse_error.110] parse error at byte 5: syntax error while parsing BON8 number: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x8D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}), "[json.exception.parse_error.110] parse error at byte 9: syntax error while parsing BON8 number: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x8E, 0x00}), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing BON8 number: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x8F, 0x00}), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing BON8 number: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xC2}), "[json.exception.parse_error.110] parse error at byte 2: syntax error while parsing BON8 number: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xE0, 0x00}), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing BON8 number: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xF0, 0x00, 0x00}), "[json.exception.parse_error.110] parse error at byte 4: syntax error while parsing BON8 number: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x82, 'a', 0xF0, 0xC0}), "[json.exception.parse_error.110] parse error at byte 5: syntax error while parsing BON8 number: unexpected end of input", json::parse_error&); + } + + SECTION("unterminated strings") + { + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{'a'}), "[json.exception.parse_error.110] parse error at byte 2: syntax error while parsing BON8 string: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x81, 'a', 'b'}), "[json.exception.parse_error.110] parse error at byte 4: syntax error while parsing BON8 string: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xF0, 0x9F, 0x80, 0x84}), "[json.exception.parse_error.110] parse error at byte 5: syntax error while parsing BON8 string: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xF0, 0x9F, 0x80}), "[json.exception.parse_error.110] parse error at byte 4: syntax error while parsing BON8 string: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x87, 'a'}), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing BON8 string: unexpected end of input", json::parse_error&); + } + + SECTION("invalid UTF-8") + { + // overlong + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xE0, 0x80, 0x80, 0xFF}), "[json.exception.parse_error.112] parse error at byte 2: syntax error while parsing BON8 string: invalid UTF-8 byte: 0x80", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xF0, 0x80, 0x80, 0x80, 0xFF}), "[json.exception.parse_error.112] parse error at byte 2: syntax error while parsing BON8 string: invalid UTF-8 byte: 0x80", json::parse_error&); + // surrogate + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xED, 0xA0, 0x80, 0xFF}), "[json.exception.parse_error.112] parse error at byte 2: syntax error while parsing BON8 string: invalid UTF-8 byte: 0xA0", json::parse_error&); + // above U+10FFFF + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xF4, 0x90, 0x80, 0x80, 0xFF}), "[json.exception.parse_error.112] parse error at byte 2: syntax error while parsing BON8 string: invalid UTF-8 byte: 0x90", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xF5, 0x80, 0x80, 0x80, 0xFF}), "[json.exception.parse_error.112] parse error at byte 2: syntax error while parsing BON8 string: invalid UTF-8 byte: 0x80", json::parse_error&); + // missing continuation byte + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xE2, 0x82, 'a', 0xFF}), "[json.exception.parse_error.112] parse error at byte 3: syntax error while parsing BON8 string: invalid UTF-8 byte: 0x61", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x81, 'a', 0xE2, 0x82, 'b'}), "[json.exception.parse_error.112] parse error at byte 5: syntax error while parsing BON8 string: invalid UTF-8 byte: 0x62", json::parse_error&); + } + + SECTION("end-of-container marker where a value is expected") + { + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xFE}), "[json.exception.parse_error.112] parse error at byte 1: syntax error while parsing BON8 value: invalid byte: 0xFE", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x81, 0xFE}), "[json.exception.parse_error.112] parse error at byte 2: syntax error while parsing BON8 value: invalid byte: 0xFE", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x8B, 'a', 0xFE}), "[json.exception.parse_error.112] parse error at byte 3: syntax error while parsing BON8 value: invalid byte: 0xFE", json::parse_error&); + } + + SECTION("keys that are not strings") + { + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x87, 0x91, 0x91}), "[json.exception.parse_error.112] parse error at byte 2: syntax error while parsing BON8 key: expected a string; last byte: 0x91", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x87, 0xFA, 0x91}), "[json.exception.parse_error.112] parse error at byte 2: syntax error while parsing BON8 key: expected a string; last byte: 0xFA", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x87, 0x80, 0x91}), "[json.exception.parse_error.112] parse error at byte 2: syntax error while parsing BON8 key: expected a string; last byte: 0x80", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x87, 0xFE}), "[json.exception.parse_error.112] parse error at byte 2: syntax error while parsing BON8 key: expected a string; last byte: 0xFE", json::parse_error&); + // an integer that begins with a UTF-8 lead byte + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x87, 0xC2, 0x00, 0x91}), "[json.exception.parse_error.112] parse error at byte 2: syntax error while parsing BON8 key: expected a string; last byte: 0xC2", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x88, 'a', 0x91, 0xF0, 0xC0, 0x00, 0x00, 0x91}), "[json.exception.parse_error.112] parse error at byte 4: syntax error while parsing BON8 key: expected a string; last byte: 0xF0", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x87}), "[json.exception.parse_error.110] parse error at byte 2: syntax error while parsing BON8 key: unexpected end of input", json::parse_error&); + } + + SECTION("unterminated containers") + { + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x82, 0x91}), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing BON8 value: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x85, 0x91}), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing BON8 value: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x8B, 'a', 0x91}), "[json.exception.parse_error.110] parse error at byte 4: syntax error while parsing BON8 key: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x87, 'a'}), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing BON8 string: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x87, 'a', 0xFF}), "[json.exception.parse_error.110] parse error at byte 4: syntax error while parsing BON8 value: unexpected end of input", json::parse_error&); + } + + SECTION("strict mode") + { + const bytes vec = {0x90, 0x90}; + CHECK(json::from_bon8(vec, false) == 0); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(vec), "[json.exception.parse_error.110] parse error at byte 2: syntax error while parsing BON8 value: expected end of input; last byte: 0x90", json::parse_error&); + CHECK(json::from_bon8(vec, true, false).is_discarded()); + + // the byte after a string that ends a container + const bytes vec2 = {0x81, 'a', 0x90}; + CHECK(json::from_bon8(vec2, false) == json({"a"})); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(vec2), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing BON8 value: expected end of input; last byte: 0x90", json::parse_error&); + + const bytes vec3 = {0x81, 'a', 0xC2, 0x00}; + CHECK(json::from_bon8(vec3, false) == json({"a"})); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(vec3), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing BON8 value: expected end of input; last byte: 0xC2", json::parse_error&); + } + } + + SECTION("SAX aborts") + { + SECTION("start_array(len)") + { + const bytes v = {0x83, 0x91, 0x92, 0x93}; + SaxCountdown scp(0); + CHECK(!json::sax_parse(v, &scp, json::input_format_t::bon8)); + } + + SECTION("start_array()") + { + const bytes v = {0x85, 0x91, 0xFE}; + SaxCountdown scp(0); + CHECK(!json::sax_parse(v, &scp, json::input_format_t::bon8)); + } + + SECTION("end_array()") + { + const bytes v = {0x85, 0x91, 0xFE}; + SaxCountdown scp(2); + CHECK(!json::sax_parse(v, &scp, json::input_format_t::bon8)); + } + + SECTION("start_object(len)") + { + const bytes v = {0x87, 'f', 'o', 'o', 0xF8}; + SaxCountdown scp(0); + CHECK(!json::sax_parse(v, &scp, json::input_format_t::bon8)); + } + + SECTION("key()") + { + const bytes v = {0x87, 'f', 'o', 'o', 0xF8}; + SaxCountdown scp(1); + CHECK(!json::sax_parse(v, &scp, json::input_format_t::bon8)); + } + + SECTION("end_object()") + { + const bytes v = {0x87, 'f', 'o', 'o', 0xF8}; + SaxCountdown scp(3); + CHECK(!json::sax_parse(v, &scp, json::input_format_t::bon8)); + } + + SECTION("values") + { + const std::vector values = + { + {0xFA}, {0xF9}, {0x91}, {0xB8}, {0xC2, 0x00}, {0xC2, 0xC0}, {0xE0, 0x00, 0x00}, + {0x8C, 0, 0, 0, 1}, {0x8D, 0, 0, 0, 0, 0, 0, 0, 1}, {0xFB}, {0xFC}, {0xFD}, + {0x8E, 0x40, 0, 0, 0}, {0x8F, 0x40, 0, 0, 0, 0, 0, 0, 0}, {'a', 0xFF}, {0xFF} + }; + for (const auto& v : values) + { + SaxCountdown scp(0); + CHECK(!json::sax_parse(v, &scp, json::input_format_t::bon8)); + } + } + } +} + +// the test catches the exceptions of invalid input +#if !defined(JSON_NOEXCEPTION) +TEST_CASE("BON8 strings from contiguous and stream input") +{ + // contiguous input copies the valid UTF-8 of a string in bulk, a stream + // is read byte by byte; both must end strings and report errors alike + const std::string ascii(20, 'a'); + const std::vector inputs = + { + // the string ends at 0xFF, at a marker, and at an integer + concat(concat(bytes(ascii.begin(), ascii.end()), {0xC3, 0xA4}), {0xFF}), + concat(concat({0x82}, bytes(ascii.begin(), ascii.end())), {0x91}), + concat(concat({0x85}, bytes(ascii.begin(), ascii.end())), {0xFE}), + concat(concat({0x82}, bytes(ascii.begin(), ascii.end())), {0xC2, 0x05}), + concat(concat({0x87}, bytes(ascii.begin(), ascii.end())), {0xE2, 0x82, 0xAC, 0xF0, 0x05, 0x00, 0x00}), + // invalid UTF-8 and a premature end after a run of valid characters + concat(bytes(ascii.begin(), ascii.end()), {0xE0, 0x80, 0x80}), + concat(bytes(ascii.begin(), ascii.end()), {0xE2, 0x82, 0x2F}), + concat(bytes(ascii.begin(), ascii.end()), {0xE2, 0x82}), + bytes(ascii.begin(), ascii.end()), + // trailing bytes after a string that ends a container + concat(concat({0x81}, bytes(ascii.begin(), ascii.end())), {0x91}), + }; + + for (const auto& input : inputs) + { + CAPTURE(input) + std::string from_vector; + std::string from_stream; + try + { + from_vector = json::from_bon8(input).dump(); + } + catch (const json::parse_error& e) + { + from_vector = e.what(); + } + try + { + std::istringstream stream(str(input)); + from_stream = json::from_bon8(stream).dump(); + } + catch (const json::parse_error& e) + { + from_stream = e.what(); + } + CHECK(from_vector == from_stream); + } + + json _; + CHECK(json::from_bon8(inputs[0]) == ascii + "\xC3\xA4"); + CHECK(json::from_bon8(inputs[3]) == json({ascii, 45})); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(inputs[5]), "[json.exception.parse_error.112] parse error at byte 22: syntax error while parsing BON8 string: invalid UTF-8 byte: 0x80", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(inputs[6]), "[json.exception.parse_error.112] parse error at byte 23: syntax error while parsing BON8 string: invalid UTF-8 byte: 0x2F", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(inputs[7]), "[json.exception.parse_error.110] parse error at byte 23: syntax error while parsing BON8 string: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(inputs[9]), "[json.exception.parse_error.110] parse error at byte 22: syntax error while parsing BON8 value: expected end of input; last byte: 0x91", json::parse_error&); +} +#endif + +// use this testcase outside [hide] to run it with Valgrind +TEST_CASE("BON8 nesting does not consume the call stack") +{ + // Note that deeply nested values must not be compared, copied or dumped + // here: those operations are still recursive. Depth is measured by + // descending instead. + + SECTION("an unterminated chain is reported, not crashed on") + { + json _; + const bytes input(300000, 0x81); + CHECK_THROWS_WITH_AS(_ = json::from_bon8(input), "[json.exception.parse_error.110] parse error at byte 300001: syntax error while parsing BON8 value: unexpected end of input", json::parse_error&); + CHECK(json::from_bon8(input, true, false).is_discarded()); + } + + SECTION("a well-formed deep value is read through the SAX interface") + { + bytes input(300000, 0x85); + input.push_back(0x91); // innermost value + input.insert(input.end(), 300000, 0xFE); + + SaxCountdown accept_all(600001); + CHECK(json::sax_parse(input, &accept_all, json::input_format_t::bon8)); + } + + SECTION("a well-formed deep value is read into a value") + { + const std::size_t depth = 10000; + bytes input; + for (std::size_t i = 0; i < depth; ++i) + { + input.push_back(0x87); + input.push_back('a'); + } + // the innermost key is followed by a string, so it is terminated + input.push_back(0xFF); + input.push_back('b'); + input.push_back(0xFF); + + json j = json::from_bon8(input); + + std::size_t measured = 0; + const json* p = &j; + while (p->is_object() && !p->empty()) + { + p = &p->at("a"); + ++measured; + } + CHECK(measured == depth); + CHECK(*p == "b"); + } +} + +TEST_CASE("single BON8 roundtrip") +{ + SECTION("sample.json") + { + std::string const filename = TEST_DATA_DIRECTORY "/json_testsuite/sample.json"; + + // parse JSON file + std::ifstream f_json(filename); + const json j1 = json::parse(f_json); + + // parse BON8 file + auto packed = utils::read_binary_file(filename + ".bon8"); + json j2; + CHECK_NOTHROW(j2 = json::from_bon8(packed)); + + // compare parsed JSON values + CHECK(j1 == j2); + + SECTION("roundtrips") + { + SECTION("std::ostringstream") + { + std::basic_ostringstream ss; + json::to_bon8(j1, ss); + json j3 = json::from_bon8(ss.str()); + CHECK(j1 == j3); + } + + SECTION("std::string") + { + std::string s; + json::to_bon8(j1, s); + json j3 = json::from_bon8(s); + CHECK(j1 == j3); + } + } + + // check with different start index + packed.insert(packed.begin(), 5, 0xff); + CHECK(j1 == json::from_bon8(packed.begin() + 5, packed.end())); + } +} + +TEST_CASE("Parse BON8 directly from a file using iterator and sentinel") +{ + std::string const filename = TEST_DATA_DIRECTORY "/json_testsuite/sample.json.bon8"; + std::ifstream file(filename, std::ios::binary); + const std::istreambuf_iterator first(file); + const json parsed = json::from_bon8(first, utils::istreambuf_sentinel{}); + CHECK((parsed.is_object() || parsed.is_array())); +} + +TEST_CASE("BON8 roundtrips" * doctest::skip()) +{ + SECTION("input from HikoGUI") + { + // The .bon8 files were created with the BON8 encoder of HikoGUI + // (https://github.com/hikoworks/hikogui), the reference implementation + // of the format. Both implementations sort object keys, so the output + // is compared byte for byte. + for (std::string filename : + { + TEST_DATA_DIRECTORY "/json_nlohmann_tests/all_unicode.json", + TEST_DATA_DIRECTORY "/json.org/1.json", + TEST_DATA_DIRECTORY "/json.org/2.json", + TEST_DATA_DIRECTORY "/json.org/3.json", + TEST_DATA_DIRECTORY "/json.org/4.json", + TEST_DATA_DIRECTORY "/json.org/5.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip01.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip02.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip03.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip04.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip05.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip06.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip07.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip08.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip09.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip10.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip11.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip12.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip13.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip14.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip15.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip16.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip17.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip18.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip19.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip20.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip21.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip22.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip23.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip24.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip25.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip26.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip27.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip28.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip29.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip30.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip31.json", + TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip32.json", + TEST_DATA_DIRECTORY "/json_testsuite/sample.json", + TEST_DATA_DIRECTORY "/json_tests/pass1.json", + TEST_DATA_DIRECTORY "/json_tests/pass2.json", + TEST_DATA_DIRECTORY "/json_tests/pass3.json", + TEST_DATA_DIRECTORY "/regression/floats.json", + TEST_DATA_DIRECTORY "/regression/signed_ints.json", + TEST_DATA_DIRECTORY "/regression/working_file.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_array_arraysWithSpaces.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_array_empty-string.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_array_empty.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_array_ending_with_newline.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_array_false.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_array_heterogeneous.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_array_null.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_array_with_1_and_newline.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_array_with_leading_space.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_array_with_several_null.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_array_with_trailing_space.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_0e+1.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_0e1.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_after_space.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_double_close_to_zero.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_double_huge_neg_exp.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_int_with_exp.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_minus_zero.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_negative_int.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_negative_one.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_negative_zero.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_real_capital_e.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_real_capital_e_neg_exp.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_real_capital_e_pos_exp.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_real_exponent.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_real_fraction_exponent.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_real_neg_exp.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_real_pos_exponent.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_real_underflow.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_simple_int.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_simple_real.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_too_big_neg_int.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_too_big_pos_int.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_very_big_negative_int.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object_basic.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object_duplicated_key.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object_duplicated_key_and_value.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object_empty.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object_empty_key.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object_escaped_null_in_key.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object_extreme_numbers.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object_long_strings.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object_simple.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object_string_unicode.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object_with_newlines.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_1_2_3_bytes_UTF-8_sequences.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_UTF-16_Surrogates_U+1D11E_MUSICAL_SYMBOL_G_CLEF.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_accepted_surrogate_pair.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_accepted_surrogate_pairs.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_allowed_escapes.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_backslash_and_u_escaped_zero.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_backslash_doublequotes.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_comments.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_double_escape_a.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_double_escape_n.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_escaped_control_character.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_escaped_noncharacter.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_in_array.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_in_array_with_leading_space.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_last_surrogates_1_and_2.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_newline_uescaped.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_nonCharacterInUTF-8_U+10FFFF.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_nonCharacterInUTF-8_U+1FFFF.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_nonCharacterInUTF-8_U+FFFF.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_null_escape.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_one-byte-utf-8.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_pi.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_simple_ascii.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_space.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_three-byte-utf-8.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_two-byte-utf-8.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_u+2028_line_sep.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_u+2029_par_sep.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_uEscape.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_unescaped_char_delete.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_unicode.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_unicodeEscapedBackslash.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_unicode_2.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_unicode_U+200B_ZERO_WIDTH_SPACE.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_unicode_U+2064_invisible_plus.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_unicode_escaped_double_quote.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_utf8.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_with_del_character.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_structure_lonely_false.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_structure_lonely_int.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_structure_lonely_negative_real.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_structure_lonely_null.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_structure_lonely_string.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_structure_lonely_true.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_structure_string_empty.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_structure_trailing_newline.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_structure_true_in_array.json", + TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_structure_whitespace_array.json" + }) + { + CAPTURE(filename) + + // parse JSON file + std::ifstream f_json(filename); + const json j1 = json::parse(f_json); + + // parse BON8 file + const auto packed = utils::read_binary_file(filename + ".bon8"); + + { + INFO_WITH_TEMP(filename + ": std::vector"); + json j2; + CHECK_NOTHROW(j2 = json::from_bon8(packed)); + CHECK(j1 == j2); + } + + { + INFO_WITH_TEMP(filename + ": std::ifstream"); + std::ifstream f_bon8(filename + ".bon8", std::ios::binary); + json j2; + CHECK_NOTHROW(j2 = json::from_bon8(f_bon8)); + CHECK(j1 == j2); + } + + { + INFO_WITH_TEMP(filename + ": iterator pair"); + json j2; + CHECK_NOTHROW(j2 = json::from_bon8(packed.begin(), packed.end())); + CHECK(j1 == j2); + } + + { + INFO_WITH_TEMP(filename + ": output adapters: std::vector"); + std::vector vec; + json::to_bon8(j1, vec); + CHECK(vec == packed); + } + } + } +} + +#ifdef JSON_HAS_CPP_17 +TEST_CASE("BON8 with std::byte") +{ + SECTION("vector roundtrip") + { + const json original = + { + {"name", "test"}, + {"value", 42}, + {"array", {1, 2, 3}} + }; + + const std::vector temp = json::to_bon8(original); + std::vector bon8_data(temp.size()); + for (size_t i = 0; i < temp.size(); ++i) + { + bon8_data[i] = std::byte(temp[i]); + } + + json from_bytes; + CHECK_NOTHROW(from_bytes = json::from_bon8(bon8_data)); + CHECK(from_bytes == original); + } + + SECTION("empty vector") + { + const std::vector empty_data; + CHECK_THROWS_WITH_AS([&]() + { + [[maybe_unused]] auto result = json::from_bon8(empty_data); + return true; + } + (), + "[json.exception.parse_error.110] parse error at byte 1: syntax error while parsing BON8 value: unexpected end of input", + json::parse_error&); + } +} +#endif diff --git a/tests/src/unit-bson.cpp b/tests/src/unit-bson.cpp index 03cde5376..292ff4dc1 100644 --- a/tests/src/unit-bson.cpp +++ b/tests/src/unit-bson.cpp @@ -1066,6 +1066,45 @@ TEST_CASE("Incomplete BSON Input") } } +// the test catches the exceptions of invalid input +#if !defined(JSON_NOEXCEPTION) +TEST_CASE("BSON keys from contiguous and stream input") +{ + // contiguous input reads a key up to its \x00-byte in one step, a stream + // reads it byte by byte; both must give the same value or error for the + // complete document and for every truncation of it + const json j = {{"", true}, {"k", {1, 2, 3}}, {std::string(40, 'x'), {{"nested key", "value"}}}}; + const std::vector bson = json::to_bson(j); + CHECK(json::from_bson(bson) == j); + + for (std::size_t length = 0; length <= bson.size(); ++length) + { + CAPTURE(length) + const std::vector input(bson.begin(), bson.begin() + static_cast(length)); + std::string from_vector; + std::string from_stream; + try + { + from_vector = json::from_bson(input).dump(); + } + catch (const json::parse_error& e) + { + from_vector = e.what(); + } + try + { + std::istringstream stream(std::string(input.begin(), input.end())); + from_stream = json::from_bson(stream).dump(); + } + catch (const json::parse_error& e) + { + from_stream = e.what(); + } + CHECK(from_vector == from_stream); + } +} +#endif + TEST_CASE("Negative size of binary value") { // invalid BSON: the size of the binary value is -1 @@ -1244,6 +1283,44 @@ TEST_CASE("BSON nesting does not consume the call stack") } } +TEST_CASE("BSON input that cannot be read is discarded by every overload") +{ + std::vector input = json::to_bson(json({{"a", {1, 2}}})); + input.pop_back(); + + json _; + CHECK_THROWS_AS(_ = json::from_bson(input.begin(), input.end()), json::parse_error&); + CHECK(json::from_bson(input, true, false).is_discarded()); + CHECK(json::from_bson(input.begin(), input.end(), true, false).is_discarded()); + CHECK(json::from_bson(input.data(), input.size(), true, false).is_discarded()); + CHECK(json::from_bson({input.data(), input.size()}, true, false).is_discarded()); +} + +TEST_CASE("BSON SAX parsing stops at every event") +{ + // Containers are opened and closed by the loop that reads them; a SAX + // handler that rejects any event - including the end of a nested + // container - must stop the parse right there. + const auto count_events = [](const std::vector& input) + { + int events = 0; + while (true) + { + SaxCountdown scp(events); + if (json::sax_parse(input, &scp, json::input_format_t::bson)) + { + return events; + } + ++events; + REQUIRE(events < 1000); + } + }; + + // 20 events: every container kind closes inside another one + const json j = json::parse(R"({"a": [1, {"b": []}], "c": {"d": [[2]]}})"); + CHECK(count_events(json::to_bson(j)) == 20); +} + TEST_CASE("BSON numerical data") { SECTION("number") diff --git a/tests/src/unit-cbor.cpp b/tests/src/unit-cbor.cpp index 9c799b0eb..6bd792f8a 100644 --- a/tests/src/unit-cbor.cpp +++ b/tests/src/unit-cbor.cpp @@ -15,6 +15,7 @@ using nlohmann::json; #include #include #include +#include #include #include "make_test_data_available.hpp" #include "test_utils.hpp" @@ -290,7 +291,7 @@ TEST_CASE("CBOR") SECTION("-65536..-257") { - for (int32_t i = -65536; i <= -257; ++i) + for (int32_t i = -65536; i <= -257; i = utils::next_integer_sample(i, -257, 7)) { CAPTURE(i) @@ -478,7 +479,7 @@ TEST_CASE("CBOR") SECTION("256..65535") { - for (size_t i = 256; i <= 65535; ++i) + for (size_t i = 256; i <= 65535; i = utils::next_integer_sample(i, static_cast(65535), static_cast(7))) { CAPTURE(i) @@ -613,7 +614,7 @@ TEST_CASE("CBOR") SECTION("-32768..-129 (int 16)") { - for (int16_t i = -32768; i <= static_cast(-129); ++i) + for (int16_t i = -32768; i <= static_cast(-129); i = utils::next_integer_sample(i, static_cast(-129), static_cast(7))) { CAPTURE(i) @@ -718,7 +719,7 @@ TEST_CASE("CBOR") SECTION("256..65535 (two-byte uint16_t)") { - for (size_t i = 256; i <= 65535; ++i) + for (size_t i = 256; i <= 65535; i = utils::next_integer_sample(i, static_cast(65535), static_cast(7))) { CAPTURE(i) @@ -2123,6 +2124,20 @@ TEST_CASE("CBOR nesting does not consume the call stack") CHECK(json::from_cbor(input, true, false, json::cbor_tag_handler_t::ignore).is_discarded()); } + SECTION("stored tags") + { + // a tag over something other than a byte string is read like for + // ignore, so a chain of them must not recurse either (#5316) + std::vector input; + for (std::size_t i = 0; i < 500000; ++i) + { + input.push_back(0xD8); + input.push_back(0x18); + } + input.push_back(0x01); + CHECK(json::from_cbor(input, true, true, json::cbor_tag_handler_t::store) == 1); + } + SECTION("a well-formed deep value is read through the SAX interface") { std::vector input(200000, 0x9F); @@ -2175,6 +2190,52 @@ TEST_CASE("CBOR nesting does not consume the call stack") } } +TEST_CASE("CBOR input that cannot be read is discarded by every overload") +{ + std::vector input = json::to_cbor(json({{"a", {1, 2}}})); + input.pop_back(); + + json _; + CHECK_THROWS_AS(_ = json::from_cbor(input.begin(), input.end()), json::parse_error&); + CHECK(json::from_cbor(input, true, false).is_discarded()); + CHECK(json::from_cbor(input.begin(), input.end(), true, false).is_discarded()); + CHECK(json::from_cbor(input.data(), input.size(), true, false).is_discarded()); + CHECK(json::from_cbor({input.data(), input.size()}, true, false).is_discarded()); + + // a string that ends early, read through iterators that are not + // contiguous and have to be copied from one element at a time + const std::list truncated_string = {0x63, 'a', 'b'}; + CHECK(json::from_cbor(truncated_string.begin(), truncated_string.end(), true, false).is_discarded()); + const std::list complete_string = {0x63, 'a', 'b', 'c'}; + CHECK(json::from_cbor(complete_string.begin(), complete_string.end()) == "abc"); +} + +TEST_CASE("CBOR SAX parsing stops at every event") +{ + // Containers are opened and closed by the loop that reads them; a SAX + // handler that rejects any event - including the end of a nested + // container - must stop the parse right there. + const auto count_events = [](const std::vector& input) + { + int events = 0; + while (true) + { + SaxCountdown scp(events); + if (json::sax_parse(input, &scp, json::input_format_t::cbor)) + { + return events; + } + ++events; + REQUIRE(events < 1000); + } + }; + + // 20 events: every container kind closes inside another one + const json j = json::parse(R"({"a": [1, {"b": []}], "c": {"d": [[2]]}})"); + CHECK(count_events(json::to_cbor(j)) == 20); + CHECK(count_events(std::vector({0xBF, 0x61, 'a', 0x9F, 0x01, 0xFF, 0xFF})) == 6); +} + TEST_CASE("CBOR indefinite-length strings do not recurse per chunk") { // Reading an indefinite-length string or byte array used to call itself @@ -2482,60 +2543,34 @@ TEST_CASE("CBOR roundtrips" * doctest::skip()) { CAPTURE(filename) + std::ifstream f_json(filename); + const json j1 = json::parse(f_json); + const auto packed = utils::read_binary_file(filename + ".cbor"); + { INFO_WITH_TEMP(filename + ": std::vector"); - // parse JSON file - std::ifstream f_json(filename); - const json j1 = json::parse(f_json); - - // parse CBOR file - const auto packed = utils::read_binary_file(filename + ".cbor"); json j2; CHECK_NOTHROW(j2 = json::from_cbor(packed)); - - // compare parsed JSON values CHECK(j1 == j2); } { INFO_WITH_TEMP(filename + ": std::ifstream"); - // parse JSON file - std::ifstream f_json(filename); - const json j1 = json::parse(f_json); - - // parse CBOR file std::ifstream f_cbor(filename + ".cbor", std::ios::binary); json j2; CHECK_NOTHROW(j2 = json::from_cbor(f_cbor)); - - // compare parsed JSON values CHECK(j1 == j2); } { INFO_WITH_TEMP(filename + ": uint8_t* and size"); - // parse JSON file - std::ifstream f_json(filename); - const json j1 = json::parse(f_json); - - // parse CBOR file - const auto packed = utils::read_binary_file(filename + ".cbor"); json j2; CHECK_NOTHROW(j2 = json::from_cbor({packed.data(), packed.size()})); - - // compare parsed JSON values CHECK(j1 == j2); } { INFO_WITH_TEMP(filename + ": output to output adapters"); - // parse JSON file - std::ifstream f_json(filename); - json const j1 = json::parse(f_json); - - // parse CBOR file - const auto packed = utils::read_binary_file(filename + ".cbor"); - if (exclude_packed.count(filename) == 0u) { { @@ -3033,6 +3068,77 @@ TEST_CASE("Tagged values") CHECK_THROWS_AS(_ = json::from_cbor(v_tagged, true, true, json::cbor_tag_handler_t::error), json::parse_error); CHECK_THROWS_AS(_ = json::from_cbor(v_tagged, true, true, json::cbor_tag_handler_t::ignore), json::parse_error); } + + SECTION("issue #5316 - cbor_tag_handler_t::store on non-binary tagged items") + { + // 55799({"a": 1}) -- CBOR self-describe magic followed by a map + const std::vector v_map{0xD9, 0xD9, 0xF7, 0xA1, 0x61, 0x61, 0x01}; + CHECK(json::from_cbor(v_map, true, true, json::cbor_tag_handler_t::ignore) == json({{"a", 1}})); + CHECK(json::from_cbor(v_map, true, true, json::cbor_tag_handler_t::store) == json({{"a", 1}})); + + // Tag 24 over unsigned integer 5 + const std::vector v_int{0xD8, 0x18, 0x05}; + CHECK(json::from_cbor(v_int, true, true, json::cbor_tag_handler_t::ignore) == 5); + CHECK(json::from_cbor(v_int, true, true, json::cbor_tag_handler_t::store) == 5); + + // Tag 24 over text string "foo" + const std::vector v_str{0xD8, 0x18, 0x63, 'f', 'o', 'o'}; + CHECK(json::from_cbor(v_str, true, true, json::cbor_tag_handler_t::ignore) == "foo"); + CHECK(json::from_cbor(v_str, true, true, json::cbor_tag_handler_t::store) == "foo"); + + // Tag 24 over array [1, 2] + const std::vector v_arr{0xD8, 0x18, 0x82, 0x01, 0x02}; + CHECK(json::from_cbor(v_arr, true, true, json::cbor_tag_handler_t::ignore) == json({1, 2})); + CHECK(json::from_cbor(v_arr, true, true, json::cbor_tag_handler_t::store) == json({1, 2})); + + // Tag 24 over boolean true + const std::vector v_bool{0xD8, 0x18, 0xF5}; + CHECK(json::from_cbor(v_bool, true, true, json::cbor_tag_handler_t::ignore) == true); + CHECK(json::from_cbor(v_bool, true, true, json::cbor_tag_handler_t::store) == true); + + // Tag 24 over null + const std::vector v_null{0xD8, 0x18, 0xF6}; + CHECK(json::from_cbor(v_null, true, true, json::cbor_tag_handler_t::ignore) == nullptr); + CHECK(json::from_cbor(v_null, true, true, json::cbor_tag_handler_t::store) == nullptr); + + // Nested tags: tag 55799 over tag 24 over integer 42 + const std::vector v_nested{0xD9, 0xD9, 0xF7, 0xD8, 0x18, 0x18, 0x2A}; + CHECK(json::from_cbor(v_nested, true, true, json::cbor_tag_handler_t::ignore) == 42); + CHECK(json::from_cbor(v_nested, true, true, json::cbor_tag_handler_t::store) == 42); + + // Tag 24 over byte string continues to store subtype as before + const std::vector v_bin{0xD8, 0x18, 0x42, 0xCA, 0xFE}; + auto j_bin_store = json::from_cbor(v_bin, true, true, json::cbor_tag_handler_t::store); + CHECK(j_bin_store.is_binary()); + CHECK(j_bin_store.get_binary().has_subtype()); + CHECK(j_bin_store.get_binary().subtype() == 24); + CHECK(j_bin_store.get_binary() == json::binary({0xCA, 0xFE}, 24).get_binary()); + + // Tagged values inside a container under store: [24(1), 25(h'0001')] + const std::vector v_container{0x82, 0xD8, 0x18, 0x01, 0xD8, 0x19, 0x42, 0x00, 0x01}; + auto j_container_store = json::from_cbor(v_container, true, true, json::cbor_tag_handler_t::store); + CHECK(j_container_store.is_array()); + CHECK(j_container_store.size() == 2); + CHECK(j_container_store[0] == 1); + CHECK(j_container_store[1].is_binary()); + CHECK(j_container_store[1].get_binary().has_subtype()); + CHECK(j_container_store[1].get_binary().subtype() == 25); + CHECK(j_container_store[1].get_binary() == json::binary({0x00, 0x01}, 25).get_binary()); + + // Tagged values as object values under store: {"a": 55799(1), "b": 24(h'01')} + const std::vector v_object{0xA2, 0x61, 'a', 0xD9, 0xD9, 0xF7, 0x01, 0x61, 'b', 0xD8, 0x18, 0x41, 0x01}; + CHECK(json::from_cbor(v_object, true, true, json::cbor_tag_handler_t::store) == json({{"a", 1}, {"b", json::binary({0x01}, 24)}})); + + // two tags in a row before a byte string: the inner tag is stored + // (this uses item_read and then the byte-string path) + const std::vector v_nested_byte_string{0xD8, 0x18, 0xD8, 0x19, 0x42, 0x00, 0x01}; + CHECK(json::from_cbor(v_nested_byte_string, true, true, json::cbor_tag_handler_t::store) == json::binary({0x00, 0x01}, 25)); + + // errors after a stored tag are now the same as with ignore + json _; + CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector {0xD8, 0x18}, true, true, json::cbor_tag_handler_t::store), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing CBOR value: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector {0xD8, 0x18, 0x1C}, true, true, json::cbor_tag_handler_t::store), "[json.exception.parse_error.112] parse error at byte 3: syntax error while parsing CBOR value: invalid byte: 0x1C", json::parse_error&); + } } SECTION("negative integer overflow") diff --git a/tests/src/unit-class_const_iterator.cpp b/tests/src/unit-class_const_iterator.cpp index 9e5de42c7..e21e18fc9 100644 --- a/tests/src/unit-class_const_iterator.cpp +++ b/tests/src/unit-class_const_iterator.cpp @@ -43,6 +43,13 @@ TEST_CASE("const_iterator class") json::const_iterator const it(&j); json::const_iterator it2(&j); it2 = it; + + // assigning an iterator to itself leaves it unchanged + json const a = {1, 2, 3}; + json::const_iterator it3 = a.cbegin() + 1; + const json::const_iterator& same = it3; + it3 = same; + CHECK(*it3 == 2); } SECTION("copy constructor from non-const iterator") diff --git a/tests/src/unit-class_lexer.cpp b/tests/src/unit-class_lexer.cpp index e89497738..cb8ceed6b 100644 --- a/tests/src/unit-class_lexer.cpp +++ b/tests/src/unit-class_lexer.cpp @@ -12,6 +12,7 @@ #include using nlohmann::json; +#include // FLT_EVAL_METHOD #include // strtod #include // stringstream #include // string @@ -657,3 +658,45 @@ TEST_CASE("lexer string fast path") } } } + +TEST_CASE("parse_float_fast declines what it cannot convert exactly") +{ + // The lexer only hands well-formed numbers to parse_float_fast, so the + // malformed ones below can only be passed to it directly. Declining is + // always safe: the caller then falls back to a slower, exact conversion. + const auto fast = [](const std::string & s, double & out) + { + return nlohmann::detail::parse_float_fast(s.data(), s.data() + s.size(), '.', out); + }; + double out = 0; + +#if defined(FLT_EVAL_METHOD) && FLT_EVAL_METHOD != 0 + // without true double precision, the fast path declines everything + CHECK_FALSE(fast("1.5", out)); +#else + CHECK(fast("1.5", out)); + CHECK(out == 1.5); + CHECK(fast("+2.5e1", out)); + CHECK(out == 25.0); + CHECK(fast("-25E-1", out)); + CHECK(out == -2.5); + CHECK(fast("1e", out)); + CHECK(out == 1.0); +#endif + + // not a number + CHECK_FALSE(fast("", out)); + CHECK_FALSE(fast("-", out)); + CHECK_FALSE(fast(".", out)); + CHECK_FALSE(fast("1.2.3", out)); + CHECK_FALSE(fast("1x", out)); + CHECK_FALSE(fast("1e+", out)); + CHECK_FALSE(fast("1e1x", out)); + + // numbers that are not represented exactly on the fast path + CHECK_FALSE(fast("12345678901234567890", out)); + CHECK_FALSE(fast("1e10000", out)); + CHECK_FALSE(fast("9007199254740993", out)); + CHECK_FALSE(fast("1e23", out)); + CHECK_FALSE(fast("1e-23", out)); +} diff --git a/tests/src/unit-class_parser.cpp b/tests/src/unit-class_parser.cpp index 707f23c18..3d825162e 100644 --- a/tests/src/unit-class_parser.cpp +++ b/tests/src/unit-class_parser.cpp @@ -2761,7 +2761,7 @@ TEST_CASE("diagnostic positions: value lifetime, input adapters, and SAX") SECTION("binary formats have no text positions") { - // binary formats (CBOR, MessagePack, UBJSON, BSON, BJData) are + // binary formats (BJData, BON8, BSON, CBOR, MessagePack, UBJSON) are // parsed via detail::binary_reader, which never sets // start_position/end_position on the values it produces (they // have no notion of a text offset), so every value's position @@ -2778,6 +2778,10 @@ TEST_CASE("diagnostic positions: value lifetime, input adapters, and SAX") CHECK(from_msgpack.start_pos() == std::string::npos); CHECK(from_msgpack.end_pos() == std::string::npos); + const json from_bon8 = json::from_bon8(json::to_bon8(src)); + CHECK(from_bon8.start_pos() == std::string::npos); + CHECK(from_bon8.end_pos() == std::string::npos); + const json from_ubjson = json::from_ubjson(json::to_ubjson(src)); CHECK(from_ubjson.start_pos() == std::string::npos); CHECK(from_ubjson.end_pos() == std::string::npos); diff --git a/tests/src/unit-comparison.cpp b/tests/src/unit-comparison.cpp index 68d20aca9..69c0103c9 100644 --- a/tests/src/unit-comparison.cpp +++ b/tests/src/unit-comparison.cpp @@ -15,7 +15,13 @@ #include "doctest_compatibility.h" +#include + #include +#include +#include +#include +#include #define JSON_TESTS_PRIVATE #include @@ -359,6 +365,15 @@ TEST_CASE("lexicographical comparison operators") CHECK(json(1) < json(1.5)); CHECK(json(1.5) < json(2)); CHECK(json(2) > json(1.5)); + CHECK(json(-1) > json(-1.5)); + CHECK(json(-1.5) < json(-1)); + CHECK(json(-2) < json(-1.5)); + + // a float below the range of the integer type + CHECK(json(0) > json(-1e30)); + CHECK(json(-1e30) < json(0)); + CHECK(json(0u) > json(-0.5)); + CHECK(json(-0.5) < json(0u)); // a NaN operand stays unordered against either integer kind CHECK_FALSE(json(1) == json(nan)); @@ -735,3 +750,205 @@ TEST_CASE("regression #3868 - heterogeneous comparisons compile under C++20 (P24 } } #endif + +namespace +{ +// orders keys ascending or descending, as chosen when a map is created +template +class directed_less +{ + public: + directed_less() = default; + + explicit directed_less(const bool descending) noexcept + : m_descending(descending) + {} + + bool operator()(const Key& lhs, const Key& rhs) const + { + return m_descending ? rhs < lhs : lhs < rhs; + } + + private: + bool m_descending = false; +}; + +// An object type that, like std::unordered_map, enumerates its entries in no +// fixed order - ascending or descending by key, depending on how the map was +// created - and whose operator== does not depend on that order. +// std::unordered_map itself cannot be used here: the standard does not +// require it to accept an incomplete mapped type such as basic_json, and +// libstdc++ 6 to 9 as well as the EDG front ends of icpc and nvc++ reject +// basic_json. std::map, the default object type, works +// with all supported compilers. +template +struct unordered_object_t : std::map, Allocator> +{ + using base_type = std::map, Allocator>; + using base_type::base_type; + + friend bool operator==(const unordered_object_t& lhs, const unordered_object_t& rhs) + { + return lhs.size() == rhs.size() && std::all_of(lhs.begin(), lhs.end(), [&rhs](const std::pair& entry) + { + const auto it = rhs.find(entry.first); + return it != rhs.end() && it->second == entry.second; + }); + } + + friend bool operator!=(const unordered_object_t& lhs, const unordered_object_t& rhs) + { + return !(lhs == rhs); + } +}; +using unordered_json = nlohmann::basic_json; + +// the entries "0" to "9", enumerated in ascending or in descending order +unordered_json make_unordered_object(const bool descending) +{ + unordered_json j = unordered_json::object_t(directed_less(descending)); + for (int i = 0; i < 10; ++i) + { + j[std::to_string(i)] = i; + } + return j; +} + +template +Json nest(Json j, const std::size_t depth) +{ + for (std::size_t i = 0; i < depth; ++i) + { + Json outer = Json::object(); + outer["x"] = std::move(j); + j = std::move(outer); + } + return j; +} +} // namespace + +TEST_CASE("equality of objects whose entries have no fixed order") +{ + // Values nested deeper than a bound are compared without the call stack, + // entry by entry. That must agree with the object type's own operator==, + // which for unordered_object_t (as for std::unordered_map) does not + // depend on the order of the entries, and for ordered_map does. + REQUIRE(make_unordered_object(true).begin().key() == "9"); + REQUIRE(make_unordered_object(false).begin().key() == "0"); + + for (const std::size_t depth : std::vector {0, 200}) + { + CAPTURE(depth); + + const unordered_json descending = nest(make_unordered_object(true), depth); + const unordered_json ascending = nest(make_unordered_object(false), depth); + CHECK(descending == ascending); + CHECK_FALSE(descending != ascending); + + // a copy is equal to its original + const unordered_json copy = descending; // NOLINT(performance-unnecessary-copy-initialization) + CHECK(copy == descending); + + // a different value, a different key, or another entry still count + unordered_json other_value = make_unordered_object(true); + other_value["5"] = 42; + CHECK_FALSE(nest(other_value, depth) == ascending); + + unordered_json other_key = make_unordered_object(true); + other_key.erase("5"); + other_key["50"] = 5; + CHECK_FALSE(nest(other_key, depth) == ascending); + + unordered_json more_entries = make_unordered_object(true); + more_entries["10"] = 10; + CHECK_FALSE(nest(more_entries, depth) == ascending); + CHECK_FALSE(ascending == nest(more_entries, depth)); + + // ordered_json compares its entries in sequence + const nlohmann::ordered_json ab = nest(nlohmann::ordered_json({{"a", 1}, {"b", 2}}), depth); + const nlohmann::ordered_json ba = nest(nlohmann::ordered_json({{"b", 2}, {"a", 1}}), depth); + CHECK_FALSE(ab == ba); + CHECK(ab != ba); + } +} + +TEST_CASE("containers are compared element by element") +{ + // Containers nested deeper than a bound are compared without the call + // stack, by code of their own; every relation is checked both at the top + // level and below that bound. + const auto deep = [](const json & j, const std::size_t depth) + { + json result = j; + for (std::size_t i = 0; i < depth; ++i) + { + result = json::array({std::move(result)}); + } + return result; + }; + + for (const std::size_t depth : std::vector {0, 200}) + { + CAPTURE(depth); + + // objects with different keys + { + const json a = deep({{"a", 1}}, depth); + const json b = deep({{"b", 1}}, depth); + CHECK_FALSE(a == b); + CHECK(a != b); + CHECK(a < b); + CHECK(b > a); + CHECK_FALSE(b < a); +#if JSON_HAS_THREE_WAY_COMPARISON + // JSON_HAS_CPP_20 (do not remove; see note at top of file) + CHECK((a <=> b) == std::partial_ordering::less); // *NOPAD* + CHECK((b <=> a) == std::partial_ordering::greater); // *NOPAD* + CHECK((a <=> a) == std::partial_ordering::equivalent); // *NOPAD* +#endif + } + + // a container that is a prefix of the other one + { + // the one that runs out of elements first is the smaller one + const json shorter = deep({1}, depth); + const json longer = deep({1, 2}, depth); + CHECK(shorter < longer); + CHECK(longer > shorter); + CHECK_FALSE(longer < shorter); + CHECK_FALSE(shorter == longer); + + const json smaller_object = deep({{"a", 1}}, depth); + const json larger_object = deep({{"a", 1}, {"b", 2}}, depth); + CHECK(smaller_object < larger_object); + CHECK(larger_object > smaller_object); + CHECK_FALSE(smaller_object == larger_object); +#if JSON_HAS_THREE_WAY_COMPARISON + // JSON_HAS_CPP_20 (do not remove; see note at top of file) + CHECK((shorter <=> longer) == std::partial_ordering::less); // *NOPAD* + CHECK((longer <=> shorter) == std::partial_ordering::greater); // *NOPAD* +#endif + } + + // elements that cannot be ordered + { + const double nan = std::numeric_limits::quiet_NaN(); + const json lhs = deep({nan, 1}, depth); + const json rhs = deep({nan, 2}, depth); + + CHECK_FALSE(lhs == lhs); + CHECK_FALSE(rhs < lhs); +#if JSON_HAS_THREE_WAY_COMPARISON + // JSON_HAS_CPP_20 (do not remove; see note at top of file) + // operator<=> stops there, as std::lexicographical_compare_three_way + // does, and operator< is derived from it + CHECK((lhs <=> rhs) == std::partial_ordering::unordered); // *NOPAD* + CHECK_FALSE(lhs < rhs); +#else + // operator< skips a pair of elements that cannot be ordered, as + // std::lexicographical_compare does, and the next pair decides + CHECK(lhs < rhs); +#endif + } + } +} diff --git a/tests/src/unit-custom-binary-type.cpp b/tests/src/unit-custom-binary-type.cpp index d357ec9a3..d885376cb 100644 --- a/tests/src/unit-custom-binary-type.cpp +++ b/tests/src/unit-custom-binary-type.cpp @@ -49,6 +49,16 @@ TEST_CASE("binary type whose value type is not std::uint8_t") CHECK(char_binary_json::binary({}).dump() == R"({"bytes":[],"subtype":null})"); } + SECTION("a value is converted to the binary type if it is binary or an array") + { + const std::vector chars{'\0', '\x01', '\x7F'}; + CHECK(char_binary_json::binary(chars).get>() == chars); + CHECK(char_binary_json({0, 1, 127}).get>() == chars); + CHECK_THROWS_WITH_AS(char_binary_json(1).get>(), + "[json.exception.type_error.302] type must be binary or array, but is number", + char_binary_json::type_error&); + } + SECTION("the default binary type is unchanged") { CHECK(nlohmann::json::binary({0, 1, 255}, 42).dump() == R"({"bytes":[0,1,255],"subtype":42})"); @@ -74,6 +84,8 @@ TEST_CASE("binary type whose value type is not std::uint8_t") // UBJSON has no binary type, so binary values are written as an array CHECK(byte_binary_json::from_ubjson(byte_binary_json::to_ubjson(j)) == byte_binary_json({0, 1, 255})); + // the same holds for BON8 + CHECK(byte_binary_json::from_bon8(byte_binary_json::to_bon8(j)) == byte_binary_json({0, 1, 255})); } #endif } diff --git a/tests/src/unit-custom-object-type.cpp b/tests/src/unit-custom-object-type.cpp index cb2cb5ff3..a579e0b2d 100644 --- a/tests/src/unit-custom-object-type.cpp +++ b/tests/src/unit-custom-object-type.cpp @@ -297,6 +297,7 @@ TEST_CASE("object type without key_compare") const auto j = no_key_compare_json::parse(R"({"a":[1,2,3],"b":"x"})"); CHECK(no_key_compare_json::from_cbor(no_key_compare_json::to_cbor(j)) == j); CHECK(no_key_compare_json::from_msgpack(no_key_compare_json::to_msgpack(j)) == j); + CHECK(no_key_compare_json::from_bon8(no_key_compare_json::to_bon8(j)) == j); } SECTION("flatten and unflatten") diff --git a/tests/src/unit-diagnostic-positions.cpp b/tests/src/unit-diagnostic-positions.cpp index d607e935c..5326094c7 100644 --- a/tests/src/unit-diagnostic-positions.cpp +++ b/tests/src/unit-diagnostic-positions.cpp @@ -156,3 +156,38 @@ TEST_CASE("Better diagnostics with positions") #endif } } + +TEST_CASE("values read from a binary format have no positions") +{ + // only the JSON lexer knows where a value started and ended + const json source = {{"a", {1, "x", json::binary({1})}}, {"b", {{"c", true}}}, {"d", nullptr}, {"e", 1.5}}; + const std::vector cbor = json::to_cbor(source); + + const auto check_no_positions = [](const json & j) + { + CHECK(j.start_pos() == std::string::npos); + CHECK(j.end_pos() == std::string::npos); + CHECK(j.at("a").start_pos() == std::string::npos); + CHECK(j.at("a").at(1).end_pos() == std::string::npos); + CHECK(j.at("b").at("c").start_pos() == std::string::npos); + }; + + SECTION("DOM parser") + { + const json j = json::from_cbor(cbor); + CHECK(j == source); + check_no_positions(j); + } + + SECTION("DOM parser with a callback") + { + json j; + nlohmann::detail::json_sax_dom_callback_parser sdp(j, [](int /*unused*/, json::parse_event_t /*unused*/, const json& /*unused*/) noexcept + { + return true; + }); + CHECK(json::sax_parse(cbor, &sdp, json::input_format_t::cbor)); + CHECK(j == source); + check_no_positions(j); + } +} diff --git a/tests/src/unit-element_access2.cpp b/tests/src/unit-element_access2.cpp index c7c1b824d..40e8216d5 100644 --- a/tests/src/unit-element_access2.cpp +++ b/tests/src/unit-element_access2.cpp @@ -1517,6 +1517,16 @@ TEST_CASE_TEMPLATE("element access 2 (throwing tests)", Json, nlohmann::json, nl CHECK(j.value("/not/existing"_json_pointer, Json({{"foo", "bar"}})) == Json({{"foo", "bar"}})); CHECK(j.value("/not/existing"_json_pointer, Json({10, 100})) == Json({10, 100})); + // an array index that is out of range, too large to be + // represented, or "-", and a token below a scalar + CHECK(j.value("/array/3"_json_pointer, 2) == 2); + CHECK(j.value("/array/-"_json_pointer, 2) == 2); + CHECK(j.value("/array/99999999999999999999999999"_json_pointer, 2) == 2); + CHECK(j.value("/integer/0"_json_pointer, 2) == 2); + CHECK(j.value("/string/x"_json_pointer, 2) == 2); + CHECK(j.value("/null/x"_json_pointer, 2) == 2); + CHECK(j.value("/array/0"_json_pointer, 2) == 1); + CHECK(j_const.value("/not/existing"_json_pointer, 2) == 2); CHECK(j_const.value("/not/existing"_json_pointer, 2u) == 2u); CHECK(j_const.value("/not/existing"_json_pointer, false) == false); diff --git a/tests/src/unit-explicit_instantiation.cpp b/tests/src/unit-explicit_instantiation.cpp new file mode 100644 index 000000000..71132a59f --- /dev/null +++ b/tests/src/unit-explicit_instantiation.cpp @@ -0,0 +1,37 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// cmake/test.cmake selects the C++ standard versions with which to build a +// unit test based on the presence of JSON_HAS_CPP_ macros. +// The regression below only showed on C++17, so build this file for every +// standard like the other regression tests: +// JSON_HAS_CPP_17 JSON_HAS_CPP_20 (do not remove; see note at top of file) + +#include "doctest_compatibility.h" + +#include +using json = nlohmann::json; + +///////////////////////////////////////////////////////////////////// +// for #4825 - explicitly instantiating basic_json must compile; this +// forces instantiation of binary_writer::write_bjdata_ndarray, whose +// static_cast was ambiguous under explicit instantiation on +// C++17. Merely compiling this translation unit is the regression test. +// +// The instantiation compiles every member function, so it has a file of its +// own: in unit-regression3.cpp it made the object too large for the MinGW +// linker to relocate (see #5511). +///////////////////////////////////////////////////////////////////// +template class nlohmann::basic_json<>; + +TEST_CASE("explicit instantiation of basic_json (#4825)") +{ + const json j = {1, "two", 3.0}; + CHECK(j.size() == 3); + CHECK(json::from_bjdata(json::to_bjdata(j)) == j); +} diff --git a/tests/src/unit-json_patch.cpp b/tests/src/unit-json_patch.cpp index 3797267b7..78c152ced 100644 --- a/tests/src/unit-json_patch.cpp +++ b/tests/src/unit-json_patch.cpp @@ -1904,3 +1904,98 @@ TEST_CASE("JSON patch: diff of deeply nested values") } } } + +TEST_CASE("JSON patch - every operation on ordered_json") +{ + using nlohmann::ordered_json; + + const ordered_json doc = {{"foo", "bar"}, {"arr", {1, 2, 3}}, {"obj", {{"a", 1}}}}; + + SECTION("successful operations") + { + const ordered_json patch = ordered_json::parse(R"([ + {"op": "add", "path": "/obj/b", "value": 2}, + {"op": "add", "path": "/arr/1", "value": 9}, + {"op": "add", "path": "/arr/-", "value": 4}, + {"op": "remove", "path": "/arr/0"}, + {"op": "remove", "path": "/obj/a"}, + {"op": "replace", "path": "/foo", "value": "baz"}, + {"op": "move", "from": "/foo", "path": "/moved"}, + {"op": "copy", "from": "/obj", "path": "/copied"}, + {"op": "test", "path": "/copied/b", "value": 2} + ])"); + + const ordered_json expected = ordered_json::parse(R"({ + "arr": [9, 2, 3, 4], "obj": {"b": 2}, "moved": "baz", "copied": {"b": 2} + })"); + + CHECK(doc.patch(patch) == expected); + + // adding to the root replaces the document + CHECK(doc.patch(ordered_json::parse(R"([{"op": "add", "path": "", "value": [1]}])")) == ordered_json({1})); + } + + SECTION("failing operations") + { + ordered_json _; +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "add", "path": "/arr/4", "value": 1}])")), + "[json.exception.out_of_range.401] (/arr) array index 4 is out of range", ordered_json::out_of_range&); +#else + CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "add", "path": "/arr/4", "value": 1}])")), + "[json.exception.out_of_range.401] array index 4 is out of range", ordered_json::out_of_range&); +#endif + CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "add", "path": "/nope/x", "value": 1}])")), + "[json.exception.out_of_range.403] key 'nope' not found", ordered_json::out_of_range&); + CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "remove", "path": "/obj/nope"}])")), + "[json.exception.out_of_range.403] key 'nope' not found", ordered_json::out_of_range&); +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "remove", "path": "/arr/3"}])")), + "[json.exception.out_of_range.401] (/arr) array index 3 is out of range", ordered_json::out_of_range&); +#else + CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "remove", "path": "/arr/3"}])")), + "[json.exception.out_of_range.401] array index 3 is out of range", ordered_json::out_of_range&); +#endif +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "test", "path": "/foo", "value": "qux"}])")), + "[json.exception.other_error.501] (/0) unsuccessful: {\"op\":\"test\",\"path\":\"/foo\",\"value\":\"qux\"}", ordered_json::other_error&); +#elif JSON_DIAGNOSTIC_POSITIONS + CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "test", "path": "/foo", "value": "qux"}])")), + "[json.exception.other_error.501] (bytes 1-47) unsuccessful: {\"op\":\"test\",\"path\":\"/foo\",\"value\":\"qux\"}", ordered_json::other_error&); +#else + CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "test", "path": "/foo", "value": "qux"}])")), + "[json.exception.other_error.501] unsuccessful: {\"op\":\"test\",\"path\":\"/foo\",\"value\":\"qux\"}", ordered_json::other_error&); +#endif +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "add", "path": "/foo"}])")), + "[json.exception.parse_error.105] parse error: (/0) operation 'add' must have member 'value'", ordered_json::parse_error&); +#elif JSON_DIAGNOSTIC_POSITIONS + CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "add", "path": "/foo"}])")), + "[json.exception.parse_error.105] parse error: (bytes 1-30) operation 'add' must have member 'value'", ordered_json::parse_error&); +#else + CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "add", "path": "/foo"}])")), + "[json.exception.parse_error.105] parse error: operation 'add' must have member 'value'", ordered_json::parse_error&); +#endif + CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "move", "from": "/obj", "path": "/obj/a/b"}])")), + "[json.exception.out_of_range.414] cannot move value: 'from' path '/obj' is a proper prefix of 'path' '/obj/a/b'", ordered_json::out_of_range&); + } + + SECTION("diff reproduces the target") + { + const ordered_json source = {{"a", 1}, {"b", 2}, {"c", {{"x", 1}}}, {"l", {1, 2, 3}}}; + const std::vector targets = + { + // a key removed, a key added, a nested change, a shorter array + {{"a", 1}, {"c", {{"x", 2}}}, {"l", {1}}, {"d", 4}}, + // the same keys in another order + {{"c", {{"x", 1}}}, {"a", 1}, {"b", 2}, {"l", {1, 2, 3}}}, + // new keys ahead of the common ones + {{"new", true}, {"a", 1}, {"b", 3}, {"c", {{"x", 1}}}, {"l", {1, 2, 3}}}, + }; + for (const auto& target : targets) + { + CAPTURE(target.dump()); + CHECK(source.patch(ordered_json::diff(source, target)) == target); + } + } +} diff --git a/tests/src/unit-json_pointer.cpp b/tests/src/unit-json_pointer.cpp index b01df2921..e7d6df530 100644 --- a/tests/src/unit-json_pointer.cpp +++ b/tests/src/unit-json_pointer.cpp @@ -872,3 +872,16 @@ TEST_CASE("JSON pointers") } #endif } + +TEST_CASE("unescaping keeps a '~' that does not start an escape sequence") +{ + // the parser of a JSON pointer rejects such reference tokens before it + // unescapes them, so this is only reachable by calling unescape directly + std::string s = "a~2b~"; + nlohmann::detail::unescape(s); + CHECK(s == "a~2b~"); + + s = "~0~1~"; + nlohmann::detail::unescape(s); + CHECK(s == "~/~"); +} diff --git a/tests/src/unit-large_json.cpp b/tests/src/unit-large_json.cpp index 3734966d5..8204ed9b6 100644 --- a/tests/src/unit-large_json.cpp +++ b/tests/src/unit-large_json.cpp @@ -18,7 +18,7 @@ TEST_CASE("tests on very large JSONs") { SECTION("issue #1419 - Segmentation fault (stack overflow) due to unbounded recursion") { - const auto depth = 5000000; + const auto depth = 500000; std::string s(static_cast(2 * depth), '['); std::fill(s.begin() + depth, s.end(), ']'); diff --git a/tests/src/unit-locale-cpp.cpp b/tests/src/unit-locale-cpp.cpp index 0019d3b92..c2a113d06 100644 --- a/tests/src/unit-locale-cpp.cpp +++ b/tests/src/unit-locale-cpp.cpp @@ -158,6 +158,17 @@ TEST_CASE("locale-dependent test (LC_NUMERIC=de_DE)") json::sax_parse("12.34", &sax); CHECK(sax.float_string_copy == "12.34"); } + + SECTION("serializing a long double") + { + // a floating-point type that is not a float or a double is written + // with snprintf, whose locale-specific decimal point and thousands + // separator are undone afterwards + using long_double_json = nlohmann::basic_json; + CHECK(long_double_json(12345.5L).dump() == "12345.5"); + CHECK(long_double_json(1.0L).dump() == "1.0"); + CHECK(long_double_json(-0.25L).dump() == "-0.25"); + } } else { diff --git a/tests/src/unit-merge_patch.cpp b/tests/src/unit-merge_patch.cpp index c9741e85b..b1e0c431b 100644 --- a/tests/src/unit-merge_patch.cpp +++ b/tests/src/unit-merge_patch.cpp @@ -345,3 +345,32 @@ TEST_CASE("JSON Merge Patch on deeply nested values") CHECK(p->at("x") == 1); } } + +TEST_CASE("JSON Merge Patch and update on ordered_json") +{ + using nlohmann::ordered_json; + + SECTION("merge_patch") + { + ordered_json target = ordered_json::parse(R"({"a": {"b": 1, "c": 2}, "d": 3, "e": [1]})"); + target.merge_patch(ordered_json::parse(R"({"a": {"b": null, "f": 4}, "d": {"x": {"y": null}}, "e": null, "g": {"h": 5}})")); + CHECK(target == ordered_json::parse(R"({"a": {"c": 2, "f": 4}, "d": {"x": {}}, "g": {"h": 5}})")); + + // a patch that is not an object replaces the target + target.merge_patch(ordered_json({1, 2})); + CHECK(target == ordered_json({1, 2})); + // an object patch turns a target that is not an object into one + target.merge_patch(ordered_json::parse(R"({"k": {"l": null}})")); + CHECK(target == ordered_json::parse(R"({"k": {}})")); + } + + SECTION("update with merge_objects") + { + ordered_json target = ordered_json::parse(R"({"a": {"b": 1, "c": {"d": 2}}, "e": 3})"); + target.update(ordered_json::parse(R"({"a": {"c": {"x": 1}, "f": 4}, "e": {"y": 5}, "g": 6})"), true); + CHECK(target == ordered_json::parse(R"({"a": {"b": 1, "c": {"d": 2, "x": 1}, "f": 4}, "e": {"y": 5}, "g": 6})")); + + target.update(ordered_json::parse(R"({"a": 1})"), false); + CHECK(target == ordered_json::parse(R"({"a": 1, "e": {"y": 5}, "g": 6})")); + } +} diff --git a/tests/src/unit-msgpack.cpp b/tests/src/unit-msgpack.cpp index 805b03a13..e7616b27f 100644 --- a/tests/src/unit-msgpack.cpp +++ b/tests/src/unit-msgpack.cpp @@ -14,6 +14,7 @@ using nlohmann::json; using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) #endif +#include // SIZE_MAX, UINT32_MAX #include #include #include @@ -255,7 +256,7 @@ TEST_CASE("MessagePack") SECTION("256..65535 (int 16)") { - for (size_t i = 256; i <= 65535; ++i) + for (size_t i = 256; i <= 65535; i = utils::next_integer_sample(i, static_cast(65535), static_cast(7))) { CAPTURE(i) @@ -440,7 +441,7 @@ TEST_CASE("MessagePack") SECTION("-32768..-129 (int 16)") { - for (int16_t i = -32768; i <= static_cast(-129); ++i) + for (int16_t i = -32768; i <= static_cast(-129); i = utils::next_integer_sample(i, static_cast(-129), static_cast(7))) { CAPTURE(i) @@ -646,7 +647,7 @@ TEST_CASE("MessagePack") SECTION("256..65535 (uint 16)") { - for (size_t i = 256; i <= 65535; ++i) + for (size_t i = 256; i <= 65535; i = utils::next_integer_sample(i, static_cast(65535), static_cast(7))) { CAPTURE(i) @@ -1780,6 +1781,44 @@ TEST_CASE("MessagePack nesting does not consume the call stack") } } +TEST_CASE("MessagePack input that cannot be read is discarded by every overload") +{ + std::vector input = json::to_msgpack(json({{"a", {1, 2}}})); + input.pop_back(); + + json _; + CHECK_THROWS_AS(_ = json::from_msgpack(input.begin(), input.end()), json::parse_error&); + CHECK(json::from_msgpack(input, true, false).is_discarded()); + CHECK(json::from_msgpack(input.begin(), input.end(), true, false).is_discarded()); + CHECK(json::from_msgpack(input.data(), input.size(), true, false).is_discarded()); + CHECK(json::from_msgpack({input.data(), input.size()}, true, false).is_discarded()); +} + +TEST_CASE("MessagePack SAX parsing stops at every event") +{ + // Containers are opened and closed by the loop that reads them; a SAX + // handler that rejects any event - including the end of a nested + // container - must stop the parse right there. + const auto count_events = [](const std::vector& input) + { + int events = 0; + while (true) + { + SaxCountdown scp(events); + if (json::sax_parse(input, &scp, json::input_format_t::msgpack)) + { + return events; + } + ++events; + REQUIRE(events < 1000); + } + }; + + // 20 events: every container kind closes inside another one + const json j = json::parse(R"({"a": [1, {"b": []}], "c": {"d": [[2]]}})"); + CHECK(count_events(json::to_msgpack(j)) == 20); +} + TEST_CASE("single MessagePack roundtrip") { SECTION("sample.json") @@ -2004,60 +2043,34 @@ TEST_CASE("MessagePack roundtrips" * doctest::skip()) { CAPTURE(filename) + std::ifstream f_json(filename); + const json j1 = json::parse(f_json); + auto packed = utils::read_binary_file(filename + ".msgpack"); + { INFO_WITH_TEMP(filename + ": std::vector"); - // parse JSON file - std::ifstream f_json(filename); - const json j1 = json::parse(f_json); - - // parse MessagePack file - auto packed = utils::read_binary_file(filename + ".msgpack"); json j2; CHECK_NOTHROW(j2 = json::from_msgpack(packed)); - - // compare parsed JSON values CHECK(j1 == j2); } { INFO_WITH_TEMP(filename + ": std::ifstream"); - // parse JSON file - std::ifstream f_json(filename); - const json j1 = json::parse(f_json); - - // parse MessagePack file std::ifstream f_msgpack(filename + ".msgpack", std::ios::binary); json j2; CHECK_NOTHROW(j2 = json::from_msgpack(f_msgpack)); - - // compare parsed JSON values CHECK(j1 == j2); } { INFO_WITH_TEMP(filename + ": uint8_t* and size"); - // parse JSON file - std::ifstream f_json(filename); - const json j1 = json::parse(f_json); - - // parse MessagePack file - auto packed = utils::read_binary_file(filename + ".msgpack"); json j2; CHECK_NOTHROW(j2 = json::from_msgpack({packed.data(), packed.size()})); - - // compare parsed JSON values CHECK(j1 == j2); } { INFO_WITH_TEMP(filename + ": output to output adapters"); - // parse JSON file - std::ifstream f_json(filename); - json const j1 = json::parse(f_json); - - // parse MessagePack file - auto packed = utils::read_binary_file(filename + ".msgpack"); - if (exclude_packed.count(filename) == 0u) { { @@ -2150,3 +2163,249 @@ TEST_CASE("MessagePack with std::byte") } } #endif + +// the fake sizes below do not fit into a 32-bit std::size_t +#if SIZE_MAX > UINT32_MAX +template> +struct huge_array : std::vector +{ + using base = std::vector; + using base::base; + + bool fake_size = false; + + std::size_t size() const noexcept + { + if (fake_size) + { + return (std::numeric_limits::max)() + 1ULL; + } + + return base::size(); + } +}; + +using huge_array_json = nlohmann::basic_json < + std::map, huge_array, std::string, bool, std::int64_t, std::uint64_t, + double, std::allocator, nlohmann::adl_serializer, + std::vector, void >; + +TEST_CASE("MessagePack Size above uint32 for array") +{ + huge_array_json j = huge_array_json::array(); + + j.push_back(1); + j.push_back(2); + j.push_back(3); + + auto& array = j.get_ref(); + array.fake_size = true; + + CHECK_THROWS_WITH_AS( + huge_array_json::to_msgpack(j), + "[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295", + json::out_of_range&); + + array.fake_size = false; +} + +template, + typename A = std::allocator>> + struct huge_map : std::map +{ + using base = std::map; + using base::base; + + bool fake_size = false; + + std::size_t size() const noexcept + { + if (fake_size) + { + return static_cast(UINT32_MAX) + 1ULL; + } + + return base::size(); + } +}; + +using huge_object_json = nlohmann::basic_json < + huge_map, + std::vector, + std::string, + bool, + std::int64_t, + std::uint64_t, + double, + std::allocator, + nlohmann::adl_serializer, + std::vector, + void >; + +TEST_CASE("MessagePack Size above uint32 for object") +{ + + huge_object_json j = huge_object_json::object(); + + j["one"] = 1; + j["two"] = 2; + + auto& object = j.get_ref(); + object.fake_size = true; + + CHECK_THROWS_WITH_AS( + huge_object_json::to_msgpack(j), + "[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295", + json::out_of_range&); + + object.fake_size = false; +} + +struct huge_string : std::string +{ + using std::string::string; + + std::size_t size() const noexcept + { + return static_cast(UINT32_MAX) + 1ULL; + } +}; + +using huge_string_json = nlohmann::basic_json < + std::map, + std::vector, + huge_string, + bool, + std::int64_t, + std::uint64_t, + double, + std::allocator, + nlohmann::adl_serializer, + std::vector, + void >; + +TEST_CASE("MessagePack Size above uint32 for string") +{ + + huge_string_json j = "hello"; + + CHECK_THROWS_WITH_AS( + huge_string_json::to_msgpack(j), + "[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295", + json::out_of_range&); +} + +struct huge_binary : std::vector +{ + using std::vector::vector; + + std::size_t size() const noexcept + { + return static_cast(UINT32_MAX) + 1ULL; + } +}; + +using huge_binary_json = nlohmann::basic_json < + std::map, + std::vector, + std::string, + bool, + std::int64_t, + std::uint64_t, + double, + std::allocator, + nlohmann::adl_serializer, + huge_binary, + void >; + +TEST_CASE("MessagePack Size above uint32 for binary") +{ + + huge_binary_json j = huge_binary_json::binary(huge_binary{}); + + j.get_binary().push_back(0x01); + j.get_binary().push_back(0x02); + + CHECK_THROWS_WITH_AS( + huge_binary_json::to_msgpack(j), + "[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295", + json::out_of_range&); +} +#endif + +namespace +{ +// types that report a size beyond UINT32_MAX without allocating that much +// memory, so the MessagePack length limit can be tested cheaply; see the +// similar types in unit-bson.cpp +std::size_t beyond_uint32_size() +{ + return static_cast((std::numeric_limits::max)()) + 1; +} + +class beyond_uint32_binary_t : public std::vector +{ + public: + using std::vector::vector; + + size_type size() const noexcept // NOLINT(readability-convert-member-functions-to-static) + { + return beyond_uint32_size(); + } +}; + +// with clang and libstdc++ 10, the std::filesystem::path conversion that +// C++17 builds consider for every string type is ambiguous for a class +// derived from std::string, so the string case is not tested there +#if !(defined(__clang__) && defined(_GLIBCXX_RELEASE) && _GLIBCXX_RELEASE < 11) + #define JSON_TEST_BEYOND_UINT32_STRING 1 +#endif + +#ifdef JSON_TEST_BEYOND_UINT32_STRING +class beyond_uint32_string_t : public std::string +{ + public: + using std::string::string; + + size_type size() const noexcept // NOLINT(readability-convert-member-functions-to-static) + { + return beyond_uint32_size(); + } +}; + +using beyond_uint32_string_json = nlohmann::basic_json < + std::map, std::vector, beyond_uint32_string_t, bool, std::int64_t, std::uint64_t, + double, std::allocator, nlohmann::adl_serializer, std::vector, void >; +#endif + +using beyond_uint32_binary_json = nlohmann::basic_json < + std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, + double, std::allocator, nlohmann::adl_serializer, beyond_uint32_binary_t, void >; +} // namespace + +TEST_CASE("MessagePack lengths beyond UINT32_MAX cannot be serialized") +{ + // MessagePack stores the length of a string, binary value, array, or + // object in at most 32 bits; a larger one used to be written without any + // length at all +#if SIZE_MAX > UINT32_MAX + { + const char* const expected = "[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295"; + + const beyond_uint32_binary_json binary = beyond_uint32_binary_json::binary(beyond_uint32_binary_t{}); + CHECK_THROWS_WITH_AS(beyond_uint32_binary_json::to_msgpack(binary), expected, beyond_uint32_binary_json::out_of_range&); + + const beyond_uint32_binary_json ext = beyond_uint32_binary_json::binary(beyond_uint32_binary_t{}, 42); + CHECK_THROWS_WITH_AS(beyond_uint32_binary_json::to_msgpack(ext), expected, beyond_uint32_binary_json::out_of_range&); + +#ifdef JSON_TEST_BEYOND_UINT32_STRING + // created from its type rather than from a beyond_uint32_string_t: + // that would consider the std::filesystem::path conversion, which + // libstdc++ 10 cannot decide for a class derived from std::string + const beyond_uint32_string_json string(beyond_uint32_string_json::value_t::string); + CHECK_THROWS_WITH_AS(beyond_uint32_string_json::to_msgpack(string), expected, beyond_uint32_string_json::out_of_range&); +#endif + } +#endif +} diff --git a/tests/src/unit-ordered_json2.cpp b/tests/src/unit-ordered_json2.cpp index 83eb7668d..653820628 100644 --- a/tests/src/unit-ordered_json2.cpp +++ b/tests/src/unit-ordered_json2.cpp @@ -326,6 +326,15 @@ TEST_CASE("ordered_json across binary formats") CHECK(collect_keys(restored) == original_keys); CHECK(collect_keys(restored["mango"]) == original_mango_keys); } + + SECTION("BON8") + { + const auto bytes = ordered_json::to_bon8(original); + const auto restored = ordered_json::from_bon8(bytes); + CHECK(restored == original); + CHECK(collect_keys(restored) == original_keys); + CHECK(collect_keys(restored["mango"]) == original_mango_keys); + } } TEST_CASE("alt_json (custom string_t) across binary formats") @@ -353,6 +362,13 @@ TEST_CASE("alt_json (custom string_t) across binary formats") CHECK(restored == original); } + SECTION("BON8") + { + const auto bytes = alt_json::to_bon8(original); + const auto restored = alt_json::from_bon8(bytes); + CHECK(restored == original); + } + SECTION("BSON") { const auto bytes = alt_json::to_bson(original); diff --git a/tests/src/unit-regression2.cpp b/tests/src/unit-regression2.cpp index b128b7a73..c466958bf 100644 --- a/tests/src/unit-regression2.cpp +++ b/tests/src/unit-regression2.cpp @@ -332,6 +332,7 @@ TEST_CASE("regression tests 2") CHECK(float_json::from_cbor(float_json::to_cbor(j)) == j); CHECK(float_json::from_msgpack(float_json::to_msgpack(j)) == j); CHECK(float_json::from_ubjson(float_json::to_ubjson(j)) == j); + CHECK(float_json::from_bon8(float_json::to_bon8(j)) == j); float_json j2 = {1000.0, 2000.0, 3000.0}; CHECK(float_json::from_ubjson(float_json::to_ubjson(j2, true, true)) == j2); diff --git a/tests/src/unit-regression3.cpp b/tests/src/unit-regression3.cpp index 882b3866b..1c2ebd3f0 100644 --- a/tests/src/unit-regression3.cpp +++ b/tests/src/unit-regression3.cpp @@ -74,13 +74,7 @@ using ordered_json = nlohmann::ordered_json; #endif #endif -///////////////////////////////////////////////////////////////////// -// for #4825 - explicitly instantiating basic_json must compile; this -// forces instantiation of binary_writer::write_bjdata_ndarray, whose -// static_cast was ambiguous under explicit instantiation on -// C++17. Merely compiling this translation unit is the regression test. -///////////////////////////////////////////////////////////////////// -template class nlohmann::basic_json<>; +// the explicit instantiation for #4825 is in unit-explicit_instantiation.cpp ///////////////////////////////////////////////////////////////////// // for #4440 @@ -894,6 +888,7 @@ TEST_CASE("regression test #5476 - array type without reserve()") // the binary formats pass a definite length to start_array() CHECK(deque_json::from_cbor(deque_json::to_cbor(j)) == j); CHECK(deque_json::from_msgpack(deque_json::to_msgpack(j)) == j); + CHECK(deque_json::from_bon8(deque_json::to_bon8(j)) == j); // parse() instantiates the callback parser as well, which reserves too const auto with_callback = deque_json::parse(R"([1,2,3])", [](int /*depth*/, deque_json::parse_event_t /*event*/, deque_json& /*parsed*/) noexcept diff --git a/tests/src/unit-serialization.cpp b/tests/src/unit-serialization.cpp index 45617c3b3..bfb510fd0 100644 --- a/tests/src/unit-serialization.cpp +++ b/tests/src/unit-serialization.cpp @@ -639,3 +639,163 @@ TEST_CASE("serialization of deeply nested values") } } } + +namespace +{ +// wraps @a inner into @a depth single-element arrays +json wrap_in_arrays(const json& inner, const std::size_t depth) +{ + json j = inner; + for (std::size_t i = 0; i < depth; ++i) + { + j = json::array({std::move(j)}); + } + return j; +} + +// what wrap_in_arrays(inner, depth).dump(2) is expected to be: the arrays +// around inner.dump(2), with inner's own lines indented by the depth +std::string expected_pretty_in_arrays(const json& inner, const std::size_t depth) +{ + std::string expected; + for (std::size_t i = 0; i < depth; ++i) + { + expected += std::string(2 * i, ' ') + "[\n"; + } + + const std::string indent(2 * depth, ' '); + expected += indent; + for (const char c : inner.dump(2)) + { + expected += c; + if (c == '\n') + { + expected += indent; + } + } + + for (std::size_t i = depth; i > 0; --i) + { + expected += '\n' + std::string(2 * (i - 1), ' ') + ']'; + } + return expected; +} +} // namespace + +TEST_CASE("serialization of every kind of value below the bound of the descent") +{ + // Values nested deeper than the bound are written without the call stack, + // by code of their own; each kind of value must come out the same there as + // it does at the top level, compact and pretty-printed. + std::vector values = + { + json::parse(R"({"a": 1, "b": [1, 2, {"c": "x"}], "d": {}, "e": []})"), + json::parse(R"([1, [2, 3], {"k": null}, "s"])"), + json::object(), + json::array(), + json::binary({1, 2, 3}, 42), + json::binary({1, 2, 3}), + json::binary({}, 7), + json::binary({}), + "a string with \"escapes\"\n", + true, + false, + -42, + 42u, + 1.5, + nullptr, + json(json::value_t::discarded), + }; + // a pretty-printed object whose members are themselves deep + values.push_back({{"x", wrap_in_arrays(1, 5)}, {"y", {{"z", 2}}}}); + + for (const std::size_t depth : std::vector {1, 200}) + { + CAPTURE(depth); + for (const auto& inner : values) + { + CAPTURE(inner.dump()); + const json j = wrap_in_arrays(inner, depth); + CHECK(j.dump() == std::string(depth, '[') + inner.dump() + std::string(depth, ']')); + CHECK(j.dump(2) == expected_pretty_in_arrays(inner, depth)); + } + } + + SECTION("pretty-printed objects across the bound") + { + for (std::size_t d = 120; d <= 140; ++d) + { + CAPTURE(d); + + // built from the inside out: {"k": , "n": } + json j = 7; + std::string expected = "7"; + for (std::size_t i = d; i > 0; --i) + { + j = json({{"k", std::move(j)}, {"n", i}}); + + const std::string indent(2 * i, ' '); + const std::string outer_indent(2 * (i - 1), ' '); + std::string next = "{\n"; + next += indent; + next += "\"k\": "; + next += expected; + next += ",\n"; + next += indent; + next += "\"n\": "; + next += std::to_string(i); + next += '\n'; + next += outer_indent; + next += '}'; + expected = std::move(next); + } + + CHECK(j.dump(2) == expected); + CHECK(json::parse(j.dump(2)) == j); + CHECK(json::parse(j.dump()) == j); + } + } +} + +TEST_CASE("serializer buffers are flushed mid-string and mid-binary") +{ + SECTION("a long run of escaped characters") + { + // each character is escaped on its own, so the escape buffer fills up + const json newlines = std::string(600, '\n'); + std::string expected = "\""; + for (int i = 0; i < 600; ++i) + { + expected += "\\n"; + } + expected += '"'; + CHECK(newlines.dump() == expected); + + // every character is \u-escaped under ensure_ascii + std::string umlauts; + std::string escaped_umlauts = "\""; + for (int i = 0; i < 300; ++i) + { + umlauts += "\xC3\xA4"; + escaped_umlauts += "\\u00e4"; + } + escaped_umlauts += '"'; + CHECK(json(umlauts).dump(-1, ' ', true) == escaped_umlauts); + } + + SECTION("a large binary value") + { + std::vector bytes(3000); + std::string expected_bytes; + std::string expected_pretty_bytes; + for (std::size_t i = 0; i < bytes.size(); ++i) + { + bytes[i] = static_cast(i % 256); + expected_bytes += (i == 0 ? "" : ",") + std::to_string(i % 256); + expected_pretty_bytes += (i == 0 ? "" : ", ") + std::to_string(i % 256); + } + const json j = json::binary(bytes); + CHECK(j.dump() == "{\"bytes\":[" + expected_bytes + "],\"subtype\":null}"); + CHECK(j.dump(2) == "{\n \"bytes\": [" + expected_pretty_bytes + "],\n \"subtype\": null\n}"); + } +} diff --git a/tests/src/unit-std-format.cpp b/tests/src/unit-std-format.cpp index 58cbbf5cc..f2be8d5ec 100644 --- a/tests/src/unit-std-format.cpp +++ b/tests/src/unit-std-format.cpp @@ -102,6 +102,29 @@ TEST_CASE("std::formatter") CHECK_THROWS_AS(std::vformat("{:{}}", std::make_format_args(j, dynamic_width)), std::format_error); // dynamic width } + SECTION("a format spec may run to the end of the parse context") + { + // std::format always hands parse() a range that still holds the closing + // '}', but a parse context may also end right after the spec + const auto parse = [](const char* spec) + { + std::format_parse_context ctx(spec); + std::formatter f; + CHECK(f.parse(ctx) == ctx.end()); + return f; + }; + + CHECK(parse("").indent == -1); + CHECK(parse(">").indent == -1); + CHECK(parse("#").indent == 4); + CHECK(parse("3").indent == 3); + CHECK(parse("#12").indent == 12); + + const auto f = parse(".>"); + CHECK(f.indent == -1); + CHECK(f.indent_char == '.'); + } + SECTION("std::format_to writes through an arbitrary output iterator") { const json j = {{"foo", 1}, {"bar", {1, 2, 3}}}; diff --git a/tests/src/unit-ubjson.cpp b/tests/src/unit-ubjson.cpp index c8458c44d..c315fac94 100644 --- a/tests/src/unit-ubjson.cpp +++ b/tests/src/unit-ubjson.cpp @@ -265,7 +265,7 @@ TEST_CASE("UBJSON") SECTION("-32768..-129 (int16)") { - for (int32_t i = -32768; i <= -129; ++i) + for (int32_t i = -32768; i <= -129; i = utils::next_integer_sample(i, -129, 7)) { CAPTURE(i) @@ -425,7 +425,7 @@ TEST_CASE("UBJSON") SECTION("256..32767 (int16)") { - for (size_t i = 256; i <= 32767; ++i) + for (size_t i = 256; i <= 32767; i = utils::next_integer_sample(i, static_cast(32767), static_cast(7))) { CAPTURE(i) @@ -631,7 +631,7 @@ TEST_CASE("UBJSON") SECTION("256..32767 (int16)") { - for (size_t i = 256; i <= 32767; ++i) + for (size_t i = 256; i <= 32767; i = utils::next_integer_sample(i, static_cast(32767), static_cast(7))) { CAPTURE(i) @@ -1640,6 +1640,29 @@ TEST_CASE("UBJSON") }); CHECK_THROWS_AS(_ = json::sax_parse(v_ubjson, &scp, json::input_format_t::ubjson), json::out_of_range&); } + + SECTION("array with a known size, read with a callback") + { + // a sized array announces its length to start_array() + std::vector const v_ubjson = {'[', '#', 'i', 2, 'i', 1, 'i', 2}; + json j; + nlohmann::detail::json_sax_dom_callback_parser scp(j, [](int /*unused*/, json::parse_event_t /*unused*/, const json& /*unused*/) noexcept + { + return true; + }); + CHECK(json::sax_parse(v_ubjson, &scp, json::input_format_t::ubjson)); + CHECK(j == json({1, 2})); + + // the readers reject a size this large before they announce + // it, so it can only reach start_array() directly (the largest + // value stands for an unknown size and is never checked) + json k; + nlohmann::detail::json_sax_dom_callback_parser scp2(k, [](int /*unused*/, json::parse_event_t /*unused*/, const json& /*unused*/) noexcept + { + return true; + }); + CHECK_THROWS_AS(scp2.start_array((std::numeric_limits::max)() - 1), json::out_of_range&); + } } } @@ -2255,6 +2278,46 @@ TEST_CASE("UBJSON nesting does not consume the call stack") } } +TEST_CASE("UBJSON input that cannot be read is discarded by every overload") +{ + std::vector input = json::to_ubjson(json({{"a", {1, 2}}})); + input.pop_back(); + + json _; + CHECK_THROWS_AS(_ = json::from_ubjson(input.begin(), input.end()), json::parse_error&); + CHECK(json::from_ubjson(input, true, false).is_discarded()); + CHECK(json::from_ubjson(input.begin(), input.end(), true, false).is_discarded()); + CHECK(json::from_ubjson(input.data(), input.size(), true, false).is_discarded()); + CHECK(json::from_ubjson({input.data(), input.size()}, true, false).is_discarded()); +} + +TEST_CASE("UBJSON SAX parsing stops at every event") +{ + // Containers are opened and closed by the loop that reads them; a SAX + // handler that rejects any event - including the end of a nested + // container - must stop the parse right there. + const auto count_events = [](const std::vector& input) + { + int events = 0; + while (true) + { + SaxCountdown scp(events); + if (json::sax_parse(input, &scp, json::input_format_t::ubjson)) + { + return events; + } + ++events; + REQUIRE(events < 1000); + } + }; + + // 20 events: every container kind closes inside another one + const json j = json::parse(R"({"a": [1, {"b": []}], "c": {"d": [[2]]}})"); + CHECK(count_events(json::to_ubjson(j)) == 20); + CHECK(count_events(json::to_ubjson(j, true)) == 20); + CHECK(count_events(json::to_ubjson(j, true, true)) == 20); +} + TEST_CASE("UBJSON optimized arrays of a valueless type are bounded") { // An element of type 'Z', 'T' or 'F' is encoded by its marker alone, so an @@ -2917,60 +2980,34 @@ TEST_CASE("UBJSON roundtrips" * doctest::skip()) { CAPTURE(filename) + std::ifstream f_json(filename); + json const j1 = json::parse(f_json); + auto const packed = utils::read_binary_file(filename + ".ubjson"); + { INFO_WITH_TEMP(filename + ": std::vector"); - // parse JSON file - std::ifstream f_json(filename); - json const j1 = json::parse(f_json); - - // parse UBJSON file - auto const packed = utils::read_binary_file(filename + ".ubjson"); json j2; CHECK_NOTHROW(j2 = json::from_ubjson(packed)); - - // compare parsed JSON values CHECK(j1 == j2); } { INFO_WITH_TEMP(filename + ": std::ifstream"); - // parse JSON file - std::ifstream f_json(filename); - json const j1 = json::parse(f_json); - - // parse UBJSON file std::ifstream f_ubjson(filename + ".ubjson", std::ios::binary); json j2; CHECK_NOTHROW(j2 = json::from_ubjson(f_ubjson)); - - // compare parsed JSON values CHECK(j1 == j2); } { INFO_WITH_TEMP(filename + ": uint8_t* and size"); - // parse JSON file - std::ifstream f_json(filename); - const json j1 = json::parse(f_json); - - // parse UBJSON file - auto const packed = utils::read_binary_file(filename + ".ubjson"); json j2; CHECK_NOTHROW(j2 = json::from_ubjson({packed.data(), packed.size()})); - - // compare parsed JSON values CHECK(j1 == j2); } { INFO_WITH_TEMP(filename + ": output to output adapters"); - // parse JSON file - std::ifstream f_json(filename); - json const j1 = json::parse(f_json); - - // parse UBJSON file - auto const packed = utils::read_binary_file(filename + ".ubjson"); - { INFO_WITH_TEMP(filename + ": output adapters: std::vector"); std::vector vec; @@ -2981,3 +3018,18 @@ TEST_CASE("UBJSON roundtrips" * doctest::skip()) } } } + +TEST_CASE("UBJSON optimized array of unsigned integers beyond int64") +{ + // UBJSON has no unsigned 64-bit type, so such values are written as + // high-precision numbers - also as the type of an optimized container + const json j = {18446744073709551615ULL, 9223372036854775808ULL}; + const std::vector expected = + { + '[', '$', 'H', '#', 'i', 2, + 'i', 20, '1', '8', '4', '4', '6', '7', '4', '4', '0', '7', '3', '7', '0', '9', '5', '5', '1', '6', '1', '5', + 'i', 19, '9', '2', '2', '3', '3', '7', '2', '0', '3', '6', '8', '5', '4', '7', '7', '5', '8', '0', '8' + }; + CHECK(json::to_ubjson(j, true, true) == expected); + CHECK(json::from_ubjson(expected) == j); +} diff --git a/tests/src/unit-wstring.cpp b/tests/src/unit-wstring.cpp index a38df3aaa..6e0aff29e 100644 --- a/tests/src/unit-wstring.cpp +++ b/tests/src/unit-wstring.cpp @@ -70,6 +70,8 @@ TEST_CASE("wide strings") CHECK_THROWS_WITH_AS(_ = json::parse(std::wstring{L'"', static_cast(0xDC00), L'"'}), error_low_surrogate, json::parse_error&); // a high surrogate followed by a non-low-surrogate unit is invalid CHECK_THROWS_WITH_AS(_ = json::parse(std::wstring{L'"', static_cast(0xD800), L'a', L'"'}), error_high_surrogate, json::parse_error&); + // ... also when the unit is above the low surrogates + CHECK_THROWS_WITH_AS(_ = json::parse(std::wstring{L'"', static_cast(0xD800), static_cast(0xE000), L'"'}), error_high_surrogate, json::parse_error&); // a lone low surrogate must not swallow the following unit: pairing // it with any second unit would produce valid UTF-8, so the error // has to report an ill-formed byte at the surrogate's own position @@ -99,6 +101,8 @@ TEST_CASE("wide strings") CHECK_THROWS_WITH_AS(_ = json::parse(std::u16string{u'"', 0xDC00, u'"'}), "[json.exception.parse_error.101] parse error at line 1, column 2: syntax error while parsing value - invalid string: ill-formed UTF-8 byte; last read: '\"'", json::parse_error&); // a high surrogate followed by a non-low-surrogate unit is invalid CHECK_THROWS_WITH_AS(_ = json::parse(std::u16string{u'"', 0xD800, u'a', u'"'}), "[json.exception.parse_error.101] parse error at line 1, column 2: syntax error while parsing value - invalid string: ill-formed UTF-8 byte; last read: '\"'", json::parse_error&); + // ... also when the unit is above the low surrogates + CHECK_THROWS_WITH_AS(_ = json::parse(std::u16string{u'"', 0xD800, 0xE000, u'"'}), "[json.exception.parse_error.101] parse error at line 1, column 2: syntax error while parsing value - invalid string: ill-formed UTF-8 byte; last read: '\"'", json::parse_error&); // a lone low surrogate must not swallow the following unit: pairing // it with any second unit would produce valid UTF-8, so the error // has to report an ill-formed byte at the surrogate's own position