| // __ _____ _____ _____ |
| // __| | __| | | | JSON for Modern C++ (supporting code) |
| // | | |__ | | | | | | version 3.12.0 |
| // |_____|_____|_____|_|___| https://github.com/nlohmann/json |
| // |
| // SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me> |
| // SPDX-License-Identifier: MIT |
| |
| #include "doctest_compatibility.h" |
| |
| // capture whether JSON_DELETE_DEPRECATED_FUNCTIONS was enabled on the command |
| // line *before* including json.hpp, since the library #undefs it once the header |
| // has been fully processed (see include/nlohmann/detail/macro_unscope.hpp); the |
| // tests of deprecated functions are skipped if these functions are deleted |
| #if defined(JSON_DELETE_DEPRECATED_FUNCTIONS) && (JSON_DELETE_DEPRECATED_FUNCTIONS == 1) |
| #define JSON_TEST_DEPRECATED_FUNCTIONS_DELETED |
| #endif |
| |
| #include <nlohmann/json.hpp> |
| using nlohmann::json; |
| #ifdef JSON_TEST_NO_GLOBAL_UDLS |
| using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) |
| #endif |
| |
| #include <cmath> |
| #include <fstream> |
| #include <limits> |
| #include <sstream> |
| #include <string> |
| #include <vector> |
| #include "make_test_data_available.hpp" |
| #include "round_trip_corpus.hpp" |
| #include "test_utils.hpp" |
| #include "sax_countdown.hpp" |
| using utils::SaxCountdown; |
| |
| namespace |
| { |
| using bytes = std::vector<std::uint8_t>; |
| |
| /// @return the string with the given bytes |
| std::string str(const bytes& b) |
| { |
| // converted one by one: constructing the string from the byte range |
| // converts implicitly, which -fsanitize=integer reports for bytes >= 0x80 |
| std::string result; |
| result.reserve(b.size()); |
| for (const auto c : b) |
| { |
| result.push_back(static_cast<char>(c)); |
| } |
| return result; |
| } |
| |
| /// check that @a j is serialized to @a expected and that @a expected is read back as @a j |
| void check_bon8(const json& j, const bytes& expected) |
| { |
| CAPTURE(j) |
| CHECK(json::to_bon8(j) == expected); |
| |
| const json decoded = json::from_bon8(expected); |
| CHECK(decoded == j); |
| // integers are not read back as floats and vice versa |
| CHECK(decoded.type() == j.type()); |
| |
| // a stream is read byte by byte rather than in bulk |
| std::istringstream stream(str(expected)); |
| CHECK(json::from_bon8(stream) == decoded); |
| } |
| |
| /// @return @a b followed by @a tail |
| bytes concat(bytes b, const bytes& tail) |
| { |
| b.insert(b.end(), tail.begin(), tail.end()); |
| return b; |
| } |
| } // namespace |
| |
| TEST_CASE("BON8") |
| { |
| SECTION("individual values") |
| { |
| SECTION("discarded") |
| { |
| // discarded values are not serialized |
| const json j = json::value_t::discarded; |
| CHECK(json::to_bon8(j).empty()); |
| } |
| |
| SECTION("null") |
| { |
| check_bon8(nullptr, {0xFA}); |
| } |
| |
| SECTION("boolean") |
| { |
| check_bon8(true, {0xF9}); |
| check_bon8(false, {0xF8}); |
| } |
| |
| SECTION("integers") |
| { |
| // test vectors from HikoGUI (src/hikogui/codec/BON8_tests.cpp, |
| // Copyright Take Vos 2021, Boost Software License 1.0), which |
| // cover the first and last value of each encoding |
| SECTION("positive") |
| { |
| check_bon8(0u, {0x90}); |
| check_bon8(39u, {0xB7}); |
| check_bon8(40u, {0xC2, 0x00}); |
| check_bon8(3879u, {0xDF, 0x7F}); |
| check_bon8(3880u, {0xE0, 0x00, 0x00}); |
| check_bon8(528167u, {0xEF, 0x7F, 0xFF}); |
| check_bon8(528168u, {0xF0, 0x00, 0x00, 0x00}); |
| check_bon8(67637031u, {0xF7, 0x7F, 0xFF, 0xFF}); |
| check_bon8(67637032u, {0x8C, 0x04, 0x08, 0x0F, 0x28}); |
| check_bon8(2147483647u, {0x8C, 0x7F, 0xFF, 0xFF, 0xFF}); |
| check_bon8(2147483648u, {0x8D, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00}); |
| check_bon8(9223372036854775807u, {0x8D, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); |
| } |
| |
| SECTION("negative") |
| { |
| check_bon8(-1, {0xB8}); |
| check_bon8(-10, {0xC1}); |
| check_bon8(-11, {0xC2, 0xC0}); |
| check_bon8(-1930, {0xDF, 0xFF}); |
| check_bon8(-1931, {0xE0, 0xC0, 0x00}); |
| check_bon8(-264074, {0xEF, 0xFF, 0xFF}); |
| check_bon8(-264075, {0xF0, 0xC0, 0x00, 0x00}); |
| check_bon8(-33818506, {0xF7, 0xFF, 0xFF, 0xFF}); |
| check_bon8(-33818507, {0x8C, 0xFD, 0xFB, 0xF8, 0x75}); |
| check_bon8(-2147483648LL, {0x8C, 0x80, 0x00, 0x00, 0x00}); |
| check_bon8(-2147483649LL, {0x8D, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF}); |
| check_bon8((std::numeric_limits<std::int64_t>::min)(), {0x8D, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); |
| } |
| |
| SECTION("values inside the ranges") |
| { |
| check_bon8(1u, {0x91}); |
| check_bon8(100u, {0xC2, 0x3C}); |
| check_bon8(1000u, {0xC9, 0x40}); |
| check_bon8(100000u, {0xE2, 0x77, 0x78}); |
| check_bon8(1000000u, {0xF0, 0x07, 0x33, 0x18}); |
| check_bon8(-9, {0xC0}); |
| check_bon8(-100, {0xC3, 0xD9}); |
| check_bon8(-100000, {0xE5, 0xFF, 0x15}); |
| check_bon8(-1000000, {0xF0, 0xCB, 0x3A, 0xB5}); |
| } |
| |
| SECTION("every integer from -300000 to 600000") |
| { |
| for (std::int64_t i = -300000; i <= 600000; ++i) |
| { |
| const json j = i; |
| const auto packed = json::to_bon8(j); |
| CHECK(packed.size() == (i >= -10 && i <= 39 ? 1u : i >= -1930 && i <= 3879 ? 2u : i >= -264074 && i <= 528167 ? 3u : 4u)); |
| const json decoded = json::from_bon8(packed); |
| if (decoded != j) |
| { |
| CAPTURE(i) |
| CHECK(decoded == j); |
| } |
| } |
| } |
| |
| SECTION("signed values are read back as unsigned when not negative") |
| { |
| const json j = json::from_bon8(json::to_bon8(json(static_cast<std::int64_t>(1000)))); |
| CHECK(j.is_number_unsigned()); |
| CHECK(j == 1000); |
| } |
| |
| SECTION("unsigned integers above int64") |
| { |
| json _; |
| const json j = 9223372036854775808u; |
| CHECK_THROWS_WITH_AS(_ = json::to_bon8(j), "[json.exception.out_of_range.407] integer number 9223372036854775808 cannot be represented by BON8 as it does not fit int64", json::out_of_range&); |
| } |
| } |
| |
| SECTION("floating-point numbers") |
| { |
| SECTION("one-byte values") |
| { |
| check_bon8(-1.0, {0xFB}); |
| check_bon8(0.0, {0xFC}); |
| check_bon8(1.0, {0xFD}); |
| } |
| |
| SECTION("binary32") |
| { |
| check_bon8(2.0, {0x8E, 0x40, 0x00, 0x00, 0x00}); |
| check_bon8(0.5, {0x8E, 0x3F, 0x00, 0x00, 0x00}); |
| check_bon8(-1.5, {0x8E, 0xBF, 0xC0, 0x00, 0x00}); |
| check_bon8(3.4028234663852886e38, {0x8E, 0x7F, 0x7F, 0xFF, 0xFF}); |
| } |
| |
| SECTION("binary64") |
| { |
| check_bon8(100000000.1, {0x8F, 0x41, 0x97, 0xD7, 0x84, 0x00, 0x66, 0x66, 0x66}); |
| check_bon8(3.14159, {0x8F, 0x40, 0x09, 0x21, 0xF9, 0xF0, 0x1B, 0x86, 0x6E}); |
| check_bon8(1e300, {0x8F, 0x7E, 0x37, 0xE4, 0x3C, 0x88, 0x00, 0x75, 0x9C}); |
| } |
| |
| SECTION("-0.0 is written as binary32") |
| { |
| const json j = -0.0; |
| CHECK(json::to_bon8(j) == bytes{0x8E, 0x80, 0x00, 0x00, 0x00}); |
| const json decoded = json::from_bon8(json::to_bon8(j)); |
| CHECK(decoded.get<double>() == 0.0); |
| CHECK(std::signbit(decoded.get<double>())); |
| } |
| |
| SECTION("infinity is written as binary32") |
| { |
| check_bon8(std::numeric_limits<double>::infinity(), {0x8E, 0x7F, 0x80, 0x00, 0x00}); |
| check_bon8(-std::numeric_limits<double>::infinity(), {0x8E, 0xFF, 0x80, 0x00, 0x00}); |
| } |
| |
| SECTION("NaN is written as binary32 0x7F800001") |
| { |
| const json j = std::numeric_limits<double>::quiet_NaN(); |
| CHECK(json::to_bon8(j) == bytes{0x8E, 0x7F, 0x80, 0x00, 0x01}); |
| CHECK(std::isnan(json::from_bon8(json::to_bon8(j)).get<double>())); |
| } |
| } |
| |
| SECTION("strings") |
| { |
| SECTION("empty string") |
| { |
| check_bon8("", {0xFF}); |
| } |
| |
| SECTION("ASCII") |
| { |
| check_bon8("a", {'a', 0xFF}); |
| check_bon8("This is a string.", concat(bytes{'T', 'h', 'i', 's', ' ', 'i', 's', ' ', 'a', ' ', 's', 't', 'r', 'i', 'n', 'g', '.'}, {0xFF})); |
| check_bon8(str({0x00}), {0x00, 0xFF}); |
| check_bon8(str({0x7F}), {0x7F, 0xFF}); |
| } |
| |
| SECTION("multi-byte UTF-8") |
| { |
| check_bon8("\xC2\xA3", {0xC2, 0xA3, 0xFF}); // U+00A3 |
| check_bon8("\xEF\xB8\xBB", {0xEF, 0xB8, 0xBB, 0xFF}); // U+FE3B |
| check_bon8("\xF0\x9F\x80\x84", {0xF0, 0x9F, 0x80, 0x84, 0xFF}); // U+1F004 |
| check_bon8("\xC2\x80", {0xC2, 0x80, 0xFF}); // U+0080 |
| check_bon8("\xDF\xBF", {0xDF, 0xBF, 0xFF}); // U+07FF |
| check_bon8("\xE0\xA0\x80", {0xE0, 0xA0, 0x80, 0xFF}); // U+0800 |
| check_bon8("\xED\x9F\xBF", {0xED, 0x9F, 0xBF, 0xFF}); // U+D7FF |
| check_bon8("\xEE\x80\x80", {0xEE, 0x80, 0x80, 0xFF}); // U+E000 |
| check_bon8("\xF0\x90\x80\x80", {0xF0, 0x90, 0x80, 0x80, 0xFF}); // U+10000 |
| check_bon8("\xF4\x8F\xBF\xBF", {0xF4, 0x8F, 0xBF, 0xBF, 0xFF}); // U+10FFFF |
| check_bon8("a\xC2\xA3" "b", {'a', 0xC2, 0xA3, 'b', 0xFF}); |
| } |
| |
| SECTION("invalid UTF-8 cannot be written") |
| { |
| json _; |
| CHECK_THROWS_WITH_AS(_ = json::to_bon8(str({0x80})), "[json.exception.type_error.316] invalid UTF-8 byte at index 0: 0x80", json::type_error&); |
| CHECK_THROWS_WITH_AS(_ = json::to_bon8(str({'a', 0xC0, 0x80})), "[json.exception.type_error.316] invalid UTF-8 byte at index 1: 0xC0", json::type_error&); |
| CHECK_THROWS_WITH_AS(_ = json::to_bon8(str({0xE0, 0x80, 0x80})), "[json.exception.type_error.316] invalid UTF-8 byte at index 0: 0xE0", json::type_error&); |
| CHECK_THROWS_WITH_AS(_ = json::to_bon8(str({0xED, 0xA0, 0x80})), "[json.exception.type_error.316] invalid UTF-8 byte at index 0: 0xED", json::type_error&); |
| CHECK_THROWS_WITH_AS(_ = json::to_bon8(str({0xF4, 0x90, 0x80, 0x80})), "[json.exception.type_error.316] invalid UTF-8 byte at index 0: 0xF4", json::type_error&); |
| CHECK_THROWS_WITH_AS(_ = json::to_bon8(str({0xF5, 0x80, 0x80, 0x80})), "[json.exception.type_error.316] invalid UTF-8 byte at index 0: 0xF5", json::type_error&); |
| CHECK_THROWS_WITH_AS(_ = json::to_bon8(str({0xC2, 'a'})), "[json.exception.type_error.316] invalid UTF-8 byte at index 0: 0xC2", json::type_error&); |
| CHECK_THROWS_WITH_AS(_ = json::to_bon8(str({'a', 0xE2, 0x82})), "[json.exception.type_error.316] invalid UTF-8 byte at index 1: 0xE2", json::type_error&); |
| CHECK_THROWS_WITH_AS(_ = json::to_bon8(json::object({{str({0xFF}), 1}})), "[json.exception.type_error.316] invalid UTF-8 byte at index 0: 0xFF", json::type_error&); |
| // after a run of ASCII characters that is checked 8 bytes at a time |
| CHECK_THROWS_WITH_AS(_ = json::to_bon8(std::string(17, 'a') + str({0xC0})), "[json.exception.type_error.316] invalid UTF-8 byte at index 17: 0xC0", json::type_error&); |
| CHECK_THROWS_WITH_AS(_ = json::to_bon8(std::string(8, 'a') + "\xC3\xA4" + std::string(8, 'a') + str({0xE2, 0x82})), "[json.exception.type_error.316] invalid UTF-8 byte at index 18: 0xE2", json::type_error&); |
| } |
| } |
| |
| SECTION("arrays") |
| { |
| check_bon8(json::array(), {0x80}); |
| check_bon8({false}, {0x81, 0xF8}); |
| check_bon8({false, nullptr}, {0x82, 0xF8, 0xFA}); |
| 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}); |
| check_bon8({{{1}}}, {0x81, 0x81, 0x81, 0x91}); |
| check_bon8({{{"foo"}}}, {0x81, 0x81, 0x81, 'f', 'o', 'o', 0xFF}); |
| check_bon8({{{""}}}, {0x81, 0x81, 0x81, 0xFF}); |
| |
| SECTION("large array") |
| { |
| json j = json::array(); |
| bytes expected = {0x85}; |
| for (int i = 0; i < 1000; ++i) |
| { |
| j.push_back(nullptr); |
| expected.push_back(0xFA); |
| } |
| expected.push_back(0xFE); |
| check_bon8(j, expected); |
| } |
| } |
| |
| SECTION("objects") |
| { |
| check_bon8(json::object(), {0x86}); |
| check_bon8({{"foo", nullptr}}, {0x87, 'f', 'o', 'o', 0xFA}); |
| check_bon8({{"", true}, {"foo", nullptr}}, {0x88, 0xFF, 0xF9, 'f', 'o', 'o', 0xFA}); |
| check_bon8({{"a", 1}, {"b", 2}, {"c", 3}}, {0x89, 'a', 0x91, 'b', 0x92, 'c', 0x93}); |
| check_bon8({{"a", 1}, {"b", 2}, {"c", 3}, {"d", 4}}, {0x8A, 'a', 0x91, 'b', 0x92, 'c', 0x93, 'd', 0x94}); |
| const json five = {{"one", 1}, {"two", 2}, {"three", 3}, {"four", 4}, {"five", 5}}; |
| check_bon8(five, {0x8B, 'f', 'i', 'v', 'e', 0x95, 'f', 'o', 'u', 'r', 0x94, 'o', 'n', 'e', 0x91, 't', 'h', 'r', 'e', 'e', 0x93, 't', 'w', 'o', 0x92, 0xFE}); |
| } |
| |
| SECTION("binary values are written as arrays of integers") |
| { |
| CHECK(json::to_bon8(json::binary({})) == bytes{0x80}); |
| CHECK(json::to_bon8(json::binary({0x00, 0x27, 0x28, 0xFF})) == bytes{0x84, 0x90, 0xB7, 0xC2, 0x00, 0xC3, 0x57}); |
| CHECK(json::to_bon8(json::binary({1, 2, 3, 4, 5}, 42)) == bytes{0x85, 0x91, 0x92, 0x93, 0x94, 0x95, 0xFE}); |
| CHECK(json::to_bon8({"a", json::binary({1})}) == bytes{0x82, 'a', 0x81, 0x91}); |
| CHECK(json::from_bon8(json::to_bon8(json::binary({1, 2, 3, 4, 5}))) == json({1, 2, 3, 4, 5})); |
| } |
| } |
| |
| SECTION("examples from the specification") |
| { |
| check_bon8("ab", {'a', 'b', 0xFF}); |
| check_bon8({"ab", "bc"}, {0x82, 'a', 'b', 0xFF, 'b', 'c', 0xFF}); |
| check_bon8({"a", "b", "c", "d", "e"}, {0x85, 'a', 0xFF, 'b', 0xFF, 'c', 0xFF, 'd', 0xFF, 'e', 0xFE}); |
| check_bon8({{"ab", 1}, {"bc", 2}}, {0x88, 'a', 'b', 0x91, 'b', 'c', 0x92}); |
| // the specification prints this example without the end-of-string |
| // markers after "b" and "c", which its own rules require: without |
| // them, the bytes read as the single string "bcd" |
| check_bon8({{"a", {"b", "c"}}, {"d", 1}}, {0x88, 'a', 0x82, 'b', 0xFF, 'c', 0xFF, 'd', 0x91}); |
| check_bon8({{"", 1}, {"a", 2}}, {0x88, 0xFF, 0x91, 'a', 0x92}); |
| } |
| |
| SECTION("examples from the discussion of the specification (#2980)") |
| { |
| check_bon8({{"a", ""}, {"c", "d"}}, {0x88, 'a', 0xFF, 0xFF, 'c', 0xFF, 'd', 0xFF}); |
| } |
| |
| SECTION("end of strings") |
| { |
| SECTION("a string ends at the first byte of an integer") |
| { |
| check_bon8({{"a", 100}}, {0x87, 'a', 0xC2, 0x3C}); |
| check_bon8({{"a", -9}}, {0x87, 'a', 0xC0}); |
| check_bon8({{"a", -10}}, {0x87, 'a', 0xC1}); |
| check_bon8({{"a", 5}}, {0x87, 'a', 0x95}); |
| check_bon8({{"a", 100000}}, {0x87, 'a', 0xE2, 0x77, 0x78}); |
| check_bon8({{"a", -1000000}}, {0x87, 'a', 0xF0, 0xCB, 0x3A, 0xB5}); |
| check_bon8({{"a", 67637032}}, {0x87, 'a', 0x8C, 0x04, 0x08, 0x0F, 0x28}); |
| check_bon8({{"a", 2147483648}}, {0x87, 'a', 0x8D, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00}); |
| check_bon8({"\xC2\xA3", 100}, {0x82, 0xC2, 0xA3, 0xC2, 0x3C}); |
| check_bon8({"\xF0\x9F\x80\x84", -1000000}, {0x82, 0xF0, 0x9F, 0x80, 0x84, 0xF0, 0xCB, 0x3A, 0xB5}); |
| } |
| |
| SECTION("a string ends at the first byte of other values") |
| { |
| check_bon8({"a", 2.0}, {0x82, 'a', 0x8E, 0x40, 0x00, 0x00, 0x00}); |
| check_bon8({"a", 0.1}, {0x82, 'a', 0x8F, 0x3F, 0xB9, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9A}); |
| check_bon8({"a", nullptr, "b", true, "c", false}, {0x85, 'a', 0xFA, 'b', 0xF9, 'c', 0xF8, 0xFE}); |
| check_bon8({"a", -1.0, "b", 0.0, "c", 1.0}, {0x85, 'a', 0xFB, 'b', 0xFC, 'c', 0xFD, 0xFE}); |
| check_bon8({"a", json::array()}, {0x82, 'a', 0x80}); |
| check_bon8({"a", json::object()}, {0x82, 'a', 0x86}); |
| check_bon8({{"a", {1, 2, 3, 4, 5}}}, {0x87, 'a', 0x85, 0x91, 0x92, 0x93, 0x94, 0x95, 0xFE}); |
| check_bon8({{"a", {{"b", 1}, {"c", 2}, {"d", 3}, {"e", 4}, {"f", 5}}}}, {0x87, 'a', 0x8B, 'b', 0x91, 'c', 0x92, 'd', 0x93, 'e', 0x94, 'f', 0x95, 0xFE}); |
| } |
| |
| SECTION("a string ends at the end of its container") |
| { |
| check_bon8({1, 2, 3, 4, "e"}, {0x85, 0x91, 0x92, 0x93, 0x94, 'e', 0xFE}); |
| check_bon8({{"a", 1}, {"b", 2}, {"c", 3}, {"d", 4}, {"e", "f"}}, {0x8B, 'a', 0x91, 'b', 0x92, 'c', 0x93, 'd', 0x94, 'e', 0xFF, 'f', 0xFE}); |
| check_bon8({{1, 2, 3, 4, "e"}, 1}, {0x82, 0x85, 0x91, 0x92, 0x93, 0x94, 'e', 0xFE, 0x91}); |
| check_bon8({{1, 2, 3, 4, "\xC2\xA3"}, 100}, {0x82, 0x85, 0x91, 0x92, 0x93, 0x94, 0xC2, 0xA3, 0xFE, 0xC2, 0x3C}); |
| } |
| |
| SECTION("a string that another string follows is terminated") |
| { |
| check_bon8({"s", "s"}, {0x82, 's', 0xFF, 's', 0xFF}); |
| check_bon8({"", "s"}, {0x82, 0xFF, 's', 0xFF}); |
| check_bon8({"s", ""}, {0x82, 's', 0xFF, 0xFF}); |
| check_bon8({"", ""}, {0x82, 0xFF, 0xFF}); |
| check_bon8({{"a", "b"}, {"c", "d"}}, {0x88, 'a', 0xFF, 'b', 0xFF, 'c', 0xFF, 'd', 0xFF}); |
| check_bon8({{"a", ""}}, {0x87, 'a', 0xFF, 0xFF}); |
| check_bon8({{"", ""}}, {0x87, 0xFF, 0xFF}); |
| } |
| |
| SECTION("a container that ends with a string and that a string follows") |
| { |
| check_bon8({{"a"}, "b"}, {0x82, 0x81, 'a', 0xFF, 'b', 0xFF}); |
| check_bon8({{{{"a"}}}, "b"}, {0x82, 0x81, 0x81, 0x81, 'a', 0xFF, 'b', 0xFF}); |
| check_bon8({{"a", {"x"}}, {"b", 1}}, {0x88, 'a', 0x81, 'x', 0xFF, 'b', 0x91}); |
| check_bon8({{"a", {{"b", "c"}}}, {"d", 1}}, {0x88, 'a', 0x87, 'b', 0xFF, 'c', 0xFF, 'd', 0x91}); |
| check_bon8({{"a"}, {"b"}}, {0x82, 0x81, 'a', 0x81, 'b', 0xFF}); |
| } |
| |
| SECTION("a string that ends the message is terminated") |
| { |
| check_bon8({"a"}, {0x81, 'a', 0xFF}); |
| check_bon8({{"a", "b"}}, {0x87, 'a', 0xFF, 'b', 0xFF}); |
| check_bon8({{{"a"}}}, {0x81, 0x81, 0x81, 'a', 0xFF}); |
| } |
| } |
| |
| SECTION("non-canonical input is accepted") |
| { |
| // an integer with a longer encoding than necessary |
| CHECK(json::from_bon8(bytes{0x8C, 0x00, 0x00, 0x00, 0x01}) == 1); |
| CHECK(json::from_bon8(bytes{0x8D, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) == -1); |
| // a float with a longer encoding than necessary |
| CHECK(json::from_bon8(bytes{0x8F, 0x3F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) == 1.0); |
| CHECK(json::from_bon8(bytes{0x8E, 0x00, 0x00, 0x00, 0x00}) == 0.0); |
| // an unsized container with few elements |
| CHECK(json::from_bon8(bytes{0x85, 0x91, 0x92, 0xFE}) == json({1, 2})); |
| CHECK(json::from_bon8(bytes{0x85, 0xFE}) == json::array()); |
| CHECK(json::from_bon8(bytes{0x8B, 'a', 0x91, 0xFE}) == json({{"a", 1}})); |
| // unsorted keys |
| CHECK(json::from_bon8(bytes{0x88, 'b', 0x91, 'a', 0x92}) == json({{"a", 2}, {"b", 1}})); |
| // an end-of-string marker that is not needed |
| CHECK(json::from_bon8(bytes{0x82, 'a', 0xFF, 0x91}) == json({"a", 1})); |
| CHECK(json::from_bon8(bytes{0x87, 'a', 0xFF, 0x91}) == json({{"a", 1}})); |
| } |
| |
| SECTION("types of values read") |
| { |
| CHECK(json::from_bon8(bytes{0x90}).is_number_unsigned()); |
| CHECK(json::from_bon8(bytes{0xC2, 0x00}).is_number_unsigned()); |
| CHECK(json::from_bon8(bytes{0x8C, 0x00, 0x00, 0x00, 0x00}).is_number_unsigned()); |
| CHECK(json::from_bon8(bytes{0xB8}).is_number_integer()); |
| CHECK(!json::from_bon8(bytes{0xB8}).is_number_unsigned()); |
| CHECK(json::from_bon8(bytes{0xC2, 0xC0}).is_number_integer()); |
| CHECK(json::from_bon8(bytes{0xFC}).is_number_float()); |
| CHECK(json::from_bon8(bytes{0x8E, 0x40, 0x00, 0x00, 0x00}).is_number_float()); |
| CHECK(json::from_bon8(bytes{0xFF}).is_string()); |
| } |
| |
| SECTION("errors") |
| { |
| json _; |
| |
| SECTION("empty input") |
| { |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes()), "[json.exception.parse_error.110] parse error at byte 1: syntax error while parsing BON8 value: unexpected end of input", json::parse_error&); |
| CHECK(json::from_bon8(bytes(), true, false).is_discarded()); |
| } |
| |
| SECTION("too short numbers") |
| { |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x8C}), "[json.exception.parse_error.110] parse error at byte 2: syntax error while parsing BON8 number: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x8C, 0x00, 0x00, 0x00}), "[json.exception.parse_error.110] parse error at byte 5: syntax error while parsing BON8 number: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x8D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}), "[json.exception.parse_error.110] parse error at byte 9: syntax error while parsing BON8 number: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x8E, 0x00}), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing BON8 number: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x8F, 0x00}), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing BON8 number: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xC2}), "[json.exception.parse_error.110] parse error at byte 2: syntax error while parsing BON8 number: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xE0, 0x00}), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing BON8 number: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xF0, 0x00, 0x00}), "[json.exception.parse_error.110] parse error at byte 4: syntax error while parsing BON8 number: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x82, 'a', 0xF0, 0xC0}), "[json.exception.parse_error.110] parse error at byte 5: syntax error while parsing BON8 number: unexpected end of input", json::parse_error&); |
| } |
| |
| SECTION("unterminated strings") |
| { |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{'a'}), "[json.exception.parse_error.110] parse error at byte 2: syntax error while parsing BON8 string: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x81, 'a', 'b'}), "[json.exception.parse_error.110] parse error at byte 4: syntax error while parsing BON8 string: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xF0, 0x9F, 0x80, 0x84}), "[json.exception.parse_error.110] parse error at byte 5: syntax error while parsing BON8 string: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xF0, 0x9F, 0x80}), "[json.exception.parse_error.110] parse error at byte 4: syntax error while parsing BON8 string: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x87, 'a'}), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing BON8 string: unexpected end of input", json::parse_error&); |
| } |
| |
| SECTION("input that ends after a UTF-8 lead byte") |
| { |
| // the lead byte begins either a character or an integer; both are |
| // incomplete, so the lead byte must not end the string before it |
| for (const bool strict : |
| { |
| true, false |
| }) |
| { |
| CAPTURE(strict) |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{'a', 0xC3}, strict), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing BON8 string: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x81, 'a', 0xC3}, strict), "[json.exception.parse_error.110] parse error at byte 4: syntax error while parsing BON8 string: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x81, 'a', 0xE2}, strict), "[json.exception.parse_error.110] parse error at byte 4: syntax error while parsing BON8 string: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x81, 'a', 0xF0}, strict), "[json.exception.parse_error.110] parse error at byte 4: syntax error while parsing BON8 string: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x81, 0xC3, 0xA9, 0xC3}, strict), "[json.exception.parse_error.110] parse error at byte 5: syntax error while parsing BON8 string: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x87, 'a', 0xC3}, strict), "[json.exception.parse_error.110] parse error at byte 4: syntax error while parsing BON8 string: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x87, 0xC3}, strict), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing BON8 key: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x88, 'a', 0x91, 0xE2}, strict), "[json.exception.parse_error.110] parse error at byte 5: syntax error while parsing BON8 key: unexpected end of input", json::parse_error&); |
| } |
| } |
| |
| SECTION("a message that is cut off is not read as a shorter value") |
| { |
| const json values = {"\xC3\xA9", "a\xE2\x82\xAC", "\xF0\x9F\x98\x80\xC3\xA9", {"a\xC3\xA9"}, {{"\xC3\xA9", "\xE2\x82\xAC"}}, {{"a", {"b\xC3\xA9", 1}}}}; |
| for (const auto& j : values) |
| { |
| const bytes message = json::to_bon8(j); |
| for (std::size_t length = 0; length < message.size(); ++length) |
| { |
| CAPTURE(j) |
| CAPTURE(length) |
| bytes prefix = message; |
| prefix.resize(length); |
| CHECK(json::from_bon8(prefix, false, false).is_discarded()); |
| // a stream is read byte by byte rather than in bulk |
| std::istringstream stream(str(prefix)); |
| CHECK(json::from_bon8(stream, false, false).is_discarded()); |
| } |
| } |
| } |
| |
| SECTION("invalid UTF-8") |
| { |
| // overlong |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xE0, 0x80, 0x80, 0xFF}), "[json.exception.parse_error.112] parse error at byte 2: syntax error while parsing BON8 string: invalid UTF-8 byte: 0x80", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xF0, 0x80, 0x80, 0x80, 0xFF}), "[json.exception.parse_error.112] parse error at byte 2: syntax error while parsing BON8 string: invalid UTF-8 byte: 0x80", json::parse_error&); |
| // surrogate |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xED, 0xA0, 0x80, 0xFF}), "[json.exception.parse_error.112] parse error at byte 2: syntax error while parsing BON8 string: invalid UTF-8 byte: 0xA0", json::parse_error&); |
| // above U+10FFFF |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xF4, 0x90, 0x80, 0x80, 0xFF}), "[json.exception.parse_error.112] parse error at byte 2: syntax error while parsing BON8 string: invalid UTF-8 byte: 0x90", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xF5, 0x80, 0x80, 0x80, 0xFF}), "[json.exception.parse_error.112] parse error at byte 2: syntax error while parsing BON8 string: invalid UTF-8 byte: 0x80", json::parse_error&); |
| // missing continuation byte |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xE2, 0x82, 'a', 0xFF}), "[json.exception.parse_error.112] parse error at byte 3: syntax error while parsing BON8 string: invalid UTF-8 byte: 0x61", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x81, 'a', 0xE2, 0x82, 'b'}), "[json.exception.parse_error.112] parse error at byte 5: syntax error while parsing BON8 string: invalid UTF-8 byte: 0x62", json::parse_error&); |
| } |
| |
| SECTION("end-of-container marker where a value is expected") |
| { |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0xFE}), "[json.exception.parse_error.112] parse error at byte 1: syntax error while parsing BON8 value: invalid byte: 0xFE", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x81, 0xFE}), "[json.exception.parse_error.112] parse error at byte 2: syntax error while parsing BON8 value: invalid byte: 0xFE", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x8B, 'a', 0xFE}), "[json.exception.parse_error.112] parse error at byte 3: syntax error while parsing BON8 value: invalid byte: 0xFE", json::parse_error&); |
| } |
| |
| SECTION("keys that are not strings") |
| { |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x87, 0x91, 0x91}), "[json.exception.parse_error.112] parse error at byte 2: syntax error while parsing BON8 key: expected a string; last byte: 0x91", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x87, 0xFA, 0x91}), "[json.exception.parse_error.112] parse error at byte 2: syntax error while parsing BON8 key: expected a string; last byte: 0xFA", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x87, 0x80, 0x91}), "[json.exception.parse_error.112] parse error at byte 2: syntax error while parsing BON8 key: expected a string; last byte: 0x80", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x87, 0xFE}), "[json.exception.parse_error.112] parse error at byte 2: syntax error while parsing BON8 key: expected a string; last byte: 0xFE", json::parse_error&); |
| // an integer that begins with a UTF-8 lead byte |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x87, 0xC2, 0x00, 0x91}), "[json.exception.parse_error.112] parse error at byte 2: syntax error while parsing BON8 key: expected a string; last byte: 0xC2", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x88, 'a', 0x91, 0xF0, 0xC0, 0x00, 0x00, 0x91}), "[json.exception.parse_error.112] parse error at byte 4: syntax error while parsing BON8 key: expected a string; last byte: 0xF0", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x87}), "[json.exception.parse_error.110] parse error at byte 2: syntax error while parsing BON8 key: unexpected end of input", json::parse_error&); |
| } |
| |
| SECTION("unterminated containers") |
| { |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x82, 0x91}), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing BON8 value: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x85, 0x91}), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing BON8 value: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x8B, 'a', 0x91}), "[json.exception.parse_error.110] parse error at byte 4: syntax error while parsing BON8 key: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x87, 'a'}), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing BON8 string: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(bytes{0x87, 'a', 0xFF}), "[json.exception.parse_error.110] parse error at byte 4: syntax error while parsing BON8 value: unexpected end of input", json::parse_error&); |
| } |
| |
| SECTION("strict mode") |
| { |
| const bytes vec = {0x90, 0x90}; |
| CHECK(json::from_bon8(vec, false) == 0); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(vec), "[json.exception.parse_error.110] parse error at byte 2: syntax error while parsing BON8 value: expected end of input; last byte: 0x90", json::parse_error&); |
| CHECK(json::from_bon8(vec, true, false).is_discarded()); |
| |
| // the byte after a string that ends a container |
| const bytes vec2 = {0x81, 'a', 0x90}; |
| CHECK(json::from_bon8(vec2, false) == json({"a"})); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(vec2), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing BON8 value: expected end of input; last byte: 0x90", json::parse_error&); |
| |
| const bytes vec3 = {0x81, 'a', 0xC2, 0x00}; |
| CHECK(json::from_bon8(vec3, false) == json({"a"})); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(vec3), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing BON8 value: expected end of input; last byte: 0xC2", json::parse_error&); |
| } |
| } |
| |
| SECTION("SAX aborts") |
| { |
| SECTION("start_array(len)") |
| { |
| const bytes v = {0x83, 0x91, 0x92, 0x93}; |
| SaxCountdown scp(0); |
| CHECK(!json::sax_parse(v, &scp, json::input_format_t::bon8)); |
| } |
| |
| SECTION("start_array()") |
| { |
| const bytes v = {0x85, 0x91, 0xFE}; |
| SaxCountdown scp(0); |
| CHECK(!json::sax_parse(v, &scp, json::input_format_t::bon8)); |
| } |
| |
| SECTION("end_array()") |
| { |
| const bytes v = {0x85, 0x91, 0xFE}; |
| SaxCountdown scp(2); |
| CHECK(!json::sax_parse(v, &scp, json::input_format_t::bon8)); |
| } |
| |
| SECTION("start_object(len)") |
| { |
| const bytes v = {0x87, 'f', 'o', 'o', 0xF8}; |
| SaxCountdown scp(0); |
| CHECK(!json::sax_parse(v, &scp, json::input_format_t::bon8)); |
| } |
| |
| SECTION("key()") |
| { |
| const bytes v = {0x87, 'f', 'o', 'o', 0xF8}; |
| SaxCountdown scp(1); |
| CHECK(!json::sax_parse(v, &scp, json::input_format_t::bon8)); |
| } |
| |
| SECTION("end_object()") |
| { |
| const bytes v = {0x87, 'f', 'o', 'o', 0xF8}; |
| SaxCountdown scp(3); |
| CHECK(!json::sax_parse(v, &scp, json::input_format_t::bon8)); |
| } |
| |
| SECTION("values") |
| { |
| const std::vector<bytes> values = |
| { |
| {0xFA}, {0xF9}, {0x91}, {0xB8}, {0xC2, 0x00}, {0xC2, 0xC0}, {0xE0, 0x00, 0x00}, |
| {0x8C, 0, 0, 0, 1}, {0x8D, 0, 0, 0, 0, 0, 0, 0, 1}, {0xFB}, {0xFC}, {0xFD}, |
| {0x8E, 0x40, 0, 0, 0}, {0x8F, 0x40, 0, 0, 0, 0, 0, 0, 0}, {'a', 0xFF}, {0xFF} |
| }; |
| for (const auto& v : values) |
| { |
| SaxCountdown scp(0); |
| CHECK(!json::sax_parse(v, &scp, json::input_format_t::bon8)); |
| } |
| } |
| } |
| } |
| |
| // the test catches the exceptions of invalid input |
| #if !defined(JSON_NOEXCEPTION) |
| TEST_CASE("BON8 strings from contiguous and stream input") |
| { |
| // contiguous input copies the valid UTF-8 of a string in bulk, a stream |
| // is read byte by byte; both must end strings and report errors alike |
| const std::string ascii(20, 'a'); |
| const std::vector<bytes> inputs = |
| { |
| // the string ends at 0xFF, at a marker, and at an integer |
| concat(concat(bytes(ascii.begin(), ascii.end()), {0xC3, 0xA4}), {0xFF}), |
| concat(concat({0x82}, bytes(ascii.begin(), ascii.end())), {0x91}), |
| concat(concat({0x85}, bytes(ascii.begin(), ascii.end())), {0xFE}), |
| concat(concat({0x82}, bytes(ascii.begin(), ascii.end())), {0xC2, 0x05}), |
| concat(concat({0x87}, bytes(ascii.begin(), ascii.end())), {0xE2, 0x82, 0xAC, 0xF0, 0x05, 0x00, 0x00}), |
| // invalid UTF-8 and a premature end after a run of valid characters |
| concat(bytes(ascii.begin(), ascii.end()), {0xE0, 0x80, 0x80}), |
| concat(bytes(ascii.begin(), ascii.end()), {0xE2, 0x82, 0x2F}), |
| concat(bytes(ascii.begin(), ascii.end()), {0xE2, 0x82}), |
| bytes(ascii.begin(), ascii.end()), |
| // trailing bytes after a string that ends a container |
| concat(concat({0x81}, bytes(ascii.begin(), ascii.end())), {0x91}), |
| }; |
| |
| for (const auto& input : inputs) |
| { |
| CAPTURE(input) |
| std::string from_vector; |
| std::string from_stream; |
| try |
| { |
| from_vector = json::from_bon8(input).dump(); |
| } |
| catch (const json::parse_error& e) |
| { |
| from_vector = e.what(); |
| } |
| try |
| { |
| std::istringstream stream(str(input)); |
| from_stream = json::from_bon8(stream).dump(); |
| } |
| catch (const json::parse_error& e) |
| { |
| from_stream = e.what(); |
| } |
| CHECK(from_vector == from_stream); |
| } |
| |
| json _; |
| CHECK(json::from_bon8(inputs[0]) == ascii + "\xC3\xA4"); |
| CHECK(json::from_bon8(inputs[3]) == json({ascii, 45})); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(inputs[5]), "[json.exception.parse_error.112] parse error at byte 22: syntax error while parsing BON8 string: invalid UTF-8 byte: 0x80", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(inputs[6]), "[json.exception.parse_error.112] parse error at byte 23: syntax error while parsing BON8 string: invalid UTF-8 byte: 0x2F", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(inputs[7]), "[json.exception.parse_error.110] parse error at byte 23: syntax error while parsing BON8 string: unexpected end of input", json::parse_error&); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(inputs[9]), "[json.exception.parse_error.110] parse error at byte 22: syntax error while parsing BON8 value: expected end of input; last byte: 0x91", json::parse_error&); |
| } |
| #endif |
| |
| // use this testcase outside [hide] to run it with Valgrind |
| TEST_CASE("BON8 nesting does not consume the call stack") |
| { |
| // Note that deeply nested values must not be compared, copied or dumped |
| // here: those operations are still recursive. Depth is measured by |
| // descending instead. |
| |
| SECTION("an unterminated chain is reported, not crashed on") |
| { |
| json _; |
| const bytes input(300000, 0x81); |
| CHECK_THROWS_WITH_AS(_ = json::from_bon8(input), "[json.exception.parse_error.110] parse error at byte 300001: syntax error while parsing BON8 value: unexpected end of input", json::parse_error&); |
| CHECK(json::from_bon8(input, true, false).is_discarded()); |
| } |
| |
| SECTION("a well-formed deep value is read through the SAX interface") |
| { |
| bytes input(300000, 0x85); |
| input.push_back(0x91); // innermost value |
| input.insert(input.end(), 300000, 0xFE); |
| |
| SaxCountdown accept_all(600001); |
| CHECK(json::sax_parse(input, &accept_all, json::input_format_t::bon8)); |
| } |
| |
| SECTION("a well-formed deep value is read into a value") |
| { |
| const std::size_t depth = 10000; |
| bytes input; |
| for (std::size_t i = 0; i < depth; ++i) |
| { |
| input.push_back(0x87); |
| input.push_back('a'); |
| } |
| // the innermost key is followed by a string, so it is terminated |
| input.push_back(0xFF); |
| input.push_back('b'); |
| input.push_back(0xFF); |
| |
| json j = json::from_bon8(input); |
| |
| std::size_t measured = 0; |
| const json* p = &j; |
| while (p->is_object() && !p->empty()) |
| { |
| p = &p->at("a"); |
| ++measured; |
| } |
| CHECK(measured == depth); |
| CHECK(*p == "b"); |
| } |
| } |
| |
| TEST_CASE("single BON8 roundtrip") |
| { |
| SECTION("sample.json") |
| { |
| std::string const filename = TEST_DATA_DIRECTORY "/json_testsuite/sample.json"; |
| |
| // parse JSON file |
| std::ifstream f_json(filename); |
| const json j1 = json::parse(f_json); |
| |
| // parse BON8 file |
| auto packed = utils::read_binary_file(filename + ".bon8"); |
| json j2; |
| CHECK_NOTHROW(j2 = json::from_bon8(packed)); |
| |
| // compare parsed JSON values |
| CHECK(j1 == j2); |
| |
| SECTION("roundtrips") |
| { |
| SECTION("std::ostringstream") |
| { |
| std::basic_ostringstream<char> ss; |
| json::to_bon8(j1, ss); |
| json j3 = json::from_bon8(ss.str()); |
| CHECK(j1 == j3); |
| } |
| |
| SECTION("std::string") |
| { |
| std::string s; |
| json::to_bon8(j1, s); |
| json j3 = json::from_bon8(s); |
| CHECK(j1 == j3); |
| } |
| } |
| |
| // check with different start index |
| packed.insert(packed.begin(), 5, 0xff); |
| CHECK(j1 == json::from_bon8(packed.begin() + 5, packed.end())); |
| } |
| } |
| |
| TEST_CASE("Parse BON8 directly from a file using iterator and sentinel") |
| { |
| std::string const filename = TEST_DATA_DIRECTORY "/json_testsuite/sample.json.bon8"; |
| std::ifstream file(filename, std::ios::binary); |
| const std::istreambuf_iterator<char> first(file); |
| const json parsed = json::from_bon8(first, utils::istreambuf_sentinel{}); |
| CHECK((parsed.is_object() || parsed.is_array())); |
| } |
| |
| #if !defined(JSON_NOEXCEPTION) // corpus values that do not survive the round trip are skipped by catching the exception |
| TEST_CASE("BON8 round-trip invariants") |
| { |
| // This checks what the parse_bon8_fuzzer driver checks (see |
| // tests/src/fuzzer-parse_bon8.cpp), so that a regression shows up in CI |
| // rather than as an OSS-Fuzz report: anything from_bon8() returns (j1) |
| // can be serialized, parsed back (j2), and serialized again to reproduce |
| // the exact bytes. The stream-versus-contiguous input check the driver |
| // also performs is not covered here (see #5601). |
| for (const auto& j0 : utils::round_trip_corpus::values()) |
| { |
| json j1; |
| try |
| { |
| // turn the corpus value into a value as from_bon8() returns it |
| j1 = json::from_bon8(json::to_bon8(j0)); |
| } |
| catch (const json::exception&) |
| { |
| // BON8 cannot represent an unsigned integer above INT64_MAX, and |
| // the fuzzer driver only ever sees values from_bon8() actually |
| // produced, so skip such corpus values here, too |
| continue; |
| } |
| |
| INFO("j1 = " << j1.dump()); |
| const std::vector<std::uint8_t> vec = json::to_bon8(j1); |
| json j2; |
| // anything the library writes must be parsable by the library |
| REQUIRE_NOTHROW(j2 = json::from_bon8(vec)); |
| CHECK(json::to_bon8(j2) == vec); |
| } |
| } |
| #endif |
| |
| TEST_CASE("BON8 roundtrips" * doctest::skip()) |
| { |
| SECTION("input from HikoGUI") |
| { |
| // The .bon8 files were created with the BON8 encoder of HikoGUI |
| // (https://github.com/hikoworks/hikogui), the reference implementation |
| // of the format. Both implementations sort object keys, so the output |
| // is compared byte for byte. |
| for (std::string filename : |
| { |
| TEST_DATA_DIRECTORY "/json_nlohmann_tests/all_unicode.json", |
| TEST_DATA_DIRECTORY "/json.org/1.json", |
| TEST_DATA_DIRECTORY "/json.org/2.json", |
| TEST_DATA_DIRECTORY "/json.org/3.json", |
| TEST_DATA_DIRECTORY "/json.org/4.json", |
| TEST_DATA_DIRECTORY "/json.org/5.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip01.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip02.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip03.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip04.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip05.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip06.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip07.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip08.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip09.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip10.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip11.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip12.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip13.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip14.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip15.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip16.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip17.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip18.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip19.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip20.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip21.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip22.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip23.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip24.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip25.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip26.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip27.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip28.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip29.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip30.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip31.json", |
| TEST_DATA_DIRECTORY "/json_roundtrip/roundtrip32.json", |
| TEST_DATA_DIRECTORY "/json_testsuite/sample.json", |
| TEST_DATA_DIRECTORY "/json_tests/pass1.json", |
| TEST_DATA_DIRECTORY "/json_tests/pass2.json", |
| TEST_DATA_DIRECTORY "/json_tests/pass3.json", |
| TEST_DATA_DIRECTORY "/regression/floats.json", |
| TEST_DATA_DIRECTORY "/regression/signed_ints.json", |
| TEST_DATA_DIRECTORY "/regression/working_file.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_array_arraysWithSpaces.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_array_empty-string.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_array_empty.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_array_ending_with_newline.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_array_false.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_array_heterogeneous.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_array_null.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_array_with_1_and_newline.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_array_with_leading_space.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_array_with_several_null.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_array_with_trailing_space.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_0e+1.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_0e1.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_after_space.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_double_close_to_zero.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_double_huge_neg_exp.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_int_with_exp.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_minus_zero.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_negative_int.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_negative_one.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_negative_zero.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_real_capital_e.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_real_capital_e_neg_exp.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_real_capital_e_pos_exp.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_real_exponent.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_real_fraction_exponent.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_real_neg_exp.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_real_pos_exponent.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_real_underflow.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_simple_int.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_simple_real.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_too_big_neg_int.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_too_big_pos_int.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_number_very_big_negative_int.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object_basic.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object_duplicated_key.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object_duplicated_key_and_value.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object_empty.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object_empty_key.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object_escaped_null_in_key.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object_extreme_numbers.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object_long_strings.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object_simple.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object_string_unicode.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_object_with_newlines.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_1_2_3_bytes_UTF-8_sequences.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_UTF-16_Surrogates_U+1D11E_MUSICAL_SYMBOL_G_CLEF.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_accepted_surrogate_pair.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_accepted_surrogate_pairs.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_allowed_escapes.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_backslash_and_u_escaped_zero.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_backslash_doublequotes.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_comments.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_double_escape_a.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_double_escape_n.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_escaped_control_character.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_escaped_noncharacter.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_in_array.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_in_array_with_leading_space.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_last_surrogates_1_and_2.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_newline_uescaped.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_nonCharacterInUTF-8_U+10FFFF.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_nonCharacterInUTF-8_U+1FFFF.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_nonCharacterInUTF-8_U+FFFF.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_null_escape.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_one-byte-utf-8.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_pi.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_simple_ascii.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_space.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_three-byte-utf-8.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_two-byte-utf-8.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_u+2028_line_sep.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_u+2029_par_sep.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_uEscape.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_unescaped_char_delete.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_unicode.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_unicodeEscapedBackslash.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_unicode_2.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_unicode_U+200B_ZERO_WIDTH_SPACE.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_unicode_U+2064_invisible_plus.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_unicode_escaped_double_quote.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_utf8.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_string_with_del_character.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_structure_lonely_false.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_structure_lonely_int.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_structure_lonely_negative_real.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_structure_lonely_null.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_structure_lonely_string.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_structure_lonely_true.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_structure_string_empty.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_structure_trailing_newline.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_structure_true_in_array.json", |
| TEST_DATA_DIRECTORY "/nst_json_testsuite/test_parsing/y_structure_whitespace_array.json" |
| }) |
| { |
| CAPTURE(filename) |
| |
| // parse JSON file |
| std::ifstream f_json(filename); |
| const json j1 = json::parse(f_json); |
| |
| // parse BON8 file |
| const auto packed = utils::read_binary_file(filename + ".bon8"); |
| |
| { |
| INFO_WITH_TEMP(filename + ": std::vector<uint8_t>"); |
| json j2; |
| CHECK_NOTHROW(j2 = json::from_bon8(packed)); |
| CHECK(j1 == j2); |
| } |
| |
| { |
| INFO_WITH_TEMP(filename + ": std::ifstream"); |
| std::ifstream f_bon8(filename + ".bon8", std::ios::binary); |
| json j2; |
| CHECK_NOTHROW(j2 = json::from_bon8(f_bon8)); |
| CHECK(j1 == j2); |
| } |
| |
| { |
| INFO_WITH_TEMP(filename + ": iterator pair"); |
| json j2; |
| CHECK_NOTHROW(j2 = json::from_bon8(packed.begin(), packed.end())); |
| CHECK(j1 == j2); |
| } |
| |
| { |
| INFO_WITH_TEMP(filename + ": output adapters: std::vector<uint8_t>"); |
| std::vector<uint8_t> vec; |
| json::to_bon8(j1, vec); |
| CHECK(vec == packed); |
| } |
| } |
| } |
| } |
| |
| TEST_CASE("issue #5648 - from_bon8(ptr, len) must read len bytes, not treat ptr as a C string") |
| { |
| // to_bon8() encodes the integer 40 as the two bytes 0xC2 0x00 (a BON8 |
| // lead byte followed by a continuation byte of 0x00), so the packed data |
| // below contains a 0x00 byte before its end. |
| const json j = {{"a", 40}, {"b", "x"}}; |
| const std::vector<std::uint8_t> packed = json::to_bon8(j); |
| bool contains_nul = false; |
| for (const auto byte : packed) |
| { |
| contains_nul |= (byte == 0x00); |
| } |
| REQUIRE(contains_nul); |
| |
| #ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED |
| // before the fix, from_bon8 had no (ptr, len) overload, so this call |
| // bound to from_bon8(InputType&&, bool strict) instead: ptr was read as |
| // a NUL-terminated C string (stopping at the embedded 0x00 byte), and |
| // len was silently converted to the strict flag. The deprecated |
| // overload added for this issue forwards to from_bon8(ptr, ptr + len, |
| // ...) instead, like from_cbor's deprecated (ptr, len) overload does. |
| json result; |
| CHECK_NOTHROW(result = json::from_bon8(packed.data(), packed.size())); |
| CHECK(result == j); |
| |
| // len must not collapse into the strict flag either |
| CHECK(json::from_bon8(packed.data(), packed.size(), false) == j); |
| #endif |
| } |
| |
| #ifdef JSON_HAS_CPP_17 |
| TEST_CASE("BON8 with std::byte") |
| { |
| SECTION("vector roundtrip") |
| { |
| const json original = |
| { |
| {"name", "test"}, |
| {"value", 42}, |
| {"array", {1, 2, 3}} |
| }; |
| |
| const std::vector<uint8_t> temp = json::to_bon8(original); |
| std::vector<std::byte> bon8_data(temp.size()); |
| for (size_t i = 0; i < temp.size(); ++i) |
| { |
| bon8_data[i] = std::byte(temp[i]); |
| } |
| |
| json from_bytes; |
| CHECK_NOTHROW(from_bytes = json::from_bon8(bon8_data)); |
| CHECK(from_bytes == original); |
| } |
| |
| SECTION("empty vector") |
| { |
| const std::vector<std::byte> empty_data; |
| CHECK_THROWS_WITH_AS([&]() |
| { |
| [[maybe_unused]] auto result = json::from_bon8(empty_data); |
| return true; |
| } |
| (), |
| "[json.exception.parse_error.110] parse error at byte 1: syntax error while parsing BON8 value: unexpected end of input", |
| json::parse_error&); |
| } |
| } |
| #endif |