mirror of
https://github.com/fmtlib/fmt.git
synced 2025-07-30 10:47:35 +02:00
Fix overflow in time_point formatting with large dates (#3727)
* Fix #3725 and rename fmt_safe_duration_cast to fmt_duration_cast The function is now more generic and will handle all casts. It also takes care of toggling safe vs unsafe casts using FMT_SAFE_DURATION_CAST. * Refactor fmt_duration_cast to put #ifdef inside the function * Fix compilation error with FMT_USE_LOCAL_TIME
This commit is contained in:
committed by
GitHub
parent
ccc9ab7bf9
commit
7f8d419115
@ -430,6 +430,51 @@ auto write(OutputIt out, const std::tm& time, const std::locale& loc,
|
|||||||
return write_encoded_tm_str(out, string_view(buf.data(), buf.size()), loc);
|
return write_encoded_tm_str(out, string_view(buf.data(), buf.size()), loc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <typename Rep1, typename Rep2>
|
||||||
|
struct is_same_arithmetic_type
|
||||||
|
: public std::integral_constant<bool,
|
||||||
|
(std::is_integral<Rep1>::value &&
|
||||||
|
std::is_integral<Rep2>::value) ||
|
||||||
|
(std::is_floating_point<Rep1>::value &&
|
||||||
|
std::is_floating_point<Rep2>::value)> {
|
||||||
|
};
|
||||||
|
|
||||||
|
template <
|
||||||
|
typename To, typename FromRep, typename FromPeriod,
|
||||||
|
FMT_ENABLE_IF(is_same_arithmetic_type<FromRep, typename To::rep>::value)>
|
||||||
|
To fmt_duration_cast(std::chrono::duration<FromRep, FromPeriod> from) {
|
||||||
|
#if FMT_SAFE_DURATION_CAST
|
||||||
|
// throwing version of safe_duration_cast
|
||||||
|
// only available for integer<->integer or float<->float casts
|
||||||
|
int ec;
|
||||||
|
To to = safe_duration_cast::safe_duration_cast<To>(from, ec);
|
||||||
|
if (ec) FMT_THROW(format_error("cannot format duration"));
|
||||||
|
return to;
|
||||||
|
#else
|
||||||
|
// standard duration cast, may overflow and invoke undefined behavior
|
||||||
|
return std::chrono::duration_cast<To>(from);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
template <
|
||||||
|
typename To, typename FromRep, typename FromPeriod,
|
||||||
|
FMT_ENABLE_IF(!is_same_arithmetic_type<FromRep, typename To::rep>::value)>
|
||||||
|
To fmt_duration_cast(std::chrono::duration<FromRep, FromPeriod> from) {
|
||||||
|
// mixed integer<->float cast is not supported with safe_duration_cast
|
||||||
|
// fallback to standard duration cast in this case
|
||||||
|
return std::chrono::duration_cast<To>(from);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Duration>
|
||||||
|
std::time_t to_time_t(
|
||||||
|
std::chrono::time_point<std::chrono::system_clock, Duration> time_point) {
|
||||||
|
// cannot use std::chrono::system_clock::to_time_t() since this would first
|
||||||
|
// require a cast to std::chrono::system_clock::time_point, which could
|
||||||
|
// overflow.
|
||||||
|
return fmt_duration_cast<std::chrono::duration<std::time_t>>(
|
||||||
|
time_point.time_since_epoch())
|
||||||
|
.count();
|
||||||
|
}
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
|
|
||||||
FMT_BEGIN_EXPORT
|
FMT_BEGIN_EXPORT
|
||||||
@ -478,8 +523,8 @@ inline std::tm localtime(std::time_t time) {
|
|||||||
#if FMT_USE_LOCAL_TIME
|
#if FMT_USE_LOCAL_TIME
|
||||||
template <typename Duration>
|
template <typename Duration>
|
||||||
inline auto localtime(std::chrono::local_time<Duration> time) -> std::tm {
|
inline auto localtime(std::chrono::local_time<Duration> time) -> std::tm {
|
||||||
return localtime(std::chrono::system_clock::to_time_t(
|
return localtime(
|
||||||
std::chrono::current_zone()->to_sys(time)));
|
detail::to_time_t(std::chrono::current_zone()->to_sys(time)));
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ -523,9 +568,10 @@ inline std::tm gmtime(std::time_t time) {
|
|||||||
return gt.tm_;
|
return gt.tm_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <typename Duration>
|
||||||
inline std::tm gmtime(
|
inline std::tm gmtime(
|
||||||
std::chrono::time_point<std::chrono::system_clock> time_point) {
|
std::chrono::time_point<std::chrono::system_clock, Duration> time_point) {
|
||||||
return gmtime(std::chrono::system_clock::to_time_t(time_point));
|
return gmtime(detail::to_time_t(time_point));
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace detail {
|
namespace detail {
|
||||||
@ -1051,13 +1097,12 @@ void write_fractional_seconds(OutputIt& out, Duration d, int precision = -1) {
|
|||||||
std::chrono::seconds::rep>::type,
|
std::chrono::seconds::rep>::type,
|
||||||
std::ratio<1, detail::pow10(num_fractional_digits)>>;
|
std::ratio<1, detail::pow10(num_fractional_digits)>>;
|
||||||
|
|
||||||
const auto fractional =
|
const auto fractional = d - fmt_duration_cast<std::chrono::seconds>(d);
|
||||||
d - std::chrono::duration_cast<std::chrono::seconds>(d);
|
|
||||||
const auto subseconds =
|
const auto subseconds =
|
||||||
std::chrono::treat_as_floating_point<
|
std::chrono::treat_as_floating_point<
|
||||||
typename subsecond_precision::rep>::value
|
typename subsecond_precision::rep>::value
|
||||||
? fractional.count()
|
? fractional.count()
|
||||||
: std::chrono::duration_cast<subsecond_precision>(fractional).count();
|
: fmt_duration_cast<subsecond_precision>(fractional).count();
|
||||||
auto n = static_cast<uint32_or_64_or_128_t<long long>>(subseconds);
|
auto n = static_cast<uint32_or_64_or_128_t<long long>>(subseconds);
|
||||||
const int num_digits = detail::count_digits(n);
|
const int num_digits = detail::count_digits(n);
|
||||||
|
|
||||||
@ -1620,17 +1665,6 @@ template <typename T> struct make_unsigned_or_unchanged<T, true> {
|
|||||||
using type = typename std::make_unsigned<T>::type;
|
using type = typename std::make_unsigned<T>::type;
|
||||||
};
|
};
|
||||||
|
|
||||||
#if FMT_SAFE_DURATION_CAST
|
|
||||||
// throwing version of safe_duration_cast
|
|
||||||
template <typename To, typename FromRep, typename FromPeriod>
|
|
||||||
To fmt_safe_duration_cast(std::chrono::duration<FromRep, FromPeriod> from) {
|
|
||||||
int ec;
|
|
||||||
To to = safe_duration_cast::safe_duration_cast<To>(from, ec);
|
|
||||||
if (ec) FMT_THROW(format_error("cannot format duration"));
|
|
||||||
return to;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
template <typename Rep, typename Period,
|
template <typename Rep, typename Period,
|
||||||
FMT_ENABLE_IF(std::is_integral<Rep>::value)>
|
FMT_ENABLE_IF(std::is_integral<Rep>::value)>
|
||||||
inline std::chrono::duration<Rep, std::milli> get_milliseconds(
|
inline std::chrono::duration<Rep, std::milli> get_milliseconds(
|
||||||
@ -1640,17 +1674,17 @@ inline std::chrono::duration<Rep, std::milli> get_milliseconds(
|
|||||||
#if FMT_SAFE_DURATION_CAST
|
#if FMT_SAFE_DURATION_CAST
|
||||||
using CommonSecondsType =
|
using CommonSecondsType =
|
||||||
typename std::common_type<decltype(d), std::chrono::seconds>::type;
|
typename std::common_type<decltype(d), std::chrono::seconds>::type;
|
||||||
const auto d_as_common = fmt_safe_duration_cast<CommonSecondsType>(d);
|
const auto d_as_common = fmt_duration_cast<CommonSecondsType>(d);
|
||||||
const auto d_as_whole_seconds =
|
const auto d_as_whole_seconds =
|
||||||
fmt_safe_duration_cast<std::chrono::seconds>(d_as_common);
|
fmt_duration_cast<std::chrono::seconds>(d_as_common);
|
||||||
// this conversion should be nonproblematic
|
// this conversion should be nonproblematic
|
||||||
const auto diff = d_as_common - d_as_whole_seconds;
|
const auto diff = d_as_common - d_as_whole_seconds;
|
||||||
const auto ms =
|
const auto ms =
|
||||||
fmt_safe_duration_cast<std::chrono::duration<Rep, std::milli>>(diff);
|
fmt_duration_cast<std::chrono::duration<Rep, std::milli>>(diff);
|
||||||
return ms;
|
return ms;
|
||||||
#else
|
#else
|
||||||
auto s = std::chrono::duration_cast<std::chrono::seconds>(d);
|
auto s = fmt_duration_cast<std::chrono::seconds>(d);
|
||||||
return std::chrono::duration_cast<std::chrono::milliseconds>(d - s);
|
return fmt_duration_cast<std::chrono::milliseconds>(d - s);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1751,14 +1785,8 @@ struct chrono_formatter {
|
|||||||
|
|
||||||
// this may overflow and/or the result may not fit in the
|
// this may overflow and/or the result may not fit in the
|
||||||
// target type.
|
// target type.
|
||||||
#if FMT_SAFE_DURATION_CAST
|
|
||||||
// might need checked conversion (rep!=Rep)
|
// might need checked conversion (rep!=Rep)
|
||||||
auto tmpval = std::chrono::duration<rep, Period>(val);
|
s = fmt_duration_cast<seconds>(std::chrono::duration<rep, Period>(val));
|
||||||
s = fmt_safe_duration_cast<seconds>(tmpval);
|
|
||||||
#else
|
|
||||||
s = std::chrono::duration_cast<seconds>(
|
|
||||||
std::chrono::duration<rep, Period>(val));
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns true if nan or inf, writes to out.
|
// returns true if nan or inf, writes to out.
|
||||||
@ -2082,25 +2110,22 @@ struct formatter<std::chrono::time_point<std::chrono::system_clock, Duration>,
|
|||||||
period::num != 1 || period::den != 1 ||
|
period::num != 1 || period::den != 1 ||
|
||||||
std::is_floating_point<typename Duration::rep>::value)) {
|
std::is_floating_point<typename Duration::rep>::value)) {
|
||||||
const auto epoch = val.time_since_epoch();
|
const auto epoch = val.time_since_epoch();
|
||||||
auto subsecs = std::chrono::duration_cast<Duration>(
|
auto subsecs = detail::fmt_duration_cast<Duration>(
|
||||||
epoch - std::chrono::duration_cast<std::chrono::seconds>(epoch));
|
epoch - detail::fmt_duration_cast<std::chrono::seconds>(epoch));
|
||||||
|
|
||||||
if (subsecs.count() < 0) {
|
if (subsecs.count() < 0) {
|
||||||
auto second =
|
auto second =
|
||||||
std::chrono::duration_cast<Duration>(std::chrono::seconds(1));
|
detail::fmt_duration_cast<Duration>(std::chrono::seconds(1));
|
||||||
if (epoch.count() < ((Duration::min)() + second).count())
|
if (epoch.count() < ((Duration::min)() + second).count())
|
||||||
FMT_THROW(format_error("duration is too small"));
|
FMT_THROW(format_error("duration is too small"));
|
||||||
subsecs += second;
|
subsecs += second;
|
||||||
val -= second;
|
val -= second;
|
||||||
}
|
}
|
||||||
|
|
||||||
return formatter<std::tm, Char>::do_format(
|
return formatter<std::tm, Char>::do_format(gmtime(val), ctx, &subsecs);
|
||||||
gmtime(std::chrono::time_point_cast<std::chrono::seconds>(val)), ctx,
|
|
||||||
&subsecs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return formatter<std::tm, Char>::format(
|
return formatter<std::tm, Char>::format(gmtime(val), ctx);
|
||||||
gmtime(std::chrono::time_point_cast<std::chrono::seconds>(val)), ctx);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2119,17 +2144,13 @@ struct formatter<std::chrono::local_time<Duration>, Char>
|
|||||||
if (period::num != 1 || period::den != 1 ||
|
if (period::num != 1 || period::den != 1 ||
|
||||||
std::is_floating_point<typename Duration::rep>::value) {
|
std::is_floating_point<typename Duration::rep>::value) {
|
||||||
const auto epoch = val.time_since_epoch();
|
const auto epoch = val.time_since_epoch();
|
||||||
const auto subsecs = std::chrono::duration_cast<Duration>(
|
const auto subsecs = detail::fmt_duration_cast<Duration>(
|
||||||
epoch - std::chrono::duration_cast<std::chrono::seconds>(epoch));
|
epoch - detail::fmt_duration_cast<std::chrono::seconds>(epoch));
|
||||||
|
|
||||||
return formatter<std::tm, Char>::do_format(
|
return formatter<std::tm, Char>::do_format(localtime(val), ctx, &subsecs);
|
||||||
localtime(std::chrono::time_point_cast<std::chrono::seconds>(val)),
|
|
||||||
ctx, &subsecs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return formatter<std::tm, Char>::format(
|
return formatter<std::tm, Char>::format(localtime(val), ctx);
|
||||||
localtime(std::chrono::time_point_cast<std::chrono::seconds>(val)),
|
|
||||||
ctx);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
@ -874,6 +874,20 @@ TEST(chrono_test, timestamps_ratios) {
|
|||||||
t4(std::chrono::duration<int, std::ratio<63>>(1));
|
t4(std::chrono::duration<int, std::ratio<63>>(1));
|
||||||
|
|
||||||
EXPECT_EQ(fmt::format("{:%M:%S}", t4), "01:03");
|
EXPECT_EQ(fmt::format("{:%M:%S}", t4), "01:03");
|
||||||
|
|
||||||
|
std::chrono::time_point<std::chrono::system_clock, std::chrono::milliseconds>
|
||||||
|
t5(std::chrono::seconds(32503680000));
|
||||||
|
|
||||||
|
EXPECT_EQ(fmt::format("{:%Y-%m-%d}", t5), "3000-01-01");
|
||||||
|
|
||||||
|
#if FMT_SAFE_DURATION_CAST
|
||||||
|
using years = std::chrono::duration<std::int64_t, std::ratio<31556952>>;
|
||||||
|
std::chrono::time_point<std::chrono::system_clock, years> t6(
|
||||||
|
(years(std::numeric_limits<std::int64_t>::max())));
|
||||||
|
|
||||||
|
EXPECT_THROW_MSG((void)fmt::format("{:%Y-%m-%d}", t6), fmt::format_error,
|
||||||
|
"cannot format duration");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(chrono_test, timestamps_sub_seconds) {
|
TEST(chrono_test, timestamps_sub_seconds) {
|
||||||
|
Reference in New Issue
Block a user