Merge branch 'develop' into claude/fix-issue-3989-db7e45

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann committed 2026-10-21 11:59:40 +02:00
commit c25aad3ca5
342 files changed
+605561 -780

No files matched your search

+95
View File
@@ -0,0 +1,95 @@
// __ _____ _____ _____
// __| | __| | | | 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
/*
This file implements a test of json_document images suitable for fuzz
testing. The input is used twice:
- as an image: json_document::load() with image_check::full must either throw
a parse_error or yield a document that serializes to the JSON text it reads
as; with image_check::bounds, reading and serializing must be safe (checked
by the sanitizers), and serializing may only throw type_error.316
- as a JSON text: if json_document::parse() accepts it, the image of the
document must load (with every check) and serialize to the same text
The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer
drivers.
*/
#include <cassert>
#include <cstdint>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
#include <nlohmann/json_view.hpp>
// the checks below are assertions; NDEBUG would compile them away
#ifdef NDEBUG
#error "the fuzzer drivers must be built without NDEBUG"
#endif
using json = nlohmann::json;
using json_document = nlohmann::json_document;
using image_check = json_document::image_check;
// see http://llvm.org/docs/LibFuzzer.html
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{
// the input as an image
for (const image_check check :
{
image_check::full, image_check::bounds
})
{
json_document d;
try
{
d = json_document::load(data, size, check);
}
catch (const json::parse_error& e)
{
assert(e.id == 116);
continue;
}
std::string dumped;
try
{
dumped = d.root().dump();
}
catch (const json::type_error& e)
{
// invalid UTF-8 can only pass the bounds check
assert(check == image_check::bounds && e.id == 316);
continue;
}
const json j = d.root().materialize();
if (check == image_check::full)
{
assert(json::parse(dumped) == j);
// an image of the loaded document is the input
assert(d.save() == std::vector<std::uint8_t>(data, data + size));
}
}
// the input as a JSON text
const std::string text(reinterpret_cast<const char*>(data), size); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
const json_document parsed = json_document::parse(text, false);
if (!parsed.is_discarded())
{
const std::vector<std::uint8_t> image = parsed.save();
for (const image_check check :
{
image_check::full, image_check::bounds, image_check::none
})
{
const json_document loaded = json_document::load(image, check);
assert(loaded.root().dump() == parsed.root().dump());
}
}
return 0;
}
+155
View File
@@ -0,0 +1,155 @@
// __ _____ _____ _____
// __| | __| | | | 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
/*
This file implements a parser test suitable for fuzz testing. It checks that
json_document (the zero-copy, read-only view of a parsed JSON text declared in
json_view.hpp) agrees with basic_json on every input, for the parse options
selected by the low bits of the first input byte (bit 0: ignore_comments, bit 1:
ignore_trailing_commas; the byte stays part of the text):
- json_document::accept(data) must equal json::accept(data)
- if the input is accepted, json_document::parse(data).root().materialize()
must equal json::parse(data)
- if the input is rejected, json_document::parse(data) (with exceptions
enabled) must throw a json::parse_error or json::out_of_range whose what()
is identical to the one json::parse(data) throws
This is checked for two kinds of input: a std::string, which the document
borrows and which ends in the NUL the parser uses as sentinel, and an
exact-size byte vector, which has no NUL after its last byte and takes the
parser's bounds-checked path (AddressSanitizer reports any read past the end).
The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer
drivers.
*/
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
#include <nlohmann/json_view.hpp>
// the checks below are assertions; NDEBUG would compile them away
#ifdef NDEBUG
#error "the fuzzer drivers must be built without NDEBUG"
#endif
using json = nlohmann::json;
using json_document = nlohmann::json_document;
namespace
{
// what json::parse does with a text: the value, or the message of the exception
struct reference_result
{
bool accepted = false;
json value{};
std::string what{}; // NOLINT(readability-redundant-member-init)
};
reference_result parse_reference(const std::uint8_t* data, std::size_t size, bool comments, bool trailing_commas)
{
reference_result r;
r.accepted = json::accept(data, data + size, comments, trailing_commas);
bool json_threw = false;
try
{
r.value = json::parse(data, data + size, nullptr, true, comments, trailing_commas);
}
catch (const json::parse_error& e)
{
r.what = e.what();
json_threw = true;
}
catch (const json::out_of_range& e)
{
r.what = e.what();
json_threw = true;
}
// json::accept and json::parse must agree
assert(json_threw == !r.accepted);
static_cast<void>(json_threw);
return r;
}
// json_document must agree with the reference for this input (a container
// that json_document::parse borrows)
template<typename Input>
void check_input(const Input& input, const reference_result& expected, bool comments, bool trailing_commas)
{
// json_document::accept must agree with json::accept on every input
const bool accepted_by_view = json_document::accept(input, comments, trailing_commas);
assert(expected.accepted == accepted_by_view);
static_cast<void>(accepted_by_view);
if (expected.accepted)
{
// both parsers must agree on the resulting value
json_document const doc = json_document::parse(input, true, comments, trailing_commas);
assert(!doc.is_discarded());
json const j2 = doc.root().materialize();
assert(expected.value == j2);
static_cast<void>(j2);
// (without exceptions, the same document)
json_document const quiet = json_document::parse(input, false, comments, trailing_commas);
assert(!quiet.is_discarded());
assert(quiet.node_count() == doc.node_count());
}
else
{
// both parsers must reject the input the same way when exceptions are used
bool view_threw = false;
try
{
static_cast<void>(json_document::parse(input, true, comments, trailing_commas));
}
catch (const json::parse_error& e)
{
assert(e.what() == expected.what);
view_threw = true;
}
catch (const json::out_of_range& e)
{
assert(e.what() == expected.what);
view_threw = true;
}
assert(view_threw);
static_cast<void>(view_threw);
// and without exceptions, the document is discarded
json_document const quiet = json_document::parse(input, false, comments, trailing_commas);
assert(quiet.is_discarded());
static_cast<void>(quiet);
}
}
} // namespace
// see http://llvm.org/docs/LibFuzzer.html
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{
// the parse options are taken from the low bits of the first byte
const bool comments = size > 0 && (data[0] & 1U) != 0;
const bool trailing_commas = size > 0 && (data[0] & 2U) != 0;
const reference_result expected = parse_reference(data, size, comments, trailing_commas);
// a std::string: borrowed, with the NUL of std::string as sentinel
const std::string input(reinterpret_cast<const char*>(data), size); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
check_input(input, expected, comments, trailing_commas);
// an exact-size byte vector: borrowed, with nothing after its last byte
const std::vector<std::uint8_t> exact(data, data + size);
check_input(exact, expected, comments, trailing_commas);
// return 0 - non-zero return values are reserved for future use
return 0;
}
+106
View File
@@ -0,0 +1,106 @@
// __ _____ _____ _____
// __| | __| | | | 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
#pragma once
// helpers shared by the unit-json_view*.cpp files, which are split to keep every
// object file small enough for the MinGW linker (it fails to link objects with
// more than 32767 sections)
#include <nlohmann/json_view.hpp>
#include <algorithm> // any_of
#include <functional> // function
#include <random> // mt19937
#include <string> // string
namespace json_view_test
{
// a small deterministic generator of documents
struct generator
{
std::mt19937 rng{5295}; // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
int r(int n)
{
return static_cast<int>(rng() % static_cast<unsigned>(n));
}
void str(std::string& o)
{
static const char* const pieces[] = {"a", "Z", " ", "\\n", "\\\"", "\\u00e9", "\\ud83d\\ude00", "\xc3\xa9", "\xe3\x81\x82", "long text beyond the first sixteen bytes"}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
o += '"';
for (int n = r(5); n > 0; --n)
{
o += pieces[r(10)];
}
o += '"';
}
void value(std::string& o, int depth)
{
static const char* const scalars[] = {"0", "-1", "123456789012", "18446744073709551615", "18446744073709551616", "-9223372036854775809", // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
"1.5", "-2.25e-3", "1E2", "0.1", "true", "false", "null"
};
const int k = depth > 5 ? 2 + r(4) : r(6);
if (k < 2)
{
const bool object = k == 0;
o += object ? '{' : '[';
for (int i = r(5); i > 0; --i)
{
if (object)
{
str(o);
o += r(2) == 0 ? ":" : " : ";
}
value(o, depth + 1);
o += i > 1 ? ", " : "";
}
o += object ? '}' : ']';
}
else if (k < 4)
{
str(o);
}
else
{
o += scalars[r(13)];
}
}
};
// whether an object of the view repeats a key
inline bool has_duplicate_keys(const nlohmann::ordered_json_view& v)
{
if (v.is_object() && v.size() != v.materialize().size())
{
return true;
}
return std::any_of(v.begin(), v.end(), [](const nlohmann::ordered_json_view e)
{
return e.is_structured() && has_duplicate_keys(e);
});
}
#if !defined(JSON_NOEXCEPTION)
// the exception a call throws, or "" if it throws none
inline std::string exception_of_call(const std::function<void()>& f)
{
try
{
f();
}
catch (const nlohmann::json::exception& e)
{
return e.what();
}
return "";
}
#endif
} // namespace json_view_test
+279 -1
View File
@@ -11,8 +11,10 @@
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <array>
#include <cstdint>
#include <limits>
#include <sstream>
#include <string>
#include <vector>
@@ -35,6 +37,17 @@ std::vector<json> test_values()
big_object[std::to_string(i)] = i;
}
// large strings and binary values between small ones: a write too large
// for the space the vector sink has ahead takes a different path than a
// small one, and the small writes after it must still land behind it
json large_payloads = json::array();
for (int i = 0; i < 50; ++i)
{
large_payloads.push_back(std::string(300, static_cast<char>('a' + (i % 26))));
large_payloads.push_back(i);
large_payloads.push_back(json::binary(std::vector<std::uint8_t>(static_cast<std::size_t>(100 * i), static_cast<std::uint8_t>(i))));
}
return
{
json(nullptr), json(true), json(false),
@@ -46,10 +59,23 @@ std::vector<json> test_values()
json::array(), json::object(),
json::array({1, 2, 3}), json({{"a", 1}, {"b", nullptr}}),
json({{"nested", {{"deep", json::array({1, "two", 3.0, nullptr})}}}}),
big_array, big_object
big_array, big_object, large_payloads,
json::binary(std::vector<std::uint8_t>(5000, 0x42))
};
}
// explicit char -> uint8_t conversion (the iterator-range constructor converts implicitly, which -fsanitize=integer flags)
std::vector<std::uint8_t> to_bytes(const std::string& str)
{
std::vector<std::uint8_t> result;
result.reserve(str.size());
for (const char c : str)
{
result.push_back(static_cast<std::uint8_t>(c));
}
return result;
}
// BON8 has no integers above the int64 range, so to_bon8() rejects them
bool bon8_representable(const json& j)
{
@@ -170,6 +196,258 @@ TEST_CASE("binary writer output sinks")
}
}
namespace
{
// the CBOR head of an integer, encoded independently of binary_writer
void reference_cbor_head(std::vector<std::uint8_t>& out, const std::uint8_t major_type, const std::uint64_t argument)
{
if (argument <= 23)
{
out.push_back(static_cast<std::uint8_t>(major_type + argument));
return;
}
int length = 8;
std::uint8_t additional = 27;
if (argument <= 0xFF)
{
length = 1;
additional = 24;
}
else if (argument <= 0xFFFF)
{
length = 2;
additional = 25;
}
else if (argument <= 0xFFFFFFFF)
{
length = 4;
additional = 26;
}
out.push_back(static_cast<std::uint8_t>(major_type + additional));
for (int i = length - 1; i >= 0; --i)
{
out.push_back(static_cast<std::uint8_t>(argument >> (8 * i)));
}
}
// the CBOR encoding of an integer value
void reference_cbor_integer(std::vector<std::uint8_t>& out, const json& j)
{
if (j.is_number_unsigned())
{
reference_cbor_head(out, 0x00, j.get<std::uint64_t>());
return;
}
const auto n = j.get<std::int64_t>();
if (n >= 0)
{
reference_cbor_head(out, 0x00, static_cast<std::uint64_t>(n));
}
else
{
reference_cbor_head(out, 0x20, static_cast<std::uint64_t>(-1 - n));
}
}
// the MessagePack encoding of an integer value, encoded independently of
// binary_writer
void reference_msgpack_integer(std::vector<std::uint8_t>& out, const json& j)
{
const auto put = [&out](const std::uint8_t marker, const std::uint64_t bits, const int length)
{
out.push_back(marker);
for (int i = length - 1; i >= 0; --i)
{
out.push_back(static_cast<std::uint8_t>(bits >> (8 * i)));
}
};
if (j.is_number_unsigned() || j.get<std::int64_t>() >= 0)
{
const auto u = j.get<std::uint64_t>();
if (u < 128)
{
out.push_back(static_cast<std::uint8_t>(u));
}
else if (u <= 0xFF)
{
put(0xCC, u, 1);
}
else if (u <= 0xFFFF)
{
put(0xCD, u, 2);
}
else if (u <= 0xFFFFFFFF)
{
put(0xCE, u, 4);
}
else
{
put(0xCF, u, 8);
}
return;
}
const auto n = j.get<std::int64_t>();
const auto bits = static_cast<std::uint64_t>(n);
if (n >= -32)
{
out.push_back(static_cast<std::uint8_t>(bits));
}
else if (n >= -128)
{
put(0xD0, bits, 1);
}
else if (n >= -32768)
{
put(0xD1, bits, 2);
}
else if (n >= -2147483648LL)
{
put(0xD2, bits, 4);
}
else
{
put(0xD3, bits, 8);
}
}
// integers at every boundary of the CBOR and MessagePack sizes, signed and
// unsigned
std::vector<json> boundary_integers()
{
std::vector<json> result;
const std::array<std::uint64_t, 38> boundaries =
{
{
0, 1, 22, 23, 24, 25, 31, 32, 33, 126, 127, 128, 129, 254, 255, 256, 257,
32766, 32767, 32768, 32769, 65534, 65535, 65536, 65537,
2147483646ULL, 2147483647ULL, 2147483648ULL, 2147483649ULL,
4294967294ULL, 4294967295ULL, 4294967296ULL, 4294967297ULL,
9223372036854775806ULL, 9223372036854775807ULL, 9223372036854775808ULL,
18446744073709551614ULL, 18446744073709551615ULL
}
};
for (const std::uint64_t u : boundaries)
{
result.emplace_back(u);
if (u <= static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()))
{
const auto n = static_cast<std::int64_t>(u);
result.emplace_back(n);
result.emplace_back(-n);
result.emplace_back(-n - 1);
}
}
return result;
}
} // namespace
// Arrays of integers are written in runs straight into space reserved from the
// sink, with branch-free encodings; check them against independent encoders for
// runs shorter and longer than one reservation, broken up by other values, and
// through each sink.
TEST_CASE("CBOR and MessagePack integer arrays")
{
const std::vector<json> integers = boundary_integers();
for (const std::size_t length :
{
std::size_t{1}, std::size_t{27}, std::size_t{28}, std::size_t{29}, std::size_t{57}, std::size_t{1000}
})
{
for (const std::size_t break_every :
{
std::size_t{0}, std::size_t{1}, std::size_t{7}, std::size_t{28}
})
{
CAPTURE(length)
CAPTURE(break_every)
json array = json::array();
std::vector<std::uint8_t> expected_cbor;
std::vector<std::uint8_t> expected_msgpack;
for (std::size_t i = 0; i < length; ++i)
{
if (break_every != 0 && i % (break_every + 1) == break_every)
{
array.push_back("x");
expected_cbor.push_back(0x61);
expected_cbor.push_back('x');
expected_msgpack.push_back(0xA1);
expected_msgpack.push_back('x');
}
else
{
const json& value = integers[(i * 7) % integers.size()];
array.push_back(value);
reference_cbor_integer(expected_cbor, value);
reference_msgpack_integer(expected_msgpack, value);
}
}
std::vector<std::uint8_t> head;
reference_cbor_head(head, 0x80, length);
expected_cbor.insert(expected_cbor.begin(), head.begin(), head.end());
head.clear();
if (length <= 15)
{
head.push_back(static_cast<std::uint8_t>(0x90 + length));
}
else
{
head = {0xDC, static_cast<std::uint8_t>(length >> 8), static_cast<std::uint8_t>(length)};
}
expected_msgpack.insert(expected_msgpack.begin(), head.begin(), head.end());
CHECK(json::to_cbor(array) == expected_cbor);
CHECK(json::to_msgpack(array) == expected_msgpack);
std::vector<std::uint8_t> through_adapter = {0xAA};
json::to_cbor(array, through_adapter);
CHECK(std::vector<std::uint8_t>(through_adapter.begin() + 1, through_adapter.end()) == expected_cbor);
through_adapter = {0xAA};
json::to_msgpack(array, through_adapter);
CHECK(std::vector<std::uint8_t>(through_adapter.begin() + 1, through_adapter.end()) == expected_msgpack);
std::ostringstream stream;
json::to_cbor(array, stream);
std::string streamed = stream.str();
CHECK(to_bytes(streamed) == expected_cbor);
stream.str("");
json::to_msgpack(array, stream);
streamed = stream.str();
CHECK(to_bytes(streamed) == expected_msgpack);
CHECK(json::from_cbor(expected_cbor) == array);
CHECK(json::from_msgpack(expected_msgpack) == array);
// the same array as object members
const json object = {{"a", array}, {"b", array}};
CHECK(json::from_cbor(json::to_cbor(object)) == object);
CHECK(json::from_msgpack(json::to_msgpack(object)) == object);
}
}
// single values, outside an array
for (const auto& value : integers)
{
CAPTURE(value)
std::vector<std::uint8_t> expected;
reference_cbor_integer(expected, value);
CHECK(json::to_cbor(value) == expected);
expected.clear();
reference_msgpack_integer(expected, value);
CHECK(json::to_msgpack(value) == expected);
}
}
// binary_reserve_hint() is documented as a *lower* bound on the serialized size,
// so that reserving it up front can never leave the returned vector holding
// capacity beyond what the value actually needs.
+46
View File
@@ -9,7 +9,9 @@
#include "doctest_compatibility.h"
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <cstdint>
#include <deque>
#include <map>
#include <memory>
@@ -164,3 +166,47 @@ TEST_CASE("array type without at()")
CHECK(j.at(no_at_json::json_pointer("/1")) == 2);
CHECK_THROWS_AS(j.at(no_at_json::json_pointer("/3")), no_at_json::out_of_range);
}
TEST_CASE("regression test #5476 - array type without reserve()")
{
// the capacity reserved for definite-length arrays must not require the
// array type to have a reserve() member function
SECTION("std::deque")
{
const auto j = deque_json::parse(R"({"a":[1,[2,3]],"b":[]})");
CHECK(j.dump() == R"({"a":[1,[2,3]],"b":[]})");
// the binary formats pass a definite length to start_array()
CHECK(deque_json::from_cbor(deque_json::to_cbor(j)) == j);
CHECK(deque_json::from_msgpack(deque_json::to_msgpack(j)) == j);
CHECK(deque_json::from_bon8(deque_json::to_bon8(j)) == j);
// parse() instantiates the callback parser as well, which reserves too
const auto with_callback = deque_json::parse(R"([1,2,3])", [](int /*depth*/, deque_json::parse_event_t /*event*/, deque_json& /*parsed*/) noexcept
{
return true;
});
CHECK(with_callback == deque_json({1, 2, 3}));
}
SECTION("std::vector still reserves")
{
json array = json::array();
for (int i = 0; i < 100; ++i)
{
array.push_back(i);
}
const auto j = json::from_cbor(json::to_cbor(array));
CHECK(j == array);
CHECK(j.get_ref<const json::array_t&>().capacity() >= 100);
}
SECTION("the reservation stays capped")
{
// CBOR array announcing 2^32-1 elements, but truncated right after the
// header: the input must be rejected without reserving that capacity
const std::vector<std::uint8_t> truncated = {0x9A, 0xFF, 0xFF, 0xFF, 0xFF};
CHECK(json::from_cbor(truncated, true, false).is_discarded());
}
}
+367
View File
@@ -0,0 +1,367 @@
// __ _____ _____ _____
// __| | __| | | | 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>
#include <cstdint>
#include <limits>
#include <map>
#include <string>
#include <type_traits>
#include <vector>
#if JSON_HAS_THREE_WAY_COMPARISON
#include <compare>
#endif
namespace custom_number_types
{
// a signed integer of class type: std::is_signed is only true for arithmetic
// types, so the library has to take the signedness from std::numeric_limits,
// as it does for 128-bit and multiprecision integer classes such as
// absl::int128 or boost::multiprecision::cpp_int
class class_int
{
public:
// trivial, like the 128-bit integer classes: the value is stored in a union
class_int() = default;
// implicit from built-in integers and explicit from floats, like absl::int128;
// non-template overloads (as absl::int128 has) because clang 3.4 and 3.5
// reject basic_json's defaulted data() for a number type with template
// constructors
class_int(int v) : value(v) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions)
class_int(unsigned v) : value(v) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions)
class_int(long v) : value(v) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions)
class_int(unsigned long v) : value(static_cast<std::int64_t>(v)) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions)
class_int(long long v) : value(v) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions)
class_int(unsigned long long v) : value(static_cast<std::int64_t>(v)) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions)
explicit class_int(double v) : value(static_cast<std::int64_t>(v)) {}
template<typename T, typename std::enable_if<std::is_arithmetic<T>::value, int>::type = 0>
explicit operator T() const
{
return static_cast<T>(value);
}
friend bool operator==(class_int lhs, class_int rhs)
{
return lhs.value == rhs.value;
}
friend bool operator!=(class_int lhs, class_int rhs)
{
return lhs.value != rhs.value;
}
friend bool operator<(class_int lhs, class_int rhs)
{
return lhs.value < rhs.value;
}
#if JSON_HAS_THREE_WAY_COMPARISON
friend std::strong_ordering operator<=>(class_int lhs, class_int rhs) // *NOPAD*
{
return lhs.value <=> rhs.value; // *NOPAD*
}
#endif
private:
std::int64_t value;
};
} // namespace custom_number_types
using custom_number_types::class_int;
namespace std
{
// only the members the library uses
template<>
class numeric_limits<class_int>
{
public:
static constexpr bool is_signed = true;
static constexpr int digits = std::numeric_limits<std::int64_t>::digits;
};
} // namespace std
namespace
{
using class_int_json = nlohmann::basic_json<std::map, std::vector, std::string, bool, class_int, std::uint64_t, double>;
class_int_json make_class_int(std::int64_t v)
{
class_int_json j(class_int_json::value_t::number_integer);
j.get_ref<class_int&>() = class_int(v);
return j;
}
} // namespace
// the serializer cannot print an integer of class type, so doctest must not
// try when an assertion fails
namespace doctest
{
template<>
struct StringMaker<class_int_json>
{
static String convert(const class_int_json& j)
{
return j.type_name();
}
};
} // namespace doctest
// __int128 as number type needs the std::numeric_limits (for the range checks)
// and std::is_integral (for the serializer) specializations, which libc++
// always provides and libstdc++ only outside strict ISO modes; the MSVC
// standard library has none
#if defined(__SIZEOF_INT128__) && (defined(_LIBCPP_VERSION) || defined(__GLIBCXX_TYPE_INT_N_0))
#define JSON_TEST_INT128_NUMBER_TYPES 1
#else
#define JSON_TEST_INT128_NUMBER_TYPES 0
#endif
#if JSON_TEST_INT128_NUMBER_TYPES
namespace
{
// __extension__ keeps -Wpedantic from flagging the non-standard type
__extension__ typedef __int128 int128; // NOLINT(modernize-use-using)
__extension__ typedef unsigned __int128 uint128; // NOLINT(modernize-use-using)
static_assert(std::numeric_limits<int128>::digits == 127 && std::numeric_limits<uint128>::digits == 128 &&
std::is_integral<int128>::value && std::is_integral<uint128>::value, // NOLINT(misc-redundant-expression)
"__int128 is not fully supported by the standard library");
using wide_json = nlohmann::basic_json<std::map, std::vector, std::string, bool, int128, uint128, double>;
wide_json make_int(int128 v)
{
wide_json j(wide_json::value_t::number_integer);
j.get_ref<int128&>() = v;
return j;
}
wide_json make_uint(uint128 v)
{
wide_json j(wide_json::value_t::number_unsigned);
j.get_ref<uint128&>() = v;
return j;
}
wide_json wrap(const wide_json& value)
{
wide_json o(wide_json::value_t::object);
o["v"] = value;
return o;
}
} // namespace
TEST_CASE("custom number types: integers beyond 64 bits")
{
using out_of_range = wide_json::out_of_range;
const int128 two_64 = static_cast<int128>(1) << 64;
const int128 two_100 = static_cast<int128>(1) << 100;
const int128 max_int64 = (std::numeric_limits<std::int64_t>::max)();
const int128 min_int64 = (std::numeric_limits<std::int64_t>::min)();
const uint128 max_uint64 = (std::numeric_limits<std::uint64_t>::max)();
// before the range checks, these values were silently truncated, e.g.,
// 2^100 was written as 0
const wide_json int_big = make_int(two_100);
const wide_json int_big_negative = make_int(-two_100);
const wide_json uint_big = make_uint(static_cast<uint128>(two_100));
std::vector<std::uint8_t> _;
SECTION("CBOR")
{
CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(int_big), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by CBOR as it does not fit [-2^64, 2^64-1]", out_of_range&);
CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(int_big_negative), "[json.exception.out_of_range.407] integer number -1267650600228229401496703205376 cannot be represented by CBOR as it does not fit [-2^64, 2^64-1]", out_of_range&);
CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(uint_big), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by CBOR as it does not fit uint64", out_of_range&);
// the extremes of the 128-bit types, whose digits are produced without
// negating the minimum as a whole
CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(make_int((std::numeric_limits<int128>::min)())), "[json.exception.out_of_range.407] integer number -170141183460469231731687303715884105728 cannot be represented by CBOR as it does not fit [-2^64, 2^64-1]", out_of_range&);
CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(make_int((std::numeric_limits<int128>::max)())), "[json.exception.out_of_range.407] integer number 170141183460469231731687303715884105727 cannot be represented by CBOR as it does not fit [-2^64, 2^64-1]", out_of_range&);
CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(make_uint((std::numeric_limits<uint128>::max)())), "[json.exception.out_of_range.407] integer number 340282366920938463463374607431768211455 cannot be represented by CBOR as it does not fit uint64", out_of_range&);
CHECK_THROWS_AS(_ = wide_json::to_cbor(make_int(two_64)), out_of_range&);
CHECK_THROWS_AS(_ = wide_json::to_cbor(make_int(-two_64 - 1)), out_of_range&);
CHECK_THROWS_AS(_ = wide_json::to_cbor(make_uint(static_cast<uint128>(max_uint64) + 1)), out_of_range&);
// CBOR's integers cover [-2^64, 2^64-1]
CHECK(wide_json::to_cbor(make_int(two_64 - 1)) == std::vector<std::uint8_t>({0x1B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}));
CHECK(wide_json::to_cbor(make_int(-two_64)) == std::vector<std::uint8_t>({0x3B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}));
CHECK(wide_json::to_cbor(make_uint(max_uint64)) == std::vector<std::uint8_t>({0x1B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}));
}
SECTION("MessagePack")
{
CHECK_THROWS_WITH_AS(_ = wide_json::to_msgpack(int_big), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by MessagePack as it does not fit [-2^63, 2^64-1]", out_of_range&);
CHECK_THROWS_WITH_AS(_ = wide_json::to_msgpack(int_big_negative), "[json.exception.out_of_range.407] integer number -1267650600228229401496703205376 cannot be represented by MessagePack as it does not fit [-2^63, 2^64-1]", out_of_range&);
CHECK_THROWS_WITH_AS(_ = wide_json::to_msgpack(uint_big), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by MessagePack as it does not fit uint64", out_of_range&);
CHECK_THROWS_AS(_ = wide_json::to_msgpack(make_int(two_64)), out_of_range&);
CHECK_THROWS_AS(_ = wide_json::to_msgpack(make_int(min_int64 - 1)), out_of_range&);
// MessagePack's integers cover [-2^63, 2^64-1]
CHECK(wide_json::to_msgpack(make_int(two_64 - 1)) == std::vector<std::uint8_t>({0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}));
CHECK(wide_json::to_msgpack(make_int(min_int64)) == std::vector<std::uint8_t>({0xD3, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}));
CHECK(wide_json::to_msgpack(make_uint(max_uint64)) == std::vector<std::uint8_t>({0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}));
}
SECTION("BSON")
{
// wrap() stores the value at /v, which JSON_DIAGNOSTICS adds to the message
#if JSON_DIAGNOSTICS
CHECK_THROWS_WITH_AS(_ = wide_json::to_bson(wrap(int_big)), "[json.exception.out_of_range.407] (/v) integer number 1267650600228229401496703205376 cannot be represented by BSON as it does not fit int64", out_of_range&);
CHECK_THROWS_WITH_AS(_ = wide_json::to_bson(wrap(int_big_negative)), "[json.exception.out_of_range.407] (/v) integer number -1267650600228229401496703205376 cannot be represented by BSON as it does not fit int64", out_of_range&);
CHECK_THROWS_WITH_AS(_ = wide_json::to_bson(wrap(uint_big)), "[json.exception.out_of_range.407] (/v) integer number 1267650600228229401496703205376 cannot be represented by BSON as it does not fit uint64", out_of_range&);
#else
CHECK_THROWS_WITH_AS(_ = wide_json::to_bson(wrap(int_big)), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by BSON as it does not fit int64", out_of_range&);
CHECK_THROWS_WITH_AS(_ = wide_json::to_bson(wrap(int_big_negative)), "[json.exception.out_of_range.407] integer number -1267650600228229401496703205376 cannot be represented by BSON as it does not fit int64", out_of_range&);
CHECK_THROWS_WITH_AS(_ = wide_json::to_bson(wrap(uint_big)), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by BSON as it does not fit uint64", out_of_range&);
#endif
CHECK_THROWS_AS(_ = wide_json::to_bson(wrap(make_int(max_int64 + 1))), out_of_range&);
CHECK_THROWS_AS(_ = wide_json::to_bson(wrap(make_int(min_int64 - 1))), out_of_range&);
// nested values are checked as well, before anything is written
wide_json nested(wide_json::value_t::object);
nested["a"] = wide_json::array({wrap(int_big)});
std::vector<std::uint8_t> out;
CHECK_THROWS_AS(wide_json::to_bson(nested, out), out_of_range&);
CHECK(out.empty());
CHECK(wide_json::from_bson(wide_json::to_bson(wrap(make_int(max_int64)))) == wrap(make_int(max_int64)));
CHECK(wide_json::from_bson(wide_json::to_bson(wrap(make_int(min_int64)))) == wrap(make_int(min_int64)));
CHECK_NOTHROW(wide_json::to_bson(wrap(make_uint(max_uint64))));
}
SECTION("BON8")
{
CHECK_THROWS_WITH_AS(_ = wide_json::to_bon8(int_big), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by BON8 as it does not fit int64", out_of_range&);
CHECK_THROWS_WITH_AS(_ = wide_json::to_bon8(int_big_negative), "[json.exception.out_of_range.407] integer number -1267650600228229401496703205376 cannot be represented by BON8 as it does not fit int64", out_of_range&);
CHECK_THROWS_WITH_AS(_ = wide_json::to_bon8(uint_big), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by BON8 as it does not fit int64", out_of_range&);
CHECK_THROWS_AS(_ = wide_json::to_bon8(make_int(max_int64 + 1)), out_of_range&);
CHECK_THROWS_AS(_ = wide_json::to_bon8(make_int(min_int64 - 1)), out_of_range&);
CHECK(wide_json::from_bon8(wide_json::to_bon8(make_int(max_int64))) == make_int(max_int64));
CHECK(wide_json::from_bon8(wide_json::to_bon8(make_int(min_int64))) == make_int(min_int64));
}
SECTION("UBJSON and BJData")
{
// integers beyond 64 bits are written exactly as high-precision numbers
const std::string digits = "1267650600228229401496703205376";
std::vector<std::uint8_t> expected = {'H', 'i', static_cast<std::uint8_t>(digits.size())};
expected.insert(expected.end(), digits.begin(), digits.end());
CHECK(wide_json::to_ubjson(int_big) == expected);
CHECK(wide_json::to_ubjson(uint_big) == expected);
CHECK(wide_json::to_bjdata(int_big) == expected);
// BJData's uint64 marker 'M' was used for any unsigned value beyond
// int64, which truncated the ones beyond 64 bits
CHECK(wide_json::to_bjdata(uint_big) == expected);
// the uint64 marker is still used where the value fits
CHECK(wide_json::to_bjdata(make_uint(max_uint64)) == std::vector<std::uint8_t>({'M', 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}));
// the elements of an optimized container are written the same way;
// BJData does not allow 'H' as the type of an optimized container
std::vector<std::uint8_t> expected_ubjson_array = {'[', '$', 'H', '#', 'i', 2};
std::vector<std::uint8_t> expected_bjdata_array = {'[', '#', 'i', 2};
for (int i = 0; i < 2; ++i)
{
expected_ubjson_array.insert(expected_ubjson_array.end(), expected.begin() + 1, expected.end());
expected_bjdata_array.insert(expected_bjdata_array.end(), expected.begin(), expected.end());
}
CHECK(wide_json::to_ubjson(wide_json::array({uint_big, uint_big}), true, true) == expected_ubjson_array);
CHECK(wide_json::to_bjdata(wide_json::array({uint_big, uint_big}), true, true) == expected_bjdata_array);
}
SECTION("BJData ND-array")
{
// an ND-array element beyond 64 bits does not fit any dtype, so the
// annotated object is written as a plain object instead of truncating
// the element into range
wide_json element(wide_json::value_t::object);
element["_ArrayType_"] = "uint8";
element["_ArraySize_"] = wide_json::array({make_uint(2), make_uint(1)});
element["_ArrayData_"] = wide_json::array({make_uint(1), make_uint(static_cast<uint128>(two_64) + 1)});
const auto element_bytes = wide_json::to_bjdata(element, true, true);
REQUIRE(!element_bytes.empty());
CHECK(element_bytes[0] == '{');
wide_json signed_element = element;
signed_element["_ArrayType_"] = "int8";
signed_element["_ArrayData_"] = wide_json::array({make_int(1), make_int(-two_64 + 1)});
const auto signed_element_bytes = wide_json::to_bjdata(signed_element, true, true);
REQUIRE(!signed_element_bytes.empty());
CHECK(signed_element_bytes[0] == '{');
// the same for a dimension beyond 64 bits, which wrapped into a
// dimension matching the size of _ArrayData_ (here: 1)
wide_json dimension = element;
dimension["_ArraySize_"] = wide_json::array({make_uint(2), make_uint(static_cast<uint128>(two_64) + 1)});
dimension["_ArrayData_"] = wide_json::array({make_uint(1), make_uint(2)});
const auto dimension_bytes = wide_json::to_bjdata(dimension, true, true);
REQUIRE(!dimension_bytes.empty());
CHECK(dimension_bytes[0] == '{');
// in range, the ND-array is still written
wide_json fits = element;
fits["_ArrayData_"] = wide_json::array({make_uint(1), make_uint(2)});
const auto fits_bytes = wide_json::to_bjdata(fits, true, true);
REQUIRE(!fits_bytes.empty());
CHECK(fits_bytes[0] == '[');
}
}
#endif
TEST_CASE("custom number types")
{
SECTION("signed integer of class type compared with a float")
{
// std::is_signed is false for a class type, which made the comparison
// treat any float below zero as less than every integer
const class_int_json minus_five = make_class_int(-5);
const class_int_json minus_two = make_class_int(-2);
const class_int_json five = make_class_int(5);
const class_int_json minus_two_and_a_half = -2.5;
const class_int_json two_and_a_half = 2.5;
const class_int_json huge_negative = -1e30;
const class_int_json huge_positive = 1e30;
CHECK(minus_five < minus_two_and_a_half);
CHECK_FALSE(minus_two_and_a_half < minus_five);
CHECK(minus_two_and_a_half > minus_five);
CHECK(minus_five <= minus_two_and_a_half);
CHECK(minus_two_and_a_half >= minus_five);
CHECK(minus_five != minus_two_and_a_half);
CHECK(minus_two_and_a_half < minus_two);
CHECK_FALSE(minus_two < minus_two_and_a_half);
CHECK(two_and_a_half < five);
CHECK(minus_five < two_and_a_half);
// floats beyond the integer's range
CHECK(huge_negative < minus_five);
CHECK_FALSE(minus_five < huge_negative);
CHECK(five < huge_positive);
CHECK_FALSE(huge_positive < five);
// equality
const class_int_json minus_two_float = -2.0;
CHECK(minus_two == minus_two_float);
CHECK(minus_two_float == minus_two);
}
}
+239
View File
@@ -0,0 +1,239 @@
// __ _____ _____ _____
// __| | __| | | | 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 <array>
#include <cstdint>
#include <cstdio>
#include <fstream>
#include <memory>
#include <random>
#include <string>
#include <thread>
#include <vector>
// Parsing from a FILE* locks the file once and reads it with the unlocked
// stdio functions (see JSON_DISABLE_UNLOCKED_FILE_IO). These tests pin that
// this is not observable: the results are the same, the file is unlocked
// again when parsing ends - also with an exception - and the same thread can
// keep using the file while it is locked, e.g., from a SAX callback.
namespace
{
// a file name of its own for every run: the test runs for several C++
// standards and configurations at the same time, in the same directory
std::string temp_file_name()
{
std::random_device rd;
return "unit-file_input-" + std::to_string(rd()) + ".tmp";
}
using file_ptr = std::unique_ptr<std::FILE, int (*)(std::FILE*)>;
// write @a content to the file @a name and open it for reading
file_ptr open_with(const char* name, const std::string& content)
{
{
std::ofstream file(name, std::ios::binary);
file << content;
}
return {std::fopen(name, "rb"), &std::fclose};
}
// The stdio lock is recursive, so whether parsing released it can only be
// seen from another thread: ftrylockfile() fails there while this thread
// holds the lock. ftrylockfile() is POSIX; elsewhere, the check is skipped.
#if defined(__GLIBC__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
void check_unlocked(std::FILE* f)
{
int result = -1;
std::thread other([f, &result]
{
result = ftrylockfile(f);
if (result == 0)
{
funlockfile(f);
}
});
other.join();
CHECK(result == 0);
}
#else
void check_unlocked(std::FILE* /*f*/) {}
#endif
// SAX consumer that also reads the position of the file it is parsing, which
// takes the file's lock again on the same thread
class position_reading_sax : public nlohmann::json_sax<json>
{
public:
explicit position_reading_sax(std::FILE* f) : m_file(f) {}
position_reading_sax(const position_reading_sax&) = delete;
position_reading_sax(position_reading_sax&&) = delete;
position_reading_sax& operator=(const position_reading_sax&) = delete;
position_reading_sax& operator=(position_reading_sax&&) = delete;
~position_reading_sax() override = default;
bool null() override
{
return record();
}
bool boolean(bool /*val*/) override
{
return record();
}
bool number_integer(number_integer_t /*val*/) override
{
return record();
}
bool number_unsigned(number_unsigned_t /*val*/) override
{
return record();
}
bool number_float(number_float_t /*val*/, const string_t& /*s*/) override
{
return record();
}
bool string(string_t& /*val*/) override
{
return record();
}
bool binary(binary_t& /*val*/) override
{
return record();
}
bool start_object(std::size_t /*elements*/) override
{
return record();
}
bool key(string_t& /*val*/) override
{
return record();
}
bool end_object() override
{
return record();
}
bool start_array(std::size_t /*elements*/) override
{
return record();
}
bool end_array() override
{
return record();
}
bool parse_error(std::size_t /*position*/, const std::string& /*last_token*/, const nlohmann::detail::exception& /*ex*/) override
{
return false;
}
const std::vector<long>& positions() const
{
return m_positions;
}
private:
bool record()
{
m_positions.push_back(std::ftell(m_file));
return true;
}
std::FILE* m_file;
std::vector<long> m_positions{}; // NOLINT(readability-redundant-member-init)
};
} // namespace
TEST_CASE("parsing from a FILE*")
{
const std::string temp_name = temp_file_name();
const char* const name = temp_name.c_str();
const std::string input = "{\"a\": [1, -2.5, \"three\", true, null], \"b\": {\"c\": \"caf\xc3\xa9\"}}";
SECTION("values are the same as when parsing from a string")
{
const file_ptr f = open_with(name, input);
REQUIRE(f != nullptr);
CHECK(json::parse(f.get()) == json::parse(input));
// the whole input was consumed
std::array<char, 8> rest{};
CHECK(std::fgets(rest.data(), static_cast<int>(rest.size()), f.get()) == nullptr);
check_unlocked(f.get());
}
#if !defined(JSON_NOEXCEPTION)
SECTION("the file is unlocked after a parse error")
{
const file_ptr f = open_with(name, "[1, 2, x]");
REQUIRE(f != nullptr);
json _;
CHECK_THROWS_WITH_AS(_ = json::parse(f.get()),
"[json.exception.parse_error.101] parse error at line 1, column 8: syntax error while parsing value - invalid literal; last read: '2, x'",
json::parse_error&);
check_unlocked(f.get());
// the file is still usable: the rest of the input is there
std::array<char, 8> rest{};
CHECK(std::fgets(rest.data(), static_cast<int>(rest.size()), f.get()) != nullptr);
CHECK(std::string(rest.data()) == "]");
}
#endif // !defined(JSON_NOEXCEPTION)
SECTION("the file can be parsed again and read with stdio afterwards")
{
const file_ptr f = open_with(name, "[1] [2] tail");
REQUIRE(f != nullptr);
// sax_parse with strict == false stops after the first value; an array
// ends with its closing bracket, so the space after it is not read
json j;
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> sax1(j);
CHECK(json::sax_parse(f.get(), &sax1, json::input_format_t::json, false));
CHECK(j == json({1}));
check_unlocked(f.get());
json k;
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> sax2(k);
CHECK(json::sax_parse(f.get(), &sax2, json::input_format_t::json, false));
CHECK(k == json({2}));
check_unlocked(f.get());
std::array<char, 8> rest{};
CHECK(std::fgets(rest.data(), static_cast<int>(rest.size()), f.get()) != nullptr);
CHECK(std::string(rest.data()) == " tail");
}
SECTION("the same thread can use the file while it is parsed")
{
const file_ptr f = open_with(name, "[10, 20, 30]");
REQUIRE(f != nullptr);
position_reading_sax sax(f.get());
CHECK(json::sax_parse(f.get(), &sax));
// start_array, three numbers, end_array
CHECK(sax.positions().size() == 5);
CHECK(sax.positions().front() == 1);
check_unlocked(f.get());
}
SECTION("binary formats read from a FILE*")
{
const json j = {{"string", std::string(300, 'x')}, {"array", {1, 2, 3}}, {"number", 1.5}};
const std::vector<std::uint8_t> cbor = json::to_cbor(j);
const file_ptr f = open_with(name, std::string(reinterpret_cast<const char*>(cbor.data()), cbor.size()));
REQUIRE(f != nullptr);
CHECK(json::from_cbor(f.get()) == j);
check_unlocked(f.get());
}
static_cast<void>(std::remove(name));
}
+503
View File
@@ -0,0 +1,503 @@
// __ _____ _____ _____
// __| | __| | | | 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
// The fast paths for contiguous input (bulk string scanning, SWAR searches,
// UTF-8 prefix validation, the 8-digit number loop, the 8-byte indentation
// skip, the BSON/BON8 bulk string readers) load several bytes at a time. This
// test places every input directly in front of (or directly behind) an
// inaccessible page, so reading even one byte outside the input crashes, with
// or without sanitizers. ASan alone misses over-reads into the slack of the
// same heap allocation.
#include "doctest_compatibility.h"
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <string>
#include <utility>
#include <vector>
#if defined(_WIN32)
#define JSON_TEST_GUARD_PAGE_WINDOWS
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#elif defined(__unix__) || defined(__APPLE__)
#define JSON_TEST_GUARD_PAGE_POSIX
#include <sys/mman.h>
#include <unistd.h>
#endif
#if defined(JSON_TEST_GUARD_PAGE_WINDOWS) || defined(JSON_TEST_GUARD_PAGE_POSIX)
namespace
{
// Three pages: an inaccessible one, a readable one, an inaccessible one. An
// input is copied either to the end of the middle page (any read past its last
// byte faults) or to its start (any read before its first byte faults).
class guarded_buffer
{
public:
guarded_buffer()
: page(page_size())
{
#if defined(JSON_TEST_GUARD_PAGE_WINDOWS)
void* const p = VirtualAlloc(nullptr, 3 * page, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS);
REQUIRE(p != nullptr);
base = static_cast<unsigned char*>(p);
DWORD old_protect = 0;
REQUIRE(VirtualProtect(base + page, page, PAGE_READWRITE, &old_protect) != 0);
#else
void* const p = mmap(nullptr, 3 * page, PROT_NONE, MAP_PRIVATE | MAP_ANON, -1, 0);
REQUIRE(p != MAP_FAILED);
base = static_cast<unsigned char*>(p);
REQUIRE(mprotect(base + page, page, PROT_READ | PROT_WRITE) == 0);
#endif
}
~guarded_buffer()
{
#if defined(JSON_TEST_GUARD_PAGE_WINDOWS)
VirtualFree(base, 0, MEM_RELEASE);
#else
munmap(base, 3 * page);
#endif
}
guarded_buffer(const guarded_buffer&) = delete;
guarded_buffer& operator=(const guarded_buffer&) = delete;
guarded_buffer(guarded_buffer&&) = delete;
guarded_buffer& operator=(guarded_buffer&&) = delete;
// copies @a input so its last byte is the last readable byte
const std::uint8_t* at_end(const std::string& input)
{
REQUIRE(input.size() <= page);
unsigned char* const first = base + (2 * page) - input.size();
std::transform(input.begin(), input.end(), first, [](const char c)
{
return static_cast<unsigned char>(c);
});
return first;
}
// copies @a input so its first byte is the first readable byte
const std::uint8_t* at_start(const std::string& input)
{
REQUIRE(input.size() <= page);
unsigned char* const first = base + page;
std::transform(input.begin(), input.end(), first, [](const char c)
{
return static_cast<unsigned char>(c);
});
return first;
}
private:
static std::size_t page_size()
{
#if defined(JSON_TEST_GUARD_PAGE_WINDOWS)
SYSTEM_INFO info;
GetSystemInfo(&info);
return static_cast<std::size_t>(info.dwPageSize);
#else
return static_cast<std::size_t>(sysconf(_SC_PAGESIZE));
#endif
}
std::size_t page = 0;
unsigned char* base = nullptr;
};
// the serialization of the result of @a f, or the exception message; @a f
// takes the allow_exceptions flag, which is false without exceptions, where
// an error yields a discarded value instead
template<typename F>
std::string outcome(F f)
{
#if defined(JSON_NOEXCEPTION)
return "value: " + f(false).dump(-1, ' ', false, json::error_handler_t::keep);
#else
try
{
return "value: " + f(true).dump(-1, ' ', false, json::error_handler_t::keep);
}
catch (const json::exception& e)
{
return std::string("error: ") + e.what();
}
#endif
}
// a SAX handler that only counts events, so sax_parse runs without a DOM
struct counting_sax : nlohmann::json_sax<json>
{
std::size_t events = 0;
std::string error{}; // NOLINT(readability-redundant-member-init)
bool null() override
{
return count();
}
bool boolean(bool /*val*/) override
{
return count();
}
bool number_integer(number_integer_t /*val*/) override
{
return count();
}
bool number_unsigned(number_unsigned_t /*val*/) override
{
return count();
}
bool number_float(number_float_t /*val*/, const string_t& /*s*/) override
{
return count();
}
bool string(string_t& /*val*/) override
{
return count();
}
bool binary(binary_t& /*val*/) override
{
return count();
}
bool start_object(std::size_t /*elements*/) override
{
return count();
}
bool key(string_t& /*val*/) override
{
return count();
}
bool end_object() override
{
return count();
}
bool start_array(std::size_t /*elements*/) override
{
return count();
}
bool end_array() override
{
return count();
}
bool parse_error(std::size_t position, const std::string& /*last_token*/, const nlohmann::detail::exception& ex) override
{
error = std::to_string(position) + " " + ex.what();
return false;
}
private:
bool count()
{
++events;
return true;
}
};
// all prefixes of each seed, so the input ends at every byte of it
std::vector<std::string> all_prefixes(const std::vector<std::string>& seeds)
{
std::vector<std::string> result;
for (const auto& seed : seeds)
{
for (std::size_t n = 0; n <= seed.size(); ++n)
{
result.push_back(seed.substr(0, n));
}
}
return result;
}
std::vector<std::string> json_seeds()
{
std::vector<std::string> seeds =
{
// string runs: ASCII, escapes, \u escapes, surrogate pairs
"\"abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\"",
R"("ab\ncd\"ef\\gh\u00e4ij\uD83D\uDE00kl\u12")",
// 2-, 3- and 4-byte UTF-8 sequences, then invalid UTF-8
std::string("\"\xC3\xA4\xE2\x82\xAC\xF0\x9F\x98\x80\xC3\xA4\xE2\x82\xAC\xF0\x9F\x98\x80") + "abcdefgh\"",
"\"abcdefgh\xC3\x28 \xED\xA0\x80 \xF4\x90\x80\x80 \xF8 \xC0\xAF\"",
std::string("\"abcdefg\x01") + "abcdefgh\"",
// numbers, so the 8-digit loop runs up to the end
"12345678",
"1234567890123456",
"12345678901234567890123",
"-0.000000001234567890123456789",
"3.14159265358979323846264338327950288e+100",
"-1.2345678901234567e-12",
"1E-5",
"[1.5,2e10,-3E-5,0.000000001,123456789012345678]",
// literals
"[true,false,null]",
// comments (only parsed with ignore_comments)
"/* comment */ [1, // line comment\n 2] /**/",
// a pretty-printed document
json::parse(R"({"a": [1, 2.5, "three", {"b": null, "c": [true, false]}], "d": {"e": {"f": {"g": "h"}}}})").dump(4),
json::parse(R"({"a": [1, 2.5, "three", {"b": null, "c": [true, false]}], "d": {"e": {"f": {"g": "h"}}}})").dump(8, '\t'),
};
// indentation runs of every length, with and without a closing bracket
for (std::size_t n = 0; n <= 20; ++n)
{
seeds.push_back("[\n" + std::string(n, ' ') + "1,\n" + std::string(n, ' ') + "2\n]");
}
return all_prefixes(seeds);
}
// a value whose binary serializations contain long strings and keys, numbers
// of every width, and nested containers
json binary_document()
{
json j;
j["a key that is longer than eight bytes"] = "a string value that is longer than eight bytes";
j["\xC3\xA4\xE2\x82\xAC\xF0\x9F\x98\x80"] = "\xF0\x9F\x98\x80\xE2\x82\xAC\xC3\xA4 utf-8 0123456789";
j["numbers"] = {0, 1, -1, 255, -256, 65536, -2147483649LL, 4294967296ULL, 1.5, -2.25e100};
j["nested"] = {{"x", {{"y", {true, false, nullptr}}}}};
j["s"] = "";
return j;
}
// readers for check_binary(), callable with a container or an iterator range
// followed by the allow_exceptions flag
#define JSON_TEST_GUARD_PAGE_READER(name, from) \
struct name \
{ \
template<typename... Args> \
json operator()(bool allow_exceptions, Args&& ... args) const \
{ \
return json::from(std::forward<Args>(args)..., true, allow_exceptions); \
} \
};
JSON_TEST_GUARD_PAGE_READER(read_bson, from_bson)
JSON_TEST_GUARD_PAGE_READER(read_bon8, from_bon8)
JSON_TEST_GUARD_PAGE_READER(read_cbor, from_cbor)
JSON_TEST_GUARD_PAGE_READER(read_msgpack, from_msgpack)
JSON_TEST_GUARD_PAGE_READER(read_ubjson, from_ubjson)
JSON_TEST_GUARD_PAGE_READER(read_bjdata, from_bjdata)
#undef JSON_TEST_GUARD_PAGE_READER
// @a bytes as characters (explicitly converted: the iterator-range constructor
// would convert implicitly, which -fsanitize=integer reports)
std::string as_chars(const std::vector<std::uint8_t>& bytes)
{
std::string result;
result.reserve(bytes.size());
for (const std::uint8_t b : bytes)
{
result.push_back(static_cast<char>(b));
}
return result;
}
// reads every prefix of @a bytes at both ends of the guarded page and from a
// std::string, which must give the same outcome
template<typename Reader>
void check_binary(guarded_buffer& guard, const std::vector<std::uint8_t>& bytes, Reader parse)
{
for (const auto& input : all_prefixes({as_chars(bytes)}))
{
CAPTURE(input)
const std::string expected = outcome([&](bool allow)
{
return parse(allow, input);
});
for (int placement = 0; placement < 2; ++placement)
{
const std::uint8_t* const first = placement == 0 ? guard.at_end(input) : guard.at_start(input);
CHECK(outcome([&](bool allow)
{
return parse(allow, first, first + input.size());
}) == expected);
}
}
}
// parse_float_native() at both ends of the guarded page, compared bit for bit
// with converting the same token from a std::string
template<typename FloatType>
std::uint64_t float_bits(FloatType value)
{
static_assert(sizeof(FloatType) <= sizeof(std::uint64_t), "unsupported floating-point width");
std::uint64_t bits = 0;
std::memcpy(&bits, &value, sizeof(value));
return bits;
}
template<typename FloatType>
void check_float_native(guarded_buffer& guard, const std::string& token)
{
const auto decimal_point = token.find('.');
const auto exponent = token.find_first_of("eE");
const auto mantissa_end = exponent == std::string::npos ? token.size() : exponent;
const auto expected = nlohmann::detail::parse_float_native<FloatType>(token.data(), token.data() + token.size(), decimal_point, mantissa_end);
for (int placement = 0; placement < 2; ++placement)
{
const char* const first = reinterpret_cast<const char*>(placement == 0 ? guard.at_end(token) : guard.at_start(token));
const auto value = nlohmann::detail::parse_float_native<FloatType>(first, first + token.size(), decimal_point, mantissa_end);
CHECK(float_bits(value) == float_bits(expected));
}
}
} // namespace
TEST_CASE("guard page")
{
guarded_buffer guard;
SECTION("string scanning")
{
// fills that drive each function to a different stop
const std::vector<std::string> fills =
{
std::string(64, 'a'),
std::string(64, '"'),
std::string(64, '\\'),
std::string(64, '\x01'),
std::string(64, '\x7F'),
std::string(64, '\xFF'),
};
std::vector<std::string> patterns = fills;
{
std::string utf8;
while (utf8.size() < 64)
{
utf8 += "\xC3\xA4\xE2\x82\xAC\xF0\x9F\x98\x80" "ab";
}
patterns.push_back(utf8);
}
{
std::string bytes;
for (unsigned int i = 0; i < 64; ++i)
{
bytes.push_back(static_cast<char>((i * 37u) & 0xFFu));
}
patterns.push_back(bytes);
}
for (const auto& pattern : patterns)
{
for (std::size_t n = 0; n <= 64; ++n)
{
const std::string input = pattern.substr(0, n);
for (int placement = 0; placement < 2; ++placement)
{
const unsigned char* const p = placement == 0 ? guard.at_end(input) : guard.at_start(input);
CHECK(nlohmann::detail::find_string_special(p, n) <= n);
CHECK(nlohmann::detail::find_ascii_copyable_run(p, n) <= n);
CHECK(nlohmann::detail::valid_utf8_prefix(p, n) <= n);
#if defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17)
CHECK(nlohmann::detail::find_string_delimiter(p, n) <= n);
#endif
CHECK(nlohmann::detail::string_bulk_run(p, n) <= n);
CHECK(nlohmann::detail::scalar_string_bulk_run(p, n) <= n);
for (std::size_t i = 0; i < n; ++i)
{
CHECK(nlohmann::detail::validate_one_utf8(p + i, n - i) <= n - i);
}
}
}
}
}
SECTION("float conversion")
{
// the lexer converts floats from its token buffer, so the JSON section
// below never places a float token at the page boundary; this calls the
// conversion directly: the 8-digit loops of parse_float_significand()
// and digit_comparison() read up to the end of the token
const std::vector<std::string> tokens = all_prefixes(
{
"1234567812345678123456781234567812345678",
"-1.2345678901234567890123456789e-123",
"0.00000000000000000000000000000000000001",
"98765432.10987654",
"123456789012345678901234567890e-30",
// halfway between 1 and the next double, then just above it
"1.00000000000000011102230246251565404236316680908203125",
"1.000000000000000111022302462515654042363166809082031250000000001",
});
for (const auto& token : tokens)
{
// only complete number tokens: the conversion expects validated input
if (!token.empty() && json::accept(token))
{
CAPTURE(token)
check_float_native<double>(guard, token);
check_float_native<float>(guard, token);
}
}
}
SECTION("JSON")
{
for (const auto& input : json_seeds())
{
CAPTURE(input)
const std::string expected = outcome([&](bool allow)
{
return json::parse(input, nullptr, allow);
});
const std::string expected_comments = outcome([&](bool allow)
{
return json::parse(input, nullptr, allow, true);
});
const bool expected_accept = json::accept(input);
counting_sax expected_sax;
const bool expected_sax_result = json::sax_parse(input, &expected_sax);
for (int placement = 0; placement < 2; ++placement)
{
const std::uint8_t* const first = placement == 0 ? guard.at_end(input) : guard.at_start(input);
const std::uint8_t* const last = first + input.size();
CHECK(outcome([&](bool allow)
{
return json::parse(first, last, nullptr, allow);
}) == expected);
CHECK(outcome([&](bool allow)
{
return json::parse(first, last, nullptr, allow, true);
}) == expected_comments);
CHECK(json::accept(first, last) == expected_accept);
counting_sax sax;
CHECK(json::sax_parse(first, last, &sax) == expected_sax_result);
CHECK(sax.events == expected_sax.events);
CHECK(sax.error == expected_sax.error);
}
}
}
SECTION("binary formats")
{
// every prefix ends inside a value, e.g., inside a BSON key or BON8
// string without its terminator
const json j = binary_document();
check_binary(guard, json::to_bson(j), read_bson());
check_binary(guard, json::to_bon8(j), read_bon8());
check_binary(guard, json::to_cbor(j), read_cbor());
check_binary(guard, json::to_msgpack(j), read_msgpack());
check_binary(guard, json::to_ubjson(j), read_ubjson());
check_binary(guard, json::to_bjdata(j), read_bjdata());
}
}
#endif
File diff suppressed because it is too large. Load diff
+610
View File
@@ -0,0 +1,610 @@
// __ _____ _____ _____
// __| | __| | | | 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>
#if JSON_TEST_USING_MULTIPLE_HEADERS
#include <nlohmann/detail/view/builder.hpp>
#include <nlohmann/detail/view/materialize.hpp>
#include <nlohmann/detail/view/string_ref.hpp>
#else
#include <nlohmann/json_view.hpp> // the single header contains the internal headers
#endif
using nlohmann::json;
#include <array>
#include <cstdint>
#include <fstream>
#include <limits>
#include <map>
#include <memory>
#include <new>
#include <random>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
#include <test_data.hpp>
namespace
{
using nlohmann::detail::view::document_data;
using nlohmann::detail::view::node;
// the node index of a text; a vector input has no terminating NUL, so that
// AddressSanitizer catches any read past the last byte
struct built
{
std::unique_ptr<document_data, document_data::deleter> data{}; // NOLINT(readability-redundant-member-init)
std::vector<char> copy{}; // NOLINT(readability-redundant-member-init)
bool ok = false;
nlohmann::detail::view::parse_failure failure{};
};
template<typename FloatType = double>
built build(const std::string& text, bool comments, bool trailing_commas, bool sentinel)
{
built r;
r.data.reset(document_data::create(nlohmann::detail::view::estimate_nodes(text.data(), text.size())));
const char* src = text.c_str();
if (!sentinel)
{
r.copy.assign(text.begin(), text.end());
src = r.copy.data();
}
r.ok = nlohmann::detail::view::build < FloatType, !nlohmann::detail::abi_config::strict_nul_handling > (*r.data, src, text.size(), comments, trailing_commas, sentinel, r.failure);
r.data->src = src;
r.data->base[0] = src;
r.data->base[1] = r.data->arena.data();
return r;
}
// the value of a subtree, as json::parse would build it
json value_of(const document_data& d, const node*& n)
{
const node& x = *n;
++n;
switch (static_cast<json::value_t>(x.kind))
{
case json::value_t::object:
{
json o = json::object();
const node* const end = &x + x.next;
while (n != end)
{
const std::string key(d.str(*n), n->len);
++n;
o[key] = value_of(d, n);
}
return o;
}
case json::value_t::array:
{
json a = json::array();
const node* const end = &x + x.next;
while (n != end)
{
a.push_back(value_of(d, n));
}
return a;
}
case json::value_t::string:
return std::string(d.str(x), x.len);
case json::value_t::boolean:
return (x.flags & nlohmann::detail::view::node_flags::is_true) != 0;
case json::value_t::number_integer:
return static_cast<std::int64_t>(nlohmann::detail::view::integer_bits(x));
case json::value_t::number_unsigned:
return nlohmann::detail::view::integer_bits(x);
case json::value_t::number_float:
return json::parse(std::string(d.src + x.off, x.len)).get<double>();
case json::value_t::null:
case json::value_t::binary:
case json::value_t::discarded:
default:
return nullptr;
}
}
json value_of(const built& b)
{
const node* n = b.data->nodes;
json v = value_of(*b.data, n);
CHECK(n == b.data->nodes + b.data->node_count);
return v;
}
// accept/reject and the value must match json::parse, for all options and
// with and without a NUL after the text
void check_same(const std::string& text)
{
CAPTURE(text)
for (int options = 0; options < 4; ++options)
{
const bool comments = (options & 1) != 0;
const bool trailing_commas = (options & 2) != 0;
const bool accepted = json::accept(text, comments, trailing_commas);
for (const bool sentinel :
{
true, false
})
{
const built b = build(text, comments, trailing_commas, sentinel);
CHECK(b.ok == accepted);
if (b.ok && accepted)
{
CHECK(value_of(b) == json::parse(text, nullptr, true, comments, trailing_commas));
}
}
}
}
// a string value and a key must be accepted or rejected as json::parse does,
// and give its value (cheaper than check_same: the options do not matter)
void check_string(const std::string& content)
{
for (const std::string& text :
{
"[\"" + content + "\"]", "{\"" + content + "\":1}"
})
{
const bool accepted = json::accept(text);
for (const bool sentinel :
{
true, false
})
{
const built b = build(text, false, false, sentinel);
if (b.ok != accepted || (accepted && value_of(b) != json::parse(text)))
{
CAPTURE(text)
CHECK(b.ok == accepted);
CHECK((b.ok && accepted ? value_of(b) == json::parse(text) : true));
}
}
}
}
// a small deterministic generator of documents
struct generator
{
std::mt19937 rng{5295}; // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
int r(int n)
{
return static_cast<int>(rng() % static_cast<unsigned>(n));
}
void ws(std::string& o)
{
for (int n = r(4) == 0 ? r(12) : r(2); n > 0; --n)
{
o += " \n\t\r "[r(6)];
}
}
void str(std::string& o)
{
static const char* const pieces[] = {"a", "Z", " ", "~", "\\n", "\\\"", "\\\\", "\\/", "\\u00e9", "\\ud83d\\ude00", "\xc3\xa9", "\xe3\x81\x82", "\xf0\x9f\x98\x80", "\x7f", "\\u001f", "long enough text to leave the first 16 bytes"}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
o += '"';
for (int n = r(3) == 0 ? r(20) : r(6); n > 0; --n)
{
o += pieces[r(16)];
}
o += '"';
}
void num(std::string& o)
{
static const char* const numbers[] = {"0", "-0", "1", "-1", "12", "123456789", "1234567890123456789", "9223372036854775807", "-9223372036854775808", // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
"9223372036854775808", "18446744073709551615", "18446744073709551616", "-9223372036854775809",
"1.5", "-2.25e-3", "1e10", "1E+2", "0.000001", "3.141592653589793238462643", "1e308", "-1e-400", "123.456e7"
};
o += numbers[r(22)];
}
void value(std::string& o, int depth)
{
ws(o);
const int k = depth > 5 ? 2 + r(6) : r(8);
if (k == 0 || k == 1)
{
const bool object = k == 0;
o += object ? '{' : '[';
for (int i = r(5); i > 0; --i)
{
ws(o);
if (object)
{
str(o);
ws(o);
o += ':';
}
value(o, depth + 1);
o += i > 1 ? "," : "";
}
ws(o);
o += object ? '}' : ']';
}
else if (k < 4)
{
str(o);
}
else if (k < 6)
{
num(o);
}
else
{
static const char* const literals[] = {"true", "false", "null"}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
o += literals[r(3)];
}
ws(o);
}
};
} // namespace
TEST_CASE("json_view string_ref")
{
// std::string_view in C++17, a stand-in with the same members before
using nlohmann::detail::view::string_ref;
const std::string text = "abc";
const string_ref r(text);
CHECK(r.length() == 3);
CHECK(std::string(r.begin(), r.end()) == "abc");
CHECK(r[1] == 'b');
CHECK(r != string_ref("abd"));
CHECK_FALSE(r != string_ref("abcd", 3));
std::ostringstream o;
o << r;
CHECK(o.str() == "abc");
}
TEST_CASE("json_view builder")
{
SECTION("scalars and containers")
{
for (const char* text :
{
"null", "true", "false", "0", "-0", "42", "-42", "1.5", "\"\"", "\"abc\"", "[]", "{}", "[1,2,3]", "{\"a\":1,\"b\":[true,null]}", // NOLINT(modernize-raw-string-literal)
" [ 1 , 2 ] ", "{\"a\" : {\"b\" : {}}}", "[[[]]]", "\"\\u00e4\\n\\ud83d\\ude00\"", "{\"a\":1,\"a\":2}", "18446744073709551616", // NOLINT(modernize-raw-string-literal)
"-9223372036854775809", "123456789012345678901234567890", "1e400", "-1e400", "1.7976931348623157e308"
})
{
check_same(text);
}
// the midpoint between the largest double and 2^1024 rounds to
// infinity (an overflow), one less to the largest double: with more
// than 19 digits, Eisel-Lemire cannot decide these, and the overflow
// check needs the exact comparison with the midpoint
const std::string midpoint = "179769313486231580793728971405303415079934132710037826936173778980444968292764750946649017977587207096330286416692887910946555547851940402630657488671505820681908902000708383676273854845817711531764475730270069855571366959622842914819860834936475292719074168444365510704342711559699508093042880177904174497792";
const std::string below = "179769313486231580793728971405303415079934132710037826936173778980444968292764750946649017977587207096330286416692887910946555547851940402630657488671505820681908902000708383676273854845817711531764475730270069855571366959622842914819860834936475292719074168444365510704342711559699508093042880177904174497791";
check_same(midpoint);
check_same("-" + midpoint);
check_same(below);
check_same("[" + below + "," + midpoint + "]");
// the check uses the floating-point type of the document: with float,
// the view rejects what parse() rejects (out_of_range.406), and a
// double document is not affected
using float_json = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
CHECK_FALSE(float_json::accept("1e39"));
CHECK(float_json::accept("3.4028235e38"));
CHECK_FALSE(float_json::accept("3.4028236e38"));
for (const char* text :
{
"1e39", "-1e39", "3.4028235e38", "-3.4028235e38", "3.4028236e38", "-3.4028236e38", "3.4028234663852886e38", "1e38",
"340282356779733661637539395458142568448", "340282356779733661637539395458142568447.99", "0.00034028236e42",
"[1.5e38, 3.5e38]", R"({"a": 1e-50, "b": 1e39})"
})
{
CAPTURE(text)
const bool float_accepted = float_json::accept(text);
for (const bool sentinel :
{
true, false
})
{
const built f = build<float>(text, false, false, sentinel);
CHECK(f.ok == float_accepted);
if (!f.ok)
{
CHECK(f.failure.code == nlohmann::detail::view::error_code::number_overflow);
}
CHECK(build<double>(text, false, false, sentinel).ok == json::accept(text));
}
}
}
SECTION("malformed input")
{
for (const char* text :
{
"", " ", "[", "]", "{", "}", "[1,]", "{\"a\":1,}", "[1 2]", "{\"a\" 1}", "{1:2}", "tru", "nul", "fals", "truex", "-", "01", "1.", ".5", "1e", "1e+",
"\"", "\"abc", "\"\\x\"", "\"\\u12\"", "\"\\u12G4\"", "\"\\ud800\"", "\"\\udc00\"", "\"\\ud800\\u0041\"", "\"\x01\"", "\"\xff\"", "\"\xc3\"", // NOLINT(modernize-raw-string-literal)
"\"\xe0\x80\x80\"", "\"\xed\xa0\x80\"", "[1]x", "[1] [2]", "/", "/*", "/* */ 1", "// c\n1", "1 // c", "[1,/*c*/2]", "[1,2,]"
})
{
check_same(text);
}
}
SECTION("NUL, BOM, and whitespace")
{
// a NUL inside a string is a control character, as for json::parse
// (where a NUL ends the input, it does so only between values)
for (const bool sentinel :
{
true, false
})
{
const built b = build(std::string("[\"ab\0cd\"]", 9), false, false, sentinel);
CHECK(!b.ok);
CHECK(b.failure.code == nlohmann::detail::view::error_code::string_control_character);
CHECK(b.failure.offset == 4);
}
check_same(std::string("[1]\0garbage", 11));
check_same(std::string("[1\0]", 4));
check_same(std::string("[1, // c\0\n2]", 12));
check_same(std::string("[1, /* c\0 */ 2]", 15));
check_same("\xEF\xBB\xBF[1]");
check_same("\xEF\xBB[1]");
check_same(" \t\r\n 7 \n");
for (const char* text :
{"[1]\r", "[1]\n", "[1]\r\n", "[1,\r2]", "[1,\r\n2]", "7\r", "\"x\"\r", "{\"a\":\r\n1}\r", "[\n 1,\n 2\n]", "{\n \"a\": [\n 1\n ]\n}"
})
{
check_same(text);
}
}
SECTION("deep nesting")
{
// the deepest nesting is recorded (materialize() sizes its stack from it)
CHECK(build("1", false, false, false).data->max_depth == 0);
CHECK(build("[]", false, false, false).data->max_depth == 1);
CHECK(build(R"([[1],{"a":[[]]},[]])", false, false, false).data->max_depth == 4);
// a stack that turns out too small still grows
const std::string nested = R"([[1],{"a":[[{"b":[]}]]},[]])";
const built shallow = build(nested, false, false, false);
REQUIRE(shallow.ok);
shallow.data->max_depth = 1;
CHECK(nlohmann::detail::view::materialize<json, false>(*shallow.data, shallow.data->nodes) == json::parse(nested));
// the open containers beyond 64 levels live on the heap
for (const std::size_t depth :
{
63u, 64u, 65u, 1000u, 100000u
})
{
const std::string arrays = std::string(depth, '[') + std::string(depth, ']');
const built b = build(arrays, false, false, false);
REQUIRE(b.ok);
CHECK(b.data->node_count == depth);
CHECK(b.data->nodes[0].next == depth);
CHECK(b.data->max_depth == depth);
std::string objects;
for (std::size_t i = 0; i < depth; ++i)
{
objects += "{\"a\":";
}
objects += '1' + std::string(depth, '}');
const built o = build(objects, false, false, false);
REQUIRE(o.ok);
CHECK(o.data->node_count == (2 * depth) + 1);
CHECK(o.data->max_depth == depth);
CHECK(!build(std::string(depth, '[') + std::string(depth - 1, ']'), false, false, false).ok);
}
}
SECTION("generated documents and damaged copies")
{
generator g;
for (int i = 0; i < 3000; ++i)
{
std::string text;
g.value(text, 0);
check_same(text);
// damage: flip one byte, or cut the text
std::string damaged = text;
const auto at = static_cast<std::size_t>(g.r(static_cast<int>(damaged.size())));
static const char replacements[] = {'x', '"', '\\', ',', ':', ']', '}', '[', '{', '1', '-', '.', 'e', '\0', '\n', '/'}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
damaged[at] = replacements[g.r(16)];
check_same(damaged);
check_same(text.substr(0, at));
}
}
SECTION("test files")
{
for (const char* name :
{
"/json.org/1.json", "/json.org/2.json", "/json.org/3.json", "/json.org/4.json", "/json.org/5.json",
"/json_testsuite/sample.json", "/nativejson-benchmark/canada.json", "/nativejson-benchmark/citm_catalog.json",
"/nativejson-benchmark/twitter.json", "/json_tests/pass1.json", "/json_tests/pass2.json", "/json_tests/pass3.json"
})
{
CAPTURE(name)
const std::ifstream f(std::string(TEST_DATA_DIRECTORY) + name, std::ios::binary);
std::stringstream ss;
ss << f.rdbuf();
const std::string text = ss.str();
REQUIRE(!text.empty());
const built b = build(text, false, false, true);
REQUIRE(b.ok);
CHECK(value_of(b) == json::parse(text));
}
}
}
TEST_CASE("json_view builder: strings across vector blocks")
{
// Strings are scanned 8 or 16 bytes at a time (NEON, SSE2, or SWAR) and
// non-ASCII text with the vector UTF-8 check (NEON, SSSE3) or one sequence
// at a time. Sequences are placed so that they start at every offset
// around the block boundaries of keys (16, 32) and values (8, 24), with
// text of several lengths after them.
const std::array<std::size_t, 16> prefixes = {{0, 6, 7, 8, 13, 14, 15, 16, 21, 22, 23, 24, 29, 30, 31, 32}};
const std::array<std::size_t, 3> suffixes = {{0, 3, 17}};
const auto around = [&](const std::string & seq, std::size_t prefix, std::size_t suffix)
{
return std::string(prefix, 'a') + seq + std::string(suffix, 'b');
};
SECTION("every two-byte sequence")
{
for (unsigned lead = 0x80; lead <= 0xFF; ++lead)
{
for (unsigned second = 0; second <= 0xFF; ++second)
{
if (second == '"' || second == '\\')
{
continue;
}
const std::string seq = {static_cast<char>(lead), static_cast<char>(second)};
check_string(around(seq, prefixes[(lead + second) % 16], suffixes[second % 3]));
}
}
}
SECTION("three- and four-byte sequences")
{
const std::array<unsigned, 8> conts = {{0x7F, 0x80, 0x8F, 0x90, 0x9F, 0xA0, 0xBF, 0xC0}};
for (unsigned lead = 0xE0; lead <= 0xF7; ++lead)
{
for (const unsigned b2 : conts)
{
for (const unsigned b3 : conts)
{
for (const std::size_t prefix : prefixes)
{
std::string seq = {static_cast<char>(lead), static_cast<char>(b2), static_cast<char>(b3)};
if (lead >= 0xF0)
{
seq += static_cast<char>(prefix % 2 == 0 ? 0x80 : 0xBF);
}
check_string(around(seq, prefix, suffixes[prefix % 3]));
// cut short before the end of the string
check_string(around(seq.substr(0, seq.size() - 1), prefix, suffixes[prefix % 3]));
}
}
}
}
}
SECTION("long runs of text with one damaged byte")
{
const std::array<const char*, 7> chars = {{"a", "\xc3\xa9", "\xe3\x81\x82", "\xf0\x9f\x98\x80", "\xed\x9f\xbf", "\xef\xbf\xbf", "\xf4\x8f\xbf\xbf"}};
const std::array<char, 11> damage = {{'\x80', '\xbf', '\xc0', '\xc1', '\xe0', '\xed', '\xf5', '\xff', '\x1f', '"', '\\'}};
std::mt19937 rng(5295); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
for (int i = 0; i < 4000; ++i)
{
std::string text;
const auto n = rng() % 60;
for (unsigned k = 0; k < n; ++k)
{
text += chars[rng() % chars.size()];
}
check_string(text);
if (!text.empty())
{
text[rng() % text.size()] = damage[rng() % damage.size()];
check_string(text);
}
}
}
}
TEST_CASE("json_view node integer bits")
{
using nlohmann::detail::view::integer_bits;
using nlohmann::detail::view::set_integer_bits;
// an integer lives in len (low half) and next (high half), on any byte
// order; a big-endian target must not store the native word over both
SECTION("set_integer_bits and integer_bits")
{
node n = {};
for (const std::uint64_t v :
{
std::uint64_t{0}, std::uint64_t{1}, std::uint64_t{0xFFFFFFFFu}, std::uint64_t{0x100000000u},
std::uint64_t{0x0000000200000003u}, std::uint64_t{0x0123456789ABCDEFu}, std::uint64_t{0xFFFFFFFFFFFFFFFEu}
})
{
CAPTURE(v)
n.kind = 0x5A;
n.flags = 0xA5;
n.extra = 0x1234;
n.off = 0x89ABCDEFu;
set_integer_bits(n, v);
CHECK(integer_bits(n) == v);
CHECK(n.len == static_cast<std::uint32_t>(v));
CHECK(n.next == static_cast<std::uint32_t>(v >> 32));
// the other fields are untouched
CHECK(n.kind == 0x5A);
CHECK(n.flags == 0xA5);
CHECK(n.extra == 0x1234);
CHECK(n.off == 0x89ABCDEFu);
}
}
SECTION("parsed integers")
{
struct integer_case
{
const char* text;
std::uint64_t bits;
};
for (const integer_case c :
{
integer_case{"[8589934595]", 0x0000000200000003u}, integer_case{"[4294967296]", 0x100000000u}, integer_case{"[4294967295]", 0xFFFFFFFFu},
integer_case{"[-2]", 0xFFFFFFFFFFFFFFFEu}, integer_case{"[-4294967297]", 0xFFFFFFFEFFFFFFFFu}, integer_case{"[18446744073709551615]", 0xFFFFFFFFFFFFFFFFu},
integer_case{"[7]", 7u}
})
{
CAPTURE(c.text)
const built b = build(c.text, false, false, true);
REQUIRE(b.ok);
const node& n = b.data->nodes[1];
CHECK(integer_bits(n) == c.bits);
CHECK(n.len == static_cast<std::uint32_t>(c.bits));
CHECK(n.next == static_cast<std::uint32_t>(c.bits >> 32));
}
}
}
TEST_CASE("json_view node array size limit")
{
// a node array larger than the address space is refused, not wrapped to a
// small allocation (the size computation overflows on 32-bit targets, and
// for absurd counts everywhere)
std::unique_ptr<document_data, document_data::deleter> d(document_data::create(0));
d->reserve(8);
REQUIRE(d->node_capacity >= 8);
d->node_count = 2;
const std::size_t cap = d->node_capacity;
node* const nodes = d->nodes;
#if !defined(JSON_NOEXCEPTION)
const std::size_t too_many = document_data::max_nodes() + 1;
CHECK_THROWS_AS(d->reserve(too_many), std::bad_alloc&);
CHECK_THROWS_AS(d->reserve((std::numeric_limits<std::size_t>::max)()), std::bad_alloc&);
// the array is unchanged
CHECK(d->nodes == nodes);
CHECK(d->node_capacity == cap);
CHECK(d->node_count == 2);
#endif
// the largest count that fits is not refused by the check (nothing is
// allocated for a count that is already there)
d->reserve(cap);
CHECK(d->nodes == nodes);
}
+592
View File
@@ -0,0 +1,592 @@
// __ _____ _____ _____
// __| | __| | | | 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_view.hpp>
using nlohmann::json;
using nlohmann::ordered_json;
using nlohmann::json_document;
using nlohmann::json_view;
using nlohmann::ordered_json_document;
using nlohmann::ordered_json_view;
#include "json_view_test_helpers.hpp"
using json_view_test::generator;
using json_view_test::has_duplicate_keys;
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <iomanip>
#include <map>
#include <random>
#include <sstream>
#include <string>
#include <vector>
// These tests were split off unit-json_view.cpp, whose object file got too large
// for the MinGW linker (see json_view_test_helpers.hpp). Like that file, this one
// is also built with C++17, as it mentions JSON_HAS_CPP_17.
TEST_CASE("json_view dump")
{
SECTION("the output of ordered_json::dump()")
{
generator g;
for (int i = 0; i < 2000; ++i)
{
std::string text;
g.value(text, 0);
const ordered_json_document d = ordered_json_document::parse(text);
if (has_duplicate_keys(d.root()))
{
continue;
}
CAPTURE(text)
const ordered_json j = ordered_json::parse(text);
for (const int indent :
{
-1, 0, 2
})
{
for (const bool ensure_ascii :
{
false, true
})
{
CHECK(d.root().dump(indent, i % 2 == 0 ? ' ' : '\t', ensure_ascii) == j.dump(indent, i % 2 == 0 ? ' ' : '\t', ensure_ascii));
}
}
// also of each element
for (const ordered_json_view e : d.root())
{
CHECK(e.dump() == e.materialize().dump());
}
}
}
SECTION("strings")
{
const std::string text = R"(["plain", "\u0000\u0001\u001f\u007f\u0080é€￿😀", "\"\\\/\b\f\n\r\t", "aéあ😀b", "long text beyond the eight bytes of a word \n with an escape in the middle"])";
const ordered_json_document d = ordered_json_document::parse(text);
const ordered_json j = ordered_json::parse(text);
CHECK(d.root().dump() == j.dump());
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(d.root().dump(4, ' ', true) == j.dump(4, ' ', true));
const std::string key_text = R"({"é\n": {"\"": [], "": {}}})";
const ordered_json_document keys = ordered_json_document::parse(key_text);
const ordered_json key_json = ordered_json::parse(key_text);
CHECK(keys.root().dump(2, ' ', true) == key_json.dump(2, ' ', true));
}
SECTION("numbers")
{
const std::string text = "[1.50, 1E2, -0, -0.0, 123456789012345678901234567890, 18446744073709551615, -9223372036854775808, 0.1, 1e-7, 5e-324]";
const json_document d = json_document::parse(text);
CHECK(d.root().dump() == json::parse(text).dump());
CHECK(d.root().dump() == "[1.5,100.0,0,-0.0,1.2345678901234568e+29,18446744073709551615,-9223372036854775808,0.1,1e-07,5e-324]");
CHECK(d.root().dump(-1, ' ', false, json_view::number_format::source) == "[1.50,1E2,-0,-0.0,123456789012345678901234567890,18446744073709551615,-9223372036854775808,0.1,1e-7,5e-324]");
// also indented, and with ensure_ascii
CHECK(d.root().dump(0, ' ', false, json_view::number_format::source) == "[\n1.50,\n1E2,\n-0,\n-0.0,\n123456789012345678901234567890,\n18446744073709551615,\n-9223372036854775808,\n0.1,\n1e-7,\n5e-324\n]");
CHECK(d.root().dump(-1, ' ', true, json_view::number_format::source) == "[1.50,1E2,-0,-0.0,123456789012345678901234567890,18446744073709551615,-9223372036854775808,0.1,1e-7,5e-324]");
// float tokens of up to 17 significant digits in every spelling: those
// of at most 15 digits are written from their digits, the others
// through the conversion; both as dump() writes them
{
std::mt19937_64 tokens(1170); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
// a number below n; the remainder is a std::uint64_t, which is
// std::size_t on some platforms and wider on others
const auto draw = [&tokens](std::size_t n)
{
const std::uint64_t r = tokens() % n;
return static_cast<std::size_t>(r);
};
std::string many_tokens = "[";
for (int i = 0; i < 20000; ++i)
{
const std::size_t length = 1 + draw(17);
std::string digits(1, static_cast<char>('1' + draw(9)));
for (std::size_t k = 1; k < length; ++k)
{
digits += static_cast<char>('0' + draw(10));
}
digits += std::string(draw(4), '0'); // trailing zeros
std::string token = draw(3) == 0 ? "-" : "";
const std::size_t point = draw(digits.size() + 1);
if (point == 0)
{
token += "0." + std::string(draw(5), '0') + digits;
}
else
{
token += digits.substr(0, point) + (point < digits.size() ? "." + digits.substr(point) : "");
}
// an exponent that keeps the value between about 1e-320 and 1e300
const int exponent = static_cast<int>(draw(600)) - 300 - static_cast<int>(point);
if (draw(4) != 0)
{
token += (draw(2) == 0 ? "e" : "E") + std::string(exponent >= 0 && draw(2) == 0 ? "+" : "") + std::to_string(exponent);
}
else if (point == digits.size())
{
token += ".0"; // (a float, not an integer)
}
many_tokens += (i != 0 ? "," : "") + token;
}
many_tokens += ']';
const json_document many_doc = json_document::parse(many_tokens);
CHECK(many_doc.root().dump() == json::parse(many_tokens).dump());
}
// random doubles, written as parse() and dump() would
std::mt19937_64 rng(1170); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
std::string many = "[";
for (int i = 0; i < 5000; ++i)
{
const std::uint64_t bits = rng();
double x = 0;
std::memcpy(&x, &bits, sizeof(x));
if (std::isfinite(x))
{
many += (many.size() > 1 ? "," : "") + json(x).dump();
}
}
many += ']';
const json_document many_document = json_document::parse(many);
CHECK(many_document.root().dump() == json::parse(many).dump());
using json_float = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
const nlohmann::basic_json_document<json_float> float_document = nlohmann::basic_json_document<json_float>::parse("[0.1, 1.5e10, 3.4028235e38]");
CHECK(float_document.root().dump() == json_float::parse("[0.1, 1.5e10, 3.4028235e38]").dump());
}
SECTION("members in document order, all of them")
{
const json_document d = json_document::parse(R"({"b": 1, "a": 2, "b": 3})");
CHECK(d.root().dump() == R"({"b":1,"a":2,"b":3})");
CHECK(d.root().dump(1) == "{\n \"b\": 1,\n \"a\": 2,\n \"b\": 3\n}");
// dump() writes all members, though the lookup finds the first
CHECK(d.root()["b"].dump() == "1");
}
SECTION("deep nesting")
{
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
const json_document deep_document = json_document::parse(deep);
CHECK(deep_document.root().dump() == deep);
}
SECTION("the output buffer of a small value is small")
{
// an escaped key after the value: its node lies in the arena, so the
// source extent of the value cannot be read from the next node
const std::string big(100000, 'a');
const std::string text = R"({"small":1,"list":[1,2,3],"k\n":")" + big + R"("})";
const json_document d = json_document::parse(text);
const auto small = d.root()["small"].dump();
CHECK(small == "1");
CHECK(small.capacity() < 4096);
const auto list = d.root()["list"].dump();
CHECK(list == "[1,2,3]");
CHECK(list.capacity() < 4096);
CHECK(d.root()["list"].dump(2).capacity() < 4096);
// the whole document and the large value are unaffected
CHECK(d.root().dump() == ordered_json::parse(text).dump());
CHECK(d.root()["k\n"].dump() == "\"" + big + "\"");
}
SECTION("output that outgrows the estimate")
{
// ensure_ascii writes six bytes for each two-byte character
std::string chars;
for (int i = 0; i < 5000; ++i)
{
chars += "\xC3\xA9";
}
const json_document d = json_document::parse(R"({"a":")" + chars + R"(","k\n":1})");
const json expected = json::parse("\"" + chars + "\"");
CHECK(d.root()["a"].dump(-1, ' ', true) == expected.dump(-1, ' ', true));
CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6));
}
SECTION("streams and discarded views")
{
const json_document d = json_document::parse(R"({"a": [1, 2]})");
std::ostringstream compact;
compact << d.root();
CHECK(compact.str() == R"({"a":[1,2]})");
std::ostringstream pretty;
pretty << std::setw(2) << std::setfill('.') << d.root() << d.root()["a"];
CHECK(pretty.str() == "{\n..\"a\": [\n....1,\n....2\n..]\n}[1,2]");
CHECK(json_view().dump() == json(json::value_t::discarded).dump());
}
}
TEST_CASE("json_view comparison")
{
SECTION("equality of the values parse() produces")
{
generator g;
std::vector<std::string> texts;
for (int i = 0; i < 600; ++i)
{
std::string text;
g.value(text, 0);
texts.push_back(text);
// the same value written differently: sorted keys, canonical numbers
texts.push_back(json::parse(text).dump(1));
}
for (std::size_t i = 0; i + 2 < texts.size(); ++i)
{
for (std::size_t k = i; k < i + 3; ++k)
{
CAPTURE(texts[i])
CAPTURE(texts[k])
const json_document a = json_document::parse(texts[i]);
const json_document b = json_document::parse(texts[k]);
const json ja = json::parse(texts[i]);
const json jb = json::parse(texts[k]);
CHECK((a.root() == b.root()) == (ja == jb));
CHECK((a.root() != b.root()) == (ja != jb));
CHECK((a.root() == jb) == (ja == jb));
CHECK((jb == a.root()) == (ja == jb));
CHECK((a.root() != jb) == (ja != jb));
CHECK((jb != a.root()) == (ja != jb));
// ordered_json compares members in order
const ordered_json_document oa = ordered_json_document::parse(texts[i]);
const ordered_json_document ob = ordered_json_document::parse(texts[k]);
const ordered_json oja = ordered_json::parse(texts[i]);
const ordered_json ojb = ordered_json::parse(texts[k]);
CHECK((oa.root() == ob.root()) == (oja == ojb));
CHECK((oa.root() == ojb) == (oja == ojb));
}
}
}
SECTION("numbers, duplicate keys, member order")
{
const auto same = [](const char* x, const char* y)
{
const json_document dx = json_document::parse(x);
const json_document dy = json_document::parse(y);
return dx.root() == dy.root();
};
CHECK(same("1", "1.0"));
CHECK(same("[1, -1, 2.5]", "[1.0, -1.0, 25e-1]"));
CHECK(!same("1", "1.5"));
CHECK(same("18446744073709551615", "18446744073709551615"));
CHECK(same(R"({"a": 1, "a": 2})", R"({"a": 2})"));
CHECK(!same(R"({"a": 1, "a": 2})", R"({"a": 1})"));
CHECK(same(R"({"a": 1, "b": 2})", R"({"b": 2, "a": 1})"));
CHECK(!same(R"({"a": 1})", R"({"a": 1, "b": 2})"));
CHECK(!same("[1, 2]", "[2, 1]"));
CHECK(!same("\"a\"", "\"b\""));
CHECK(same("\"\\u00e9\"", "\"\xc3\xa9\""));
CHECK(!same("null", "false"));
CHECK(!same("[]", "{}"));
const ordered_json_document dup = ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})");
const ordered_json_document last = ordered_json_document::parse(R"({"a": 3, "b": 2})");
CHECK(dup.root() == last.root());
// (== compares as parse() resolves duplicates, a lookup finds the first)
CHECK(dup.root()["a"].materialize() == 1);
CHECK(dup.root()["a"] != last.root()["a"]);
const ordered_json_document ab = ordered_json_document::parse(R"({"a": 1, "b": 2})");
const ordered_json_document ba = ordered_json_document::parse(R"({"b": 2, "a": 1})");
CHECK(ab.root() != ba.root());
// discarded values compare as basic_json's do
const json discarded(json::value_t::discarded);
CHECK((json_view() == json_view()) == (discarded == discarded)); // NOLINT(readability-container-size-empty): operator== is tested
CHECK((json_view() == discarded) == (discarded == discarded)); // NOLINT(readability-container-size-empty)
const json_document null_document = json_document::parse("null");
CHECK(!(json_view() == null_document.root())); // NOLINT(readability-container-size-empty)
CHECK(!(null_document.root() == discarded));
}
SECTION("deep nesting")
{
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
const json_document a = json_document::parse(deep);
const json_document b = json_document::parse(deep);
CHECK(a.root() == b.root());
CHECK(a.root() == json::parse(deep));
const std::string other = std::string(100000, '[') + "1" + std::string(100000, ']');
const json_document c = json_document::parse(other);
CHECK(a.root() != c.root());
}
}
TEST_CASE("json_view large objects")
{
// objects with 128 members or more are looked up with a hash index
for (const std::size_t members :
{
127u, 128u, 129u, 10000u
})
{
CAPTURE(members)
std::string text = "{";
for (std::size_t i = 0; i < members; ++i)
{
text += (i != 0 ? ",\"" : "\"") + std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\\n" : "") + "\":" + std::to_string(i);
}
text += R"(,"":"empty key","k1":"a duplicate of an earlier key"})";
const json_document d = json_document::parse(text);
const json_view v = d.root();
const json j = json::parse(text);
for (std::size_t i = 0; i < members; ++i)
{
const std::string key = std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\n" : "");
CHECK(v[key].get<std::size_t>() == i);
CHECK(v.contains(key));
CHECK(v.find(key).key() == key);
CHECK(v.at(key).get<std::size_t>() == i);
CHECK(!v.contains(key + "x"));
}
CHECK(v[""].get_string() == "empty key");
CHECK(v["k1"].get<int>() == 1); // the first of duplicate keys, as for small objects
CHECK(!v.contains("missing"));
CHECK_THROWS_WITH_AS(v.at("missing"), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&);
CHECK(v == j);
CHECK(v.materialize() == j);
}
SECTION("duplicate keys: the first member wins, with and without a table")
{
// an object of `total` members: the keys "k0".."k<n-1>" in order, then
// three keys repeated twice more (one copy in the middle, one at the
// end), and two keys repeated once; the value of a member is its
// position, so that the first member of a key can be told apart
struct member
{
std::string key;
std::size_t position;
};
const auto make_members = [](std::size_t total)
{
std::vector<std::string> keys;
for (std::size_t i = 0; i + 8 < total; ++i)
{
keys.push_back("k" + std::to_string(i));
}
const std::size_t n = keys.size();
const std::array<std::size_t, 3> triple = {{3, 17, n - 1}};
const std::array<std::size_t, 2> twice = {{5, n / 2}};
std::vector<std::string> ordered = keys;
for (const std::size_t i : triple)
{
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 2), keys[i]);
}
for (const std::size_t i : twice)
{
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 3), keys[i]);
}
for (const std::size_t i : triple)
{
ordered.push_back(keys[i]);
}
std::vector<member> result;
result.reserve(ordered.size());
for (std::size_t i = 0; i < ordered.size(); ++i)
{
result.push_back({ordered[i], i});
}
return result;
};
// 100 and 127 members: no table; 128 members and more: a table
for (const std::size_t total :
{
100u, 127u, 128u, 200u, 5000u
})
{
CAPTURE(total)
const std::vector<member> members = make_members(total);
REQUIRE(members.size() >= total);
REQUIRE(members.size() >= 100);
std::string text = "{";
std::map<std::string, std::size_t> first;
std::map<std::string, std::size_t> last;
std::map<std::string, std::size_t> occurrences;
for (const member& m : members)
{
text += (text.size() > 1 ? ",\"" : "\"") + m.key + "\":" + std::to_string(m.position);
first.insert({m.key, m.position});
last[m.key] = m.position;
++occurrences[m.key];
}
text += '}';
REQUIRE(first.size() < members.size());
const json_document d = json_document::parse(text);
const json_view v = d.root();
const json j = json::parse(text);
CHECK(v.size() == members.size()); // every occurrence is visited
const json m = v.materialize();
for (const auto& entry : first)
{
CAPTURE(entry.first)
const std::size_t expected = entry.second;
CHECK(v[entry.first].get<std::size_t>() == expected);
CHECK(v.at(entry.first).get<std::size_t>() == expected);
CHECK(v.find(entry.first).value().get<std::size_t>() == expected);
CHECK(v.value(entry.first, std::size_t{0}) == expected);
CHECK(v.contains(entry.first));
CHECK(v.count(entry.first) == occurrences[entry.first]);
const std::string pointer = "/" + entry.first;
CHECK(v[json::json_pointer(pointer)].get<std::size_t>() == expected);
CHECK(v.at(json::json_pointer(pointer)).get<std::size_t>() == expected);
CHECK(v.value(json::json_pointer(pointer), std::size_t{0}) == expected);
CHECK(v.contains(json::json_pointer(pointer)));
// materialize() and parse() keep the last value instead
CHECK(j[entry.first].get<std::size_t>() == last.at(entry.first));
CHECK(m[entry.first].get<std::size_t>() == last.at(entry.first));
}
CHECK(!v.contains("k"));
CHECK(v["missing"].is_discarded());
CHECK(v.materialize() == j);
CHECK(v == j);
}
}
SECTION("colliding keys")
{
// the hash is not seeded, so keys that all land in one place must not
// make the table build quadratic: such an object gets no table and is
// searched linearly
constexpr std::size_t members = 300;
constexpr std::size_t table_size = 1024; // the table size for 300 members: the next power of two >= 600
std::vector<std::string> colliding;
std::vector<std::string> spread;
for (std::uint64_t counter = 0; colliding.size() < members || spread.size() < members; ++counter)
{
std::string key(8, 'a');
std::uint64_t x = counter;
for (std::size_t i = 0; i < 8; ++i, x /= 26)
{
key[i] = static_cast<char>('a' + (x % 26));
}
const bool lands_in_slot_zero = (nlohmann::detail::view::key_hash(key.data(), key.size()) & (table_size - 1)) == 0;
if (lands_in_slot_zero && colliding.size() < members)
{
colliding.push_back(key);
}
else if (!lands_in_slot_zero && spread.size() < members)
{
spread.push_back(key);
}
}
const auto make_text = [](const std::vector<std::string>& keys)
{
std::string text = "{";
for (std::size_t i = 0; i < keys.size(); ++i)
{
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i % 10);
}
return text + "}";
};
const std::string colliding_text = make_text(colliding);
const std::string spread_text = make_text(spread);
json_document with_collisions = json_document::parse(colliding_text);
json_document without_collisions = json_document::parse(spread_text);
for (const auto* pair :
{
&colliding, &spread
})
{
const json_view v = (pair == &colliding ? with_collisions : without_collisions).root();
for (std::size_t i = 0; i < members; ++i)
{
CAPTURE(i)
CHECK(v[(*pair)[i]].get<std::size_t>() == i % 10);
CHECK(v.at((*pair)[i]).get<std::size_t>() == i % 10);
CHECK(v.find((*pair)[i]).key() == (*pair)[i]);
CHECK(!v.contains((*pair)[i] + "x"));
}
CHECK(!v.contains("missing"));
}
CHECK(with_collisions.root() == json::parse(colliding_text));
// only the object with the spread keys got a table (both texts have the
// same length, so the tables are the only difference)
with_collisions.shrink_to_fit();
without_collisions.shrink_to_fit();
CHECK(without_collisions.memory_usage() >= with_collisions.memory_usage() + (table_size * sizeof(std::uint32_t)));
}
SECTION("shrink_to_fit releases the tables' spare capacity")
{
const auto make_text = [](int objects, int members)
{
std::string text = "[";
for (int object = 0; object < objects; ++object)
{
text += object != 0 ? ",{" : "{";
for (int i = 0, n = members + object; i < n; ++i)
{
text += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\":" + std::to_string(i);
}
text += '}';
}
return text + "]";
};
const std::string small_text = make_text(5, 150);
const std::string big_text = make_text(40, 400);
// reading a big text, and then a small one, leaves the spare capacity
// of the big one: shrink_to_fit() brings the document to the size of
// one parsed from the small text alone
json_document d = json_document::parse(big_text);
const std::size_t big = d.memory_usage();
d.read(small_text);
CHECK(d.memory_usage() >= big);
d.shrink_to_fit();
json_document fresh = json_document::parse(small_text);
fresh.shrink_to_fit();
CHECK(d.memory_usage() == fresh.memory_usage());
CHECK(d.memory_usage() < big / 2);
CHECK(d.root() == json::parse(small_text));
for (int object = 0; object < 5; ++object)
{
const json_view v = d.root()[static_cast<std::size_t>(object)];
for (int i = 0, n = 150 + object; i < n; ++i)
{
CHECK(v[std::to_string(i)].get<int>() == i);
}
}
}
SECTION("nested, reused, and in arrays")
{
std::string inner = "{";
for (int i = 0; i < 300; ++i)
{
inner += (i != 0 ? ",\"m" : "\"m") + std::to_string(i) + "\":" + std::to_string(i);
}
inner += '}';
const std::string text = "[" + inner + ",{\"x\":" + inner + "}," + inner + "]";
json_document d = json_document::parse(text);
CHECK(d.root()[0]["m299"].get<int>() == 299);
CHECK(d.root()[1]["x"]["m150"].get<int>() == 150);
CHECK(d.root()[2]["m0"].get<int>() == 0);
const std::size_t with_index = d.memory_usage();
d.read(std::string("{\"small\": 1}"));
CHECK(d.root()["small"].get<int>() == 1);
d.read(text);
CHECK(d.root()[2]["m7"].get<int>() == 7);
CHECK(d.memory_usage() >= with_index / 2);
}
}
+834
View File
@@ -0,0 +1,834 @@
// __ _____ _____ _____
// __| | __| | | | 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_view.hpp>
using nlohmann::json;
using nlohmann::ordered_json;
using nlohmann::json_document;
using nlohmann::json_editable_document;
using nlohmann::json_editable_view;
using nlohmann::ordered_json_document;
using nlohmann::ordered_json_editable_document;
using nlohmann::ordered_json_editable_view;
using ptr_t = ordered_json::json_pointer;
#include "json_view_test_helpers.hpp"
#if !defined(JSON_NOEXCEPTION)
using json_view_test::exception_of_call;
#endif
#include <array>
#include <cmath>
#include <cstring>
#include <cstdint>
#include <iterator>
#include <limits>
#include <map>
#include <memory>
#include <random>
#include <set>
#include <string>
#include <vector>
namespace
{
std::uint32_t rng()
{
static std::mt19937 generator(5295); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
// result_type is std::uint_fast32_t, which may be wider than 32 bits
const std::mt19937::result_type value = generator();
return static_cast<std::uint32_t>(value);
}
int r(int n)
{
return static_cast<int>(rng() % static_cast<unsigned>(n));
}
int counter = 0;
std::uint64_t bits(double x)
{
std::uint64_t b = 0;
std::memcpy(&b, &x, sizeof(b));
return b;
}
std::string random_string()
{
static const std::array<const char*, 15> pieces = {{"a", "Z", " ", "~", "\n", "\"", "\\", "/", "\xc3\xa9", "\xe3\x81\x82", "\xf0\x9f\x98\x80", "\x7f", "\x1f", "0", "key"}};
std::string s;
for (int i = r(3) == 0 ? r(30) : r(6); i > 0; --i)
{
s += pieces[static_cast<std::size_t>(r(15))];
}
return s;
}
ordered_json random_scalar()
{
switch (r(9))
{
case 0:
return nullptr;
case 1:
return r(2) == 0;
case 2:
return static_cast<std::int64_t>(rng()) - 2147483648LL;
case 3:
return (static_cast<std::uint64_t>(rng()) * 4294967296ULL) + rng();
case 4:
return static_cast<double>(static_cast<std::int32_t>(rng())) / (1 + r(1000));
case 5:
return r(2) == 0 ? 1e300 * (r(2) == 0 ? 1 : -1) : 5e-324;
case 6:
return -0.0;
default:
return random_string();
}
}
ordered_json random_value(int depth)
{
const int k = depth > 3 ? 4 : r(7);
if (k == 0)
{
ordered_json o = ordered_json::object();
for (int i = r(5); i > 0; --i)
{
o[random_string() + "#" + std::to_string(counter++)] = random_value(depth + 1);
}
return o;
}
if (k == 1)
{
ordered_json a = ordered_json::array();
for (int i = r(5); i > 0; --i)
{
a.push_back(random_value(depth + 1));
}
return a;
}
return random_scalar();
}
void collect(const ordered_json& j, const ptr_t& p, std::vector<ptr_t>& out)
{
out.push_back(p);
if (j.is_object())
{
for (const auto& kv : j.items())
{
collect(kv.value(), p / kv.key(), out);
}
}
else if (j.is_array())
{
for (std::size_t i = 0; i < j.size(); ++i)
{
collect(j[i], p / i, out);
}
}
}
// the edited view and the ordered_json value read the same through the whole
// read API
void compare(const ordered_json_editable_view& v, const ordered_json& j)
{
REQUIRE(v.type() == j.type());
REQUIRE(v.size() == j.size());
if (j.is_object())
{
auto jt = j.begin();
for (auto it = v.begin(); it != v.end(); ++it, ++jt)
{
CHECK(std::string(it.key().data(), it.key().size()) == jt.key());
CHECK(v[jt.key()].dump() == jt.value().dump());
CHECK(v.contains(jt.key()));
compare(*it, jt.value());
}
}
else if (j.is_array())
{
for (std::size_t i = 0; i < j.size(); ++i)
{
CHECK(v[i].dump() == j[i].dump());
}
std::size_t i = 0;
for (const auto e : v)
{
compare(e, j[i++]);
}
if (!j.empty())
{
CHECK(v.back().dump() == j.back().dump());
}
}
else if (j.is_string())
{
CHECK(v.get<std::string>() == j.get<std::string>());
}
else if (j.is_number_float())
{
CHECK(bits(v.get<double>()) == bits(j.get<double>()));
}
else if (j.is_number_integer())
{
CHECK(v.get<std::int64_t>() == j.get<std::int64_t>());
CHECK(v.get<std::uint64_t>() == j.get<std::uint64_t>());
}
}
// the text of j, in which some objects repeat a key of theirs after their
// members: lookups of the edited document find the first member of a key, so
// j (the first members) is what they must agree with, while parse() would
// keep the last value
std::string text_with_duplicates(const ordered_json& j)
{
if (j.is_object())
{
std::vector<std::string> keys;
for (const auto& kv : j.items())
{
keys.push_back(kv.key());
}
const auto repeated = [&keys]()
{
return ordered_json(keys[static_cast<std::size_t>(r(static_cast<int>(keys.size())))]).dump() + ":" + random_value(2).dump();
};
std::string text = "{";
bool first = true;
for (const auto& kv : j.items())
{
text += (first ? "" : ",") + ordered_json(kv.key()).dump() + ":" + text_with_duplicates(kv.value());
first = false;
}
for (int i = keys.empty() ? 0 : r(3); i > 0; --i)
{
text += "," + repeated();
}
return text + "}";
}
if (j.is_array())
{
std::string text = "[";
for (std::size_t i = 0; i < j.size(); ++i)
{
text += (i != 0 ? "," : "") + text_with_duplicates(j[i]);
}
return text + "]";
}
return j.dump();
}
// the number of members of an object view with this key, by iteration
std::size_t occurrences(const ordered_json_editable_view& v, const std::string& key)
{
std::size_t result = 0;
for (const auto& item : v.items())
{
result += std::string(item.key().data(), item.key().size()) == key ? 1u : 0u;
}
return result;
}
// every lookup of the edited view finds the first member of its key, which
// the iteration of the view shows
void check_lookups(const ordered_json_editable_view& v)
{
if (v.is_object())
{
std::set<std::string> seen;
for (const auto item : v.items())
{
const std::string key(item.key().data(), item.key().size());
if (!seen.insert(key).second)
{
continue; // (not the first member of the key)
}
const ptr_t ptr = ptr_t() / key;
CAPTURE(key)
const ordered_json expected = item.value().materialize();
const ordered_json_editable_view m = v[key];
REQUIRE(!m.is_discarded());
CHECK(m.materialize() == expected);
CHECK(v.at(key).materialize() == expected);
CHECK(v[ptr].materialize() == expected);
CHECK(v.at(ptr).materialize() == expected);
CHECK(v.contains(key));
CHECK(v.contains(ptr));
CHECK(v.count(key) == occurrences(v, key));
const auto it = v.find(key);
REQUIRE(it != v.end());
CHECK((*it).materialize() == expected);
CHECK(v.value(ptr, ordered_json(nullptr)) == expected);
if (expected.is_string())
{
CHECK(v.value(key, std::string("-")) == expected.get<std::string>());
}
check_lookups(m);
}
CHECK(v["missing#key"].is_discarded());
CHECK(!v.contains("missing#key"));
CHECK(v.find("missing#key") == v.end());
}
else if (v.is_array())
{
for (std::size_t i = 0; i < v.size(); ++i) // NOLINT(modernize-loop-convert): both v[i] and v.at(i) are tested
{
check_lookups(v[i]);
check_lookups(v.at(i));
}
}
}
// every lookup of the edited view finds what the first members of j hold
void check_first_members(const ordered_json_editable_view& v, const ordered_json& j)
{
REQUIRE(v.type() == j.type());
if (j.is_object())
{
for (const auto& kv : j.items())
{
const std::string& key = kv.key();
const ptr_t ptr = ptr_t() / key;
CAPTURE(key)
REQUIRE(v.contains(key));
CHECK(v.contains(ptr));
CHECK(v.count(key) == occurrences(v, key));
CHECK(v.find(key) != v.end());
CHECK(v.at(ptr).type() == kv.value().type());
check_first_members(v[key], kv.value());
check_first_members(v.at(key), kv.value());
check_first_members(v[ptr], kv.value());
}
CHECK(!v.contains("missing#key"));
}
else if (j.is_array())
{
REQUIRE(v.size() == j.size());
for (std::size_t i = 0; i < j.size(); ++i)
{
check_first_members(v[i], j[i]);
}
}
else
{
CHECK(v.materialize() == j);
}
}
// (for documents whose objects repeat keys: j holds the first members, the
// lookups must find them; dump(), size(), and iteration list all members, but
// materialize() keeps the last value of a key, as parse() does of the dump)
void check_duplicates(const ordered_json_editable_document& d, const ordered_json& j)
{
check_first_members(d.root(), j);
check_lookups(d.root());
CHECK(d.root().materialize() == ordered_json::parse(d.root().dump()));
}
void check_all(const ordered_json_editable_document& d, const ordered_json& j, bool deep)
{
const std::string text = d.root().dump();
REQUIRE(text == j.dump());
CHECK(d.root().materialize() == j);
if (deep)
{
CHECK(d.root().dump(2) == j.dump(2));
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(d.root() == j);
compare(d.root(), j);
// a fresh document of the text reads the same
const ordered_json_document fresh = ordered_json_document::parse(text);
CHECK(fresh.root() == d.root());
}
}
} // namespace
TEST_CASE("json_view edits: differential")
{
// random edits are applied to an ordered_json_editable_document and to the
// ordered_json parse() produces; after every edit both must serialize,
// materialize, and read back the same
for (int n = 0; n < 250; ++n)
{
// the last 100 documents repeat keys in some of their objects; j then
// holds the first member of each key, which the edits address
const bool duplicates = n >= 150;
ordered_json j = random_value(0);
if (r(4) == 0)
{
j = ordered_json::object({{"a", random_value(1)}, {"b", random_value(1)}});
}
if (duplicates)
{
j = ordered_json::parse(j.dump()); // (normalized as the document reads it)
}
const std::string text = duplicates ? text_with_duplicates(j) : j.dump(r(2) == 0 ? -1 : 2);
CAPTURE(text)
ordered_json_editable_document d = ordered_json_editable_document::parse(text);
if (!duplicates)
{
j = ordered_json::parse(text);
}
const ordered_json_document other = ordered_json_document::parse(random_value(0).dump());
const int edits = 30;
for (int e = 0; e < edits; ++e)
{
std::vector<ptr_t> paths;
collect(j, ptr_t(), paths);
const ptr_t p = paths[static_cast<std::size_t>(r(static_cast<int>(paths.size())))];
const ordered_json& target = j[p];
const ordered_json_editable_view tv = d.root().at(p);
const int op = r(12);
{
if (op == 0) // assign a scalar
{
const ordered_json v = random_scalar();
if (v.is_string() && r(2) == 0)
{
d.set(tv, v.get<std::string>());
}
else if (v.is_number_unsigned() && r(2) == 0)
{
d.set(tv, v.get<std::uint64_t>());
}
else
{
d.set(tv, v);
}
j[p] = v;
}
else if (op == 1) // assign a new array/object (or anything), sometimes via the pointer API
{
const ordered_json v = random_value(2);
if (r(2) == 0)
{
d.set(p, v);
}
else
{
d.set(tv, v);
}
j[p] = v;
}
else if (op == 2) // copy a value of the same document
{
const ptr_t& q = paths[static_cast<std::size_t>(r(static_cast<int>(paths.size())))];
const ordered_json v = j[q];
d.set(tv, d.root().at(q));
j[p] = v;
}
else if (op == 3) // copy a value of another document
{
d.set(tv, other.root());
j[p] = other.root().materialize();
}
else if ((op == 4 || op == 5) && target.is_object()) // set a member (new or existing)
{
std::string key = random_string() + "#" + std::to_string(counter++);
if (op == 5 && !target.empty())
{
key = std::next(target.begin(), r(static_cast<int>(target.size()))).key();
}
const ordered_json v = random_value(2);
d.set(tv, key, v);
j[p][key] = v;
}
else if (op == 6 && target.is_object() && !target.empty()) // erase a member
{
const std::string key = std::next(target.begin(), r(static_cast<int>(target.size()))).key();
if (r(2) == 0)
{
d.erase(tv, key);
}
else
{
d.erase(p / key);
}
j[p].erase(key);
}
else if (op == 7 && (target.is_array() || target.is_null())) // push_back
{
const ordered_json v = random_value(2);
d.push_back(tv, v);
j[p].push_back(v);
}
else if (op == 8 && target.is_array()) // insert
{
const auto i = static_cast<std::size_t>(r(static_cast<int>(target.size()) + 1));
const ordered_json v = random_value(2);
d.insert(tv, i, v);
j[p].insert(j[p].begin() + static_cast<std::ptrdiff_t>(i), v);
}
else if (op == 9 && target.is_array() && !target.empty()) // erase an element
{
const auto i = static_cast<std::size_t>(r(static_cast<int>(target.size())));
if (r(2) == 0)
{
d.erase(tv, i);
}
else
{
d.erase(p / i);
}
j[p].erase(i);
}
else if (op == 10 && target.is_array() && !target.empty()) // assign an element
{
const auto i = static_cast<std::size_t>(r(static_cast<int>(target.size())));
const ordered_json v = random_value(2);
d.set(tv, i, v);
j[p][i] = v;
}
else if (op == 11) // a held view sees the assignment
{
const ordered_json v = random_value(2);
const ordered_json_editable_view held = d.root().at(p);
d.set(p, v);
j[p] = v;
CHECK(held.dump() == v.dump());
}
else
{
continue;
}
}
CAPTURE(p.to_string())
CAPTURE(op)
if (duplicates)
{
check_duplicates(d, j);
}
else
{
check_all(d, j, e % 8 == 7 || e == edits - 1);
}
}
}
}
TEST_CASE("json_view edits: errors")
{
SECTION("an empty document")
{
json_editable_document d;
CHECK_THROWS_WITH_AS(d.set(d.root(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&);
}
json_editable_document d = json_editable_document::parse(R"({"o": {"a": 1}, "a": [1, 2], "n": 1, "z": null})");
const json_editable_view root = d.root();
const json_document other = json_document::parse("[1]");
json_editable_document other_editable = json_editable_document::parse("[1]");
CHECK_THROWS_WITH_AS(d.set(other_editable.root(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&);
CHECK_THROWS_WITH_AS(d.set(json_editable_view(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&);
CHECK_THROWS_WITH_AS(d.set(root["a"], "k", 1), "[json.exception.type_error.305] cannot use operator[] with a string argument with array", json::type_error&);
CHECK_THROWS_WITH_AS(d.set(root["o"], 0, 1), "[json.exception.type_error.305] cannot use operator[] with a numeric argument with object", json::type_error&);
CHECK_THROWS_WITH_AS(d.set(root["a"], 2, 1), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.set(root["a"], -1, 1), "[json.exception.out_of_range.401] array index -1 is out of range", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.push_back(root["o"], 1), "[json.exception.type_error.308] cannot use push_back() with object", json::type_error&);
CHECK_THROWS_WITH_AS(d.insert(root["n"], 0, 1), "[json.exception.type_error.309] cannot use insert() with number", json::type_error&);
CHECK_THROWS_WITH_AS(d.insert(root["a"], 3, 1), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.erase(root["n"], "k"), "[json.exception.type_error.307] cannot use erase() with number", json::type_error&);
CHECK_THROWS_WITH_AS(d.erase(root["o"], 0), "[json.exception.type_error.307] cannot use erase() with object", json::type_error&);
CHECK_THROWS_WITH_AS(d.erase(root["a"], 2), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("")), "[json.exception.out_of_range.405] JSON pointer has no parent", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("/missing/x")), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.set(json::json_pointer("/a/01"), 1), "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&);
CHECK_THROWS_WITH_AS(d.set(root, json_editable_view()), "[json.exception.type_error.302] type must be a value, but is discarded", json::type_error&);
CHECK_THROWS_WITH_AS(d.set(root, json::binary({1, 2})), "[json.exception.type_error.319] cannot store a binary value in a json_document", json::type_error&);
#if !defined(JSON_NOEXCEPTION)
// invalid UTF-8 is rejected when it enters the document, with the error
// basic_json::dump() reports for the same string
for (const std::string bad :
{"\xC3\x28", "a\xE2\x28\xA1", "\xF0\x28\x8C\xBC", "\xE2\x82", "x\xF0\x9F\x98", "\xFF", "\xED\xA0\x80", "\xC0\xAF"
})
{
CAPTURE(bad)
const std::string expected = exception_of_call([&]
{
const std::string text = json(bad).dump();
static_cast<void>(text);
});
CHECK(!expected.empty());
CHECK(exception_of_call([&] { d.set(root["z"], bad); }) == expected);
CHECK(exception_of_call([&] { d.set(root["o"], bad, 1); }) == expected);
CHECK(exception_of_call([&] { d.set(root["z"], json{{"k", bad}}); }) == expected);
}
#endif
// nothing of the failed edits is visible
CHECK(root.dump() == R"({"o":{"a":1},"a":[1,2],"n":1,"z":null})");
static_cast<void>(other);
}
#if !defined(JSON_NOEXCEPTION)
TEST_CASE("json_view edits: the size of a text arena")
{
using nlohmann::detail::view::text_capacity;
constexpr std::size_t limit = 0xFFFFFFFFu;
// grows by doubling, or to what is needed (plus some room)
CHECK(text_capacity(0, 0, 10) == 266);
CHECK(text_capacity(1000, 990, 20) == 2000);
CHECK(text_capacity(100, 100, 5000) == 5356);
// an arena beyond 2 GiB: doubling is clamped to 4 GiB - 1
CHECK(text_capacity(0x90000000u, 0x8FFFFFFFu, 2) == limit);
CHECK(text_capacity(limit, limit - 10, 10) == limit);
// exactly what fits is accepted, without room to spare
CHECK(text_capacity(100, 90, limit - 90) == limit);
CHECK(text_capacity(limit - 100, limit - 100, 100) == limit);
// what does not fit is an error
CHECK_THROWS_WITH_AS(text_capacity(100, 90, limit - 89), "[json.exception.out_of_range.416] edits of 4 GiB or more are not supported by json_document", json::out_of_range&);
CHECK_THROWS_WITH_AS(text_capacity(limit, limit, 1), "[json.exception.out_of_range.416] edits of 4 GiB or more are not supported by json_document", json::out_of_range&);
}
#endif
TEST_CASE("json_view edits: the chunks of new nodes")
{
using nlohmann::detail::view::alloc_nodes;
using nlohmann::detail::view::document_data;
const std::unique_ptr<document_data, document_data::deleter> d(document_data::create(0));
// the first chunk has room for 64 nodes, the next one for 128
const nlohmann::detail::view::node* const first = alloc_nodes(*d, 10);
CHECK(d->edits->chunks.size() == 1);
// a request of half of the next chunk (128 nodes) or more gets a chunk of
// its own
const nlohmann::detail::view::node* const large = alloc_nodes(*d, 100);
CHECK(d->edits->chunks.size() == 2);
CHECK(large == d->edits->chunks.back().get());
// and the rest of the first chunk is still used
CHECK(alloc_nodes(*d, 50) == first + 10);
CHECK(d->edits->chunks.size() == 2);
// a smaller request that does not fit starts a new (doubled) chunk; the
// call is a statement of its own, as the operands of == are unsequenced
// and MSVC reads chunks.back() before the call adds the chunk
const nlohmann::detail::view::node* const doubled = alloc_nodes(*d, 10);
CHECK(doubled == d->edits->chunks.back().get());
CHECK(d->edits->chunks.size() == 3);
CHECK(d->edits->chunk_end - d->edits->chunk_cur == 128 - 10);
CHECK(d->edits->bytes == (64 + 100 + 128) * sizeof(nlohmann::detail::view::node));
}
TEST_CASE("json_view edits: replacing arrays and objects by scalars")
{
// the first assignment switches the parent to links; the later ones do
// not need to look for the parent again
std::string text = "[";
json expected = json::array();
for (int i = 0; i < 300; ++i)
{
text += (i != 0 ? ",[" : "[") + std::to_string(i) + ",{\"k\":" + std::to_string(i) + "}]";
expected.push_back(json::array({i, json{{"k", i}}}));
}
text += ']';
json_editable_document d = json_editable_document::parse(text);
CHECK(d.root().dump() == expected.dump());
for (int i = 0; i < 300; i += 2)
{
d.set(d.root()[static_cast<std::size_t>(i)], i);
expected[static_cast<std::size_t>(i)] = i;
}
CHECK(d.root().dump() == expected.dump());
for (int i = 1; i < 300; i += 2) // (elements that are still in the parsed layout of their parent)
{
d.set(d.root()[static_cast<std::size_t>(i)][1], "x"); // replaces an object
expected[static_cast<std::size_t>(i)][1] = "x";
}
CHECK(d.root().dump() == expected.dump());
// new values, and values in new values
d.push_back(d.root(), json::parse(R"([[1,2],{"a":[3]}])"));
expected.push_back(json::parse(R"([[1,2],{"a":[3]}])"));
d.set(d.root()[300][0], 7);
expected[300][0] = 7;
d.insert(d.root(), 0, json::array({1, 2}));
expected.insert(expected.begin(), json::array({1, 2}));
d.set(d.root()[0], nullptr);
expected[0] = nullptr;
d.set(d.root()[301], 5);
expected[301] = 5;
CHECK(d.root().dump() == expected.dump());
CHECK(d.root().materialize() == expected);
}
namespace
{
// reads of "a" in an object that repeats it: the first member wins, however the
// object is stored
template<typename View>
void check_first_wins(const View& o, int expected)
{
CAPTURE(expected)
CHECK(o["a"].template get<int>() == expected);
CHECK(o.at("a").template get<int>() == expected);
CHECK(o.find("a")->template get<int>() == expected);
CHECK(o.value("a", -1) == expected);
CHECK(o[json::json_pointer("/a")].template get<int>() == expected);
CHECK(o.at(json::json_pointer("/a")).template get<int>() == expected);
CHECK(o.value(json::json_pointer("/a"), -1) == expected);
CHECK(o.contains("a"));
CHECK(o.contains(json::json_pointer("/a")));
std::size_t members_a = 0;
for (const auto& item : o.items())
{
members_a += std::string(item.key().data(), item.key().size()) == "a" ? 1u : 0u;
}
CHECK(o.count("a") == members_a); // every member, unlike the lookups
}
} // namespace
TEST_CASE("json_view edits: duplicate keys")
{
SECTION("an object in its parsed layout, then moved by edits")
{
json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})");
check_first_wins(d.root(), 1);
CHECK(d.root().size() == 3);
CHECK(d.root().dump() == R"({"a":1,"b":2,"a":3})");
CHECK(d.root().materialize()["a"] == 3); // (parse() keeps the last value)
// an edit of a value does not move the object
d.set(d.root()["b"], 5);
check_first_wins(d.root(), 1);
d.set(d.root()["a"], 4); // the member that lookups find
check_first_wins(d.root(), 4);
CHECK(d.root().dump() == R"({"a":4,"b":5,"a":3})");
// an appended member moves it
d.set(d.root(), "c", true);
check_first_wins(d.root(), 4);
CHECK(d.root().size() == 4);
CHECK(d.root().dump() == R"({"a":4,"b":5,"a":3,"c":true})");
d.set(d.root()["a"], 6);
check_first_wins(d.root(), 6);
CHECK(d.root().dump() == R"({"a":6,"b":5,"a":3,"c":true})");
d.set(json::json_pointer("/a"), 7);
check_first_wins(d.root(), 7);
}
SECTION("set assigns the member that lookups find")
{
// the key stays where its first occurrence is, the later members are dropped
ordered_json_editable_document d = ordered_json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3, "c": 4, "a": 5})");
const ordered_json_editable_view held = d.root()["a"];
CHECK(held.get<int>() == 1);
const ordered_json_editable_view assigned = d.set(d.root(), "a", "x");
CHECK(held.get<std::string>() == "x");
CHECK(assigned.get<std::string>() == "x");
CHECK(d.root().dump() == R"({"a":"x","b":2,"c":4})");
CHECK(d.root().materialize() == ordered_json::parse(R"({"a": "x", "b": 2, "c": 4})"));
CHECK(d.root().size() == 3);
// as for the object that parse() makes of the text
ordered_json j = ordered_json::parse(R"({"a": 1, "b": 2, "a": 3, "c": 4, "a": 5})");
j["a"] = "x";
CHECK(d.root().materialize().dump() == j.dump());
// through a pointer, below a duplicate
d = ordered_json_editable_document::parse(R"({"a": {"x": 1}, "b": 2, "a": {"x": 3}})");
d.set(ptr_t("/a/x"), 4);
CHECK(d.root().dump() == R"({"a":{"x":4},"b":2,"a":{"x":3}})");
d.set(ptr_t("/a"), 0);
CHECK(d.root().dump() == R"({"a":0,"b":2})");
}
SECTION("erase removes every member")
{
json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})");
d.set(d.root(), "c", 4);
check_first_wins(d.root(), 1);
CHECK(d.erase(d.root(), "a") == 2);
CHECK(d.root().dump() == R"({"b":2,"c":4})");
CHECK(!d.root().contains("a"));
CHECK(d.root()["a"].is_discarded());
CHECK(d.erase(d.root(), "a") == 0);
d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})");
CHECK(d.erase(json::json_pointer("/a")) == 2);
CHECK(d.root().dump() == R"({"b":2})");
}
SECTION("a large object with an index")
{
std::string text = R"({"a":0,"k7":"first")";
for (int i = 0; i < 200; ++i)
{
text += ",\"k" + std::to_string(i) + "\":" + std::to_string(i);
}
text += R"(,"a":1,"k7":"last","a":2})";
json_editable_document d = json_editable_document::parse(text);
check_first_wins(d.root(), 0);
CHECK(d.root()["k7"].get_string() == "first");
CHECK(d.root()["k199"].get<int>() == 199);
// a value assigned in place: the index stays in use
d.set(d.root()["a"], 3);
check_first_wins(d.root(), 3);
d.set(d.root()["k7"], "changed");
CHECK(d.root()["k7"].get_string() == "changed");
CHECK(d.root()["k199"].get<int>() == 199);
// an appended member moves the members: the lookup scans them
d.set(d.root(), "new", 1);
check_first_wins(d.root(), 3);
CHECK(d.root()["k7"].get_string() == "changed");
CHECK(d.root()["k7"].get_string() == d.root().at("k7").get_string());
CHECK(d.root()["k199"].get<int>() == 199);
CHECK(d.root()["new"].get<int>() == 1);
// a value replaced by a container, in an object that was not moved
d = json_editable_document::parse(text);
d.set(d.root()["a"], json{{"x", 1}});
CHECK(d.root()["a"]["x"].get<int>() == 1);
CHECK(d.root()["k7"].get_string() == "first");
CHECK(d.erase(d.root(), "k7") == 3); // (the key occurs three times)
CHECK(d.root()["k7"].is_discarded());
CHECK(d.root()["a"]["x"].get<int>() == 1);
}
SECTION("objects in moved arrays and in new values")
{
json_editable_document d = json_editable_document::parse(R"([{"a": 1, "a": 2}, {"a": 3, "b": 0, "a": 4}])");
check_first_wins(d.root()[0], 1);
check_first_wins(d.root()[1], 3);
d.insert(d.root(), 0, json::parse(R"({"a": 0})"));
d.push_back(d.root(), json::parse(R"({"a": 5, "a": 6})")); // (a basic_json value has one member)
check_first_wins(d.root()[1], 1);
check_first_wins(d.root()[2], 3);
CHECK(d.root()[3]["a"].get<int>() == 6);
d.set(d.root()[1], "a", 8);
check_first_wins(d.root()[1], 8);
CHECK(d.root()[1].dump() == R"({"a":8})");
d.erase(d.root(), 0);
check_first_wins(d.root()[1], 3);
CHECK(d.root().dump() == R"([{"a":8},{"a":3,"b":0,"a":4},{"a":6}])");
}
SECTION("values of other documents")
{
const json_document source = json_document::parse(R"({"list": [{"a": 1, "a": 2}], "o": {"b": {"a": 3, "a": 4}, "a": 5, "a": 6}})");
json_editable_document d = json_editable_document::parse("{}");
d.set(d.root(), "copy", source.root()["list"]);
d.set(d.root(), "o", source.root()["o"]);
// (the copies keep the repeated members)
CHECK(d.root().dump() == R"({"copy":[{"a":1,"a":2}],"o":{"b":{"a":3,"a":4},"a":5,"a":6}})");
check_first_wins(d.root()["copy"][0], 1);
check_first_wins(d.root()["o"]["b"], 3);
check_first_wins(d.root()["o"], 5);
d.set(d.root()["o"]["b"], "c", 0);
check_first_wins(d.root()["o"]["b"], 3);
d.set(d.root()["o"], "d", 0);
check_first_wins(d.root()["o"], 5);
CHECK(d.root()["o"]["b"]["a"].get<int>() == 3);
CHECK(d.root()["o"]["d"].get<int>() == 0);
}
}
+357
View File
@@ -0,0 +1,357 @@
// __ _____ _____ _____
// __| | __| | | | 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_view.hpp>
using nlohmann::json;
using nlohmann::json_document;
using nlohmann::json_editable_document;
using nlohmann::json_editable_view;
#include "json_view_test_helpers.hpp"
#if !defined(JSON_NOEXCEPTION)
using json_view_test::exception_of_call;
#endif
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <map>
#include <string>
#include <utility>
#include <vector>
// These tests were split off unit-json_view_edit.cpp, whose object file got too
// large for the MinGW linker (see json_view_test_helpers.hpp).
TEST_CASE("json_view edits: views and values")
{
SECTION("a value that is no longer part of the document")
{
json_editable_document d = json_editable_document::parse("[[[1,2]]]");
const json_editable_view inner = d.root()[0][0];
d.set(d.root()[0], json::array({7}));
d.set(inner, 5);
CHECK(d.root().dump() == "[[7]]");
CHECK(inner.get<int>() == 5);
}
SECTION("views keep referring to their value")
{
json_editable_document d = json_editable_document::parse(R"({"a": [10, 20, 30], "b": {"c": "text"}})");
const json_editable_view a = d.root()["a"];
const json_editable_view twenty = a[1];
const json_editable_view c = d.root()["b"]["c"];
d.insert(a, 0, 5);
d.push_back(a, 40);
CHECK(twenty.get<int>() == 20);
CHECK(a[2].get<int>() == 20);
d.erase(a, 2);
CHECK(twenty.get<int>() == 20); // an erased value keeps its last value
d.set(c, 7);
CHECK(c.get<int>() == 7); // a held view sees an assignment
d.set(d.root()["b"], json::array({1, 2}));
CHECK(d.root()["b"].dump() == "[1,2]");
CHECK(d.root().dump() == R"({"a":[5,10,30,40],"b":[1,2]})");
CHECK(d.root()["a"][0].source_offset() == static_cast<std::size_t>(-1)); // a new value
CHECK(d.root()["a"][1].source_offset() != static_cast<std::size_t>(-1));
}
SECTION("strings stay valid while more edits come")
{
json_editable_document d = json_editable_document::parse("[]");
const auto first = d.push_back(d.root(), std::string(100, 'x')).get_string();
for (int i = 0; i < 1000; ++i)
{
d.push_back(d.root(), std::string(static_cast<std::size_t>(i % 50), 'y'));
}
CHECK(std::string(first.data(), first.size()) == std::string(100, 'x'));
CHECK(d.root().size() == 1001);
}
SECTION("numbers")
{
json_editable_document d = json_editable_document::parse(R"([1.50, 1E2, 3])");
d.set(d.root(), 2, 0.1);
d.push_back(d.root(), std::numeric_limits<double>::quiet_NaN());
d.push_back(d.root(), -std::numeric_limits<double>::infinity());
d.push_back(d.root(), (std::numeric_limits<std::uint64_t>::max)());
d.push_back(d.root(), (std::numeric_limits<std::int64_t>::min)());
CHECK(d.root().dump() == "[1.5,100.0,0.1,null,null,18446744073709551615,-9223372036854775808]");
CHECK(d.root().dump(-1, ' ', false, json_editable_view::number_format::source) == "[1.50,1E2,0.1,null,null,18446744073709551615,-9223372036854775808]");
CHECK(std::isnan(d.root()[3].get<double>()));
CHECK(std::isinf(d.root()[4].get<double>()));
CHECK(d.root()[2].number_token() == "0.1");
CHECK(d.root()[5].get<std::uint64_t>() == 18446744073709551615u);
CHECK(d.root()[6].number_token() == "-9223372036854775808");
CHECK(d.root().materialize().dump() == json::parse(R"([1.5, 100.0, 0.1, null, null, 18446744073709551615, -9223372036854775808])").dump());
}
SECTION("numbers of other float types")
{
// doubles have their own path to the output; other float types are
// written as basic_json writes them, non-finite values as null
using json_float = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
using document_float = nlohmann::basic_json_document<json_float, true>;
document_float d = document_float::parse("[1.5]");
d.push_back(d.root(), std::numeric_limits<float>::quiet_NaN());
d.push_back(d.root(), -std::numeric_limits<float>::infinity());
CHECK(d.root().dump() == "[1.5,null,null]");
CHECK(d.root().dump(2) == json_float::parse("[1.5, null, null]").dump(2));
}
SECTION("nulls become containers, and the root can be replaced")
{
json_editable_document d = json_editable_document::parse("[null, null]");
d.set(d.root()[0], "k", 1);
d.push_back(d.root()[1], true);
CHECK(d.root().dump() == R"([{"k":1},[true]])");
d.set(d.root(), "scalar");
CHECK(d.root().dump() == R"("scalar")");
d.set(d.root(), json{{"x", {1, 2}}});
d.set(json::json_pointer("/x/-"), 3);
d.set(json::json_pointer("/x/3"), 4); // the size of the array appends too
d.set(json::json_pointer("/y"), false);
CHECK(d.root().dump() == R"({"x":[1,2,3,4],"y":false})");
CHECK(d.erase(json::json_pointer("/x/0")) == 1);
CHECK(d.erase(json::json_pointer("/y")) == 1);
CHECK(d.erase(json::json_pointer("/nothing")) == 0);
CHECK(d.root().dump() == R"({"x":[2,3,4]})");
}
SECTION("duplicate keys")
{
json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})");
d.set(d.root(), "a", 4); // the first member is assigned, the others dropped
CHECK(d.root().dump() == R"({"a":4,"b":2})");
d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})");
CHECK(d.erase(d.root(), "a") == 2);
CHECK(d.root().dump() == R"({"b":2})");
}
SECTION("values from other documents")
{
const json_document source = json_document::parse(R"({"list": [1, "two", {"three": 3.5}], "text": "a\nb"})");
json_editable_document edited = json_editable_document::parse("[0]");
edited.set(edited.root(), 0, json{{"inner", {1, 2}}});
json_editable_document d = json_editable_document::parse("{}");
d.set(d.root(), "copy", source.root()["list"]);
d.set(d.root(), "text", source.root()["text"]);
d.set(d.root(), "edited", edited.root()[0]);
d.set(d.root(), "self", d.root()["copy"]);
// (a raw string with a backslash must not be a macro argument: MSVC C2017)
const std::string expected = R"({"copy":[1,"two",{"three":3.5}],"text":"a\nb","edited":{"inner":[1,2]},"self":[1,"two",{"three":3.5}]})";
CHECK(d.root().dump() == expected);
CHECK(d.root()["copy"] == source.root()["list"]);
CHECK(source.root()["list"] == d.root()["self"]);
CHECK(d.root() != source.root());
}
SECTION("large objects")
{
std::string text = "{";
for (int i = 0; i < 200; ++i)
{
text += (i != 0 ? ",\"k" : "\"k") + std::to_string(i) + "\":" + std::to_string(i);
}
text += '}';
json_editable_document d = json_editable_document::parse(text);
d.set(d.root(), "k7", "seven"); // assigned in place: the index stays in use
CHECK(d.root()["k7"].get_string() == "seven");
d.set(d.root(), "new", 1); // appended: the members move, the lookup is linear
CHECK(d.root()["new"].get<int>() == 1);
CHECK(d.root()["k199"].get<int>() == 199);
d.erase(d.root(), "k0");
CHECK(!d.root().contains("k0"));
CHECK(d.root().size() == 200);
}
SECTION("reuse and memory")
{
json_editable_document d = json_editable_document::parse("[1, 2, 3]");
const std::size_t before = d.memory_usage();
for (int i = 0; i < 100; ++i)
{
d.push_back(d.root(), "some text");
}
CHECK(d.memory_usage() > before);
const json_editable_view first = d.root()[0];
d.shrink_to_fit(); // (with edits, the index stays in place)
CHECK(first.get<int>() == 1);
d.read(std::string("[true]"));
CHECK(d.root().dump() == "[true]");
d.push_back(d.root(), false);
CHECK(d.root().dump() == "[true,false]");
}
}
TEST_CASE("json_view edits: deeply nested values")
{
// copying a value into a document must not recurse per nesting level
const std::size_t depth = 100000;
const std::string brackets = std::string(depth, '[') + std::string(depth, ']');
std::string braces;
for (std::size_t i = 0; i < depth; ++i)
{
braces += "{\"a\":";
}
braces += '1';
braces += std::string(depth, '}');
SECTION("a view of a read-only document")
{
const json_document source = json_document::parse(brackets);
json_editable_document d = json_editable_document::parse("[]");
d.push_back(d.root(), source.root());
CHECK(d.root().dump() == "[" + brackets + "]");
}
SECTION("a view of an editable document")
{
const json_editable_document source = json_editable_document::parse(braces);
json_editable_document d = json_editable_document::parse("{}");
d.set(d.root(), "deep", source.root());
CHECK(d.root().dump() == "{\"deep\":" + braces + "}");
}
SECTION("a view of an edited document (values behind links)")
{
const json_document source = json_document::parse(brackets);
json_editable_document edited = json_editable_document::parse("[[]]");
edited.push_back(edited.root()[0], source.root());
edited.push_back(edited.root(), source.root());
json_editable_document d = json_editable_document::parse("null");
d.set(d.root(), edited.root());
CHECK(d.root().dump() == "[[" + brackets + "]," + brackets + "]");
}
SECTION("a basic_json value")
{
json deep = json::array();
json* inner = &deep;
for (std::size_t i = 1; i < depth; ++i)
{
inner->push_back(json::array());
inner = &inner->back();
}
json_editable_document d = json_editable_document::parse("[]");
d.push_back(d.root(), deep);
CHECK(d.root().dump() == "[" + brackets + "]");
}
SECTION("a basic_json value with objects")
{
json deep = 1;
for (std::size_t i = 0; i < depth; ++i)
{
json outer = json::object();
outer["a"] = std::move(deep);
deep = std::move(outer);
}
json_editable_document d = json_editable_document::parse("{}");
d.set(d.root(), "deep", deep);
CHECK(d.root().dump() == "{\"deep\":" + braces + "}");
}
}
#if !defined(JSON_NOEXCEPTION)
TEST_CASE("json_view edits: pointers below a null value")
{
// a null value on the way becomes what basic_json makes of it: an array
// for "-" and for digits, an object otherwise
struct test_case
{
const char* document;
const char* pointer;
};
const std::array<test_case, 16> cases =
{
{
{R"({"a":null})", "/a/0"},
{R"({"a":null})", "/a/-"},
{R"({"a":null})", "/a/3"},
{R"({"a":null})", "/a/x"},
{R"({"a":null})", "/a/+1"},
{R"({"a":null})", "/a/01"},
{R"({"a":null})", "/a/"},
{R"({"a":{"b":null}})", "/a/b/1"},
{R"({"a":{"b":null}})", "/a/b/-"},
{R"({"a":[null]})", "/a/0/0"},
{R"({"a":[null,null]})", "/a/1/k"},
{R"([null])", "/0"},
{"null", "/0"},
{"null", "/-"},
{"null", "/k"},
{"null", ""},
}
};
for (const test_case& c : cases)
{
CAPTURE(c.document)
CAPTURE(c.pointer)
json expected = json::parse(c.document);
const std::string error = exception_of_call([&]
{
expected[json::json_pointer(c.pointer)] = 1;
});
json_editable_document d = json_editable_document::parse(c.document);
if (error.empty())
{
d.set(json::json_pointer(c.pointer), 1);
CHECK(d.root().dump() == expected.dump());
CHECK(d.root().materialize() == expected);
}
else
{
// the same error, and the document is not changed
CHECK(exception_of_call([&] { d.set(json::json_pointer(c.pointer), 1); }) == error);
CHECK(d.root().dump() == json::parse(c.document).dump());
}
}
}
TEST_CASE("json_view edits: strings of other documents are checked")
{
// A document borrows the text it was parsed from, and sees later changes
// of the text: a way to get ill-formed UTF-8 into a view. Copying it into
// an editable document is an error, as for any other string.
std::string text = R"({"key":"abc","list":["abc"]})";
const json_document source = json_document::parse(text);
const auto message_of = [](const std::string & bad)
{
return exception_of_call([&]
{
const std::string dumped = json(bad).dump();
static_cast<void>(dumped);
});
};
json_editable_document d = json_editable_document::parse("[1]");
d.push_back(d.root(), source.root());
CHECK(d.root().dump() == R"([1,{"key":"abc","list":["abc"]}])");
text[text.find("abc") + 1] = '\xC3'; // "a\xC3c"
text[text.rfind("abc") + 1] = '\xC3';
const std::string bad_value = message_of(std::string("a\xC3" "c"));
CHECK(!bad_value.empty());
CHECK(exception_of_call([&] { d.push_back(d.root(), source.root()["key"]); }) == bad_value);
CHECK(exception_of_call([&] { d.set(d.root()[0], source.root()["list"][0]); }) == bad_value);
CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()["list"]); }) == bad_value);
CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()); }) == bad_value);
text[text.find("key") + 1] = '\xC3'; // a key is checked as well
const std::string bad_key = message_of(std::string("k\xC3" "y"));
CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()); }) == bad_key);
CHECK(exception_of_call([&] { d.push_back(d.root(), source.root()); }) == bad_key);
// nothing of the failed edits is visible
CHECK(d.root().dump() == R"([1,{"key":"abc","list":["abc"]}])");
}
#endif
File diff suppressed because it is too large. Load diff
+47
View File
@@ -0,0 +1,47 @@
// __ _____ _____ _____
// __| | __| | | | 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"
// All other tests keep the library's macros (JSON_TEST_KEEP_MACROS). This one
// includes json_view.hpp as users do, so that json.hpp undefines its macros
// (JSON_HAS_CPP_17, JSON_STRICT_NUL_HANDLING, ...) before the view is compiled.
#undef JSON_TEST_KEEP_MACROS
#include <nlohmann/json_view.hpp>
#include <string>
#include <type_traits>
#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)
#include <string_view>
#define JSON_VIEW_TEST_HAS_STRING_VIEW 1
#else
#define JSON_VIEW_TEST_HAS_STRING_VIEW 0
#endif
// the view's own macros do not leak
#if defined(NLOHMANN_VIEW_LIKELY) || defined(NLOHMANN_VIEW_UNLIKELY) || defined(NLOHMANN_VIEW_ALWAYS_INLINE) || defined(NLOHMANN_VIEW_NOINLINE) \
|| defined(NLOHMANN_VIEW_NODISCARD) || defined(NLOHMANN_VIEW_THROW) || defined(NLOHMANN_VIEW_HAS_CPP_17) || defined(NLOHMANN_VIEW_LITTLE_ENDIAN) \
|| defined(NLOHMANN_VIEW_REPEAT16)
#error "json_view.hpp leaks a macro"
#endif
TEST_CASE("json_view without the library's macros")
{
// (this file also gets C++17 builds: it mentions JSON_HAS_CPP_17)
#if JSON_VIEW_TEST_HAS_STRING_VIEW
CHECK(std::is_same<nlohmann::json_view::string_view_t, std::string_view>::value);
#endif
const std::string text = "[1, 2.5, \"x\"]";
const nlohmann::json_document d = nlohmann::json_document::parse(text);
CHECK(d.root().size() == 3);
CHECK(d.root().materialize() == nlohmann::json::parse(text));
// the NUL handling of the library's configuration
const std::string with_nul("[1]\0x", 5);
CHECK(nlohmann::json_document::accept(with_nul) == nlohmann::json::accept(with_nul));
}
+296
View File
@@ -0,0 +1,296 @@
// __ _____ _____ _____
// __| | __| | | | 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
// MessagePack tests with basic_json specializations other than nlohmann::json;
// they are kept apart from unit-msgpack.cpp because every specialization adds
// many sections to the object file, and MinGW cannot link objects with more
// than 65535 sections (see the clang job in .github/workflows/windows.yml)
#include "doctest_compatibility.h"
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <cstdint> // SIZE_MAX, UINT32_MAX
#include <functional>
#include <limits>
#include <map>
#include <memory>
#include <string>
#include <utility>
#include <vector>
// the fake sizes below do not fit into a 32-bit std::size_t
// with clang and libstdc++ 10, the std::filesystem::path conversion that
// C++17 builds consider for every string type is ambiguous for a class
// derived from std::string, so the string case is not tested there
#if !(defined(__clang__) && defined(_GLIBCXX_RELEASE) && _GLIBCXX_RELEASE < 11)
#define JSON_TEST_BEYOND_UINT32_STRING 1
#endif
#if SIZE_MAX > UINT32_MAX
template<typename T, typename A = std::allocator<T>>
struct huge_array : std::vector<T, A>
{
using base = std::vector<T, A>;
using base::base;
bool fake_size = false;
std::size_t size() const noexcept
{
if (fake_size)
{
return (std::numeric_limits<std::uint32_t>::max)() + 1ULL;
}
return base::size();
}
};
using huge_array_json = nlohmann::json::with_array_t<huge_array>;
TEST_CASE("MessagePack Size above uint32 for array")
{
huge_array_json j = huge_array_json::array();
j.push_back(1);
j.push_back(2);
j.push_back(3);
auto& array = j.get_ref<huge_array_json::array_t&>();
array.fake_size = true;
// write into a caller-owned vector: to_msgpack(j) reserves space based on
// the (faked) element count, which fails with bad_alloc on Windows
std::vector<std::uint8_t> result;
CHECK_THROWS_WITH_AS(
huge_array_json::to_msgpack(j, result),
"[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295",
json::out_of_range&);
array.fake_size = false;
}
template<typename K, typename V,
typename C = std::less<K>,
typename A = std::allocator<std::pair<const K, V>>>
struct huge_map : std::map<K, V, C, A>
{
using base = std::map<K, V, C, A>;
using base::base;
bool fake_size = false;
std::size_t size() const noexcept
{
if (fake_size)
{
return static_cast<std::size_t>(UINT32_MAX) + 1ULL;
}
return base::size();
}
};
using huge_object_json = nlohmann::json::with_object_t<huge_map>;
TEST_CASE("MessagePack Size above uint32 for object")
{
huge_object_json j = huge_object_json::object();
j["one"] = 1;
j["two"] = 2;
auto& object = j.get_ref<huge_object_json::object_t&>();
object.fake_size = true;
// write into a caller-owned vector: to_msgpack(j) reserves space based on
// the (faked) element count, which fails with bad_alloc on Windows
std::vector<std::uint8_t> result;
CHECK_THROWS_WITH_AS(
huge_object_json::to_msgpack(j, result),
"[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295",
json::out_of_range&);
object.fake_size = false;
}
#ifdef JSON_TEST_BEYOND_UINT32_STRING
struct huge_string : std::string
{
using std::string::string;
std::size_t size() const noexcept // NOLINT(readability-convert-member-functions-to-static)
{
return static_cast<std::size_t>(UINT32_MAX) + 1ULL;
}
};
using huge_string_json = nlohmann::json::with_string_t<huge_string>;
TEST_CASE("MessagePack Size above uint32 for string")
{
const huge_string_json j = "hello";
CHECK_THROWS_WITH_AS(
huge_string_json::to_msgpack(j),
"[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295",
json::out_of_range&);
}
#endif
struct huge_binary : std::vector<std::uint8_t>
{
using std::vector<std::uint8_t>::vector;
std::size_t size() const noexcept // NOLINT(readability-convert-member-functions-to-static)
{
return static_cast<std::size_t>(UINT32_MAX) + 1ULL;
}
};
using huge_binary_json = nlohmann::json::with_binary_t<huge_binary>;
TEST_CASE("MessagePack Size above uint32 for binary")
{
huge_binary_json j = huge_binary_json::binary(huge_binary{});
j.get_binary().push_back(0x01);
j.get_binary().push_back(0x02);
CHECK_THROWS_WITH_AS(
huge_binary_json::to_msgpack(j),
"[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295",
json::out_of_range&);
}
#endif
namespace
{
// types that report a size beyond UINT32_MAX without allocating that much
// memory, so the MessagePack length limit can be tested cheaply; see the
// similar types in unit-bson.cpp
std::size_t beyond_uint32_size()
{
return static_cast<std::size_t>((std::numeric_limits<std::uint32_t>::max)()) + 1;
}
class beyond_uint32_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)
{
return beyond_uint32_size();
}
};
#ifdef JSON_TEST_BEYOND_UINT32_STRING
class beyond_uint32_string_t : public std::string
{
public:
using std::string::string;
size_type size() const noexcept // NOLINT(readability-convert-member-functions-to-static)
{
return beyond_uint32_size();
}
};
using beyond_uint32_string_json = nlohmann::json::with_string_t<beyond_uint32_string_t>;
#endif
using beyond_uint32_binary_json = nlohmann::json::with_binary_t<beyond_uint32_binary_t>;
} // namespace
TEST_CASE("MessagePack lengths beyond UINT32_MAX cannot be serialized")
{
// MessagePack stores the length of a string, binary value, array, or
// object in at most 32 bits; a larger one used to be written without any
// length at all
#if SIZE_MAX > UINT32_MAX
{
const char* const expected = "[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295";
const beyond_uint32_binary_json binary = beyond_uint32_binary_json::binary(beyond_uint32_binary_t{});
CHECK_THROWS_WITH_AS(beyond_uint32_binary_json::to_msgpack(binary), expected, beyond_uint32_binary_json::out_of_range&);
const beyond_uint32_binary_json ext = beyond_uint32_binary_json::binary(beyond_uint32_binary_t{}, 42);
CHECK_THROWS_WITH_AS(beyond_uint32_binary_json::to_msgpack(ext), expected, beyond_uint32_binary_json::out_of_range&);
#ifdef JSON_TEST_BEYOND_UINT32_STRING
// created from its type rather than from a beyond_uint32_string_t:
// that would consider the std::filesystem::path conversion, which
// libstdc++ 10 cannot decide for a class derived from std::string
const beyond_uint32_string_json string(beyond_uint32_string_json::value_t::string);
CHECK_THROWS_WITH_AS(beyond_uint32_string_json::to_msgpack(string), expected, beyond_uint32_string_json::out_of_range&);
#endif
}
#endif
}
TEST_CASE("MessagePack numbers use the active union member (see #5644)")
{
// when number_integer_t is narrower than number_unsigned_t, to_msgpack()
// used to read the union member that was not the active one, writing
// wrong bytes for some values; std::int64_t/std::uint64_t (the default
// types, where both members have the same width) were not affected
using int32_json = nlohmann::json::with_integers_t<std::int32_t, std::uint64_t>;
using int16_json = nlohmann::json::with_integers_t<std::int16_t, std::uint64_t>;
SECTION("number_integer_t = std::int32_t")
{
SECTION("6442450944 (uint 64; the low 32 bits used to be sign-extended)")
{
const int32_json j = 6442450944ULL;
CHECK(j.is_number_unsigned());
std::vector<uint8_t> const expected{0xcf, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00};
const auto result = int32_json::to_msgpack(j);
CHECK(result == expected);
CHECK(int32_json::from_msgpack(result) == j);
}
SECTION("4294967496 (uint 64; the low 32 bits used to be the whole value)")
{
const int32_json j = 4294967496ULL;
CHECK(j.is_number_unsigned());
std::vector<uint8_t> const expected{0xcf, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xc8};
const auto result = int32_json::to_msgpack(j);
CHECK(result == expected);
CHECK(int32_json::from_msgpack(result) == j);
}
}
SECTION("number_integer_t = std::int16_t, 98304 (uint 32)")
{
const int16_json j = 98304ULL;
CHECK(j.is_number_unsigned());
std::vector<uint8_t> const expected{0xce, 0x00, 0x01, 0x80, 0x00};
const auto result = int16_json::to_msgpack(j);
CHECK(result == expected);
CHECK(int16_json::from_msgpack(result) == j);
}
SECTION("default types (std::int64_t/std::uint64_t) are unaffected")
{
const json j = 4294967496ULL;
CHECK(j.is_number_unsigned());
std::vector<uint8_t> const expected{0xcf, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xc8};
const auto result = json::to_msgpack(j);
CHECK(result == expected);
CHECK(json::from_msgpack(result) == j);
}
}
-270
View File
@@ -2098,276 +2098,6 @@ TEST_CASE("MessagePack roundtrips" * doctest::skip())
}
}
// the fake sizes below do not fit into a 32-bit std::size_t
// with clang and libstdc++ 10, the std::filesystem::path conversion that
// C++17 builds consider for every string type is ambiguous for a class
// derived from std::string, so the string case is not tested there
#if !(defined(__clang__) && defined(_GLIBCXX_RELEASE) && _GLIBCXX_RELEASE < 11)
#define JSON_TEST_BEYOND_UINT32_STRING 1
#endif
#if SIZE_MAX > UINT32_MAX
template<typename T, typename A = std::allocator<T>>
struct huge_array : std::vector<T, A>
{
using base = std::vector<T, A>;
using base::base;
bool fake_size = false;
std::size_t size() const noexcept
{
if (fake_size)
{
return (std::numeric_limits<std::uint32_t>::max)() + 1ULL;
}
return base::size();
}
};
using huge_array_json = nlohmann::json::with_array_t<huge_array>;
TEST_CASE("MessagePack Size above uint32 for array")
{
huge_array_json j = huge_array_json::array();
j.push_back(1);
j.push_back(2);
j.push_back(3);
auto& array = j.get_ref<huge_array_json::array_t&>();
array.fake_size = true;
// write into a caller-owned vector: to_msgpack(j) reserves space based on
// the (faked) element count, which fails with bad_alloc on Windows
std::vector<std::uint8_t> result;
CHECK_THROWS_WITH_AS(
huge_array_json::to_msgpack(j, result),
"[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295",
json::out_of_range&);
array.fake_size = false;
}
template<typename K, typename V,
typename C = std::less<K>,
typename A = std::allocator<std::pair<const K, V>>>
struct huge_map : std::map<K, V, C, A>
{
using base = std::map<K, V, C, A>;
using base::base;
bool fake_size = false;
std::size_t size() const noexcept
{
if (fake_size)
{
return static_cast<std::size_t>(UINT32_MAX) + 1ULL;
}
return base::size();
}
};
using huge_object_json = nlohmann::json::with_object_t<huge_map>;
TEST_CASE("MessagePack Size above uint32 for object")
{
huge_object_json j = huge_object_json::object();
j["one"] = 1;
j["two"] = 2;
auto& object = j.get_ref<huge_object_json::object_t&>();
object.fake_size = true;
// write into a caller-owned vector: to_msgpack(j) reserves space based on
// the (faked) element count, which fails with bad_alloc on Windows
std::vector<std::uint8_t> result;
CHECK_THROWS_WITH_AS(
huge_object_json::to_msgpack(j, result),
"[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295",
json::out_of_range&);
object.fake_size = false;
}
#ifdef JSON_TEST_BEYOND_UINT32_STRING
struct huge_string : std::string
{
using std::string::string;
std::size_t size() const noexcept // NOLINT(readability-convert-member-functions-to-static)
{
return static_cast<std::size_t>(UINT32_MAX) + 1ULL;
}
};
using huge_string_json = nlohmann::json::with_string_t<huge_string>;
TEST_CASE("MessagePack Size above uint32 for string")
{
const huge_string_json j = "hello";
CHECK_THROWS_WITH_AS(
huge_string_json::to_msgpack(j),
"[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295",
json::out_of_range&);
}
#endif
struct huge_binary : std::vector<std::uint8_t>
{
using std::vector<std::uint8_t>::vector;
std::size_t size() const noexcept // NOLINT(readability-convert-member-functions-to-static)
{
return static_cast<std::size_t>(UINT32_MAX) + 1ULL;
}
};
using huge_binary_json = nlohmann::json::with_binary_t<huge_binary>;
TEST_CASE("MessagePack Size above uint32 for binary")
{
huge_binary_json j = huge_binary_json::binary(huge_binary{});
j.get_binary().push_back(0x01);
j.get_binary().push_back(0x02);
CHECK_THROWS_WITH_AS(
huge_binary_json::to_msgpack(j),
"[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295",
json::out_of_range&);
}
#endif
namespace
{
// types that report a size beyond UINT32_MAX without allocating that much
// memory, so the MessagePack length limit can be tested cheaply; see the
// similar types in unit-bson.cpp
std::size_t beyond_uint32_size()
{
return static_cast<std::size_t>((std::numeric_limits<std::uint32_t>::max)()) + 1;
}
class beyond_uint32_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)
{
return beyond_uint32_size();
}
};
#ifdef JSON_TEST_BEYOND_UINT32_STRING
class beyond_uint32_string_t : public std::string
{
public:
using std::string::string;
size_type size() const noexcept // NOLINT(readability-convert-member-functions-to-static)
{
return beyond_uint32_size();
}
};
using beyond_uint32_string_json = nlohmann::json::with_string_t<beyond_uint32_string_t>;
#endif
using beyond_uint32_binary_json = nlohmann::json::with_binary_t<beyond_uint32_binary_t>;
} // namespace
TEST_CASE("MessagePack lengths beyond UINT32_MAX cannot be serialized")
{
// MessagePack stores the length of a string, binary value, array, or
// object in at most 32 bits; a larger one used to be written without any
// length at all
#if SIZE_MAX > UINT32_MAX
{
const char* const expected = "[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295";
const beyond_uint32_binary_json binary = beyond_uint32_binary_json::binary(beyond_uint32_binary_t{});
CHECK_THROWS_WITH_AS(beyond_uint32_binary_json::to_msgpack(binary), expected, beyond_uint32_binary_json::out_of_range&);
const beyond_uint32_binary_json ext = beyond_uint32_binary_json::binary(beyond_uint32_binary_t{}, 42);
CHECK_THROWS_WITH_AS(beyond_uint32_binary_json::to_msgpack(ext), expected, beyond_uint32_binary_json::out_of_range&);
#ifdef JSON_TEST_BEYOND_UINT32_STRING
// created from its type rather than from a beyond_uint32_string_t:
// that would consider the std::filesystem::path conversion, which
// libstdc++ 10 cannot decide for a class derived from std::string
const beyond_uint32_string_json string(beyond_uint32_string_json::value_t::string);
CHECK_THROWS_WITH_AS(beyond_uint32_string_json::to_msgpack(string), expected, beyond_uint32_string_json::out_of_range&);
#endif
}
#endif
}
TEST_CASE("MessagePack numbers use the active union member (see #5644)")
{
// when number_integer_t is narrower than number_unsigned_t, to_msgpack()
// used to read the union member that was not the active one, writing
// wrong bytes for some values; std::int64_t/std::uint64_t (the default
// types, where both members have the same width) were not affected
using int32_json = nlohmann::json::with_integers_t<std::int32_t, std::uint64_t>;
using int16_json = nlohmann::json::with_integers_t<std::int16_t, std::uint64_t>;
SECTION("number_integer_t = std::int32_t")
{
SECTION("6442450944 (uint 64; the low 32 bits used to be sign-extended)")
{
const int32_json j = 6442450944ULL;
CHECK(j.is_number_unsigned());
std::vector<uint8_t> const expected{0xcf, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00};
const auto result = int32_json::to_msgpack(j);
CHECK(result == expected);
CHECK(int32_json::from_msgpack(result) == j);
}
SECTION("4294967496 (uint 64; the low 32 bits used to be the whole value)")
{
const int32_json j = 4294967496ULL;
CHECK(j.is_number_unsigned());
std::vector<uint8_t> const expected{0xcf, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xc8};
const auto result = int32_json::to_msgpack(j);
CHECK(result == expected);
CHECK(int32_json::from_msgpack(result) == j);
}
}
SECTION("number_integer_t = std::int16_t, 98304 (uint 32)")
{
const int16_json j = 98304ULL;
CHECK(j.is_number_unsigned());
std::vector<uint8_t> const expected{0xce, 0x00, 0x01, 0x80, 0x00};
const auto result = int16_json::to_msgpack(j);
CHECK(result == expected);
CHECK(int16_json::from_msgpack(result) == j);
}
SECTION("default types (std::int64_t/std::uint64_t) are unaffected")
{
const json j = 4294967496ULL;
CHECK(j.is_number_unsigned());
std::vector<uint8_t> const expected{0xcf, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xc8};
const auto result = json::to_msgpack(j);
CHECK(result == expected);
CHECK(json::from_msgpack(result) == j);
}
}
TEST_CASE("MessagePack large strings and binaries (chunked reader)")
{
// get_msgpack_string()/get_msgpack_binary() both read through get_binary(),
@@ -20,6 +20,7 @@
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <algorithm>
#include <cstddef>
#include <sstream>
#include <streambuf>
@@ -72,6 +73,40 @@ class no_putback_streambuf : public std::streambuf
std::size_t m_pos = 0;
};
// A streambuf that serves its input in get areas of a fixed size, so that the
// end of a number falls on every possible offset of a get area. The lexer scans
// the get area directly; the stream position must not depend on its size.
class chunked_streambuf : public std::streambuf
{
public:
chunked_streambuf(std::string s, std::size_t chunk_size)
: m_data(std::move(s)), m_chunk_size(chunk_size)
{}
protected:
int_type underflow() override
{
if (gptr() < egptr())
{
return traits_type::to_int_type(*gptr());
}
m_pos += static_cast<std::size_t>(egptr() - eback());
if (m_pos >= m_data.size())
{
setg(nullptr, nullptr, nullptr);
return traits_type::eof();
}
char* const begin = &m_data[m_pos];
setg(begin, begin, begin + (std::min)(m_chunk_size, m_data.size() - m_pos));
return traits_type::to_int_type(*gptr());
}
private:
std::string m_data;
std::size_t m_pos = 0;
std::size_t m_chunk_size;
};
// read the characters that are left in a stream
std::string remaining(std::istream& is)
{
@@ -263,4 +298,26 @@ TEST_CASE("JSON_PRECISE_STREAM_POSITION")
json _;
CHECK_THROWS_WITH_AS(_ = json::parse(is), "[json.exception.parse_error.101] parse error: attempting to parse an empty input; check that your input string or stream contains the expected JSON", json::parse_error&);
}
SECTION("the position does not depend on the get area of the streambuf")
{
const std::vector<std::size_t> chunk_sizes = {1, 2, 3, 4, 5, 7, 8, 13, 4096};
for (const std::size_t chunk_size : chunk_sizes)
{
CAPTURE(chunk_size)
chunked_streambuf buf("12345678901234567890true\"a long string value\"-1.25e+3,[1,2]42", chunk_size);
std::istream input(&buf);
json j;
input >> j;
CHECK(j == 12345678901234567890u);
input >> j;
CHECK(j == true);
input >> j;
CHECK(j == "a long string value");
input >> j;
CHECK(j == -1250.0);
CHECK(remaining(input) == ",[1,2]42");
}
}
}
+67
View File
@@ -0,0 +1,67 @@
// __ _____ _____ _____
// __| | __| | | | 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"
// Qt defines its keywords as macros (see qtmetamacros.h and qforeach.h) unless
// QT_NO_KEYWORDS is defined, and it is common to include Qt headers before this
// library; it must therefore not use these words as identifiers (see #5865)
#define slots
#define signals public
#define emit
#define Q_FOREACH(variable, container) for (variable : container) // NOLINT(bugprone-macro-parentheses): variable is a declaration, as in Qt
#define foreach Q_FOREACH
#define forever for (;;)
#include <nlohmann/json.hpp>
using nlohmann::json;
TEST_CASE("Qt keyword macros")
{
SECTION("parse and dump")
{
const json j = json::parse(R"({"a": [1, 2.5, "x", null, true]})");
CHECK(j.dump() == R"({"a":[1,2.5,"x",null,true]})");
}
SECTION("flatten and unflatten")
{
const json j = {{"a", {{"b", {1, 2}}, {"c", "d"}}}, {"e", nullptr}};
CHECK(j.flatten().unflatten() == j);
}
SECTION("JSON pointer")
{
json j = {{"a", {{"b", {1, 2}}}}};
CHECK(j.at(json::json_pointer("/a/b/1")) == 2);
j[json::json_pointer("/a/c")] = 3;
CHECK(j.contains(json::json_pointer("/a/c")));
}
SECTION("binary formats")
{
const json j = {{"a", {1, 2, 3}}, {"b", "c"}};
CHECK(json::from_cbor(json::to_cbor(j)) == j);
CHECK(json::from_msgpack(json::to_msgpack(j)) == j);
CHECK(json::from_ubjson(json::to_ubjson(j)) == j);
CHECK(json::from_bjdata(json::to_bjdata(j)) == j);
CHECK(json::from_bson(json::to_bson(j)) == j);
}
SECTION("patch and diff")
{
const json source = {{"a", 1}, {"b", {1, 2}}};
const json target = {{"a", 2}, {"c", "d"}};
CHECK(source.patch(json::diff(source, target)) == target);
}
}
// testing for the macros also keeps -Wunused-macros from rejecting them
#if !defined(slots) || !defined(signals) || !defined(emit) || !defined(Q_FOREACH) || !defined(foreach) || !defined(forever)
#error "the Qt keyword macros must stay defined for the tests above"
#endif
-47
View File
@@ -35,7 +35,6 @@ using ordered_json = nlohmann::ordered_json;
#endif
#include <cstdio>
#include <deque>
#include <list>
#include <type_traits>
#include <utility>
@@ -815,52 +814,6 @@ TEST_CASE("issue #5402 - update(merge_objects=true) overwrites a primitive with
}
TEST_CASE("regression test #5476 - array type without reserve()")
{
// the capacity reserved for definite-length arrays must not require the
// array type to have a reserve() member function
using deque_json = nlohmann::json::with_array_t<std::deque>;
SECTION("std::deque")
{
const auto j = deque_json::parse(R"({"a":[1,[2,3]],"b":[]})");
CHECK(j.dump() == R"({"a":[1,[2,3]],"b":[]})");
// the binary formats pass a definite length to start_array()
CHECK(deque_json::from_cbor(deque_json::to_cbor(j)) == j);
CHECK(deque_json::from_msgpack(deque_json::to_msgpack(j)) == j);
CHECK(deque_json::from_bon8(deque_json::to_bon8(j)) == j);
// parse() instantiates the callback parser as well, which reserves too
const auto with_callback = deque_json::parse(R"([1,2,3])", [](int /*depth*/, deque_json::parse_event_t /*event*/, deque_json& /*parsed*/) noexcept
{
return true;
});
CHECK(with_callback == deque_json({1, 2, 3}));
}
SECTION("std::vector still reserves")
{
json array = json::array();
for (int i = 0; i < 100; ++i)
{
array.push_back(i);
}
const auto j = json::from_cbor(json::to_cbor(array));
CHECK(j == array);
CHECK(j.get_ref<const json::array_t&>().capacity() >= 100);
}
SECTION("the reservation stays capped")
{
// CBOR array announcing 2^32-1 elements, but truncated right after the
// header: the input must be rejected without reserving that capacity
const std::vector<std::uint8_t> truncated = {0x9A, 0xFF, 0xFF, 0xFF, 0xFF};
CHECK(json::from_cbor(truncated, true, false).is_discarded());
}
}
TEST_CASE("issue #5317 - nested indefinite-length CBOR string chunks are rejected")
{
json _;
+423
View File
@@ -0,0 +1,423 @@
// __ _____ _____ _____
// __| | __| | | | 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"
// the library undefines its configuration macros at the end of the header
#if defined(JSON_DISABLE_STREAM_BULK_SCAN) && JSON_DISABLE_STREAM_BULK_SCAN
#define TEST_STREAM_BULK_SCAN 0
#else
#define TEST_STREAM_BULK_SCAN 1
#endif
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <fstream>
#include <istream>
#include <random>
#include <sstream>
#include <streambuf>
#include <string>
#include <utility>
#include <vector>
// The lexer scans the characters in the get area of a std::istream's
// streambuf directly instead of reading them one at a time. These tests pin
// that this is not observable: for any get area size - including a streambuf
// without a get area, which still takes the character-at-a-time path - the
// parsed values, the exception messages (including the "last read" token),
// the stream state, and the characters left in the stream are the same. See
// unit-stream-bulk-scan-disabled.cpp for JSON_DISABLE_STREAM_BULK_SCAN.
namespace
{
// A streambuf that serves its input in get areas of a fixed size, so that
// tokens, numbers, UTF-8 sequences, and indentation cross the end of the get
// area at every possible offset. With a size of 0, it keeps no get area at all
// and serves every character from underflow()/uflow().
class chunked_streambuf : public std::streambuf
{
public:
chunked_streambuf(std::string s, std::size_t chunk_size)
: m_data(std::move(s)), m_chunk_size(chunk_size)
{}
protected:
int_type underflow() override
{
if (m_chunk_size == 0)
{
return m_pos < m_data.size() ? traits_type::to_int_type(m_data[m_pos]) : traits_type::eof();
}
if (gptr() < egptr())
{
return traits_type::to_int_type(*gptr());
}
// the previous get area has been consumed
m_pos += static_cast<std::size_t>(egptr() - eback());
if (m_pos >= m_data.size())
{
setg(nullptr, nullptr, nullptr);
return traits_type::eof();
}
char* const begin = &m_data[m_pos];
setg(begin, begin, begin + (std::min)(m_chunk_size, m_data.size() - m_pos));
return traits_type::to_int_type(*gptr());
}
int_type uflow() override
{
if (m_chunk_size == 0)
{
return m_pos < m_data.size() ? traits_type::to_int_type(m_data[m_pos++]) : traits_type::eof();
}
return std::streambuf::uflow();
}
private:
std::string m_data;
std::size_t m_pos = 0;
std::size_t m_chunk_size;
};
// how a test reads from the stream
enum class read_mode
{
extract, // up to four times operator>>, as for concatenated values
parse, // json::parse(stream)
parse_lenient, // json::parse(stream) with comments
};
// Read @a input from a chunked_streambuf and describe everything observable:
// the values or the exception, the stream state after each value, and the
// characters left in the stream.
std::string observe(const std::string& input, std::size_t chunk_size, read_mode mode)
{
chunked_streambuf buf(input, chunk_size);
std::istream is(&buf);
std::string result;
for (int i = 0; i < 4; ++i)
{
json j;
try
{
if (mode == read_mode::extract)
{
is >> j;
}
else
{
j = json::parse(is, nullptr, true, mode == read_mode::parse_lenient);
}
result += "value " + j.dump() + "; state " + std::to_string(static_cast<unsigned int>(is.rdstate())) + "\n";
}
catch (const json::exception& e)
{
result += std::string("exception ") + e.what() + "\n";
break;
}
if (mode != read_mode::extract)
{
break;
}
is.clear();
}
is.clear();
result += "remaining ";
char c = 0;
while (is.get(c))
{
result += c;
}
return result;
}
// get area sizes that put every boundary everywhere in short inputs, plus one
// that holds most inputs completely
std::vector<std::size_t> chunk_sizes()
{
return {1, 2, 3, 4, 5, 7, 8, 9, 13, 16, 64, 4096};
}
// valid and invalid inputs covering every token type and every bulk path:
// strings (plain runs, escapes, UTF-8), numbers (all grammar parts, too large
// for an integer), and indentation
std::vector<std::string> corpus()
{
return
{
// numbers, terminated in every way
"0", "-0", "123", "-123", "1.5", "-1.5e+10", "2E-3", "123456789012345678901234567890",
"18446744073709551615", "18446744073709551616", "-9223372036854775809",
"123 456", "123,456", "123]", "1true", "42\nfoo", "1.25e3}", "7\t8\r9",
// malformed numbers
"1.", "-", "1e", "1e+", "01", "-a", "1.x", "1ee5",
// strings
R"("")", R"("abc")", R"("a long string with a run of ordinary characters to scan in bulk")",
R"("escapes \" \\ \/ \b \f \n \r \t é 😀")",
"\"UTF-8: \xc3\xa9 \xe2\x82\xac \xf0\x9f\x98\x80 done\"",
// malformed strings
R"("unterminated)", "\"control\x01character\"", "\"ill-formed \xc3\x28 UTF-8\"", R"("bad \x escape")",
R"("\ud800 lone surrogate")",
// literals
"true", "false", "null", "tru", "nul", "falsey",
// structures
"[]", "{}", "[1,2,3]", R"({"a":1,"b":[true,false,null],"c":{"d":"e"}})", "[1,]", R"({"a" 1})", "[[[[[]]]]]",
// pretty-printed
"{\n \"key\": [\n 1,\n -2.5,\n \"three\"\n ]\n}",
"[\n \"deep\"\n ]",
"{\n \"a\": 1,\n \"b\": 2x\n}",
// concatenated values and whitespace
"1 2 3", R"("a""b")", "[1][2]", "{}{}", " \n\t 42 \n\t ", "",
// comments (only accepted by parse_lenient)
"/* comment */ 1", "// comment\n[1, /* c */ 2]",
// byte order mark
"\xef\xbb\xbf[1]",
};
}
} // namespace
TEST_CASE("parsing from a std::istream scans the get area in bulk")
{
#if !defined(JSON_NOEXCEPTION)
SECTION("results do not depend on the get area")
{
const std::vector<read_mode> read_modes = {read_mode::extract, read_mode::parse, read_mode::parse_lenient};
for (const auto& input : corpus())
{
for (const read_mode mode : read_modes)
{
// no get area: the character-at-a-time path
const std::string expected = observe(input, 0, mode);
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(input)
CAPTURE(chunk_size)
CAPTURE(static_cast<int>(mode))
CHECK(observe(input, chunk_size, mode) == expected);
}
}
}
}
#endif // !defined(JSON_NOEXCEPTION)
SECTION("values are the same as when parsing from a string")
{
const std::string input = R"({"name": "nlohmann::json", "numbers": [1, -2, 3.5, 4e10, 18446744073709551615],)"
"\n \"text\": \"caf\xc3\xa9 \\u00e9 \\\"quoted\\\"\", \"nested\": [[{}], [[]]], \"flags\": [true, false, null]}";
const json expected = json::parse(input);
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(input, chunk_size);
std::istream is(&buf);
CHECK(json::parse(is) == expected);
}
std::istringstream ss(input);
CHECK(json::parse(ss) == expected);
}
#if !defined(JSON_NOEXCEPTION)
SECTION("a token that spans several get areas is reported completely")
{
// the error is found after the string has crossed many get areas; the
// token string holds everything since the start of the last token
const std::string input = R"(["this string is longer than every get area", tru])";
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(input, chunk_size);
std::istream is(&buf);
json j;
CHECK_THROWS_WITH_AS(is >> j,
"[json.exception.parse_error.101] parse error at line 1, column 50: syntax error while parsing value - invalid literal; last read: '\"this string is longer than every get area\", tru]'",
json::parse_error&);
}
const std::string unterminated = R"("this string is longer than every get area)";
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(unterminated, chunk_size);
std::istream is(&buf);
json j;
CHECK_THROWS_WITH_AS(is >> j,
"[json.exception.parse_error.101] parse error at line 1, column 43: syntax error while parsing value - invalid string: missing closing quote; last read: '\"this string is longer than every get area'",
json::parse_error&);
}
}
SECTION("an error before the first token reports everything read")
{
chunked_streambuf buf(" \n x", 2);
std::istream is(&buf);
json j;
CHECK_THROWS_WITH_AS(is >> j,
"[json.exception.parse_error.101] parse error at line 2, column 3: syntax error while parsing value - invalid literal; last read: ' <U+000A> x'",
json::parse_error&);
}
SECTION("lines and columns are counted across get areas")
{
const std::string input = "[\n 1,\n \"two\",\n 3.0,\n x\n]";
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(input, chunk_size);
std::istream is(&buf);
json j;
CHECK_THROWS_WITH_AS(is >> j,
"[json.exception.parse_error.101] parse error at line 5, column 5: syntax error while parsing value - invalid literal; last read: '3.0,<U+000A> x'",
json::parse_error&);
}
}
#endif // !defined(JSON_NOEXCEPTION)
SECTION("many values from one stream")
{
std::string input;
for (int i = 0; i < 200; ++i)
{
input += R"({"id":)" + std::to_string(i) + R"(,"name":"user)" + std::to_string(i) + R"(","score":)" + std::to_string(i) + ".5}\n";
input += std::to_string(i * 7919) + "\n";
}
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(input, chunk_size);
std::istream is(&buf);
json j;
int count = 0;
while (!(is >> std::ws).eof())
{
is >> j;
if (count % 2 == 0)
{
CHECK(j["id"] == count / 2);
}
else
{
CHECK(j == (count / 2) * 7919);
}
++count;
}
CHECK(count == 400);
}
}
SECTION("the stream can be used after parsing")
{
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(R"({"a": "some text"} trailing words 42)", chunk_size);
std::istream is(&buf);
json j;
is >> j;
CHECK(j == json({{"a", "some text"}}));
std::string word;
int number = 0;
is >> word;
CHECK(word == "trailing");
is >> word >> number;
CHECK(word == "words");
CHECK(number == 42);
}
}
SECTION("std::stringstream and std::ifstream")
{
const std::string input = R"([1, 2.5, "three", {"four": [true, null]}] 5)";
std::stringstream ss(input);
json j1;
json j2;
ss >> j1 >> j2;
CHECK(j1 == json::parse(R"([1, 2.5, "three", {"four": [true, null]}])"));
CHECK(j2 == 5);
CHECK(ss.eof());
// a file name of its own for every run: the test runs for several C++
// standards and configurations at the same time, in the same directory
std::random_device rd;
const std::string temp_name = "unit-stream-bulk-scan-" + std::to_string(rd()) + ".tmp";
const char* const filename = temp_name.c_str();
{
std::ofstream file(filename);
file << input;
}
{
std::ifstream file(filename);
json k1;
json k2;
file >> k1 >> k2;
CHECK(k1 == j1);
CHECK(k2 == j2);
}
static_cast<void>(std::remove(filename));
}
SECTION("binary formats read from a stream")
{
const json j = {{"string", std::string(300, 'x')}, {"array", {1, 2, 3}}, {"number", 1.5}};
const std::vector<std::uint8_t> cbor = json::to_cbor(j);
const std::string bytes(reinterpret_cast<const char*>(cbor.data()), cbor.size());
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(bytes, chunk_size);
std::istream is(&buf);
CHECK(json::from_cbor(is) == j);
}
}
#if TEST_STREAM_BULK_SCAN
SECTION("the adapter keeps no token unless the lexer starts one")
{
// the binary readers read through the same adapter, but never start a
// token; refilling the get area must then not collect the input
chunked_streambuf buf(std::string(1000, 'x'), 7);
std::istream is(&buf);
nlohmann::detail::input_stream_adapter ia(is);
for (int i = 0; i < 1000; ++i)
{
CHECK(ia.get_character() == 'x');
}
std::vector<char> token;
ia.copy_token(token, false);
CHECK(token.empty());
}
#endif
SECTION("SAX parsing from a stream")
{
const std::string input = R"({"a": [1, "two", 3.5]} rest)";
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(input, chunk_size);
std::istream is(&buf);
json j;
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> sax(j);
CHECK(json::sax_parse(is, &sax, json::input_format_t::json, false));
CHECK(j == json::parse(R"({"a": [1, "two", 3.5]})"));
}
}
}
+82
View File
@@ -846,3 +846,85 @@ TEST_CASE("choice of the conversion")
DOCTEST_MSVC_SUPPRESS_WARNING_POP
}
}
TEST_CASE("short decimals")
{
// write_short_decimal() writes digits * 10^exp for the digits of a double
// that need no conversion (at most 15, the first not 0): as to_chars()
// writes the (positive) double that has these digits
const auto written = [](std::uint64_t digits, int exp) -> std::string
{
std::array<char, 64> buf{}; // (up to 41 bytes are written)
char* const end = nlohmann::detail::dtoa_impl::write_short_decimal(buf.data(), digits, exp);
return {buf.data(), end};
};
const auto written_counted = [](std::uint64_t digits, int count, int exp) -> std::string
{
std::array<char, 64> buf{};
char* const end = nlohmann::detail::dtoa_impl::write_short_decimal(buf.data(), digits, count, exp);
return {buf.data(), end};
};
const auto expected = [](std::uint64_t digits, int exp) -> std::string
{
const double value = std::strtod((std::to_string(digits) + "e" + std::to_string(exp)).c_str(), nullptr);
std::array<char, 64> buf{};
char* const end = nlohmann::detail::to_chars(buf.data(), buf.data() + 32, value);
return {buf.data(), end};
};
SECTION("powers of ten")
{
const auto& powers = nlohmann::detail::dtoa_impl::powers_of_ten();
CHECK(powers.front() == 1u);
for (std::size_t i = 1; i < powers.size(); ++i)
{
CHECK(powers[i] == powers[i - 1] * 10u);
}
CHECK(powers.back() == 10000000000000000000u);
}
SECTION("examples")
{
CHECK(written(1, 0) == "1.0");
CHECK(written(15, -1) == "1.5");
CHECK(written(125, -2) == "1.25");
CHECK(written(1, 22) == "1e+22");
CHECK(written(123456789012345, -2) == "1234567890123.45");
CHECK(written(999999999999999, -15) == "0.999999999999999");
CHECK(written(5, -324) == "5e-324");
CHECK(written_counted(1, 1, 0) == "1.0");
CHECK(written_counted(125, 3, -2) == "1.25");
CHECK(written_counted(100, 3, -2) == "1.0");
CHECK(written_counted(999999999999999, 15, -15) == "0.999999999999999");
}
SECTION("random digits, exponents and trailing zeros")
{
std::mt19937_64 rng(1170); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
for (int i = 0; i < 100000; ++i)
{
// 1 to 15 digits, the first not 0, and up to 14 of them trailing zeros
std::uint64_t count = 1 + (rng() % 15u);
std::uint64_t power = 1;
for (std::uint64_t k = 1; k < count; ++k)
{
power *= 10;
}
std::uint64_t digits = power + (rng() % (9 * power));
const std::uint64_t zeros = (rng() % 3u == 0) ? (rng() % count) : 0;
for (std::uint64_t k = 0; k < zeros; ++k)
{
digits = (digits / 10) * 10;
}
// (a value between 1e-300 and 1e300)
const int exp = static_cast<int>(rng() % 560u) - 300 - static_cast<int>(count);
CAPTURE(digits)
CAPTURE(count)
CAPTURE(exp)
const std::string want = expected(digits, exp);
CHECK(written(digits, exp) == want);
CHECK(written_counted(digits, static_cast<int>(count), exp) == want);
}
}
}