From 959c362bef3b21cb827ce830655a24cf6183e62a Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 25 Sep 2026 21:11:50 +0200 Subject: [PATCH] Add tests for uncovered code paths Cover code the test suite did not reach, found from the Coveralls report of develop and a local coverage run of HEAD: - dump() of every kind of value below the bound of the recursive descent (pretty-printed objects, binary values, discarded values, scalars), and flushes of the escape and write buffers mid-string and mid-binary - the iterative comparison: objects with different keys, containers that are a prefix of each other, and elements that cannot be ordered, each both at the top level and below the nesting bound - SAX handlers that stop at any event, including the end of a nested container, in the BSON, CBOR, MessagePack, UBJSON and BJData readers - from_bson/cbor/msgpack/ubjson/bjdata returning a discarded value through the iterator and pointer overloads - JSON Patch, diff, merge_patch and update(..., true) on ordered_json - smaller gaps: get_allocator(), to_ubjson/to_bjdata into a string, value() with an unresolvable JSON pointer, integer/float comparison below the integer range and with negative fractions, conversion to a custom binary type, std::formatter::parse on a spec without '}', unescape() of a lone '~', and the callback parser's start_array() Signed-off-by: Niels Lohmann --- tests/src/unit-allocator.cpp | 6 ++ tests/src/unit-bjdata.cpp | 102 ++++++++++++++++++ tests/src/unit-bson.cpp | 38 +++++++ tests/src/unit-cbor.cpp | 39 +++++++ tests/src/unit-comparison.cpp | 90 ++++++++++++++++ tests/src/unit-custom-binary-type.cpp | 10 ++ tests/src/unit-element_access2.cpp | 10 ++ tests/src/unit-json_patch.cpp | 69 ++++++++++++ tests/src/unit-json_pointer.cpp | 13 +++ tests/src/unit-merge_patch.cpp | 29 +++++ tests/src/unit-msgpack.cpp | 38 +++++++ tests/src/unit-serialization.cpp | 150 ++++++++++++++++++++++++++ tests/src/unit-std-format.cpp | 23 ++++ tests/src/unit-ubjson.cpp | 63 +++++++++++ 14 files changed, 680 insertions(+) diff --git a/tests/src/unit-allocator.cpp b/tests/src/unit-allocator.cpp index 5c7b4230f..21e5d48cb 100644 --- a/tests/src/unit-allocator.cpp +++ b/tests/src/unit-allocator.cpp @@ -37,6 +37,12 @@ struct bad_allocator : std::allocator }; } // namespace +TEST_CASE("get_allocator") +{ + const auto alloc = nlohmann::json::get_allocator(); + CHECK(alloc == std::allocator()); +} + TEST_CASE("bad_alloc") { SECTION("bad_alloc") diff --git a/tests/src/unit-bjdata.cpp b/tests/src/unit-bjdata.cpp index ebbbbfaf6..9a05a7a79 100644 --- a/tests/src/unit-bjdata.cpp +++ b/tests/src/unit-bjdata.cpp @@ -3763,6 +3763,49 @@ TEST_CASE("BJData") } } +TEST_CASE("BJData input that cannot be read is discarded by every overload") +{ + std::vector input = json::to_bjdata(json({{"a", {1, 2}}})); + input.pop_back(); + + json _; + CHECK_THROWS_AS(_ = json::from_bjdata(input.begin(), input.end()), json::parse_error&); + CHECK(json::from_bjdata(input, true, false).is_discarded()); + CHECK(json::from_bjdata(input.begin(), input.end(), true, false).is_discarded()); +} + +TEST_CASE("BJData SAX parsing stops at every event") +{ + // Containers are opened and closed by the loop that reads them; a SAX + // handler that rejects any event - including the end of a nested + // container - must stop the parse right there. + const auto count_events = [](const std::vector& input) + { + int events = 0; + while (true) + { + SaxCountdown scp(events); + if (json::sax_parse(input, &scp, json::input_format_t::bjdata)) + { + return events; + } + ++events; + REQUIRE(events < 1000); + } + }; + + // 20 events: every container kind closes inside another one + const json j = json::parse(R"({"a": [1, {"b": []}], "c": {"d": [[2]]}})"); + CHECK(count_events(json::to_bjdata(j)) == 20); + CHECK(count_events(json::to_bjdata(j, true)) == 20); + CHECK(count_events(json::to_bjdata(j, true, true)) == 20); + + // an ND-array is announced as an annotated object: start_object, then + // _ArrayType_, _ArraySize_ and _ArrayData_ with its elements + const json ndarray = json::parse(R"({"_ArrayType_": "uint8", "_ArraySize_": [2, 2], "_ArrayData_": [1, 2, 3, 4]})"); + CHECK(count_events(json::to_bjdata(ndarray, true, true)) == 16); +} + TEST_CASE("issue #5405 - array reserve for definite-length BJData arrays") { #if !defined(JSON_NOEXCEPTION) @@ -4247,6 +4290,54 @@ TEST_CASE("all BJData first bytes") } #endif +TEST_CASE("BJData and UBJSON can be written to a string") +{ + const std::vector values = + { + {{"a", {1, 2.5, "x", nullptr}}, {"b", json::binary({1, 2})}}, + // an annotated ND-array, and objects that only look like one + json::parse(R"({"_ArrayType_": "uint8", "_ArraySize_": [2, 2], "_ArrayData_": [1, 2, 3, 4]})"), + json::parse(R"({"_ArrayType_": 1, "_ArraySize_": [2, 2], "_ArrayData_": [1, 2, 3, 4]})"), + json::parse(R"({"_ArrayType_": "uint8", "_ArraySize_": 4, "_ArrayData_": [1, 2, 3, 4]})"), + json::parse(R"({"_ArrayType_": "uint8", "_ArraySize_": [2, -2], "_ArrayData_": [1, 2, 3, 4]})"), + json::parse(R"({"_ArrayType_": "uint8", "_ArraySize_": [2, 2], "_ArrayData_": [1, 2, 3]})"), + json::parse(R"({"_ArrayType_": "uint8", "_ArraySize_": [2, 2], "_ArrayData_": 1})"), + }; + + for (const auto& j : values) + { + CAPTURE(j.dump()); + for (const bool use_size : + { + false, true + }) + { + for (const bool use_type : + { + false, true + }) + { + if (use_type && !use_size) + { + continue; + } + CAPTURE(use_size); + CAPTURE(use_type); + + const auto bjdata = json::to_bjdata(j, use_size, use_type); + std::string bjdata_string; + json::to_bjdata(j, bjdata_string, use_size, use_type); + CHECK(bjdata_string == std::string(bjdata.begin(), bjdata.end())); + + const auto ubjson = json::to_ubjson(j, use_size, use_type); + std::string ubjson_string; + json::to_ubjson(j, ubjson_string, use_size, use_type); + CHECK(ubjson_string == std::string(ubjson.begin(), ubjson.end())); + } + } + } +} + TEST_CASE("BJData use_type requires use_size") { SECTION("non-empty object throws other_error.502") @@ -4265,6 +4356,17 @@ TEST_CASE("BJData use_type requires use_size") json::other_error&); } + SECTION("non-empty binary value throws other_error.502") + { + const json j = json::binary({1, 2, 3}); + CHECK_THROWS_WITH_AS(json::to_bjdata(j, false, true), + "[json.exception.other_error.502] use_type requires use_size = true", + json::other_error&); + CHECK_THROWS_WITH_AS(json::to_ubjson(j, false, true), + "[json.exception.other_error.502] use_type requires use_size = true", + json::other_error&); + } + SECTION("scalars do not throw with use_type=true, use_count=false") { CHECK_NOTHROW(json::to_bjdata(42, false, true)); diff --git a/tests/src/unit-bson.cpp b/tests/src/unit-bson.cpp index 669a4bfe1..688390931 100644 --- a/tests/src/unit-bson.cpp +++ b/tests/src/unit-bson.cpp @@ -1244,6 +1244,44 @@ TEST_CASE("BSON nesting does not consume the call stack") } } +TEST_CASE("BSON input that cannot be read is discarded by every overload") +{ + std::vector input = json::to_bson(json({{"a", {1, 2}}})); + input.pop_back(); + + json _; + CHECK_THROWS_AS(_ = json::from_bson(input.begin(), input.end()), json::parse_error&); + CHECK(json::from_bson(input, true, false).is_discarded()); + CHECK(json::from_bson(input.begin(), input.end(), true, false).is_discarded()); + CHECK(json::from_bson(input.data(), input.size(), true, false).is_discarded()); + CHECK(json::from_bson({input.data(), input.size()}, true, false).is_discarded()); +} + +TEST_CASE("BSON SAX parsing stops at every event") +{ + // Containers are opened and closed by the loop that reads them; a SAX + // handler that rejects any event - including the end of a nested + // container - must stop the parse right there. + const auto count_events = [](const std::vector& input) + { + int events = 0; + while (true) + { + SaxCountdown scp(events); + if (json::sax_parse(input, &scp, json::input_format_t::bson)) + { + return events; + } + ++events; + REQUIRE(events < 1000); + } + }; + + // 20 events: every container kind closes inside another one + const json j = json::parse(R"({"a": [1, {"b": []}], "c": {"d": [[2]]}})"); + CHECK(count_events(json::to_bson(j)) == 20); +} + TEST_CASE("BSON numerical data") { SECTION("number") diff --git a/tests/src/unit-cbor.cpp b/tests/src/unit-cbor.cpp index 4c9107517..b2bfe3304 100644 --- a/tests/src/unit-cbor.cpp +++ b/tests/src/unit-cbor.cpp @@ -2122,6 +2122,45 @@ TEST_CASE("CBOR nesting does not consume the call stack") } } +TEST_CASE("CBOR input that cannot be read is discarded by every overload") +{ + std::vector input = json::to_cbor(json({{"a", {1, 2}}})); + input.pop_back(); + + json _; + CHECK_THROWS_AS(_ = json::from_cbor(input.begin(), input.end()), json::parse_error&); + CHECK(json::from_cbor(input, true, false).is_discarded()); + CHECK(json::from_cbor(input.begin(), input.end(), true, false).is_discarded()); + CHECK(json::from_cbor(input.data(), input.size(), true, false).is_discarded()); + CHECK(json::from_cbor({input.data(), input.size()}, true, false).is_discarded()); +} + +TEST_CASE("CBOR SAX parsing stops at every event") +{ + // Containers are opened and closed by the loop that reads them; a SAX + // handler that rejects any event - including the end of a nested + // container - must stop the parse right there. + const auto count_events = [](const std::vector& input) + { + int events = 0; + while (true) + { + SaxCountdown scp(events); + if (json::sax_parse(input, &scp, json::input_format_t::cbor)) + { + return events; + } + ++events; + REQUIRE(events < 1000); + } + }; + + // 20 events: every container kind closes inside another one + const json j = json::parse(R"({"a": [1, {"b": []}], "c": {"d": [[2]]}})"); + CHECK(count_events(json::to_cbor(j)) == 20); + CHECK(count_events(std::vector({0xBF, 0x61, 'a', 0x9F, 0x01, 0xFF, 0xFF})) == 6); +} + TEST_CASE("CBOR indefinite-length strings do not recurse per chunk") { // Reading an indefinite-length string or byte array used to call itself diff --git a/tests/src/unit-comparison.cpp b/tests/src/unit-comparison.cpp index 68d20aca9..9a6606256 100644 --- a/tests/src/unit-comparison.cpp +++ b/tests/src/unit-comparison.cpp @@ -359,6 +359,15 @@ TEST_CASE("lexicographical comparison operators") CHECK(json(1) < json(1.5)); CHECK(json(1.5) < json(2)); CHECK(json(2) > json(1.5)); + CHECK(json(-1) > json(-1.5)); + CHECK(json(-1.5) < json(-1)); + CHECK(json(-2) < json(-1.5)); + + // a float below the range of the integer type + CHECK(json(0) > json(-1e30)); + CHECK(json(-1e30) < json(0)); + CHECK(json(0u) > json(-0.5)); + CHECK(json(-0.5) < json(0u)); // a NaN operand stays unordered against either integer kind CHECK_FALSE(json(1) == json(nan)); @@ -735,3 +744,84 @@ TEST_CASE("regression #3868 - heterogeneous comparisons compile under C++20 (P24 } } #endif + +TEST_CASE("containers are compared element by element") +{ + // Containers nested deeper than a bound are compared without the call + // stack, by code of their own; every relation is checked both at the top + // level and below that bound. + const auto deep = [](const json & j, const std::size_t depth) + { + json result = j; + for (std::size_t i = 0; i < depth; ++i) + { + result = json::array({std::move(result)}); + } + return result; + }; + + for (const std::size_t depth : std::vector {0, 200}) + { + CAPTURE(depth); + + // objects with different keys + { + const json a = deep({{"a", 1}}, depth); + const json b = deep({{"b", 1}}, depth); + CHECK_FALSE(a == b); + CHECK(a != b); + CHECK(a < b); + CHECK(b > a); + CHECK_FALSE(b < a); +#if JSON_HAS_THREE_WAY_COMPARISON + // JSON_HAS_CPP_20 (do not remove; see note at top of file) + CHECK((a <=> b) == std::partial_ordering::less); // *NOPAD* + CHECK((b <=> a) == std::partial_ordering::greater); // *NOPAD* + CHECK((a <=> a) == std::partial_ordering::equivalent); // *NOPAD* +#endif + } + + // a container that is a prefix of the other one + { + // the one that runs out of elements first is the smaller one + const json shorter = deep({1}, depth); + const json longer = deep({1, 2}, depth); + CHECK(shorter < longer); + CHECK(longer > shorter); + CHECK_FALSE(longer < shorter); + CHECK_FALSE(shorter == longer); + + const json smaller_object = deep({{"a", 1}}, depth); + const json larger_object = deep({{"a", 1}, {"b", 2}}, depth); + CHECK(smaller_object < larger_object); + CHECK(larger_object > smaller_object); + CHECK_FALSE(smaller_object == larger_object); +#if JSON_HAS_THREE_WAY_COMPARISON + // JSON_HAS_CPP_20 (do not remove; see note at top of file) + CHECK((shorter <=> longer) == std::partial_ordering::less); // *NOPAD* + CHECK((longer <=> shorter) == std::partial_ordering::greater); // *NOPAD* +#endif + } + + // elements that cannot be ordered + { + const double nan = std::numeric_limits::quiet_NaN(); + const json lhs = deep({nan, 1}, depth); + const json rhs = deep({nan, 2}, depth); + + CHECK_FALSE(lhs == lhs); + CHECK_FALSE(rhs < lhs); +#if JSON_HAS_THREE_WAY_COMPARISON + // JSON_HAS_CPP_20 (do not remove; see note at top of file) + // operator<=> stops there, as std::lexicographical_compare_three_way + // does, and operator< is derived from it + CHECK((lhs <=> rhs) == std::partial_ordering::unordered); // *NOPAD* + CHECK_FALSE(lhs < rhs); +#else + // operator< skips a pair of elements that cannot be ordered, as + // std::lexicographical_compare does, and the next pair decides + CHECK(lhs < rhs); +#endif + } + } +} diff --git a/tests/src/unit-custom-binary-type.cpp b/tests/src/unit-custom-binary-type.cpp index d357ec9a3..efedba3cd 100644 --- a/tests/src/unit-custom-binary-type.cpp +++ b/tests/src/unit-custom-binary-type.cpp @@ -49,6 +49,16 @@ TEST_CASE("binary type whose value type is not std::uint8_t") CHECK(char_binary_json::binary({}).dump() == R"({"bytes":[],"subtype":null})"); } + SECTION("a value is converted to the binary type if it is binary or an array") + { + const std::vector chars{'\0', '\x01', '\x7F'}; + CHECK(char_binary_json::binary(chars).get>() == chars); + CHECK(char_binary_json({0, 1, 127}).get>() == chars); + CHECK_THROWS_WITH_AS(char_binary_json(1).get>(), + "[json.exception.type_error.302] type must be binary or array, but is number", + char_binary_json::type_error&); + } + SECTION("the default binary type is unchanged") { CHECK(nlohmann::json::binary({0, 1, 255}, 42).dump() == R"({"bytes":[0,1,255],"subtype":42})"); diff --git a/tests/src/unit-element_access2.cpp b/tests/src/unit-element_access2.cpp index c7c1b824d..40e8216d5 100644 --- a/tests/src/unit-element_access2.cpp +++ b/tests/src/unit-element_access2.cpp @@ -1517,6 +1517,16 @@ TEST_CASE_TEMPLATE("element access 2 (throwing tests)", Json, nlohmann::json, nl CHECK(j.value("/not/existing"_json_pointer, Json({{"foo", "bar"}})) == Json({{"foo", "bar"}})); CHECK(j.value("/not/existing"_json_pointer, Json({10, 100})) == Json({10, 100})); + // an array index that is out of range, too large to be + // represented, or "-", and a token below a scalar + CHECK(j.value("/array/3"_json_pointer, 2) == 2); + CHECK(j.value("/array/-"_json_pointer, 2) == 2); + CHECK(j.value("/array/99999999999999999999999999"_json_pointer, 2) == 2); + CHECK(j.value("/integer/0"_json_pointer, 2) == 2); + CHECK(j.value("/string/x"_json_pointer, 2) == 2); + CHECK(j.value("/null/x"_json_pointer, 2) == 2); + CHECK(j.value("/array/0"_json_pointer, 2) == 1); + CHECK(j_const.value("/not/existing"_json_pointer, 2) == 2); CHECK(j_const.value("/not/existing"_json_pointer, 2u) == 2u); CHECK(j_const.value("/not/existing"_json_pointer, false) == false); diff --git a/tests/src/unit-json_patch.cpp b/tests/src/unit-json_patch.cpp index 7731c7d92..d3f030dd8 100644 --- a/tests/src/unit-json_patch.cpp +++ b/tests/src/unit-json_patch.cpp @@ -1751,3 +1751,72 @@ TEST_CASE("JSON patch - diff emits array removals in descending index order") CHECK(source.patch(patch) == target); } } + +TEST_CASE("JSON patch - every operation on ordered_json") +{ + using nlohmann::ordered_json; + + const ordered_json doc = {{"foo", "bar"}, {"arr", {1, 2, 3}}, {"obj", {{"a", 1}}}}; + + SECTION("successful operations") + { + const ordered_json patch = ordered_json::parse(R"([ + {"op": "add", "path": "/obj/b", "value": 2}, + {"op": "add", "path": "/arr/1", "value": 9}, + {"op": "add", "path": "/arr/-", "value": 4}, + {"op": "remove", "path": "/arr/0"}, + {"op": "remove", "path": "/obj/a"}, + {"op": "replace", "path": "/foo", "value": "baz"}, + {"op": "move", "from": "/foo", "path": "/moved"}, + {"op": "copy", "from": "/obj", "path": "/copied"}, + {"op": "test", "path": "/copied/b", "value": 2} + ])"); + + const ordered_json expected = ordered_json::parse(R"({ + "arr": [9, 2, 3, 4], "obj": {"b": 2}, "moved": "baz", "copied": {"b": 2} + })"); + + CHECK(doc.patch(patch) == expected); + + // adding to the root replaces the document + CHECK(doc.patch(ordered_json::parse(R"([{"op": "add", "path": "", "value": [1]}])")) == ordered_json({1})); + } + + SECTION("failing operations") + { + ordered_json _; + CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "add", "path": "/arr/4", "value": 1}])")), + "[json.exception.out_of_range.401] array index 4 is out of range", ordered_json::out_of_range&); + CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "add", "path": "/nope/x", "value": 1}])")), + "[json.exception.out_of_range.403] key 'nope' not found", ordered_json::out_of_range&); + CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "remove", "path": "/obj/nope"}])")), + "[json.exception.out_of_range.403] key 'nope' not found", ordered_json::out_of_range&); + CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "remove", "path": "/arr/3"}])")), + "[json.exception.out_of_range.401] array index 3 is out of range", ordered_json::out_of_range&); + CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "test", "path": "/foo", "value": "qux"}])")), + "[json.exception.other_error.501] unsuccessful: {\"op\":\"test\",\"path\":\"/foo\",\"value\":\"qux\"}", ordered_json::other_error&); + CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "add", "path": "/foo"}])")), + "[json.exception.parse_error.105] parse error: operation 'add' must have member 'value'", ordered_json::parse_error&); + CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "move", "from": "/obj", "path": "/obj/a/b"}])")), + "[json.exception.out_of_range.414] cannot move value: 'from' path '/obj' is a proper prefix of 'path' '/obj/a/b'", ordered_json::out_of_range&); + } + + SECTION("diff reproduces the target") + { + const ordered_json source = {{"a", 1}, {"b", 2}, {"c", {{"x", 1}}}, {"l", {1, 2, 3}}}; + const std::vector targets = + { + // a key removed, a key added, a nested change, a shorter array + {{"a", 1}, {"c", {{"x", 2}}}, {"l", {1}}, {"d", 4}}, + // the same keys in another order + {{"c", {{"x", 1}}}, {"a", 1}, {"b", 2}, {"l", {1, 2, 3}}}, + // new keys ahead of the common ones + {{"new", true}, {"a", 1}, {"b", 3}, {"c", {{"x", 1}}}, {"l", {1, 2, 3}}}, + }; + for (const auto& target : targets) + { + CAPTURE(target.dump()); + CHECK(source.patch(ordered_json::diff(source, target)) == target); + } + } +} diff --git a/tests/src/unit-json_pointer.cpp b/tests/src/unit-json_pointer.cpp index b01df2921..e7d6df530 100644 --- a/tests/src/unit-json_pointer.cpp +++ b/tests/src/unit-json_pointer.cpp @@ -872,3 +872,16 @@ TEST_CASE("JSON pointers") } #endif } + +TEST_CASE("unescaping keeps a '~' that does not start an escape sequence") +{ + // the parser of a JSON pointer rejects such reference tokens before it + // unescapes them, so this is only reachable by calling unescape directly + std::string s = "a~2b~"; + nlohmann::detail::unescape(s); + CHECK(s == "a~2b~"); + + s = "~0~1~"; + nlohmann::detail::unescape(s); + CHECK(s == "~/~"); +} diff --git a/tests/src/unit-merge_patch.cpp b/tests/src/unit-merge_patch.cpp index c9741e85b..b1e0c431b 100644 --- a/tests/src/unit-merge_patch.cpp +++ b/tests/src/unit-merge_patch.cpp @@ -345,3 +345,32 @@ TEST_CASE("JSON Merge Patch on deeply nested values") CHECK(p->at("x") == 1); } } + +TEST_CASE("JSON Merge Patch and update on ordered_json") +{ + using nlohmann::ordered_json; + + SECTION("merge_patch") + { + ordered_json target = ordered_json::parse(R"({"a": {"b": 1, "c": 2}, "d": 3, "e": [1]})"); + target.merge_patch(ordered_json::parse(R"({"a": {"b": null, "f": 4}, "d": {"x": {"y": null}}, "e": null, "g": {"h": 5}})")); + CHECK(target == ordered_json::parse(R"({"a": {"c": 2, "f": 4}, "d": {"x": {}}, "g": {"h": 5}})")); + + // a patch that is not an object replaces the target + target.merge_patch(ordered_json({1, 2})); + CHECK(target == ordered_json({1, 2})); + // an object patch turns a target that is not an object into one + target.merge_patch(ordered_json::parse(R"({"k": {"l": null}})")); + CHECK(target == ordered_json::parse(R"({"k": {}})")); + } + + SECTION("update with merge_objects") + { + ordered_json target = ordered_json::parse(R"({"a": {"b": 1, "c": {"d": 2}}, "e": 3})"); + target.update(ordered_json::parse(R"({"a": {"c": {"x": 1}, "f": 4}, "e": {"y": 5}, "g": 6})"), true); + CHECK(target == ordered_json::parse(R"({"a": {"b": 1, "c": {"d": 2, "x": 1}, "f": 4}, "e": {"y": 5}, "g": 6})")); + + target.update(ordered_json::parse(R"({"a": 1})"), false); + CHECK(target == ordered_json::parse(R"({"a": 1, "e": {"y": 5}, "g": 6})")); + } +} diff --git a/tests/src/unit-msgpack.cpp b/tests/src/unit-msgpack.cpp index a8892081d..318bbf3c7 100644 --- a/tests/src/unit-msgpack.cpp +++ b/tests/src/unit-msgpack.cpp @@ -1759,6 +1759,44 @@ TEST_CASE("MessagePack nesting does not consume the call stack") } } +TEST_CASE("MessagePack input that cannot be read is discarded by every overload") +{ + std::vector input = json::to_msgpack(json({{"a", {1, 2}}})); + input.pop_back(); + + json _; + CHECK_THROWS_AS(_ = json::from_msgpack(input.begin(), input.end()), json::parse_error&); + CHECK(json::from_msgpack(input, true, false).is_discarded()); + CHECK(json::from_msgpack(input.begin(), input.end(), true, false).is_discarded()); + CHECK(json::from_msgpack(input.data(), input.size(), true, false).is_discarded()); + CHECK(json::from_msgpack({input.data(), input.size()}, true, false).is_discarded()); +} + +TEST_CASE("MessagePack SAX parsing stops at every event") +{ + // Containers are opened and closed by the loop that reads them; a SAX + // handler that rejects any event - including the end of a nested + // container - must stop the parse right there. + const auto count_events = [](const std::vector& input) + { + int events = 0; + while (true) + { + SaxCountdown scp(events); + if (json::sax_parse(input, &scp, json::input_format_t::msgpack)) + { + return events; + } + ++events; + REQUIRE(events < 1000); + } + }; + + // 20 events: every container kind closes inside another one + const json j = json::parse(R"({"a": [1, {"b": []}], "c": {"d": [[2]]}})"); + CHECK(count_events(json::to_msgpack(j)) == 20); +} + TEST_CASE("single MessagePack roundtrip") { SECTION("sample.json") diff --git a/tests/src/unit-serialization.cpp b/tests/src/unit-serialization.cpp index 00b305a75..d9b10f45c 100644 --- a/tests/src/unit-serialization.cpp +++ b/tests/src/unit-serialization.cpp @@ -629,3 +629,153 @@ TEST_CASE("serialization of deeply nested values") } } } + +namespace +{ +// wraps @a inner into @a depth single-element arrays +json wrap_in_arrays(const json& inner, const std::size_t depth) +{ + json j = inner; + for (std::size_t i = 0; i < depth; ++i) + { + j = json::array({std::move(j)}); + } + return j; +} + +// what wrap_in_arrays(inner, depth).dump(2) is expected to be: the arrays +// around inner.dump(2), with inner's own lines indented by the depth +std::string expected_pretty_in_arrays(const json& inner, const std::size_t depth) +{ + std::string expected; + for (std::size_t i = 0; i < depth; ++i) + { + expected += std::string(2 * i, ' ') + "[\n"; + } + + const std::string indent(2 * depth, ' '); + expected += indent; + for (const char c : inner.dump(2)) + { + expected += c; + if (c == '\n') + { + expected += indent; + } + } + + for (std::size_t i = depth; i > 0; --i) + { + expected += '\n' + std::string(2 * (i - 1), ' ') + ']'; + } + return expected; +} +} // namespace + +TEST_CASE("serialization of every kind of value below the bound of the descent") +{ + // Values nested deeper than the bound are written without the call stack, + // by code of their own; each kind of value must come out the same there as + // it does at the top level, compact and pretty-printed. + std::vector values = + { + json::parse(R"({"a": 1, "b": [1, 2, {"c": "x"}], "d": {}, "e": []})"), + json::parse(R"([1, [2, 3], {"k": null}, "s"])"), + json::object(), + json::array(), + json::binary({1, 2, 3}, 42), + json::binary({1, 2, 3}), + json::binary({}, 7), + json::binary({}), + "a string with \"escapes\"\n", + true, + false, + -42, + 42u, + 1.5, + nullptr, + json(json::value_t::discarded), + }; + // a pretty-printed object whose members are themselves deep + values.push_back({{"x", wrap_in_arrays(1, 5)}, {"y", {{"z", 2}}}}); + + for (const std::size_t depth : std::vector {1, 200}) + { + CAPTURE(depth); + for (const auto& inner : values) + { + CAPTURE(inner.dump()); + const json j = wrap_in_arrays(inner, depth); + CHECK(j.dump() == std::string(depth, '[') + inner.dump() + std::string(depth, ']')); + CHECK(j.dump(2) == expected_pretty_in_arrays(inner, depth)); + } + } + + SECTION("pretty-printed objects across the bound") + { + for (std::size_t d = 120; d <= 140; ++d) + { + CAPTURE(d); + + // built from the inside out: {"k": , "n": } + json j = 7; + std::string expected = "7"; + for (std::size_t i = d; i > 0; --i) + { + j = json({{"k", std::move(j)}, {"n", i}}); + + const std::string indent(2 * i, ' '); + const std::string outer_indent(2 * (i - 1), ' '); + expected = "{\n" + indent + "\"k\": " + expected + ",\n" + + indent + "\"n\": " + std::to_string(i) + "\n" + outer_indent + "}"; + } + + CHECK(j.dump(2) == expected); + CHECK(json::parse(j.dump(2)) == j); + CHECK(json::parse(j.dump()) == j); + } + } +} + +TEST_CASE("serializer buffers are flushed mid-string and mid-binary") +{ + SECTION("a long run of escaped characters") + { + // each character is escaped on its own, so the escape buffer fills up + const json newlines = std::string(600, '\n'); + std::string expected = "\""; + for (int i = 0; i < 600; ++i) + { + expected += "\\n"; + } + expected += '"'; + CHECK(newlines.dump() == expected); + + // every character is \u-escaped under ensure_ascii + std::string umlauts; + std::string escaped_umlauts = "\""; + for (int i = 0; i < 300; ++i) + { + umlauts += "\xC3\xA4"; + escaped_umlauts += "\\u00e4"; + } + escaped_umlauts += '"'; + CHECK(json(umlauts).dump(-1, ' ', true) == escaped_umlauts); + } + + SECTION("a large binary value") + { + std::vector bytes(3000); + std::string expected_bytes; + std::string expected_pretty_bytes; + for (std::size_t i = 0; i < bytes.size(); ++i) + { + bytes[i] = static_cast(i % 256); + expected_bytes += (i == 0 ? "" : ",") + std::to_string(i % 256); + expected_pretty_bytes += (i == 0 ? "" : ", ") + std::to_string(i % 256); + } + const json j = json::binary(bytes); + CHECK(j.dump() == "{\"bytes\":[" + expected_bytes + "],\"subtype\":null}"); + CHECK(j.dump(2) == "{\n \"bytes\": [" + expected_pretty_bytes + "],\n \"subtype\": null\n}"); + } +} diff --git a/tests/src/unit-std-format.cpp b/tests/src/unit-std-format.cpp index 58cbbf5cc..f2be8d5ec 100644 --- a/tests/src/unit-std-format.cpp +++ b/tests/src/unit-std-format.cpp @@ -102,6 +102,29 @@ TEST_CASE("std::formatter") CHECK_THROWS_AS(std::vformat("{:{}}", std::make_format_args(j, dynamic_width)), std::format_error); // dynamic width } + SECTION("a format spec may run to the end of the parse context") + { + // std::format always hands parse() a range that still holds the closing + // '}', but a parse context may also end right after the spec + const auto parse = [](const char* spec) + { + std::format_parse_context ctx(spec); + std::formatter f; + CHECK(f.parse(ctx) == ctx.end()); + return f; + }; + + CHECK(parse("").indent == -1); + CHECK(parse(">").indent == -1); + CHECK(parse("#").indent == 4); + CHECK(parse("3").indent == 3); + CHECK(parse("#12").indent == 12); + + const auto f = parse(".>"); + CHECK(f.indent == -1); + CHECK(f.indent_char == '.'); + } + SECTION("std::format_to writes through an arbitrary output iterator") { const json j = {{"foo", 1}, {"bar", {1, 2, 3}}}; diff --git a/tests/src/unit-ubjson.cpp b/tests/src/unit-ubjson.cpp index c8458c44d..1de4c9813 100644 --- a/tests/src/unit-ubjson.cpp +++ b/tests/src/unit-ubjson.cpp @@ -1640,6 +1640,29 @@ TEST_CASE("UBJSON") }); CHECK_THROWS_AS(_ = json::sax_parse(v_ubjson, &scp, json::input_format_t::ubjson), json::out_of_range&); } + + SECTION("array with a known size, read with a callback") + { + // a sized array announces its length to start_array() + std::vector const v_ubjson = {'[', '#', 'i', 2, 'i', 1, 'i', 2}; + json j; + nlohmann::detail::json_sax_dom_callback_parser scp(j, [](int /*unused*/, json::parse_event_t /*unused*/, const json& /*unused*/) noexcept + { + return true; + }); + CHECK(json::sax_parse(v_ubjson, &scp, json::input_format_t::ubjson)); + CHECK(j == json({1, 2})); + + // the readers reject a size this large before they announce + // it, so it can only reach start_array() directly (the largest + // value stands for an unknown size and is never checked) + json k; + nlohmann::detail::json_sax_dom_callback_parser scp2(k, [](int /*unused*/, json::parse_event_t /*unused*/, const json& /*unused*/) noexcept + { + return true; + }); + CHECK_THROWS_AS(scp2.start_array((std::numeric_limits::max)() - 1), json::out_of_range&); + } } } @@ -2255,6 +2278,46 @@ TEST_CASE("UBJSON nesting does not consume the call stack") } } +TEST_CASE("UBJSON input that cannot be read is discarded by every overload") +{ + std::vector input = json::to_ubjson(json({{"a", {1, 2}}})); + input.pop_back(); + + json _; + CHECK_THROWS_AS(_ = json::from_ubjson(input.begin(), input.end()), json::parse_error&); + CHECK(json::from_ubjson(input, true, false).is_discarded()); + CHECK(json::from_ubjson(input.begin(), input.end(), true, false).is_discarded()); + CHECK(json::from_ubjson(input.data(), input.size(), true, false).is_discarded()); + CHECK(json::from_ubjson({input.data(), input.size()}, true, false).is_discarded()); +} + +TEST_CASE("UBJSON SAX parsing stops at every event") +{ + // Containers are opened and closed by the loop that reads them; a SAX + // handler that rejects any event - including the end of a nested + // container - must stop the parse right there. + const auto count_events = [](const std::vector& input) + { + int events = 0; + while (true) + { + SaxCountdown scp(events); + if (json::sax_parse(input, &scp, json::input_format_t::ubjson)) + { + return events; + } + ++events; + REQUIRE(events < 1000); + } + }; + + // 20 events: every container kind closes inside another one + const json j = json::parse(R"({"a": [1, {"b": []}], "c": {"d": [[2]]}})"); + CHECK(count_events(json::to_ubjson(j)) == 20); + CHECK(count_events(json::to_ubjson(j, true)) == 20); + CHECK(count_events(json::to_ubjson(j, true, true)) == 20); +} + TEST_CASE("UBJSON optimized arrays of a valueless type are bounded") { // An element of type 'Z', 'T' or 'F' is encoded by its marker alone, so an