// __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ (supporting code) // | | |__ | | | | | | version 3.12.0 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // // SPDX-FileCopyrightText: 2013-2026 Niels Lohmann // SPDX-License-Identifier: MIT #include "doctest_compatibility.h" #include using nlohmann::json; #include #include #include #include #include // split from unit-large_json.cpp so that both run in parallel and stay below // the ctest timeout under Valgrind namespace { json nested_array(const std::size_t depth, json leaf) { json j = std::move(leaf); for (std::size_t i = 0; i < depth; ++i) { json a = json::array(); a.push_back(std::move(j)); j = std::move(a); } return j; } json nested_object(const std::size_t depth, json leaf) { json j = std::move(leaf); for (std::size_t i = 0; i < depth; ++i) { json o = json::object(); o["k"] = std::move(j); j = std::move(o); } return j; } } // namespace 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))); SECTION("roundtrip past the recursion bound") { CHECK(json::from_cbor(json::to_cbor(deep_array)) == deep_array); CHECK(json::from_msgpack(json::to_msgpack(deep_array)) == deep_array); CHECK(json::from_ubjson(json::to_ubjson(deep_array)) == deep_array); CHECK(json::from_ubjson(json::to_ubjson(deep_array, true, false)) == deep_array); CHECK(json::from_ubjson(json::to_ubjson(deep_array, true, true)) == deep_array); CHECK(json::from_bjdata(json::to_bjdata(deep_array)) == deep_array); CHECK(json::from_bon8(json::to_bon8(deep_array)) == deep_array); CHECK(json::from_cbor(json::to_cbor(deep_object)) == deep_object); CHECK(json::from_msgpack(json::to_msgpack(deep_object)) == deep_object); CHECK(json::from_ubjson(json::to_ubjson(deep_object)) == deep_object); CHECK(json::from_ubjson(json::to_ubjson(deep_object, true, true)) == deep_object); CHECK(json::from_bjdata(json::to_bjdata(deep_object)) == deep_object); CHECK(json::from_bon8(json::to_bon8(deep_object)) == deep_object); CHECK(json::from_cbor(json::to_cbor(empty_array)) == empty_array); CHECK(json::from_msgpack(json::to_msgpack(empty_array)) == empty_array); CHECK(json::from_ubjson(json::to_ubjson(empty_array)) == empty_array); CHECK(json::from_ubjson(json::to_ubjson(empty_array, true, true)) == empty_array); CHECK(json::from_bon8(json::to_bon8(empty_array)) == empty_array); CHECK(json::from_cbor(json::to_cbor(empty_object)) == empty_object); CHECK(json::from_msgpack(json::to_msgpack(empty_object)) == empty_object); CHECK(json::from_ubjson(json::to_ubjson(empty_object)) == empty_object); CHECK(json::from_bon8(json::to_bon8(empty_object)) == empty_object); CHECK(json::from_cbor(json::to_cbor(mixed)) == mixed); CHECK(json::from_msgpack(json::to_msgpack(mixed)) == mixed); CHECK(json::from_ubjson(json::to_ubjson(mixed)) == mixed); CHECK(json::from_bjdata(json::to_bjdata(mixed)) == mixed); CHECK(json::from_bon8(json::to_bon8(mixed)) == mixed); } SECTION("the two ways of writing a value meet at the bound") { for (std::size_t depth = 120; depth <= 140; ++depth) { CAPTURE(depth) const json array = nested_array(depth, json(7)); CHECK(json::from_cbor(json::to_cbor(array)) == array); CHECK(json::from_msgpack(json::to_msgpack(array)) == array); CHECK(json::from_ubjson(json::to_ubjson(array, true, true)) == array); CHECK(json::from_bon8(json::to_bon8(array)) == array); const json object = nested_object(depth, json(7)); CHECK(json::from_cbor(json::to_cbor(object)) == object); CHECK(json::from_msgpack(json::to_msgpack(object)) == object); CHECK(json::from_bjdata(json::to_bjdata(object)) == object); CHECK(json::from_bon8(json::to_bon8(object)) == object); } } SECTION("a BJData ndarray below the bound is still an ndarray") { 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); CHECK(json::from_bjdata(json::to_bjdata(deep_ndarray)) == deep_ndarray); CHECK(json::from_bjdata(json::to_bjdata(deep_invalid)) == deep_invalid); CHECK(json::from_bjdata(json::to_bjdata(ndarray)) == ndarray); } SECTION("byte-exact across the switch-over") { // 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 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 }) { CAPTURE(depth) const json array = nested_array(depth, json(0)); std::vector expected_cbor(depth, 0x81); expected_cbor.push_back(0x00); CHECK(json::to_cbor(array) == expected_cbor); std::vector expected_msgpack(depth, 0x91); expected_msgpack.push_back(0x00); CHECK(json::to_msgpack(array) == expected_msgpack); std::string expected_ubjson(depth, '['); expected_ubjson += "i"; expected_ubjson += '\0'; expected_ubjson.append(depth, ']'); const auto packed_ubjson = json::to_ubjson(array); CHECK(std::string(packed_ubjson.begin(), packed_ubjson.end()) == expected_ubjson); std::vector expected_bon8(depth, 0x81); expected_bon8.push_back(0x90); CHECK(json::to_bon8(array) == expected_bon8); } } SECTION("a deep object, and a BJData ndarray, past the recursion bound") { const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 50; const json object = nested_object(depth, json(42)); CHECK(json::from_cbor(json::to_cbor(object)) == object); CHECK(json::from_msgpack(json::to_msgpack(object)) == object); CHECK(json::from_ubjson(json::to_ubjson(object, true, true)) == object); CHECK(json::from_bjdata(json::to_bjdata(object)) == object); CHECK(json::from_bon8(json::to_bon8(object)) == object); const json ndarray = json({{"_ArrayType_", "uint8"}, {"_ArraySize_", {2, 3}}, {"_ArrayData_", {1, 2, 3, 4, 5, 6}}}); const json deep_ndarray = nested_array(depth, ndarray); CHECK(json::from_bjdata(json::to_bjdata(deep_ndarray)) == deep_ndarray); } SECTION("a discarded value past the recursion bound still throws type_error.321") { const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 50; const json discarded_leaf(json::value_t::discarded); const json deep_discarded = nested_array(depth, discarded_leaf); // with diagnostics, the message names the path to the discarded leaf #if JSON_DIAGNOSTICS std::string path; for (std::size_t i = 0; i < depth; ++i) { path += "/0"; } const std::string prefix = "[json.exception.type_error.321] (" + path + ") "; #else const std::string prefix = "[json.exception.type_error.321] "; #endif CHECK_THROWS_WITH_AS(json::to_cbor(deep_discarded), (prefix + "cannot serialize discarded value to CBOR").c_str(), json::type_error); CHECK_THROWS_WITH_AS(json::to_msgpack(deep_discarded), (prefix + "cannot serialize discarded value to MessagePack").c_str(), json::type_error); CHECK_THROWS_WITH_AS(json::to_ubjson(deep_discarded), (prefix + "cannot serialize discarded value to UBJSON").c_str(), json::type_error); CHECK_THROWS_WITH_AS(json::to_bjdata(deep_discarded), (prefix + "cannot serialize discarded value to BJData").c_str(), json::type_error); } SECTION("BON8 past the recursion bound: discarded values and errors") { const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 50; // BON8 rejects a discarded value (type_error.321), deep or not const json deep_discarded = nested_array(depth, json(json::value_t::discarded)); CHECK_THROWS_AS(json::to_bon8(deep_discarded), json::type_error); // errors are thrown from below the bound as from above it const json deep_invalid_string = nested_array(depth, json(std::string("\x80"))); CHECK_THROWS_AS(json::to_bon8(deep_invalid_string), json::type_error); json deep_invalid_key = json::object(); deep_invalid_key[std::string("\x80")] = 1; deep_invalid_key = nested_object(depth, deep_invalid_key); CHECK_THROWS_AS(json::to_bon8(deep_invalid_key), json::type_error); const json deep_too_large = nested_array(depth, json(9223372036854775808u)); CHECK_THROWS_AS(json::to_bon8(deep_too_large), json::out_of_range); } SECTION("does not overflow the C++ stack") { const std::size_t depth = 100000; const json j = json::parse(std::string(depth, '[') + "0" + std::string(depth, ']')); std::vector packed; CHECK_NOTHROW(packed = json::to_cbor(j)); CHECK(json::from_cbor(packed) == j); CHECK_NOTHROW(packed = json::to_msgpack(j)); CHECK(json::from_msgpack(packed) == j); CHECK_NOTHROW(packed = json::to_ubjson(j)); CHECK(json::from_ubjson(packed) == j); CHECK_NOTHROW(packed = json::to_ubjson(j, true, false)); CHECK(json::from_ubjson(packed) == j); CHECK_NOTHROW(packed = json::to_bjdata(j)); CHECK(json::from_bjdata(packed) == j); CHECK_NOTHROW(packed = json::to_bon8(j)); CHECK(json::from_bon8(packed) == j); } SECTION("BON8 containers with more than four elements past the recursion bound") { // such containers are closed with 0xFE, which the iterative writer // must emit when it leaves them; strings next to each other must // still be separated with 0xFF const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 50; json wide_array = json::array({1, "a", "b", json::array(), 5}); json wide_object = json({{"k1", "v"}, {"k2", 2}, {"k3", ""}, {"k4", json::object()}, {"k5", nullptr}}); for (std::size_t i = 0; i < depth; ++i) { wide_array = json::array({"s", wide_array, "t", wide_object, true}); wide_object = json({{"a", wide_object}, {"b", "x"}, {"c", "y"}, {"d", wide_array.size()}, {"e", "z"}}); } CHECK(json::from_bon8(json::to_bon8(wide_array)) == wide_array); CHECK(json::from_bon8(json::to_bon8(wide_object)) == wide_object); } SECTION("regression test for https://issues.oss-fuzz.com/issues/566583014") { // 200000 nested one-element CBOR arrays, the innermost holding null; // round-tripping this used to recurse once per level on the way back // out through to_cbor(), deep enough to overflow the stack std::vector v(200000, 0x81); v.push_back(0xf6); const json j = json::from_cbor(v); CHECK(json::to_cbor(j) == v); // the MessagePack analogue: fixarray of 1 nesting down to nil std::vector v_msgpack(200000, 0x91); v_msgpack.push_back(0xc0); const json j_msgpack = json::from_msgpack(v_msgpack); CHECK(json::to_msgpack(j_msgpack) == v_msgpack); } SECTION("regression test for https://issues.oss-fuzz.com/issues/572238015") { // the BON8 analogue: 200000 nested one-element arrays down to null std::vector v(200000, 0x81); v.push_back(0xFA); const json j = json::from_bon8(v); CHECK(json::to_bon8(j) == v); } }