mirror of
https://github.com/nlohmann/json.git
synced 2026-09-28 06:10:22 +01:00
* Write BSON in linear time, without recursing per nesting level to_bson() had two problems with nested values: - It recursed once per nesting level, so a value nested deeply enough - 100,000 levels on an 8 MiB stack - exhausted the call stack and terminated the process, although parse() accepts such values without complaint. - BSON prefixes every document and array with its length. The writer computed that length by walking the entire value below it, again for every nested document it wrote, which made serializing O(size x depth). A 200-level document took 30 ms instead of 1. Both passes are now iterative, and each length is computed exactly once: - calc_bson_sizes() computes the length of every document and array in one pass, each from the lengths of its entries, into a table ordered the way they are written. - write_bson_document() then writes the document, taking each length from the table. Everything observable is unchanged, as a differential test against develop confirms byte for byte: - The same bytes are written. - A key containing U+0000 still throws out_of_range.409 for the same first key, with the same diagnostics path, before anything is written. - A document too large for BSON still throws out_of_range.412 before anything is written. - A binary subtype above 255 still throws out_of_range.415 after the same partial output. Only the enclosing objects and arrays are kept on a stack, so a flat document allocates nothing for it. Measured against develop (clang -O3, median of 201 runs): flat objects unchanged, flat arrays 37% faster (the array length was computed twice), a nested 3,000-object document 2x faster, a 200-level document 33x faster. to_bson.md documented the quadratic complexity since #5334; it is linear again. Fixes #5392 for BSON, and #5308. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Do not require a default-constructible string_t in the BSON writer GCC 4.9 and MSVC rejected the test's huge_string_t, which has no default constructor; develop never default-constructed string_t here either. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Let the BSON index-name helper only fill its output parameter It returned a reference to the string it filled, so callers held a second name for index_name. Addresses review feedback. Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me>
1837 lines
67 KiB
C++
1837 lines
67 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"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
using nlohmann::json;
|
|
|
|
#include <cstdint>
|
|
#include <fstream>
|
|
#include <limits>
|
|
#include <sstream>
|
|
#include <vector>
|
|
#include "make_test_data_available.hpp"
|
|
#include "test_utils.hpp"
|
|
|
|
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;
|
|
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("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("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);
|
|
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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace
|
|
{
|
|
class SaxCountdown
|
|
{
|
|
public:
|
|
explicit SaxCountdown(const int count) : events_left(count)
|
|
{}
|
|
|
|
bool null()
|
|
{
|
|
return events_left-- > 0;
|
|
}
|
|
|
|
bool boolean(bool /*unused*/)
|
|
{
|
|
return events_left-- > 0;
|
|
}
|
|
|
|
bool number_integer(json::number_integer_t /*unused*/)
|
|
{
|
|
return events_left-- > 0;
|
|
}
|
|
|
|
bool number_unsigned(json::number_unsigned_t /*unused*/)
|
|
{
|
|
return events_left-- > 0;
|
|
}
|
|
|
|
bool number_float(json::number_float_t /*unused*/, const std::string& /*unused*/)
|
|
{
|
|
return events_left-- > 0;
|
|
}
|
|
|
|
bool string(std::string& /*unused*/)
|
|
{
|
|
return events_left-- > 0;
|
|
}
|
|
|
|
bool binary(std::vector<std::uint8_t>& /*unused*/)
|
|
{
|
|
return events_left-- > 0;
|
|
}
|
|
|
|
bool start_object(std::size_t /*unused*/)
|
|
{
|
|
return events_left-- > 0;
|
|
}
|
|
|
|
bool key(std::string& /*unused*/)
|
|
{
|
|
return events_left-- > 0;
|
|
}
|
|
|
|
bool end_object()
|
|
{
|
|
return events_left-- > 0;
|
|
}
|
|
|
|
bool start_array(std::size_t /*unused*/)
|
|
{
|
|
return events_left-- > 0;
|
|
}
|
|
|
|
bool end_array()
|
|
{
|
|
return events_left-- > 0;
|
|
}
|
|
|
|
bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, const json::exception& /*unused*/) // NOLINT(readability-convert-member-functions-to-static)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
private:
|
|
int events_left = 0;
|
|
};
|
|
} // namespace
|
|
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
|
|
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 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 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);
|
|
}
|
|
|
|
{
|
|
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);
|
|
}
|
|
|
|
{
|
|
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("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());
|
|
}
|
|
}
|