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