diff --git a/src/json.hpp b/src/json.hpp index 7f31377f0..41a85eaa8 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -468,16 +468,8 @@ struct external_constructor template static void construct(BasicJsonType& j, typename BasicJsonType::number_float_t val) noexcept { - // replace infinity and NAN by null - if (not std::isfinite(val)) - { - j = BasicJsonType{}; - } - else - { - j.m_type = value_t::number_float; - j.m_value = val; - } + j.m_type = value_t::number_float; + j.m_value = val; j.assert_invariant(); } }; @@ -904,22 +896,15 @@ void from_json(const BasicJsonType& j, typename BasicJsonType::array_t& arr) } // forward_list doesn't have an insert method -template +template::value, int> = 0> void from_json(const BasicJsonType& j, std::forward_list& l) { - // do not perform the check when user wants to retrieve jsons - // (except when it's null.. ?) - if (j.is_null()) + if (not j.is_array()) { JSON_THROW(type_error(302, "type must be array, but is " + j.type_name())); } - if (not std::is_same::value) - { - if (not j.is_array()) - { - JSON_THROW(type_error(302, "type must be array, but is " + j.type_name())); - } - } + for (auto it = j.rbegin(), end = j.rend(); it != end; ++it) { l.push_front(it->template get()); @@ -951,8 +936,8 @@ auto from_json_array_impl(const BasicJsonType& j, CompatibleArrayType& arr, prio using std::end; arr.reserve(j.size()); - std::transform( - j.begin(), j.end(), std::inserter(arr, end(arr)), [](const BasicJsonType & i) + std::transform(j.begin(), j.end(), + std::inserter(arr, end(arr)), [](const BasicJsonType & i) { // get() returns *this, this won't call a from_json // method when value_type is BasicJsonType @@ -962,22 +947,15 @@ auto from_json_array_impl(const BasicJsonType& j, CompatibleArrayType& arr, prio template::value and + std::is_convertible::value and not std::is_same::value, int> = 0> void from_json(const BasicJsonType& j, CompatibleArrayType& arr) { - if (j.is_null()) + if (not j.is_array()) { JSON_THROW(type_error(302, "type must be array, but is " + j.type_name())); } - // when T == BasicJsonType, do not check if value_t is correct - if (not std::is_same::value) - { - if (not j.is_array()) - { - JSON_THROW(type_error(302, "type must be array, but is " + j.type_name())); - } - } from_json_array_impl(j, arr, priority_tag<1> {}); } @@ -6916,6 +6894,13 @@ class basic_json */ void dump_float(number_float_t x) { + // NaN / inf + if (not std::isfinite(x) or std::isnan(x)) + { + o.write("null", 4); + return; + } + // special case for 0.0 and -0.0 if (x == 0) { @@ -8020,6 +8005,35 @@ class basic_json } } + /*! + @brief check if the next byte belongs to a string + + While parsing a map, the keys must be strings. This function checks if the + current byte is one of the start bytes for a string in MessagePack: + + - 0xa0 - 0xbf: fixstr + - 0xd9: str 8 + - 0xda: str 16 + - 0xdb: str 32 + + @param[in] v MessagePack serialization + @param[in] idx byte index in @a v to check for a string + + @throw std::invalid_argument if `v[idx]` does not belong to a string + */ + static void msgpack_expect_string(const std::vector& v, size_t idx) + { + check_length(v.size(), 1, idx); + + const auto byte = v[idx]; + if ((byte >= 0xa0 and byte <= 0xbf) or (byte >= 0xd9 and byte <= 0xdb)) + { + return; + } + + JSON_THROW(std::invalid_argument("error parsing a msgpack string @ " + std::to_string(idx) + ": " + std::to_string(static_cast(v[idx])))); + } + /*! @brief create a JSON value from a given MessagePack vector @@ -8054,6 +8068,7 @@ class basic_json const size_t len = v[current_idx] & 0x0f; for (size_t i = 0; i < len; ++i) { + msgpack_expect_string(v, idx); std::string key = from_msgpack_internal(v, idx); result[key] = from_msgpack_internal(v, idx); } @@ -8233,6 +8248,7 @@ class basic_json idx += 2; // skip 2 size bytes for (size_t i = 0; i < len; ++i) { + msgpack_expect_string(v, idx); std::string key = from_msgpack_internal(v, idx); result[key] = from_msgpack_internal(v, idx); } @@ -8246,6 +8262,7 @@ class basic_json idx += 4; // skip 4 size bytes for (size_t i = 0; i < len; ++i) { + msgpack_expect_string(v, idx); std::string key = from_msgpack_internal(v, idx); result[key] = from_msgpack_internal(v, idx); } @@ -11697,11 +11714,10 @@ basic_json_parser_74: result.m_type = value_t::number_float; result.m_value = val; - // replace infinity and NAN by null + // throw in case of infinity or NAN if (not std::isfinite(result.m_value.number_float)) { - result.m_type = value_t::null; - result.m_value = basic_json::json_value(); + JSON_THROW(std::out_of_range("number overflow: " + get_token_string())); } return true; diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index 48111521e..711dd6889 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -468,16 +468,8 @@ struct external_constructor template static void construct(BasicJsonType& j, typename BasicJsonType::number_float_t val) noexcept { - // replace infinity and NAN by null - if (not std::isfinite(val)) - { - j = BasicJsonType{}; - } - else - { - j.m_type = value_t::number_float; - j.m_value = val; - } + j.m_type = value_t::number_float; + j.m_value = val; j.assert_invariant(); } }; @@ -904,22 +896,15 @@ void from_json(const BasicJsonType& j, typename BasicJsonType::array_t& arr) } // forward_list doesn't have an insert method -template +template::value, int> = 0> void from_json(const BasicJsonType& j, std::forward_list& l) { - // do not perform the check when user wants to retrieve jsons - // (except when it's null.. ?) - if (j.is_null()) + if (not j.is_array()) { JSON_THROW(type_error(302, "type must be array, but is " + j.type_name())); } - if (not std::is_same::value) - { - if (not j.is_array()) - { - JSON_THROW(type_error(302, "type must be array, but is " + j.type_name())); - } - } + for (auto it = j.rbegin(), end = j.rend(); it != end; ++it) { l.push_front(it->template get()); @@ -951,8 +936,8 @@ auto from_json_array_impl(const BasicJsonType& j, CompatibleArrayType& arr, prio using std::end; arr.reserve(j.size()); - std::transform( - j.begin(), j.end(), std::inserter(arr, end(arr)), [](const BasicJsonType & i) + std::transform(j.begin(), j.end(), + std::inserter(arr, end(arr)), [](const BasicJsonType & i) { // get() returns *this, this won't call a from_json // method when value_type is BasicJsonType @@ -962,22 +947,15 @@ auto from_json_array_impl(const BasicJsonType& j, CompatibleArrayType& arr, prio template::value and + std::is_convertible::value and not std::is_same::value, int> = 0> void from_json(const BasicJsonType& j, CompatibleArrayType& arr) { - if (j.is_null()) + if (not j.is_array()) { JSON_THROW(type_error(302, "type must be array, but is " + j.type_name())); } - // when T == BasicJsonType, do not check if value_t is correct - if (not std::is_same::value) - { - if (not j.is_array()) - { - JSON_THROW(type_error(302, "type must be array, but is " + j.type_name())); - } - } from_json_array_impl(j, arr, priority_tag<1> {}); } @@ -6916,6 +6894,13 @@ class basic_json */ void dump_float(number_float_t x) { + // NaN / inf + if (not std::isfinite(x) or std::isnan(x)) + { + o.write("null", 4); + return; + } + // special case for 0.0 and -0.0 if (x == 0) { @@ -8020,6 +8005,35 @@ class basic_json } } + /*! + @brief check if the next byte belongs to a string + + While parsing a map, the keys must be strings. This function checks if the + current byte is one of the start bytes for a string in MessagePack: + + - 0xa0 - 0xbf: fixstr + - 0xd9: str 8 + - 0xda: str 16 + - 0xdb: str 32 + + @param[in] v MessagePack serialization + @param[in] idx byte index in @a v to check for a string + + @throw std::invalid_argument if `v[idx]` does not belong to a string + */ + static void msgpack_expect_string(const std::vector& v, size_t idx) + { + check_length(v.size(), 1, idx); + + const auto byte = v[idx]; + if ((byte >= 0xa0 and byte <= 0xbf) or (byte >= 0xd9 and byte <= 0xdb)) + { + return; + } + + JSON_THROW(std::invalid_argument("error parsing a msgpack string @ " + std::to_string(idx) + ": " + std::to_string(static_cast(v[idx])))); + } + /*! @brief create a JSON value from a given MessagePack vector @@ -8054,6 +8068,7 @@ class basic_json const size_t len = v[current_idx] & 0x0f; for (size_t i = 0; i < len; ++i) { + msgpack_expect_string(v, idx); std::string key = from_msgpack_internal(v, idx); result[key] = from_msgpack_internal(v, idx); } @@ -8233,6 +8248,7 @@ class basic_json idx += 2; // skip 2 size bytes for (size_t i = 0; i < len; ++i) { + msgpack_expect_string(v, idx); std::string key = from_msgpack_internal(v, idx); result[key] = from_msgpack_internal(v, idx); } @@ -8246,6 +8262,7 @@ class basic_json idx += 4; // skip 4 size bytes for (size_t i = 0; i < len; ++i) { + msgpack_expect_string(v, idx); std::string key = from_msgpack_internal(v, idx); result[key] = from_msgpack_internal(v, idx); } @@ -10730,11 +10747,10 @@ class basic_json result.m_type = value_t::number_float; result.m_value = val; - // replace infinity and NAN by null + // throw in case of infinity or NAN if (not std::isfinite(result.m_value.number_float)) { - result.m_type = value_t::null; - result.m_value = basic_json::json_value(); + JSON_THROW(std::out_of_range("number overflow: " + get_token_string())); } return true; diff --git a/test/src/unit-cbor.cpp b/test/src/unit-cbor.cpp index 87cbadc20..b4b539754 100644 --- a/test/src/unit-cbor.cpp +++ b/test/src/unit-cbor.cpp @@ -744,13 +744,17 @@ TEST_CASE("CBOR") SECTION("infinity") { json j = json::from_cbor(std::vector({0xf9, 0x7c, 0x00})); - CHECK(j == nullptr); + json::number_float_t d = j; + CHECK(not std::isfinite(d)); + CHECK(j.dump() == "null"); } SECTION("NaN") { json j = json::from_cbor(std::vector({0xf9, 0x7c, 0x01})); - CHECK(j == nullptr); + json::number_float_t d = j; + CHECK(std::isnan(d)); + CHECK(j.dump() == "null"); } } } @@ -1430,7 +1434,7 @@ TEST_CASE("CBOR roundtrips", "[hide]") "test/data/nst_json_testsuite/test_parsing/y_number_after_space.json", "test/data/nst_json_testsuite/test_parsing/y_number_double_close_to_zero.json", "test/data/nst_json_testsuite/test_parsing/y_number_double_huge_neg_exp.json", - "test/data/nst_json_testsuite/test_parsing/y_number_huge_exp.json", + //"test/data/nst_json_testsuite/test_parsing/y_number_huge_exp.json", "test/data/nst_json_testsuite/test_parsing/y_number_int_with_exp.json", "test/data/nst_json_testsuite/test_parsing/y_number_minus_zero.json", "test/data/nst_json_testsuite/test_parsing/y_number_negative_int.json", @@ -1442,9 +1446,9 @@ TEST_CASE("CBOR roundtrips", "[hide]") "test/data/nst_json_testsuite/test_parsing/y_number_real_exponent.json", "test/data/nst_json_testsuite/test_parsing/y_number_real_fraction_exponent.json", "test/data/nst_json_testsuite/test_parsing/y_number_real_neg_exp.json", - "test/data/nst_json_testsuite/test_parsing/y_number_real_neg_overflow.json", + //"test/data/nst_json_testsuite/test_parsing/y_number_real_neg_overflow.json", "test/data/nst_json_testsuite/test_parsing/y_number_real_pos_exponent.json", - "test/data/nst_json_testsuite/test_parsing/y_number_real_pos_overflow.json", + //"test/data/nst_json_testsuite/test_parsing/y_number_real_pos_overflow.json", "test/data/nst_json_testsuite/test_parsing/y_number_real_underflow.json", "test/data/nst_json_testsuite/test_parsing/y_number_simple_int.json", "test/data/nst_json_testsuite/test_parsing/y_number_simple_real.json", diff --git a/test/src/unit-class_parser.cpp b/test/src/unit-class_parser.cpp index d5e7c99b8..c9240c9f4 100644 --- a/test/src/unit-class_parser.cpp +++ b/test/src/unit-class_parser.cpp @@ -276,7 +276,9 @@ TEST_CASE("parser class") SECTION("overflow") { - CHECK(json::parser("1.18973e+4932").parse() == json()); + // overflows during parsing yield an exception + CHECK_THROWS_AS(json::parser("1.18973e+4932").parse() == json(), std::out_of_range); + CHECK_THROWS_WITH(json::parser("1.18973e+4932").parse() == json(), "number overflow: 1.18973e+4932"); } SECTION("invalid numbers") diff --git a/test/src/unit-constructor1.cpp b/test/src/unit-constructor1.cpp index e71c2c460..18c032e02 100644 --- a/test/src/unit-constructor1.cpp +++ b/test/src/unit-constructor1.cpp @@ -702,11 +702,17 @@ TEST_CASE("constructors") SECTION("infinity") { - // infinity is stored as null - // should change in the future: https://github.com/nlohmann/json/issues/388 + // infinity is stored properly, but serialized to null json::number_float_t n(std::numeric_limits::infinity()); json j(n); - CHECK(j.type() == json::value_t::null); + CHECK(j.type() == json::value_t::number_float); + + // check round trip of infinity + json::number_float_t d = j; + CHECK(d == n); + + // check that inf is serialized to null + CHECK(j.dump() == "null"); } } diff --git a/test/src/unit-msgpack.cpp b/test/src/unit-msgpack.cpp index de1a7bca4..1da33153b 100644 --- a/test/src/unit-msgpack.cpp +++ b/test/src/unit-msgpack.cpp @@ -1200,7 +1200,7 @@ TEST_CASE("MessagePack roundtrips", "[hide]") "test/data/nst_json_testsuite/test_parsing/y_number_after_space.json", "test/data/nst_json_testsuite/test_parsing/y_number_double_close_to_zero.json", "test/data/nst_json_testsuite/test_parsing/y_number_double_huge_neg_exp.json", - "test/data/nst_json_testsuite/test_parsing/y_number_huge_exp.json", + //"test/data/nst_json_testsuite/test_parsing/y_number_huge_exp.json", "test/data/nst_json_testsuite/test_parsing/y_number_int_with_exp.json", "test/data/nst_json_testsuite/test_parsing/y_number_minus_zero.json", "test/data/nst_json_testsuite/test_parsing/y_number_negative_int.json", @@ -1212,9 +1212,9 @@ TEST_CASE("MessagePack roundtrips", "[hide]") "test/data/nst_json_testsuite/test_parsing/y_number_real_exponent.json", "test/data/nst_json_testsuite/test_parsing/y_number_real_fraction_exponent.json", "test/data/nst_json_testsuite/test_parsing/y_number_real_neg_exp.json", - "test/data/nst_json_testsuite/test_parsing/y_number_real_neg_overflow.json", + //"test/data/nst_json_testsuite/test_parsing/y_number_real_neg_overflow.json", "test/data/nst_json_testsuite/test_parsing/y_number_real_pos_exponent.json", - "test/data/nst_json_testsuite/test_parsing/y_number_real_pos_overflow.json", + //"test/data/nst_json_testsuite/test_parsing/y_number_real_pos_overflow.json", "test/data/nst_json_testsuite/test_parsing/y_number_real_underflow.json", "test/data/nst_json_testsuite/test_parsing/y_number_simple_int.json", "test/data/nst_json_testsuite/test_parsing/y_number_simple_real.json", diff --git a/test/src/unit-regression.cpp b/test/src/unit-regression.cpp index 286661696..a8287aeb3 100644 --- a/test/src/unit-regression.cpp +++ b/test/src/unit-regression.cpp @@ -32,6 +32,7 @@ SOFTWARE. using nlohmann::json; #include +#include TEST_CASE("regression tests") { @@ -48,6 +49,7 @@ TEST_CASE("regression tests") SECTION("issue #70 - Handle infinity and NaN cases") { + /* SECTION("NAN value") { CHECK(json(NAN) == json()); @@ -59,6 +61,36 @@ TEST_CASE("regression tests") CHECK(json(INFINITY) == json()); CHECK(json(json::number_float_t(INFINITY)) == json()); } + */ + + // With 3.0.0, the semantics of this changed: NAN and infinity are + // stored properly inside the JSON value (no exception or conversion + // to null), but are serialized as null. + SECTION("NAN value") + { + json j1 = NAN; + CHECK(j1.is_number_float()); + json::number_float_t f1 = j1; + CHECK(std::isnan(f1)); + + json j2 = json::number_float_t(NAN); + CHECK(j2.is_number_float()); + json::number_float_t f2 = j2; + CHECK(std::isnan(f2)); + } + + SECTION("infinity") + { + json j1 = INFINITY; + CHECK(j1.is_number_float()); + json::number_float_t f1 = j1; + CHECK(not std::isfinite(f1)); + + json j2 = json::number_float_t(INFINITY); + CHECK(j2.is_number_float()); + json::number_float_t f2 = j2; + CHECK(not std::isfinite(f2)); + } } SECTION("pull request #71 - handle enum type") @@ -558,8 +590,8 @@ TEST_CASE("regression tests") SECTION("issue #329 - serialized value not always can be parsed") { - json j = json::parse("22e2222"); - CHECK(j == json()); + CHECK_THROWS_AS(json::parse("22e2222"), std::out_of_range); + CHECK_THROWS_WITH(json::parse("22e2222"), "number overflow: 22e2222"); } SECTION("issue #366 - json::parse on failed stream gets stuck") @@ -834,6 +866,55 @@ TEST_CASE("regression tests") CHECK(s1 == s2); } + SECTION("issue #473 - inconsistent behavior in conversion to array type") + { + json j_array = {1, 2, 3, 4}; + json j_number = 42; + json j_null = nullptr; + + SECTION("std::vector") + { + auto create = [](const json & j) + { + std::vector v = j; + }; + + CHECK_NOTHROW(create(j_array)); + CHECK_THROWS_AS(create(j_number), json::type_error); + CHECK_THROWS_WITH(create(j_number), "[json.exception.type_error.302] type must be array, but is number"); + CHECK_THROWS_AS(create(j_null), json::type_error); + CHECK_THROWS_WITH(create(j_null), "[json.exception.type_error.302] type must be array, but is null"); + } + + SECTION("std::list") + { + auto create = [](const json & j) + { + std::list v = j; + }; + + CHECK_NOTHROW(create(j_array)); + CHECK_THROWS_AS(create(j_number), json::type_error); + CHECK_THROWS_WITH(create(j_number), "[json.exception.type_error.302] type must be array, but is number"); + CHECK_THROWS_AS(create(j_null), json::type_error); + CHECK_THROWS_WITH(create(j_null), "[json.exception.type_error.302] type must be array, but is null"); + } + + SECTION("std::forward_list") + { + auto create = [](const json & j) + { + std::forward_list v = j; + }; + + CHECK_NOTHROW(create(j_array)); + CHECK_THROWS_AS(create(j_number), json::type_error); + CHECK_THROWS_WITH(create(j_number), "[json.exception.type_error.302] type must be array, but is number"); + CHECK_THROWS_AS(create(j_null), json::type_error); + CHECK_THROWS_WITH(create(j_null), "[json.exception.type_error.302] type must be array, but is null"); + } + } + SECTION("issue #486 - json::value_t can't be a map's key type in VC++ 2015") { // the code below must compile with MSVC diff --git a/test/src/unit-testsuites.cpp b/test/src/unit-testsuites.cpp index 701348a80..71b45b916 100644 --- a/test/src/unit-testsuites.cpp +++ b/test/src/unit-testsuites.cpp @@ -460,7 +460,6 @@ TEST_CASE("nst's JSONTestSuite") "test/data/nst_json_testsuite/test_parsing/y_number_after_space.json", "test/data/nst_json_testsuite/test_parsing/y_number_double_close_to_zero.json", "test/data/nst_json_testsuite/test_parsing/y_number_double_huge_neg_exp.json", - "test/data/nst_json_testsuite/test_parsing/y_number_huge_exp.json", "test/data/nst_json_testsuite/test_parsing/y_number_int_with_exp.json", "test/data/nst_json_testsuite/test_parsing/y_number_minus_zero.json", "test/data/nst_json_testsuite/test_parsing/y_number_negative_int.json", @@ -472,9 +471,7 @@ TEST_CASE("nst's JSONTestSuite") "test/data/nst_json_testsuite/test_parsing/y_number_real_exponent.json", "test/data/nst_json_testsuite/test_parsing/y_number_real_fraction_exponent.json", "test/data/nst_json_testsuite/test_parsing/y_number_real_neg_exp.json", - "test/data/nst_json_testsuite/test_parsing/y_number_real_neg_overflow.json", "test/data/nst_json_testsuite/test_parsing/y_number_real_pos_exponent.json", - "test/data/nst_json_testsuite/test_parsing/y_number_real_pos_overflow.json", "test/data/nst_json_testsuite/test_parsing/y_number_real_underflow.json", "test/data/nst_json_testsuite/test_parsing/y_number_simple_int.json", "test/data/nst_json_testsuite/test_parsing/y_number_simple_real.json", @@ -765,9 +762,6 @@ TEST_CASE("nst's JSONTestSuite") { for (auto filename : { - // we currently do not limit exponents - "test/data/nst_json_testsuite/test_parsing/i_number_neg_int_huge_exp.json", - "test/data/nst_json_testsuite/test_parsing/i_number_pos_double_huge_exp.json", // we do not pose a limit on nesting "test/data/nst_json_testsuite/test_parsing/i_structure_500_nested_arrays.json", // we silently ignore BOMs @@ -787,6 +781,26 @@ TEST_CASE("nst's JSONTestSuite") } } + // numbers that overflow during parsing + SECTION("i/y -> n (out of range)") + { + for (auto filename : + { + "test/data/nst_json_testsuite/test_parsing/i_number_neg_int_huge_exp.json", + "test/data/nst_json_testsuite/test_parsing/i_number_pos_double_huge_exp.json", + "test/data/nst_json_testsuite/test_parsing/y_number_huge_exp.json", + "test/data/nst_json_testsuite/test_parsing/y_number_real_neg_overflow.json", + "test/data/nst_json_testsuite/test_parsing/y_number_real_pos_overflow.json" + } + ) + { + CAPTURE(filename); + std::ifstream f(filename); + json j; + CHECK_THROWS_AS(j << f, std::out_of_range); + } + } + SECTION("i -> n") { for (auto filename :