in bistro/cron/utils/date_time.cpp [73:171]
UTCTimestampsForLocalTime _systemTimezoneLocalPTimeToUTCTimestamps(
ptime local_pt
) {
UTCTimestampsForLocalTime res;
struct tm tm = to_tm(local_pt);
auto save_timestamp_if_valid = [tm, &local_pt](
time_t t, int is_dst, time_t *out) {
// Convert the timestamp to a local time to see if the guess was right.
struct tm new_tm;
auto out_tm = localtime_r(&t, &new_tm);
if (out_tm == nullptr) { // Not sure if such errors can be handled.
throw logic_error(folly::format(
"{}: localtime_r error {}", to_simple_string(local_pt), errno
).str());
}
// Does the original tm argree with the tm generated from the mktime()
// UTC timestamp? (We'll check tm_isdst separately.)
//
// This test never passes when we have a local time label that is
// skipped when a DST change moves the clock forward.
//
// A valid local time label always has one or two valid DST values.
// When the timezone has not DST, that value is "false".
//
// This test always passes when:
// - The DST value is ambiguous (due to the local clock moving back).
// - We guessed the uniquely valid DST value.
//
// The test may or may not always pass (implementation-dependent) when
// we did not guess a valid DST value.
// (a) If it does not pass, we are good, because we also try the other
// DST value, which will make the test pass, and then res will have
// a unique timestamp.
// (b) If it does pass, we're in more trouble, because it means that
// the implementation ignored our is_dst value. Then, the timestamp
// t is the same as for the other is_dst value. But, we don't want
// res to be labeled ambiguous, and we don't want to randomly pick
// a DST value to set to kNotATime, because clients may want to
// know the real DST value. The solution is the extra test below.
if (
tm.tm_sec == new_tm.tm_sec && tm.tm_min == new_tm.tm_min &&
tm.tm_hour == new_tm.tm_hour && tm.tm_mday == new_tm.tm_mday &&
tm.tm_mon == new_tm.tm_mon && tm.tm_year == new_tm.tm_year &&
// To fix problem (b) above, we must assume that localtime_r returns
// the correct tm_isdst (if not, it's a system bug anyhow). Then, we
// can just check our DST guess against the truth. If our guess was
// invalid, we shouldn't store the result, avoiding (b).
!( // tm_isdst can also be negative but we'll check that later
(new_tm.tm_isdst == 0 && is_dst) || (new_tm.tm_isdst > 0 && !is_dst)
)
) {
*out = t;
}
return new_tm.tm_isdst < 0; // Used for a sanity-check below.
};
int num_negative_isdst = 0;
int num_missing_timestamps = 0;
for (int is_dst = 0; is_dst <= 1; ++is_dst) {
// Try to make a UTC timestamp based on our DST guess and local time.
struct tm tmp_tm = tm; // Make a copy since mktime changes the tm
tmp_tm.tm_isdst = is_dst;
time_t t = mktime(&tmp_tm);
if (t == -1) { // Not sure of the error cause or how to handle it.
++num_missing_timestamps;
continue;
}
num_negative_isdst += save_timestamp_if_valid(
t, is_dst, is_dst ? &res.dst_time : &res.non_dst_time);
}
// The only legitimate way for localtime_r() to give back a negative
// tm_isdst is if the input local time label is ambiguous due to DST.
//
// FWIW, we could also error on `isAmbiguous && !num_negative_isdst`,
// but it shouldn't affect our correctness.
if (num_negative_isdst && !res.isAmbiguous()) {
throw logic_error(folly::format(
"{}: negative tm_isdst but time label is unambiguous",
to_simple_string(local_pt)
).str());
}
// On older glibc versions, `mktime` would succeed even when `is_dst` was
// invalid.
if (num_missing_timestamps == 0) {
if (num_negative_isdst == 1) { // Can't be ambiguous half the time
throw logic_error(folly::format(
"{}: one tm_isdst negative but not both", to_simple_string(local_pt)
).str());
}
} else if (num_missing_timestamps == 2) {
throw logic_error(folly::format(
"{}: mktime failed for both is_dst choices", to_simple_string(local_pt)
).str());
}
return res;
}