Add JSON_RECURSION_DEPTH_LIMIT to set how deep value walks recurse (#6081)
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
1 parent
0660d78b9c
commit
1dae5a66b1
30 files changed
+1340
-95
No files matched your search
@@ -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
|
||||
|
||||
@@ -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"})}};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}}}};
|
||||
|
||||
@@ -1978,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);
|
||||
}
|
||||
@@ -2527,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};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = ¤t->at(0);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 :
|
||||
{
|
||||
|
||||
@@ -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 :
|
||||
{
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user