Merge develop into fix/ubjson-valueless-budget and avoid MSVC C4127

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann committed 2026-11-13 07:33:26 +01:00
commit a2cc727dd5
94 files changed
+2871 -1461

No files matched your search

+24
View File
@@ -355,6 +355,20 @@ json_test_add_test_for(src/unit-comparison.cpp
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
)
# test the walks over deeply nested values with the extreme settings of
# JSON_RECURSION_DEPTH_LIMIT: 0 (every walk uses its explicit stack from the
# first level on), 1, and 254 (the largest limit that fits the byte that counts
# the nesting of a copy)
foreach(recursion_depth_limit 0 1 254)
json_test_set_test_options(test-recursion_depth_limit_${recursion_depth_limit}
COMPILE_DEFINITIONS JSON_RECURSION_DEPTH_LIMIT=${recursion_depth_limit}
)
json_test_add_test_for(src/unit-recursion_depth_limit.cpp
NAME test-recursion_depth_limit_${recursion_depth_limit}
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
)
endforeach()
# test the parser again with JSON_DIAGNOSTIC_POSITIONS enabled
json_test_set_test_options(test-class_parser_diagnostic_positions
COMPILE_DEFINITIONS JSON_DIAGNOSTIC_POSITIONS=1
@@ -364,6 +378,16 @@ json_test_add_test_for(src/unit-class_parser.cpp
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
)
# test the conversions again without std::filesystem::path support: a
# std::u8string must be a JSON string in this configuration, too
json_test_set_test_options(test-conversions2_no_filesystem
COMPILE_DEFINITIONS JSON_HAS_FILESYSTEM=0 JSON_HAS_EXPERIMENTAL_FILESYSTEM=0
)
json_test_add_test_for(src/unit-conversions2.cpp
NAME test-conversions2_no_filesystem
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
)
# test diagnostic positions again without regular diagnostics (JSON pointer paths)
json_test_set_test_options(test-diagnostic-positions_only
COMPILE_DEFINITIONS JSON_DIAGNOSTICS=0
+18 -1
View File
@@ -56,9 +56,26 @@ int main()
const nlohmann::json_document doc = nlohmann::json_document::parse(R"({"a": 1, "list": [1, 2, 3]})");
const std::size_t doc_size = doc.root().size();
// editable documents and views, view_mode, is_json_view and the ordered variants
nlohmann::json_editable_document edoc = nlohmann::json_editable_document::parse(R"({"a": 1, "list": [1, 2, 3]})");
edoc.set(edoc.root(), "b", 2);
edoc.push_back(edoc.root()["list"], 4);
const nlohmann::json_editable_view eroot = edoc.root();
const std::size_t edit_size = eroot.size();
const std::size_t list_size = eroot["list"].size();
nlohmann::ordered_json_editable_document oedoc = nlohmann::ordered_json_editable_document::parse(R"({"z": 1, "y": 2})");
oedoc.erase(oedoc.root(), "z");
const nlohmann::ordered_json_editable_view oeroot = oedoc.root();
nlohmann::basic_json_document<nlohmann::json, nlohmann::view_mode::editable> mdoc = nlohmann::basic_json_document<nlohmann::json, nlohmann::view_mode::editable>::parse("[]");
mdoc.push_back(mdoc.root(), 1);
static_assert(nlohmann::is_json_view<nlohmann::json_view>::value, "json_view is a view");
static_assert(nlohmann::is_json_view<nlohmann::json_editable_view>::value, "json_editable_view is a view");
static_assert(!nlohmann::is_json_view<nlohmann::json>::value, "json is not a view");
// use every result so the references cannot be optimized away
return (a == 1 && last == 3 && b == 2 && lit.size() == 3
&& m.size() == 1 && !dumped.empty() && !os.str().empty()
&& doc_size == 2)
&& doc_size == 2
&& edit_size == 3 && list_size == 4 && oeroot.size() == 1 && mdoc.root().size() == 1)
? 0 : 1;
}
+15 -11
View File
@@ -236,7 +236,8 @@ TEST_CASE("controlled bad_alloc")
next_construct_fails = false;
// deeper than the 128 levels the copy constructor descends into
// deeper than the levels the copy constructor descends into
// (recursion_depth_limit(): 128 by default, 254 at most)
const std::size_t depth = 300;
my_json j = 1;
@@ -300,8 +301,9 @@ struct nth_alloc_fails_allocator : std::allocator<T>
};
};
// builds a value nested more than 128 levels deep - the bound the copy
// constructor descends into before it continues without the call stack - and
// builds a value nested more than recursion_depth_limit() (128 by default)
// levels deep - the bound the copy constructor descends into before it
// continues without the call stack - and
// checks that a copy survives any single allocation of it failing: every
// attempt either throws std::bad_alloc, without crashing or leaving the
// source altered, or completes the copy
@@ -312,9 +314,9 @@ void check_deep_copy_survives_failing_allocation(bool nest_objects)
fail_at_alloc_call = -1;
// [[[ ... [1] ... ]]], or the same nesting with objects, 130 levels deep
// [[[ ... [1] ... ]]], or the same nesting with objects, two levels deeper than the limit
BasicJsonType src = 1;
for (std::size_t i = 0; i < 130; ++i)
for (std::size_t i = 0; i < nlohmann::detail::recursion_depth_limit() + 2; ++i)
{
if (nest_objects)
{
@@ -440,8 +442,9 @@ TEST_CASE("deep copy uses the provided allocator")
{
using counting_json = nlohmann::json::with_allocator_t<scratch_counting_allocator>;
// deeper than the 128 levels the copy constructor descends into, so the
// innermost objects are copied by the iterative deep copy
// deeper than the levels the copy constructor descends into (128 by
// default, 254 at most), so the innermost objects are copied by the
// iterative deep copy
counting_json j = 1;
for (std::size_t i = 0; i < 300; ++i)
{
@@ -499,11 +502,12 @@ TEST_CASE("converting a deeply nested value from another specialization fails cl
#if !(defined(_MSC_VER) && _MSC_VER < 1910 && defined(_ITERATOR_DEBUG_LEVEL) && _ITERATOR_DEBUG_LEVEL > 0)
using countdown_json = nlohmann::json::with_allocator_t<countdown_allocator>;
// deeper than the 128 levels the converting constructor descends into, so
// that failures land on both sides of the bound - or, built with
// JSON_NO_THREAD_LOCAL, all in the iterative conversion
// deeper than the levels the converting constructor descends into
// (recursion_depth_limit(), 128 by default), so that failures land on both
// sides of the bound - or, built with JSON_NO_THREAD_LOCAL, all in the
// iterative conversion
json j = {1, "two", {{"three", 3}}};
for (std::size_t i = 0; i < 150; ++i)
for (std::size_t i = 0; i < nlohmann::detail::recursion_depth_limit() + 22; ++i)
{
j = json{{"a", json::array({j, "sibling"})}};
}
+19
View File
@@ -531,9 +531,28 @@ TEST_CASE("alternative string type with json_view")
CHECK(d.root().materialize() == expected);
CHECK(alt_json::parse(d.root().dump()) == expected);
// save() and load() do not exist on big-endian targets
#if !(defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
const alt_editable_document loaded = alt_editable_document::load(d.save());
CHECK(loaded.root().materialize() == expected);
#endif
}
}
// a std::u8string is a JSON string, whatever the string type of the JSON value
// JSON_HAS_CPP_20 JSON_HAS_CPP_23 (do not remove; see note at top of file)
#if defined(__cpp_lib_char8_t)
TEST_CASE("alternative string type and std::u8string")
{
const std::u8string s = u8"abc";
const alt_json j = s;
CHECK(j.is_string());
CHECK(j.get<alt_string>() == alt_string("abc"));
CHECK(j.get<std::u8string>() == s);
// the array of code units written by versions before 3.13.0
CHECK(alt_json::array({0x61, 0x62, 0x63}).get<std::u8string>() == s);
}
#endif
DOCTEST_CLANG_SUPPRESS_WARNING_POP
@@ -94,6 +94,37 @@ TEST_CASE("JSON_BRACE_INIT_COPY_SEMANTICS")
CHECK(j3 == pair_shaped);
}
SECTION("push_back and operator+= with a single braced JSON value")
{
// with the macro, the result does not depend on the compiler;
// without it, GCC wraps a JSON value and Clang does not (a literal
// such as {1} is wrapped by both)
json const obj = {{"key", "value"}};
json const arr = {1, 2};
json a1 = json::array();
a1.push_back({obj});
CHECK(a1 == json::array({obj}));
json a2 = json::array();
a2 += {obj};
CHECK(a2 == json::array({obj}));
json b1 = json::array();
b1.push_back({arr});
CHECK(b1 == json::array({arr}));
json b2 = json::array();
b2 += {arr};
CHECK(b2 == json::array({arr}));
// a literal is copied as well
json c = json::array();
c.push_back({1});
c += {2};
CHECK(c == json::array({1, 2}));
}
SECTION("what the macro does not change")
{
// lists with more than one element are unaffected
+37
View File
@@ -3232,3 +3232,40 @@ TEST_CASE("object keys are delivered decoded and validated")
CHECK(keys_of_format(std::string("\x87\xC3\x28\x91"), json::input_format_t::bon8).empty());
}
}
namespace
{
// a plain function (not a lambda or std::function) used as parser callback
bool keep_everything_callback(int /*depth*/, json::parse_event_t /*event*/, json& /*parsed*/)
{
return true;
}
} // namespace
TEST_CASE("a plain function is accepted as parser callback for containers")
{
// ADL finds basic_json's friend operator!= for "container != function
// pointer", which must not make the iterator/sentinel overloads viable
const json expected = {1, 2};
const std::string text = "[1,2]";
const std::vector<std::uint8_t> bytes(text.begin(), text.end());
const std::vector<char> chars(text.begin(), text.end());
CHECK(json::parse(text, keep_everything_callback) == expected);
CHECK(json::parse(text, &keep_everything_callback) == expected);
CHECK(json::parse(std::string("[1,2]"), keep_everything_callback) == expected);
CHECK(json::parse(bytes, keep_everything_callback) == expected);
CHECK(json::parse(bytes, &keep_everything_callback) == expected);
CHECK(json::parse(chars, keep_everything_callback) == expected);
CHECK(json::parse(text.begin(), text.end(), keep_everything_callback) == expected);
// the sentinel overloads (shared constraint of parse, accept, sax_parse and
// the from_* functions) are not viable for a container and a function
// pointer, but still are for iterator pairs
using callback_pointer = decltype(&keep_everything_callback);
CHECK_FALSE((nlohmann::detail::can_compare_ne<std::string, callback_pointer>::value));
CHECK_FALSE((nlohmann::detail::can_compare_ne<std::vector<std::uint8_t>, callback_pointer>::value));
CHECK((nlohmann::detail::can_compare_ne<std::string::const_iterator, std::string::const_iterator>::value));
CHECK((nlohmann::detail::can_compare_ne<const char*, const char*>::value));
}
+15 -5
View File
@@ -62,6 +62,15 @@ template<> struct StringMaker<std::partial_ordering>
namespace
{
// nesting depths on either side of detail::recursion_depth_limit(): none, the
// last one within the limit, the first one past it, and well past it (0, 127,
// 128, and 200 by default)
std::vector<std::size_t> depths_around_recursion_limit()
{
const std::size_t limit = nlohmann::detail::recursion_depth_limit();
return {0, limit > 0 ? limit - 1 : 0, limit, limit + 72};
}
// helper function to check std::less<json::value_t>
// see https://en.cppreference.com/w/cpp/utility/functional/less
template <typename A, typename B, typename U = std::less<json::value_t>>
@@ -968,7 +977,7 @@ TEST_CASE("copying an object preserves its comparator's state")
object["a"] = 3;
REQUIRE(object.dump() == R"({"B":2,"a":3,"b":1})");
for (const std::size_t depth : std::vector<std::size_t> {0, 127, 128, 200})
for (const std::size_t depth : depths_around_recursion_limit())
{
CAPTURE(depth)
@@ -1011,7 +1020,7 @@ TEST_CASE("equality of an object whose comparator treats different keys as equiv
// sanity check: the object type's own comparison already disagrees
CHECK_FALSE(a.get_ref<const ci_json::object_t&>() == b.get_ref<const ci_json::object_t&>());
for (const std::size_t depth : std::vector<std::size_t> {0, 127, 128, 200})
for (const std::size_t depth : depths_around_recursion_limit())
{
CAPTURE(depth)
@@ -1135,9 +1144,10 @@ TEST_CASE("operator<=> of binary values with a different subtype does not depend
return result;
};
// 127 levels stay within nesting_depth_limit() (128); 128 and 200 do not,
// and must still agree with the levels that do
for (const std::size_t depth : std::vector<std::size_t> {0, 127, 128, 200})
// one level less than the recursion depth limit (127 by default) stays within it;
// the limit itself and more (128 and 200) do not, and must still agree with the
// levels that do
for (const std::size_t depth : depths_around_recursion_limit())
{
CAPTURE(depth)
const json x = deep(a, depth);
+92 -15
View File
@@ -27,10 +27,12 @@ using nlohmann::json;
#include <deque>
#include <forward_list>
#include <list>
#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>
#include <valarray>
#include <vector>
// NLOHMANN_JSON_SERIALIZE_ENUM uses a static std::pair
DOCTEST_CLANG_SUPPRESS_WARNING_PUSH
@@ -707,38 +709,113 @@ TEST_CASE("std::filesystem::path")
}
#endif
// the ADL to_json overload for std::u8string only exists under the same guard
// as std::filesystem::path support (it is otherwise only reached indirectly,
// via std::filesystem::path::u8string()) -- mirror both #if conditions from
// include/nlohmann/detail/conversions/to_json.hpp exactly
#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM
// a std::u8string is a JSON string since 3.13.0 (it was an array of code units
// before); independent of whether std::filesystem::path is supported, see the
// test-conversions2_no_filesystem target
// JSON_HAS_CPP_20 JSON_HAS_CPP_23 (do not remove; see note at top of file)
#if defined(__cpp_lib_char8_t)
TEST_CASE("std::u8string")
{
// use \u universal-character-names (rather than raw \x byte escapes
// or literal non-ASCII source bytes) to compose the multi-byte UTF-8
// encoding -- MSVC treats \x escapes used that way inside a u8
// literal as a nonstandard extension (warning C5321), which some of
// our CI configs promote to an error; \u is portable and produces
// the exact same encoded bytes without depending on the source
// file's encoding
const std::u8string utf8 = u8"P\u011B\u0161ina";
SECTION("ascii")
{
const std::u8string s = u8"Path";
json const j = s;
CHECK(j.is_string());
CHECK(j.template get<std::string>() == "Path");
}
SECTION("utf-8")
{
// use \u universal-character-names (rather than raw \x byte escapes
// or literal non-ASCII source bytes) to compose the multi-byte UTF-8
// encoding -- MSVC treats \x escapes used that way inside a u8
// literal as a nonstandard extension (warning C5321), which some of
// our CI configs promote to an error; \u is portable and produces
// the exact same encoded bytes without depending on the source
// file's encoding
const std::u8string s = u8"P\u011B\u0161ina";
json const j = s;
json const j = utf8;
CHECK(j.is_string());
CHECK(j.template get<std::string>() == "P\xc4\x9b\xc5\xa1ina");
}
}
SECTION("round trip")
{
json const j = utf8;
CHECK(j.template get<std::u8string>() == utf8);
std::u8string back;
j.get_to(back);
CHECK(back == utf8);
// from a string
json const j_string = "P\xc4\x9b\xc5\xa1ina";
CHECK(j_string.template get<std::u8string>() == utf8);
// empty, and with an embedded NUL
CHECK(json(std::u8string()).template get<std::u8string>().empty());
const std::u8string with_nul(u8"a\0b", 3);
CHECK(json(with_nul).template get<std::u8string>() == with_nul);
// the lvalue and rvalue paths, and in containers
CHECK(json(utf8).template get<std::u8string>() == utf8);
CHECK(json(std::u8string(utf8)).template get<std::u8string>() == utf8);
const std::vector<std::u8string> vec = {utf8, u8"x"};
json const j_vec = vec;
CHECK(j_vec.is_array());
CHECK(j_vec[0].is_string());
CHECK(j_vec.template get<std::vector<std::u8string>>() == vec);
const std::map<std::string, std::u8string> map = {{"k", utf8}};
CHECK(json(map).template get<std::map<std::string, std::u8string>>() == map);
}
SECTION("array of code units (written by versions before 3.13.0)")
{
json const j = json::array({0x50, 0xC4, 0x9B, 0xC5, 0xA1, 0x69, 0x6E, 0x61});
CHECK(j.template get<std::u8string>() == utf8);
CHECK(json::array().template get<std::u8string>().empty());
CHECK(json::parse("[97, 98]").template get<std::u8string>() == u8"ab");
std::u8string s = u8"overwritten";
j.get_to(s);
CHECK(s == utf8);
}
SECTION("other types")
{
#if !defined(JSON_NOEXCEPTION)
CHECK_THROWS_WITH_AS(json(1).template get<std::u8string>(), "[json.exception.type_error.302] type must be string or array, but is number", json::type_error);
CHECK_THROWS_WITH_AS(json(nullptr).template get<std::u8string>(), "[json.exception.type_error.302] type must be string or array, but is null", json::type_error);
CHECK_THROWS_AS(json::object().template get<std::u8string>(), json::type_error);
// an element that is not a number
CHECK_THROWS_AS(json::array({"a"}).template get<std::u8string>(), json::type_error);
#endif
}
SECTION("ordered_json")
{
nlohmann::ordered_json const j = utf8;
CHECK(j.is_string());
CHECK(j.template get<std::string>() == "P\xc4\x9b\xc5\xa1ina");
CHECK(j.template get<std::u8string>() == utf8);
CHECK(nlohmann::ordered_json::array({0x61, 0x62}).template get<std::u8string>() == u8"ab");
}
SECTION("std::u8string_view and char8_t arrays are unchanged (arrays of code units)")
{
const std::u8string_view view = utf8;
json const j_view = view;
CHECK(j_view.is_array());
CHECK(j_view.size() == 8);
const char8_t arr[] = u8"ab"; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
json const j_arr = arr;
CHECK(j_arr == json::array({0x61, 0x62, 0}));
}
}
#endif
#if !defined(JSON_NOEXCEPTION)
+5 -3
View File
@@ -11,6 +11,7 @@
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <cstddef>
#include <cstdint>
#include <deque>
#include <map>
@@ -217,16 +218,17 @@ TEST_CASE("array type with std::deque nested beyond the recursion limit")
// recursion_depth_limit() with an explicit stack of frames; with the
// iterators of std::deque, these frames are copied and moved through
// their own special members
const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 72; // 200 by default
deque_json j = 1;
for (int i = 0; i < 200; ++i)
for (std::size_t i = 0; i < depth; ++i)
{
deque_json outer = deque_json::array();
outer.push_back(std::move(j));
j = std::move(outer);
}
// 200 arrays of one element each (0x81) around the integer 1
std::vector<std::uint8_t> expected(200, 0x81);
// `depth` arrays of one element each (0x81) around the integer 1
std::vector<std::uint8_t> expected(depth, 0x81);
expected.push_back(0x01);
CHECK(deque_json::to_cbor(j) == expected);
}
+2 -2
View File
@@ -475,10 +475,10 @@ TEST_CASE("Regression test for issue #5674 - copy construction must not require
SECTION("nested deeper than the copy constructor's descent bound")
{
// beyond nesting_depth_limit() (128) levels, the copy constructor
// beyond recursion_depth_limit() (128 by default) levels, the copy constructor
// copies without the call stack (copy_iteratively / copy_array_level),
// which used to assign the base class of every element it created
const std::size_t depth = 300;
const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 172;
const json_with_const_base j = make_nested_array(depth);
const json_with_const_base copy = j; // NOLINT(performance-unnecessary-copy-initialization)
+15 -9
View File
@@ -128,16 +128,19 @@ TEST_CASE("Better diagnostics with positions")
check_copy(depth, true);
};
// around detail::recursion_depth_limit() (127, 128, and 129 by default)
const std::size_t limit = nlohmann::detail::recursion_depth_limit();
check_arrays(1);
check_arrays(127);
check_arrays(128);
check_arrays(129);
check_arrays(limit > 0 ? limit - 1 : 0);
check_arrays(limit);
check_arrays(limit + 1);
check_arrays(300);
check_objects(1);
check_objects(127);
check_objects(128);
check_objects(129);
check_objects(limit > 0 ? limit - 1 : 0);
check_objects(limit);
check_objects(limit + 1);
check_objects(300);
}
@@ -186,10 +189,13 @@ TEST_CASE("Better diagnostics with positions")
}
};
// around detail::recursion_depth_limit() (127, 128, and 129 by default)
const std::size_t limit = nlohmann::detail::recursion_depth_limit();
check_conversion(1);
check_conversion(127);
check_conversion(128);
check_conversion(129);
check_conversion(limit > 0 ? limit - 1 : 0);
check_conversion(limit);
check_conversion(limit + 1);
check_conversion(300);
}
+3 -1
View File
@@ -515,9 +515,11 @@ TEST_CASE("Regression tests for extended diagnostics")
// update(j, true) around its descent bound, where the nested vectors
// grow while the objects are merged without recursing
const std::size_t limit = nlohmann::detail::recursion_depth_limit();
// (the limit can be 0, where "one level less" is no level)
for (const std::size_t depth :
{
nlohmann::detail::recursion_depth_limit() - 1, nlohmann::detail::recursion_depth_limit(), nlohmann::detail::recursion_depth_limit() + 2
limit > 0 ? limit - 1 : 0, limit, limit + 2
})
{
ordered_json j = {{"z", {{"x", 1}}}};
+426 -3
View File
@@ -14,9 +14,12 @@ using nlohmann::json;
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
#endif
#include <algorithm>
#include <cstddef>
#include <ctime>
#include <fstream>
#include <limits>
#include <random>
#include <string>
#include <utility>
#include <vector>
@@ -1975,14 +1978,15 @@ TEST_CASE("JSON patch: diff of deeply nested values")
SECTION("the diff reproduces the target at every depth")
{
// depths on either side of the nesting depth up to which diff()
// recurses (detail::recursion_depth_limit(), 128); not every depth up
// recurses (detail::recursion_depth_limit(), 128 by default); not every depth up
// to 300, as the test would then time out under Valgrind
std::vector<std::size_t> depths;
for (std::size_t depth = 0; depth <= 16; ++depth)
{
depths.push_back(depth);
}
for (std::size_t depth = 120; depth <= 136; ++depth)
const std::size_t limit = nlohmann::detail::recursion_depth_limit();
for (std::size_t depth = limit > 8 ? limit - 8 : 0; depth <= limit + 8; ++depth)
{
depths.push_back(depth);
}
@@ -2524,7 +2528,7 @@ TEST_CASE("JSON patch - diff of leaf values below the bound of the recursive des
// the values at the innermost level are diffed without recursion: equal
// leaves yield nothing, leaves of different types and different leaves
// are replaced
const std::size_t depth = 200;
const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 72; // 200 by default
json source = {1, "text", 3, {{"x", 1}, {"y", 2}}, true, nullptr};
json target = {1, 2, 4, {{"x", 1}, {"y", 3}}, true, nullptr};
@@ -2551,3 +2555,422 @@ TEST_CASE("JSON patch - diff of leaf values below the bound of the recursive des
CHECK(object_patch.size() == 2);
CHECK(object_source.patch(object_patch) == object_target);
}
namespace
{
// all pointers to values below `value` (not to `value` itself)
void collect_pointers(const json& value, const std::string& path, std::vector<std::string>& pointers)
{
if (value.is_object())
{
for (const auto& element : value.items())
{
std::string child = path;
child += '/';
for (const char c : element.key())
{
if (c == '~')
{
child += "~0";
}
else if (c == '/')
{
child += "~1";
}
else
{
child += c;
}
}
pointers.push_back(child);
collect_pointers(element.value(), child, pointers);
}
}
else if (value.is_array())
{
for (std::size_t i = 0; i < value.size(); ++i)
{
std::string child = path;
child += '/';
child += std::to_string(i);
pointers.push_back(child);
collect_pointers(value[i], child, pointers);
}
}
}
// a random document of at most `depth` levels, with the names as keys
json random_document(std::mt19937& gen, const std::vector<std::string>& names, const int depth)
{
const int kind = std::uniform_int_distribution<int>(0, 9)(gen);
if (depth == 0 || kind < 2)
{
return std::uniform_int_distribution<int>(0, 3)(gen);
}
const int size = std::uniform_int_distribution<int>(1, 3)(gen);
json result = kind < 6 ? json::array() : json::object();
for (int i = 0; i < size; ++i)
{
if (kind < 6)
{
result.push_back(random_document(gen, names, depth - 1));
}
else
{
result[names[std::uniform_int_distribution<std::size_t>(0, names.size() - 1)(gen)]] = random_document(gen, names, depth - 1);
}
}
return result;
}
// the strings, one after the other
std::string concat(std::string first, const std::string& second, const std::string& third = std::string())
{
first += second;
first += third;
return first;
}
// the best of five times in seconds that std::clock() measures to apply the patch
double patch_cpu_time(json& doc, const json& patch)
{
double best = 1e30;
for (int i = 0; i < 5; ++i)
{
const std::clock_t start = std::clock();
doc.patch_inplace(patch);
best = (std::min)(best, static_cast<double>(std::clock() - start) / static_cast<double>(CLOCKS_PER_SEC));
}
return best;
}
} // namespace
TEST_CASE("JSON patch - consecutive operations with paths that share a prefix")
{
SECTION("tokens that start the same way")
{
const json doc = {{"a", {{"bc", {{"x", 1}}}, {"b", {{"x", 2}}}, {"b2", 3}}}};
// the second path shares "/a/b" with the first, but not the token "bc"
const json patch = json::parse(R"([
{"op": "test", "path": "/a/bc/x", "value": 1},
{"op": "test", "path": "/a/b/x", "value": 2},
{"op": "test", "path": "/a/bc/x", "value": 1},
{"op": "test", "path": "/a/b2", "value": 3},
{"op": "test", "path": "/a/b/x", "value": 2}
])");
CHECK(doc.patch(patch) == doc);
json inplace = doc;
inplace.patch_inplace(patch);
CHECK(inplace == doc);
// a path that is a prefix of the previous one, and the other way round
const json prefixes = json::parse(R"([
{"op": "test", "path": "/a/bc/x", "value": 1},
{"op": "test", "path": "/a/bc", "value": {"x": 1}},
{"op": "test", "path": "/a/bc/x", "value": 1},
{"op": "test", "path": "/a", "value": {"bc": {"x": 1}, "b": {"x": 2}, "b2": 3}},
{"op": "test", "path": "/a/b/x", "value": 2},
{"op": "test", "path": "", "value": {"a": {"bc": {"x": 1}, "b": {"x": 2}, "b2": 3}}}
])");
CHECK(doc.patch(prefixes) == doc);
}
SECTION("operations above a path that was resolved before")
{
const json doc = json::parse(R"({"a": [{"b": [1, 2, 3]}, {"b": [4, 5, 6]}], "c": {"d": {"e": 7}}})");
// an element inserted before the elements of an array moves them
const json inserted = json::parse(R"([
{"op": "test", "path": "/a/0/b/2", "value": 3},
{"op": "add", "path": "/a/0", "value": {"b": [9]}},
{"op": "test", "path": "/a/1/b/2", "value": 3},
{"op": "test", "path": "/a/0/b/0", "value": 9},
{"op": "test", "path": "/a/2/b/0", "value": 4}
])");
CHECK(doc.patch(inserted) == json::parse(R"({"a": [{"b": [9]}, {"b": [1, 2, 3]}, {"b": [4, 5, 6]}], "c": {"d": {"e": 7}}})"));
// an element removed from the front
const json removed_front = json::parse(R"([
{"op": "test", "path": "/a/1/b/2", "value": 6},
{"op": "remove", "path": "/a/0"},
{"op": "test", "path": "/a/0/b/2", "value": 6},
{"op": "replace", "path": "/a/0/b/2", "value": 60},
{"op": "test", "path": "/a/0/b/2", "value": 60}
])");
CHECK(doc.patch(removed_front) == json::parse(R"({"a": [{"b": [4, 5, 60]}], "c": {"d": {"e": 7}}})"));
// a value replaced above the path that was resolved before
const json replaced_above = json::parse(R"([
{"op": "test", "path": "/c/d/e", "value": 7},
{"op": "replace", "path": "/c/d", "value": {"e": 8, "f": {"g": 9}}},
{"op": "test", "path": "/c/d/e", "value": 8},
{"op": "test", "path": "/c/d/f/g", "value": 9},
{"op": "remove", "path": "/c/d/f"},
{"op": "add", "path": "/c/d/f", "value": [1]},
{"op": "test", "path": "/c/d/f/0", "value": 1}
])");
CHECK(doc.patch(replaced_above) == json::parse(R"({"a": [{"b": [1, 2, 3]}, {"b": [4, 5, 6]}], "c": {"d": {"e": 8, "f": [1]}}})"));
#if !defined(JSON_NOEXCEPTION)
// a value removed above the path that was resolved before is gone
const json removed = json::parse(R"([
{"op": "test", "path": "/c/d/e", "value": 7},
{"op": "remove", "path": "/c/d"},
{"op": "test", "path": "/c/d/e", "value": 7}
])");
CHECK_THROWS_AS(doc.patch(removed), json::other_error&);
json inplace = doc;
CHECK_THROWS_AS(inplace.patch_inplace(removed), json::other_error&);
const json emptied = json::parse(R"([
{"op": "test", "path": "/a/1/b/2", "value": 6},
{"op": "remove", "path": "/a/1"},
{"op": "test", "path": "/a/1/b/2", "value": 6}
])");
CHECK_THROWS_AS(doc.patch(emptied), json::other_error&);
#endif
}
SECTION("paths with more than 16 tokens")
{
// 40 levels, with an array at the bottom and a second branch at level 20
std::string prefix20;
std::string prefix40;
json doc = {{"leaf", {1, 2, 3}}};
for (int i = 40; i > 0; --i)
{
if (i == 20)
{
doc["other"] = {{"x", 5}};
}
doc = {{"n", doc}};
}
for (int i = 1; i <= 40; ++i)
{
prefix40 += "/n";
if (i == 20)
{
prefix20 = prefix40;
}
}
const auto op = [](const char* name, const std::string & path, const json & value)
{
return json({{"op", name}, {"path", path}, {"value", value}});
};
const json patch =
{
op("test", concat(prefix40, "/leaf/2"), 3),
op("test", concat(prefix20, "/other/x"), 5),
op("test", concat(prefix40, "/leaf/0"), 1),
op("add", concat(prefix40, "/leaf/0"), 0),
op("test", concat(prefix40, "/leaf/3"), 3),
op("replace", concat(prefix20, "/other"), {{"x", 6}}),
op("test", concat(prefix20, "/other/x"), 6),
op("test", concat(prefix40, "/leaf/1"), 1),
{{"op", "remove"}, {"path", concat(prefix20, "/other")}},
op("test", concat(prefix40, "/leaf/1"), 1)
};
json expected = doc;
for (const auto& operation : patch)
{
expected = expected.patch(json::array({operation}));
}
CHECK(doc.patch(patch) == expected);
json inplace = doc;
inplace.patch_inplace(patch);
CHECK(inplace == expected);
CHECK(expected[json::json_pointer(concat(prefix40, "/leaf"))] == json({0, 1, 2, 3}));
CHECK(!expected[json::json_pointer(prefix20)].contains("other"));
#if !defined(JSON_NOEXCEPTION)
// the value that was resolved before is gone with the member that held it
const json gone =
{
op("test", concat(prefix40, "/leaf/2"), 3),
{{"op", "remove"}, {"path", prefix20}},
op("test", concat(prefix40, "/leaf/2"), 3)
};
CHECK_THROWS_AS(doc.patch(gone), json::other_error&);
#endif
}
SECTION("random sequences of operations give the same result as one operation at a time")
{
std::mt19937 gen(20261110); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
const std::vector<std::string> names = {"a", "b", "ab", "abc", "a/b", "a~b", "1", "10", "token_longer_than_eight_bytes"};
const auto pick = [&gen](const std::size_t n)
{
return std::uniform_int_distribution<std::size_t>(0, n - 1)(gen);
};
for (int round = 0; round < 300; ++round)
{
const json start = random_document(gen, names, 7);
json current = start;
json patch = json::array();
std::string previous;
for (int step = 0; step < 25; ++step)
{
std::vector<std::string> pointers;
collect_pointers(current, "", pointers);
if (pointers.empty())
{
break;
}
// most of the time, stay below a part of the previous path
std::vector<std::string> candidates;
if (!previous.empty() && pick(10) < 8)
{
std::string prefix = previous;
const std::size_t cut = prefix.find('/', 1 + pick(prefix.size()));
prefix = prefix.substr(0, cut == std::string::npos ? prefix.size() : cut);
for (const auto& p : pointers)
{
if (p.compare(0, prefix.size(), prefix) == 0)
{
candidates.push_back(p);
}
}
}
if (candidates.empty())
{
candidates = pointers;
}
const std::string path = candidates[pick(candidates.size())];
const json::json_pointer pointer(path);
json operation;
switch (pick(5))
{
case 0:
operation = {{"op", "test"}, {"path", path}, {"value", current[pointer]}};
break;
case 1:
operation = {{"op", "replace"}, {"path", path}, {"value", static_cast<int>(pick(100))}};
break;
case 2:
operation = {{"op", "remove"}, {"path", path}};
break;
default:
{
// add below an array or object, which may be the root
const json::json_pointer parent_pointer = pick(4) == 0 ? json::json_pointer() : pointer;
const std::string parent_path = parent_pointer.to_string();
const json& parent = current[parent_pointer];
std::string token = "-";
if (parent.is_array())
{
token = pick(3) == 0 ? "-" : std::to_string(pick(parent.size() + 1));
}
else if (parent.is_object())
{
token = pick(2) == 0 ? "new" : "ab";
}
else
{
// a primitive parent: replace it instead
operation = {{"op", "replace"}, {"path", parent_path}, {"value", json::array({1, 2})}};
break;
}
operation = {{"op", "add"}, {"path", concat(parent_path, "/", token)}, {"value", json::object({{"k", {1, 2}}})}};
break;
}
}
if (operation["path"].get<std::string>().empty())
{
continue;
}
// one operation at a time: the cache is empty at the start of each
current = current.patch(json::array({operation}));
patch.push_back(operation);
previous = operation["path"].get<std::string>();
}
CHECK(start.patch(patch) == current);
json inplace = start;
inplace.patch_inplace(patch);
CHECK(inplace == current);
// the same for a type that keeps the order of the members, which makes
// objects that were built in a different order unequal: leave out the
// tests of structured values
json without_tests = json::array();
for (const auto& operation : patch)
{
if (operation["op"] != "test" || !operation["value"].is_structured())
{
without_tests.push_back(operation);
}
}
const nlohmann::ordered_json ordered_patch = without_tests;
nlohmann::ordered_json ordered_inplace = start;
ordered_inplace.patch_inplace(ordered_patch);
CHECK(json(ordered_inplace) == current);
}
}
}
TEST_CASE("JSON patch - time is linear for long paths that share a prefix")
{
// The time of the patch is compared with the time of a patch that is
// linear for sure, because a bound in seconds would depend on the machine:
// both test the same two values at the end of paths with 2n tokens. The
// second path of the first patch shares n tokens with the first one and
// continues for n tokens: the ratio is 1 to 2, and about 60 when every
// step is compared with the start of the path. The time that std::clock()
// measures is mostly the time of the processor, which a busy machine does
// not inflate (on MSVC, it is the time that has passed).
constexpr std::size_t n = 25000;
const auto nest = [](const std::size_t depth, const std::string & inner)
{
std::string text;
for (std::size_t i = 0; i < depth; ++i)
{
text += "{\"a\":";
}
text += inner;
text.append(depth, '}');
return text;
};
std::string as;
for (std::size_t i = 0; i < n; ++i)
{
as += "/a";
}
const std::string deep = nest(n, "1");
std::string members = "{\"x\":";
members += deep;
members += ",\"y\":";
members += deep;
members += "}";
json doc = json::parse(nest(n, members));
const auto test_op = [](const std::string & path)
{
return json({{"op", "test"}, {"path", path}, {"value", 1}});
};
const json shared = json::array({test_op(concat(as, "/x", as)), test_op(concat(as, "/y", as))});
const json same = json::array({test_op(concat(as, "/x", as)), test_op(concat(as, "/x", as))});
// the operations do not change the document, which is too deep to copy;
// a busy machine may spoil one comparison, hardly two
bool linear = false;
double t_shared = 0;
double t_same = 0;
for (int attempt = 0; attempt < 2 && !linear; ++attempt)
{
t_shared = patch_cpu_time(doc, shared);
t_same = (std::max)(patch_cpu_time(doc, same), 1e-3);
linear = t_shared < 10 * t_same;
}
CAPTURE(t_shared)
CAPTURE(t_same)
CHECK(linear);
}
+69
View File
@@ -906,3 +906,72 @@ TEST_CASE("json_view node array size limit")
d->reserve(cap);
CHECK(d->nodes == nodes);
}
TEST_CASE("json_view document_data::holds() (the debug check of read())")
{
std::unique_ptr<document_data, document_data::deleter> d(document_data::create(0));
const std::string other = "[1,2,3]";
// an empty document holds nothing
CHECK(!d->holds(other.data(), other.size()));
SECTION("owned text")
{
d->owned = R"({"a":"some text that does not fit a short string"})";
const char* const b = d->owned.data();
CHECK(d->holds(b, 1));
CHECK(d->holds(b + 5, 3));
CHECK(d->holds(b + d->owned.size() - 1, 1));
// also after clear(), which keeps the buffer that the next parse writes
d->owned.clear();
// (b is only compared with the buffer, never read)
CHECK(d->holds(b, 4)); // NOLINT(clang-analyzer-cplusplus.InnerPointer)
CHECK(!d->holds(other.data(), other.size()));
CHECK(!d->holds(b, 0));
}
SECTION("decoded strings")
{
d->arena.assign(32, 'x');
CHECK(d->holds(d->arena.data() + 31, 1));
CHECK(d->holds(d->arena.data() + 31, 8)); // (overlapping the end)
CHECK(!d->holds(other.data(), other.size()));
}
SECTION("owned image")
{
d->owned_image.assign(16, 0);
CHECK(d->holds(reinterpret_cast<const char*>(d->owned_image.data()) + 8, 2)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
}
SECTION("current edit buffer")
{
d->edits.reset(new document_data::edit_state()); // NOLINT(cppcoreguidelines-owning-memory)
CHECK(!d->holds(other.data(), other.size()));
d->edits->texts.emplace_back(new char[16]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays,cppcoreguidelines-owning-memory)
d->edits->text_cap = 16;
CHECK(d->holds(d->edits->texts.back().get() + 3, 3));
CHECK(!d->holds(other.data(), other.size()));
}
}
TEST_CASE("json_view read() of inputs the check must accept")
{
// in a debug build each read() below runs the check; none of the inputs
// is held by the document
const std::string text = R"({"a":"x\ny","b":[1,2]})";
nlohmann::json_document d;
d.read(text); // borrowed
d.read(text); // the same borrowed text again
CHECK(d.root()["b"][1].get<int>() == 2);
const std::string copy(d.root()["a"].get_string()); // a decoded string, copied first
d.read(std::string(text)); // owned
d.read(R"(["x"])");
d.read(text);
CHECK(copy == "x\ny");
nlohmann::json_editable_document e;
e.read(text);
e.set(e.root(), "a", "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]");
const std::string edited(e.root()["a"].get_string()); // an edited string, copied first
e.read(edited);
CHECK(e.root().size() == 16);
}
+38
View File
@@ -206,6 +206,44 @@ TEST_CASE("json_view edits: views and values")
CHECK(d.root().materialize().dump() == json::parse(R"([1.5, 100.0, 0.1, null, null, 18446744073709551615, -9223372036854775808])").dump());
}
SECTION("a NaN written by an edit is not equal to itself")
{
// as for basic_json: NaN != NaN, so a view of it, of the array that holds it, and the same view twice agree
json_editable_document d = json_editable_document::parse("[0, 0, 1.5]");
d.set(d.root(), 0, std::numeric_limits<double>::quiet_NaN());
d.set(d.root(), 1, std::numeric_limits<double>::quiet_NaN());
json_editable_document other = json_editable_document::parse("[0, 0, 1.5]");
other.set(other.root(), 0, std::numeric_limits<double>::quiet_NaN());
other.set(other.root(), 1, std::numeric_limits<double>::quiet_NaN());
const json value = d.root().materialize();
CHECK_FALSE(value == value);
CHECK_FALSE(d.root()[0] == d.root()[0]);
CHECK_FALSE(d.root()[0] == d.root()[1]);
CHECK_FALSE(d.root() == d.root());
CHECK_FALSE(d.root() == other.root());
CHECK_FALSE(d.root()[0] == other.root()[0]);
CHECK(d.root() != d.root());
CHECK(d.root()[0] != d.root()[0]);
CHECK_FALSE(d.root() == value);
CHECK_FALSE(d.root()[0] == value[0]);
// the other elements are equal as before
CHECK(d.root()[2] == d.root()[2]);
CHECK(d.root()[2] == other.root()[2]);
// finite and infinite floats written by an edit stay equal to themselves
d.set(d.root(), 0, 0.1);
d.set(d.root(), 1, std::numeric_limits<double>::infinity());
other.set(other.root(), 0, 0.1);
other.set(other.root(), 1, std::numeric_limits<double>::infinity());
CHECK(d.root() == d.root());
CHECK(d.root() == other.root());
CHECK(d.root()[1] == other.root()[1]);
// a document that was never edited
const json_editable_document untouched = json_editable_document::parse("[0.5, [1, 2]]");
CHECK(untouched.root() == untouched.root());
CHECK(untouched.root()[1] == untouched.root()[1]);
}
SECTION("numbers of other float types")
{
// doubles have their own path to the output; other float types are
+42
View File
@@ -759,6 +759,48 @@ TEST_CASE("json_view images: errors")
}
}
SECTION("float tokens: an integer token is a float node only if it does not fit 64 bits")
{
for (const char* const token :
{
"18446744073709551616", "-9223372036854775809", "100000000000000000000", "-18446744073709551616", "123456789012345678901234567890"
})
{
CAPTURE(token)
const std::string text = std::string("[") + token + "]";
const json_document d = json_document::parse(text);
CHECK(d.root()[0].is_number_float());
const std::vector<std::uint8_t> image = d.save();
const std::size_t at = text_at(image) + 1; // the first byte of the token
CHECK(loaded_dump(image) == json::parse(text).dump());
for (std::size_t pos = token[0] == '-' ? 1 : 0; token[pos] != '\0'; ++pos) // (not the sign: that changes the digits)
{
for (char digit = '0'; digit <= '9'; ++digit)
{
std::vector<std::uint8_t> changed = image;
changed[at + pos] = static_cast<std::uint8_t>(digit);
std::string t = token;
t[pos] = digit;
CAPTURE(pos)
CAPTURE(digit)
CAPTURE(t)
// the parser stores an integer for the changed token if it is one (and fits)
const bool float_node = json::accept(t) && json::parse(t).is_number_float();
const std::string result = load_result(changed, image_check::full);
if (float_node)
{
CHECK(result.empty());
CHECK(loaded_dump(changed) == json::parse("[" + t + "]").dump());
}
else
{
CHECK(result == check_failed);
}
}
}
}
}
SECTION("invalid UTF-8 of a decoded string: the compact dump writes it, the others throw")
{
const json_document d = json_document::parse(R"({"k\n":"\u00e9x"})");
+17 -12
View File
@@ -50,13 +50,18 @@ json nested_object(const std::size_t depth, json leaf)
TEST_CASE("issue #5392 - binary writers on deeply nested values")
{
// 200 is past the point where the writers stop recursing, and still
// shallow enough that from_* and operator== (which still recurse) are fine.
const json deep_array = nested_array(200, json(0));
const json deep_object = nested_object(200, json("x"));
const json empty_array = nested_array(200, json::array());
const json empty_object = nested_object(200, json::object());
const json mixed = nested_object(80, nested_array(80, json(true)));
// The limit (128 by default) of the levels that the writers recurse into.
const std::size_t limit = nlohmann::detail::recursion_depth_limit();
// 72 levels more than the limit (200 by default) is past the point where
// the writers stop recursing, and still shallow enough that from_* (which
// does not recurse) and operator== (which does, up to the limit) are quick.
const json deep_array = nested_array(limit + 72, json(0));
const json deep_object = nested_object(limit + 72, json("x"));
const json empty_array = nested_array(limit + 72, json::array());
const json empty_object = nested_object(limit + 72, json::object());
// 2 * (limit / 2 + 16) levels: 160 by default, and more than the limit for every limit
const json mixed = nested_object((limit / 2) + 16, nested_array((limit / 2) + 16, json(true)));
SECTION("roundtrip past the recursion bound")
{
@@ -95,7 +100,7 @@ TEST_CASE("issue #5392 - binary writers on deeply nested values")
SECTION("the two ways of writing a value meet at the bound")
{
for (std::size_t depth = 120; depth <= 140; ++depth)
for (std::size_t depth = limit > 8 ? limit - 8 : 0; depth <= limit + 12; ++depth)
{
CAPTURE(depth)
@@ -118,8 +123,8 @@ TEST_CASE("issue #5392 - binary writers on deeply nested values")
const json ndarray = json({{"_ArrayType_", "uint8"}, {"_ArraySize_", {2, 3}}, {"_ArrayData_", {1, 2, 3, 4, 5, 6}}});
const json invalid = json({{"_ArrayType_", "nope"}, {"_ArraySize_", {1}}, {"_ArrayData_", {1}}});
const json deep_ndarray = nested_array(140, ndarray);
const json deep_invalid = nested_array(140, invalid);
const json deep_ndarray = nested_array(limit + 12, ndarray);
const json deep_invalid = nested_array(limit + 12, invalid);
CHECK(json::from_bjdata(json::to_bjdata(deep_ndarray)) == deep_ndarray);
CHECK(json::from_bjdata(json::to_bjdata(deep_invalid)) == deep_invalid);
@@ -131,10 +136,10 @@ TEST_CASE("issue #5392 - binary writers on deeply nested values")
// nested one-element arrays around the recursion bound: the exact
// bytes a writer produces do not depend on whether it stayed on the
// call stack or moved to the heap one partway through
// (the limit can be 0, where "one level less" is no level)
for (const std::size_t depth :
{
nlohmann::detail::recursion_depth_limit() - 1, nlohmann::detail::recursion_depth_limit(),
nlohmann::detail::recursion_depth_limit() + 1, nlohmann::detail::recursion_depth_limit() + 2
limit > 0 ? limit - 1 : 0, limit, limit + 1, limit + 2
})
{
CAPTURE(depth)
+2 -2
View File
@@ -115,7 +115,7 @@ TEST_CASE("issue #5650 - stack overflow converting between specializations")
// members that the two object types enumerate in different orders.
const auto bury = [](nlohmann::ordered_json value)
{
for (std::size_t i = 0; i < 200; ++i)
for (std::size_t i = 0; i < nlohmann::detail::recursion_depth_limit() + 72; ++i)
{
value = nlohmann::ordered_json::array({std::move(value)});
}
@@ -124,7 +124,7 @@ TEST_CASE("issue #5650 - stack overflow converting between specializations")
const auto dig = [](const json & value)
{
const json* current = &value;
for (std::size_t i = 0; i < 200; ++i)
for (std::size_t i = 0; i < nlohmann::detail::recursion_depth_limit() + 72; ++i)
{
current = &current->at(0);
}
+3 -2
View File
@@ -930,14 +930,15 @@ TEST_CASE("JSON Merge Patch diff of deeply nested values")
SECTION("the patch is the same past the descent bound")
{
// depths on either side of the nesting depth up to which merge_diff()
// recurses (detail::recursion_depth_limit(), 128); not every depth up
// recurses (detail::recursion_depth_limit(), 128 by default); not every depth up
// to 300, to keep the test fast under Valgrind
std::vector<std::size_t> depths;
for (std::size_t depth = 0; depth <= 16; ++depth)
{
depths.push_back(depth);
}
for (std::size_t depth = 120; depth <= 136; ++depth)
const std::size_t limit = nlohmann::detail::recursion_depth_limit();
for (std::size_t depth = limit > 8 ? limit - 8 : 0; depth <= limit + 8; ++depth)
{
depths.push_back(depth);
}
+2 -2
View File
@@ -302,7 +302,7 @@ TEST_CASE("JSON Merge Patch on deeply nested values")
SECTION("patching past the descent bound gives the same result")
{
// every depth on either side of where the iterative version takes
// over (detail::recursion_depth_limit(), 128)
// over (detail::recursion_depth_limit(), 128 by default)
for (std::size_t depth = 0; depth <= 300; ++depth)
{
CAPTURE(depth)
@@ -396,7 +396,7 @@ TEST_CASE("merge_patch() with an argument that aliases *this (#5641)")
SECTION("a patch nested past the iterative descent bound aliases *this")
{
// every depth on either side of where the iterative version takes
// over (detail::recursion_depth_limit(), 128); patching *this with
// over (detail::recursion_depth_limit(), 128 by default); patching *this with
// itself is idempotent, aliased or not
for (const std::size_t depth :
{
+2 -2
View File
@@ -1124,7 +1124,7 @@ TEST_CASE("update() on deeply nested values")
SECTION("merging past the descent bound gives the same result")
{
// every depth on either side of where the iterative version takes
// over (detail::recursion_depth_limit(), 128)
// over (detail::recursion_depth_limit(), 128 by default)
for (std::size_t depth = 0; depth <= 300; ++depth)
{
CAPTURE(depth)
@@ -1215,7 +1215,7 @@ TEST_CASE("update() with an argument that aliases *this (#5641)")
SECTION("nested past the iterative descent bound aliases *this")
{
// every depth on either side of where the iterative version takes
// over (detail::recursion_depth_limit(), 128); merging *this into
// over (detail::recursion_depth_limit(), 128 by default); merging *this into
// itself is idempotent, aliased or not
for (const std::size_t depth :
{
+614
View File
@@ -0,0 +1,614 @@
// __ _____ _____ _____
// __| | __| | | | 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 <algorithm>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <iomanip>
#include <map>
#include <memory>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
// JSON_RECURSION_DEPTH_LIMIT changes only how much call stack the walks over a
// value use, never what they return. This file checks both: the macro is
// honoured (the number of levels the containers handle themselves is the limit)
// and every walk returns what it returns with the default limit. The expected
// results are built here, without the library, so that the same file passes for
// every limit; tests/CMakeLists.txt builds it with limits 0, 1, and 254 besides
// the default. Values are nested up to 300 levels (and past every limit, so that
// the explicit stack of every walk is exercised, even for the limit of 254) and
// are built and taken apart without recursion. Keep the depths moderate: walks
// like diff and dump with indentation are quadratic in the depth.
namespace
{
constexpr std::size_t limit()
{
return nlohmann::detail::recursion_depth_limit();
}
// a value nested as @a kinds says, outermost level first: 'a' is an array of
// one element, 'o' an object with the single key "a"; @a leaf is innermost
json build(const std::string& kinds, json leaf)
{
json j = std::move(leaf);
for (std::size_t i = kinds.size(); i-- > 0;)
{
if (kinds[i] == 'a')
{
json a = json::array();
a.push_back(std::move(j));
j = std::move(a);
}
else
{
json o = json::object();
o["a"] = std::move(j);
j = std::move(o);
}
}
return j;
}
std::string repeat(const std::string& s, std::size_t n)
{
std::string result;
for (std::size_t i = 0; i < n; ++i)
{
result += s;
}
return result;
}
// the nesting of the given shape and depth: arrays, objects, or both alternating
std::string make_kinds(char shape, std::size_t depth)
{
std::string kinds;
for (std::size_t i = 0; i < depth; ++i)
{
if (shape == 'm')
{
kinds += i % 2 == 0 ? 'a' : 'o';
}
else
{
kinds += shape;
}
}
return kinds;
}
std::string expected_compact(const std::string& kinds, const std::string& leaf)
{
std::string s;
for (const char kind : kinds)
{
s += kind == 'a' ? "[" : "{\"a\":";
}
s += leaf;
for (std::size_t i = kinds.size(); i-- > 0;)
{
s += kinds[i] == 'a' ? "]" : "}";
}
return s;
}
// dump(1)
std::string expected_indented(const std::string& kinds, const std::string& leaf)
{
std::string s;
for (std::size_t i = 0; i < kinds.size(); ++i)
{
s += kinds[i] == 'a' ? "[\n" : "{\n";
s += std::string(i + 1, ' ');
if (kinds[i] == 'o')
{
s += "\"a\": ";
}
}
s += leaf;
for (std::size_t i = kinds.size(); i-- > 0;)
{
s += "\n" + std::string(i, ' ') + (kinds[i] == 'a' ? "]" : "}");
}
return s;
}
// the JSON pointer to the innermost value
std::string pointer(const std::string& kinds)
{
std::string s;
for (const char kind : kinds)
{
s += kind == 'a' ? "/0" : "/a";
}
return s;
}
// the binary formats: the bytes that open and close a level, and the leaf 1
std::vector<std::uint8_t> expected_cbor(const std::string& kinds)
{
std::vector<std::uint8_t> bytes;
for (const char kind : kinds)
{
if (kind == 'a')
{
bytes.push_back(0x81);
}
else
{
bytes.push_back(0xA1);
bytes.push_back(0x61);
bytes.push_back('a');
}
}
bytes.push_back(0x01);
return bytes;
}
std::vector<std::uint8_t> expected_msgpack(const std::string& kinds)
{
std::vector<std::uint8_t> bytes;
for (const char kind : kinds)
{
if (kind == 'a')
{
bytes.push_back(0x91);
}
else
{
bytes.push_back(0x81);
bytes.push_back(0xA1);
bytes.push_back('a');
}
}
bytes.push_back(0x01);
return bytes;
}
std::vector<std::uint8_t> expected_ubjson(const std::string& kinds)
{
std::vector<std::uint8_t> bytes;
for (const char kind : kinds)
{
if (kind == 'a')
{
bytes.push_back('[');
}
else
{
bytes.push_back('{');
bytes.push_back('i');
bytes.push_back(1);
bytes.push_back('a');
}
}
bytes.push_back('i');
bytes.push_back(1);
for (std::size_t i = kinds.size(); i-- > 0;)
{
bytes.push_back(kinds[i] == 'a' ? ']' : '}');
}
return bytes;
}
// the depths worth trying for the limit: either side of it, twice it (if that
// is not too deep to be quick), and a value nested well past all limits
std::vector<std::size_t> depths()
{
std::vector<std::size_t> result;
result.push_back(1);
result.push_back(2);
result.push_back(limit() > 0 ? limit() - 1 : 3);
result.push_back(limit());
result.push_back(limit() + 1);
if ((2 * limit()) + 5 <= 300)
{
result.push_back((2 * limit()) + 5);
}
result.push_back(300);
std::sort(result.begin(), result.end());
result.erase(std::unique(result.begin(), result.end()), result.end());
result.erase(std::remove(result.begin(), result.end(), static_cast<std::size_t>(0)), result.end());
return result;
}
// the call for every shape and depth
void for_each_value(const std::function<void(const std::string&)>& check)
{
const std::string shapes = "aom";
for (const char shape : shapes)
{
for (const std::size_t depth : depths())
{
CAPTURE(shape)
CAPTURE(depth)
check(make_kinds(shape, depth));
}
}
}
// An array type that counts how many of its copies are being made at once. A
// copy of a value copies one level at a time through the array type until the
// limit has been reached, so the peak is the number of levels that were copied
// with the call stack.
std::size_t copies_going_on = 0;
std::size_t copies_peak = 0;
std::size_t equal_going_on = 0;
std::size_t equal_peak = 0;
template<class T, class Allocator = std::allocator<T>>
class counting_vector : public std::vector<T, Allocator>
{
public:
counting_vector() = default;
template<class InputIt>
counting_vector(InputIt first, InputIt last) : std::vector<T, Allocator>(first, last) {}
counting_vector(const counting_vector& other) : std::vector<T, Allocator>()
{
++copies_going_on;
copies_peak = (std::max)(copies_peak, copies_going_on);
this->std::vector<T, Allocator>::operator=(static_cast<const std::vector<T, Allocator>&>(other));
--copies_going_on;
}
counting_vector(counting_vector&&) = default;
counting_vector& operator=(const counting_vector&) = default;
counting_vector& operator=(counting_vector&&) = default;
~counting_vector() = default;
friend bool operator==(const counting_vector& lhs, const counting_vector& rhs)
{
++equal_going_on;
equal_peak = (std::max)(equal_peak, equal_going_on);
const bool result = static_cast<const std::vector<T, Allocator>&>(lhs) == static_cast<const std::vector<T, Allocator>&>(rhs);
--equal_going_on;
return result;
}
};
using counting_json = json::with_array_t<counting_vector>;
// A base class whose copy constructor copies a whole value of the same
// specialization when it copies a marked element, as user code may do from a
// hook. That copy runs while the copy that it is part of is going on.
struct copy_hook // NOLINT(cppcoreguidelines-special-member-functions,hicpp-special-member-functions)
{
int mark = 0;
copy_hook() = default;
copy_hook(const copy_hook& other);
copy_hook(copy_hook&&) = default;
copy_hook& operator=(const copy_hook&) = default;
copy_hook& operator=(copy_hook&&) = default;
};
using hook_json = nlohmann::basic_json<std::map, counting_vector, std::string, bool, std::int64_t, std::uint64_t, double,
std::allocator, nlohmann::adl_serializer, std::vector<std::uint8_t>, copy_hook>;
const hook_json* hook_copy_source = nullptr;
int hook_copies_going_on = 0;
copy_hook::copy_hook(const copy_hook& other) : mark(other.mark)
{
// the value that is copied is marked, too: its copy copies it again, once
if (other.mark == 1 && hook_copy_source != nullptr && hook_copies_going_on < 2)
{
++hook_copies_going_on;
const hook_json nested = *hook_copy_source; // NOLINT(performance-unnecessary-copy-initialization)
static_cast<void>(nested);
--hook_copies_going_on;
}
}
// an array nested @a depth levels around the integer 1, which is marked if @a marked
hook_json hook_nested(std::size_t depth, bool marked)
{
hook_json j = 1;
if (marked)
{
static_cast<copy_hook&>(j).mark = 1;
}
for (std::size_t i = 0; i < depth; ++i)
{
hook_json a = hook_json::array();
a.push_back(std::move(j));
j = std::move(a);
}
return j;
}
} // namespace
// the macro is the limit, and is in the range the library supports
static_assert(nlohmann::detail::recursion_depth_limit() == static_cast<std::size_t>(JSON_RECURSION_DEPTH_LIMIT),
"the library does not use JSON_RECURSION_DEPTH_LIMIT");
static_assert(nlohmann::detail::recursion_depth_limit() <= 254, "the nesting depth must fit in a byte");
// the macro must not be a character or bool: every character type is refused
static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool<bool>::value, "bool is refused");
static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool<char>::value, "char is refused");
static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool<signed char>::value, "signed char is refused");
static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool<unsigned char>::value, "unsigned char is refused");
static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool<wchar_t>::value, "wchar_t is refused");
static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool<char16_t>::value, "char16_t is refused");
static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool<char32_t>::value, "char32_t is refused");
#if defined(__cpp_char8_t)
static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool<char8_t>::value, "char8_t is refused");
#endif
static_assert(!nlohmann::detail::recursion_depth_limit_is_char_or_bool<int>::value, "int is accepted");
static_assert(!nlohmann::detail::recursion_depth_limit_is_char_or_bool<unsigned long long>::value, "unsigned long long is accepted");
TEST_CASE("JSON_RECURSION_DEPTH_LIMIT is honoured")
{
SECTION("copying and comparing stop descending at the limit")
{
// copy and compare a value nested deeper than the limit, with the array type that counts
counting_json inner = 1;
for (std::size_t i = 0; i < (2 * limit()) + 5; ++i)
{
counting_json a = counting_json::array();
a.push_back(std::move(inner));
inner = std::move(a);
}
copies_going_on = copies_peak = 0;
const counting_json copy = inner;
CHECK(copies_going_on == 0);
equal_going_on = equal_peak = 0;
CHECK((inner == copy));
CHECK(equal_going_on == 0);
#ifdef JSON_NO_THREAD_LOCAL
// without the counters of the levels, no level is descended into
CHECK(copies_peak == 0);
CHECK(equal_peak == 0);
#else
// copying descends into as many levels as the limit says, and nothing further
CHECK(copies_peak == limit());
CHECK(equal_peak == limit());
#endif
// a value that is not nested deeper than the limit is not cut short
const std::size_t shallow = limit() > 0 ? limit() : 1;
counting_json small = 1;
for (std::size_t i = 0; i < shallow; ++i)
{
counting_json a = counting_json::array();
a.push_back(std::move(small));
small = std::move(a);
}
copies_going_on = copies_peak = 0;
const counting_json small_copy = small;
CHECK(copies_going_on == 0);
#ifndef JSON_NO_THREAD_LOCAL
CHECK(copies_peak == limit());
#endif
CHECK(small_copy.dump() == expected_compact(make_kinds('a', shallow), "1"));
}
}
TEST_CASE("a copy made while a copy is going on is bounded, too")
{
// The innermost element of the value is marked, and copying it (which happens
// after limit() levels have been entered, when the copy continues without the
// call stack) copies the value again, and that copy once more. Those copies
// must not be given limit() levels of their own: the count of the levels entered
// never exceeds the limit, not even by one, which for the largest limit would
// wrap the byte that holds it around to 0.
const hook_json marked = hook_nested((2 * limit()) + 5, true);
hook_copy_source = &marked;
copies_going_on = copies_peak = 0;
const hook_json copy = marked; // NOLINT(performance-unnecessary-copy-initialization)
hook_copy_source = nullptr;
CHECK(copies_going_on == 0);
CHECK(static_cast<const copy_hook&>(copy).mark == 0); // (the top level is not marked)
#ifdef JSON_NO_THREAD_LOCAL
CHECK(copies_peak == 0);
#else
CHECK(copies_peak == limit());
#endif
}
TEST_CASE("walks over values nested around the recursion depth limit")
{
SECTION("copying and comparing")
{
for_each_value([](const std::string & kinds)
{
const json a = build(kinds, 1);
const json b = a; // NOLINT(performance-unnecessary-copy-initialization)
CHECK((a == b));
CHECK_FALSE((a != b));
CHECK_FALSE((a < b));
CHECK((a <= b));
CHECK((a >= b));
CHECK_FALSE((a > b));
json assigned;
assigned = a;
CHECK((assigned == a));
// differing only in the innermost value
const json c = build(kinds, 2);
CHECK((a != c));
CHECK((a < c));
CHECK((c > a));
CHECK_FALSE((c < a));
CHECK_FALSE((a == c));
// a same-shape array or object that is one level shallower
const json shallower = build(kinds.substr(1), 1);
CHECK((shallower != a));
const json outer = build(kinds.substr(0, 1), 1);
CHECK((outer != a) == (kinds.size() > 1));
const json moved = std::move(assigned);
CHECK((moved == a));
});
}
SECTION("serializing")
{
for_each_value([](const std::string & kinds)
{
const json a = build(kinds, 1);
const std::string compact = expected_compact(kinds, "1");
const std::string indented = expected_indented(kinds, "1");
CHECK(a.dump() == compact);
CHECK(a.dump(1) == indented);
std::ostringstream compact_stream;
compact_stream << a;
CHECK(compact_stream.str() == compact);
std::ostringstream indented_stream;
indented_stream << std::setw(1) << a;
CHECK(indented_stream.str() == indented);
CHECK((json::parse(compact) == a));
CHECK((json::parse(indented) == a));
});
}
SECTION("hashing")
{
for_each_value([](const std::string & kinds)
{
const json a = build(kinds, 1);
const std::hash<json> hash{};
CHECK(hash(a) == hash(json(a)));
CHECK(hash(a) == hash(json::parse(expected_compact(kinds, "1"))));
CHECK(hash(a) != hash(build(kinds, 2)));
CHECK(hash(a) != hash(build(kinds.substr(1), 1)));
});
}
SECTION("diff and patch")
{
for_each_value([](const std::string & kinds)
{
const json a = build(kinds, 1);
const json b = build(kinds, 2);
const json difference = json::diff(a, b);
CHECK(difference.dump() == "[{\"op\":\"replace\",\"path\":\"" + pointer(kinds) + "\",\"value\":2}]");
CHECK((a.patch(difference) == b));
CHECK((json::diff(a, a) == json::array()));
});
}
SECTION("flatten and unflatten")
{
for_each_value([](const std::string & kinds)
{
const json a = build(kinds, 1);
const json flat = a.flatten();
CHECK(flat.dump() == "{\"" + pointer(kinds) + "\":1}");
CHECK((flat.unflatten() == a));
});
}
SECTION("merge_patch and update")
{
for (const std::size_t depth : depths())
{
CAPTURE(depth)
const std::string kinds = make_kinds('o', depth);
const std::string all_but_leaf = repeat("{\"a\":", depth - 1);
// the patch removes the innermost member, or replaces it
json target = build(kinds, 1);
target.merge_patch(build(kinds, json(nullptr)));
CHECK(target.dump() == all_but_leaf + "{}" + repeat("}", depth - 1));
target = build(kinds, 1);
target.merge_patch(build(kinds, 2));
CHECK(target.dump() == expected_compact(kinds, "2"));
// merging the objects of two values nested alike, and replacing the first level
const json base = build(kinds.substr(1), json::parse("{\"x\":1}"));
const json other = build(kinds.substr(1), json::parse("{\"y\":2}"));
json merged = json{{"a", base}};
merged.update(json{{"a", other}}, true);
CHECK(merged.dump() == expected_compact(kinds, "{\"x\":1,\"y\":2}"));
json replaced = json{{"a", base}};
replaced.update(json{{"a", other}});
CHECK(replaced.dump() == expected_compact(kinds, "{\"y\":2}"));
// update from a range
json from_range = json{{"a", base}};
const json source = json{{"a", other}};
from_range.update(source.begin(), source.end(), true);
CHECK((from_range == merged));
}
}
SECTION("binary formats")
{
for_each_value([](const std::string & kinds)
{
const json a = build(kinds, 1);
const std::vector<std::uint8_t> cbor = json::to_cbor(a);
CHECK(cbor == expected_cbor(kinds));
CHECK((json::from_cbor(cbor) == a));
const std::vector<std::uint8_t> msgpack = json::to_msgpack(a);
CHECK(msgpack == expected_msgpack(kinds));
CHECK((json::from_msgpack(msgpack) == a));
const std::vector<std::uint8_t> ubjson = json::to_ubjson(a);
CHECK(ubjson == expected_ubjson(kinds));
CHECK((json::from_ubjson(ubjson) == a));
CHECK((json::from_bjdata(json::to_bjdata(a)) == a));
CHECK((json::from_bon8(json::to_bon8(a)) == a));
// BSON documents are objects
const json document = json{{"a", a}};
CHECK((json::from_bson(json::to_bson(document)) == document));
});
}
SECTION("conversion to another specialization")
{
for_each_value([](const std::string & kinds)
{
const json a = build(kinds, 1);
const nlohmann::ordered_json ordered = a;
CHECK(ordered.dump() == expected_compact(kinds, "1"));
const json back = ordered;
CHECK((back == a));
});
}
SECTION("destroying")
{
for_each_value([](const std::string & kinds)
{
json a = build(kinds, 1);
a = nullptr;
CHECK(a.is_null());
});
}
}
+4 -2
View File
@@ -1554,8 +1554,10 @@ TEST_CASE("regression tests 1")
SECTION("a missing member of an object")
{
my_json j = make();
#ifdef JSON_NO_THREAD_LOCAL
// (set by the user, or by the library for clang with MinGW)
#if defined(JSON_NO_THREAD_LOCAL) || JSON_RECURSION_DEPTH_LIMIT < 2
// (set by the user, or by the library for clang with MinGW; or fewer than two
// levels are copied by the object type itself, as for a recursion depth limit
// of 0 or 1: the objects below the outermost one are the second level)
// the objects below the outermost one are not copied by their
// object_t here, but member by member, which keeps their
// timestamps correct: "c" is not mistaken for "b" and is inserted