mirror of
https://github.com/nlohmann/json.git
synced 2026-10-01 22:45:17 +00:00
* Fix stack overflow converting deep values between specializations Constructing a basic_json from another specialization (json to ordered_json or back, also via get<ordered_json>()) converted every container with its range constructor, which calls the converting constructor for each element. The call stack therefore grew with every nesting level, and a value nested some 30,000 levels deep overflowed it. The conversion now bounds its descent the way the copy constructor does since #5387: the first 128 levels are converted exactly as before, and below that convert_iteratively() finishes the value with an explicit stack. It builds each container bottom-up from its converted elements with the container's range constructor, so member order and keys that become equal are handled as before, and it gives a value its type only once its container exists, so an exception leaves nothing behind that cannot be destroyed. Parents (JSON_DIAGNOSTICS) and positions (JSON_DIAGNOSTIC_POSITIONS) are set for every value. Converting a null value no longer resets its positions: the constructor assigned null to a value that already was null, which swapped in the positions of the temporary. Fixes #5650. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Explain why converting null keeps positions and why next is a reference Review feedback on #5723 (gregmarr): clarify in comments that the converting constructor has already copied the positions of val, which the null case keeps like every other case, and that next must be a reference into pending so that ++next advances the stored iterator. Comments only; no code change. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Refer to recursion_depth_limit() in the convert_structured() docs The comment still named nesting_depth_limit, which #5637 removed on develop in favor of detail::recursion_depth_limit(). Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Advance the pending iterator through pending.back() and shorten the null comment Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me>
246 lines
9.0 KiB
C++
246 lines
9.0 KiB
C++
// __ _____ _____ _____
|
|
// __| | __| | | | JSON for Modern C++ (supporting code)
|
|
// | | |__ | | | | | | version 3.12.0
|
|
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
|
//
|
|
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#include "doctest_compatibility.h"
|
|
|
|
#ifndef JSON_DIAGNOSTICS
|
|
#define JSON_DIAGNOSTICS 1
|
|
#endif
|
|
#define JSON_DIAGNOSTIC_POSITIONS 1
|
|
#include <nlohmann/json.hpp>
|
|
|
|
using json = nlohmann::json;
|
|
|
|
TEST_CASE("Better diagnostics with positions")
|
|
{
|
|
SECTION("invalid type")
|
|
{
|
|
const std::string json_invalid_string = R"(
|
|
{
|
|
"address": {
|
|
"street": "Fake Street",
|
|
"housenumber": "1"
|
|
}
|
|
}
|
|
)";
|
|
json j = json::parse(json_invalid_string);
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.at("address").at("housenumber").get<int>(),
|
|
"[json.exception.type_error.302] (/address/housenumber) (bytes 108-111) type must be number, but is string", json::type_error);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.at("address").at("housenumber").get<int>(),
|
|
"[json.exception.type_error.302] (bytes 108-111) type must be number, but is string", json::type_error);
|
|
#endif
|
|
}
|
|
|
|
SECTION("invalid type without positions")
|
|
{
|
|
const json j = "foo";
|
|
CHECK_THROWS_WITH_AS(j.get<int>(),
|
|
"[json.exception.type_error.302] type must be number, but is string", json::type_error);
|
|
}
|
|
|
|
SECTION("positions of strings containing escape sequences")
|
|
{
|
|
// escape sequences make the token longer than the string it parses to,
|
|
// so the positions must not be derived from the parsed value's length
|
|
const auto check = [](const std::string & text, const std::string & token)
|
|
{
|
|
CAPTURE(text)
|
|
CAPTURE(token)
|
|
const json j = json::parse(text);
|
|
const json& v = j.at("a");
|
|
CHECK(text.substr(v.start_pos(), v.end_pos() - v.start_pos()) == token);
|
|
};
|
|
|
|
check(R"({"a":"plain"})", R"("plain")");
|
|
check(R"({"a":"tab\there"})", R"("tab\there")");
|
|
check(R"({"a":"\n\n\n\n\n\n"})", R"("\n\n\n\n\n\n")");
|
|
check(R"({"a":"\""})", R"("\"")");
|
|
check(R"({"a":"\\"})", R"("\\")");
|
|
check(R"({"a":"é"})", R"("é")");
|
|
check(R"({"a":"🌞"})", R"("🌞")");
|
|
check("{\"a\":\"\xc3\xa9\"}", "\"\xc3\xa9\""); // multi-byte UTF-8, no escapes
|
|
|
|
// a string at the root, where an escape would otherwise push the
|
|
// reported start position past the opening quote
|
|
const std::string root = R"("a\tb")";
|
|
const json j = json::parse(root);
|
|
CHECK(j.start_pos() == 0);
|
|
CHECK(j.end_pos() == root.size());
|
|
}
|
|
|
|
SECTION("copying keeps the positions of nested values (#5387)")
|
|
{
|
|
// Values nested deeper than the copy constructor's descent bound are
|
|
// copied without the call stack, on a path that has to carry the
|
|
// positions over itself; shallower ones copy their containers, which
|
|
// bring the positions along. Both sides of the bound are checked here.
|
|
const auto check_copy = [](std::size_t depth, bool objects)
|
|
{
|
|
CAPTURE(depth)
|
|
CAPTURE(objects)
|
|
|
|
const std::string opening = objects ? R"({"a":)" : "[";
|
|
const std::string closing = objects ? "}" : "]";
|
|
|
|
std::string text;
|
|
for (std::size_t i = 0; i < depth; ++i)
|
|
{
|
|
text += opening;
|
|
}
|
|
text += "12";
|
|
for (std::size_t i = 0; i < depth; ++i)
|
|
{
|
|
text += closing;
|
|
}
|
|
|
|
const json original = json::parse(text);
|
|
const json copy(original); // NOLINT(performance-unnecessary-copy-initialization)
|
|
|
|
const json* o = &original;
|
|
const json* c = ©
|
|
for (std::size_t level = 0; level <= depth; ++level)
|
|
{
|
|
CAPTURE(level)
|
|
REQUIRE(c->start_pos() == o->start_pos());
|
|
REQUIRE(c->end_pos() == o->end_pos());
|
|
|
|
if (level < depth)
|
|
{
|
|
o = objects ? &o->at("a") : &o->at(0);
|
|
c = objects ? &c->at("a") : &c->at(0);
|
|
}
|
|
}
|
|
};
|
|
|
|
const auto check_arrays = [&check_copy](std::size_t depth)
|
|
{
|
|
check_copy(depth, false);
|
|
};
|
|
const auto check_objects = [&check_copy](std::size_t depth)
|
|
{
|
|
check_copy(depth, true);
|
|
};
|
|
|
|
check_arrays(1);
|
|
check_arrays(127);
|
|
check_arrays(128);
|
|
check_arrays(129);
|
|
check_arrays(300);
|
|
|
|
check_objects(1);
|
|
check_objects(127);
|
|
check_objects(128);
|
|
check_objects(129);
|
|
check_objects(300);
|
|
}
|
|
|
|
SECTION("converting keeps the positions of nested values (#5650)")
|
|
{
|
|
// Values nested deeper than the converting constructor's descent bound
|
|
// are converted without the call stack, on a path that has to carry the
|
|
// positions of every value over itself. Objects and arrays take turns,
|
|
// and the innermost value is null, which used to lose its positions.
|
|
const auto check_conversion = [](std::size_t depth)
|
|
{
|
|
CAPTURE(depth)
|
|
|
|
std::string text;
|
|
std::string closing;
|
|
for (std::size_t i = 0; i < depth; ++i)
|
|
{
|
|
text += (i % 2 == 0) ? "[12, " : R"({"b":1, "a":)";
|
|
closing += (i % 2 == 0) ? ']' : '}';
|
|
}
|
|
text += "null";
|
|
text.append(closing.rbegin(), closing.rend());
|
|
|
|
const json original = json::parse(text);
|
|
const nlohmann::ordered_json converted = original;
|
|
|
|
const json* o = &original;
|
|
const nlohmann::ordered_json* c = &converted;
|
|
for (std::size_t level = 0; level <= depth; ++level)
|
|
{
|
|
CAPTURE(level)
|
|
REQUIRE(c->start_pos() == o->start_pos());
|
|
REQUIRE(c->end_pos() == o->end_pos());
|
|
|
|
if (level < depth)
|
|
{
|
|
// the number beside the value nested next
|
|
const json& o_number = o->is_object() ? o->at("b") : o->at(0);
|
|
const nlohmann::ordered_json& c_number = c->is_object() ? c->at("b") : c->at(0);
|
|
REQUIRE(c_number.start_pos() == o_number.start_pos());
|
|
REQUIRE(c_number.end_pos() == o_number.end_pos());
|
|
|
|
o = o->is_object() ? &o->at("a") : &o->at(1);
|
|
c = c->is_object() ? &c->at("a") : &c->at(1);
|
|
}
|
|
}
|
|
};
|
|
|
|
check_conversion(1);
|
|
check_conversion(127);
|
|
check_conversion(128);
|
|
check_conversion(129);
|
|
check_conversion(300);
|
|
}
|
|
|
|
SECTION("JSON patch add to primitive parent (#4292)")
|
|
{
|
|
// the JSON Patch "add" target /foo/bar/baz has a string parent
|
|
// (/foo/bar); the position of that parent is reported in the message
|
|
const json doc = json::parse(R"({"foo":{"bar":"a string"}})");
|
|
const json patch = json::parse(R"([{"op":"add","path":"/foo/bar/baz","value":1}])");
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(doc.patch(patch),
|
|
"[json.exception.out_of_range.411] (/foo/bar) (bytes 14-24) cannot add value: the JSON Patch 'add' target's parent is of type string, but must be an object or array", json::out_of_range);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(doc.patch(patch),
|
|
"[json.exception.out_of_range.411] (bytes 14-24) cannot add value: the JSON Patch 'add' target's parent is of type string, but must be an object or array", json::out_of_range);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
TEST_CASE("values read from a binary format have no positions")
|
|
{
|
|
// only the JSON lexer knows where a value started and ended
|
|
const json source = {{"a", {1, "x", json::binary({1})}}, {"b", {{"c", true}}}, {"d", nullptr}, {"e", 1.5}};
|
|
const std::vector<std::uint8_t> cbor = json::to_cbor(source);
|
|
|
|
const auto check_no_positions = [](const json & j)
|
|
{
|
|
CHECK(j.start_pos() == std::string::npos);
|
|
CHECK(j.end_pos() == std::string::npos);
|
|
CHECK(j.at("a").start_pos() == std::string::npos);
|
|
CHECK(j.at("a").at(1).end_pos() == std::string::npos);
|
|
CHECK(j.at("b").at("c").start_pos() == std::string::npos);
|
|
};
|
|
|
|
SECTION("DOM parser")
|
|
{
|
|
const json j = json::from_cbor(cbor);
|
|
CHECK(j == source);
|
|
check_no_positions(j);
|
|
}
|
|
|
|
SECTION("DOM parser with a callback")
|
|
{
|
|
json j;
|
|
nlohmann::detail::json_sax_dom_callback_parser<json, decltype(nlohmann::detail::input_adapter(cbor))> sdp(j, [](int /*unused*/, json::parse_event_t /*unused*/, const json& /*unused*/) noexcept
|
|
{
|
|
return true;
|
|
});
|
|
CHECK(json::sax_parse(cbor, &sdp, json::input_format_t::cbor));
|
|
CHECK(j == source);
|
|
check_no_positions(j);
|
|
}
|
|
}
|