summaryrefslogtreecommitdiffstats
path: root/js/src/vm/DateTime.h
blob: 20feae33a82f1cd1262aa3679ad23aa75aaf0b38 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * vim: set ts=8 sts=2 et sw=2 tw=80:
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#ifndef vm_DateTime_h
#define vm_DateTime_h

#include "mozilla/UniquePtr.h"

#include <stdint.h>

#include "js/Utility.h"
#include "threading/ExclusiveData.h"

#if JS_HAS_INTL_API
#  include "mozilla/intl/ICU4CGlue.h"
#  include "mozilla/intl/TimeZone.h"
#endif

namespace JS {
class Realm;
}

namespace js {

/* Constants defined by ES5 15.9.1.10. */
constexpr double HoursPerDay = 24;
constexpr double MinutesPerHour = 60;
constexpr double SecondsPerMinute = 60;
constexpr double msPerSecond = 1000;
constexpr double msPerMinute = msPerSecond * SecondsPerMinute;
constexpr double msPerHour = msPerMinute * MinutesPerHour;

/* ES5 15.9.1.2. */
constexpr double msPerDay = msPerHour * HoursPerDay;

/*
 * Additional quantities not mentioned in the spec.  Be careful using these!
 * They aren't doubles and aren't defined in terms of all the other constants.
 * If you need constants that trigger floating point semantics, you'll have to
 * manually cast to get it.
 */
constexpr unsigned SecondsPerHour = 60 * 60;
constexpr unsigned SecondsPerDay = SecondsPerHour * 24;

constexpr double StartOfTime = -8.64e15;
constexpr double EndOfTime = 8.64e15;

extern bool InitDateTimeState();

extern void FinishDateTimeState();

enum class ResetTimeZoneMode : bool {
  DontResetIfOffsetUnchanged,
  ResetEvenIfOffsetUnchanged,
};

/**
 * Engine-internal variant of JS::ResetTimeZone with an additional flag to
 * control whether to forcibly reset all time zone data (this is the default
 * behavior when calling JS::ResetTimeZone) or to try to reuse the previous
 * time zone data.
 */
extern void ResetTimeZoneInternal(ResetTimeZoneMode mode);

/**
 * Stores date/time information, particularly concerning the current local
 * time zone, and implements a small cache for daylight saving time offset
 * computation.
 *
 * The basic idea is premised upon this fact: the DST offset never changes more
 * than once in any thirty-day period.  If we know the offset at t_0 is o_0,
 * the offset at [t_1, t_2] is also o_0, where t_1 + 3_0 days == t_2,
 * t_1 <= t_0, and t0 <= t2.  (In other words, t_0 is always somewhere within a
 * thirty-day range where the DST offset is constant: DST changes never occur
 * more than once in any thirty-day period.)  Therefore, if we intelligently
 * retain knowledge of the offset for a range of dates (which may vary over
 * time), and if requests are usually for dates within that range, we can often
 * provide a response without repeated offset calculation.
 *
 * Our caching strategy is as follows: on the first request at date t_0 compute
 * the requested offset o_0.  Save { start: t_0, end: t_0, offset: o_0 } as the
 * cache's state.  Subsequent requests within that range are straightforwardly
 * handled.  If a request for t_i is far outside the range (more than thirty
 * days), compute o_i = dstOffset(t_i) and save { start: t_i, end: t_i,
 * offset: t_i }.  Otherwise attempt to *overextend* the range to either
 * [start - 30d, end] or [start, end + 30d] as appropriate to encompass
 * t_i.  If the offset o_i30 is the same as the cached offset, extend the
 * range.  Otherwise the over-guess crossed a DST change -- compute
 * o_i = dstOffset(t_i) and either extend the original range (if o_i == offset)
 * or start a new one beneath/above the current one with o_i30 as the offset.
 *
 * This cache strategy results in 0 to 2 DST offset computations.  The naive
 * always-compute strategy is 1 computation, and since cache maintenance is a
 * handful of integer arithmetic instructions the speed difference between
 * always-1 and 1-with-cache is negligible.  Caching loses if two computations
 * happen: when the date is within 30 days of the cached range and when that
 * 30-day range crosses a DST change.  This is relatively uncommon.  Further,
 * instances of such are often dominated by in-range hits, so caching is an
 * overall slight win.
 *
 * Why 30 days?  For correctness the duration must be smaller than any possible
 * duration between DST changes.  Past that, note that 1) a large duration
 * increases the likelihood of crossing a DST change while reducing the number
 * of cache misses, and 2) a small duration decreases the size of the cached
 * range while producing more misses.  Using a month as the interval change is
 * a balance between these two that tries to optimize for the calendar month at
 * a time that a site might display.  (One could imagine an adaptive duration
 * that accommodates near-DST-change dates better; we don't believe the
 * potential win from better caching offsets the loss from extra complexity.)
 */
class DateTimeInfo {
 public:
  // Whether we should resist fingerprinting. For realms in RFP mode a separate
  // DateTimeInfo instance is used that is always in the UTC time zone.
  enum class ShouldRFP { No, Yes };

 private:
  static ExclusiveData<DateTimeInfo>* instance;
  static ExclusiveData<DateTimeInfo>* instanceRFP;

  friend class ExclusiveData<DateTimeInfo>;

  friend bool InitDateTimeState();
  friend void FinishDateTimeState();

  explicit DateTimeInfo(bool shouldResistFingerprinting);
  ~DateTimeInfo();

  static auto acquireLockWithValidTimeZone(ShouldRFP shouldRFP) {
    auto guard =
        shouldRFP == ShouldRFP::Yes ? instanceRFP->lock() : instance->lock();
    if (guard->timeZoneStatus_ != TimeZoneStatus::Valid) {
      guard->updateTimeZone();
    }
    return guard;
  }

 public:
  static ShouldRFP shouldRFP(JS::Realm* realm);

  // The spec implicitly assumes DST and time zone adjustment information
  // never change in the course of a function -- sometimes even across
  // reentrancy.  So make critical sections as narrow as possible.

  /**
   * Get the DST offset in milliseconds at a UTC time.  This is usually
   * either 0 or |msPerSecond * SecondsPerHour|, but at least one exotic time
   * zone (Lord Howe Island, Australia) has a fractional-hour offset, just to
   * keep things interesting.
   */
  static int32_t getDSTOffsetMilliseconds(ShouldRFP shouldRFP,
                                          int64_t utcMilliseconds) {
    auto guard = acquireLockWithValidTimeZone(shouldRFP);
    return guard->internalGetDSTOffsetMilliseconds(utcMilliseconds);
  }

  /**
   * The offset in seconds from the current UTC time to the current local
   * standard time (i.e. not including any offset due to DST) as computed by the
   * operating system.
   */
  static int32_t utcToLocalStandardOffsetSeconds(ShouldRFP shouldRFP) {
    auto guard = acquireLockWithValidTimeZone(shouldRFP);
    return guard->utcToLocalStandardOffsetSeconds_;
  }

#if JS_HAS_INTL_API
  enum class TimeZoneOffset { UTC, Local };

  /**
   * Return the time zone offset, including DST, in milliseconds at the
   * given time. The input time can be either at UTC or at local time.
   */
  static int32_t getOffsetMilliseconds(ShouldRFP shouldRFP,
                                       int64_t milliseconds,
                                       TimeZoneOffset offset) {
    auto guard = acquireLockWithValidTimeZone(shouldRFP);
    return guard->internalGetOffsetMilliseconds(milliseconds, offset);
  }

  /**
   * Copy the display name for the current time zone at the given time,
   * localized for the specified locale, into the supplied buffer. If the
   * buffer is too small, an empty string is stored. The stored display name
   * is null-terminated in any case.
   */
  static bool timeZoneDisplayName(ShouldRFP shouldRFP, char16_t* buf,
                                  size_t buflen, int64_t utcMilliseconds,
                                  const char* locale) {
    auto guard = acquireLockWithValidTimeZone(shouldRFP);
    return guard->internalTimeZoneDisplayName(buf, buflen, utcMilliseconds,
                                              locale);
  }

  /**
   * Copy the identifier for the current time zone to the provided resizable
   * buffer.
   */
  template <typename B>
  static mozilla::intl::ICUResult timeZoneId(ShouldRFP shouldRFP, B& buffer) {
    auto guard = acquireLockWithValidTimeZone(shouldRFP);
    return guard->timeZone()->GetId(buffer);
  }

  /**
   * A number indicating the raw offset from GMT in milliseconds.
   */
  static mozilla::Result<int32_t, mozilla::intl::ICUError> getRawOffsetMs(
      ShouldRFP shouldRFP) {
    auto guard = acquireLockWithValidTimeZone(shouldRFP);
    return guard->timeZone()->GetRawOffsetMs();
  }
#else
  /**
   * Return the local time zone adjustment (ES2019 20.3.1.7) as computed by
   * the operating system.
   */
  static int32_t localTZA(ShouldRFP shouldRFP) {
    return utcToLocalStandardOffsetSeconds(shouldRFP) * msPerSecond;
  }
#endif /* JS_HAS_INTL_API */

 private:
  // The method below should only be called via js::ResetTimeZoneInternal().
  friend void js::ResetTimeZoneInternal(ResetTimeZoneMode);

  static void resetTimeZone(ResetTimeZoneMode mode) {
    {
      auto guard = instance->lock();
      guard->internalResetTimeZone(mode);
    }
    {
      // Only needed to initialize the default state and any later call will
      // perform an unnecessary reset.
      auto guard = instanceRFP->lock();
      guard->internalResetTimeZone(mode);
    }
  }

  struct RangeCache {
    // Start and end offsets in seconds describing the current and the
    // last cached range.
    int64_t startSeconds, endSeconds;
    int64_t oldStartSeconds, oldEndSeconds;

    // The current and the last cached offset in milliseconds.
    int32_t offsetMilliseconds;
    int32_t oldOffsetMilliseconds;

    void reset();

    void sanityCheck();
  };

  bool shouldResistFingerprinting_;

  enum class TimeZoneStatus : uint8_t { Valid, NeedsUpdate, UpdateIfChanged };

  TimeZoneStatus timeZoneStatus_;

  /**
   * The offset in seconds from the current UTC time to the current local
   * standard time (i.e. not including any offset due to DST) as computed by the
   * operating system.
   *
   * Cached because retrieving this dynamically is Slow, and a certain venerable
   * benchmark which shall not be named depends on it being fast.
   *
   * SpiderMonkey occasionally and arbitrarily updates this value from the
   * system time zone to attempt to keep this reasonably up-to-date.  If
   * temporary inaccuracy can't be tolerated, JSAPI clients may call
   * JS::ResetTimeZone to forcibly sync this with the system time zone.
   *
   * In most cases this value is consistent with the raw time zone offset as
   * returned by the ICU default time zone (`icu::TimeZone::getRawOffset()`),
   * but it is possible to create cases where the operating system default time
   * zone differs from the ICU default time zone. For example ICU doesn't
   * support the full range of TZ environment variable settings, which can
   * result in <ctime> returning a different time zone than what's returned by
   * ICU. One example is "TZ=WGT3WGST,M3.5.0/-2,M10.5.0/-1", where <ctime>
   * returns -3 hours as the local offset, but ICU flat out rejects the TZ value
   * and instead infers the default time zone via "/etc/localtime" (on Unix).
   * This offset can also differ from ICU when the operating system and ICU use
   * different tzdata versions and the time zone rules of the current system
   * time zone have changed. Or, on Windows, when the Windows default time zone
   * can't be mapped to a IANA time zone, see for example
   * <https://unicode-org.atlassian.net/browse/ICU-13845>.
   *
   * When ICU is exclusively used for time zone computations, that means when
   * |JS_HAS_INTL_API| is true, this field is only used to detect system default
   * time zone changes. It must not be used to convert between local and UTC
   * time, because, as outlined above, this could lead to different results when
   * compared to ICU.
   */
  int32_t utcToLocalStandardOffsetSeconds_;

  RangeCache dstRange_;  // UTC-based ranges

#if JS_HAS_INTL_API
  // Use the full date-time range when we can use mozilla::intl::TimeZone.
  static constexpr int64_t MinTimeT =
      static_cast<int64_t>(StartOfTime / msPerSecond);
  static constexpr int64_t MaxTimeT =
      static_cast<int64_t>(EndOfTime / msPerSecond);

  RangeCache utcRange_;    // localtime-based ranges
  RangeCache localRange_;  // UTC-based ranges

  /**
   * The current time zone. Lazily constructed to avoid potential I/O access
   * when initializing this class.
   */
  mozilla::UniquePtr<mozilla::intl::TimeZone> timeZone_;

  /**
   * Cached names of the standard and daylight savings display names of the
   * current time zone for the default locale.
   */
  JS::UniqueChars locale_;
  JS::UniqueTwoByteChars standardName_;
  JS::UniqueTwoByteChars daylightSavingsName_;
#else
  // Restrict the data-time range to the minimum required time_t range as
  // specified in POSIX. Most operating systems support 64-bit time_t
  // values, but we currently still have some configurations which use
  // 32-bit time_t, e.g. the ARM simulator on 32-bit Linux (bug 1406993).
  // Bug 1406992 explores to use 64-bit time_t when supported by the
  // underlying operating system.
  static constexpr int64_t MinTimeT = 0;          /* time_t 01/01/1970 */
  static constexpr int64_t MaxTimeT = 2145830400; /* time_t 12/31/2037 */
#endif /* JS_HAS_INTL_API */

  static constexpr int64_t RangeExpansionAmount = 30 * SecondsPerDay;

  void internalResetTimeZone(ResetTimeZoneMode mode);

  void updateTimeZone();

  void internalResyncICUDefaultTimeZone();

  int64_t toClampedSeconds(int64_t milliseconds);

  using ComputeFn = int32_t (DateTimeInfo::*)(int64_t);

  /**
   * Get or compute an offset value for the requested seconds value.
   */
  int32_t getOrComputeValue(RangeCache& range, int64_t seconds,
                            ComputeFn compute);

  /**
   * Compute the DST offset at the given UTC time in seconds from the epoch.
   * (getDSTOffsetMilliseconds attempts to return a cached value from the
   * dstRange_ member, but in case of a cache miss it calls this method.)
   */
  int32_t computeDSTOffsetMilliseconds(int64_t utcSeconds);

  int32_t internalGetDSTOffsetMilliseconds(int64_t utcMilliseconds);

#if JS_HAS_INTL_API
  /**
   * Compute the UTC offset in milliseconds for the given local time. Called
   * by internalGetOffsetMilliseconds on a cache miss.
   */
  int32_t computeUTCOffsetMilliseconds(int64_t localSeconds);

  /**
   * Compute the local time offset in milliseconds for the given UTC time.
   * Called by internalGetOffsetMilliseconds on a cache miss.
   */
  int32_t computeLocalOffsetMilliseconds(int64_t utcSeconds);

  int32_t internalGetOffsetMilliseconds(int64_t milliseconds,
                                        TimeZoneOffset offset);

  bool internalTimeZoneDisplayName(char16_t* buf, size_t buflen,
                                   int64_t utcMilliseconds, const char* locale);

  mozilla::intl::TimeZone* timeZone();
#endif /* JS_HAS_INTL_API */
};

} /* namespace js */

#endif /* vm_DateTime_h */