| // __ _____ _____ _____ |
| // __| | __| | | | JSON for Modern C++ (supporting code) |
| // | | |__ | | | | | | version 3.12.0 |
| // |_____|_____|_____|_|___| https://github.com/nlohmann/json |
| // |
| // SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me> |
| // SPDX-License-Identifier: MIT |
| |
| #include "doctest_compatibility.h" |
| |
| // capture whether JSON_DELETE_DEPRECATED_FUNCTIONS was enabled on the command |
| // line *before* including json.hpp, since the library #undefs it once the header |
| // has been fully processed (see include/nlohmann/detail/macro_unscope.hpp); the |
| // tests of deprecated functions are skipped if these functions are deleted |
| #if defined(JSON_DELETE_DEPRECATED_FUNCTIONS) && (JSON_DELETE_DEPRECATED_FUNCTIONS == 1) |
| #define JSON_TEST_DEPRECATED_FUNCTIONS_DELETED |
| #endif |
| |
| #include <nlohmann/json.hpp> |
| using nlohmann::json; |
| |
| #include <cstdint> |
| #include <fstream> |
| #include <limits> |
| #include <sstream> |
| #include <vector> |
| #include "make_test_data_available.hpp" |
| #include "round_trip_corpus.hpp" |
| #include "test_utils.hpp" |
| #include "sax_countdown.hpp" |
| using utils::SaxCountdown; |
| |
| namespace |
| { |
| // a binary container that reports a size beyond INT32_MAX without allocating |
| // that much memory, so the BSON length overflow can be tested cheaply |
| class huge_binary_t : public std::vector<std::uint8_t> |
| { |
| public: |
| using std::vector<std::uint8_t>::vector; |
| |
| size_type size() const noexcept // NOLINT(readability-convert-member-functions-to-static) |
| { |
| // one byte more than the BSON length field can represent |
| return static_cast<size_type>((std::numeric_limits<std::int32_t>::max)()) + 1; |
| } |
| }; |
| |
| using huge_binary_json = nlohmann::json::with_binary_t<huge_binary_t>; |
| |
| // a string type that can be made to report a size beyond INT32_MAX without |
| // allocating that much memory, so BSON length overflow can be tested for |
| // strings and (embedded) documents as well, following the same idea as |
| // huge_binary_t. |
| // |
| // Unlike huge_binary_t (which is only ever used as the BSON *value* type), |
| // this type doubles as basic_json's StringType and is therefore also used |
| // for *object keys* (e.g. "s" or "nested" below). Only the designated test |
| // value is meant to lie about its size - if every huge_string_t (including |
| // keys) reported a huge size, the running totals computed while walking the |
| // BSON document (see calc_bson_sizes in binary_writer.hpp) |
| // would need more than 32 bits, and on platforms where std::size_t is only |
| // 32 bits wide that arithmetic would silently wrap around, producing wrong |
| // (or even unguarded) lengths. The fake size is therefore opt-in via |
| // as_huge(), and plain strings - in particular object keys - keep reporting |
| // their real, small size. |
| class huge_string_t : public std::string |
| { |
| public: |
| using std::string::string; |
| // inheriting std::string's constructors does not inherit its default constructor |
| huge_string_t() = default; |
| huge_string_t(const std::string& s) : std::string(s) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) |
| |
| // returns a copy of @a s whose size() pretends to be huge |
| static huge_string_t as_huge(const std::string& s) |
| { |
| huge_string_t result(s); |
| result.pretend_huge = true; |
| return result; |
| } |
| |
| size_type size() const noexcept |
| { |
| if (pretend_huge) |
| { |
| // one byte more than the BSON length field can represent |
| return static_cast<size_type>((std::numeric_limits<std::int32_t>::max)()) + 1; |
| } |
| return std::string::size(); |
| } |
| |
| private: |
| bool pretend_huge = false; |
| }; |
| |
| using huge_string_json = nlohmann::json::with_string_t<huge_string_t>; |
| } // namespace |
| |
| TEST_CASE("BSON") |
| { |
| SECTION("individual values not supported") |
| { |
| SECTION("null") |
| { |
| json const j = nullptr; |
| CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.type_error.317] to serialize to BSON, top-level type must be object, but is null", json::type_error&); |
| } |
| |
| SECTION("boolean") |
| { |
| SECTION("true") |
| { |
| json const j = true; |
| CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.type_error.317] to serialize to BSON, top-level type must be object, but is boolean", json::type_error&); |
| } |
| |
| SECTION("false") |
| { |
| json const j = false; |
| CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.type_error.317] to serialize to BSON, top-level type must be object, but is boolean", json::type_error&); |
| } |
| } |
| |
| SECTION("number") |
| { |
| json const j = 42; |
| CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.type_error.317] to serialize to BSON, top-level type must be object, but is number", json::type_error&); |
| } |
| |
| SECTION("float") |
| { |
| json const j = 4.2; |
| CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.type_error.317] to serialize to BSON, top-level type must be object, but is number", json::type_error&); |
| } |
| |
| SECTION("string") |
| { |
| json const j = "not supported"; |
| CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.type_error.317] to serialize to BSON, top-level type must be object, but is string", json::type_error&); |
| } |
| |
| SECTION("array") |
| { |
| json const j = std::vector<int> {1, 2, 3, 4, 5, 6, 7}; |
| CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.type_error.317] to serialize to BSON, top-level type must be object, but is array", json::type_error&); |
| } |
| |
| SECTION("discarded") |
| { |
| json const j = json::value_t::discarded; |
| CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.type_error.317] to serialize to BSON, top-level type must be object, but is discarded", json::type_error&); |
| } |
| } |
| |
| SECTION("discarded values nested in a container cannot be serialized to BSON") |
| { |
| json const discarded = json::value_t::discarded; |
| |
| SECTION("as an object value") |
| { |
| json j; |
| j["a"] = 1; |
| j["b"] = discarded; |
| #if JSON_DIAGNOSTICS |
| CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.type_error.321] (/b) cannot serialize discarded value to BSON", json::type_error&); |
| #else |
| CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.type_error.321] cannot serialize discarded value to BSON", json::type_error&); |
| #endif |
| } |
| |
| SECTION("in an array that is an object value") |
| { |
| json j; |
| j["a"] = json::array({1, discarded, 2}); |
| #if JSON_DIAGNOSTICS |
| CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.type_error.321] (/a/1) cannot serialize discarded value to BSON", json::type_error&); |
| #else |
| CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.type_error.321] cannot serialize discarded value to BSON", json::type_error&); |
| #endif |
| } |
| |
| SECTION("nested deeper (array in object in object)") |
| { |
| json inner_array = {1, discarded}; |
| json middle_object; |
| middle_object["x"] = inner_array; |
| json j; |
| j["outer"] = middle_object; |
| #if JSON_DIAGNOSTICS |
| CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.type_error.321] (/outer/x/1) cannot serialize discarded value to BSON", json::type_error&); |
| #else |
| CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.type_error.321] cannot serialize discarded value to BSON", json::type_error&); |
| #endif |
| } |
| } |
| |
| SECTION("keys containing code-point U+0000 cannot be serialized to BSON") |
| { |
| json const j = |
| { |
| { std::string("en\0try", 6), true } |
| }; |
| #if JSON_DIAGNOSTICS |
| CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.out_of_range.409] (/en) BSON key cannot contain code point U+0000 (at byte 2)", json::out_of_range&); |
| #else |
| CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.out_of_range.409] BSON key cannot contain code point U+0000 (at byte 2)", json::out_of_range&); |
| #endif |
| } |
| |
| SECTION("ill-formed UTF-8 (see #5529, #5651)") |
| { |
| // a BSON document {"s": "\xC0\xAE"} (0xC0 0xAE is an overlong |
| // encoding of '.'); the BSON spec does not require a decoder to |
| // reject ill-formed UTF-8 in a string value, so the reader hands the |
| // bytes back unchanged |
| const std::vector<uint8_t> v = |
| { |
| 0x0F, 0x00, 0x00, 0x00, // document length |
| 0x02, 's', 0x00, // type 0x02 (string), key "s" |
| 0x03, 0x00, 0x00, 0x00, // string length (including null) |
| 0xc0, 0xae, 0x00, // string content and its null terminator |
| 0x00 // document terminator |
| }; |
| json j; |
| CHECK_NOTHROW(j = json::from_bson(v)); |
| REQUIRE(j.is_object()); |
| REQUIRE(j.contains("s")); |
| CHECK(j["s"].get_ref<const json::string_t&>() == std::string("\xc0\xae")); |
| // dump() still requires valid UTF-8 and throws for such a value |
| CHECK_THROWS_AS(utils::ignore_return_value(j.dump()), json::type_error&); |
| // to_bson() writes the bytes back unchanged, as before 3.13.0, |
| // unless JSON_STRICT_BINARY_UTF8 is enabled (see unit-binary_utf8_strict.cpp) |
| CHECK(json::from_bson(json::to_bson(j)) == j); |
| |
| CHECK(json::from_bson(json::to_bson(json{{"s", "\xFF"}})) == json{{"s", "\xFF"}}); |
| // a truncated multi-byte sequence |
| CHECK(json::from_bson(json::to_bson(json{{"s", "\xC3"}})) == json{{"s", "\xC3"}}); |
| // an encoded surrogate half (U+D800) |
| CHECK(json::from_bson(json::to_bson(json{{"s", "\xED\xA0\x80"}})) == json{{"s", "\xED\xA0\x80"}}); |
| // an overlong encoding of '.' |
| CHECK(json::from_bson(json::to_bson(json{{"s", "\xC0\xAF"}})) == json{{"s", "\xC0\xAF"}}); |
| |
| // an object key with ill-formed UTF-8 is kept as well |
| CHECK(json::from_bson(json::to_bson(json{{"\xFF", 1}})) == json{{"\xFF", 1}}); |
| } |
| |
| SECTION("lengths exceeding INT32_MAX cannot be serialized to BSON") |
| { |
| // out_of_range.412 is thrown from a single shared helper |
| // (to_bson_length) that guards the BSON length fields of binary |
| // values, strings, and (embedded) documents alike |
| SECTION("binary") |
| { |
| huge_binary_json j; |
| j["b"] = huge_binary_json::binary(huge_binary_t{}); |
| |
| CHECK_THROWS_WITH_AS(huge_binary_json::to_bson(j), "[json.exception.out_of_range.412] BSON length 2147483661 exceeds maximum of 2147483647", huge_binary_json::out_of_range&); |
| } |
| |
| SECTION("string") |
| { |
| huge_string_json j; |
| j["s"] = huge_string_t::as_huge("value"); |
| |
| CHECK_THROWS_WITH_AS(huge_string_json::to_bson(j), "[json.exception.out_of_range.412] BSON length 2147483661 exceeds maximum of 2147483647", huge_string_json::out_of_range&); |
| } |
| |
| SECTION("document") |
| { |
| // an oversized string nested one level deep makes the |
| // *embedded* document's own length exceed INT32_MAX as well |
| huge_string_json nested; |
| nested["s"] = huge_string_t::as_huge("value"); |
| huge_string_json j; |
| j["nested"] = nested; |
| |
| CHECK_THROWS_WITH_AS(huge_string_json::to_bson(j), "[json.exception.out_of_range.412] BSON length 2147483674 exceeds maximum of 2147483647", huge_string_json::out_of_range&); |
| } |
| } |
| |
| SECTION("string length must be at least 1") |
| { |
| // from https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=11175 |
| std::vector<std::uint8_t> const v = |
| { |
| 0x20, 0x20, 0x20, 0x20, |
| 0x02, |
| 0x00, |
| 0x00, 0x00, 0x00, 0x80 |
| }; |
| json _; |
| CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 10: syntax error while parsing BSON string: string length must be at least 1, is -2147483648", json::parse_error&); |
| } |
| |
| SECTION("objects") |
| { |
| SECTION("empty object") |
| { |
| json const j = json::object(); |
| std::vector<std::uint8_t> const expected = |
| { |
| 0x05, 0x00, 0x00, 0x00, // size (little endian) |
| // no entries |
| 0x00 // end marker |
| }; |
| |
| const auto result = json::to_bson(j); |
| CHECK(result == expected); |
| |
| // roundtrip |
| CHECK(json::from_bson(result) == j); |
| CHECK(json::from_bson(result, true, false) == j); |
| } |
| |
| SECTION("non-empty object with bool") |
| { |
| json const j = |
| { |
| { "entry", true } |
| }; |
| |
| std::vector<std::uint8_t> const expected = |
| { |
| 0x0D, 0x00, 0x00, 0x00, // size (little endian) |
| 0x08, // entry: boolean |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| 0x01, // value = true |
| 0x00 // end marker |
| }; |
| |
| const auto result = json::to_bson(j); |
| CHECK(result == expected); |
| |
| // roundtrip |
| CHECK(json::from_bson(result) == j); |
| CHECK(json::from_bson(result, true, false) == j); |
| } |
| |
| SECTION("non-empty object with bool") |
| { |
| json const j = |
| { |
| { "entry", false } |
| }; |
| |
| std::vector<std::uint8_t> const expected = |
| { |
| 0x0D, 0x00, 0x00, 0x00, // size (little endian) |
| 0x08, // entry: boolean |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| 0x00, // value = false |
| 0x00 // end marker |
| }; |
| |
| const auto result = json::to_bson(j); |
| CHECK(result == expected); |
| |
| // roundtrip |
| CHECK(json::from_bson(result) == j); |
| CHECK(json::from_bson(result, true, false) == j); |
| } |
| |
| SECTION("non-empty object with bool from a non-0/1 byte (lenient parsing)") |
| { |
| // documented lenient behavior (see gh-5333): any non-zero byte |
| // is accepted as `true`, not just 0x01 |
| std::vector<std::uint8_t> const input = |
| { |
| 0x0D, 0x00, 0x00, 0x00, // size (little endian) |
| 0x08, // entry: boolean |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| 0x02, // value = 0x02 (neither 0x00 nor 0x01) |
| 0x00 // end marker |
| }; |
| |
| const json expected = { { "entry", true } }; |
| CHECK(json::from_bson(input) == expected); |
| } |
| |
| SECTION("non-empty object with double") |
| { |
| json const j = |
| { |
| { "entry", 4.2 } |
| }; |
| |
| std::vector<std::uint8_t> const expected = |
| { |
| 0x14, 0x00, 0x00, 0x00, // size (little endian) |
| 0x01, /// entry: double |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| 0xcd, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0x10, 0x40, |
| 0x00 // end marker |
| }; |
| |
| const auto result = json::to_bson(j); |
| CHECK(result == expected); |
| |
| // roundtrip |
| CHECK(json::from_bson(result) == j); |
| CHECK(json::from_bson(result, true, false) == j); |
| } |
| |
| SECTION("non-empty object with string") |
| { |
| json const j = |
| { |
| { "entry", "bsonstr" } |
| }; |
| |
| std::vector<std::uint8_t> const expected = |
| { |
| 0x18, 0x00, 0x00, 0x00, // size (little endian) |
| 0x02, /// entry: string (UTF-8) |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| 0x08, 0x00, 0x00, 0x00, 'b', 's', 'o', 'n', 's', 't', 'r', '\x00', |
| 0x00 // end marker |
| }; |
| |
| const auto result = json::to_bson(j); |
| CHECK(result == expected); |
| |
| // roundtrip |
| CHECK(json::from_bson(result) == j); |
| CHECK(json::from_bson(result, true, false) == j); |
| } |
| |
| SECTION("non-empty object with null member") |
| { |
| json const j = |
| { |
| { "entry", nullptr } |
| }; |
| |
| std::vector<std::uint8_t> const expected = |
| { |
| 0x0C, 0x00, 0x00, 0x00, // size (little endian) |
| 0x0A, /// entry: null |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| 0x00 // end marker |
| }; |
| |
| const auto result = json::to_bson(j); |
| CHECK(result == expected); |
| |
| // roundtrip |
| CHECK(json::from_bson(result) == j); |
| CHECK(json::from_bson(result, true, false) == j); |
| } |
| |
| SECTION("non-empty object with integer (32-bit) member") |
| { |
| json const j = |
| { |
| { "entry", std::int32_t{0x12345678} } |
| }; |
| |
| std::vector<std::uint8_t> const expected = |
| { |
| 0x10, 0x00, 0x00, 0x00, // size (little endian) |
| 0x10, /// entry: int32 |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| 0x78, 0x56, 0x34, 0x12, |
| 0x00 // end marker |
| }; |
| |
| const auto result = json::to_bson(j); |
| CHECK(result == expected); |
| |
| // roundtrip |
| CHECK(json::from_bson(result) == j); |
| CHECK(json::from_bson(result, true, false) == j); |
| } |
| |
| SECTION("non-empty object with integer (64-bit) member") |
| { |
| json const j = |
| { |
| { "entry", std::int64_t{0x1234567804030201} } |
| }; |
| |
| std::vector<std::uint8_t> const expected = |
| { |
| 0x14, 0x00, 0x00, 0x00, // size (little endian) |
| 0x12, /// entry: int64 |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| 0x01, 0x02, 0x03, 0x04, 0x78, 0x56, 0x34, 0x12, |
| 0x00 // end marker |
| }; |
| |
| const auto result = json::to_bson(j); |
| CHECK(result == expected); |
| |
| // roundtrip |
| CHECK(json::from_bson(result) == j); |
| CHECK(json::from_bson(result, true, false) == j); |
| } |
| |
| SECTION("non-empty object with negative integer (32-bit) member") |
| { |
| json const j = |
| { |
| { "entry", std::int32_t{-1} } |
| }; |
| |
| std::vector<std::uint8_t> const expected = |
| { |
| 0x10, 0x00, 0x00, 0x00, // size (little endian) |
| 0x10, /// entry: int32 |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| 0xFF, 0xFF, 0xFF, 0xFF, |
| 0x00 // end marker |
| }; |
| |
| const auto result = json::to_bson(j); |
| CHECK(result == expected); |
| |
| // roundtrip |
| CHECK(json::from_bson(result) == j); |
| CHECK(json::from_bson(result, true, false) == j); |
| } |
| |
| SECTION("non-empty object with negative integer (64-bit) member") |
| { |
| json const j = |
| { |
| { "entry", std::int64_t{-1} } |
| }; |
| |
| std::vector<std::uint8_t> const expected = |
| { |
| 0x10, 0x00, 0x00, 0x00, // size (little endian) |
| 0x10, /// entry: int32 |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| 0xFF, 0xFF, 0xFF, 0xFF, |
| 0x00 // end marker |
| }; |
| |
| const auto result = json::to_bson(j); |
| CHECK(result == expected); |
| |
| // roundtrip |
| CHECK(json::from_bson(result) == j); |
| CHECK(json::from_bson(result, true, false) == j); |
| } |
| |
| SECTION("non-empty object with unsigned integer (64-bit) member") |
| { |
| json const j = |
| { |
| { "entry", std::uint64_t{0x1234567804030201} } |
| }; |
| |
| std::vector<std::uint8_t> const expected = |
| { |
| 0x14, 0x00, 0x00, 0x00, // size (little endian) |
| 0x12, /// entry: int64 |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| 0x01, 0x02, 0x03, 0x04, 0x78, 0x56, 0x34, 0x12, |
| 0x00 // end marker |
| }; |
| |
| const auto result = json::to_bson(j); |
| CHECK(result == expected); |
| |
| // roundtrip |
| CHECK(json::from_bson(result) == j); |
| CHECK(json::from_bson(result, true, false) == j); |
| } |
| |
| SECTION("non-empty object with small unsigned integer member") |
| { |
| json const j = |
| { |
| { "entry", std::uint64_t{0x42} } |
| }; |
| |
| std::vector<std::uint8_t> const expected = |
| { |
| 0x10, 0x00, 0x00, 0x00, // size (little endian) |
| 0x10, /// entry: int32 |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| 0x42, 0x00, 0x00, 0x00, |
| 0x00 // end marker |
| }; |
| |
| const auto result = json::to_bson(j); |
| CHECK(result == expected); |
| |
| // roundtrip |
| CHECK(json::from_bson(result) == j); |
| CHECK(json::from_bson(result, true, false) == j); |
| } |
| |
| SECTION("non-empty object with object member") |
| { |
| json const j = |
| { |
| { "entry", json::object() } |
| }; |
| |
| std::vector<std::uint8_t> const expected = |
| { |
| 0x11, 0x00, 0x00, 0x00, // size (little endian) |
| 0x03, /// entry: embedded document |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| |
| 0x05, 0x00, 0x00, 0x00, // size (little endian) |
| // no entries |
| 0x00, // end marker (embedded document) |
| |
| 0x00 // end marker |
| }; |
| |
| const auto result = json::to_bson(j); |
| CHECK(result == expected); |
| |
| // roundtrip |
| CHECK(json::from_bson(result) == j); |
| CHECK(json::from_bson(result, true, false) == j); |
| } |
| |
| SECTION("non-empty object with array member") |
| { |
| json const j = |
| { |
| { "entry", json::array() } |
| }; |
| |
| std::vector<std::uint8_t> const expected = |
| { |
| 0x11, 0x00, 0x00, 0x00, // size (little endian) |
| 0x04, /// entry: embedded document |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| |
| 0x05, 0x00, 0x00, 0x00, // size (little endian) |
| // no entries |
| 0x00, // end marker (embedded document) |
| |
| 0x00 // end marker |
| }; |
| |
| const auto result = json::to_bson(j); |
| CHECK(result == expected); |
| |
| // roundtrip |
| CHECK(json::from_bson(result) == j); |
| CHECK(json::from_bson(result, true, false) == j); |
| } |
| |
| SECTION("non-empty object with non-empty array member") |
| { |
| json const j = |
| { |
| { "entry", json::array({1, 2, 3, 4, 5, 6, 7, 8}) } |
| }; |
| |
| std::vector<std::uint8_t> const expected = |
| { |
| 0x49, 0x00, 0x00, 0x00, // size (little endian) |
| 0x04, /// entry: embedded document |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| |
| 0x3D, 0x00, 0x00, 0x00, // size (little endian) |
| 0x10, '0', 0x00, 0x01, 0x00, 0x00, 0x00, |
| 0x10, '1', 0x00, 0x02, 0x00, 0x00, 0x00, |
| 0x10, '2', 0x00, 0x03, 0x00, 0x00, 0x00, |
| 0x10, '3', 0x00, 0x04, 0x00, 0x00, 0x00, |
| 0x10, '4', 0x00, 0x05, 0x00, 0x00, 0x00, |
| 0x10, '5', 0x00, 0x06, 0x00, 0x00, 0x00, |
| 0x10, '6', 0x00, 0x07, 0x00, 0x00, 0x00, |
| 0x10, '7', 0x00, 0x08, 0x00, 0x00, 0x00, |
| 0x00, // end marker (embedded document) |
| |
| 0x00 // end marker |
| }; |
| |
| const auto result = json::to_bson(j); |
| CHECK(result == expected); |
| |
| // roundtrip |
| CHECK(json::from_bson(result) == j); |
| CHECK(json::from_bson(result, true, false) == j); |
| } |
| |
| SECTION("array elements with non-conforming keys (lenient parsing)") |
| { |
| // documented lenient behavior (see gh-5333): BSON array element |
| // keys are not checked against the required decimal sequence |
| // "0", "1", "2", ... - elements are taken in encoded order |
| std::vector<std::uint8_t> const input = |
| { |
| 0x26, 0x00, 0x00, 0x00, // size (little endian) |
| 0x04, 'e', 'n', 't', 'r', 'y', '\x00', // entry: embedded array |
| |
| 0x1A, 0x00, 0x00, 0x00, // size (little endian) |
| 0x10, '5', 0x00, 0x0A, 0x00, 0x00, 0x00, // key "5" (bogus) -> 10 |
| 0x10, 'x', 0x00, 0x14, 0x00, 0x00, 0x00, // key "x" (non-numeric) -> 20 |
| 0x10, '1', 0x00, 0x1E, 0x00, 0x00, 0x00, // key "1" (out of order) -> 30 |
| 0x00, // end marker (embedded array) |
| |
| 0x00 // end marker |
| }; |
| |
| const json expected = { { "entry", json::array({10, 20, 30}) } }; |
| CHECK(json::from_bson(input) == expected); |
| } |
| |
| SECTION("non-empty object with binary member") |
| { |
| const size_t N = 10; |
| const auto s = std::vector<std::uint8_t>(N, 'x'); |
| json const j = |
| { |
| { "entry", json::binary(s, 0) } |
| }; |
| |
| std::vector<std::uint8_t> const expected = |
| { |
| 0x1B, 0x00, 0x00, 0x00, // size (little endian) |
| 0x05, // entry: binary |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| |
| 0x0A, 0x00, 0x00, 0x00, // size of binary (little endian) |
| 0x00, // Generic binary subtype |
| 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, |
| |
| 0x00 // end marker |
| }; |
| |
| const auto result = json::to_bson(j); |
| CHECK(result == expected); |
| |
| // roundtrip |
| CHECK(json::from_bson(result) == j); |
| CHECK(json::from_bson(result, true, false) == j); |
| } |
| |
| SECTION("non-empty object with binary member without subtype") |
| { |
| const size_t N = 10; |
| const auto s = std::vector<std::uint8_t>(N, 'x'); |
| json const j = |
| { |
| { "entry", json::binary(s) } |
| }; |
| |
| CHECK(!j.at("entry").get_binary().has_subtype()); |
| |
| std::vector<std::uint8_t> const expected = |
| { |
| 0x1B, 0x00, 0x00, 0x00, // size (little endian) |
| 0x05, // entry: binary |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| |
| 0x0A, 0x00, 0x00, 0x00, // size of binary (little endian) |
| 0x00, // Generic binary subtype |
| 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, |
| |
| 0x00 // end marker |
| }; |
| |
| const auto result = json::to_bson(j); |
| CHECK(result == expected); |
| |
| // roundtrip adds the generic binary subtype |
| const auto roundtrip = json::from_bson(result); |
| CHECK(roundtrip != j); |
| CHECK(roundtrip.at("entry").get_binary().has_subtype()); |
| CHECK(roundtrip.at("entry").get_binary().subtype() == 0); |
| CHECK(json::from_bson(result, true, false) == roundtrip); |
| } |
| |
| SECTION("non-empty object with binary member with subtype") |
| { |
| // an MD5 hash |
| const std::vector<std::uint8_t> md5hash = {0xd7, 0x7e, 0x27, 0x54, 0xbe, 0x12, 0x37, 0xfe, 0xd6, 0x0c, 0x33, 0x98, 0x30, 0x3b, 0x8d, 0xc4}; |
| json const j = |
| { |
| { "entry", json::binary(md5hash, 5) } |
| }; |
| |
| std::vector<std::uint8_t> const expected = |
| { |
| 0x21, 0x00, 0x00, 0x00, // size (little endian) |
| 0x05, // entry: binary |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| |
| 0x10, 0x00, 0x00, 0x00, // size of binary (little endian) |
| 0x05, // MD5 binary subtype |
| 0xd7, 0x7e, 0x27, 0x54, 0xbe, 0x12, 0x37, 0xfe, 0xd6, 0x0c, 0x33, 0x98, 0x30, 0x3b, 0x8d, 0xc4, |
| |
| 0x00 // end marker |
| }; |
| |
| const auto result = json::to_bson(j); |
| CHECK(result == expected); |
| |
| // roundtrip |
| CHECK(json::from_bson(result) == j); |
| CHECK(json::from_bson(result, true, false) == j); |
| } |
| |
| SECTION("binary member with subtype 0x02 (old binary) keeps its inner length prefix (lenient parsing)") |
| { |
| // documented lenient behavior (see gh-5333): the payload for |
| // binary subtype 0x02 ("old binary") is returned as-is, |
| // including its own inner 4-byte length prefix; it is not |
| // stripped or reinterpreted |
| std::vector<std::uint8_t> const input = |
| { |
| 0x17, 0x00, 0x00, 0x00, // size (little endian) |
| 0x05, 'e', 'n', 't', 'r', 'y', '\x00', // entry: binary |
| |
| 0x06, 0x00, 0x00, 0x00, // size of binary (little endian) |
| 0x02, // "old binary" subtype |
| 0x02, 0x00, 0x00, 0x00, // inner length prefix (part of the old-binary payload) |
| 0x68, 0x69, // payload ('h', 'i') |
| |
| 0x00 // end marker |
| }; |
| |
| // the inner length prefix is part of the (unmodified) payload |
| const std::vector<std::uint8_t> expected_payload = {0x02, 0x00, 0x00, 0x00, 0x68, 0x69}; |
| const json expected = { { "entry", json::binary(expected_payload, 0x02) } }; |
| CHECK(json::from_bson(input) == expected); |
| } |
| |
| SECTION("Some more complex document") |
| { |
| json const j = |
| { |
| {"double", 42.5}, |
| {"entry", 4.2}, |
| {"number", 12345}, |
| {"object", {{ "string", "value" }}} |
| }; |
| |
| std::vector<std::uint8_t> const expected = |
| { |
| /*size */ 0x4f, 0x00, 0x00, 0x00, |
| /*entry*/ 0x01, 'd', 'o', 'u', 'b', 'l', 'e', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x45, 0x40, |
| /*entry*/ 0x01, 'e', 'n', 't', 'r', 'y', 0x00, 0xcd, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0x10, 0x40, |
| /*entry*/ 0x10, 'n', 'u', 'm', 'b', 'e', 'r', 0x00, 0x39, 0x30, 0x00, 0x00, |
| /*entry*/ 0x03, 'o', 'b', 'j', 'e', 'c', 't', 0x00, |
| /*entry: obj-size */ 0x17, 0x00, 0x00, 0x00, |
| /*entry: obj-entry*/0x02, 's', 't', 'r', 'i', 'n', 'g', 0x00, 0x06, 0x00, 0x00, 0x00, 'v', 'a', 'l', 'u', 'e', 0, |
| /*entry: obj-term.*/0x00, |
| /*obj-term*/ 0x00 |
| }; |
| |
| const auto result = json::to_bson(j); |
| CHECK(result == expected); |
| |
| // roundtrip |
| CHECK(json::from_bson(result) == j); |
| CHECK(json::from_bson(result, true, false) == j); |
| } |
| } |
| |
| SECTION("Examples from https://bsonspec.org/faq.html") |
| { |
| SECTION("Example 1") |
| { |
| std::vector<std::uint8_t> input = {0x16, 0x00, 0x00, 0x00, 0x02, 'h', 'e', 'l', 'l', 'o', 0x00, 0x06, 0x00, 0x00, 0x00, 'w', 'o', 'r', 'l', 'd', 0x00, 0x00}; |
| const json parsed = json::from_bson(input); |
| const json expected = {{"hello", "world"}}; |
| CHECK(parsed == expected); |
| const auto dumped = json::to_bson(parsed); |
| CHECK(dumped == input); |
| CHECK(json::from_bson(dumped) == expected); |
| } |
| |
| SECTION("Example 2") |
| { |
| std::vector<std::uint8_t> input = {0x31, 0x00, 0x00, 0x00, 0x04, 'B', 'S', 'O', 'N', 0x00, 0x26, 0x00, 0x00, 0x00, 0x02, 0x30, 0x00, 0x08, 0x00, 0x00, 0x00, 'a', 'w', 'e', 's', 'o', 'm', 'e', 0x00, 0x01, 0x31, 0x00, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x14, 0x40, 0x10, 0x32, 0x00, 0xc2, 0x07, 0x00, 0x00, 0x00, 0x00}; |
| const json parsed = json::from_bson(input); |
| const json expected = {{"BSON", {"awesome", 5.05, 1986}}}; |
| CHECK(parsed == expected); |
| const auto dumped = json::to_bson(parsed); |
| CHECK(dumped == input); |
| CHECK(json::from_bson(dumped) == expected); |
| } |
| } |
| } |
| |
| TEST_CASE("regression test - BSON binary subtype rejects a value that doesn't fit a single byte") |
| { |
| json const doc255 = {{"b", json::binary({1, 2}, 255)}}; |
| CHECK(json::from_bson(json::to_bson(doc255))["b"].get_binary().subtype() == 255); |
| |
| CHECK_THROWS_AS(json::to_bson(json{{"b", json::binary({1, 2}, 256)}}), json::out_of_range); |
| #if JSON_DIAGNOSTICS |
| CHECK_THROWS_WITH_AS(json::to_bson(json {{"b", json::binary({1, 2}, 300)}}), "[json.exception.out_of_range.415] (/b) subtype 300 is too large for the BSON binary subtype (max 255)", json::out_of_range); |
| #else |
| CHECK_THROWS_WITH_AS(json::to_bson(json {{"b", json::binary({1, 2}, 300)}}), "[json.exception.out_of_range.415] subtype 300 is too large for the BSON binary subtype (max 255)", json::out_of_range); |
| #endif |
| } |
| |
| TEST_CASE("BSON input/output_adapters") |
| { |
| const json json_representation = |
| { |
| {"double", 42.5}, |
| {"entry", 4.2}, |
| {"number", 12345}, |
| {"object", {{ "string", "value" }}} |
| }; |
| |
| const std::vector<std::uint8_t> bson_representation = |
| { |
| /*size */ 0x4f, 0x00, 0x00, 0x00, |
| /*entry*/ 0x01, 'd', 'o', 'u', 'b', 'l', 'e', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x45, 0x40, |
| /*entry*/ 0x01, 'e', 'n', 't', 'r', 'y', 0x00, 0xcd, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0x10, 0x40, |
| /*entry*/ 0x10, 'n', 'u', 'm', 'b', 'e', 'r', 0x00, 0x39, 0x30, 0x00, 0x00, |
| /*entry*/ 0x03, 'o', 'b', 'j', 'e', 'c', 't', 0x00, |
| /*entry: obj-size */ 0x17, 0x00, 0x00, 0x00, |
| /*entry: obj-entry*/0x02, 's', 't', 'r', 'i', 'n', 'g', 0x00, 0x06, 0x00, 0x00, 0x00, 'v', 'a', 'l', 'u', 'e', 0, |
| /*entry: obj-term.*/0x00, |
| /*obj-term*/ 0x00 |
| }; |
| |
| json j2; |
| CHECK_NOTHROW(j2 = json::from_bson(bson_representation)); |
| |
| // compare parsed JSON values |
| CHECK(json_representation == j2); |
| |
| SECTION("roundtrips") |
| { |
| SECTION("std::ostringstream") |
| { |
| std::basic_ostringstream<char> ss; |
| json::to_bson(json_representation, ss); |
| const json j3 = json::from_bson(ss.str()); |
| CHECK(json_representation == j3); |
| } |
| |
| SECTION("std::string") |
| { |
| std::string s; |
| json::to_bson(json_representation, s); |
| const json j3 = json::from_bson(s); |
| CHECK(json_representation == j3); |
| } |
| |
| SECTION("std::vector") |
| { |
| std::vector<std::uint8_t> v; |
| json::to_bson(json_representation, v); |
| const json j3 = json::from_bson(v); |
| CHECK(json_representation == j3); |
| } |
| } |
| } |
| |
| |
| TEST_CASE("Incomplete BSON Input") |
| { |
| SECTION("Incomplete BSON Input 1") |
| { |
| std::vector<std::uint8_t> const incomplete_bson = |
| { |
| 0x0D, 0x00, 0x00, 0x00, // size (little endian) |
| 0x08, // entry: boolean |
| 'e', 'n', 't' // unexpected EOF |
| }; |
| |
| json _; |
| CHECK_THROWS_WITH_AS(_ = json::from_bson(incomplete_bson), "[json.exception.parse_error.110] parse error at byte 9: syntax error while parsing BSON cstring: unexpected end of input", json::parse_error&); |
| |
| CHECK(json::from_bson(incomplete_bson, true, false).is_discarded()); |
| |
| SaxCountdown scp(0); |
| CHECK(!json::sax_parse(incomplete_bson, &scp, json::input_format_t::bson)); |
| } |
| |
| SECTION("Incomplete BSON Input 2") |
| { |
| std::vector<std::uint8_t> const incomplete_bson = |
| { |
| 0x0D, 0x00, 0x00, 0x00, // size (little endian) |
| 0x08, // entry: boolean, unexpected EOF |
| }; |
| |
| json _; |
| CHECK_THROWS_WITH_AS(_ = json::from_bson(incomplete_bson), "[json.exception.parse_error.110] parse error at byte 6: syntax error while parsing BSON cstring: unexpected end of input", json::parse_error&); |
| CHECK(json::from_bson(incomplete_bson, true, false).is_discarded()); |
| |
| SaxCountdown scp(0); |
| CHECK(!json::sax_parse(incomplete_bson, &scp, json::input_format_t::bson)); |
| } |
| |
| SECTION("Incomplete BSON Input 3") |
| { |
| std::vector<std::uint8_t> const incomplete_bson = |
| { |
| 0x41, 0x00, 0x00, 0x00, // size (little endian) |
| 0x04, /// entry: embedded document |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| |
| 0x35, 0x00, 0x00, 0x00, // size (little endian) |
| 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, |
| 0x10, 0x00, 0x02, 0x00, 0x00, 0x00 |
| // missing input data... |
| }; |
| |
| json _; |
| CHECK_THROWS_WITH_AS(_ = json::from_bson(incomplete_bson), "[json.exception.parse_error.110] parse error at byte 28: syntax error while parsing BSON element list: unexpected end of input", json::parse_error&); |
| CHECK(json::from_bson(incomplete_bson, true, false).is_discarded()); |
| |
| SaxCountdown scp(1); |
| CHECK(!json::sax_parse(incomplete_bson, &scp, json::input_format_t::bson)); |
| } |
| |
| SECTION("Incomplete BSON Input 4") |
| { |
| std::vector<std::uint8_t> const incomplete_bson = |
| { |
| 0x0D, 0x00, // size (incomplete), unexpected EOF |
| }; |
| |
| json _; |
| CHECK_THROWS_WITH_AS(_ = json::from_bson(incomplete_bson), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing BSON number: unexpected end of input", json::parse_error&); |
| CHECK(json::from_bson(incomplete_bson, true, false).is_discarded()); |
| |
| SaxCountdown scp(0); |
| CHECK(!json::sax_parse(incomplete_bson, &scp, json::input_format_t::bson)); |
| } |
| |
| SECTION("Incomplete BSON Input 5") |
| { |
| std::vector<std::uint8_t> const incomplete_bson = |
| { |
| 0x09, 0x00, 0x00, 0x00, // size (little endian) |
| 0x08, // entry: boolean |
| 'b', '\x00' // key, unexpected EOF before the value |
| }; |
| |
| json _; |
| CHECK_THROWS_WITH_AS(_ = json::from_bson(incomplete_bson), "[json.exception.parse_error.110] parse error at byte 8: syntax error while parsing BSON number: unexpected end of input", json::parse_error&); |
| CHECK(json::from_bson(incomplete_bson, true, false).is_discarded()); |
| |
| SaxCountdown scp(0); |
| CHECK(!json::sax_parse(incomplete_bson, &scp, json::input_format_t::bson)); |
| } |
| |
| SECTION("Incomplete BSON Input 6") |
| { |
| std::vector<std::uint8_t> const incomplete_bson = |
| { |
| 0x0F, 0x00, 0x00, 0x00, // size (little endian) |
| 0x05, // entry: binary |
| 'b', '\x00', // key |
| 0x00, 0x00, 0x00, 0x00 // length, unexpected EOF before the subtype |
| }; |
| |
| json _; |
| CHECK_THROWS_WITH_AS(_ = json::from_bson(incomplete_bson), "[json.exception.parse_error.110] parse error at byte 12: syntax error while parsing BSON number: unexpected end of input", json::parse_error&); |
| CHECK(json::from_bson(incomplete_bson, true, false).is_discarded()); |
| |
| SaxCountdown scp(0); |
| CHECK(!json::sax_parse(incomplete_bson, &scp, json::input_format_t::bson)); |
| } |
| |
| SECTION("Improve coverage") |
| { |
| SECTION("key") |
| { |
| json const j = {{"key", "value"}}; |
| auto bson_vec = json::to_bson(j); |
| SaxCountdown scp(2); |
| CHECK(!json::sax_parse(bson_vec, &scp, json::input_format_t::bson)); |
| } |
| |
| SECTION("array") |
| { |
| json const j = |
| { |
| { "entry", json::array() } |
| }; |
| auto bson_vec = json::to_bson(j); |
| SaxCountdown scp(2); |
| CHECK(!json::sax_parse(bson_vec, &scp, json::input_format_t::bson)); |
| } |
| } |
| } |
| |
| // 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<std::uint8_t> 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<std::uint8_t> input(bson.begin(), bson.begin() + static_cast<std::ptrdiff_t>(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 |
| std::vector<std::uint8_t> const input = |
| { |
| 0x21, 0x00, 0x00, 0x00, // size (little endian) |
| 0x05, // entry: binary |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| |
| 0xFF, 0xFF, 0xFF, 0xFF, // size of binary (little endian) |
| 0x05, // MD5 binary subtype |
| 0xd7, 0x7e, 0x27, 0x54, 0xbe, 0x12, 0x37, 0xfe, 0xd6, 0x0c, 0x33, 0x98, 0x30, 0x3b, 0x8d, 0xc4, |
| |
| 0x00 // end marker |
| }; |
| json _; |
| CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 15: syntax error while parsing BSON binary: byte array length cannot be negative, is -1", json::parse_error); |
| } |
| |
| TEST_CASE("Unsupported BSON input") |
| { |
| std::vector<std::uint8_t> const bson = |
| { |
| 0x0C, 0x00, 0x00, 0x00, // size (little endian) |
| 0xFF, // entry type: Min key (not supported yet) |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| 0x00 // end marker |
| }; |
| |
| json _; |
| CHECK_THROWS_WITH_AS(_ = json::from_bson(bson), "[json.exception.parse_error.114] parse error at byte 5: Unsupported BSON record type 0xFF", json::parse_error&); |
| CHECK(json::from_bson(bson, true, false).is_discarded()); |
| |
| SaxCountdown scp(0); |
| CHECK(!json::sax_parse(bson, &scp, json::input_format_t::bson)); |
| } |
| |
| TEST_CASE("BSON document size mismatch") |
| { |
| json _; |
| |
| SECTION("top-level document declaring more bytes than it contains") |
| { |
| // empty object, but the length prefix claims 6 bytes instead of 5 |
| std::vector<std::uint8_t> const input = {0x06, 0x00, 0x00, 0x00, 0x00}; |
| CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 5: syntax error while parsing BSON document: document size 6 does not match the number of bytes read (5)", json::parse_error&); |
| CHECK(json::from_bson(input, true, false).is_discarded()); |
| } |
| |
| SECTION("top-level document with a negative size") |
| { |
| std::vector<std::uint8_t> const input = {0xFF, 0xFF, 0xFF, 0xFF, 0x00}; |
| CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 5: syntax error while parsing BSON document: document size -1 does not match the number of bytes read (5)", json::parse_error&); |
| CHECK(json::from_bson(input, true, false).is_discarded()); |
| } |
| |
| SECTION("embedded document whose size disagrees with its terminator") |
| { |
| // the embedded document "d" declares 0x7FFFFFFF bytes but its 0x00 |
| // terminator falls right after {"a":null}; the length prefix would |
| // otherwise let the following "h" element be read as a member of the |
| // enclosing document instead of "d" |
| std::vector<std::uint8_t> const input = |
| { |
| 0x00, 0x00, 0x00, 0x00, // outer size |
| 0x03, 'd', 0x00, // entry: embedded document "d" |
| 0xFF, 0xFF, 0xFF, 0x7F, // embedded size 0x7FFFFFFF |
| 0x0A, 'a', 0x00, // entry: null "a" |
| 0x00, // embedded end marker |
| 0x08, 'h', 0x00, 0x01, // entry: bool "h" = true |
| 0x00 // outer end marker |
| }; |
| CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 15: syntax error while parsing BSON document: document size 2147483647 does not match the number of bytes read (8)", json::parse_error&); |
| CHECK(json::from_bson(input, true, false).is_discarded()); |
| } |
| |
| SECTION("embedded array whose size disagrees with its terminator") |
| { |
| // array [42] is 12 bytes, but the length prefix claims 13 |
| std::vector<std::uint8_t> const input = |
| { |
| 0x00, 0x00, 0x00, 0x00, // outer size |
| 0x04, 'a', 0x00, // entry: array "a" |
| 0x0D, 0x00, 0x00, 0x00, // array size 13 (real is 12) |
| 0x10, '0', 0x00, 0x2A, 0x00, 0x00, 0x00, // entry: int32 "0" = 42 |
| 0x00, // array end marker |
| 0x00 // outer end marker |
| }; |
| CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 19: syntax error while parsing BSON document: document size 13 does not match the number of bytes read (12)", json::parse_error&); |
| CHECK(json::from_bson(input, true, false).is_discarded()); |
| } |
| } |
| |
| TEST_CASE("BSON nesting does not consume the call stack") |
| { |
| // An embedded document or array used to be read by calling back into the |
| // document reader, so the native call stack grew with the nesting depth of |
| // the input (#5104). The open documents are kept on a heap stack now. |
| // |
| // Deeply nested values must not be compared, copied or dumped here: those |
| // operations are still recursive and would reintroduce the crash. |
| |
| // A document nested deeply enough to have crashed. The bytes are built |
| // here rather than with to_bson(), because the writer still recurses once |
| // per level and would overflow the stack before the reader is ever |
| // reached. Every level is |
| // <int32 size> 0x03 'a' 0x00 <inner document> 0x00 |
| // so a level is eight bytes larger than the one it holds, and the sizes |
| // can be filled in from the outside in. |
| const std::size_t depth = 30000; |
| std::vector<uint8_t> input; |
| input.reserve(5 + (8 * depth)); |
| for (std::size_t i = 0; i < depth; ++i) |
| { |
| const auto size = static_cast<std::uint32_t>(5 + (8 * (depth - i))); |
| input.push_back(static_cast<uint8_t>(size & 0xFF)); |
| input.push_back(static_cast<uint8_t>((size >> 8) & 0xFF)); |
| input.push_back(static_cast<uint8_t>((size >> 16) & 0xFF)); |
| input.push_back(static_cast<uint8_t>((size >> 24) & 0xFF)); |
| input.push_back(0x03); // embedded document |
| input.push_back('a'); |
| input.push_back(0x00); |
| } |
| // the innermost document is empty, then one terminator closes each level |
| input.insert(input.end(), {0x05, 0x00, 0x00, 0x00, 0x00}); |
| input.insert(input.end(), depth, 0x00); |
| |
| SECTION("a well-formed deep document is read through the SAX interface") |
| { |
| SaxCountdown accept_all(1000000); |
| CHECK(json::sax_parse(input, &accept_all, json::input_format_t::bson)); |
| } |
| |
| SECTION("a well-formed deep document is read into a value") |
| { |
| json j = json::from_bson(input); |
| |
| // walked rather than compared: comparing, copying or dumping a value |
| // this deep is still recursive |
| std::size_t measured = 0; |
| const json* q = &j; |
| while (q->is_object() && !q->empty()) |
| { |
| q = &q->begin().value(); |
| ++measured; |
| } |
| CHECK(measured == depth); |
| } |
| |
| SECTION("embedded documents and arrays are still read the same way") |
| { |
| const json values = {{"a", {{"b", {{"c", 1}}}}}}; |
| CHECK(json::from_bson(json::to_bson(values)) == values); |
| |
| const json array = {{"a", {1, 2, 3}}}; |
| CHECK(json::from_bson(json::to_bson(array)) == array); |
| |
| const json mixed = {{"a", {json{{"x", 1}}, json{{"y", 2}}}}}; |
| CHECK(json::from_bson(json::to_bson(mixed)) == mixed); |
| |
| CHECK(json::from_bson(json::to_bson(json::object())) == json::object()); |
| } |
| |
| SECTION("a size that does not match is still reported per document") |
| { |
| // the embedded document claims one byte too many |
| std::vector<uint8_t> const bad = |
| { |
| 0x15, 0x00, 0x00, 0x00, 0x03, 'a', 0x00, |
| 0x0D, 0x00, 0x00, 0x00, 0x08, 'b', 0x00, 0x01, 0x00, |
| 0x00 |
| }; |
| json _; |
| CHECK_THROWS_AS(_ = json::from_bson(bad), json::parse_error&); |
| CHECK(json::from_bson(bad, true, false).is_discarded()); |
| } |
| } |
| |
| TEST_CASE("BSON input that cannot be read is discarded by every overload") |
| { |
| std::vector<std::uint8_t> 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()); |
| #ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED |
| CHECK(json::from_bson(input.data(), input.size(), true, false).is_discarded()); |
| CHECK(json::from_bson({input.data(), input.size()}, true, false).is_discarded()); |
| #endif |
| } |
| |
| 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<std::uint8_t>& 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") |
| { |
| SECTION("signed") |
| { |
| SECTION("std::int64_t: INT64_MIN .. INT32_MIN-1") |
| { |
| std::vector<int64_t> const numbers |
| { |
| (std::numeric_limits<int64_t>::min)(), |
| -1000000000000000000LL, |
| -100000000000000000LL, |
| -10000000000000000LL, |
| -1000000000000000LL, |
| -100000000000000LL, |
| -10000000000000LL, |
| -1000000000000LL, |
| -100000000000LL, |
| -10000000000LL, |
| static_cast<std::int64_t>((std::numeric_limits<std::int32_t>::min)()) - 1, |
| }; |
| |
| for (const auto i : numbers) |
| { |
| |
| CAPTURE(i) |
| |
| json const j = |
| { |
| { "entry", i } |
| }; |
| CHECK(j.at("entry").is_number_integer()); |
| |
| std::uint64_t const iu = *reinterpret_cast<const std::uint64_t*>(&i); |
| std::vector<std::uint8_t> const expected_bson = |
| { |
| 0x14u, 0x00u, 0x00u, 0x00u, // size (little endian) |
| 0x12u, /// entry: int64 |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| static_cast<std::uint8_t>((iu >> (8u * 0u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 1u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 2u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 3u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 4u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 5u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 6u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 7u)) & 0xffu), |
| 0x00u // end marker |
| }; |
| |
| const auto bson = json::to_bson(j); |
| CHECK(bson == expected_bson); |
| |
| auto j_roundtrip = json::from_bson(bson); |
| |
| CHECK(j_roundtrip.at("entry").is_number_integer()); |
| CHECK(j_roundtrip == j); |
| CHECK(json::from_bson(bson, true, false) == j); |
| |
| } |
| } |
| |
| SECTION("signed std::int32_t: INT32_MIN .. INT32_MAX") |
| { |
| std::vector<int32_t> const numbers |
| { |
| (std::numeric_limits<int32_t>::min)(), |
| -2147483647L, |
| -1000000000L, |
| -100000000L, |
| -10000000L, |
| -1000000L, |
| -100000L, |
| -10000L, |
| -1000L, |
| -100L, |
| -10L, |
| -1L, |
| 0L, |
| 1L, |
| 10L, |
| 100L, |
| 1000L, |
| 10000L, |
| 100000L, |
| 1000000L, |
| 10000000L, |
| 100000000L, |
| 1000000000L, |
| 2147483646L, |
| (std::numeric_limits<int32_t>::max)() |
| }; |
| |
| for (const auto i : numbers) |
| { |
| |
| CAPTURE(i) |
| |
| json const j = |
| { |
| { "entry", i } |
| }; |
| CHECK(j.at("entry").is_number_integer()); |
| |
| std::uint32_t const iu = *reinterpret_cast<const std::uint32_t*>(&i); |
| std::vector<std::uint8_t> const expected_bson = |
| { |
| 0x10u, 0x00u, 0x00u, 0x00u, // size (little endian) |
| 0x10u, /// entry: int32 |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| static_cast<std::uint8_t>((iu >> (8u * 0u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 1u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 2u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 3u)) & 0xffu), |
| 0x00u // end marker |
| }; |
| |
| const auto bson = json::to_bson(j); |
| CHECK(bson == expected_bson); |
| |
| auto j_roundtrip = json::from_bson(bson); |
| |
| CHECK(j_roundtrip.at("entry").is_number_integer()); |
| CHECK(j_roundtrip == j); |
| CHECK(json::from_bson(bson, true, false) == j); |
| |
| } |
| } |
| |
| SECTION("signed std::int64_t: INT32_MAX+1 .. INT64_MAX") |
| { |
| std::vector<int64_t> const numbers |
| { |
| (std::numeric_limits<int64_t>::max)(), |
| 1000000000000000000LL, |
| 100000000000000000LL, |
| 10000000000000000LL, |
| 1000000000000000LL, |
| 100000000000000LL, |
| 10000000000000LL, |
| 1000000000000LL, |
| 100000000000LL, |
| 10000000000LL, |
| static_cast<std::int64_t>((std::numeric_limits<int32_t>::max)()) + 1, |
| }; |
| |
| for (const auto i : numbers) |
| { |
| |
| CAPTURE(i) |
| |
| json const j = |
| { |
| { "entry", i } |
| }; |
| CHECK(j.at("entry").is_number_integer()); |
| |
| std::uint64_t const iu = *reinterpret_cast<const std::uint64_t*>(&i); |
| std::vector<std::uint8_t> const expected_bson = |
| { |
| 0x14u, 0x00u, 0x00u, 0x00u, // size (little endian) |
| 0x12u, /// entry: int64 |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| static_cast<std::uint8_t>((iu >> (8u * 0u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 1u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 2u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 3u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 4u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 5u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 6u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 7u)) & 0xffu), |
| 0x00u // end marker |
| }; |
| |
| const auto bson = json::to_bson(j); |
| CHECK(bson == expected_bson); |
| |
| auto j_roundtrip = json::from_bson(bson); |
| |
| CHECK(j_roundtrip.at("entry").is_number_integer()); |
| CHECK(j_roundtrip == j); |
| CHECK(json::from_bson(bson, true, false) == j); |
| |
| } |
| } |
| } |
| |
| SECTION("unsigned") |
| { |
| SECTION("unsigned std::uint64_t: 0 .. INT32_MAX") |
| { |
| std::vector<std::uint64_t> const numbers |
| { |
| 0ULL, |
| 1ULL, |
| 10ULL, |
| 100ULL, |
| 1000ULL, |
| 10000ULL, |
| 100000ULL, |
| 1000000ULL, |
| 10000000ULL, |
| 100000000ULL, |
| 1000000000ULL, |
| 2147483646ULL, |
| static_cast<std::uint64_t>((std::numeric_limits<int32_t>::max)()) |
| }; |
| |
| for (const auto i : numbers) |
| { |
| |
| CAPTURE(i) |
| |
| json const j = |
| { |
| { "entry", i } |
| }; |
| |
| auto iu = i; |
| std::vector<std::uint8_t> const expected_bson = |
| { |
| 0x10u, 0x00u, 0x00u, 0x00u, // size (little endian) |
| 0x10u, /// entry: int32 |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| static_cast<std::uint8_t>((iu >> (8u * 0u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 1u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 2u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 3u)) & 0xffu), |
| 0x00u // end marker |
| }; |
| |
| const auto bson = json::to_bson(j); |
| CHECK(bson == expected_bson); |
| |
| auto j_roundtrip = json::from_bson(bson); |
| |
| CHECK(j.at("entry").is_number_unsigned()); |
| CHECK(j_roundtrip.at("entry").is_number_integer()); |
| CHECK(j_roundtrip == j); |
| CHECK(json::from_bson(bson, true, false) == j); |
| |
| } |
| } |
| |
| SECTION("unsigned std::uint64_t: INT32_MAX+1 .. INT64_MAX") |
| { |
| std::vector<std::uint64_t> const numbers |
| { |
| static_cast<std::uint64_t>((std::numeric_limits<std::int32_t>::max)()) + 1, |
| 4000000000ULL, |
| static_cast<std::uint64_t>((std::numeric_limits<std::uint32_t>::max)()), |
| 10000000000ULL, |
| 100000000000ULL, |
| 1000000000000ULL, |
| 10000000000000ULL, |
| 100000000000000ULL, |
| 1000000000000000ULL, |
| 10000000000000000ULL, |
| 100000000000000000ULL, |
| 1000000000000000000ULL, |
| static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()), |
| }; |
| |
| for (const auto i : numbers) |
| { |
| |
| CAPTURE(i) |
| |
| json const j = |
| { |
| { "entry", i } |
| }; |
| |
| auto iu = i; |
| std::vector<std::uint8_t> const expected_bson = |
| { |
| 0x14u, 0x00u, 0x00u, 0x00u, // size (little endian) |
| 0x12u, /// entry: int64 |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| static_cast<std::uint8_t>((iu >> (8u * 0u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 1u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 2u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 3u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 4u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 5u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 6u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 7u)) & 0xffu), |
| 0x00u // end marker |
| }; |
| |
| const auto bson = json::to_bson(j); |
| CHECK(bson == expected_bson); |
| |
| auto j_roundtrip = json::from_bson(bson); |
| |
| CHECK(j.at("entry").is_number_unsigned()); |
| CHECK(j_roundtrip.at("entry").is_number_integer()); |
| CHECK(j_roundtrip == j); |
| CHECK(json::from_bson(bson, true, false) == j); |
| } |
| } |
| |
| SECTION("unsigned std::uint64_t: INT64_MAX+1 .. UINT64_MAX") |
| { |
| std::vector<std::uint64_t> const numbers |
| { |
| static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()) + 1ULL, |
| 0xffffffffffffffff, |
| }; |
| |
| for (const auto i : numbers) |
| { |
| |
| CAPTURE(i) |
| |
| json const j = |
| { |
| { "entry", i } |
| }; |
| |
| auto iu = i; |
| std::vector<std::uint8_t> const expected_bson = |
| { |
| 0x14u, 0x00u, 0x00u, 0x00u, // size (little endian) |
| 0x11u, /// entry: uint64 |
| 'e', 'n', 't', 'r', 'y', '\x00', |
| static_cast<std::uint8_t>((iu >> (8u * 0u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 1u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 2u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 3u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 4u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 5u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 6u)) & 0xffu), |
| static_cast<std::uint8_t>((iu >> (8u * 7u)) & 0xffu), |
| 0x00u // end marker |
| }; |
| |
| const auto bson = json::to_bson(j); |
| CHECK(bson == expected_bson); |
| |
| auto j_roundtrip = json::from_bson(bson); |
| |
| CHECK(j.at("entry").is_number_unsigned()); |
| CHECK(j_roundtrip.at("entry").is_number_unsigned()); |
| CHECK(j_roundtrip == j); |
| CHECK(json::from_bson(bson, true, false) == j); |
| } |
| } |
| |
| } |
| } |
| } |
| |
| TEST_CASE("Parse BSON directly from a file using iterator and sentinel") |
| { |
| std::string const filename = TEST_DATA_DIRECTORY "/json.org/1.json"; |
| |
| std::ifstream f_json(filename); |
| const json expected = json::parse(f_json); |
| |
| std::ifstream file(filename + ".bson", std::ios::binary); |
| const std::istreambuf_iterator<char> first(file); |
| const json parsed = json::from_bson(first, utils::istreambuf_sentinel{}); |
| CHECK(parsed == expected); |
| } |
| |
| TEST_CASE("BSON round-trip invariants") |
| { |
| // This checks what the parse_bson_fuzzer driver checks (see |
| // tests/src/fuzzer-parse_bson.cpp), so that a regression shows up in CI |
| // rather than as an OSS-Fuzz report: anything from_bson() returns (j1) |
| // can be serialized, parsed back (j2), and serialized again to reproduce |
| // the exact bytes. BSON only serializes objects, so non-object corpus |
| // values are skipped. |
| for (const auto& j0 : utils::round_trip_corpus::values()) |
| { |
| if (!j0.is_object()) |
| { |
| continue; |
| } |
| |
| json j1; |
| try |
| { |
| // turn the corpus value into a value as from_bson() returns it |
| j1 = json::from_bson(json::to_bson(j0)); |
| } |
| catch (const json::exception&) |
| { |
| // the fuzzer driver only ever sees values from_bson() actually |
| // produced, so skip corpus values that do not survive the |
| // round trip here, too |
| continue; |
| } |
| |
| INFO("j1 = " << j1.dump()); |
| const std::vector<std::uint8_t> vec = json::to_bson(j1); |
| json j2; |
| // anything the library writes must be parsable by the library |
| REQUIRE_NOTHROW(j2 = json::from_bson(vec)); |
| CHECK(json::to_bson(j2) == vec); |
| } |
| } |
| |
| TEST_CASE("BSON roundtrips" * doctest::skip()) |
| { |
| SECTION("reference files") |
| { |
| for (const std::string filename : |
| { |
| 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" |
| }) |
| { |
| CAPTURE(filename) |
| |
| { |
| INFO_WITH_TEMP(filename + ": std::vector<std::uint8_t>"); |
| // parse JSON file |
| std::ifstream f_json(filename); |
| const json j1 = json::parse(f_json); |
| |
| // parse BSON file |
| auto packed = utils::read_binary_file(filename + ".bson"); |
| json j2; |
| CHECK_NOTHROW(j2 = json::from_bson(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 BSON file |
| std::ifstream f_bson(filename + ".bson", std::ios::binary); |
| json j2; |
| CHECK_NOTHROW(j2 = json::from_bson(f_bson)); |
| |
| // compare parsed JSON values |
| CHECK(j1 == j2); |
| } |
| |
| #ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED |
| { |
| INFO_WITH_TEMP(filename + ": uint8_t* and size"); |
| // parse JSON file |
| std::ifstream f_json(filename); |
| const json j1 = json::parse(f_json); |
| |
| // parse BSON file |
| auto packed = utils::read_binary_file(filename + ".bson"); |
| json j2; |
| CHECK_NOTHROW(j2 = json::from_bson({packed.data(), packed.size()})); |
| |
| // compare parsed JSON values |
| CHECK(j1 == j2); |
| } |
| #endif |
| |
| { |
| INFO_WITH_TEMP(filename + ": output to output adapters"); |
| // parse JSON file |
| std::ifstream f_json(filename); |
| json const j1 = json::parse(f_json); |
| |
| // parse BSON file |
| auto packed = utils::read_binary_file(filename + ".bson"); |
| |
| { |
| INFO_WITH_TEMP(filename + ": output adapters: std::vector<std::uint8_t>"); |
| std::vector<std::uint8_t> vec; |
| json::to_bson(j1, vec); |
| |
| if (vec != packed) |
| { |
| // the exact serializations may differ due to the order of |
| // object keys; in these cases, just compare whether both |
| // serializations create the same JSON value |
| CHECK(json::from_bson(vec) == json::from_bson(packed)); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| TEST_CASE("BSON: deeply nested values") |
| { |
| SECTION("documents and arrays round-trip at every depth") |
| { |
| // nested documents and arrays, with siblings on every level, so |
| // every length prefix covers entries of both kinds |
| json value = "leaf"; |
| for (std::size_t depth = 0; depth <= 300; ++depth) |
| { |
| CAPTURE(depth) |
| const json document = {{"value", value}, {"n", depth}}; |
| CHECK(json::from_bson(json::to_bson(document)) == document); |
| |
| value = depth % 2 == 0 ? json{{"a", std::move(value)}, {"b", {1, "x"}}} : |
| json::array({std::move(value), depth, json::object()}); |
| } |
| } |
| |
| SECTION("a key containing U+0000 is rejected before anything is written") |
| { |
| json value = json::object({{std::string("bad\0key", 7), 1}}); |
| for (std::size_t depth = 0; depth < 200; ++depth) |
| { |
| value = json{{"a", {{"b", 1}}}, {"z", std::move(value)}}; |
| } |
| std::vector<std::uint8_t> output; |
| CHECK_THROWS_AS(json::to_bson(value, output), json::out_of_range&); |
| CHECK(output.empty()); |
| } |
| |
| SECTION("a binary subtype that doesn't fit a byte is rejected before anything is written (#5675)") |
| { |
| // the offending value is nested, so this also covers that the check |
| // is not limited to a directly written value's own document |
| json const j = {{"a", {{"b", json::binary({1, 2}, 300)}}}}; |
| |
| std::vector<std::uint8_t> vector_output; |
| CHECK_THROWS_AS(json::to_bson(j, vector_output), json::out_of_range&); |
| CHECK(vector_output.empty()); |
| |
| std::string string_output; |
| CHECK_THROWS_AS(json::to_bson(j, string_output), json::out_of_range&); |
| CHECK(string_output.empty()); |
| } |
| |
| SECTION("values nested too deeply for the call stack (#5392)") |
| { |
| // serializing recursed once per nesting level, and computed every |
| // nested document's length by walking everything below it again. |
| // The values are only parsed, serialized and walked, never copied or |
| // compared, since those recurse too. |
| const std::size_t depth = 100000; |
| for (const bool objects : |
| { |
| false, true |
| }) |
| { |
| CAPTURE(objects) |
| std::string text = "{\"a\":"; |
| for (std::size_t i = 0; i < depth; ++i) |
| { |
| text += objects ? "{\"a\":" : "["; |
| } |
| text += "1"; |
| text.append(depth, objects ? '}' : ']'); |
| text += "}"; |
| |
| const auto bson = json::to_bson(json::parse(text)); |
| const auto result = json::from_bson(bson); |
| const json* p = &result.at("a"); |
| for (std::size_t i = 0; i < depth; ++i) |
| { |
| p = objects ? &p->at("a") : &p->at(0); |
| } |
| CHECK(*p == 1); |
| } |
| } |
| } |
| |
| TEST_CASE("Invalid document size handling") |
| { |
| SECTION("document size must be at least 5") |
| { |
| std::vector<std::uint8_t> const v = {0x04, 0x00, 0x00, 0x00, 0x00}; |
| json _; |
| CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 5: syntax error while parsing BSON document: document size 4 does not match the number of bytes read (5)", json::parse_error&); |
| CHECK(json::from_bson(v, true, false).is_discarded()); |
| } |
| |
| SECTION("declared document size must match consumed bytes (extra trailing element)") |
| { |
| // Declares 5-byte empty document but appends an int32 element after the declared end. |
| std::vector<std::uint8_t> const v = |
| { |
| 0x05, 0x00, 0x00, 0x00, |
| 0x10, 'a', 'd', 'm', 'i', 'n', 0x00, |
| 0x01, 0x00, 0x00, 0x00, |
| 0x00 |
| }; |
| json _; |
| CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 16: syntax error while parsing BSON document: document size 5 does not match the number of bytes read (16)", json::parse_error&); |
| CHECK(json::from_bson(v, true, false).is_discarded()); |
| } |
| |
| SECTION("declared document size must match consumed bytes (premature terminator)") |
| { |
| // Declares 32-byte document but only contains the size field followed by an immediate terminator. |
| std::vector<std::uint8_t> const v = |
| { |
| 0x20, 0x00, 0x00, 0x00, |
| 0x00 |
| }; |
| json _; |
| CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 5: syntax error while parsing BSON document: document size 32 does not match the number of bytes read (5)", json::parse_error&); |
| CHECK(json::from_bson(v, true, false).is_discarded()); |
| } |
| |
| SECTION("array declared size must match consumed bytes") |
| { |
| // Outer object contains an array "a" that declares 5 bytes (empty) but |
| // actually contains an int32 element before its terminator. |
| std::vector<std::uint8_t> const v = |
| { |
| 0x14, 0x00, 0x00, 0x00, // object size = 20 |
| 0x04, 'a', 0x00, // key "a", array type |
| 0x05, 0x00, 0x00, 0x00, // array declared size = 5 (empty) |
| 0x10, '0', 0x00, 0x01, 0x00, 0x00, 0x00, // extra int32 element "0" = 1 |
| 0x00, // array terminator |
| 0x00 // object terminator |
| }; |
| json _; |
| CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 19: syntax error while parsing BSON document: document size 5 does not match the number of bytes read (12)", json::parse_error&); |
| CHECK(json::from_bson(v, true, false).is_discarded()); |
| } |
| |
| SECTION("BSON string must end with 0x00") |
| { |
| // Length-prefixed string whose terminator byte is 'X' (0x58), not 0x00. |
| std::vector<std::uint8_t> const v = |
| { |
| 0x0F, 0x00, 0x00, 0x00, |
| 0x02, 's', 0x00, |
| 0x02, 0x00, 0x00, 0x00, |
| 'A', 'X', |
| 0x00 |
| }; |
| json _; |
| CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 13: syntax error while parsing BSON string: BSON string is not null-terminated", json::parse_error&); |
| CHECK(json::from_bson(v, true, false).is_discarded()); |
| } |
| } |
| |
| TEST_CASE("BSON large strings and binaries (chunked reader)") |
| { |
| // get_bson_string()/get_bson_binary() both read through get_string()/ |
| // get_binary(), which read in bounded chunks (binary_reader.hpp, |
| // chunk_size == 4096); make sure roundtripping is correct for lengths |
| // around and beyond that chunk size, for both vector (iterator) and |
| // pointer inputs. BSON only accepts an object at the top level, so the |
| // string/binary value is wrapped in one. |
| for (const std::size_t len : |
| { |
| std::size_t{0}, std::size_t{1}, std::size_t{4095}, std::size_t{4096}, |
| std::size_t{4097}, std::size_t{8192}, std::size_t{100000} |
| }) |
| { |
| CAPTURE(len) |
| |
| // string |
| const json j_string = {{"k", std::string(len, 'x')}}; |
| const std::vector<std::uint8_t> v_string = json::to_bson(j_string); |
| CHECK(json::from_bson(v_string) == j_string); |
| // pointer input exercises the std::memcpy fast path |
| CHECK(json::from_bson(reinterpret_cast<const char*>(v_string.data()), |
| reinterpret_cast<const char*>(v_string.data()) + v_string.size()) == j_string); |
| |
| // binary (BSON binary values always carry a subtype, so give one |
| // explicitly; otherwise from_bson() would round-trip to subtype 0 |
| // rather than back to the original "no subtype" value) |
| const json j_binary = {{"k", json::binary(std::vector<std::uint8_t>(len, 0xCD), std::uint8_t{0})}}; |
| const std::vector<std::uint8_t> v_binary = json::to_bson(j_binary); |
| CHECK(json::from_bson(v_binary) == j_binary); |
| CHECK(json::from_bson(reinterpret_cast<const char*>(v_binary.data()), |
| reinterpret_cast<const char*>(v_binary.data()) + v_binary.size()) == j_binary); |
| |
| // a truncated payload must still be reported as an error |
| if (len > 16) |
| { |
| std::vector<std::uint8_t> truncated = v_string; |
| truncated.resize(truncated.size() - 8); |
| json _; |
| CHECK_THROWS_AS(_ = json::from_bson(truncated), json::parse_error); |
| } |
| } |
| } |