// __ _____ _____ _____ // __| | __| | | | 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 // cmake/test.cmake selects the C++ standard versions with which to build a // unit test based on the presence of JSON_HAS_CPP_ macros. // When using macros that are only defined for particular versions of the standard // (e.g., JSON_HAS_FILESYSTEM for C++17 and up), please mention the corresponding // version macro in a comment close by, like this: // JSON_HAS_CPP_ (do not remove; see note at top of file) #include "doctest_compatibility.h" // for some reason including this after the json header leads to linker errors with VS 2017... #include #define JSON_TESTS_PRIVATE #include using json = nlohmann::json; using ordered_json = nlohmann::ordered_json; #ifdef JSON_TEST_NO_GLOBAL_UDLS using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) #endif #include #include #include #include #include "test_utils.hpp" #ifdef JSON_HAS_CPP_17 #include #include #endif #ifdef JSON_HAS_CPP_17 #if __has_include() #include #elif __has_include() #include #endif ///////////////////////////////////////////////////////////////////// // for #4804 ///////////////////////////////////////////////////////////////////// using json_4804 = nlohmann::basic_json, // BinaryType void // CustomBaseClass >; #endif #ifdef JSON_HAS_CPP_20 #if __has_include() #include #endif #endif ///////////////////////////////////////////////////////////////////// // for #4825 - explicitly instantiating basic_json must compile; this // forces instantiation of binary_writer::write_bjdata_ndarray, whose // static_cast was ambiguous under explicit instantiation on // C++17. Merely compiling this translation unit is the regression test. ///////////////////////////////////////////////////////////////////// template class nlohmann::basic_json<>; ///////////////////////////////////////////////////////////////////// // for #4440 ///////////////////////////////////////////////////////////////////// #if JSON_HAS_RANGES == 1 #include #endif // NLOHMANN_JSON_SERIALIZE_ENUM uses a static std::pair DOCTEST_CLANG_SUPPRESS_WARNING_PUSH DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors") ///////////////////////////////////////////////////////////////////// // for #1021 ///////////////////////////////////////////////////////////////////// using float_json = nlohmann::basic_json; ///////////////////////////////////////////////////////////////////// // for #1647 ///////////////////////////////////////////////////////////////////// namespace { struct NonDefaultFromJsonStruct {}; inline bool operator==(NonDefaultFromJsonStruct const& /*unused*/, NonDefaultFromJsonStruct const& /*unused*/) { return true; } enum class for_1647 { one, two }; // NOLINTNEXTLINE(misc-const-correctness,cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays): this is a false positive NLOHMANN_JSON_SERIALIZE_ENUM(for_1647, { {for_1647::one, "one"}, {for_1647::two, "two"}, }) } // namespace ///////////////////////////////////////////////////////////////////// // for #1299 ///////////////////////////////////////////////////////////////////// struct Data { Data() = default; Data(std::string a_, std::string b_) : a(std::move(a_)) , b(std::move(b_)) {} std::string a{}; // NOLINT(readability-redundant-member-init) std::string b{}; // NOLINT(readability-redundant-member-init) }; void from_json(const json& j, Data& data); // NOLINT(misc-use-internal-linkage) void from_json(const json& j, Data& data) { j["a"].get_to(data.a); j["b"].get_to(data.b); } bool operator==(Data const& lhs, Data const& rhs); // NOLINT(misc-use-internal-linkage) bool operator==(Data const& lhs, Data const& rhs) { return lhs.a == rhs.a && lhs.b == rhs.b; } //bool operator!=(Data const& lhs, Data const& rhs) //{ // return !(lhs == rhs); //} namespace nlohmann { template<> struct adl_serializer { static NonDefaultFromJsonStruct from_json(json const& /*unused*/) noexcept { return {}; } }; } // namespace nlohmann ///////////////////////////////////////////////////////////////////// // for #1805 ///////////////////////////////////////////////////////////////////// struct NotSerializableData { int mydata; float myfloat; }; ///////////////////////////////////////////////////////////////////// // for #2574 ///////////////////////////////////////////////////////////////////// struct NonDefaultConstructible { explicit NonDefaultConstructible(int a) : x(a) {} int x; }; namespace nlohmann { template<> struct adl_serializer { static NonDefaultConstructible from_json(json const& j) { return NonDefaultConstructible(j.get()); } }; } // namespace nlohmann ///////////////////////////////////////////////////////////////////// // for #2824 ///////////////////////////////////////////////////////////////////// class sax_no_exception : public nlohmann::detail::json_sax_dom_parser { public: explicit sax_no_exception(json& j) : nlohmann::detail::json_sax_dom_parser(j, false) {} static bool parse_error(std::size_t /*position*/, const std::string& /*last_token*/, const json::exception& ex) { error_string = new std::string(ex.what()); // NOLINT(cppcoreguidelines-owning-memory) return false; } static std::string* error_string; }; std::string* sax_no_exception::error_string = nullptr; ///////////////////////////////////////////////////////////////////// // for #2982 ///////////////////////////////////////////////////////////////////// template class my_allocator : public std::allocator { public: using std::allocator::allocator; my_allocator() = default; template my_allocator(const my_allocator& /*unused*/) { } template struct rebind { using other = my_allocator; }; }; TEST_CASE("regression tests 2") { SECTION("issue #1001 - Fix memory leak during parser callback") { const auto* geojsonExample = R"( { "type": "FeatureCollection", "features": [ { "type": "Feature", "geometry": {"type": "Point", "coordinates": [102.0, 0.5]}, "properties": {"prop0": "value0"} }, { "type": "Feature", "geometry": { "type": "LineString", "coordinates": [ [102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0] ] }, "properties": { "prop0": "value0", "prop1": 0.0 } }, { "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [ [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ] ] }, "properties": { "prop0": "value0", "prop1": {"this": "that"} } } ] })"; const json::parser_callback_t cb = [&](int /*level*/, json::parse_event_t event, json & parsed) noexcept { // skip uninteresting events if (event == json::parse_event_t::value && !parsed.is_primitive()) { return false; } switch (event) { case json::parse_event_t::key: { return true; } case json::parse_event_t::value: { return false; } case json::parse_event_t::object_start: { return true; } case json::parse_event_t::object_end: { return false; } case json::parse_event_t::array_start: { return true; } case json::parse_event_t::array_end: { return false; } default: { return true; } } }; auto j = json::parse(geojsonExample, cb, true); CHECK(j == json()); } SECTION("issue #1021 - to/from_msgpack only works with standard typization") { float_json j = 1000.0; CHECK(float_json::from_cbor(float_json::to_cbor(j)) == j); CHECK(float_json::from_msgpack(float_json::to_msgpack(j)) == j); CHECK(float_json::from_ubjson(float_json::to_ubjson(j)) == j); CHECK(float_json::from_bon8(float_json::to_bon8(j)) == j); float_json j2 = {1000.0, 2000.0, 3000.0}; CHECK(float_json::from_ubjson(float_json::to_ubjson(j2, true, true)) == j2); } SECTION("issue #1045 - Using STL algorithms with JSON containers with expected results?") { json diffs = nlohmann::json::array(); json m1{{"key1", 42}}; json m2{{"key2", 42}}; auto p1 = m1.items(); auto p2 = m2.items(); using it_type = decltype(p1.begin()); std::set_difference( p1.begin(), p1.end(), p2.begin(), p2.end(), std::inserter(diffs, diffs.end()), [&](const it_type & e1, const it_type & e2) -> bool { using comper_pair = std::pair; // Trying to avoid unneeded copy return comper_pair(e1.key(), e1.value()) < comper_pair(e2.key(), e2.value()); // Using pair comper }); CHECK(diffs.size() == 1); // Note the change here, was 2 } #ifdef JSON_HAS_CPP_17 SECTION("issue #1292 - Serializing std::variant causes stack overflow") { static_assert(!std::is_constructible>::value, "unexpected value"); } #endif SECTION("issue #1299 - compile error in from_json converting to container " "with std::pair") { const json j = { {"1", {{"a", "testa_1"}, {"b", "testb_1"}}}, {"2", {{"a", "testa_2"}, {"b", "testb_2"}}}, {"3", {{"a", "testa_3"}, {"b", "testb_3"}}}, }; const std::map expected { {"1", {"testa_1", "testb_1"}}, {"2", {"testa_2", "testb_2"}}, {"3", {"testa_3", "testb_3"}}, }; const auto data = j.get(); CHECK(expected == data); } SECTION("issue #1445 - buffer overflow in dumping invalid utf-8 strings") { SECTION("a bunch of -1, ensure_ascii=true") { const auto length = 300; json dump_test; dump_test["1"] = std::string(length, static_cast(-1)); std::string expected = R"({"1":")"; for (int i = 0; i < length; ++i) { expected += "\\ufffd"; } expected += "\"}"; auto s = dump_test.dump(-1, ' ', true, nlohmann::json::error_handler_t::replace); CHECK(s == expected); } SECTION("a bunch of -2, ensure_ascii=false") { const auto length = 500; json dump_test; dump_test["1"] = std::string(length, static_cast(-2)); std::string expected = R"({"1":")"; for (int i = 0; i < length; ++i) { expected += "\xEF\xBF\xBD"; } expected += "\"}"; auto s = dump_test.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace); CHECK(s == expected); } SECTION("test case in issue #1445") { nlohmann::json dump_test; const std::array data = { {109, 108, 103, 125, -122, -53, 115, 18, 3, 0, 102, 19, 1, 15, -110, 13, -3, -1, -81, 32, 2, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -80, 2, 0, 0, 96, -118, 46, -116, 46, 109, -84, -87, 108, 14, 109, -24, -83, 13, -18, -51, -83, -52, -115, 14, 6, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 3, 0, 0, 0, 35, -74, -73, 55, 57, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 33, 0, 0, 0, -96, -54, -28, -26} }; std::string s; for (const int i : data) { s += static_cast(i); } dump_test["1"] = s; // dump() is nodiscard; this only checks that dumping does not throw/crash utils::ignore_return_value(dump_test.dump(-1, ' ', true, nlohmann::json::error_handler_t::replace)); } } SECTION("issue #1447 - Integer Overflow (OSS-Fuzz 12506)") { const json j = json::parse("[-9223372036854775808]"); CHECK(j.dump() == "[-9223372036854775808]"); } SECTION("issue #1708 - minimum value of int64_t can be outputted") { constexpr auto smallest = (std::numeric_limits::min)(); const json j = smallest; CHECK(j.dump() == std::to_string(smallest)); } SECTION("issue #1727 - Contains with non-const lvalue json_pointer picks the wrong overload") { const json j = {{"root", {{"settings", {{"logging", true}}}}}}; auto jptr1 = "/root/settings/logging"_json_pointer; auto jptr2 = json::json_pointer{"/root/settings/logging"}; CHECK(j.contains(jptr1)); CHECK(j.contains(jptr2)); } SECTION("issue #1647 - compile error when deserializing enum if both non-default from_json and non-member operator== exists for other type") { // does not compile on ICPC when targeting C++20 #if !(defined(__INTEL_COMPILER) && __cplusplus >= 202000) { const json j; const NonDefaultFromJsonStruct x(j); NonDefaultFromJsonStruct y; CHECK(x == y); } #endif auto val = nlohmann::json("one").get(); CHECK(val == for_1647::one); const json j = val; } SECTION("issue #1715 - json::from_cbor does not respect allow_exceptions = false when input is string literal") { SECTION("string literal") { const json cbor = json::from_cbor("B", true, false); CHECK(cbor.is_discarded()); } SECTION("string array") { const std::array input = {{'B', 0x00}}; const json cbor = json::from_cbor(input, true, false); CHECK(cbor.is_discarded()); } SECTION("std::string") { const json cbor = json::from_cbor(std::string("B"), true, false); CHECK(cbor.is_discarded()); } } SECTION("issue #1805 - A pair is json constructible only if T1 and T2 are json constructible") { static_assert(!std::is_constructible>::value, "unexpected result"); static_assert(!std::is_constructible>::value, "unexpected result"); static_assert(std::is_constructible>::value, "unexpected result"); } SECTION("issue #1825 - A tuple is json constructible only if all T in Args are json constructible") { static_assert(!std::is_constructible>::value, "unexpected result"); static_assert(!std::is_constructible>::value, "unexpected result"); static_assert(std::is_constructible>::value, "unexpected result"); } SECTION("issue #1983 - JSON patch diff for op=add formation is not as per standard (RFC 6902)") { const auto source = R"({ "foo": [ "1", "2" ] })"_json; const auto target = R"({"foo": [ "1", "2", "3" ]})"_json; const auto result = json::diff(source, target); CHECK(result.dump() == R"([{"op":"add","path":"/foo/-","value":"3"}])"); } SECTION("issue #2067 - cannot serialize binary data to text JSON") { const std::array data = {{0x81, 0xA4, 0x64, 0x61, 0x74, 0x61, 0xC4, 0x0F, 0x33, 0x30, 0x30, 0x32, 0x33, 0x34, 0x30, 0x31, 0x30, 0x37, 0x30, 0x35, 0x30, 0x31, 0x30}}; const json j = json::from_msgpack(data.data(), data.size()); // dump() is nodiscard; this only checks that dumping does not throw CHECK_NOTHROW( utils::ignore_return_value( j.dump(4, // Indent ' ', // Indent char false, // Ensure ascii json::error_handler_t::strict // Error ))); } SECTION("PR #2181 - regression bug with lvalue") { // see https://github.com/nlohmann/json/pull/2181#issuecomment-653326060 const json j{{"x", "test"}}; const std::string defval = "default value"; auto val = j.value("x", defval); // NOLINT(bugprone-unused-local-non-trivial-variable) auto val2 = j.value("y", defval); // NOLINT(bugprone-unused-local-non-trivial-variable) } SECTION("issue #2293 - eof doesn't cause parsing to stop") { const std::vector data = { 0x7B, 0x6F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x20, 0x4F, 0x42 }; const json result = json::from_cbor(data, true, false); CHECK(result.is_discarded()); } SECTION("issue #2315 - json.update and vectordoes not work with ordered_json") { nlohmann::ordered_json jsonAnimals = {{"animal", "dog"}}; const nlohmann::ordered_json jsonCat = {{"animal", "cat"}}; jsonAnimals.update(jsonCat); CHECK(jsonAnimals["animal"] == "cat"); auto jsonAnimals_parsed = nlohmann::ordered_json::parse(jsonAnimals.dump()); CHECK(jsonAnimals == jsonAnimals_parsed); const std::vector> intData = {std::make_pair("aaaa", 11), std::make_pair("bbb", 222) }; nlohmann::ordered_json jsonObj; for (const auto& data : intData) { jsonObj[data.first] = data.second; } CHECK(jsonObj["aaaa"] == 11); CHECK(jsonObj["bbb"] == 222); } SECTION("issue #2330 - ignore_comment=true fails on multiple consecutive lines starting with comments") { const std::string ss = "//\n//\n{\n}\n"; const json j = json::parse(ss, nullptr, true, true); CHECK(j.dump() == "{}"); } #ifdef JSON_HAS_CPP_20 #ifndef _LIBCPP_VERSION // see https://github.com/nlohmann/json/issues/4490 // classic Intel ICC reports as includable but cannot actually compile // std::span/std::as_bytes usage below #if __has_include() && !defined(__ICC) && !defined(__INTEL_COMPILER) SECTION("issue #2546 - parsing containers of std::byte") { const char DATA[] = R"("Hello, world!")"; // NOLINT(misc-const-correctness,cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) // exclude the trailing '\0' that string-literal initialization adds to // DATA: std::span(DATA) would span the full array extent (including // that NUL), which is only silently accepted as end-of-input by default // and would fail under JSON_STRICT_NUL_HANDLING const auto s = std::as_bytes(std::span(DATA, sizeof(DATA) - 1)); const json j = json::parse(s); CHECK(j.dump() == "\"Hello, world!\""); } #endif #endif #endif SECTION("issue #2574 - Deserialization to std::array, std::pair, and std::tuple with non-default constructable types fails") { SECTION("std::array") { { const json j = {7, 4}; auto arr = j.get>(); CHECK(arr[0].x == 7); CHECK(arr[1].x == 4); } { const json j = 7; CHECK_THROWS_AS((j.get>()), json::type_error); } } SECTION("std::pair") { { const json j = {3, 8}; auto p = j.get>(); CHECK(p.first.x == 3); CHECK(p.second.x == 8); } { const json j = {4, 1}; auto p = j.get>(); CHECK(p.first == 4); CHECK(p.second.x == 1); } { const json j = {6, 7}; auto p = j.get>(); CHECK(p.first.x == 6); CHECK(p.second == 7); } { const json j = 7; CHECK_THROWS_AS((j.get>()), json::type_error); } } SECTION("std::tuple") { { const json j = {9}; auto t = j.get>(); CHECK(std::get<0>(t).x == 9); } { const json j = {9, 8, 7}; auto t = j.get>(); CHECK(std::get<0>(t).x == 9); CHECK(std::get<1>(t) == 8); CHECK(std::get<2>(t).x == 7); } { const json j = 7; CHECK_THROWS_AS((j.get>()), json::type_error); } } } SECTION("issue #4530 - Serialization of empty tuple") { const auto source_tuple = std::tuple<>(); const nlohmann::json j = source_tuple; CHECK(j.get() == source_tuple); CHECK("[]" == j.dump()); } SECTION("issue #2865 - ASAN detects memory leaks") { // the code below is expected to not leak memory { nlohmann::json o; const std::string s = "bar"; nlohmann::to_json(o["foo"], s); nlohmann::json p = o; // call to_json with a non-null JSON value nlohmann::to_json(p["foo"], s); } { nlohmann::json o; const std::string s = "bar"; nlohmann::to_json(o["foo"], s); // call to_json with a non-null JSON value nlohmann::to_json(o["foo"], s); } } SECTION("issue #2824 - encoding of json::exception::what()") { json j; sax_no_exception sax(j); CHECK(!json::sax_parse("xyz", &sax)); CHECK(*sax_no_exception::error_string == "[json.exception.parse_error.101] parse error at line 1, column 1: syntax error while parsing value - invalid literal; last read: 'x'"); delete sax_no_exception::error_string; // NOLINT(cppcoreguidelines-owning-memory) } SECTION("issue #2825 - Properly constrain the basic_json conversion operator") { static_assert(std::is_copy_assignable::value, "ordered_json must be copy assignable"); } SECTION("issue #2958 - Inserting in unordered json using a pointer retains the leading slash") { const std::string p = "/root"; json test1; test1[json::json_pointer(p)] = json::object(); CHECK(test1.dump() == "{\"root\":{}}"); ordered_json test2; test2[ordered_json::json_pointer(p)] = json::object(); CHECK(test2.dump() == "{\"root\":{}}"); // json::json_pointer and ordered_json::json_pointer are the same type; behave as above ordered_json test3; test3[json::json_pointer(p)] = json::object(); CHECK(std::is_same::value); CHECK(test3.dump() == "{\"root\":{}}"); } SECTION("issue #2982 - to_{binary format} does not provide a mechanism for specifying a custom allocator for the returned type") { std::vector> my_vector; const json j = {1, 2, 3, 4}; json::to_cbor(j, my_vector); json k = json::from_cbor(my_vector); CHECK(j == k); } } TEST_CASE("regression test - parser callback must not lose a duplicate key's prior value") { // a callback that rejects only the scalar value 2 const json::parser_callback_t drop_value_2 = [](int /*depth*/, json::parse_event_t ev, json & v) noexcept { return !(ev == json::parse_event_t::value && v == 2); }; SECTION("duplicate key, second (scalar) value rejected - prior value is restored") { const json j = json::parse(R"({"a":1,"a":2})", drop_value_2); CHECK(j.dump() == "{\"a\":1}"); } SECTION("duplicate key, second value is an object rejected at object_end - prior value is restored") { const json j = json::parse(R"({"a":1,"a":{"x":2}})", [](int depth, json::parse_event_t ev, json& /*parsed*/) noexcept { return !(ev == json::parse_event_t::object_end && depth == 1); }); CHECK(j.dump() == "{\"a\":1}"); } SECTION("duplicate key, second value is an array rejected at array_end - prior value is restored") { const json j = json::parse(R"({"a":1,"a":[9,9]})", [](int depth, json::parse_event_t ev, json& /*parsed*/) noexcept { return !(ev == json::parse_event_t::array_end && depth == 1); }); CHECK(j.dump() == "{\"a\":1}"); } SECTION("duplicate key, second value accepted (scalar) - last value wins") { const json j = json::parse(R"({"a":1,"a":2})", [](int, json::parse_event_t, json&) noexcept { return true; }); CHECK(j.dump() == "{\"a\":2}"); } SECTION("duplicate key, second value accepted (object) - last value wins") { const json j = json::parse(R"({"a":1,"a":{"x":2}})", [](int, json::parse_event_t, json&) noexcept { return true; }); CHECK(j.dump() == "{\"a\":{\"x\":2}}"); } SECTION("brand new (non-duplicate) key, value rejected - member is fully absent") { const json j = json::parse(R"({"a":1,"b":2})", drop_value_2); CHECK(j.dump() == "{\"a\":1}"); } SECTION("duplicate key nested two levels deep") { const json j = json::parse(R"({"outer":{"a":1,"a":2}})", drop_value_2); CHECK(j.dump() == "{\"outer\":{\"a\":1}}"); } SECTION("three occurrences of the same key - middle rejected, last accepted") { const json j = json::parse(R"({"k":1,"k":2,"k":3})", drop_value_2); CHECK(j.dump() == "{\"k\":3}"); } } TEST_CASE("regression test - excessive binary container size honors allow_exceptions=false") { // CBOR array with declared length 2^63 const std::vector cbor = {0x9b, 0x80, 0, 0, 0, 0, 0, 0, 0}; // CBOR map with declared length 2^63 const std::vector cbor_m = {0xbb, 0x80, 0, 0, 0, 0, 0, 0, 0}; // UBJSON array with declared length 2^63-1 const std::vector ubj = {'[', '#', 'L', 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; // BJData array with declared length 2^63-1 (little endian) const std::vector bjd = {'[', '#', 'L', 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f}; // allow_exceptions=false must report failure instead of throwing/aborting CHECK(json::from_cbor(cbor, true, false).is_discarded()); CHECK(json::from_cbor(cbor_m, true, false).is_discarded()); CHECK(json::from_ubjson(ubj, true, false).is_discarded()); CHECK(json::from_bjdata(bjd, true, false).is_discarded()); // allow_exceptions=true (the default) must still throw exactly as before. // The exact message text is not checked here: on platforms where // std::size_t is 32-bit, the CBOR reader's own length-narrowing check // (get_cbor_container_size(), unrelated to this fix) intercepts a // declared length of 2^63 before it ever reaches the check this test // targets, with different (but equally valid, and already correct) // wording -- see unit-cbor.cpp for coverage of that message. json _; CHECK_THROWS_AS(_ = json::from_cbor(cbor), json::out_of_range); // regression guard: a genuinely truncated CBOR input must remain discarded CHECK(json::from_cbor(std::vector {0x9b, 0, 0, 0, 0, 0, 0, 0, 0x02}, true, false).is_discarded()); } namespace { /// builds a value from SAX events, asks the parser to recover from its first /// 100 errors, and checks that the events are balanced (see #3989) class RecoveringParser : public nlohmann::detail::json_sax_dom_parser { using base = nlohmann::detail::json_sax_dom_parser; public: explicit RecoveringParser(json& j) : base(j, false) {} bool null() { value(); return base::null(); } bool boolean(bool val) { value(); return base::boolean(val); } bool number_integer(json::number_integer_t val) { value(); return base::number_integer(val); } bool number_unsigned(json::number_unsigned_t val) { value(); return base::number_unsigned(val); } bool number_float(json::number_float_t val, const std::string& s) { value(); return base::number_float(val, s); } bool string(std::string& val) { value(); return base::string(val); } bool binary(json::binary_t& val) { value(); return base::binary(val); } bool start_object(std::size_t elements) { value(); stack.push_back('o'); return base::start_object(elements); } bool key(std::string& val) { if (stack.empty() || stack.back() != 'o') { well_formed = false; return false; } stack.back() = 'v'; return base::key(val); } bool end_object() { if (stack.empty() || stack.back() != 'o') { well_formed = false; return false; } stack.pop_back(); return base::end_object(); } bool start_array(std::size_t elements) { value(); stack.push_back('a'); return base::start_array(elements); } bool end_array() { if (stack.empty() || stack.back() != 'a') { well_formed = false; return false; } stack.pop_back(); return base::end_array(); } bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, const json::exception& /*unused*/) { // a limit, so that a reader that does not stop fails the test // instead of making it hang return ++errors < 100; } /// whether the events were balanced and every key was followed by a value bool balanced() const { return well_formed && stack.empty(); } std::size_t errors = 0; std::vector stack {}; // NOLINT(readability-redundant-member-init) bool well_formed = true; private: void value() { if (!stack.empty()) { if (stack.back() == 'v') { stack.back() = 'o'; } else if (stack.back() == 'o') { well_formed = false; } } } }; struct BinaryParseResult { json value; std::size_t errors; bool ok; bool balanced; }; BinaryParseResult parse_binary_recovering(const std::vector& input, const json::input_format_t format) { json j; RecoveringParser sax(j); const bool ok = json::sax_parse(input, &sax, format); return {j, sax.errors, ok, sax.balanced()}; } } // namespace TEST_CASE("regression test - #3989 SAX parse_error() returning true") { SECTION("binary formats stop after an error and complete what was read") { const json j = {{"a", {1, -2, {{"b", "c"}}, json::array()}}, {"d", {{"e", nullptr}, {"f", true}}}, {"g", 1.5}, {"h", json::binary({1, 2, 3})}}; const std::vector>> encodings = { {json::input_format_t::cbor, json::to_cbor(j)}, {json::input_format_t::msgpack, json::to_msgpack(j)}, {json::input_format_t::ubjson, json::to_ubjson(j)}, {json::input_format_t::ubjson, json::to_ubjson(j, true, true)}, {json::input_format_t::bjdata, json::to_bjdata(j)}, {json::input_format_t::bjdata, json::to_bjdata(j, true, true)}, {json::input_format_t::bson, json::to_bson(j)}, {json::input_format_t::bon8, json::to_bon8(j)}, }; for (const auto& encoding : encodings) { const auto format = encoding.first; const auto& bytes = encoding.second; CAPTURE(format); // every prefix is truncated input for (std::size_t length = 0; length < bytes.size(); ++length) { CAPTURE(length); const auto result = parse_binary_recovering(std::vector(bytes.begin(), bytes.begin() + static_cast(length)), format); CHECK(!result.ok); CHECK(result.errors == 1); CHECK(result.balanced); } // the complete input is read as usual (binary values do not // round-trip through every format, so compare with a plain parse) json expected; nlohmann::detail::json_sax_dom_parser dom(expected); CHECK(json::sax_parse(bytes, &dom, format)); const auto complete = parse_binary_recovering(bytes, format); CHECK(complete.ok); CHECK(complete.errors == 0); CHECK(complete.value == expected); // a byte after the value auto trailing_bytes = bytes; trailing_bytes.push_back(0x01); const auto trailing = parse_binary_recovering(trailing_bytes, format); CHECK(!trailing.ok); CHECK(trailing.errors == 1); CHECK(trailing.value == expected); } } SECTION("containers without an end") { // these made the readers loop, or read on, after the error const auto cbor_array = parse_binary_recovering({0x9F}, json::input_format_t::cbor); CHECK(cbor_array.errors == 1); CHECK(cbor_array.value == json::array()); const auto cbor_map = parse_binary_recovering({0xBF, 0x61, 'a'}, json::input_format_t::cbor); CHECK(cbor_map.errors == 1); CHECK(cbor_map.value == json({{"a", nullptr}})); const auto msgpack_array = parse_binary_recovering({0xDD, 0xFF, 0xFF, 0xFF, 0xFF}, json::input_format_t::msgpack); CHECK(msgpack_array.errors == 1); CHECK(msgpack_array.value == json::array()); const auto msgpack_map = parse_binary_recovering({0x81, 0xA1, 'a', 0x92, 0x01}, json::input_format_t::msgpack); CHECK(msgpack_map.errors == 1); CHECK(msgpack_map.value == json({{"a", {1}}})); } SECTION("BJData ndarray") { // a 2x3 int8 array with two of its six elements; the annotated array // format opens an object and two arrays of its own const auto result = parse_binary_recovering({'[', '$', 'i', '#', '[', '$', 'i', '#', 'i', 2, 2, 3, 1, 2}, json::input_format_t::bjdata); CHECK(result.errors == 1); CHECK(result.balanced); CHECK(result.value == json({{"_ArrayType_", "int8"}, {"_ArraySize_", {2, 3}}, {"_ArrayData_", {1, 2}}})); } SECTION("JSON text") { // the parser stopped, but reported success json j; RecoveringParser sax(j); CHECK(!json::sax_parse("[1,2,3,]", &sax)); CHECK(sax.errors == 1); CHECK(j == json({1, 2, 3})); } SECTION("the SAX parsers of the library stop") { json _; CHECK(json::from_cbor(std::vector {0x9F}, true, false).is_discarded()); CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector {0x9F}), "[json.exception.parse_error.110] parse error at byte 2: syntax error while parsing CBOR value: unexpected end of input", json::parse_error&); CHECK(json::parse("[1,2,3,]", nullptr, false).is_discarded()); CHECK(!json::accept("[1,2,3,]")); } } DOCTEST_CLANG_SUPPRESS_WARNING_POP