1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2  * vim: set ts=8 sts=2 et sw=2 tw=80:
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/ScopeExit.h"
10 #include "mozilla/TextUtils.h"
11 
12 #include <algorithm>
13 #include <cstdlib>
14 #include <cstring>
15 #include <iterator>
16 #include <time.h>
17 
18 #if !defined(XP_WIN)
19 #  include <limits.h>
20 #  include <unistd.h>
21 #endif /* !defined(XP_WIN) */
22 
23 #include "js/Date.h"
24 #include "js/GCAPI.h"
25 #include "threading/ExclusiveData.h"
26 
27 #if JS_HAS_INTL_API && !MOZ_SYSTEM_ICU
28 #  include "unicode/basictz.h"
29 #  include "unicode/locid.h"
30 #  include "unicode/timezone.h"
31 #  include "unicode/unistr.h"
32 #endif /* JS_HAS_INTL_API && !MOZ_SYSTEM_ICU */
33 
34 #include "util/Text.h"
35 #include "vm/MutexIDs.h"
36 
ComputeLocalTime(time_t local,struct tm * ptm)37 static bool ComputeLocalTime(time_t local, struct tm* ptm) {
38 #if defined(_WIN32)
39   return localtime_s(ptm, &local) == 0;
40 #elif defined(HAVE_LOCALTIME_R)
41   return localtime_r(&local, ptm);
42 #else
43   struct tm* otm = localtime(&local);
44   if (!otm) {
45     return false;
46   }
47   *ptm = *otm;
48   return true;
49 #endif
50 }
51 
ComputeUTCTime(time_t t,struct tm * ptm)52 static bool ComputeUTCTime(time_t t, struct tm* ptm) {
53 #if defined(_WIN32)
54   return gmtime_s(ptm, &t) == 0;
55 #elif defined(HAVE_GMTIME_R)
56   return gmtime_r(&t, ptm);
57 #else
58   struct tm* otm = gmtime(&t);
59   if (!otm) {
60     return false;
61   }
62   *ptm = *otm;
63   return true;
64 #endif
65 }
66 
67 /*
68  * Compute the offset in seconds from the current UTC time to the current local
69  * standard time (i.e. not including any offset due to DST).
70  *
71  * Examples:
72  *
73  * Suppose we are in California, USA on January 1, 2013 at 04:00 PST (UTC-8, no
74  * DST in effect), corresponding to 12:00 UTC.  This function would then return
75  * -8 * SecondsPerHour, or -28800.
76  *
77  * Or suppose we are in Berlin, Germany on July 1, 2013 at 17:00 CEST (UTC+2,
78  * DST in effect), corresponding to 15:00 UTC.  This function would then return
79  * +1 * SecondsPerHour, or +3600.
80  */
UTCToLocalStandardOffsetSeconds()81 static int32_t UTCToLocalStandardOffsetSeconds() {
82   using js::SecondsPerDay;
83   using js::SecondsPerHour;
84   using js::SecondsPerMinute;
85 
86   // Get the current time.
87   time_t currentMaybeWithDST = time(nullptr);
88   if (currentMaybeWithDST == time_t(-1)) {
89     return 0;
90   }
91 
92   // Break down the current time into its (locally-valued, maybe with DST)
93   // components.
94   struct tm local;
95   if (!ComputeLocalTime(currentMaybeWithDST, &local)) {
96     return 0;
97   }
98 
99   // Compute a |time_t| corresponding to |local| interpreted without DST.
100   time_t currentNoDST;
101   if (local.tm_isdst == 0) {
102     // If |local| wasn't DST, we can use the same time.
103     currentNoDST = currentMaybeWithDST;
104   } else {
105     // If |local| respected DST, we need a time broken down into components
106     // ignoring DST.  Turn off DST in the broken-down time.  Create a fresh
107     // copy of |local|, because mktime() will reset tm_isdst = 1 and will
108     // adjust tm_hour and tm_hour accordingly.
109     struct tm localNoDST = local;
110     localNoDST.tm_isdst = 0;
111 
112     // Compute a |time_t t| corresponding to the broken-down time with DST
113     // off.  This has boundary-condition issues (for about the duration of
114     // a DST offset) near the time a location moves to a different time
115     // zone.  But 1) errors will be transient; 2) locations rarely change
116     // time zone; and 3) in the absence of an API that provides the time
117     // zone offset directly, this may be the best we can do.
118     currentNoDST = mktime(&localNoDST);
119     if (currentNoDST == time_t(-1)) {
120       return 0;
121     }
122   }
123 
124   // Break down the time corresponding to the no-DST |local| into UTC-based
125   // components.
126   struct tm utc;
127   if (!ComputeUTCTime(currentNoDST, &utc)) {
128     return 0;
129   }
130 
131   // Finally, compare the seconds-based components of the local non-DST
132   // representation and the UTC representation to determine the actual
133   // difference.
134   int utc_secs = utc.tm_hour * SecondsPerHour + utc.tm_min * SecondsPerMinute;
135   int local_secs =
136       local.tm_hour * SecondsPerHour + local.tm_min * SecondsPerMinute;
137 
138   // Same-day?  Just subtract the seconds counts.
139   if (utc.tm_mday == local.tm_mday) {
140     return local_secs - utc_secs;
141   }
142 
143   // If we have more UTC seconds, move local seconds into the UTC seconds'
144   // frame of reference and then subtract.
145   if (utc_secs > local_secs) {
146     return (SecondsPerDay + local_secs) - utc_secs;
147   }
148 
149   // Otherwise we have more local seconds, so move the UTC seconds into the
150   // local seconds' frame of reference and then subtract.
151   return local_secs - (utc_secs + SecondsPerDay);
152 }
153 
internalResetTimeZone(ResetTimeZoneMode mode)154 void js::DateTimeInfo::internalResetTimeZone(ResetTimeZoneMode mode) {
155   // Nothing to do when an update request is already enqueued.
156   if (timeZoneStatus_ == TimeZoneStatus::NeedsUpdate) {
157     return;
158   }
159 
160   // Mark the state as needing an update, but defer the actual update until it's
161   // actually needed to delay any system calls to the last possible moment. This
162   // is beneficial when this method is called during start-up, because it avoids
163   // main-thread I/O blocking the process.
164   if (mode == ResetTimeZoneMode::ResetEvenIfOffsetUnchanged) {
165     timeZoneStatus_ = TimeZoneStatus::NeedsUpdate;
166   } else {
167     timeZoneStatus_ = TimeZoneStatus::UpdateIfChanged;
168   }
169 }
170 
updateTimeZone()171 void js::DateTimeInfo::updateTimeZone() {
172   MOZ_ASSERT(timeZoneStatus_ != TimeZoneStatus::Valid);
173 
174   bool updateIfChanged = timeZoneStatus_ == TimeZoneStatus::UpdateIfChanged;
175 
176   timeZoneStatus_ = TimeZoneStatus::Valid;
177 
178   /*
179    * The difference between local standard time and UTC will never change for
180    * a given time zone.
181    */
182   int32_t newOffset = UTCToLocalStandardOffsetSeconds();
183 
184   if (updateIfChanged && newOffset == utcToLocalStandardOffsetSeconds_) {
185     return;
186   }
187 
188   utcToLocalStandardOffsetSeconds_ = newOffset;
189 
190   dstRange_.reset();
191 
192 #if JS_HAS_INTL_API && !MOZ_SYSTEM_ICU
193   utcRange_.reset();
194   localRange_.reset();
195 
196   {
197     // Tell the analysis the |pFree| function pointer called by uprv_free
198     // cannot GC.
199     JS::AutoSuppressGCAnalysis nogc;
200 
201     timeZone_ = nullptr;
202   }
203 
204   standardName_ = nullptr;
205   daylightSavingsName_ = nullptr;
206 #endif /* JS_HAS_INTL_API && !MOZ_SYSTEM_ICU */
207 
208   // Propagate the time zone change to ICU, too.
209   {
210     // Tell the analysis calling into ICU cannot GC.
211     JS::AutoSuppressGCAnalysis nogc;
212 
213     internalResyncICUDefaultTimeZone();
214   }
215 }
216 
DateTimeInfo()217 js::DateTimeInfo::DateTimeInfo() {
218   // Set the time zone status into the invalid state, so we compute the actual
219   // defaults on first access. We don't yet want to initialize neither <ctime>
220   // nor ICU's time zone classes, because that may cause I/O operations slowing
221   // down the JS engine initialization, which we're currently in the middle of.
222   timeZoneStatus_ = TimeZoneStatus::NeedsUpdate;
223 }
224 
225 js::DateTimeInfo::~DateTimeInfo() = default;
226 
toClampedSeconds(int64_t milliseconds)227 int64_t js::DateTimeInfo::toClampedSeconds(int64_t milliseconds) {
228   int64_t seconds = milliseconds / msPerSecond;
229   if (seconds > MaxTimeT) {
230     seconds = MaxTimeT;
231   } else if (seconds < MinTimeT) {
232     /* Go ahead a day to make localtime work (does not work with 0). */
233     seconds = SecondsPerDay;
234   }
235   return seconds;
236 }
237 
computeDSTOffsetMilliseconds(int64_t utcSeconds)238 int32_t js::DateTimeInfo::computeDSTOffsetMilliseconds(int64_t utcSeconds) {
239   MOZ_ASSERT(utcSeconds >= MinTimeT);
240   MOZ_ASSERT(utcSeconds <= MaxTimeT);
241 
242 #if JS_HAS_INTL_API && !MOZ_SYSTEM_ICU
243   UDate date = UDate(utcSeconds * msPerSecond);
244   constexpr bool dateIsLocalTime = false;
245   int32_t rawOffset, dstOffset;
246   UErrorCode status = U_ZERO_ERROR;
247 
248   timeZone()->getOffset(date, dateIsLocalTime, rawOffset, dstOffset, status);
249   if (U_FAILURE(status)) {
250     return 0;
251   }
252 
253   return dstOffset;
254 #else
255   struct tm tm;
256   if (!ComputeLocalTime(static_cast<time_t>(utcSeconds), &tm)) {
257     return 0;
258   }
259 
260   // NB: The offset isn't computed correctly when the standard local offset
261   //     at |utcSeconds| is different from |utcToLocalStandardOffsetSeconds|.
262   int32_t dayoff =
263       int32_t((utcSeconds + utcToLocalStandardOffsetSeconds_) % SecondsPerDay);
264   int32_t tmoff = tm.tm_sec + (tm.tm_min * SecondsPerMinute) +
265                   (tm.tm_hour * SecondsPerHour);
266 
267   int32_t diff = tmoff - dayoff;
268 
269   if (diff < 0) {
270     diff += SecondsPerDay;
271   } else if (uint32_t(diff) >= SecondsPerDay) {
272     diff -= SecondsPerDay;
273   }
274 
275   return diff * msPerSecond;
276 #endif /* JS_HAS_INTL_API && !MOZ_SYSTEM_ICU */
277 }
278 
internalGetDSTOffsetMilliseconds(int64_t utcMilliseconds)279 int32_t js::DateTimeInfo::internalGetDSTOffsetMilliseconds(
280     int64_t utcMilliseconds) {
281   int64_t utcSeconds = toClampedSeconds(utcMilliseconds);
282   return getOrComputeValue(dstRange_, utcSeconds,
283                            &DateTimeInfo::computeDSTOffsetMilliseconds);
284 }
285 
getOrComputeValue(RangeCache & range,int64_t seconds,ComputeFn compute)286 int32_t js::DateTimeInfo::getOrComputeValue(RangeCache& range, int64_t seconds,
287                                             ComputeFn compute) {
288   range.sanityCheck();
289 
290   auto checkSanity =
291       mozilla::MakeScopeExit([&range]() { range.sanityCheck(); });
292 
293   // NB: Be aware of the initial range values when making changes to this
294   //     code: the first call to this method, with those initial range
295   //     values, must result in a cache miss.
296   MOZ_ASSERT(seconds != INT64_MIN);
297 
298   if (range.startSeconds <= seconds && seconds <= range.endSeconds) {
299     return range.offsetMilliseconds;
300   }
301 
302   if (range.oldStartSeconds <= seconds && seconds <= range.oldEndSeconds) {
303     return range.oldOffsetMilliseconds;
304   }
305 
306   range.oldOffsetMilliseconds = range.offsetMilliseconds;
307   range.oldStartSeconds = range.startSeconds;
308   range.oldEndSeconds = range.endSeconds;
309 
310   if (range.startSeconds <= seconds) {
311     int64_t newEndSeconds =
312         std::min({range.endSeconds + RangeExpansionAmount, MaxTimeT});
313     if (newEndSeconds >= seconds) {
314       int32_t endOffsetMilliseconds = (this->*compute)(newEndSeconds);
315       if (endOffsetMilliseconds == range.offsetMilliseconds) {
316         range.endSeconds = newEndSeconds;
317         return range.offsetMilliseconds;
318       }
319 
320       range.offsetMilliseconds = (this->*compute)(seconds);
321       if (range.offsetMilliseconds == endOffsetMilliseconds) {
322         range.startSeconds = seconds;
323         range.endSeconds = newEndSeconds;
324       } else {
325         range.endSeconds = seconds;
326       }
327       return range.offsetMilliseconds;
328     }
329 
330     range.offsetMilliseconds = (this->*compute)(seconds);
331     range.startSeconds = range.endSeconds = seconds;
332     return range.offsetMilliseconds;
333   }
334 
335   int64_t newStartSeconds =
336       std::max<int64_t>({range.startSeconds - RangeExpansionAmount, MinTimeT});
337   if (newStartSeconds <= seconds) {
338     int32_t startOffsetMilliseconds = (this->*compute)(newStartSeconds);
339     if (startOffsetMilliseconds == range.offsetMilliseconds) {
340       range.startSeconds = newStartSeconds;
341       return range.offsetMilliseconds;
342     }
343 
344     range.offsetMilliseconds = (this->*compute)(seconds);
345     if (range.offsetMilliseconds == startOffsetMilliseconds) {
346       range.startSeconds = newStartSeconds;
347       range.endSeconds = seconds;
348     } else {
349       range.startSeconds = seconds;
350     }
351     return range.offsetMilliseconds;
352   }
353 
354   range.startSeconds = range.endSeconds = seconds;
355   range.offsetMilliseconds = (this->*compute)(seconds);
356   return range.offsetMilliseconds;
357 }
358 
reset()359 void js::DateTimeInfo::RangeCache::reset() {
360   // The initial range values are carefully chosen to result in a cache miss
361   // on first use given the range of possible values. Be careful to keep
362   // these values and the caching algorithm in sync!
363   offsetMilliseconds = 0;
364   startSeconds = endSeconds = INT64_MIN;
365   oldOffsetMilliseconds = 0;
366   oldStartSeconds = oldEndSeconds = INT64_MIN;
367 
368   sanityCheck();
369 }
370 
sanityCheck()371 void js::DateTimeInfo::RangeCache::sanityCheck() {
372   auto assertRange = [](int64_t start, int64_t end) {
373     MOZ_ASSERT(start <= end);
374     MOZ_ASSERT_IF(start == INT64_MIN, end == INT64_MIN);
375     MOZ_ASSERT_IF(end == INT64_MIN, start == INT64_MIN);
376     MOZ_ASSERT_IF(start != INT64_MIN, start >= MinTimeT && end >= MinTimeT);
377     MOZ_ASSERT_IF(start != INT64_MIN, start <= MaxTimeT && end <= MaxTimeT);
378   };
379 
380   assertRange(startSeconds, endSeconds);
381   assertRange(oldStartSeconds, oldEndSeconds);
382 }
383 
384 #if JS_HAS_INTL_API && !MOZ_SYSTEM_ICU
computeUTCOffsetMilliseconds(int64_t localSeconds)385 int32_t js::DateTimeInfo::computeUTCOffsetMilliseconds(int64_t localSeconds) {
386   MOZ_ASSERT(localSeconds >= MinTimeT);
387   MOZ_ASSERT(localSeconds <= MaxTimeT);
388 
389   UDate date = UDate(localSeconds * msPerSecond);
390 
391   // ES2019 draft rev 0ceb728a1adbffe42b26972a6541fd7f398b1557
392   //
393   // 20.3.1.7 LocalTZA
394   //
395   // If |localSeconds| represents either a skipped (at a positive time zone
396   // transition) or repeated (at a negative time zone transition) locale
397   // time, it must be interpreted as a time value before the transition.
398   constexpr int32_t skippedTime = icu::BasicTimeZone::kFormer;
399   constexpr int32_t repeatedTime = icu::BasicTimeZone::kFormer;
400 
401   int32_t rawOffset, dstOffset;
402   UErrorCode status = U_ZERO_ERROR;
403 
404   // All ICU TimeZone classes derive from BasicTimeZone, so we can safely
405   // perform the static_cast.
406   // Once <https://unicode-org.atlassian.net/browse/ICU-13705> is fixed we
407   // can remove this extra cast.
408   auto* basicTz = static_cast<icu::BasicTimeZone*>(timeZone());
409   basicTz->getOffsetFromLocal(date, skippedTime, repeatedTime, rawOffset,
410                               dstOffset, status);
411   if (U_FAILURE(status)) {
412     return 0;
413   }
414 
415   return rawOffset + dstOffset;
416 }
417 
computeLocalOffsetMilliseconds(int64_t utcSeconds)418 int32_t js::DateTimeInfo::computeLocalOffsetMilliseconds(int64_t utcSeconds) {
419   MOZ_ASSERT(utcSeconds >= MinTimeT);
420   MOZ_ASSERT(utcSeconds <= MaxTimeT);
421 
422   UDate date = UDate(utcSeconds * msPerSecond);
423   constexpr bool dateIsLocalTime = false;
424   int32_t rawOffset, dstOffset;
425   UErrorCode status = U_ZERO_ERROR;
426 
427   timeZone()->getOffset(date, dateIsLocalTime, rawOffset, dstOffset, status);
428   if (U_FAILURE(status)) {
429     return 0;
430   }
431 
432   return rawOffset + dstOffset;
433 }
434 
internalGetOffsetMilliseconds(int64_t milliseconds,TimeZoneOffset offset)435 int32_t js::DateTimeInfo::internalGetOffsetMilliseconds(int64_t milliseconds,
436                                                         TimeZoneOffset offset) {
437   int64_t seconds = toClampedSeconds(milliseconds);
438   return offset == TimeZoneOffset::UTC
439              ? getOrComputeValue(localRange_, seconds,
440                                  &DateTimeInfo::computeLocalOffsetMilliseconds)
441              : getOrComputeValue(utcRange_, seconds,
442                                  &DateTimeInfo::computeUTCOffsetMilliseconds);
443 }
444 
internalTimeZoneDisplayName(char16_t * buf,size_t buflen,int64_t utcMilliseconds,const char * locale)445 bool js::DateTimeInfo::internalTimeZoneDisplayName(char16_t* buf, size_t buflen,
446                                                    int64_t utcMilliseconds,
447                                                    const char* locale) {
448   MOZ_ASSERT(buf != nullptr);
449   MOZ_ASSERT(buflen > 0);
450   MOZ_ASSERT(locale != nullptr);
451 
452   // Clear any previously cached names when the default locale changed.
453   if (!locale_ || std::strcmp(locale_.get(), locale) != 0) {
454     locale_ = DuplicateString(locale);
455     if (!locale_) {
456       return false;
457     }
458 
459     standardName_.reset();
460     daylightSavingsName_.reset();
461   }
462 
463   bool daylightSavings = internalGetDSTOffsetMilliseconds(utcMilliseconds) != 0;
464 
465   JS::UniqueTwoByteChars& cachedName =
466       daylightSavings ? daylightSavingsName_ : standardName_;
467   if (!cachedName) {
468     // Retrieve the display name for the given locale.
469     icu::UnicodeString displayName;
470     timeZone()->getDisplayName(daylightSavings, icu::TimeZone::LONG,
471                                icu::Locale(locale), displayName);
472 
473     size_t capacity = displayName.length() + 1;  // Null-terminate.
474     JS::UniqueTwoByteChars displayNameChars(js_pod_malloc<char16_t>(capacity));
475     if (!displayNameChars) {
476       return false;
477     }
478 
479     // Copy the display name. This operation always succeeds because the
480     // destination buffer is large enough to hold the complete string.
481     UErrorCode status = U_ZERO_ERROR;
482     displayName.extract(displayNameChars.get(), capacity, status);
483     MOZ_ASSERT(U_SUCCESS(status));
484     MOZ_ASSERT(displayNameChars[capacity - 1] == '\0');
485 
486     cachedName = std::move(displayNameChars);
487   }
488 
489   // Return an empty string if the display name doesn't fit into the buffer.
490   size_t length = js_strlen(cachedName.get());
491   if (length < buflen) {
492     std::copy(cachedName.get(), cachedName.get() + length, buf);
493   } else {
494     length = 0;
495   }
496 
497   buf[length] = '\0';
498   return true;
499 }
500 
timeZone()501 icu::TimeZone* js::DateTimeInfo::timeZone() {
502   if (!timeZone_) {
503     timeZone_.reset(icu::TimeZone::createDefault());
504     MOZ_ASSERT(timeZone_);
505   }
506 
507   return timeZone_.get();
508 }
509 #endif /* JS_HAS_INTL_API && !MOZ_SYSTEM_ICU */
510 
511 /* static */ js::ExclusiveData<js::DateTimeInfo>* js::DateTimeInfo::instance;
512 
InitDateTimeState()513 bool js::InitDateTimeState() {
514   MOZ_ASSERT(!DateTimeInfo::instance, "we should be initializing only once");
515 
516   DateTimeInfo::instance =
517       js_new<ExclusiveData<DateTimeInfo>>(mutexid::DateTimeInfoMutex);
518   return !!DateTimeInfo::instance;
519 }
520 
521 /* static */
FinishDateTimeState()522 void js::FinishDateTimeState() {
523   js_delete(DateTimeInfo::instance);
524   DateTimeInfo::instance = nullptr;
525 }
526 
ResetTimeZoneInternal(ResetTimeZoneMode mode)527 void js::ResetTimeZoneInternal(ResetTimeZoneMode mode) {
528   js::DateTimeInfo::resetTimeZone(mode);
529 }
530 
ResetTimeZone()531 JS_PUBLIC_API void JS::ResetTimeZone() {
532   js::ResetTimeZoneInternal(js::ResetTimeZoneMode::ResetEvenIfOffsetUnchanged);
533 }
534 
535 #if defined(XP_WIN)
IsOlsonCompatibleWindowsTimeZoneId(const char * tz)536 static bool IsOlsonCompatibleWindowsTimeZoneId(const char* tz) {
537   // ICU ignores the TZ environment variable on Windows and instead directly
538   // invokes Win API functions to retrieve the current time zone. But since
539   // we're still using the POSIX-derived localtime_s() function on Windows
540   // and localtime_s() does return a time zone adjusted value based on the
541   // TZ environment variable, we need to manually adjust the default ICU
542   // time zone if TZ is set.
543   //
544   // Windows supports the following format for TZ: tzn[+|-]hh[:mm[:ss]][dzn]
545   // where "tzn" is the time zone name for standard time, the time zone
546   // offset is positive for time zones west of GMT, and "dzn" is the
547   // optional time zone name when daylight savings are observed. Daylight
548   // savings are always based on the U.S. daylight saving rules, that means
549   // for example it's not possible to use "TZ=CET-1CEST" to select the IANA
550   // time zone "CET".
551   //
552   // When comparing this restricted format for TZ to all IANA time zone
553   // names, the following time zones are in the intersection of what's
554   // supported by Windows and is also a valid IANA time zone identifier.
555   //
556   // Even though the time zone offset is marked as mandatory on MSDN, it
557   // appears it defaults to zero when omitted. This in turn means we can
558   // also allow the time zone identifiers "UCT", "UTC", and "GMT".
559 
560   static const char* const allowedIds[] = {
561       // From tzdata's "northamerica" file:
562       "EST5EDT",
563       "CST6CDT",
564       "MST7MDT",
565       "PST8PDT",
566 
567       // From tzdata's "backward" file:
568       "GMT+0",
569       "GMT-0",
570       "GMT0",
571       "UCT",
572       "UTC",
573 
574       // From tzdata's "etcetera" file:
575       "GMT",
576   };
577   for (const auto& allowedId : allowedIds) {
578     if (std::strcmp(allowedId, tz) == 0) {
579       return true;
580     }
581   }
582   return false;
583 }
584 #elif JS_HAS_INTL_API && !MOZ_SYSTEM_ICU
TZContainsAbsolutePath(const char * tzVar)585 static inline const char* TZContainsAbsolutePath(const char* tzVar) {
586   // A TZ environment variable may be an absolute path. The path
587   // format of TZ may begin with a colon. (ICU handles relative paths.)
588   if (tzVar[0] == ':' && tzVar[1] == '/') {
589     return tzVar + 1;
590   }
591   if (tzVar[0] == '/') {
592     return tzVar;
593   }
594   return nullptr;
595 }
596 
597 /**
598  * Reject the input if it doesn't match the time zone id pattern or legacy time
599  * zone names.
600  *
601  * See <https://github.com/eggert/tz/blob/master/theory.html>.
602  */
MaybeTimeZoneId(const char * timeZone)603 static icu::UnicodeString MaybeTimeZoneId(const char* timeZone) {
604   size_t timeZoneLen = std::strlen(timeZone);
605 
606   for (size_t i = 0; i < timeZoneLen; i++) {
607     char c = timeZone[i];
608 
609     // According to theory.html, '.' is allowed in time zone ids, but the
610     // accompanying zic.c file doesn't allow it. Assume the source file is
611     // correct and disallow '.' here, too.
612     if (mozilla::IsAsciiAlphanumeric(c) || c == '_' || c == '-' || c == '+') {
613       continue;
614     }
615 
616     // Reject leading, trailing, or consecutive '/' characters.
617     if (c == '/' && i > 0 && i + 1 < timeZoneLen && timeZone[i + 1] != '/') {
618       continue;
619     }
620 
621     return icu::UnicodeString();
622   }
623 
624   return icu::UnicodeString(timeZone, timeZoneLen, US_INV);
625 }
626 
627 /**
628  * Given a presumptive path |tz| to a zoneinfo time zone file
629  * (e.g. /etc/localtime), attempt to compute the time zone encoded by that
630  * path by repeatedly resolving symlinks until a path containing "/zoneinfo/"
631  * followed by time zone looking components is found. If a symlink is broken,
632  * symlink-following recurs too deeply, non time zone looking components are
633  * encountered, or some other error is encountered, return the empty string.
634  *
635  * If a non-empty string is returned, it's only guaranteed to have certain
636  * syntactic validity. It might not actually *be* a time zone name.
637  */
ReadTimeZoneLink(const char * tz)638 static icu::UnicodeString ReadTimeZoneLink(const char* tz) {
639   // The resolved link name can have different paths depending on the OS.
640   // Follow ICU and only search for "/zoneinfo/"; see $ICU/common/putil.cpp.
641   static constexpr char ZoneInfoPath[] = "/zoneinfo/";
642   constexpr size_t ZoneInfoPathLength = js_strlen(ZoneInfoPath);
643 
644   // Stop following symlinks after a fixed depth, because some common time
645   // zones are stored in files whose name doesn't match an Olson time zone
646   // name. For example on Ubuntu, "/usr/share/zoneinfo/America/New_York" is a
647   // symlink to "/usr/share/zoneinfo/posixrules" and "posixrules" is not an
648   // Olson time zone name.
649   // Four hops should be a reasonable limit for most use cases.
650   constexpr uint32_t FollowDepthLimit = 4;
651 
652 #  ifdef PATH_MAX
653   constexpr size_t PathMax = PATH_MAX;
654 #  else
655   constexpr size_t PathMax = 4096;
656 #  endif
657   static_assert(PathMax > 0, "PathMax should be larger than zero");
658 
659   char linkName[PathMax];
660   constexpr size_t linkNameLen =
661       std::size(linkName) - 1;  // -1 to null-terminate.
662 
663   // Return if the TZ value is too large.
664   if (std::strlen(tz) > linkNameLen) {
665     return icu::UnicodeString();
666   }
667 
668   std::strcpy(linkName, tz);
669 
670   char linkTarget[PathMax];
671   constexpr size_t linkTargetLen =
672       std::size(linkTarget) - 1;  // -1 to null-terminate.
673 
674   uint32_t depth = 0;
675 
676   // Search until we find "/zoneinfo/" in the link name.
677   const char* timeZoneWithZoneInfo;
678   while (!(timeZoneWithZoneInfo = std::strstr(linkName, ZoneInfoPath))) {
679     // Return if the symlink nesting is too deep.
680     if (++depth > FollowDepthLimit) {
681       return icu::UnicodeString();
682     }
683 
684     // Return on error or if the result was truncated.
685     ssize_t slen = readlink(linkName, linkTarget, linkTargetLen);
686     if (slen < 0 || size_t(slen) >= linkTargetLen) {
687       return icu::UnicodeString();
688     }
689 
690     // Ensure linkTarget is null-terminated. (readlink may not necessarily
691     // null-terminate the string.)
692     size_t len = size_t(slen);
693     linkTarget[len] = '\0';
694 
695     // If the target is absolute, continue with that.
696     if (linkTarget[0] == '/') {
697       std::strcpy(linkName, linkTarget);
698       continue;
699     }
700 
701     // If the target is relative, it must be resolved against either the
702     // directory the link was in, or against the current working directory.
703     char* separator = std::strrchr(linkName, '/');
704 
705     // If the link name is just something like "foo", resolve linkTarget
706     // against the current working directory.
707     if (!separator) {
708       std::strcpy(linkName, linkTarget);
709       continue;
710     }
711 
712     // Remove everything after the final path separator in linkName.
713     separator[1] = '\0';
714 
715     // Return if the concatenated path name is too large.
716     if (std::strlen(linkName) + len > linkNameLen) {
717       return icu::UnicodeString();
718     }
719 
720     // Keep it simple and just concatenate the path names.
721     std::strcat(linkName, linkTarget);
722   }
723 
724   const char* timeZone = timeZoneWithZoneInfo + ZoneInfoPathLength;
725   return MaybeTimeZoneId(timeZone);
726 }
727 #endif /* JS_HAS_INTL_API && !MOZ_SYSTEM_ICU */
728 
ResyncICUDefaultTimeZone()729 void js::ResyncICUDefaultTimeZone() {
730   js::DateTimeInfo::resyncICUDefaultTimeZone();
731 }
732 
internalResyncICUDefaultTimeZone()733 void js::DateTimeInfo::internalResyncICUDefaultTimeZone() {
734 #if JS_HAS_INTL_API && !MOZ_SYSTEM_ICU
735   if (const char* tz = std::getenv("TZ")) {
736     icu::UnicodeString tzid;
737 
738 #  if defined(XP_WIN)
739     // If TZ is set and its value is valid under Windows' and IANA's time zone
740     // identifier rules, update the ICU default time zone to use this value.
741     if (IsOlsonCompatibleWindowsTimeZoneId(tz)) {
742       tzid.setTo(icu::UnicodeString(tz, -1, US_INV));
743     } else {
744       // If |tz| isn't a supported time zone identifier, use the default Windows
745       // time zone for ICU.
746       // TODO: Handle invalid time zone identifiers (bug 342068).
747     }
748 #  else
749     // The TZ environment variable allows both absolute and relative paths,
750     // optionally beginning with a colon (':'). (Relative paths, without the
751     // colon, are just Olson time zone names.)  We need to handle absolute paths
752     // ourselves, including handling that they might be symlinks.
753     // <https://unicode-org.atlassian.net/browse/ICU-13694>
754     if (const char* tzlink = TZContainsAbsolutePath(tz)) {
755       tzid.setTo(ReadTimeZoneLink(tzlink));
756     }
757 
758 #    ifdef ANDROID
759     // ICU ignores the TZ environment variable on Android. If it doesn't contain
760     // an absolute path, try to parse it as a time zone name.
761     else {
762       tzid.setTo(MaybeTimeZoneId(tz));
763     }
764 #    endif
765 #  endif /* defined(XP_WIN) */
766 
767     if (!tzid.isEmpty()) {
768       mozilla::UniquePtr<icu::TimeZone> newTimeZone(
769           icu::TimeZone::createTimeZone(tzid));
770       MOZ_ASSERT(newTimeZone);
771       if (*newTimeZone != icu::TimeZone::getUnknown()) {
772         // adoptDefault() takes ownership of the time zone.
773         icu::TimeZone::adoptDefault(newTimeZone.release());
774         return;
775       }
776     }
777   }
778 
779   if (icu::TimeZone* defaultZone = icu::TimeZone::detectHostTimeZone()) {
780     icu::TimeZone::adoptDefault(defaultZone);
781   }
782 #endif
783 }
784