Harden failure signal handler in the face of memory corruptions
[folly.git] / folly / experimental / cron / date_time_utils.cpp
1 /*
2  * Copyright 2014 Facebook, Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *   http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 #include "folly/experimental/cron/date_time_utils.h"
18
19 #include <boost/date_time/c_local_time_adjustor.hpp>
20 #include <cerrno>
21
22 #include "folly/Format.h"
23
24 namespace folly { namespace cron {
25
26 using namespace boost::local_time;
27 using namespace boost::posix_time;
28 using namespace std;
29
30 // NB: The exceptions below are intended to confirm that the underlying
31 // libraries behave in a sane way.  This makes them untestable.  I got each
32 // of them to fire by temporarily changing their checks, so if they do fire,
33 // the printouts should be okay.  It's fine to change them to CHECKs.
34
35 time_t getUTCOffset(time_t utc_time, time_zone_ptr tz) {
36   auto utc_pt = from_time_t(utc_time);
37   auto local_pt = utcPTimeToTimezoneLocalPTime(utc_pt, tz);
38   return (local_pt - utc_pt).total_seconds();
39 }
40
41 ptime utcPTimeToTimezoneLocalPTime(ptime utc_pt, time_zone_ptr tz) {
42   if (tz) {
43     return local_date_time{utc_pt, tz}.local_time();
44   } else {
45     return boost::date_time::c_local_adjustor<ptime>::utc_to_local(utc_pt);
46   }
47 }
48
49 UTCTimestampsForLocalTime _boostTimezoneLocalPTimeToUTCTimestamps(
50   ptime local_pt,
51   time_zone_ptr tz
52 ) {
53   UTCTimestampsForLocalTime res;
54   auto local_date = local_pt.date();
55   auto local_time = local_pt.time_of_day();
56
57   auto save_timestamp_if_valid = [&](bool is_dst, time_t *out) {
58     try {
59       auto local_dt = local_date_time(local_date, local_time, tz, is_dst);
60       // local_date_time() ignores is_dst if the timezone does not have
61       // DST (instead of throwing dst_not_valid).  So, we must confirm
62       // that our is_dst guess was correct to avoid storing the same
63       // timestamp in both fields of res (same as problem (b) in the
64       // localtime_r code path).
65       if (local_dt.is_dst() == is_dst) {
66         *out = (local_dt.utc_time() - from_time_t(0)).total_seconds();
67       }
68     } catch (dst_not_valid& e) {
69       // Continue, we're trying both values of is_dst
70     }
71   };
72
73   try {
74     save_timestamp_if_valid(true, &res.dst_time);
75     save_timestamp_if_valid(false, &res.non_dst_time);
76   } catch (time_label_invalid& e) {
77     // This local time label was skipped by DST, so res will be empty.
78   }
79   return res;
80 }
81
82 UTCTimestampsForLocalTime _systemTimezoneLocalPTimeToUTCTimestamps(
83   ptime local_pt
84 ) {
85   UTCTimestampsForLocalTime res;
86   struct tm tm = to_tm(local_pt);
87   auto save_timestamp_if_valid = [tm, &local_pt](int is_dst, time_t *out) {
88     // Try to make a UTC timestamp based on our DST guess and local time.
89     struct tm tmp_tm = tm;  // Make a copy since mktime changes the tm
90     tmp_tm.tm_isdst = is_dst;
91     time_t t = mktime(&tmp_tm);
92     if (t == -1) {  // Not sure of the error cause or how to handle it.
93       throw runtime_error(folly::format(
94         "{}: mktime error {}", to_simple_string(local_pt), errno
95       ).str());
96     }
97
98     // Convert the timestamp to a local time to see if the guess was right.
99     struct tm new_tm;
100     auto out_tm = localtime_r(&t, &new_tm);
101     if (out_tm == nullptr) {  // Not sure if such errors can be handled.
102       throw runtime_error(folly::format(
103         "{}: localtime_r error {}", to_simple_string(local_pt), errno
104       ).str());
105     }
106
107     // Does the original tm argree with the tm generated from the mktime()
108     // UTC timestamp?  (We'll check tm_isdst separately.)
109     //
110     // This test never passes when we have a local time label that is
111     // skipped when a DST change moves the clock forward.
112     //
113     // A valid local time label always has one or two valid DST values.
114     // When the timezone has not DST, that value is "false".
115     //
116     // This test always passes when:
117     //  - The DST value is ambiguous (due to the local clock moving back).
118     //  - We guessed the uniquely valid DST value.
119     //
120     // The test may or may not always pass (implementation-dependent) when
121     // we did not guess a valid DST value.
122     //  (a) If it does not pass, we are good, because we also try the other
123     //      DST value, which will make the test pass, and then res will have
124     //      a unique timestamp.
125     //  (b) If it does pass, we're in more trouble, because it means that
126     //      the implementation ignored our is_dst value. Then, the timestamp
127     //      t is the same as for the other is_dst value.  But, we don't want
128     //      res to be labeled ambiguous, and we don't want to randomly pick
129     //      a DST value to set to kNotATime, because clients may want to
130     //      know the real DST value.  The solution is the extra test below.
131     if (
132       tm.tm_sec == new_tm.tm_sec && tm.tm_min == new_tm.tm_min &&
133       tm.tm_hour == new_tm.tm_hour && tm.tm_mday == new_tm.tm_mday &&
134       tm.tm_mon == new_tm.tm_mon && tm.tm_year == new_tm.tm_year &&
135       // To fix problem (b) above, we must assume that localtime_r returns
136       // the correct tm_isdst (if not, it's a system bug anyhow).  Then, we
137       // can just check our DST guess against the truth.  If our guess was
138       // invalid, we shouldn't store the result, avoiding (b).
139       !(  // tm_isdst can also be negative but we'll check that later
140         (new_tm.tm_isdst == 0 && is_dst) || (new_tm.tm_isdst > 0 && !is_dst)
141       )
142     ) {
143       *out = t;
144     }
145     return new_tm.tm_isdst < 0;  // Used for a sanity-check below.
146   };
147
148   bool neg_isdst1 = save_timestamp_if_valid(1, &res.dst_time);
149   bool neg_isdst2 = save_timestamp_if_valid(0, &res.non_dst_time);
150
151   // The only legitimate way for localtime_r() to give back a negative
152   // tm_isdst is if the input local time label is ambiguous due to DST.
153   if (neg_isdst1 || neg_isdst2) {
154     if (neg_isdst1 ^ neg_isdst2) {  // Can't be ambiguous half the time
155       throw runtime_error(folly::format(
156         "{}: one tm_isdst negative but not both", to_simple_string(local_pt)
157       ).str());
158     }
159     if (!res.isAmbiguous()) {
160       throw runtime_error(folly::format(
161         "{}: negative tm_isdst but time label is unambiguous",
162         to_simple_string(local_pt)
163       ).str());
164     }
165   }
166   return res;
167 }
168
169 UTCTimestampsForLocalTime timezoneLocalPTimeToUTCTimestamps(
170   ptime local_pt,
171   time_zone_ptr tz
172 ) {
173   UTCTimestampsForLocalTime res;
174   if (tz) {
175     res = _boostTimezoneLocalPTimeToUTCTimestamps(local_pt, tz);
176   } else {
177     res = _systemTimezoneLocalPTimeToUTCTimestamps(local_pt);
178   }
179   // Both code paths have fixes to prevent this (see e.g. problem (b) above).
180   if (res.isAmbiguous() && res.dst_time == res.non_dst_time) {
181     throw runtime_error(folly::format(
182       "{}: local time maps to {} regardless of tm_isdst",
183       to_simple_string(local_pt), res.dst_time
184     ).str());
185   }
186   return res;
187 }
188
189 }}