fix unchecked float to integral conversion in detail::duration_cast

This commit is contained in:
Aizal Khan 2026-10-14 22:14:28 +05:30
parent b4ffa39b99
commit 7dc456208e
2 changed files with 35 additions and 9 deletions

View File

@ -448,12 +448,6 @@ template <> struct locale_writer<false> {
}
};
template <typename T, typename U>
using is_similar_arithmetic_type =
bool_constant<(std::is_integral<T>::value && std::is_integral<U>::value) ||
(std::is_floating_point<T>::value &&
std::is_floating_point<U>::value)>;
FMT_NORETURN inline void throw_duration_error() {
FMT_THROW(format_error("cannot format duration"));
}
@ -510,14 +504,37 @@ auto duration_cast(std::chrono::duration<FromRep, FromPeriod> from) -> To {
#endif
}
// Mixed integer <-> float casts are not supported by safe_duration_cast.
// An integer to float conversion is always defined.
template <typename To, typename FromRep, typename FromPeriod,
FMT_ENABLE_IF(
!is_similar_arithmetic_type<FromRep, typename To::rep>::value)>
FMT_ENABLE_IF(std::is_integral<FromRep>::value&&
std::is_floating_point<typename To::rep>::value)>
auto duration_cast(std::chrono::duration<FromRep, FromPeriod> from) -> To {
// Mixed integer <-> float cast is not supported by safe duration_cast.
return std::chrono::duration_cast<To>(from);
}
// Cast a floating-point duration to an integral one with a range check since
// converting an out-of-range or NaN value to an integer is undefined behavior.
template <typename To, typename FromRep, typename FromPeriod,
FMT_ENABLE_IF(std::is_floating_point<FromRep>::value&&
std::is_integral<typename To::rep>::value)>
auto duration_cast(std::chrono::duration<FromRep, FromPeriod> from) -> To {
#if FMT_SAFE_DURATION_CAST
using to_rep = typename To::rep;
auto count = detail::duration_cast<
std::chrono::duration<FromRep, typename To::period>>(from)
.count();
// max + 1 is a power of 2 so the cast to FromRep is exact even when max
// itself is rounded up by the conversion; the comparison is false for NaN.
auto upper = static_cast<FromRep>(max_value<to_rep>()) + 1;
auto lower = static_cast<FromRep>((std::numeric_limits<to_rep>::min)());
if (!(count >= lower && count < upper)) throw_duration_error();
return To(static_cast<to_rep>(count));
#else
return std::chrono::duration_cast<To>(from);
#endif
}
template <typename Duration>
auto to_time_t(sys_time<Duration> time_point) -> std::time_t {
// Cannot use std::chrono::system_clock::to_time_t since this would first

View File

@ -995,6 +995,15 @@ TEST(chrono_test, out_of_range) {
// A floating-point day count that doesn't fit in int.
auto fd = std::chrono::duration<double>(1e300);
EXPECT_THROW((void)fmt::format("{:%j}", fd), fmt::format_error);
// A floating-point time point whose seconds don't fit in time_t.
auto ftp = sys_time<std::chrono::duration<double>>(fd);
EXPECT_THROW((void)fmt::format("{:%T}", ftp), fmt::format_error);
auto fnan = sys_time<std::chrono::duration<double>>(
std::chrono::duration<double>(std::numeric_limits<double>::quiet_NaN()));
EXPECT_THROW((void)fmt::format("{:%T}", fnan), fmt::format_error);
auto flocal = fmt::local_time<std::chrono::duration<double>>(
std::chrono::duration<double>(-1e300));
EXPECT_THROW((void)fmt::format("{:%F}", flocal), fmt::format_error);
}
TEST(chrono_test, calendar_padding) {