From 056ba3b7c142ecbce6689cab7f0f211cd0f460d3 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sun, 1 Nov 2026 22:32:42 +0100 Subject: [PATCH] Tests: cover the remaining lines of the library headers Add tests for the lines of include/nlohmann that were not covered: the binary readers and writers, the lexer and locale paths, the json_view modules, the custom number, key and array types, json_pointer, json_patch, diff and the iterators. Signed-off-by: Niels Lohmann --- tests/src/unit-binary_writer_sinks.cpp | 68 ++++++++ tests/src/unit-bjdata.cpp | 26 +++ tests/src/unit-bon8.cpp | 3 + tests/src/unit-bson.cpp | 75 ++++++++ tests/src/unit-cbor.cpp | 24 +++ tests/src/unit-class_lexer.cpp | 75 ++++++++ tests/src/unit-custom-array-type.cpp | 20 +++ tests/src/unit-custom-key-type-full.cpp | 21 +++ tests/src/unit-custom-number-types.cpp | 66 +++++++ tests/src/unit-custom-object-key-type.cpp | 178 +++++++++++++++++++ tests/src/unit-deserialization.cpp | 22 +++ tests/src/unit-diagnostics.cpp | 20 +++ tests/src/unit-iterators2.cpp | 19 ++ tests/src/unit-json_patch.cpp | 98 +++++++++++ tests/src/unit-json_pointer.cpp | 16 ++ tests/src/unit-json_view.cpp | 52 +++++- tests/src/unit-json_view_compare.cpp | 62 +++++++ tests/src/unit-json_view_dump.cpp | 27 +++ tests/src/unit-json_view_dump_escape.cpp | 40 +++++ tests/src/unit-json_view_edit.cpp | 17 ++ tests/src/unit-json_view_from_json.cpp | 11 ++ tests/src/unit-json_view_image.cpp | 7 + tests/src/unit-json_view_large.cpp | 56 ++++++ tests/src/unit-json_view_positions.cpp | 70 ++++++++ tests/src/unit-large_json.cpp | 53 ++++++ tests/src/unit-locale-cpp.cpp | 203 ++++++++++++++++++++++ tests/src/unit-msgpack-cpp17.cpp | 30 ++++ tests/src/unit-ordered_json.cpp | 85 +++++++++ tests/src/unit-regression1.cpp | 63 +++++++ tests/src/unit-regression2.cpp | 4 + tests/src/unit-stream-bulk-scan.cpp | 46 +++++ tests/src/unit-to_chars.cpp | 46 +++++ tests/src/unit-ubjson.cpp | 25 +++ 33 files changed, 1627 insertions(+), 1 deletion(-) diff --git a/tests/src/unit-binary_writer_sinks.cpp b/tests/src/unit-binary_writer_sinks.cpp index 1e18c3f15..7ec2eb8fc 100644 --- a/tests/src/unit-binary_writer_sinks.cpp +++ b/tests/src/unit-binary_writer_sinks.cpp @@ -14,7 +14,9 @@ using nlohmann::json; #include #include #include +#include #include +#include #include #include @@ -767,3 +769,69 @@ TEST_CASE("a write that throws leaves the same bytes in every kind of output") } } #endif + +#if !defined(JSON_NOEXCEPTION) +namespace +{ + +// an adapter whose every write fails +class failing_adapter : public nlohmann::detail::output_adapter_protocol +{ + public: + [[noreturn]] void write_character(char /*c*/) override + { + throw std::runtime_error("write failed"); + } + + [[noreturn]] void write_characters(const char* /*s*/, std::size_t /*length*/) override + { + throw std::runtime_error("write failed"); + } +}; + +} // namespace +#endif + +TEST_CASE("output adapters") +{ + SECTION("writing a single character") + { + // the writers pass whole buffers on, but an adapter takes single + // characters as well + std::vector as_vector; + nlohmann::detail::output_vector_adapter vector_adapter(as_vector); + vector_adapter.write_character('a'); + vector_adapter.write_character('b'); + CHECK(as_vector == std::vector({'a', 'b'})); + + std::ostringstream as_stream; + nlohmann::detail::output_stream_adapter stream_adapter(as_stream); + stream_adapter.write_character('c'); + stream_adapter.write_character('d'); + CHECK(as_stream.str() == "cd"); + + std::string as_string; + nlohmann::detail::output_string_adapter string_adapter(as_string); + string_adapter.write_character('e'); + string_adapter.write_character('f'); + CHECK(as_string == "ef"); + } + +#if !defined(JSON_NOEXCEPTION) + SECTION("a sink that is destroyed with characters left over") + { + // the destructor passes the pending characters on, as a write that + // threw leaves them there; the adapter failing then must not escape + { + nlohmann::detail::output_adapter_sink sink(std::make_shared()); + sink.write_character('x'); + } + + // while flush() reports the failure + nlohmann::detail::output_adapter_sink sink(std::make_shared()); + sink.write_character('x'); + CHECK_THROWS_AS(sink.flush(), std::runtime_error&); + // (the character counts as written, so there is nothing left for the destructor) + } +#endif +} diff --git a/tests/src/unit-bjdata.cpp b/tests/src/unit-bjdata.cpp index 04ee3acc4..ec7faa6cd 100644 --- a/tests/src/unit-bjdata.cpp +++ b/tests/src/unit-bjdata.cpp @@ -2312,6 +2312,23 @@ TEST_CASE("BJData") CHECK_FALSE(json::sax_parse(v, &scp, json::input_format_t::bjdata)); } + SECTION("every event of an ndarray of chars") + { + // the elements of an ndarray of 'C' are read one by one + std::vector const v = {'[', '$', 'C', '#', '[', '$', 'U', '#', 'U', 2, 2, 2, 'a', 'b', 'c', 'd'}; + // start_object, key, string, key, start_array, 2 numbers, end_array, + // key, start_array, 4 chars, end_array, end_object + constexpr int events = 16; + for (int allowed = 0; allowed < events; ++allowed) + { + CAPTURE(allowed) + SaxCountdown scp(allowed); + CHECK_FALSE(json::sax_parse(v, &scp, json::input_format_t::bjdata)); + } + SaxCountdown scp(events); + CHECK(json::sax_parse(v, &scp, json::input_format_t::bjdata)); + } + SECTION("start_array() in ndarray _ArrayData_") { std::vector const v = {'[', '$', 'U', '#', '[', 'i', 2, 'i', 3, ']', 6, 5, 4, 3, 2, 1}; @@ -3095,6 +3112,15 @@ TEST_CASE("BJData") json _; CHECK_THROWS_WITH(_ = json::from_bjdata(v), "[json.exception.parse_error.113] parse error at byte 2: syntax error while parsing BJData char: byte after 'C' must be in range 0x00..0x7F; last byte: 0x82"); } + + SECTION("ndarray of chars cut off by the end of the input") + { + // 2x2 chars announced, three present + std::vector const v = {'[', '$', 'C', '#', '[', '$', 'U', '#', 'U', 2, 2, 2, 'a', 'b', 'c'}; + json _; + CHECK_THROWS_WITH_AS(_ = json::from_bjdata(v), "[json.exception.parse_error.110] parse error at byte 16: syntax error while parsing BJData char: unexpected end of input", json::parse_error&); + CHECK(json::from_bjdata(v, true, false).is_discarded()); + } } SECTION("byte") diff --git a/tests/src/unit-bon8.cpp b/tests/src/unit-bon8.cpp index 1e24d32d4..85c9fa821 100644 --- a/tests/src/unit-bon8.cpp +++ b/tests/src/unit-bon8.cpp @@ -329,6 +329,9 @@ TEST_CASE("BON8") check_bon8({false, nullptr, true}, {0x83, 0xF8, 0xFA, 0xF9}); check_bon8({false, nullptr, true, 1.0}, {0x84, 0xF8, 0xFA, 0xF9, 0xFD}); check_bon8({false, nullptr, true, 1.0, json::array(), 0.0}, {0x85, 0xF8, 0xFA, 0xF9, 0xFD, 0x80, 0xFC, 0xFE}); + // an array that starts with a number: -1, 0, and 1 have bytes of their own + check_bon8({-1.0, 0.0, 1.0}, {0x83, 0xFB, 0xFC, 0xFD}); + check_bon8({1.0, -1.0, -0.0}, {0x83, 0xFD, 0xFB, 0x8E, 0x80, 0x00, 0x00, 0x00}); check_bon8({{{1}}}, {0x81, 0x81, 0x81, 0x91}); check_bon8({{{"foo"}}}, {0x81, 0x81, 0x81, 'f', 'o', 'o', 0xFF}); check_bon8({{{""}}}, {0x81, 0x81, 0x81, 0xFF}); diff --git a/tests/src/unit-bson.cpp b/tests/src/unit-bson.cpp index f1f43ccd7..dff0d070d 100644 --- a/tests/src/unit-bson.cpp +++ b/tests/src/unit-bson.cpp @@ -81,8 +81,20 @@ class huge_string_t : public std::string return result; } + // returns a copy of @a s whose size() pretends to be @a n + static huge_string_t as_size(const std::string& s, const size_type n) + { + huge_string_t result(s); + result.fake_size = n; + return result; + } + size_type size() const noexcept { + if (fake_size != 0) + { + return fake_size; + } if (pretend_huge) { // one byte more than the BSON length field can represent @@ -93,6 +105,7 @@ class huge_string_t : public std::string private: bool pretend_huge = false; + size_type fake_size = 0; }; using huge_string_json = nlohmann::json::with_string_t; @@ -267,6 +280,17 @@ TEST_CASE("BSON") CHECK_THROWS_WITH_AS(huge_string_json::to_bson(j), "[json.exception.out_of_range.412] BSON length 2147483661 exceeds maximum of 2147483647", huge_string_json::out_of_range&); } + SECTION("an array element that no longer fits the document") + { + // the string fits the length field, but not the document that + // holds it; nothing of it is written + const auto limit = static_cast((std::numeric_limits::max)()); + huge_string_json j; + j["a"] = huge_string_json::array({huge_string_t::as_size("value", limit - 8)}); + + CHECK_THROWS_AS(huge_string_json::to_bson(j), huge_string_json::out_of_range&); + } + SECTION("document") { // an oversized string nested one level deep makes the @@ -898,6 +922,38 @@ TEST_CASE("BSON") CHECK_THROWS_AS(_ = json::from_bson(input), json::parse_error&); } + SECTION("binary member with subtype 0x02 (old binary) with a truncated inner length") + { + // the inner length field is cut off by the end of the input + std::vector const input = + { + 0x0F, 0x00, 0x00, 0x00, // size (little endian) + 0x05, 'a', 0x00, // entry: binary + 0x08, 0x00, 0x00, 0x00, // size of binary (little endian) + 0x02, // "old binary" subtype + 0x01 // first byte of the inner length + }; + + json _; + CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.110] parse error at byte 14: syntax error while parsing BSON number: unexpected end of input", json::parse_error&); + CHECK(json::from_bson(input, true, false).is_discarded()); + } + + SECTION("string member that is cut off by the end of the input") + { + std::vector const input = + { + 0x0F, 0x00, 0x00, 0x00, // size (little endian) + 0x02, 'a', 0x00, // entry: string + 0x0A, 0x00, 0x00, 0x00, // size of the string (little endian) + 'b', 'c' // the string ends early + }; + + json _; + CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.110] parse error at byte 14: syntax error while parsing BSON string: unexpected end of input", json::parse_error&); + CHECK(json::from_bson(input, true, false).is_discarded()); + } + SECTION("Some more complex document") { json const j = @@ -2136,6 +2192,25 @@ value = depth % 2 == 0 ? json{{"a", std::move(value)}, {"b", {1, "x"}}} : CHECK(output.empty()); } +#if !defined(JSON_NOEXCEPTION) + SECTION("U+0000 in a long key is found at any position") + { + // the key is searched eight characters at a time + for (const std::string& key : + { + std::string("abc\0defghijkl", 12), std::string("abcdefgh\0ijkl", 13), std::string("abcdefghijklmnop\0", 17), std::string("\0abcdefghijkl", 13), + std::string("abcdefghijklmno\0", 16) + }) + { + CAPTURE(key) + const json value = json::object({{key, 1}}); + std::vector output; + CHECK_THROWS_AS(json::to_bson(value, output), json::out_of_range&); + CHECK(output.empty()); + } + } +#endif + SECTION("a binary subtype that doesn't fit a byte is rejected before anything is written (#5675)") { // the offending value is nested, so this also covers that the check diff --git a/tests/src/unit-cbor.cpp b/tests/src/unit-cbor.cpp index 930cbe7f5..442050cf4 100644 --- a/tests/src/unit-cbor.cpp +++ b/tests/src/unit-cbor.cpp @@ -1809,6 +1809,30 @@ TEST_CASE("CBOR") CHECK(json::from_cbor(std::vector({0xa1, 0xff, 0x01}), true, false).is_discarded()); } + SECTION("tag in front of a key that is cut off by the end of the input") + { + // 0xD8: the tag number is in the next byte, which is missing + const std::vector input = {0xA1, 0xD8}; + json _; + for (const auto handler : + { + json::cbor_tag_handler_t::ignore, json::cbor_tag_handler_t::store + }) + { + CHECK_THROWS_WITH_AS(_ = json::from_cbor(input, true, true, handler), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing CBOR number: unexpected end of input", json::parse_error&); + CHECK(json::from_cbor(input, true, false, handler).is_discarded()); + } + } + + SECTION("chunk of an indefinite-length string that is cut off by the end of the input") + { + // the second byte announces a chunk of three bytes, but two follow + const std::vector input = {0x7F, 0x63, 'a', 'b'}; + json _; + CHECK_THROWS_WITH_AS(_ = json::from_cbor(input), "[json.exception.parse_error.110] parse error at byte 5: syntax error while parsing CBOR string: unexpected end of input", json::parse_error&); + CHECK(json::from_cbor(input, true, false).is_discarded()); + } + SECTION("tag in front of a key") { const auto ignore = json::cbor_tag_handler_t::ignore; diff --git a/tests/src/unit-class_lexer.cpp b/tests/src/unit-class_lexer.cpp index 2926ef88b..0e8b22317 100644 --- a/tests/src/unit-class_lexer.cpp +++ b/tests/src/unit-class_lexer.cpp @@ -2092,6 +2092,12 @@ TEST_CASE("UTF-8 validators accept the same sequences") } }; + // an ASCII byte is a sequence of its own + for (unsigned lead = 0; lead < 0x80; ++lead) + { + CHECK(nlohmann::detail::utf8_shape(static_cast(lead)).length == 1); + } + for (unsigned lead = 0x80; lead <= 0xFF; ++lead) { const auto shape = nlohmann::detail::utf8_shape(static_cast(lead)); @@ -2157,3 +2163,72 @@ TEST_CASE("UTF-8 validators accept the same sequences") } } } + +TEST_CASE("float_bigint") +{ + // the big integer of the exact comparison of a float token with the + // midpoint between two floats (digit_comparison()) + using nlohmann::detail::float_bigint; + + SECTION("zero") + { + // shifting zero keeps it zero, whatever the distance + float_bigint zero(0); + zero.shift_left(0); + zero.shift_left(5); + zero.shift_left(64); + CHECK(zero.compare(float_bigint(0)) == 0); + CHECK(zero.compare(float_bigint(1)) == -1); + CHECK(float_bigint(1).compare(zero) == 1); + } + + SECTION("compare numbers of different lengths") + { + // the number with more 32-bit limbs is the greater one + const float_bigint small(0xFFFFFFFFu); + const float_bigint large(std::uint64_t{1} << 32u); + CHECK(small.compare(large) == -1); + CHECK(large.compare(small) == 1); + CHECK(large.compare(large) == 0); + + // a shift can add a limb + float_bigint shifted(1); + shifted.shift_left(32); + CHECK(shifted.compare(large) == 0); + shifted.shift_left(1); + CHECK(shifted.compare(large) == 1); + CHECK(large.compare(shifted) == -1); + } + + SECTION("compare numbers of the same length") + { + CHECK(float_bigint(2).compare(float_bigint(3)) == -1); + CHECK(float_bigint(3).compare(float_bigint(2)) == 1); + CHECK(float_bigint(3).compare(float_bigint(3)) == 0); + CHECK(float_bigint((std::uint64_t{3} << 32u) | 1u).compare(float_bigint((std::uint64_t{3} << 32u) | 2u)) == -1); + } +} + +TEST_CASE("serialization of short strings") +{ + // the serializer takes strings of up to 32 characters that need no + // escaping by a table lookup of its own where the scan is not vectorized + // (this file is built that way, too, with JSON_NO_SIMD) + for (const std::string& s : + { + std::string(), std::string("a"), std::string("hello"), std::string("0123456789abcdef"), std::string("0123456789abcdef0123456789abcdef"), + std::string("0123456789abcdef0123456789abcdef0") + }) + { + CAPTURE(s) + const json j = s; + CHECK(j.dump() == "\"" + s + "\""); + CHECK(j.dump(-1, ' ', true) == "\"" + s + "\""); + CHECK(json({{s, s}}).dump() == "{\"" + s + "\":\"" + s + "\"}"); + } + + // a character that needs an escape + CHECK(json("a\"b").dump() == "\"a\\\"b\""); + CHECK(json("caf\xC3\xA9").dump() == "\"caf\xC3\xA9\""); + CHECK(json("caf\xC3\xA9").dump(-1, ' ', true) == "\"caf\\u00e9\""); +} diff --git a/tests/src/unit-custom-array-type.cpp b/tests/src/unit-custom-array-type.cpp index e602b4716..65285eb54 100644 --- a/tests/src/unit-custom-array-type.cpp +++ b/tests/src/unit-custom-array-type.cpp @@ -210,3 +210,23 @@ TEST_CASE("regression test #5476 - array type without reserve()") CHECK(json::from_cbor(truncated, true, false).is_discarded()); } } + +TEST_CASE("array type with std::deque nested beyond the recursion limit") +{ + // the binary writers finish a value nested deeper than + // 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 + deque_json j = 1; + for (int i = 0; i < 200; ++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 expected(200, 0x81); + expected.push_back(0x01); + CHECK(deque_json::to_cbor(j) == expected); +} diff --git a/tests/src/unit-custom-key-type-full.cpp b/tests/src/unit-custom-key-type-full.cpp index b72bd7ae0..69a052c9f 100644 --- a/tests/src/unit-custom-key-type-full.cpp +++ b/tests/src/unit-custom-key-type-full.cpp @@ -49,3 +49,24 @@ TEST_CASE("custom object key types: MessagePack (json_full)") { custom_key_test::test_msgpack(); } + +TEST_CASE("custom object key types: patch (json_full)") +{ + // the members of a patch are found by lookup, as their keys are not std::string + const json_full j = json_full::parse(R"({"a":1,"b":[1,2],"c":{"d":2}})"); + const json_full patch = json_full::parse(R"([ + {"op": "add", "path": "/e", "value": 5}, + {"op": "remove", "path": "/a"}, + {"op": "replace", "path": "/c/d", "value": 3}, + {"op": "test", "path": "/c/d", "value": 3}, + {"op": "move", "from": "/e", "path": "/f"}, + {"op": "copy", "from": "/f", "path": "/g"}, + {"op": "add", "path": "/b/-", "value": 7} + ])"); + + CHECK(j.patch(patch).dump() == R"({"b":[1,2,7],"c":{"d":3},"f":5,"g":5})"); + + // a patch with a missing member is rejected + CHECK_THROWS_AS(j.patch(json_full::parse(R"([{"op": "add", "path": "/e"}])")), json_full::parse_error&); + CHECK_THROWS_AS(j.patch(json_full::parse(R"([{"op": "test", "path": "/a", "value": 2}])")), json_full::other_error&); +} diff --git a/tests/src/unit-custom-number-types.cpp b/tests/src/unit-custom-number-types.cpp index e324417db..58353d67e 100644 --- a/tests/src/unit-custom-number-types.cpp +++ b/tests/src/unit-custom-number-types.cpp @@ -337,6 +337,52 @@ TEST_CASE("custom number types: integers beyond 64 bits") CHECK(wide_json::to_bson(fit_members) == nlohmann::json::to_bson(fit_members64)); } + SECTION("MessagePack integers of every width") + { + // an integer of a number type wider than 64 bits is written with the + // smallest MessagePack integer that holds it, as for 64-bit types + const wide_json widths = wide_json::array( + { + make_int(-33), make_int(-100), make_int(-128), make_int(-129), make_int(-32768), make_int(-32769), make_int(-2147483648LL), make_int(-2147483649LL), + make_int(128), make_int(255), make_int(256), make_int(300), make_int(65535), make_int(65536), make_int(70000), make_int(4294967295LL), make_int(4294967296LL), + make_uint(128), make_uint(255), make_uint(256), make_uint(300), make_uint(65535), make_uint(65536), make_uint(70000), make_uint(4294967295ULL), make_uint(4294967296ULL) + }); + const nlohmann::json widths64 = nlohmann::json::array( + { + -33, -100, -128, -129, -32768, -32769, -2147483648LL, -2147483649LL, + 128, 255, 256, 300, 65535, 65536, 70000, 4294967295LL, 4294967296LL, + 128u, 255u, 256u, 300u, 65535u, 65536u, 70000u, 4294967295ULL, 4294967296ULL + }); + CHECK(wide_json::to_msgpack(widths) == nlohmann::json::to_msgpack(widths64)); + + // as single values, too + CHECK(wide_json::to_msgpack(make_int(-100)) == std::vector({0xD0, 0x9C})); + CHECK(wide_json::to_msgpack(make_int(300)) == std::vector({0xCD, 0x01, 0x2C})); + CHECK(wide_json::to_msgpack(make_int(70000)) == std::vector({0xCE, 0x00, 0x01, 0x11, 0x70})); + CHECK(wide_json::to_msgpack(make_uint(300)) == std::vector({0xCD, 0x01, 0x2C})); + CHECK(wide_json::to_msgpack(make_uint(70000)) == std::vector({0xCE, 0x00, 0x01, 0x11, 0x70})); + } + + SECTION("dump") + { + // beyond 64 bits, the digits are counted before they are written + CHECK(make_int(0).dump() == "0"); + CHECK(make_int(two_64).dump() == "18446744073709551616"); + CHECK(make_int(-two_64).dump() == "-18446744073709551616"); + CHECK(make_int(two_100).dump() == "1267650600228229401496703205376"); + CHECK(make_int(-two_100).dump() == "-1267650600228229401496703205376"); + // powers of ten, where the count of digits ends on a single digit or two + const int128 ten_20 = static_cast(100000000000ULL) * static_cast(1000000000ULL); + CHECK(make_int(ten_20).dump() == "100000000000000000000"); + CHECK(make_int(-ten_20).dump() == "-100000000000000000000"); + CHECK(make_int(ten_20 * 10).dump() == "1000000000000000000000"); + CHECK(make_int(ten_20 * 100).dump() == "10000000000000000000000"); + CHECK(make_int(ten_20 * 1000).dump() == "100000000000000000000000"); + CHECK(make_uint(static_cast(ten_20) * 10u).dump() == "1000000000000000000000"); + CHECK(make_uint((std::numeric_limits::max)()).dump() == "340282366920938463463374607431768211455"); + CHECK(make_int((std::numeric_limits::min)()).dump() == "-170141183460469231731687303715884105728"); + } + SECTION("BJData ND-array") { // an ND-array element beyond 64 bits does not fit any dtype, so the @@ -417,3 +463,23 @@ TEST_CASE("custom number types") } } + +TEST_CASE("custom number types: 32-bit integers in the binary formats") +{ + // integers narrower than 64 bits are widened (a signed one with its sign) + // before the CBOR, MessagePack, and UBJSON writers pick their encoding + using json32 = nlohmann::json::with_integers_t; + json32 values = json32::array(); + values.push_back(std::uint32_t{0}); + values.push_back(std::uint32_t{200}); + values.push_back(std::uint32_t{4294967295u}); + values.push_back(std::int32_t{-1}); + values.push_back(std::int32_t{-2147483647 - 1}); + REQUIRE(values[2].is_number_unsigned()); + REQUIRE(values[4].is_number_integer()); + + const nlohmann::json values64 = {0u, 200u, 4294967295u, -1, -2147483648LL}; + CHECK(json32::to_cbor(values) == nlohmann::json::to_cbor(values64)); + CHECK(json32::to_msgpack(values) == nlohmann::json::to_msgpack(values64)); + CHECK(json32::to_ubjson(values) == nlohmann::json::to_ubjson(values64)); +} diff --git a/tests/src/unit-custom-object-key-type.cpp b/tests/src/unit-custom-object-key-type.cpp index 9c64ebce0..350a30050 100644 --- a/tests/src/unit-custom-object-key-type.cpp +++ b/tests/src/unit-custom-object-key-type.cpp @@ -11,8 +11,13 @@ #include #include +#include +#include +#include #include +#include #include +#include #include "custom_object_key_type.hpp" @@ -125,3 +130,176 @@ TEST_CASE("MessagePack supports custom object key types nested deeper than the r CHECK(encoded == nlohmann::json::to_msgpack(expected)); CHECK(nlohmann::json::from_msgpack(encoded) == expected); } + +namespace +{ +// A key type that cannot be constructed from a std::string, so that a value +// with such keys cannot be converted from a nlohmann::json by the range +// constructor of its object type: the serializer converts the object instead. +class strict_key +{ + public: + strict_key() = default; + + strict_key(const char* value) + : m_value(value) + {} + + strict_key(const std::string& /*value*/) = delete; + + // Required by JSON_DIAGNOSTICS, which reads object keys through data() + // when building the path of an exception. + const char* data() const noexcept + { + return m_value.data(); + } + + friend bool operator<(const strict_key& lhs, const strict_key& rhs) + { + return lhs.m_value < rhs.m_value; + } + + private: + std::string m_value{}; +}; + +strict_key make_strict_key(const std::string& key) +{ + return strict_key(key.c_str()); +} + +strict_key make_strict_key(const strict_key& key) +{ + return key; +} + +template +class strict_object + : public std::map < + strict_key, + Value, + std::less, // NOLINT(modernize-use-transparent-functors) + typename std::allocator_traits::template rebind_alloc < + std::pair>> +{ + public: + strict_object() = default; + + // the range of another object type, whose keys are made into strict_keys + template + strict_object(Iterator first, Iterator last) + { + for (; first != last; ++first) + { + this->emplace(make_strict_key(first->first), Value(std::move(first->second))); + } + } +}; + +template +struct strict_serializer : nlohmann::adl_serializer {}; + +using strict_json = nlohmann::basic_json < strict_object, std::vector, std::string, bool, std::int64_t, + std::uint64_t, double, std::allocator, strict_serializer, std::vector>; + +// the members of an object are converted one by one, their keys by make_strict_key() +template<> +struct strict_serializer +{ + static void to_json(strict_json& j, const nlohmann::json::object_t& object) + { + strict_json::object_t converted; + for (const auto& member : object) + { + converted.emplace(make_strict_key(member.first), strict_json(member.second)); + } + j = strict_json(std::move(converted)); + } +}; +} // namespace + +TEST_CASE("converting a value with keys that cannot be made from a std::string nested deeper than the recursion depth limit") +{ + static_assert(!std::is_constructible::value, "the keys cannot be made from a std::string"); + + // below detail::recursion_depth_limit(), objects and arrays are converted + // by the serializers instead of the range constructors + const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 10; + + nlohmann::json value = 1; + for (std::size_t i = 0; i < depth; ++i) + { + if (i % 2 == 0) + { + value = nlohmann::json::object({{"a", std::move(value)}, {"b", true}}); + } + else + { + value = nlohmann::json::array({std::move(value), "sibling"}); + } + } + + const strict_json converted = value; + + // at() needs to make a std::string of a key for its error message + const auto member = [](const strict_json & object, const char* name) -> const strict_json & + { + const auto it = object.find(strict_key(name)); + REQUIRE(it != object.end()); + return *it; + }; + + // walk down the nesting levels without recursion + const strict_json* current = &converted; + for (std::size_t i = depth; i > 0; --i) + { + if (i % 2 == 1) + { + REQUIRE(current->is_object()); + CHECK(current->size() == 2); + CHECK(member(*current, "b").get()); + current = &member(*current, "a"); + } + else + { + REQUIRE(current->is_array()); + CHECK(current->size() == 2); + CHECK(current->at(1).get() == "sibling"); + current = ¤t->at(0); + } + } + CHECK(current->get() == 1); +} + +namespace +{ +// shorter keys come first +struct shortlex_less +{ + bool operator()(const std::string& lhs, const std::string& rhs) const + { + return lhs.size() != rhs.size() ? lhs.size() < rhs.size() : lhs < rhs; + } +}; + +template +using shortlex_map = std::map; + +using shortlex_json = nlohmann::json::with_object_t; +} // namespace + +TEST_CASE("unflatten with an object type that does not sort the keys alphabetically") +{ + // the keys of /a come before and after one of /b: unlike in an object + // sorted alphabetically, the values below a prefix are not found one + // after the other, so the value of /a must be looked up again + shortlex_json flat = shortlex_json::object(); + flat["/a/p"] = 1; + flat["/b/qq"] = 2; + flat["/a/pqr"] = 3; + REQUIRE(flat.begin().key() == "/a/p"); + REQUIRE((++flat.begin()).key() == "/b/qq"); + + CHECK(flat.unflatten().dump() == R"({"a":{"p":1,"pqr":3},"b":{"qq":2}})"); + CHECK(flat.unflatten().flatten() == flat); +} diff --git a/tests/src/unit-deserialization.cpp b/tests/src/unit-deserialization.cpp index e82685311..5b8f086fd 100644 --- a/tests/src/unit-deserialization.cpp +++ b/tests/src/unit-deserialization.cpp @@ -1195,6 +1195,28 @@ TEST_CASE("deserialization") #endif } +#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED +TEST_CASE("deserialization from a pointer and a length (deprecated)") +{ + // the unit test build disables -Wdeprecated-declarations (see + // cmake/clang_flags.cmake) + const std::string text = R"({"foo": [1, 2, 3]})"; + const std::string invalid = R"({"foo": )"; + + SECTION("parse") + { + const json j = json::parse({text.data(), text.size()}); + CHECK(j == json({{"foo", {1, 2, 3}}})); + } + + SECTION("accept") + { + CHECK(json::accept({text.data(), text.size()})); + CHECK_FALSE(json::accept({invalid.data(), invalid.size()})); + } +} +#endif + // select the types to test - char8_t is only available since C++20 if and only // if __cpp_char8_t is defined. #define TYPE_LIST(...) __VA_ARGS__ diff --git a/tests/src/unit-diagnostics.cpp b/tests/src/unit-diagnostics.cpp index 504a95322..2987d348c 100644 --- a/tests/src/unit-diagnostics.cpp +++ b/tests/src/unit-diagnostics.cpp @@ -868,3 +868,23 @@ TEST_CASE("Parent pointers stay consistent for object types not known to keep th obj["1"].push_back(42); CHECK_THROWS_WITH_AS(obj["1"].at(5), "[json.exception.out_of_range.401] (/1) array index 5 is out of range", relocating_json::out_of_range&); } + +TEST_CASE("patch_inplace() with a patch that is stored inside the patched value") +{ + // the patch is modified-while-iterated if it is not copied first: it is a + // descendant of the value it is applied to, which only the parent pointers + // (JSON_DIAGNOSTICS) reveal + json j = + { + {"patch", {{{"op", "add"}, {"path", "/x"}, {"value", 1}}, {{"op", "remove"}, {"path", "/patch/0"}}}}, + {"y", 2} + }; + + j.patch_inplace(j["patch"]); + + CHECK(j.size() == 3); + CHECK(j["x"] == 1); + CHECK(j["y"] == 2); + CHECK(j["patch"].size() == 1); + CHECK(j["patch"][0]["op"] == "remove"); +} diff --git a/tests/src/unit-iterators2.cpp b/tests/src/unit-iterators2.cpp index 5bc738e21..67a5752e4 100644 --- a/tests/src/unit-iterators2.cpp +++ b/tests/src/unit-iterators2.cpp @@ -18,6 +18,8 @@ #include using nlohmann::json; +#include + #if JSON_HAS_RANGES #include #include @@ -665,6 +667,23 @@ TEST_CASE("iterators 2") } } + SECTION("reverse iterators of a const value") + { + const json j_array = {1, 2, 3}; + const json j_object = {{"one", 1}, {"two", 2}}; + + // rbegin() and rend() of a const value are crbegin() and crend() + CHECK(j_array.rbegin() == j_array.crbegin()); + CHECK(j_array.rend() == j_array.crend()); + CHECK(*j_array.rbegin() == 3); + CHECK(std::distance(j_array.rbegin(), j_array.rend()) == 3); + + CHECK(j_object.rbegin() == j_object.crbegin()); + CHECK(j_object.rend() == j_object.crend()); + CHECK(j_object.rbegin().key() == "two"); + CHECK(std::distance(j_object.rbegin(), j_object.rend()) == 2); + } + SECTION("reverse iterator arithmetic") { json j_object = {{"one", 1}, {"two", 2}, {"three", 3}}; diff --git a/tests/src/unit-json_patch.cpp b/tests/src/unit-json_patch.cpp index 83df15350..37c6e29d9 100644 --- a/tests/src/unit-json_patch.cpp +++ b/tests/src/unit-json_patch.cpp @@ -14,9 +14,11 @@ using nlohmann::json; using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) #endif +#include #include #include #include +#include #include #include "make_test_data_available.hpp" @@ -2453,3 +2455,99 @@ TEST_CASE("JSON patch - diff behavior documented in the API reference") CHECK(json::diff(json(nullptr), json::array()) == R"([{"op": "replace", "path": "", "value": []}])"_json); } } + +TEST_CASE("JSON patch - operations that are left to json_pointer") +{ + // the usual operations on a plain pointer are applied without building a + // json_pointer; the following are not plain, and some are errors + SECTION("a path without a slash") + { + const json j = {{"a", 1}}; + + CHECK_THROWS_AS(j.patch(R"([{"op": "add", "path": "abc", "value": 1}])"_json), json::parse_error&); + CHECK_THROWS_AS(j.patch(R"([{"op": "remove", "path": "abc"}])"_json), json::parse_error&); + } + + SECTION("remove with an escaped reference token") + { + const json j = {{"a/b", 1}, {"c~d", 2}, {"e", 3}}; + + CHECK(j.patch(R"([{"op": "remove", "path": "/a~1b"}])"_json) == json({{"c~d", 2}, {"e", 3}})); + CHECK(j.patch(R"([{"op": "remove", "path": "/c~0d"}])"_json) == json({{"a/b", 1}, {"e", 3}})); + CHECK_THROWS_AS(j.patch(R"([{"op": "remove", "path": "/missing~1key"}])"_json), json::out_of_range&); + } + + SECTION("a path through a primitive value") + { + const json j = {{"a", 1}}; + + CHECK_THROWS_AS(j.patch(R"([{"op": "test", "path": "/a/b", "value": 1}])"_json), json::other_error&); + CHECK_THROWS_AS(j.patch(R"([{"op": "replace", "path": "/a/b", "value": 1}])"_json), json::out_of_range&); + } +} + +TEST_CASE("JSON patch - diff of objects with many members") +{ + // more members than diff() keeps without allocating + for (const std::size_t members : + { + 16u, 17u, 40u + }) + { + CAPTURE(members) + + json source = json::object(); + json target = json::object(); + for (std::size_t i = 0; i < members; ++i) + { + const std::string key = "key" + std::to_string(i); + source[key] = i; + // every third member differs + target[key] = (i % 3 == 0) ? i + 1 : i; + } + // the same keys, a removed and an added one + source["removed"] = true; + target["added"] = false; + + const json patch = json::diff(source, target); + CHECK(source.patch(patch) == target); + CHECK(patch.size() == ((members + 2) / 3) + 2); + + const nlohmann::ordered_json ordered_source = source; + const nlohmann::ordered_json ordered_target = target; + CHECK(ordered_source.patch(nlohmann::ordered_json::diff(ordered_source, ordered_target)) == ordered_target); + } +} + +TEST_CASE("JSON patch - diff of leaf values below the bound of the recursive descent") +{ + // 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; + + json source = {1, "text", 3, {{"x", 1}, {"y", 2}}, true, nullptr}; + json target = {1, 2, 4, {{"x", 1}, {"y", 3}}, true, nullptr}; + for (std::size_t i = 0; i < depth; ++i) + { + source = json::array({std::move(source)}); + target = json::array({std::move(target)}); + } + + const json patch = json::diff(source, target); + CHECK(patch.size() == 3); + CHECK(source.patch(patch) == target); + + // the same with objects as the outer levels + json object_source = {{"equal", 1}, {"type", "text"}, {"value", 3}}; + json object_target = {{"equal", 1}, {"type", 2}, {"value", 4}}; + for (std::size_t i = 0; i < depth; ++i) + { + object_source = json::object({{"a", std::move(object_source)}}); + object_target = json::object({{"a", std::move(object_target)}}); + } + + const json object_patch = json::diff(object_source, object_target); + CHECK(object_patch.size() == 2); + CHECK(object_source.patch(object_patch) == object_target); +} diff --git a/tests/src/unit-json_pointer.cpp b/tests/src/unit-json_pointer.cpp index 0e225b86e..8afb52f48 100644 --- a/tests/src/unit-json_pointer.cpp +++ b/tests/src/unit-json_pointer.cpp @@ -106,6 +106,12 @@ TEST_CASE("JSON pointers") CHECK_THROWS_WITH_AS(p.pop_back(), "[json.exception.out_of_range.405] JSON pointer has no parent", json::out_of_range&); + // the top of a pointer is its first reference token + const json::json_pointer nonempty("/foo/bar/0"); + CHECK(nonempty.top() == json::json_pointer("/foo")); + CHECK(nonempty.top().to_string() == "/foo"); + CHECK(json::json_pointer("/a~1b").top() == json::json_pointer("/a~1b")); + SECTION("array index error") { json v = {1, 2, 3, 4}; @@ -697,6 +703,16 @@ TEST_CASE("JSON pointers") CHECK_THROWS_WITH_AS(j_error.unflatten(), "[json.exception.type_error.313] invalid value to unflatten", json::type_error&); + // error for an invalid escape sequence in a key + CHECK_THROWS_WITH_AS(json({{"/a~2", 1}}).unflatten(), + "[json.exception.parse_error.108] parse error: escape character '~' must be followed with '0' or '1'", json::parse_error&); + CHECK_THROWS_WITH_AS(json({{"/a/b", 1}, {"/c~", 1}}).unflatten(), + "[json.exception.parse_error.108] parse error: escape character '~' must be followed with '0' or '1'", json::parse_error&); + + // error for an element of an array that is also a container + CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/0/x", 2}}).unflatten(), + "[json.exception.type_error.313] invalid value to unflatten", json::type_error&); + // explicit roundtrip check CHECK(j.flatten().unflatten() == j); diff --git a/tests/src/unit-json_view.cpp b/tests/src/unit-json_view.cpp index c823ef9e2..9d8822dca 100644 --- a/tests/src/unit-json_view.cpp +++ b/tests/src/unit-json_view.cpp @@ -31,9 +31,11 @@ using json_view_test::has_duplicate_keys; #include #include #include +#include #include #include #include +#include #include #include #include @@ -98,6 +100,37 @@ template using bool_conversion_t = decltype(static_cast(std::declval())); #if !defined(JSON_NOEXCEPTION) +// an allocator that never has room +template +struct failing_allocator +{ + using value_type = T; + + failing_allocator() = default; + + template + explicit failing_allocator(const failing_allocator& /*other*/) noexcept {} + + T* allocate(std::size_t /*n*/) // NOLINT(readability-convert-member-functions-to-static) + { + throw std::bad_alloc(); + } + + void deallocate(T* /*p*/, std::size_t /*n*/) noexcept {} // NOLINT(readability-convert-member-functions-to-static) + + template + bool operator==(const failing_allocator& /*other*/) const noexcept + { + return true; + } + + template + bool operator!=(const failing_allocator& /*other*/) const noexcept + { + return false; + } +}; + // the exception parse() throws for a text, or "" if it accepts it std::string parse_exception(const std::string& text, bool comments = false, bool trailing_commas = false) { @@ -235,7 +268,9 @@ TEST_CASE("json_view") { "", " ", "[", "]", "{", "[1,]", "{\"a\":1,}", "[1 2]", "{\"a\" 1}", "{1:2}", "tru", "nul", "fals", "truex", "-", "01", "1.", ".5", "1e", "\"", "\"abc", "\"\\x\"", "\"\\u12\"", "\"\\ud800\"", "\"\\udc00\"", "\"\x01\"", "\"\xff\"", "\"\xc3\"", "[1]x", "/", "/*", "[\n 1,\n x\n]", // NOLINT(modernize-raw-string-literal) - "1e400", "-1e400", "[1.7976931348623159e308]", "{\"a\":\n{\"b\": [1, 2,\n 3 x]}}" + "1e400", "-1e400", "[1.7976931348623159e308]", "{\"a\":\n{\"b\": [1, 2,\n 3 x]}}", + // (an error after every kind of value: the error is found by replaying the parser) + "[null, true, -1, 1, 1.5, \"s\", {\"a\": 1} 2]", "{\"a\": null, \"b\": -1, \"c\": 1.5, \"d\": \"s\", \"e\": {}, 1}" }) { CAPTURE(text) @@ -368,6 +403,10 @@ TEST_CASE("json_view") CHECK(from_stream.root().materialize() == expected); const std::list list(text.begin(), text.end()); CHECK(json_document::parse(list.begin(), list.end()).owns_source()); + // (an input that is not valid) + const std::list bad_list{'[', '1', ','}; + json_document from_bad_list; + CHECK_THROWS_AS(from_bad_list = json_document::parse(bad_list.begin(), bad_list.end()), json::parse_error&); CHECK(materialized(list.begin(), list.end()) == expected); // iterator pairs: pointers are borrowed, and so are contiguous library @@ -1511,6 +1550,17 @@ TEST_CASE("json_view values") const json_document dup = json_document::parse(R"({"a":1,"a":2})"); CHECK((dup.root().get>() == std::map {{"a", 2}})); + // a map of views from something that is not an object + using view_map = std::map; + const json_document not_an_object = json_document::parse("[1,2]"); + CHECK_THROWS_WITH_AS(not_an_object.root().get(), json_view_test::message("type_error.302", "", 0, 5, "type must be object, but is array").c_str(), json::type_error&); +#if !defined(JSON_NOEXCEPTION) + // (and one that cannot allocate its nodes) + using failing_map = std::map, failing_allocator>>; + const json_document one_member = json_document::parse(R"({"a":1})"); + CHECK_THROWS_AS(one_member.root().get(), std::bad_alloc&); +#endif + const json_view invalid{}; CHECK_THROWS_WITH_AS(invalid.get(), "[json.exception.type_error.302] type must be number, but is discarded", json::type_error&); CHECK(invalid.get().is_discarded()); diff --git a/tests/src/unit-json_view_compare.cpp b/tests/src/unit-json_view_compare.cpp index 48527adb4..8316ec74a 100644 --- a/tests/src/unit-json_view_compare.cpp +++ b/tests/src/unit-json_view_compare.cpp @@ -21,13 +21,22 @@ using json_view_test::generator; #include #include +#include #include +#include #include // These tests were split off unit-json_view_dump.cpp, whose object file got too // large for the MinGW assembler ("file too big"). Like that file, this one is // also built with C++17, as it mentions JSON_HAS_CPP_17. +namespace +{ +// an object type without a comparison of keys, and with an arbitrary order of members +template +using unordered_object = std::unordered_map, std::equal_to, Allocator>; +} // namespace + TEST_CASE("json_view comparison") { SECTION("equality of the values parse() produces") @@ -205,6 +214,59 @@ TEST_CASE("json_view comparison") CHECK(!(null_document.root() == discarded)); } + SECTION("values that differ in their first bytes, or are written differently") + { + const auto same = [](const char* x, const char* y) + { + const json_document dx = json_document::parse(x); + const json_document dy = json_document::parse(y); + const bool equal = dx.root() == dy.root(); + CHECK(equal == (json::parse(x) == json::parse(y))); + CHECK(equal == (dx.root() == json::parse(y))); + const nlohmann::json_editable_document ex = nlohmann::json_editable_document::parse(x); + CHECK(equal == (ex.root() == dy.root())); + return equal; + }; + // booleans + CHECK(!same("true", "false")); + CHECK(!same("[true]", "[false]")); + CHECK(!same(R"({"a":false,"b":[1]})", R"({"a":true,"b":[1]})")); + CHECK(same(R"({"a":true,"b":[1]})", R"({"a":true,"b":[1]})")); + // strings and keys of equal length that differ in the first eight bytes (they are compared in words) + CHECK(!same(R"("abcdefghijklmnopqrstuvwxyz")", R"("Xbcdefghijklmnopqrstuvwxyz")")); + CHECK(!same(R"(["abcdefghijklmnopq",1])", R"(["abcdefghXjklmnopq",1])")); + CHECK(!same(R"({"abcdefghijklmnopq":1})", R"({"Xbcdefghijklmnopq":1})")); + CHECK(!same(R"({"abcdefghijklmnopq":[1]})", R"({"abcdefghijklmnopX":[1]})")); + // floats of one layout and different tokens are the same value + CHECK(same("[1.5e0, 2]", "[1.5E0, 2]")); + CHECK(same("[1.5e0, 2]", "[1.5e+0, 2]")); + CHECK(same("[1.50, 2]", "[1.5, 2]")); + CHECK(!same("[1.5e0, 2]", "[1.6e0, 2]")); + CHECK(!same("[1.5e0, 2]", "[1.5E1, 2]")); + CHECK(same(R"({"a":1.25e0,"b":[3]})", R"({"a":1.25E0,"b":[3]})")); + } + + SECTION("a basic_json whose members are not sorted") + { + // (an object type that is not an ordered map, and does not keep its members sorted either) + using unordered_json = nlohmann::basic_json; + using unordered_document = nlohmann::basic_json_document; + std::string text = "{"; + for (char key = 'a'; key <= 'p'; ++key) + { + text += (key != 'a' ? ",\"" : "\"") + std::string(1, key) + "\":[" + std::to_string(key) + "]"; + } + text += "}"; + const unordered_json j = unordered_json::parse(text); + const unordered_document d = unordered_document::parse(text); + CHECK(d.root() == j); + CHECK(j == d.root()); + unordered_json other = j; + other["k"] = 0; + CHECK(d.root() != other); + CHECK(other != d.root()); + } + SECTION("values that are the same text, or the same value written differently, at any depth") { // nested arrays and objects around depths where the walk over equal text gives up diff --git a/tests/src/unit-json_view_dump.cpp b/tests/src/unit-json_view_dump.cpp index be1ec48ad..301f07a53 100644 --- a/tests/src/unit-json_view_dump.cpp +++ b/tests/src/unit-json_view_dump.cpp @@ -101,6 +101,10 @@ TEST_CASE("json_view dump") CHECK(d.root().dump(0, ' ', false, json_view::number_format::source) == "[\n1.50,\n1E2,\n-0,\n-0.0,\n123456789012345678901234567890,\n18446744073709551615,\n-9223372036854775808,\n0.1,\n1e-7,\n5e-324\n]"); CHECK(d.root().dump(-1, ' ', true, json_view::number_format::source) == "[1.50,1E2,-0,-0.0,123456789012345678901234567890,18446744073709551615,-9223372036854775808,0.1,1e-7,5e-324]"); + CHECK(d.root().dump(0, ' ', true, json_view::number_format::source) == d.root().dump(0, ' ', false, json_view::number_format::source)); + const json_document escaped = json_document::parse("[\"\xC3\xA9\", 1.50, {\"\xF0\x9F\x98\x80\": 1E2}]"); + CHECK(escaped.root().dump(1, ' ', true, json_view::number_format::source) == "[\n \"\\u00e9\",\n 1.50,\n {\n \"\\ud83d\\ude00\": 1E2\n }\n]"); + // float tokens of up to 17 significant digits in every spelling: those // of at most 15 digits are written from their digits, the others // through the conversion; both as dump() writes them @@ -232,6 +236,29 @@ TEST_CASE("json_view dump") CHECK(editable.root().dump() == expected.dump()); } + // tokens of 16 and 17 digits with few digits after the point, and with a value of 0, which are + // converted by the general algorithm; padded, as a token near the end of the text is not read in this way + { + const std::string padding(40, ' '); + for (const char* token : + { + "123456789012.3456", "-123456789012.3456", "1234567890123.456", "12345678901234.56", "123456789012345.6", "1234567890123456.0", + "0.0000000000000000", "-0.0000000000000000", "0.000000000000000", "1234567890123.4567", "99999999999999.999" + }) + { + CAPTURE(token) + const std::string text = std::string("[") + token + padding + "]"; + const json_document d = json_document::parse(text); + const json j = json::parse(text); + CHECK(d.root()[0].get() == j[0].get()); + CHECK(d.root().dump() == j.dump()); + // a float document converts the tokens as well + using json_float = nlohmann::basic_json; + const nlohmann::basic_json_document float_doc = nlohmann::basic_json_document::parse(text); + CHECK(float_doc.root()[0].get() == json_float::parse(text)[0].get()); + } + } + // random doubles, written as parse() and dump() would std::mt19937_64 rng(1170); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) std::string many = "["; diff --git a/tests/src/unit-json_view_dump_escape.cpp b/tests/src/unit-json_view_dump_escape.cpp index 3def62df6..2716f76d4 100644 --- a/tests/src/unit-json_view_dump_escape.cpp +++ b/tests/src/unit-json_view_dump_escape.cpp @@ -214,6 +214,24 @@ TEST_CASE("json_view dump: escaping, nesting, and output buffers") CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6)); } + SECTION("the estimate for a value that is followed by many decoded strings") + { + // (the end of the value in the source is not known when the nodes after it have decoded strings) + std::string text = "[7"; + for (int i = 0; i < 20; ++i) + { + text += R"(,"\n")"; + } + text += ",8]"; + const json_document d = json_document::parse(text); + CHECK(d.root()[0].dump() == "7"); + CHECK(d.root()[1].dump() == R"("\n")"); + CHECK(d.root().dump() == json::parse(text).dump()); + // the same with a container + const json_document nested = json_document::parse(R"([[7],)" + text.substr(3)); + CHECK(nested.root()[0].dump() == "[7]"); + } + SECTION("ensure_ascii with strings of the source, decoded strings, and edited strings") { // plain ASCII, non-ASCII, escaped (decoded), and mixed strings, as values and as keys; @@ -259,6 +277,28 @@ TEST_CASE("json_view dump: escaping, nesting, and output buffers") } } + SECTION("the length of the run that ensure_ascii copies") + { + // (dump() only asks for strings with a byte to escape, so a run that ends the string is checked here) + for (const std::size_t length : + { + std::size_t{0}, std::size_t{1}, std::size_t{15}, std::size_t{16}, std::size_t{17}, std::size_t{31}, std::size_t{32}, std::size_t{48}, std::size_t{50} + }) + { + CAPTURE(length) + // a run of printable characters, followed by a byte that is not copied and more text + std::string text(length, 'a'); + text += "\"abcdefghijklmnopqrstuvwxyz"; + const auto* const bytes = reinterpret_cast(text.data()); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + const auto* const end = bytes + text.size(); + CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length, end) == length); + CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length + 1, end) == length); + CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length + 5, end) == length); + // the end of the buffer right after the run + CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length, bytes + length) == length); + } + } + SECTION("ensure_ascii with DEL or a non-ASCII character at every position") { // the check whether a string of the source has no byte >= 0x7F reads diff --git a/tests/src/unit-json_view_edit.cpp b/tests/src/unit-json_view_edit.cpp index 2cc3d4701..db6d3054b 100644 --- a/tests/src/unit-json_view_edit.cpp +++ b/tests/src/unit-json_view_edit.cpp @@ -547,6 +547,10 @@ TEST_CASE("json_view edits: errors") CHECK_THROWS_WITH_AS(d.set(json::json_pointer("/a/01"), 1), "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&); CHECK_THROWS_WITH_AS(d.set(root, json_editable_view()), "[json.exception.type_error.302] type must be a value, but is discarded", json::type_error&); CHECK_THROWS_WITH_AS(d.set(root, json::binary({1, 2})), "[json.exception.type_error.319] cannot store a binary value in a json_document", json::type_error&); + CHECK_THROWS_WITH_AS(d.set(root, json(json::value_t::discarded)), "[json.exception.type_error.302] type must be a value, but is discarded", json::type_error&); + // (an index that does not fit the document, in a null: basic_json would pad it with 4 Gi nulls) + CHECK_THROWS_AS(d.set(json::json_pointer("/z/4294967295"), 1), json::out_of_range&); + CHECK(root["z"].is_null()); #if !defined(JSON_NOEXCEPTION) // invalid UTF-8 is rejected when it enters the document, with the error @@ -572,6 +576,19 @@ TEST_CASE("json_view edits: errors") static_cast(other); } +TEST_CASE("json_view edits: a container that was added is replaced by a scalar") +{ + // the view of an element that push_back() added is that of the new value itself, not of + // the slot that links to it: replacing it by a scalar has to find the container it is in + json_editable_document d = json_editable_document::parse(R"({"a": [1]})"); + d.push_back(d.root()["a"], json::array({2, 3})); + d.push_back(d.root()["a"], json::object({{"k", json::array({4, 5})}})); + d.set(d.root()["a"][1], 6); + d.set(d.root()["a"][2]["k"], "s"); + CHECK(d.root().dump() == R"({"a":[1,6,{"k":"s"}]})"); + CHECK(d.root().materialize() == json::parse(R"({"a":[1,6,{"k":"s"}]})")); +} + #if !defined(JSON_NOEXCEPTION) TEST_CASE("json_view edits: the size of a text arena") { diff --git a/tests/src/unit-json_view_from_json.cpp b/tests/src/unit-json_view_from_json.cpp index 824585a56..50c310cb6 100644 --- a/tests/src/unit-json_view_from_json.cpp +++ b/tests/src/unit-json_view_from_json.cpp @@ -277,6 +277,17 @@ TEST_CASE("json_view from_json") CHECK(true); } + SECTION("a map with enum keys from a view that is not an object") + { + // an object is read with the keys as the enum's names; any other + // value is read as an array of pairs, like a map of any other key type + const json_document d = json_document::parse(R"({"pairs": [["red", 1], ["green", 2]], "number": 1})"); + const json j = d.root().materialize(); + const json_view v = d.root(); + CHECK(same>(v, j, "pairs")); + CHECK((v["pairs"].get>() == std::map {{color::red, 1}, {color::green, 2}})); + } + SECTION("std types: the same values as from the materialized value") { const json_document d = json_document::parse(R"({"ints": [3, 1, 2, 1], "strings": ["b", "a", "b"], "pairs": [[1, "x"], [2, "y"]], diff --git a/tests/src/unit-json_view_image.cpp b/tests/src/unit-json_view_image.cpp index 5e392559d..499f85342 100644 --- a/tests/src/unit-json_view_image.cpp +++ b/tests/src/unit-json_view_image.cpp @@ -819,6 +819,13 @@ TEST_CASE("json_view images: errors") static_cast(d); }) == prefix + "too short"); + // an image that is moved into the document is checked as well + CHECK(exception_of([&] + { + const json_document d = json_document::load(std::vector(10, 0)); + static_cast(d); + }) == prefix + "too short"); + std::vector bad = image; bad[0] = 'X'; CHECK(load_result(bad, image_check::full) == prefix + "unknown format"); diff --git a/tests/src/unit-json_view_large.cpp b/tests/src/unit-json_view_large.cpp index 5ad65c812..b5f58386c 100644 --- a/tests/src/unit-json_view_large.cpp +++ b/tests/src/unit-json_view_large.cpp @@ -333,6 +333,62 @@ TEST_CASE("json_view large objects") CHECK(without_collisions.memory_usage() >= with_collisions.memory_usage() + (table_size * sizeof(std::uint32_t))); } + SECTION("a lookup of a missing key stops at the end of the probe window") + { + // Every slot of the first half of the table holds a key, each at its home slot, so + // that the table is built; a key that is not in the object but whose home slot is + // the first slot finds a full window of slots with other keys. + constexpr std::size_t members = 128; + constexpr std::size_t table_size = 256; // the table size for 128 members: the next power of two >= 256 + for (const std::size_t prefix_length : + { + std::size_t{0}, std::size_t{20} + }) + { + CAPTURE(prefix_length) + std::vector keys(members); + std::vector taken(members, false); + std::string missing; + std::size_t found = 0; + for (std::uint64_t counter = 0; found < members || missing.empty(); ++counter) + { + std::string key(prefix_length, 'p'); + std::uint64_t x = counter; + for (std::size_t i = 0; i < 8; ++i, x /= 26) + { + key += static_cast('a' + (x % 26)); + } + const std::size_t slot = nlohmann::detail::conditional_static_cast(nlohmann::detail::view::key_hash(key.data(), key.size())) & (table_size - 1); + if (slot < members && !taken[slot]) + { + taken[slot] = true; + keys[slot] = key; + ++found; + } + else if (slot == 0 && missing.empty()) + { + missing = key; + } + } + std::string text = "{"; + for (std::size_t i = 0; i < members; ++i) + { + text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i); + } + text += "}"; + const json_document d = json_document::parse(text); + const json_view v = d.root(); + for (std::size_t i = 0; i < members; ++i) + { + CAPTURE(i) + CHECK(v[keys[i]].get() == i); + } + CHECK(!v.contains(missing)); + CHECK(v.find(missing) == v.end()); + CHECK(v.count(missing) == 0); + } + } + SECTION("keys that differ in their last bytes are spread over the table") { // The hash must spread the usual families of keys (numbered names, hex diff --git a/tests/src/unit-json_view_positions.cpp b/tests/src/unit-json_view_positions.cpp index c6afd7fcb..958a42ac6 100644 --- a/tests/src/unit-json_view_positions.cpp +++ b/tests/src/unit-json_view_positions.cpp @@ -20,6 +20,7 @@ using nlohmann::ordered_json; using nlohmann::json_document; using nlohmann::json_editable_document; +#include #include #include #include @@ -427,6 +428,31 @@ TEST_CASE("json_view materialize() source positions") CHECK(same_positions(m[std::string("a") + '\n'][2], j[std::string("a") + '\n'][2])); } + SECTION("a damaged text: the end of a container is not found") + { + // (the text of an image is not checked with image_check::none) + const std::string array_text = "[1,2]"; + const std::vector original = json_document::parse(array_text).save(); + const auto at = std::search(original.begin(), original.end(), array_text.begin(), array_text.end()); + REQUIRE(at != original.end()); + const auto closing = static_cast(at - original.begin()) + array_text.size() - 1; + // not a closing bracket, a slash that starts no comment, and the end of the text + for (const char replacement : + { + 'x', '/', ' ' + }) + { + CAPTURE(replacement) + std::vector damaged = original; + damaged[closing] = static_cast(replacement); + const json_document d = json_document::load(damaged, json_document::image_check::none); + const json m = d.root().materialize(); + CHECK(range(m) == "none"); + CHECK(range(m[0]) == "1-2"); + CHECK(range(m[1]) == "3-4"); + } + } + #if !defined(JSON_NOEXCEPTION) SECTION("a decoded string's position must be within the text") { @@ -461,6 +487,7 @@ TEST_CASE("json_view exceptions name the path and the bytes of the value") { "null", "true", "42", "\"s\"", "[]", "{}", "[1, [2, [3, []]]]", R"({"a": {"b": [1, {"c": null}]}, "d": "e"})", R"({"a/b": {"~": [1]}, "\u0041\n": {"x": "\t"}, "": [{}]})", + R"({"a\u0000b": [1, "x"], "c": {"d\u0000": 2}})", // (the path of a key ends at a null byte, as in basic_json) R"([1 /* ] */, {"a": [] // } , "b": {} } , ])", "\n\t[\r\n 1 ,\n {\n \"a\" : [ ]\n }\n]\n", @@ -512,6 +539,49 @@ TEST_CASE("json_view exceptions name the path and the bytes of the value") CHECK(exception_of_call([&] { static_cast(d.root()["a"][2]["n"][0].get()); }) == expected); } + SECTION("views of values that are no longer part of the document, and of no value") + { + const std::string expected_string = "[json.exception.type_error.302] type must be number, but is string"; + static_cast(expected_string); + // (a value that is not found has no path, and with it no bytes) + { + json_editable_document d = json_editable_document::parse(R"(["x", ["y"]])"); + const auto removed = d.root()[0]; + d.erase(d.root(), 0); +#if JSON_DIAGNOSTICS + CHECK(exception_of_call([&] { static_cast(removed.get()); }) == expected_string); +#endif + // (the root is a scalar now: the value cannot be in it) + d.set(d.root(), 5); +#if JSON_DIAGNOSTICS + CHECK(exception_of_call([&] { static_cast(removed.get()); }) == expected_string); +#endif + CHECK(d.root().get() == 5); + } + { + json_editable_document d = json_editable_document::parse(R"(["x"])"); + const auto removed = d.root()[0]; + d.set(d.root(), 5); +#if JSON_DIAGNOSTICS + CHECK(exception_of_call([&] { static_cast(removed.get()); }) == expected_string); +#endif + } + // a view of no value has no context + CHECK(exception_of_call([&] { static_cast(nlohmann::json_view().get()); }) == "[json.exception.type_error.302] type must be number, but is discarded"); + } + + SECTION("an array whose last element was replaced by a new container has no bytes") + { + json_editable_document d = json_editable_document::parse(R"({"a": [1, [2]], "b": 1})"); + d.set(d.root()["a"][1], json::array({3})); + std::string expected = "[json.exception.type_error.302] "; +#if JSON_DIAGNOSTICS + expected += "(/a) "; +#endif + expected += "type must be number, but is array"; + CHECK(exception_of_call([&] { static_cast(d.root()["a"].get()); }) == expected); + } + SECTION("deep documents with siblings after every nested container") { // the search for the path pushes and pops one frame per level (the diff --git a/tests/src/unit-large_json.cpp b/tests/src/unit-large_json.cpp index ed988c242..e8cc0ed42 100644 --- a/tests/src/unit-large_json.cpp +++ b/tests/src/unit-large_json.cpp @@ -12,7 +12,10 @@ using nlohmann::json; #include +#include +#include #include +#include TEST_CASE("tests on very large JSONs") { @@ -140,6 +143,56 @@ TEST_CASE("tests on deeply nested JSONs") } } + SECTION("leaf values of every type below the bound of the recursive descent") + { + // the elements and members of the containers below the bound are + // copied one by one, whatever their type + const json leaves = json::array( + { + nullptr, true, false, -1, std::uint64_t{1}, 1.5, "text", json::binary({1, 2, 3}) + }); + const json members = + { + {"null", nullptr}, {"boolean", true}, {"integer", -1}, {"unsigned", std::uint64_t{1}}, + {"float", 1.5}, {"string", "text"}, {"binary", json::binary({1, 2, 3})} + }; + + for (const bool objects : + { + false, true + }) + { + CAPTURE(objects) + + json j = objects ? members : leaves; + for (std::size_t i = 0; i < 300; ++i) + { + json wrapper = objects ? json::object() : json::array(); + if (objects) + { + wrapper["a"] = std::move(j); + } + else + { + wrapper.push_back(std::move(j)); + } + j = std::move(wrapper); + } + + const json copy(j); // NOLINT(performance-unnecessary-copy-initialization): the copy is what is tested + + const json* current = © + for (std::size_t i = 0; i < 300; ++i) + { + current = objects ? ¤t->at("a") : ¤t->front(); + } + + // the innermost value is shallow: it is safe to compare + CHECK(*current == (objects ? members : leaves)); + CHECK(current->size() == (objects ? members.size() : leaves.size())); + } + } + SECTION("a value that is deep in one place only") { json j = json::object(); diff --git a/tests/src/unit-locale-cpp.cpp b/tests/src/unit-locale-cpp.cpp index b665b6531..441563b3c 100644 --- a/tests/src/unit-locale-cpp.cpp +++ b/tests/src/unit-locale-cpp.cpp @@ -495,3 +495,206 @@ TEST_CASE("locale changes during a single dump() (#5709 item 3)") // fix #5597 made on the parser side CHECK(buf.data == "[\"" + padding + "\",1234.5]"); } + +namespace +{ +// a token whose first data() call switches LC_NUMERIC, so that the locale +// changes between convert_float_locale_aware() looking up the decimal point +// and strtod reading the token (the race of #5198, made deterministic) +struct locale_switching_token +{ + using value_type = char; + + locale_switching_token(std::string text_, const char* switch_to_) + : text(std::move(text_)), switch_to(switch_to_) + {} + + std::size_t size() const + { + return text.size(); + } + + char& operator[](std::size_t i) + { + return text[i]; + } + + char* data() + { + if (!switched) + { + switched = std::setlocale(LC_NUMERIC, switch_to) != nullptr; + } + return &text[0]; + } + + std::string text; + const char* switch_to; + bool switched = false; +}; + +// set LC_NUMERIC to the first of @a names that is usable and has a decimal +// point of @a min_size bytes at least (and at most @a max_size) +bool use_decimal_point_locale(const std::vector& names, std::size_t min_size, std::size_t max_size, const char*& used) +{ + for (const char* name : names) + { + if (std::setlocale(LC_NUMERIC, name) == nullptr) + { + continue; + } + const std::size_t size = std::string(std::localeconv()->decimal_point).size(); + if (size >= min_size && size <= max_size) + { + used = name; + return true; + } + } + return false; +} + +// convert a token with convert_float_locale_aware(); the token must be given +// back as it was +template +FloatType convert_locale_aware(const std::string& text) +{ + std::string token = text; + const std::size_t dot = token.find('.'); + FloatType value{}; + nlohmann::detail::convert_float_locale_aware(token, dot, value); + CHECK(token == text); + return value; +} + +template +void check_locale_aware_conversions() +{ + CHECK(convert_locale_aware("3.5") == FloatType(3.5)); + CHECK(convert_locale_aware("-0.25") == FloatType(-0.25)); + CHECK(convert_locale_aware("1.25e2") == FloatType(125)); + // no decimal point at all + CHECK(convert_locale_aware("12e1") == FloatType(120)); + CHECK(convert_locale_aware("7") == FloatType(7)); +} +} // namespace + +TEST_CASE("convert_float_locale_aware") +{ + const std::vector single_byte_names = {"de_DE.UTF-8", "de_DE", "fr_FR.UTF-8", "fr_FR", "ru_RU.UTF-8"}; + const std::vector multi_byte_names = {"ar_EG.UTF-8", "ar_SA.UTF-8", "fa_IR.UTF-8", "ps_AF.UTF-8", "ar_EG", "fa_IR"}; + const char* used = nullptr; + + SECTION("decimal point '.'") + { + REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr); + check_locale_aware_conversions(); + check_locale_aware_conversions(); + check_locale_aware_conversions(); + } + + SECTION("single-byte decimal point other than '.'") + { + if (use_decimal_point_locale(single_byte_names, 1, 1, used) && std::localeconv()->decimal_point[0] != '.') + { + CAPTURE(used) + check_locale_aware_conversions(); + check_locale_aware_conversions(); + check_locale_aware_conversions(); + } + else + { + MESSAGE("no locale with a single-byte decimal point other than '.' is usable"); + } + } + + SECTION("multi-byte decimal point") + { + if (use_decimal_point_locale(multi_byte_names, 2, 8, used)) + { + CAPTURE(used) + check_locale_aware_conversions(); + check_locale_aware_conversions(); + check_locale_aware_conversions(); + } + else + { + MESSAGE("no locale with a multi-byte decimal point is usable"); + } + } + + SECTION("a token that is not converted completely") + { + // the locale does not change, so there is nothing to retry: the + // value up to the garbage is kept + REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr); + std::string token = "1.5x"; + double value = 0; + nlohmann::detail::convert_float_locale_aware(token, token.find('.'), value); + CHECK(value == 1.5); + CHECK(token == "1.5x"); + } + + SECTION("the locale changes before the conversion") + { + if (!use_decimal_point_locale(single_byte_names, 1, 1, used) || std::localeconv()->decimal_point[0] == '.') + { + MESSAGE("no locale with a single-byte decimal point other than '.' is usable"); + } + else + { + const std::string comma_locale = used; + REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr); + + // "C" -> comma locale: strtod stops at the '.', so the conversion is repeated + { + locale_switching_token token("1.5", comma_locale.c_str()); + double value = 0; + nlohmann::detail::convert_float_locale_aware(token, 1, value); + if (token.switched) + { + CHECK(value == 1.5); + CHECK(token.text == "1.5"); + } + } + + // the same for the other floating-point types + REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr); + { + locale_switching_token token("2.25", comma_locale.c_str()); + float value = 0; + nlohmann::detail::convert_float_locale_aware(token, 1, value); + if (token.switched) + { + CHECK(value == 2.25f); + } + } + REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr); + { + locale_switching_token token("2.25", comma_locale.c_str()); + long double value = 0; + nlohmann::detail::convert_float_locale_aware(token, 1, value); + if (token.switched) + { + CHECK(value == 2.25L); + } + } + + // comma locale -> multi-byte decimal point + const char* multi_byte = nullptr; + if (use_decimal_point_locale(multi_byte_names, 2, 8, multi_byte)) + { + REQUIRE(std::setlocale(LC_NUMERIC, comma_locale.c_str()) != nullptr); + locale_switching_token token("1.5", multi_byte); + double value = 0; + nlohmann::detail::convert_float_locale_aware(token, 1, value); + if (token.switched) + { + CHECK(value == 1.5); + CHECK(token.text == "1.5"); + } + } + } + } + + CHECK(std::setlocale(LC_NUMERIC, "C") != nullptr); +} diff --git a/tests/src/unit-msgpack-cpp17.cpp b/tests/src/unit-msgpack-cpp17.cpp index 6ae4d6d56..6b6b207bf 100644 --- a/tests/src/unit-msgpack-cpp17.cpp +++ b/tests/src/unit-msgpack-cpp17.cpp @@ -18,6 +18,7 @@ using nlohmann::json; #ifdef JSON_HAS_CPP_17 #include +#include #include // Test suite for verifying MessagePack handling with std::byte input @@ -96,4 +97,33 @@ TEST_CASE("MessagePack with std::byte") } } } + +// JSON text (not MessagePack) as std::byte input: the error position is +// computed from the bytes consumed so far +TEST_CASE("JSON text with std::byte") +{ + const auto to_bytes = [](const std::string & text) + { + std::vector bytes; + for (const char c : text) + { + bytes.push_back(static_cast(c)); + } + return bytes; + }; + + SECTION("valid text") + { + const json j = json::parse(to_bytes("[1,\n2]")); + CHECK(j == json({1, 2})); + CHECK(json::accept(to_bytes("[1,\n2]"))); + } + + SECTION("invalid text") + { + CHECK_FALSE(json::accept(to_bytes("[1,\n2,"))); + CHECK_THROWS_WITH_AS(json::parse(to_bytes("[1,\n2,")), + "[json.exception.parse_error.101] parse error at line 2, column 3: syntax error while parsing value - unexpected end of input; expected '[', '{', or a literal", json::parse_error&); + } +} #endif diff --git a/tests/src/unit-ordered_json.cpp b/tests/src/unit-ordered_json.cpp index c328d8aea..2c72b3e8c 100644 --- a/tests/src/unit-ordered_json.cpp +++ b/tests/src/unit-ordered_json.cpp @@ -16,6 +16,7 @@ using nlohmann::ordered_json; #include #include #include +#include TEST_CASE("ordered_json") { @@ -515,3 +516,87 @@ TEST_CASE("parsing random trees of arrays and objects") CHECK(json::parse(text).dump() == expected.dump()); } } + +TEST_CASE("object_key_index") +{ + // the hash table of the DOM parser finds the position of a key in an + // ordered_json object; the parser creates it for exactly 64 elements, but + // it is correct for any number of elements it is given + using nlohmann::detail::object_key_index; + const std::size_t not_found = object_key_index::npos; + + for (const std::size_t initial : + { + std::size_t{0}, std::size_t{10}, std::size_t{64}, std::size_t{65}, std::size_t{300} + }) + { + CAPTURE(initial) + + // the hashes are not distinct on purpose: elements with the same + // hash are told apart by the caller + const auto hash_of = [](std::size_t position) + { + return (position % 50) * 0x9E3779B97F4A7C15ull; + }; + + std::vector hashes; + for (std::size_t i = 0; i < initial; ++i) + { + hashes.push_back(hash_of(i)); + } + object_key_index index(std::move(hashes)); + + // the elements that are there + for (std::size_t i = 0; i < initial; ++i) + { + CHECK(index.find(hash_of(i), [i](std::size_t position) + { + return position == i; + }) == i); + } + CHECK(index.find(hash_of(1), [](std::size_t /*position*/) + { + return false; + }) == not_found); + + // elements that are added later, until the table grows + for (std::size_t i = initial; i < initial + 600; ++i) + { + index.add(hash_of(i)); + CHECK(index.find(hash_of(i), [i](std::size_t position) + { + return position == i; + }) == i); + } + CHECK(index.find(hash_of(initial), [initial](std::size_t position) + { + return position == initial; + }) == initial); + } +} + +TEST_CASE("JSON pointers on ordered_json") +{ + using pointer = ordered_json::json_pointer; + + SECTION("operator[] inserts a missing key at the end") + { + ordered_json j = {{"z", 1}, {"x", {{"y", 2}}}}; + + j[pointer("/a")] = 3; + j[pointer("/x/b")] = 4; + + CHECK(j == ordered_json({{"z", 1}, {"x", {{"y", 2}, {"b", 4}}}, {"a", 3}})); + CHECK(j.dump() == R"({"z":1,"x":{"y":2,"b":4},"a":3})"); + } + + SECTION("unflatten of a flattened object whose keys are not sorted") + { + // the prefixes /a and /c denote arrays; a key of /a follows one of /c + const ordered_json flat = {{"/a/0/p", 1}, {"/c/0/x", 2}, {"/a/0/q", 3}, {"/a/1/p", 4}}; + const ordered_json expected = {{"a", {{{"p", 1}, {"q", 3}}, {{"p", 4}}}}, {"c", {{{"x", 2}}}}}; + + CHECK(flat.unflatten() == expected); + CHECK(expected.flatten().unflatten() == expected); + } +} diff --git a/tests/src/unit-regression1.cpp b/tests/src/unit-regression1.cpp index b98f2b6f3..e62b5c58b 100644 --- a/tests/src/unit-regression1.cpp +++ b/tests/src/unit-regression1.cpp @@ -23,11 +23,13 @@ using nlohmann::json; using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) #endif +#include #include #include #include #include #include +#include #include "make_test_data_available.hpp" #include "test_utils.hpp" @@ -1519,6 +1521,67 @@ TEST_CASE("regression tests 1") } } + SECTION("operator[](json_pointer) continues with the member that emplace() found") + { + // see the section above: the lookup does not find "k", but emplace() + // finds "x" and the pointer continues with the member of "x". Here + // the member is a container, so the pointer can go on below it. Only + // the outermost object may be a copy (with restarted timestamps): the + // value of "x" is moved in, so that its own lookups work. + const my_json orig = my_json::parse(R"({"x":null})"); + const auto make = [&orig]() + { + my_json::object_t top = orig.get_ref(); + top.at("x") = my_json::parse(R"({"a":{"b":1},"n":null,"arr":[10,20],"objs":[{"m":5},{"m":6}]})"); + return my_json(std::move(top)); + }; + + SECTION("existing members") + { + // emplace() registers the key "k" in the object, so that every + // pointer needs a new object to reach the member of "x" again + const std::array, 4> pointers = {{{"/k/a/b", "1"}, {"/k/arr/1", "20"}, {"/k/n", "null"}, {"/k/objs/1/m", "6"}}}; + for (const auto& pointer : pointers) + { + CAPTURE(pointer.first) + my_json j = make(); + CHECK(j[my_json::json_pointer(pointer.first)].dump() == pointer.second); + CHECK(j.size() == 1); + CHECK(j["x"].size() == 4); + } + } + + SECTION("a missing member of an object") + { + // the timestamp of "c" is that of "b" again, so that emplace() + // finds the member "b", a number, which has no member "d" + my_json j = make(); + CHECK_THROWS_AS(j[my_json::json_pointer("/k/a/c/d")], my_json::out_of_range&); + CHECK(j["x"]["a"].dump() == R"({"b":1})"); + } + + SECTION("a null value is converted to an object") + { + my_json j = make(); + j[my_json::json_pointer("/k/n/c")] = 3; + CHECK(j["x"]["n"].dump() == R"({"c":3})"); + } + + SECTION("the end of an array") + { + my_json j = make(); + j[my_json::json_pointer("/k/arr/-")] = 30; + CHECK(j["x"]["arr"].dump() == "[10,20,30]"); + } + + SECTION("an index past the end of an array") + { + my_json j = make(); + j[my_json::json_pointer("/k/arr/3")] = 40; + CHECK(j["x"]["arr"].dump() == "[10,20,null,40]"); + } + } + SECTION("issue #977 - Assigning between different json types") { foo_json lj = ns::foo{3}; diff --git a/tests/src/unit-regression2.cpp b/tests/src/unit-regression2.cpp index c7566c799..08d342d0a 100644 --- a/tests/src/unit-regression2.cpp +++ b/tests/src/unit-regression2.cpp @@ -441,6 +441,10 @@ TEST_CASE("regression tests 2") float_json j2 = {1000.0, 2000.0, 3000.0}; CHECK(float_json::from_ubjson(float_json::to_ubjson(j2, true, true)) == j2); + + // half-precision floats of both signs are converted to the float type + CHECK(float_json::from_cbor(std::vector {0xF9, 0xC0, 0x00}) == -2.0f); + CHECK(float_json::from_cbor(std::vector {0xF9, 0x3C, 0x00}) == 1.0f); } SECTION("issue #1045 - Using STL algorithms with JSON containers with expected results?") diff --git a/tests/src/unit-stream-bulk-scan.cpp b/tests/src/unit-stream-bulk-scan.cpp index d61ce4bd0..c1f162466 100644 --- a/tests/src/unit-stream-bulk-scan.cpp +++ b/tests/src/unit-stream-bulk-scan.cpp @@ -19,11 +19,13 @@ using nlohmann::json; #include +#include #include #include #include #include #include +#include #include #include #include @@ -93,6 +95,25 @@ class chunked_streambuf : public std::streambuf std::size_t m_unbuffered; }; +#if TEST_STREAM_BULK_SCAN +// A streambuf that claims a get area of several gigabytes, of which only the +// first bytes exist. It is only used to move the read position of the adapter, +// never to read beyond them, so that skipping more than gbump() can bump at +// once can be tested without allocating that much. +class oversized_get_area_streambuf : public std::streambuf +{ + public: + explicit oversized_get_area_streambuf(std::size_t claimed_size) + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) + setg(m_data.data(), m_data.data(), m_data.data() + claimed_size); + } + + private: + std::array m_data{{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}}; +}; +#endif + // how a test reads from the stream enum class read_mode { @@ -433,6 +454,31 @@ TEST_CASE("parsing from a std::istream scans the get area in bulk") ia.copy_token(token, false); CHECK(token.empty()); } + + SECTION("skipping more characters than gbump() takes at once") + { + // gbump() takes an int, but a get area can be larger + if (sizeof(std::size_t) < 8) + { + MESSAGE("a get area larger than an int needs a 64-bit size_t"); + } + else + { + const std::size_t claimed = std::size_t{3} << 30u; + oversized_get_area_streambuf buf(claimed); + std::istream is(&buf); + nlohmann::detail::input_stream_adapter ia(is); + CHECK(ia.get_character() == 'a'); + CHECK(ia.bulk_remaining() == claimed - 1); + + const std::size_t skipped = static_cast((std::numeric_limits::max)()) + 10; + ia.bulk_skip(skipped); + CHECK(ia.bulk_remaining() == claimed - 1 - skipped); + + ia.bulk_skip(ia.bulk_remaining()); + CHECK(ia.bulk_remaining() == 0); + } + } #endif SECTION("SAX parsing from a stream") diff --git a/tests/src/unit-to_chars.cpp b/tests/src/unit-to_chars.cpp index f8863a5a9..3a5a8841d 100644 --- a/tests/src/unit-to_chars.cpp +++ b/tests/src/unit-to_chars.cpp @@ -928,3 +928,49 @@ TEST_CASE("short decimals") } } } + +TEST_CASE("decimals of up to 17 digits") +{ + // write_decimal() lays out digits * 10^exp as format_buffer() does; to_chars() + // calls it for the doubles whose digits write_shortest() does not convert at + // once, and the digits may be as many as 17 + const auto written = [](std::uint64_t digits, int exp) -> std::string + { + std::array buf{}; // (up to 41 bytes are written) + char* const end = nlohmann::detail::dtoa_impl::write_decimal(buf.data(), digits, exp); + return {buf.data(), end}; + }; + + SECTION("fixed notation") + { + CHECK(written(1, 0) == "1.0"); + CHECK(written(1234, -2) == "12.34"); + CHECK(written(123, 2) == "12300.0"); + CHECK(written(50000000000u, -1) == "5000000000.0"); + CHECK(written(100000000u, -8) == "1.0"); + CHECK(written(12345678u, -12) == "1.2345678e-05"); + CHECK(written(1234, 11) == "123400000000000.0"); + // the point moves left of the first digit + CHECK(written(5, -3) == "0.005"); + CHECK(written(12345678901234567u, -20) == "0.00012345678901234567"); + } + + SECTION("exponent notation") + { + CHECK(written(5, -5) == "5e-05"); + CHECK(written(1234, 12) == "1.234e+15"); + CHECK(written(5, -324) == "5e-324"); + } + + SECTION("17 digits") + { + CHECK(written(12345678901234567u, -16) == "1.2345678901234567"); + CHECK(written(12345678901234567u, -19) == "0.0012345678901234567"); + CHECK(written(12345678901234567u, 5) == "1.2345678901234567e+21"); + CHECK(written(99999999999999999u, -17) == "0.99999999999999999"); + CHECK(written(10000000000000001u, -16) == "1.0000000000000001"); + // trailing zeros are not written + CHECK(written(10000000000000000u, -17) == "0.1"); + CHECK(written(12000000000000000u, -15) == "12.0"); + } +} diff --git a/tests/src/unit-ubjson.cpp b/tests/src/unit-ubjson.cpp index 04983da1c..4bc7f7ac0 100644 --- a/tests/src/unit-ubjson.cpp +++ b/tests/src/unit-ubjson.cpp @@ -1673,6 +1673,31 @@ TEST_CASE("UBJSON") return true; }); CHECK_THROWS_AS(scp2.start_array((std::numeric_limits::max)() - 1), json::out_of_range&); + + // without exceptions, the error is reported to the caller + json l; + nlohmann::detail::json_sax_dom_callback_parser scp3(l, [](int /*unused*/, json::parse_event_t /*unused*/, const json& /*unused*/) noexcept + { + return true; + }, false); + CHECK_FALSE(scp3.start_array((std::numeric_limits::max)() - 1)); + CHECK(scp3.is_errored()); + } + + SECTION("array with a known size, read without exceptions") + { + std::vector const v_ubjson = {'[', '#', 'i', 2, 'i', 1, 'i', 2}; + json j; + nlohmann::detail::json_sax_dom_parser sax(j, false); + CHECK(json::sax_parse(v_ubjson, &sax, 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 + json k; + nlohmann::detail::json_sax_dom_parser sax2(k, false); + CHECK_FALSE(sax2.start_array((std::numeric_limits::max)() - 1)); + CHECK(sax2.is_errored()); } } }