mirror of
https://github.com/nlohmann/json.git
synced 2026-10-31 09:55:42 +00:00
BSON prefixes every document and array with its size. to_bson() therefore walked the value twice: once to compute every size, once to write. When the output is the vector that to_bson(j) returns, the writer now puts a placeholder in front of each document and array and writes its size there once the document is complete, so one walk is enough. Every key and value is still checked as the size pass checked it, in the same order, so the same exception is thrown; the partial output is discarded with the vector. If a length would not fit BSON's 32-bit size fields, the writer drops what it has written and writes the document in two passes, which reports that error before writing anything, as before. The two passes remain for output adapters (streams, strings, vectors passed by reference), for error_handler_t::replace and ignore (which change sizes), and for integer types wider than 64 bits. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2399 lines
89 KiB
C++
2399 lines
89 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::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) carries an inner length")
|
|
{
|
|
// the payload of binary subtype 0x02 ("old binary") starts with an
|
|
// int32 that repeats the payload size; the bytes below are what
|
|
// pymongo and the bson-corpus (binary.json) use
|
|
std::vector<std::uint8_t> const input =
|
|
{
|
|
0x17, 0x00, 0x00, 0x00, // size (little endian)
|
|
0x05, 'e', 'n', 't', 'r', 'y', '\x00', // entry: binary
|
|
|
|
0x06, 0x00, 0x00, 0x00, // size of binary (little endian)
|
|
0x02, // "old binary" subtype
|
|
0x02, 0x00, 0x00, 0x00, // inner length
|
|
0x68, 0x69, // payload ('h', 'i')
|
|
|
|
0x00 // end marker
|
|
};
|
|
|
|
// the inner length is not part of the payload
|
|
const json expected = { { "entry", json::binary({0x68, 0x69}, 0x02) } };
|
|
CHECK(json::from_bson(input) == expected);
|
|
CHECK(json::to_bson(expected) == input);
|
|
CHECK(json::from_bson(json::to_bson(expected)) == expected);
|
|
}
|
|
|
|
SECTION("binary member with subtype 0x02 (old binary) and empty payload")
|
|
{
|
|
std::vector<std::uint8_t> const input =
|
|
{
|
|
0x15, 0x00, 0x00, 0x00, // size (little endian)
|
|
0x05, 'e', 'n', 't', 'r', 'y', '\x00', // entry: binary
|
|
0x04, 0x00, 0x00, 0x00, // size of binary (little endian)
|
|
0x02, // "old binary" subtype
|
|
0x00, 0x00, 0x00, 0x00, // inner length
|
|
0x00 // end marker
|
|
};
|
|
|
|
const json expected = { { "entry", json::binary({}, 0x02) } };
|
|
CHECK(json::from_bson(input) == expected);
|
|
CHECK(json::to_bson(expected) == input);
|
|
}
|
|
|
|
SECTION("binary member with subtype 0x02 (old binary) with an invalid inner length")
|
|
{
|
|
std::vector<std::uint8_t> input =
|
|
{
|
|
0x17, 0x00, 0x00, 0x00, // size (little endian)
|
|
0x05, 'e', 'n', 't', 'r', 'y', '\x00', // entry: binary
|
|
0x06, 0x00, 0x00, 0x00, // size of binary (little endian)
|
|
0x02, // "old binary" subtype
|
|
0x02, 0x00, 0x00, 0x00, // inner length
|
|
0x68, 0x69, // payload ('h', 'i')
|
|
0x00 // end marker
|
|
};
|
|
|
|
SECTION("inner length too long")
|
|
{
|
|
input[19] = 0x03;
|
|
}
|
|
|
|
SECTION("inner length too short")
|
|
{
|
|
input[19] = 0x01;
|
|
}
|
|
|
|
SECTION("inner length negative")
|
|
{
|
|
input[19] = 0xFF;
|
|
input[20] = 0xFF;
|
|
input[21] = 0xFF;
|
|
input[22] = 0xFF;
|
|
}
|
|
|
|
CAPTURE(input)
|
|
json _;
|
|
CHECK_THROWS_AS(_ = json::from_bson(input), json::parse_error&);
|
|
}
|
|
|
|
SECTION("binary member with subtype 0x02 (old binary) without an inner length")
|
|
{
|
|
// the payload is shorter than the inner length field
|
|
std::vector<std::uint8_t> const input =
|
|
{
|
|
0x14, 0x00, 0x00, 0x00, // size (little endian)
|
|
0x05, 'e', 'n', 't', 'r', 'y', '\x00', // entry: binary
|
|
0x03, 0x00, 0x00, 0x00, // size of binary (little endian)
|
|
0x02, // "old binary" subtype
|
|
0x01, 0x02, 0x03, // payload without an inner length
|
|
0x00 // end marker
|
|
};
|
|
|
|
json _;
|
|
CHECK_THROWS_AS(_ = json::from_bson(input), json::parse_error&);
|
|
}
|
|
|
|
SECTION("Some more complex document")
|
|
{
|
|
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 4: syntax error while parsing BSON document: document size -1 is less than 5", json::parse_error&);
|
|
CHECK(json::from_bson(input, true, false).is_discarded());
|
|
}
|
|
|
|
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 =
|
|
{
|
|
0x14, 0x00, 0x00, 0x00, // outer size
|
|
0x03, 'd', 0x00, // entry: embedded document "d"
|
|
0xFF, 0xFF, 0xFF, 0x7F, // embedded size 0x7FFFFFFF
|
|
0x0A, 'a', 0x00, // entry: null "a"
|
|
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 11: syntax error while parsing BSON document: document size 2147483647 exceeds the size of the enclosing document", json::parse_error&);
|
|
CHECK(json::from_bson(input, true, false).is_discarded());
|
|
}
|
|
|
|
SECTION("embedded document whose size is smaller than its content")
|
|
{
|
|
// {"a":null} is 8 bytes, but the length prefix claims 7
|
|
std::vector<std::uint8_t> const input =
|
|
{
|
|
0x14, 0x00, 0x00, 0x00, // outer size
|
|
0x03, 'd', 0x00, // entry: embedded document "d"
|
|
0x07, 0x00, 0x00, 0x00, // embedded size 7 (real is 8)
|
|
0x0A, 'a', 0x00, // entry: null "a"
|
|
0x00, // embedded end marker
|
|
0x08, 'h', 0x00, 0x01, // entry: bool "h" = true
|
|
0x00 // outer end marker
|
|
};
|
|
CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 15: syntax error while parsing BSON document: document size 7 does not match the number of bytes read (8)", json::parse_error&);
|
|
CHECK(json::from_bson(input, true, false).is_discarded());
|
|
}
|
|
|
|
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 =
|
|
{
|
|
0x14, 0x00, 0x00, 0x00, // outer size
|
|
0x04, 'a', 0x00, // entry: array "a"
|
|
0x0D, 0x00, 0x00, 0x00, // array size 13 (real is 12)
|
|
0x10, '0', 0x00, 0x2A, 0x00, 0x00, 0x00, // entry: int32 "0" = 42
|
|
0x00, // array end marker
|
|
0x00 // outer end marker
|
|
};
|
|
CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 11: syntax error while parsing BSON document: document size 13 exceeds the size of the enclosing document", json::parse_error&);
|
|
CHECK(json::from_bson(input, true, false).is_discarded());
|
|
}
|
|
}
|
|
|
|
TEST_CASE("BSON declared document size bounds the content")
|
|
{
|
|
// records the events that a SAX parser receives
|
|
struct recorder : nlohmann::json_sax<json>
|
|
{
|
|
// GCC's -Weffc++ wants the member initialized in the member initialization list
|
|
// NOLINTNEXTLINE(readability-redundant-member-init)
|
|
std::string events {};
|
|
|
|
bool null() override
|
|
{
|
|
events += "null ";
|
|
return true;
|
|
}
|
|
bool boolean(bool /*val*/) override
|
|
{
|
|
events += "boolean ";
|
|
return true;
|
|
}
|
|
bool number_integer(json::number_integer_t /*val*/) override
|
|
{
|
|
events += "integer ";
|
|
return true;
|
|
}
|
|
bool number_unsigned(json::number_unsigned_t /*val*/) override
|
|
{
|
|
events += "unsigned ";
|
|
return true;
|
|
}
|
|
bool number_float(json::number_float_t /*val*/, const std::string& /*s*/) override
|
|
{
|
|
events += "float ";
|
|
return true;
|
|
}
|
|
bool string(std::string& /*val*/) override
|
|
{
|
|
events += "string ";
|
|
return true;
|
|
}
|
|
bool binary(json::binary_t& /*val*/) override
|
|
{
|
|
events += "binary ";
|
|
return true;
|
|
}
|
|
bool start_object(std::size_t /*elements*/) override
|
|
{
|
|
events += "{ ";
|
|
return true;
|
|
}
|
|
bool key(std::string& /*val*/) override
|
|
{
|
|
events += "key ";
|
|
return true;
|
|
}
|
|
bool end_object() override
|
|
{
|
|
events += "} ";
|
|
return true;
|
|
}
|
|
bool start_array(std::size_t /*elements*/) override
|
|
{
|
|
events += "[ ";
|
|
return true;
|
|
}
|
|
bool end_array() override
|
|
{
|
|
events += "] ";
|
|
return true;
|
|
}
|
|
bool parse_error(std::size_t /*position*/, const std::string& /*last_token*/, const json::exception& /*ex*/) override
|
|
{
|
|
events += "error ";
|
|
return false;
|
|
}
|
|
};
|
|
|
|
SECTION("the content of a too small document is not reported")
|
|
{
|
|
// {"a":1} is 12 bytes; the length prefix claims 5, so the element
|
|
// does not fit
|
|
std::vector<std::uint8_t> const input =
|
|
{
|
|
0x05, 0x00, 0x00, 0x00,
|
|
0x10, 'a', 0x00, 0x01, 0x00, 0x00, 0x00,
|
|
0x00
|
|
};
|
|
recorder rec;
|
|
CHECK(!json::sax_parse(input, &rec, json::input_format_t::bson));
|
|
CHECK(rec.events == "{ error ");
|
|
|
|
// also without strict mode
|
|
recorder rec2;
|
|
CHECK(!json::sax_parse(input, &rec2, json::input_format_t::bson, false));
|
|
CHECK(rec2.events == "{ error ");
|
|
}
|
|
|
|
SECTION("an embedded document that does not fit its parent is not opened")
|
|
{
|
|
// the embedded document ends where the enclosing one does, so there
|
|
// is no room for the terminator of the enclosing document
|
|
std::vector<std::uint8_t> const input =
|
|
{
|
|
0x0C, 0x00, 0x00, 0x00,
|
|
0x03, 'd', 0x00,
|
|
0x05, 0x00, 0x00, 0x00, 0x00,
|
|
0x00
|
|
};
|
|
recorder rec;
|
|
CHECK(!json::sax_parse(input, &rec, json::input_format_t::bson));
|
|
CHECK(rec.events == "{ key error ");
|
|
}
|
|
|
|
SECTION("an embedded document that fits exactly")
|
|
{
|
|
std::vector<std::uint8_t> const input =
|
|
{
|
|
0x0D, 0x00, 0x00, 0x00,
|
|
0x03, 'd', 0x00,
|
|
0x05, 0x00, 0x00, 0x00, 0x00,
|
|
0x00
|
|
};
|
|
CHECK(json::from_bson(input) == json({{"d", json::object()}}));
|
|
}
|
|
|
|
SECTION("a document size that is less than 5")
|
|
{
|
|
for (const std::int32_t size :
|
|
{
|
|
0, 1, 4, -1, (std::numeric_limits<std::int32_t>::min)()
|
|
})
|
|
{
|
|
CAPTURE(size)
|
|
std::vector<std::uint8_t> input =
|
|
{
|
|
0x00, 0x00, 0x00, 0x00, 0x00
|
|
};
|
|
input[0] = static_cast<std::uint8_t>(size & 0xFF);
|
|
input[1] = static_cast<std::uint8_t>((size >> 8) & 0xFF);
|
|
input[2] = static_cast<std::uint8_t>((size >> 16) & 0xFF);
|
|
input[3] = static_cast<std::uint8_t>((size >> 24) & 0xFF);
|
|
json _;
|
|
CHECK_THROWS_AS(_ = json::from_bson(input), json::parse_error&);
|
|
CHECK(json::from_bson(input, true, false).is_discarded());
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("BSON nesting does not consume the call stack")
|
|
{
|
|
// An embedded document or array used to be read by calling back into the
|
|
// 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 4: syntax error while parsing BSON document: document size 4 is less than 5", json::parse_error&);
|
|
CHECK(json::from_bson(v, true, false).is_discarded());
|
|
}
|
|
|
|
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 5: syntax error while parsing BSON document: document size 5 is too small for its content", json::parse_error&);
|
|
CHECK(json::from_bson(v, true, false).is_discarded());
|
|
}
|
|
|
|
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 12: syntax error while parsing BSON document: document size 5 is too small for its content", json::parse_error&);
|
|
CHECK(json::from_bson(v, true, false).is_discarded());
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("BSON written in one pass and in two passes")
|
|
{
|
|
// to_bson() into a returned vector writes each size once the object or
|
|
// array is complete; through an output adapter, all sizes are computed
|
|
// first. Both must produce the same bytes and throw the same exceptions.
|
|
const auto two_pass = [](const json & j, const json::error_handler_t handler)
|
|
{
|
|
std::vector<std::uint8_t> result;
|
|
json::to_bson(j, result, handler);
|
|
return result;
|
|
};
|
|
|
|
const auto check_same = [&](const json & j, const json::error_handler_t handler)
|
|
{
|
|
CAPTURE(j.dump(-1, ' ', true, json::error_handler_t::replace))
|
|
CAPTURE(static_cast<int>(handler))
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
std::string one_pass_error;
|
|
std::string two_pass_error;
|
|
std::vector<std::uint8_t> one;
|
|
std::vector<std::uint8_t> two;
|
|
try
|
|
{
|
|
one = json::to_bson(j, handler);
|
|
}
|
|
catch (const json::exception& e)
|
|
{
|
|
one_pass_error = e.what();
|
|
}
|
|
try
|
|
{
|
|
two = two_pass(j, handler);
|
|
}
|
|
catch (const json::exception& e)
|
|
{
|
|
two_pass_error = e.what();
|
|
}
|
|
CHECK(one_pass_error == two_pass_error);
|
|
CHECK(one == two);
|
|
#else
|
|
CHECK(json::to_bson(j, handler) == two_pass(j, handler));
|
|
#endif
|
|
};
|
|
|
|
const json::error_handler_t handlers[] = {json::error_handler_t::keep, json::error_handler_t::strict,
|
|
json::error_handler_t::replace, json::error_handler_t::ignore
|
|
};
|
|
|
|
SECTION("corpus")
|
|
{
|
|
for (const auto& value : utils::round_trip_corpus::values())
|
|
{
|
|
const json document = value.is_object() ? value : json{{"value", value}};
|
|
for (const auto handler : handlers)
|
|
{
|
|
check_same(document, handler);
|
|
}
|
|
}
|
|
}
|
|
|
|
SECTION("nesting and arrays of every length class")
|
|
{
|
|
json document = json::object();
|
|
document["empty object"] = json::object();
|
|
document["empty array"] = json::array();
|
|
for (const std::size_t n :
|
|
{
|
|
std::size_t(1), std::size_t(9), std::size_t(10), std::size_t(11), std::size_t(99), std::size_t(100), std::size_t(101), std::size_t(1000)
|
|
})
|
|
{
|
|
json numbers = json::array();
|
|
json mixed = json::array();
|
|
for (std::size_t i = 0; i < n; ++i)
|
|
{
|
|
numbers.push_back(i % 3 == 0 ? json(static_cast<double>(i) / 3) : json(static_cast<std::int64_t>(i) * 1000003));
|
|
mixed.push_back(i % 4 == 0 ? json("s" + std::to_string(i)) : i % 4 == 1 ? json::array({i, json::object({{"k", i}})}) : i % 4 == 2 ? json(nullptr) : json(i % 2 == 0));
|
|
}
|
|
document["numbers " + std::to_string(n)] = numbers;
|
|
document["mixed " + std::to_string(n)] = mixed;
|
|
document["nested " + std::to_string(n)] = json::object({{"a", numbers}, {"b", json::array({mixed, json::object()})}});
|
|
}
|
|
document["binary"] = json::binary({1, 2, 3}, 0x80);
|
|
document["binary old"] = json::binary({1, 2, 3}, 0x02);
|
|
for (const auto handler : handlers)
|
|
{
|
|
check_same(document, handler);
|
|
}
|
|
}
|
|
|
|
SECTION("errors at every position")
|
|
{
|
|
// an entry that cannot be written, at each place among valid ones
|
|
const std::string bad_utf8 = "\xC3\x28";
|
|
const json bad_entries[] =
|
|
{
|
|
json::object({{std::string("a\0b", 3), 1}}),
|
|
json::object({{"s", bad_utf8}}),
|
|
json::object({{bad_utf8, 1}}),
|
|
json::array({1, 2, bad_utf8}),
|
|
json::object({{"b", json::binary({1}, 0x100)}}),
|
|
json::object({{"d", json(json::value_t::discarded)}}),
|
|
json::array({json(json::value_t::discarded)}),
|
|
};
|
|
for (const auto& bad : bad_entries)
|
|
{
|
|
for (std::size_t pos = 0; pos < 4; ++pos)
|
|
{
|
|
json document = json::object();
|
|
json list = json::array();
|
|
for (std::size_t i = 0; i < 4; ++i)
|
|
{
|
|
if (i == pos)
|
|
{
|
|
document["x" + std::to_string(i)] = bad;
|
|
list.push_back(bad);
|
|
}
|
|
document["n" + std::to_string(i)] = json::array({i, "str", json::object({{"k", i}})});
|
|
list.push_back(i);
|
|
}
|
|
document["list"] = list;
|
|
for (const auto handler : handlers)
|
|
{
|
|
check_same(document, handler);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|