From a1114fc0295a01c1ebe29753cff1915d407c6049 Mon Sep 17 00:00:00 2001 From: antonyzhilin Date: Fri, 24 Jan 2025 03:04:36 +0300 Subject: [PATCH] refactor userver: don't use boost::numeric_cast commit_hash:b1464d5bb4b4519ceecdd71a0b15a854dd80da41 --- core/src/utils/statistics/writer.cpp | 4 +- .../include/userver/utils/numeric_cast.hpp | 42 ++++++++++++++----- universal/src/crypto/public_key.cpp | 7 ++-- .../src/formats/json/parser/int_parser.cpp | 28 +++++++++---- .../src/formats/json/parser/parser_test.cpp | 21 ++++------ universal/src/utils/datetime.cpp | 10 ++--- universal/src/utils/from_string_test.cpp | 11 +++-- 7 files changed, 76 insertions(+), 47 deletions(-) diff --git a/core/src/utils/statistics/writer.cpp b/core/src/utils/statistics/writer.cpp index 3701723f20d3..13aa849eef2a 100644 --- a/core/src/utils/statistics/writer.cpp +++ b/core/src/utils/statistics/writer.cpp @@ -3,9 +3,9 @@ #include #include -#include #include +#include #include #include @@ -136,7 +136,7 @@ Writer Writer::MakeChild() { void Writer::Write(unsigned long long value) { if (state_) { ValidateUsage(); - CheckAndWrite(*state_, MetricValue{boost::numeric_cast(value)}); + CheckAndWrite(*state_, MetricValue{utils::numeric_cast(value)}); } } diff --git a/universal/include/userver/utils/numeric_cast.hpp b/universal/include/userver/utils/numeric_cast.hpp index 5ed783c396f4..b8896d5eeac0 100644 --- a/universal/include/userver/utils/numeric_cast.hpp +++ b/universal/include/userver/utils/numeric_cast.hpp @@ -3,6 +3,7 @@ /// @file userver/utils/numeric_cast.hpp /// @brief @copybrief utils::numeric_cast +#include #include #include @@ -26,21 +27,40 @@ using PrintableValue = std::conditional_t<(sizeof(T) > 1), T, int>; /// ## Example usage: /// /// @snippet utils/numeric_cast_test.cpp Sample utils::numeric_cast usage -template -constexpr U numeric_cast(T input) { - static_assert(std::is_integral_v); - static_assert(std::is_integral_v); +template +constexpr To numeric_cast(From input) { + static_assert(std::is_integral_v); + static_assert(std::is_integral_v); + using FromLimits = std::numeric_limits; + using ToLimits = std::numeric_limits; - U result = input; - if (static_cast(result) != input || ((result < 0) != (input < 0))) { + std::string_view overflow_type{}; + + if constexpr (ToLimits::digits < FromLimits::digits) { + if (input > static_cast(ToLimits::max())) { + overflow_type = "positive"; + } + } + + // signed -> signed: loss if narrowing + // signed -> unsigned: loss + if constexpr (FromLimits::is_signed && (!ToLimits::is_signed || ToLimits::digits < FromLimits::digits)) { + if (input < static_cast(ToLimits::lowest())) { + overflow_type = "negative"; + } + } + + if (!overflow_type.empty()) { throw std::runtime_error(fmt::format( - "Failed to convert {} {} into {} due to integer overflow", - compiler::GetTypeName(), - static_cast>(input), - compiler::GetTypeName() + "Failed to convert {} {} into {} due to {} integer overflow", + compiler::GetTypeName(), + static_cast>(input), + compiler::GetTypeName(), + overflow_type )); } - return result; + + return static_cast(input); } } // namespace utils diff --git a/universal/src/crypto/public_key.cpp b/universal/src/crypto/public_key.cpp index 1bc93e1afeed..ba5eb72141ad 100644 --- a/universal/src/crypto/public_key.cpp +++ b/universal/src/crypto/public_key.cpp @@ -9,11 +9,10 @@ #include #include -#include - #include #include #include +#include #include #include @@ -31,8 +30,8 @@ using Bignum = std::unique_ptr; Bignum LoadBignumFromBigEnd(const std::string_view raw) { int size = 0; try { - size = boost::numeric_cast(raw.size()); - } catch (const boost::bad_numeric_cast& ex) { + size = utils::numeric_cast(raw.size()); + } catch (const std::runtime_error& ex) { throw KeyParseError{ex.what()}; } diff --git a/universal/src/formats/json/parser/int_parser.cpp b/universal/src/formats/json/parser/int_parser.cpp index 645b98336780..b048d565436d 100644 --- a/universal/src/formats/json/parser/int_parser.cpp +++ b/universal/src/formats/json/parser/int_parser.cpp @@ -1,8 +1,10 @@ #include +#include #include +#include -#include +#include USERVER_NAMESPACE_BEGIN @@ -12,20 +14,28 @@ namespace { /* In JSON double with zero fractional part is a legal integer, * e.g. "3.0" is an integer 3 */ -template +template std::optional DoubleToInt(double value) { auto i = std::round(value); - if (std::fabs(i - value) > std::max(std::fabs(value), std::fabs(i)) * std::numeric_limits::epsilon()) { - return std::nullopt; + if (std::abs(i - value) > std::max(std::abs(value), std::abs(i)) * std::numeric_limits::epsilon()) { + return std::nullopt; // rounding } - return boost::numeric_cast(i); + + // Taken from the implementation of boost::numeric_cast(double). + if (value <= static_cast(std::numeric_limits::lowest()) - 1.0) { + throw std::runtime_error("numeric conversion: negative overflow"); + } + if (value >= static_cast(std::numeric_limits::max()) + 1.0) { + throw std::runtime_error("numeric conversion: positive overflow"); + } + return value < 0.0 ? static_cast(std::ceil(value)) : static_cast(std::floor(value)); } } // namespace -void IntegralParser::Int64(std::int64_t i) { this->SetResult(boost::numeric_cast(i)); } +void IntegralParser::Int64(std::int64_t i) { this->SetResult(utils::numeric_cast(i)); } -void IntegralParser::Uint64(std::uint64_t i) { this->SetResult(boost::numeric_cast(i)); } +void IntegralParser::Uint64(std::uint64_t i) { this->SetResult(utils::numeric_cast(i)); } void IntegralParser::Double(double value) { auto v = DoubleToInt(value); @@ -33,9 +43,9 @@ void IntegralParser::Double(double value) { this->SetResult(*std::move(v)); } -void IntegralParser::Int64(std::int64_t i) { this->SetResult(boost::numeric_cast(i)); } +void IntegralParser::Int64(std::int64_t i) { this->SetResult(utils::numeric_cast(i)); } -void IntegralParser::Uint64(std::uint64_t i) { this->SetResult(boost::numeric_cast(i)); } +void IntegralParser::Uint64(std::uint64_t i) { this->SetResult(utils::numeric_cast(i)); } void IntegralParser::Double(double value) { auto v = DoubleToInt(value); diff --git a/universal/src/formats/json/parser/parser_test.cpp b/universal/src/formats/json/parser/parser_test.cpp index e800c244a01d..dee28b24acdd 100644 --- a/universal/src/formats/json/parser/parser_test.cpp +++ b/universal/src/formats/json/parser/parser_test.cpp @@ -2,20 +2,12 @@ #include +#include #include #include -// TODO: move to utest/* // NOLINTNEXTLINE(cppcoreguidelines-macro-usage) -#define EXPECT_THROW_TEXT(code, exception_type, exc_text) \ - try { \ - code; \ - FAIL() << "expected exception " #exception_type ", but not thrown"; \ - } catch (const exception_type& e) { \ - EXPECT_EQ(e.what(), std::string{exc_text}) << "wrong exception message"; \ - } catch (const std::exception& e) { \ - FAIL() << "wrong exception type, expected " #exception_type ", but got " << typeid(e).name(); \ - } +#define EXPECT_THROW_TEXT(code, exception_type, exc_text) UEXPECT_THROW_MSG(code, exception_type, exc_text) USERVER_NAMESPACE_BEGIN namespace fjp = formats::json::parser; @@ -88,9 +80,12 @@ TEST(JsonStringParser, Int64Overflow) { EXPECT_THROW_TEXT( (fjp::ParseToType(input)), fjp::ParseError, - "Parse error at pos 20, path '': bad " - "numeric conversion: positive overflow, the latest token " - "was 18446744073709551615" + fmt::format( + "Parse error at pos 20, path '': Failed to convert {} 18446744073709551615 into {} " + "due to positive integer overflow, the latest token was 18446744073709551615", + compiler::GetTypeName(), + compiler::GetTypeName() + ) ); } diff --git a/universal/src/utils/datetime.cpp b/universal/src/utils/datetime.cpp index f4203b46a7e5..47912dd7b861 100644 --- a/universal/src/utils/datetime.cpp +++ b/universal/src/utils/datetime.cpp @@ -7,11 +7,11 @@ #include #include -#include -#include #include +#include #include +#include USERVER_NAMESPACE_BEGIN @@ -186,10 +186,10 @@ std::uint32_t ParseDayTime(const std::string& str) { std::uint8_t seconds = 0; try { - hours = boost::numeric_cast(boost::lexical_cast(std::string(str, 0, 2))); - minutes = boost::numeric_cast(boost::lexical_cast(std::string(str, 3, 2))); + hours = utils::numeric_cast(utils::FromString(std::string_view{str}.substr(0, 2))); + minutes = utils::numeric_cast(utils::FromString(std::string_view{str}.substr(3, 2))); if (str.size() == 8) - seconds = boost::numeric_cast(boost::lexical_cast(std::string(str, 6, 2))); + seconds = utils::numeric_cast(utils::FromString(std::string_view{str}.substr(6, 2))); } catch (const std::exception& ex) { throw std::invalid_argument(std::string("Failed to parse time from ") + str); diff --git a/universal/src/utils/from_string_test.cpp b/universal/src/utils/from_string_test.cpp index 8b09e160db82..5c447d767070 100644 --- a/universal/src/utils/from_string_test.cpp +++ b/universal/src/utils/from_string_test.cpp @@ -4,8 +4,9 @@ #include #include +#include +#include #include -#include #include @@ -20,8 +21,12 @@ std::string ToString(T value) { if constexpr (sizeof(value) == 1) { // Prevent printing int8_t and uint8_t as a character return std::to_string(static_cast(value)); + } else if constexpr (std::is_same_v && FMT_VERSION < 100100) { + // fmt before 10.1.0 formatted long double incorrectly + // https://github.com/fmtlib/fmt/issues/3564 + return fmt::format("{:.{}g}", value, std::numeric_limits::max_digits10); } else { - return boost::lexical_cast(value); + return fmt::to_string(value); } } @@ -42,7 +47,7 @@ auto CheckConverts(StringType input, T expectedResult) { template auto TestConverts(const std::string& input, T expectedResult) { - CheckConverts(input.data(), expectedResult); + CheckConverts(input.c_str(), expectedResult); CheckConverts(input, expectedResult); CheckConverts(std::string_view{input}, expectedResult); }