Skip to content

Commit

Permalink
Breakdown the "seconds + femtoseconds => time_point" overflow issue.
Browse files Browse the repository at this point in the history
When `cctz::parse()` produces seconds+femtoseconds values that cannot
be represented in the output `time_point<D>` it should return `false`.
Here we add overloads of `join_seconds()` that break the overflow issue
down into four cases, three of which are implemented and have test cases.

The fourth overload (for 1/N duration ratios) is yet to be implemented,
so its test cases are currently commented out, but they provide a guide
to what remains to be done.  See google#199.

Also, clarify that the `ToUnixSeconds()` implementation requires that
the `std::chrono::system_clock` uses the Unix epoch in order to avoid
arithmetic overflow.
  • Loading branch information
devbww committed Oct 7, 2021
1 parent 556a1bc commit f907bbe
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 25 deletions.
113 changes: 93 additions & 20 deletions include/cctz/time_zone.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

#include <chrono>
#include <cstdint>
#include <limits>
#include <string>
#include <utility>

Expand All @@ -37,20 +38,9 @@ using sys_seconds = seconds; // Deprecated. Use cctz::seconds instead.

namespace detail {
template <typename D>
inline std::pair<time_point<seconds>, D>
split_seconds(const time_point<D>& tp) {
auto sec = std::chrono::time_point_cast<seconds>(tp);
auto sub = tp - sec;
if (sub.count() < 0) {
sec -= seconds(1);
sub += seconds(1);
}
return {sec, std::chrono::duration_cast<D>(sub)};
}
inline std::pair<time_point<seconds>, seconds>
split_seconds(const time_point<seconds>& tp) {
return {tp, seconds::zero()};
}
std::pair<time_point<seconds>, D> split_seconds(const time_point<D>& tp);
std::pair<time_point<seconds>, seconds> split_seconds(
const time_point<seconds>& tp);
} // namespace detail

// cctz::time_zone is an opaque, small, value-type class representing a
Expand Down Expand Up @@ -274,6 +264,20 @@ std::string format(const std::string&, const time_point<seconds>&,
const femtoseconds&, const time_zone&);
bool parse(const std::string&, const std::string&, const time_zone&,
time_point<seconds>*, femtoseconds*, std::string* err = nullptr);
template <typename Rep, std::intmax_t Denom>
bool join_seconds(
const time_point<seconds>& sec, const femtoseconds& fs,
time_point<std::chrono::duration<Rep, std::ratio<1, Denom>>>* tpp);
template <typename Rep, std::intmax_t Num>
bool join_seconds(
const time_point<seconds>& sec, const femtoseconds& fs,
time_point<std::chrono::duration<Rep, std::ratio<Num, 1>>>* tpp);
template <typename Rep>
bool join_seconds(
const time_point<seconds>& sec, const femtoseconds& fs,
time_point<std::chrono::duration<Rep, std::ratio<1, 1>>>* tpp);
bool join_seconds(const time_point<seconds>& sec, const femtoseconds&,
time_point<seconds>* tpp);
} // namespace detail

// Formats the given time_point in the given cctz::time_zone according to
Expand Down Expand Up @@ -364,15 +368,84 @@ inline bool parse(const std::string& fmt, const std::string& input,
const time_zone& tz, time_point<D>* tpp) {
time_point<seconds> sec;
detail::femtoseconds fs;
const bool b = detail::parse(fmt, input, tz, &sec, &fs);
if (b) {
// TODO: Return false if unrepresentable as a time_point<D>.
*tpp = std::chrono::time_point_cast<D>(sec);
*tpp += std::chrono::duration_cast<D>(fs);
return detail::parse(fmt, input, tz, &sec, &fs) &&
detail::join_seconds(sec, fs, tpp);
}

namespace detail {

// Split a time_point<D> into a time_point<seconds> and a D subseconds.
// Undefined behavior if time_point<seconds> is not of sufficient range.
// Note that this means it is UB to call cctz::time_zone::lookup(tp) or
// cctz::format(fmt, tp, tz) with a time_point that is outside the range
// of a 64-bit std::time_t.
template <typename D>
std::pair<time_point<seconds>, D> split_seconds(const time_point<D>& tp) {
auto sec = std::chrono::time_point_cast<seconds>(tp);
auto sub = tp - sec;
if (sub.count() < 0) {
sec -= seconds(1);
sub += seconds(1);
}
return b;
return {sec, std::chrono::duration_cast<D>(sub)};
}

inline std::pair<time_point<seconds>, seconds> split_seconds(
const time_point<seconds>& tp) {
return {tp, seconds::zero()};
}

// Join a time_point<seconds> and femto subseconds into a time_point<D>.
// Floors to the resolution of time_point<D>. Returns false if time_point<D>
// is not of sufficient range.
template <typename Rep, std::intmax_t Denom>
bool join_seconds(
const time_point<seconds>& sec, const femtoseconds& fs,
time_point<std::chrono::duration<Rep, std::ratio<1, Denom>>>* tpp) {
using D = std::chrono::duration<Rep, std::ratio<1, Denom>>;
// TODO(#199): Return false if result unrepresentable as a time_point<D>.
*tpp = std::chrono::time_point_cast<D>(sec);
*tpp += std::chrono::duration_cast<D>(fs);
return true;
}

template <typename Rep, std::intmax_t Num>
bool join_seconds(
const time_point<seconds>& sec, const femtoseconds&,
time_point<std::chrono::duration<Rep, std::ratio<Num, 1>>>* tpp) {
using D = std::chrono::duration<Rep, std::ratio<Num, 1>>;
auto count = sec.time_since_epoch().count();
if (count >= 0 || count % Num == 0) {
count /= Num;
} else {
count /= Num;
count -= 1;
}
if (count > std::numeric_limits<Rep>::max()) return false;
if (count < std::numeric_limits<Rep>::min()) return false;
*tpp = time_point<D>() + D{static_cast<Rep>(count)};
return true;
}

template <typename Rep>
bool join_seconds(
const time_point<seconds>& sec, const femtoseconds&,
time_point<std::chrono::duration<Rep, std::ratio<1, 1>>>* tpp) {
using D = std::chrono::duration<Rep, std::ratio<1, 1>>;
auto count = sec.time_since_epoch().count();
if (count > std::numeric_limits<Rep>::max()) return false;
if (count < std::numeric_limits<Rep>::min()) return false;
*tpp = time_point<D>() + D{static_cast<Rep>(count)};
return true;
}

inline bool join_seconds(const time_point<seconds>& sec, const femtoseconds&,
time_point<seconds>* tpp) {
*tpp = sec;
return true;
}

} // namespace detail
} // namespace cctz

#endif // CCTZ_TIME_ZONE_H_
81 changes: 77 additions & 4 deletions src/time_zone_format_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "cctz/time_zone.h"

#include <chrono>
#include <cstdint>
#include <iomanip>
#include <sstream>
#include <string>
Expand Down Expand Up @@ -1498,7 +1499,7 @@ TEST(Parse, MaxRange) {
parse(RFC3339_sec, "292277026596-12-04T14:30:07-01:00", utc, &tp));
EXPECT_EQ(tp, time_point<cctz::seconds>::max());
EXPECT_FALSE(
parse(RFC3339_sec, "292277026596-12-04T15:30:07-01:00", utc, &tp));
parse(RFC3339_sec, "292277026596-12-04T14:30:08-01:00", utc, &tp));

// tests the lower limit using +00:00 offset
EXPECT_TRUE(
Expand All @@ -1519,10 +1520,82 @@ TEST(Parse, MaxRange) {
utc, &tp));
EXPECT_FALSE(parse(RFC3339_sec, "-9223372036854775808-01-01T00:00:00+00:01",
utc, &tp));
}

TEST(Parse, TimePointOverflow) {
const time_zone utc = utc_time_zone();

using D = chrono::duration<std::int64_t, std::nano>;
time_point<D> tp;

EXPECT_TRUE(
parse(RFC3339_full, "2262-04-11T23:47:16.8547758079+00:00", utc, &tp));
EXPECT_EQ(tp, time_point<D>::max());
EXPECT_EQ("2262-04-11T23:47:16.854775807+00:00",
format(RFC3339_full, tp, utc));
#if 0
// TODO(#199): Will fail until cctz::parse() properly detects overflow.
EXPECT_FALSE(
parse(RFC3339_full, "2262-04-11T23:47:16.8547758080+00:00", utc, &tp));
EXPECT_TRUE(
parse(RFC3339_full, "1677-09-21T00:12:43.1452241920+00:00", utc, &tp));
EXPECT_EQ(tp, time_point<D>::min());
EXPECT_EQ("1677-09-21T00:12:43.145224192+00:00",
format(RFC3339_full, tp, utc));
EXPECT_FALSE(
parse(RFC3339_full, "1677-09-21T00:12:43.1452241919+00:00", utc, &tp));
#endif

using DS = chrono::duration<std::int8_t, chrono::seconds::period>;
time_point<DS> stp;

EXPECT_TRUE(parse(RFC3339_full, "1970-01-01T00:02:07.9+00:00", utc, &stp));
EXPECT_EQ(stp, time_point<DS>::max());
EXPECT_EQ("1970-01-01T00:02:07+00:00", format(RFC3339_full, stp, utc));
EXPECT_FALSE(parse(RFC3339_full, "1970-01-01T00:02:08+00:00", utc, &stp));

EXPECT_TRUE(parse(RFC3339_full, "1969-12-31T23:57:52+00:00", utc, &stp));
EXPECT_EQ(stp, time_point<DS>::min());
EXPECT_EQ("1969-12-31T23:57:52+00:00", format(RFC3339_full, stp, utc));
EXPECT_FALSE(parse(RFC3339_full, "1969-12-31T23:57:51.9+00:00", utc, &stp));

// TODO: Add tests that parsing times with fractional seconds overflow
// appropriately. This can't be done until cctz::parse() properly detects
// overflow when combining the chrono seconds and femto.
using DM = chrono::duration<std::int8_t, chrono::minutes::period>;
time_point<DM> mtp;

EXPECT_TRUE(parse(RFC3339_full, "1970-01-01T02:07:59+00:00", utc, &mtp));
EXPECT_EQ(mtp, time_point<DM>::max());
EXPECT_EQ("1970-01-01T02:07:00+00:00", format(RFC3339_full, mtp, utc));
EXPECT_FALSE(parse(RFC3339_full, "1970-01-01T02:08:00+00:00", utc, &mtp));

EXPECT_TRUE(parse(RFC3339_full, "1969-12-31T21:52:00+00:00", utc, &mtp));
EXPECT_EQ(mtp, time_point<DM>::min());
EXPECT_EQ("1969-12-31T21:52:00+00:00", format(RFC3339_full, mtp, utc));
EXPECT_FALSE(parse(RFC3339_full, "1969-12-31T21:51:59+00:00", utc, &mtp));
}

TEST(Parse, TimePointOverflowFloor) {
const time_zone utc = utc_time_zone();

using D = chrono::duration<std::int64_t, std::micro>;
time_point<D> tp;

EXPECT_TRUE(
parse(RFC3339_full, "294247-01-10T04:00:54.7758079+00:00", utc, &tp));
EXPECT_EQ(tp, time_point<D>::max());
EXPECT_EQ("294247-01-10T04:00:54.775807+00:00",
format(RFC3339_full, tp, utc));
#if 0
// TODO(#199): Will fail until cctz::parse() properly detects overflow.
EXPECT_FALSE(
parse(RFC3339_full, "294247-01-10T04:00:54.7758080+00:00", utc, &tp));
EXPECT_TRUE(
parse(RFC3339_full, "-290308-12-21T19:59:05.2241920+00:00", utc, &tp));
EXPECT_EQ(tp, time_point<D>::min());
EXPECT_EQ("-290308-12-21T19:59:05.224192+00:00",
format(RFC3339_full, tp, utc));
EXPECT_FALSE(
parse(RFC3339_full, "-290308-12-21T19:59:05.2241919+00:00", utc, &tp));
#endif
}

//
Expand Down
3 changes: 2 additions & 1 deletion src/time_zone_if.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ class TimeZoneIf {

// Convert between time_point<seconds> and a count of seconds since the
// Unix epoch. We assume that the std::chrono::system_clock and the
// Unix clock are second aligned, but not that they share an epoch.
// Unix clock are second aligned, and that the results are representable.
// (That is, that they share an epoch, which is required since C++20.)
inline std::int_fast64_t ToUnixSeconds(const time_point<seconds>& tp) {
return (tp - std::chrono::time_point_cast<seconds>(
std::chrono::system_clock::from_time_t(0))).count();
Expand Down

0 comments on commit f907bbe

Please sign in to comment.