mirror of
https://github.com/nlohmann/json.git
synced 2026-10-08 15:37:13 +00:00
* Check in the fuzzers that parsing without exceptions agrees Each fuzzer driver now also parses its input with allow_exceptions = false. That call must never throw a parse_error, must return a discarded value where parsing with exceptions fails, and must return the same value where it succeeds. Values are compared by their dump(), because NaN is not equal to itself. A plain !is_discarded() assertion, as suggested in #3642, would never fail: the drivers parse with exceptions, so a result can never be discarded. tests/fuzzing.md describes the checks and notes that OSS-Fuzz and CIFuzz already run LeakSanitizer, because their default address sanitizer includes it. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Use JSON_HAS_RANGE_VIEW_CONVERSION in the range view regression tests #5728 combined the JSON_HAS_RANGES and MinGW conditions into JSON_HAS_RANGE_VIEW_CONVERSION, but three test guards still spelled them out. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Test the serializer's buffers at their boundaries The dump() indent overflow survived full line coverage because the tests grew its buffer by only one step. This adds tests that land exactly on, and one past, the limits of the other two serializer buffers: - write_buffer (1024 bytes): strings of 1023, 1024 and 1025 bytes at the top level, and of 1022 and 1023 bytes inside an array, so that both guards in put_string() are hit at their boundary. Each is checked for dump() and for stream output. - string_buffer (512 bytes, flushed when fewer than 13 bytes remain): runs of two-byte escapes, and a surrogate pair written with 14 bytes of room, right after a flush, and one escape later. - The 8-byte bulk scan from the serializer side: 0 to 17 plain bytes followed by a quote, a control character, or a non-ASCII character. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Test the chunked string and binary reads of all binary formats The binary readers read strings and binary values in chunks of 4096 bytes. Only CBOR tested lengths around that size. MessagePack, UBJSON, BJData and BSON now round-trip lengths 0, 1, 4095, 4096, 4097, 8192 and 100000 from vector and pointer input, and must report a truncated payload as a parse error. UBJSON reads binary values as arrays of numbers, so it is tested with strings only. BJData binary values reach the chunked read only in Draft 3. BON8 decodes strings byte by byte and does not use this path. Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2040 lines
77 KiB
C++
2040 lines
77 KiB
C++
// __ _____ _____ _____
|
|
// __| | __| | | | 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::basic_json <
|
|
std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t,
|
|
double, std::allocator, nlohmann::adl_serializer, huge_binary_t, void >;
|
|
|
|
// 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::basic_json <
|
|
std::map, std::vector, huge_string_t, bool, std::int64_t, std::uint64_t,
|
|
double, std::allocator, nlohmann::adl_serializer, std::vector<std::uint8_t>, void >;
|
|
} // 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);
|
|
}
|
|
}
|
|
}
|