1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2  * vim: set ts=8 sts=4 et sw=4 tw=99:
3  * This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "vm/DateTime.h"
8 
9 #include "mozilla/Atomics.h"
10 
11 #include <time.h>
12 
13 #include "jsutil.h"
14 
15 #include "js/Date.h"
16 #if ENABLE_INTL_API
17 #include "unicode/timezone.h"
18 #endif
19 
20 using mozilla::Atomic;
21 using mozilla::ReleaseAcquire;
22 using mozilla::UnspecifiedNaN;
23 
24 /* static */ js::DateTimeInfo
25 js::DateTimeInfo::instance;
26 
27 /* static */ mozilla::Atomic<bool, mozilla::ReleaseAcquire>
28 js::DateTimeInfo::AcquireLock::spinLock;
29 
30 static bool
ComputeLocalTime(time_t local,struct tm * ptm)31 ComputeLocalTime(time_t local, struct tm* ptm)
32 {
33 #if defined(_WIN32)
34     return localtime_s(ptm, &local) == 0;
35 #elif defined(HAVE_LOCALTIME_R)
36     return localtime_r(&local, ptm);
37 #else
38     struct tm* otm = localtime(&local);
39     if (!otm)
40         return false;
41     *ptm = *otm;
42     return true;
43 #endif
44 }
45 
46 static bool
ComputeUTCTime(time_t t,struct tm * ptm)47 ComputeUTCTime(time_t t, struct tm* ptm)
48 {
49 #if defined(_WIN32)
50     return gmtime_s(ptm, &t) == 0;
51 #elif defined(HAVE_GMTIME_R)
52     return gmtime_r(&t, ptm);
53 #else
54     struct tm* otm = gmtime(&t);
55     if (!otm)
56         return false;
57     *ptm = *otm;
58     return true;
59 #endif
60 }
61 
62 /*
63  * Compute the offset in seconds from the current UTC time to the current local
64  * standard time (i.e. not including any offset due to DST).
65  *
66  * Examples:
67  *
68  * Suppose we are in California, USA on January 1, 2013 at 04:00 PST (UTC-8, no
69  * DST in effect), corresponding to 12:00 UTC.  This function would then return
70  * -8 * SecondsPerHour, or -28800.
71  *
72  * Or suppose we are in Berlin, Germany on July 1, 2013 at 17:00 CEST (UTC+2,
73  * DST in effect), corresponding to 15:00 UTC.  This function would then return
74  * +1 * SecondsPerHour, or +3600.
75  */
76 static int32_t
UTCToLocalStandardOffsetSeconds()77 UTCToLocalStandardOffsetSeconds()
78 {
79     using js::SecondsPerDay;
80     using js::SecondsPerHour;
81     using js::SecondsPerMinute;
82 
83 #if defined(XP_WIN)
84     // Windows doesn't follow POSIX: updates to the TZ environment variable are
85     // not reflected immediately on that platform as they are on other systems
86     // without this call.
87     _tzset();
88 #endif
89 
90     // Get the current time.
91     time_t currentMaybeWithDST = time(nullptr);
92     if (currentMaybeWithDST == time_t(-1))
93         return 0;
94 
95     // Break down the current time into its (locally-valued, maybe with DST)
96     // components.
97     struct tm local;
98     if (!ComputeLocalTime(currentMaybeWithDST, &local))
99         return 0;
100 
101     // Compute a |time_t| corresponding to |local| interpreted without DST.
102     time_t currentNoDST;
103     if (local.tm_isdst == 0) {
104         // If |local| wasn't DST, we can use the same time.
105         currentNoDST = currentMaybeWithDST;
106     } else {
107         // If |local| respected DST, we need a time broken down into components
108         // ignoring DST.  Turn off DST in the broken-down time.
109         local.tm_isdst = 0;
110 
111         // Compute a |time_t t| corresponding to the broken-down time with DST
112         // off.  This has boundary-condition issues (for about the duration of
113         // a DST offset) near the time a location moves to a different time
114         // zone.  But 1) errors will be transient; 2) locations rarely change
115         // time zone; and 3) in the absence of an API that provides the time
116         // zone offset directly, this may be the best we can do.
117         currentNoDST = mktime(&local);
118         if (currentNoDST == time_t(-1))
119             return 0;
120     }
121 
122     // Break down the time corresponding to the no-DST |local| into UTC-based
123     // components.
124     struct tm utc;
125     if (!ComputeUTCTime(currentNoDST, &utc))
126         return 0;
127 
128     // Finally, compare the seconds-based components of the local non-DST
129     // representation and the UTC representation to determine the actual
130     // difference.
131     int utc_secs = utc.tm_hour * SecondsPerHour + utc.tm_min * SecondsPerMinute;
132     int local_secs = local.tm_hour * SecondsPerHour + local.tm_min * SecondsPerMinute;
133 
134     // Same-day?  Just subtract the seconds counts.
135     if (utc.tm_mday == local.tm_mday)
136         return local_secs - utc_secs;
137 
138     // If we have more UTC seconds, move local seconds into the UTC seconds'
139     // frame of reference and then subtract.
140     if (utc_secs > local_secs)
141         return (SecondsPerDay + local_secs) - utc_secs;
142 
143     // Otherwise we have more local seconds, so move the UTC seconds into the
144     // local seconds' frame of reference and then subtract.
145     return local_secs - (utc_secs + SecondsPerDay);
146 }
147 
148 void
internalUpdateTimeZoneAdjustment()149 js::DateTimeInfo::internalUpdateTimeZoneAdjustment()
150 {
151     /*
152      * The difference between local standard time and UTC will never change for
153      * a given time zone.
154      */
155     utcToLocalStandardOffsetSeconds = UTCToLocalStandardOffsetSeconds();
156 
157     double newTZA = utcToLocalStandardOffsetSeconds * msPerSecond;
158     if (newTZA == localTZA_)
159         return;
160 
161     localTZA_ = newTZA;
162 
163     /*
164      * The initial range values are carefully chosen to result in a cache miss
165      * on first use given the range of possible values.  Be careful to keep
166      * these values and the caching algorithm in sync!
167      */
168     offsetMilliseconds = 0;
169     rangeStartSeconds = rangeEndSeconds = INT64_MIN;
170     oldOffsetMilliseconds = 0;
171     oldRangeStartSeconds = oldRangeEndSeconds = INT64_MIN;
172 
173     sanityCheck();
174 }
175 
176 /*
177  * Since getDSTOffsetMilliseconds guarantees that all times seen will be
178  * positive, we can initialize the range at construction time with large
179  * negative numbers to ensure the first computation is always a cache miss and
180  * doesn't return a bogus offset.
181  */
182 /* static */ void
init()183 js::DateTimeInfo::init()
184 {
185     DateTimeInfo* dtInfo = &DateTimeInfo::instance;
186 
187     MOZ_ASSERT(dtInfo->localTZA_ == 0,
188                "we should be initializing only once, and the static instance "
189                "should have started out zeroed");
190 
191     // Set to a totally impossible TZA so that the comparison above will fail
192     // and all fields will be properly initialized.
193     dtInfo->localTZA_ = UnspecifiedNaN<double>();
194     dtInfo->internalUpdateTimeZoneAdjustment();
195 }
196 
197 int64_t
computeDSTOffsetMilliseconds(int64_t utcSeconds)198 js::DateTimeInfo::computeDSTOffsetMilliseconds(int64_t utcSeconds)
199 {
200     MOZ_ASSERT(utcSeconds >= 0);
201     MOZ_ASSERT(utcSeconds <= MaxUnixTimeT);
202 
203 #if defined(XP_WIN)
204     // Windows does not follow POSIX. Updates to the TZ environment variable
205     // are not reflected immediately on that platform as they are on UNIX
206     // systems without this call.
207     _tzset();
208 #endif
209 
210     struct tm tm;
211     if (!ComputeLocalTime(static_cast<time_t>(utcSeconds), &tm))
212         return 0;
213 
214     int32_t dayoff = int32_t((utcSeconds + utcToLocalStandardOffsetSeconds) % SecondsPerDay);
215     int32_t tmoff = tm.tm_sec + (tm.tm_min * SecondsPerMinute) + (tm.tm_hour * SecondsPerHour);
216 
217     int32_t diff = tmoff - dayoff;
218 
219     if (diff < 0)
220         diff += SecondsPerDay;
221 
222     return diff * msPerSecond;
223 }
224 
225 int64_t
internalGetDSTOffsetMilliseconds(int64_t utcMilliseconds)226 js::DateTimeInfo::internalGetDSTOffsetMilliseconds(int64_t utcMilliseconds)
227 {
228     sanityCheck();
229 
230     int64_t utcSeconds = utcMilliseconds / msPerSecond;
231 
232     if (utcSeconds > MaxUnixTimeT) {
233         utcSeconds = MaxUnixTimeT;
234     } else if (utcSeconds < 0) {
235         /* Go ahead a day to make localtime work (does not work with 0). */
236         utcSeconds = SecondsPerDay;
237     }
238 
239     /*
240      * NB: Be aware of the initial range values when making changes to this
241      *     code: the first call to this method, with those initial range
242      *     values, must result in a cache miss.
243      */
244 
245     if (rangeStartSeconds <= utcSeconds && utcSeconds <= rangeEndSeconds)
246         return offsetMilliseconds;
247 
248     if (oldRangeStartSeconds <= utcSeconds && utcSeconds <= oldRangeEndSeconds)
249         return oldOffsetMilliseconds;
250 
251     oldOffsetMilliseconds = offsetMilliseconds;
252     oldRangeStartSeconds = rangeStartSeconds;
253     oldRangeEndSeconds = rangeEndSeconds;
254 
255     if (rangeStartSeconds <= utcSeconds) {
256         int64_t newEndSeconds = Min(rangeEndSeconds + RangeExpansionAmount, MaxUnixTimeT);
257         if (newEndSeconds >= utcSeconds) {
258             int64_t endOffsetMilliseconds = computeDSTOffsetMilliseconds(newEndSeconds);
259             if (endOffsetMilliseconds == offsetMilliseconds) {
260                 rangeEndSeconds = newEndSeconds;
261                 return offsetMilliseconds;
262             }
263 
264             offsetMilliseconds = computeDSTOffsetMilliseconds(utcSeconds);
265             if (offsetMilliseconds == endOffsetMilliseconds) {
266                 rangeStartSeconds = utcSeconds;
267                 rangeEndSeconds = newEndSeconds;
268             } else {
269                 rangeEndSeconds = utcSeconds;
270             }
271             return offsetMilliseconds;
272         }
273 
274         offsetMilliseconds = computeDSTOffsetMilliseconds(utcSeconds);
275         rangeStartSeconds = rangeEndSeconds = utcSeconds;
276         return offsetMilliseconds;
277     }
278 
279     int64_t newStartSeconds = Max<int64_t>(rangeStartSeconds - RangeExpansionAmount, 0);
280     if (newStartSeconds <= utcSeconds) {
281         int64_t startOffsetMilliseconds = computeDSTOffsetMilliseconds(newStartSeconds);
282         if (startOffsetMilliseconds == offsetMilliseconds) {
283             rangeStartSeconds = newStartSeconds;
284             return offsetMilliseconds;
285         }
286 
287         offsetMilliseconds = computeDSTOffsetMilliseconds(utcSeconds);
288         if (offsetMilliseconds == startOffsetMilliseconds) {
289             rangeStartSeconds = newStartSeconds;
290             rangeEndSeconds = utcSeconds;
291         } else {
292             rangeStartSeconds = utcSeconds;
293         }
294         return offsetMilliseconds;
295     }
296 
297     rangeStartSeconds = rangeEndSeconds = utcSeconds;
298     offsetMilliseconds = computeDSTOffsetMilliseconds(utcSeconds);
299     return offsetMilliseconds;
300 }
301 
302 void
sanityCheck()303 js::DateTimeInfo::sanityCheck()
304 {
305     MOZ_ASSERT(rangeStartSeconds <= rangeEndSeconds);
306     MOZ_ASSERT_IF(rangeStartSeconds == INT64_MIN, rangeEndSeconds == INT64_MIN);
307     MOZ_ASSERT_IF(rangeEndSeconds == INT64_MIN, rangeStartSeconds == INT64_MIN);
308     MOZ_ASSERT_IF(rangeStartSeconds != INT64_MIN,
309                   rangeStartSeconds >= 0 && rangeEndSeconds >= 0);
310     MOZ_ASSERT_IF(rangeStartSeconds != INT64_MIN,
311                   rangeStartSeconds <= MaxUnixTimeT && rangeEndSeconds <= MaxUnixTimeT);
312 }
313 
314 static struct IcuTimeZoneInfo
315 {
316     Atomic<bool, ReleaseAcquire> locked;
317     enum { Valid = 0, NeedsUpdate } status;
318 
acquireIcuTimeZoneInfo319     void acquire() {
320         while (!locked.compareExchange(false, true))
321             continue;
322     }
323 
releaseIcuTimeZoneInfo324     void release() {
325         MOZ_ASSERT(locked, "should have been acquired");
326         locked = false;
327     }
328 } TZInfo;
329 
330 
JS_PUBLIC_API(void)331 JS_PUBLIC_API(void)
332 JS::ResetTimeZone()
333 {
334     js::DateTimeInfo::updateTimeZoneAdjustment();
335 
336 #if ENABLE_INTL_API && defined(ICU_TZ_HAS_RECREATE_DEFAULT)
337     TZInfo.acquire();
338     TZInfo.status = IcuTimeZoneInfo::NeedsUpdate;
339     TZInfo.release();
340 #endif
341 }
342 
343 void
ResyncICUDefaultTimeZone()344 js::ResyncICUDefaultTimeZone()
345 {
346 #if ENABLE_INTL_API && defined(ICU_TZ_HAS_RECREATE_DEFAULT)
347     TZInfo.acquire();
348     if (TZInfo.status == IcuTimeZoneInfo::NeedsUpdate) {
349         icu::TimeZone::recreateDefault();
350         TZInfo.status = IcuTimeZoneInfo::Valid;
351     }
352     TZInfo.release();
353 #endif
354 }
355