diff --git a/src/json.hpp b/src/json.hpp index e7fc76cc8..27950c0fb 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -65,6 +65,12 @@ Class @ref nlohmann::basic_json is a good entry point for the documentation. #endif #endif +// disable float-equal warnings on GCC/clang +#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wfloat-equal" +#endif + // enable ssize_t for MSVC #ifdef _MSC_VER #include @@ -99,13 +105,6 @@ struct has_mapped_type public: static constexpr bool value = sizeof(test(0)) == 1; }; - -/// "equality" comparison for floating point numbers -template -static bool approx(const T a, const T b) -{ - return not (a > b or a < b); -} } /*! @@ -2806,7 +2805,9 @@ class basic_json std::enable_if< not std::is_pointer::value and not std::is_same::value +#ifndef _MSC_VER // Fix for issue #167 operator<< abiguity under VS2015 and not std::is_same>::value +#endif , int>::type = 0> operator ValueType() const { @@ -4927,7 +4928,7 @@ class basic_json } case value_t::number_float: { - return approx(lhs.m_value.number_float, rhs.m_value.number_float); + return lhs.m_value.number_float == rhs.m_value.number_float; } default: { @@ -4937,23 +4938,23 @@ class basic_json } else if (lhs_type == value_t::number_integer and rhs_type == value_t::number_float) { - return approx(static_cast(lhs.m_value.number_integer), - rhs.m_value.number_float); + return static_cast(lhs.m_value.number_integer) == + rhs.m_value.number_float; } else if (lhs_type == value_t::number_float and rhs_type == value_t::number_integer) { - return approx(lhs.m_value.number_float, - static_cast(rhs.m_value.number_integer)); + return lhs.m_value.number_float == + static_cast(rhs.m_value.number_integer); } else if (lhs_type == value_t::number_unsigned and rhs_type == value_t::number_float) { - return approx(static_cast(lhs.m_value.number_unsigned), - rhs.m_value.number_float); + return static_cast(lhs.m_value.number_unsigned) == + rhs.m_value.number_float; } else if (lhs_type == value_t::number_float and rhs_type == value_t::number_unsigned) { - return approx(lhs.m_value.number_float, - static_cast(rhs.m_value.number_unsigned)); + return lhs.m_value.number_float == + static_cast(rhs.m_value.number_unsigned); } else if (lhs_type == value_t::number_unsigned and rhs_type == value_t::number_integer) { @@ -7478,37 +7479,57 @@ basic_json_parser_64: return result; } + /*! + @brief static_cast between two types and indicate if it results in error + + This function performs a static_cast between @a source and @a dest. It + then checks if a static_cast back to @a dest produces an error. + + @param[in] source the value to cast from + + @param[out] dest the value to cast to + + @return @a true if the cast was performed without error, @a false otherwise + */ + template + bool attempt_cast(T_A source, T_B & dest) const + { + dest = static_cast(source); + return (source == static_cast(dest)); + } + /*! @brief return number value for number tokens This function translates the last token into the most appropriate number type (either integer, unsigned integer or floating point), which is passed back to the caller via the result parameter. The pointer - m_start points to the beginning of the parsed number. We first examine + @a m_start points to the beginning of the parsed number. We first examine the first character to determine the sign of the number and then pass - this pointer to either std::strtoull (if positive) or std::strtoll - (if negative), both of which set endptr to the first character past the - converted number. If this pointer is not the same as m_cursor, then + this pointer to either @a std::strtoull (if positive) or @a std::strtoll + (if negative), both of which set @a endptr to the first character past the + converted number. If this pointer is not the same as @a m_cursor, then either more or less characters have been used during the comparison. This can happen for inputs like "01" which will be treated like number 0 followed by number 1. This will also occur for valid floating point inputs like "12e3" will be incorrectly read as 12. Numbers that are too - large or too small to be stored in the number_integer_t or - number_unsigned_t types will cause a range error (errno set to ERANGE). - In both cases (more/less characters read, or a range error) the pointer - is passed to std:strtod, which also sets endptr to the first character - past the converted number. - - The resulting number_float_t is then cast to a number_integer_t or, - if positive, to a number_unsigned_t and compared to the original. If - there is no loss of precision then it is stored as a number_integer_t - or, if positive a number_unsigned_t, otherwise as a number_float_t. - - A final comparison is made of endptr and if still not the same as - m_cursor a bad input is assumed and result parameter is set to NAN. + large or too small for a signed/unsigned long long will cause a range + error (@a errno set to ERANGE). The parsed number is cast to a @ref + number_integer_t/@ref number_unsigned_t using the helper function @ref attempt_cast, + which returns @a false if the cast could not be peformed without error. - @param[out] result basic_json object to receive the number, or NAN if the + In any of these cases (more/less characters read, range error or a cast + error) the pointer is passed to @a std:strtod, which also sets @a endptr to the + first character past the converted number. The resulting @ref number_float_t + is then cast to a @ref number_integer_t/@ref number_unsigned_t using + @ref attempt_cast and if no error occurs is stored in that form, otherwise + it is stored as a @ref number_float_t. + + A final comparison is made of @a endptr and if still not the same as + @ref m_cursor a bad input is assumed and @a result parameter is set to NAN. + + @param[out] result @ref basic_json object to receive the number, or NAN if the conversion read past the current token. The latter case needs to be treated by the caller function. */ @@ -7516,59 +7537,47 @@ basic_json_parser_64: { typename string_t::value_type* endptr; assert(m_start != nullptr); + errno = 0; // Attempt to parse it as an integer - first checking for a negative number - if(*reinterpret_cast(m_start) != '-') + if (*reinterpret_cast(m_start) != '-') { - // Positive, parse with strtoull - result.m_value.number_unsigned = std::strtoull(reinterpret_cast(m_start),&endptr,10); - result.m_type = value_t::number_unsigned; + // Positive, parse with strtoull and attempt cast to number_unsigned_t + if (attempt_cast(std::strtoull(reinterpret_cast(m_start), &endptr, 10), result.m_value.number_unsigned)) + result.m_type = value_t::number_unsigned; + else result.m_type = value_t::number_float; // Cast failed due to overflow - store as float } else { - // Negative, parse with strtoll - result.m_value.number_integer = std::strtoll(reinterpret_cast(m_start),&endptr,10); - result.m_type = value_t::number_integer; + // Negative, parse with strtoll and attempt cast to number_integer_t + if (attempt_cast(std::strtoll(reinterpret_cast(m_start), &endptr, 10), result.m_value.number_unsigned)) + result.m_type = value_t::number_integer; + else result.m_type = value_t::number_float; // Cast failed due to overflow - store as float } - // Check the end of the number was reached and no range error occurred - if(reinterpret_cast(endptr) != m_cursor || errno == ERANGE) + // Check the end of the number was reached and no range error or overflow occurred + if (reinterpret_cast(endptr) != m_cursor || errno == ERANGE || result.m_type == value_t::number_float) { - // Either the number won't fit in an integer (range error) or there was + // Either the number won't fit in an integer (range error from strtoull/strtoll or overflow on cast) or there was // something else after the number, which could be an exponent // Parse with strtod - result.m_value.number_float = std::strtod(reinterpret_cast(m_start),&endptr); - - // Check if it can be stored as an integer without loss of precision e.g. 1.2e3 = 1200 - if (result.m_type == value_t::number_integer) - { - auto int_val = static_cast(result.m_value.number_float); - if (approx(result.m_value.number_float, static_cast(int_val))) - { - // we would not lose precision -> return int - result.m_value.number_integer = int_val; - } - else - { - result.m_type = value_t::number_float; - } - } - else - { - auto int_val = static_cast(result.m_value.number_float); - if (approx(result.m_value.number_float, static_cast(int_val))) - { - // we would not lose precision -> return int - result.m_value.number_unsigned = int_val; - } - else - { - result.m_type = value_t::number_float; - } - } + auto float_val = std::strtod(reinterpret_cast(m_start), &endptr); - // Anything after the number is an error + // Check if it can be stored as an integer without loss of precision e.g. 1.2e3 = 1200 + if (result.m_type == value_t::number_unsigned) + { + if (!attempt_cast(float_val, result.m_value.number_unsigned)) result.m_type = value_t::number_float; + } + else if (result.m_type == value_t::number_integer) + { + if (!attempt_cast(float_val, result.m_value.number_integer)) result.m_type = value_t::number_float; + } + + // Actually store the float + if (result.m_type == value_t::number_float) result.m_value.number_float = static_cast(float_val); + + // Anything after the number is an error if(reinterpret_cast(endptr) != m_cursor) { result.m_value.number_float = NAN; @@ -7950,4 +7959,11 @@ inline nlohmann::json operator "" _json(const char* s, std::size_t) return nlohmann::json::parse(reinterpret_cast(s)); } +// restore GCC/clang diagnostic settings +#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) + #pragma GCC diagnostic pop #endif + +#endif + + diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index f253e7f71..6834e56d5 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -65,6 +65,12 @@ Class @ref nlohmann::basic_json is a good entry point for the documentation. #endif #endif +// disable float-equal warnings on GCC/clang +#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wfloat-equal" +#endif + // enable ssize_t for MSVC #ifdef _MSC_VER #include @@ -99,13 +105,6 @@ struct has_mapped_type public: static constexpr bool value = sizeof(test(0)) == 1; }; - -/// "equality" comparison for floating point numbers -template -static bool approx(const T a, const T b) -{ - return not (a > b or a < b); -} } /*! @@ -2806,7 +2805,9 @@ class basic_json std::enable_if< not std::is_pointer::value and not std::is_same::value +#ifndef _MSC_VER // Fix for issue #167 operator<< abiguity under VS2015 and not std::is_same>::value +#endif , int>::type = 0> operator ValueType() const { @@ -4927,7 +4928,7 @@ class basic_json } case value_t::number_float: { - return approx(lhs.m_value.number_float, rhs.m_value.number_float); + return lhs.m_value.number_float == rhs.m_value.number_float; } default: { @@ -4937,23 +4938,23 @@ class basic_json } else if (lhs_type == value_t::number_integer and rhs_type == value_t::number_float) { - return approx(static_cast(lhs.m_value.number_integer), - rhs.m_value.number_float); + return static_cast(lhs.m_value.number_integer) == + rhs.m_value.number_float; } else if (lhs_type == value_t::number_float and rhs_type == value_t::number_integer) { - return approx(lhs.m_value.number_float, - static_cast(rhs.m_value.number_integer)); + return lhs.m_value.number_float == + static_cast(rhs.m_value.number_integer); } else if (lhs_type == value_t::number_unsigned and rhs_type == value_t::number_float) { - return approx(static_cast(lhs.m_value.number_unsigned), - rhs.m_value.number_float); + return static_cast(lhs.m_value.number_unsigned) == + rhs.m_value.number_float; } else if (lhs_type == value_t::number_float and rhs_type == value_t::number_unsigned) { - return approx(lhs.m_value.number_float, - static_cast(rhs.m_value.number_unsigned)); + return lhs.m_value.number_float == + static_cast(rhs.m_value.number_unsigned); } else if (lhs_type == value_t::number_unsigned and rhs_type == value_t::number_integer) { @@ -7160,37 +7161,57 @@ class basic_json return result; } + /*! + @brief static_cast between two types and indicate if it results in error + + This function performs a static_cast between @a source and @a dest. It + then checks if a static_cast back to @a dest produces an error. + + @param[in] source the value to cast from + + @param[out] dest the value to cast to + + @return @a true if the cast was performed without error, @a false otherwise + */ + template + bool attempt_cast(T_A source, T_B & dest) const + { + dest = static_cast(source); + return (source == static_cast(dest)); + } + /*! @brief return number value for number tokens This function translates the last token into the most appropriate number type (either integer, unsigned integer or floating point), which is passed back to the caller via the result parameter. The pointer - m_start points to the beginning of the parsed number. We first examine + @a m_start points to the beginning of the parsed number. We first examine the first character to determine the sign of the number and then pass - this pointer to either std::strtoull (if positive) or std::strtoll - (if negative), both of which set endptr to the first character past the - converted number. If this pointer is not the same as m_cursor, then + this pointer to either @a std::strtoull (if positive) or @a std::strtoll + (if negative), both of which set @a endptr to the first character past the + converted number. If this pointer is not the same as @a m_cursor, then either more or less characters have been used during the comparison. This can happen for inputs like "01" which will be treated like number 0 followed by number 1. This will also occur for valid floating point inputs like "12e3" will be incorrectly read as 12. Numbers that are too - large or too small to be stored in the number_integer_t or - number_unsigned_t types will cause a range error (errno set to ERANGE). - In both cases (more/less characters read, or a range error) the pointer - is passed to std:strtod, which also sets endptr to the first character - past the converted number. - - The resulting number_float_t is then cast to a number_integer_t or, - if positive, to a number_unsigned_t and compared to the original. If - there is no loss of precision then it is stored as a number_integer_t - or, if positive a number_unsigned_t, otherwise as a number_float_t. - - A final comparison is made of endptr and if still not the same as - m_cursor a bad input is assumed and result parameter is set to NAN. + large or too small for a signed/unsigned long long will cause a range + error (@a errno set to ERANGE). The parsed number is cast to a @ref + number_integer_t/@ref number_unsigned_t using the helper function @ref attempt_cast, + which returns @a false if the cast could not be peformed without error. - @param[out] result basic_json object to receive the number, or NAN if the + In any of these cases (more/less characters read, range error or a cast + error) the pointer is passed to @a std:strtod, which also sets @a endptr to the + first character past the converted number. The resulting @ref number_float_t + is then cast to a @ref number_integer_t/@ref number_unsigned_t using + @ref attempt_cast and if no error occurs is stored in that form, otherwise + it is stored as a @ref number_float_t. + + A final comparison is made of @a endptr and if still not the same as + @ref m_cursor a bad input is assumed and @a result parameter is set to NAN. + + @param[out] result @ref basic_json object to receive the number, or NAN if the conversion read past the current token. The latter case needs to be treated by the caller function. */ @@ -7198,59 +7219,47 @@ class basic_json { typename string_t::value_type* endptr; assert(m_start != nullptr); + errno = 0; // Attempt to parse it as an integer - first checking for a negative number - if(*reinterpret_cast(m_start) != '-') + if (*reinterpret_cast(m_start) != '-') { - // Positive, parse with strtoull - result.m_value.number_unsigned = std::strtoull(reinterpret_cast(m_start),&endptr,10); - result.m_type = value_t::number_unsigned; + // Positive, parse with strtoull and attempt cast to number_unsigned_t + if (attempt_cast(std::strtoull(reinterpret_cast(m_start), &endptr, 10), result.m_value.number_unsigned)) + result.m_type = value_t::number_unsigned; + else result.m_type = value_t::number_float; // Cast failed due to overflow - store as float } else { - // Negative, parse with strtoll - result.m_value.number_integer = std::strtoll(reinterpret_cast(m_start),&endptr,10); - result.m_type = value_t::number_integer; + // Negative, parse with strtoll and attempt cast to number_integer_t + if (attempt_cast(std::strtoll(reinterpret_cast(m_start), &endptr, 10), result.m_value.number_unsigned)) + result.m_type = value_t::number_integer; + else result.m_type = value_t::number_float; // Cast failed due to overflow - store as float } - // Check the end of the number was reached and no range error occurred - if(reinterpret_cast(endptr) != m_cursor || errno == ERANGE) + // Check the end of the number was reached and no range error or overflow occurred + if (reinterpret_cast(endptr) != m_cursor || errno == ERANGE || result.m_type == value_t::number_float) { - // Either the number won't fit in an integer (range error) or there was + // Either the number won't fit in an integer (range error from strtoull/strtoll or overflow on cast) or there was // something else after the number, which could be an exponent // Parse with strtod - result.m_value.number_float = std::strtod(reinterpret_cast(m_start),&endptr); - - // Check if it can be stored as an integer without loss of precision e.g. 1.2e3 = 1200 - if (result.m_type == value_t::number_integer) - { - auto int_val = static_cast(result.m_value.number_float); - if (approx(result.m_value.number_float, static_cast(int_val))) - { - // we would not lose precision -> return int - result.m_value.number_integer = int_val; - } - else - { - result.m_type = value_t::number_float; - } - } - else - { - auto int_val = static_cast(result.m_value.number_float); - if (approx(result.m_value.number_float, static_cast(int_val))) - { - // we would not lose precision -> return int - result.m_value.number_unsigned = int_val; - } - else - { - result.m_type = value_t::number_float; - } - } + auto float_val = std::strtod(reinterpret_cast(m_start), &endptr); - // Anything after the number is an error + // Check if it can be stored as an integer without loss of precision e.g. 1.2e3 = 1200 + if (result.m_type == value_t::number_unsigned) + { + if (!attempt_cast(float_val, result.m_value.number_unsigned)) result.m_type = value_t::number_float; + } + else if (result.m_type == value_t::number_integer) + { + if (!attempt_cast(float_val, result.m_value.number_integer)) result.m_type = value_t::number_float; + } + + // Actually store the float + if (result.m_type == value_t::number_float) result.m_value.number_float = static_cast(float_val); + + // Anything after the number is an error if(reinterpret_cast(endptr) != m_cursor) { result.m_value.number_float = NAN; @@ -7632,4 +7641,11 @@ inline nlohmann::json operator "" _json(const char* s, std::size_t) return nlohmann::json::parse(reinterpret_cast(s)); } +// restore GCC/clang diagnostic settings +#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) + #pragma GCC diagnostic pop #endif + +#endif + + diff --git a/test/unit.cpp b/test/unit.cpp index 7b5c8d898..d5d19396e 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -25,6 +25,11 @@ #include "json.hpp" using nlohmann::json; +// disable float-equal warnings on GCC/clang +#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) +#pragma GCC diagnostic ignored "-Wfloat-equal" +#endif + TEST_CASE("constructors") { SECTION("create an empty value with a given type") @@ -11885,11 +11890,34 @@ TEST_CASE("regression tests") SECTION("issue #89 - nonstandard integer type") { // create JSON class with nonstandard integer number type - nlohmann::basic_json j; + using custom_json = nlohmann::basic_json; + custom_json j; j["int_1"] = 1; // we need to cast to int to compile with Catch - the value is int32_t CHECK(static_cast(j["int_1"]) == 1); - } + + // tests for correct handling of non-standard integers that overflow the type selected by the user + + // unsigned integer object creation - expected to wrap and still be stored as an integer + j = 4294967296U; // 2^32 + CHECK(static_cast(j.type()) == static_cast(custom_json::value_t::number_unsigned)); + CHECK(j.get() == 0); // Wrap + + // unsigned integer parsing - expected to overflow and be stored as a float + j = custom_json::parse("4294967296"); // 2^32 + CHECK(static_cast(j.type()) == static_cast(custom_json::value_t::number_float)); + CHECK(j.get() == 4294967296.0); + + // integer object creation - expected to wrap and still be stored as an integer + j = -2147483649LL; // -2^31-1 + CHECK(static_cast(j.type()) == static_cast(custom_json::value_t::number_integer)); + CHECK(j.get() == 2147483647.0); // Wrap + + // integer parsing - expected to overflow and be stored as a float + j = custom_json::parse("-2147483648"); // -2^31 + CHECK(static_cast(j.type()) == static_cast(custom_json::value_t::number_float)); + CHECK(j.get() == -2147483648.0); + } SECTION("issue #93 reverse_iterator operator inheritance problem") { @@ -11988,3 +12016,4 @@ TEST_CASE("regression tests") CHECK(json::parse("\"\\ud80c\\udc60abc\"").get() == u8"\U00013060abc"); } } +