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