summaryrefslogtreecommitdiffstats
path: root/js/src/vm/DateTime.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/vm/DateTime.cpp')
-rw-r--r--js/src/vm/DateTime.cpp822
1 files changed, 822 insertions, 0 deletions
diff --git a/js/src/vm/DateTime.cpp b/js/src/vm/DateTime.cpp
new file mode 100644
index 0000000000..21ecc2e9f5
--- /dev/null
+++ b/js/src/vm/DateTime.cpp
@@ -0,0 +1,822 @@
+/* -*- 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/. */
+
+#include "vm/DateTime.h"
+
+#if JS_HAS_INTL_API
+# include "mozilla/intl/TimeZone.h"
+#endif
+#include "mozilla/ScopeExit.h"
+#include "mozilla/TextUtils.h"
+
+#include <algorithm>
+#include <cstdlib>
+#include <cstring>
+#include <iterator>
+#include <string_view>
+#include <time.h>
+
+#if !defined(XP_WIN)
+# include <limits.h>
+# include <unistd.h>
+#endif /* !defined(XP_WIN) */
+
+#if JS_HAS_INTL_API
+# include "builtin/intl/FormatBuffer.h"
+#endif
+#include "js/AllocPolicy.h"
+#include "js/Date.h"
+#include "js/GCAPI.h"
+#include "js/Vector.h"
+#include "threading/ExclusiveData.h"
+
+#include "util/Text.h"
+#include "vm/MutexIDs.h"
+#include "vm/Realm.h"
+
+/* static */
+js::DateTimeInfo::ForceUTC js::DateTimeInfo::forceUTC(JS::Realm* realm) {
+ return realm->creationOptions().forceUTC() ? DateTimeInfo::ForceUTC::Yes
+ : DateTimeInfo::ForceUTC::No;
+}
+
+static bool ComputeLocalTime(time_t local, struct tm* ptm) {
+ // Neither localtime_s nor localtime_r are required to act as if tzset has
+ // been called, therefore we need to explicitly call it to ensure any time
+ // zone changes are correctly picked up.
+
+#if defined(_WIN32)
+ _tzset();
+ return localtime_s(ptm, &local) == 0;
+#elif defined(HAVE_LOCALTIME_R)
+# ifndef __wasi__
+ tzset();
+# endif
+ return localtime_r(&local, ptm);
+#else
+ struct tm* otm = localtime(&local);
+ if (!otm) {
+ return false;
+ }
+ *ptm = *otm;
+ return true;
+#endif
+}
+
+static bool ComputeUTCTime(time_t t, struct tm* ptm) {
+#if defined(_WIN32)
+ return gmtime_s(ptm, &t) == 0;
+#elif defined(HAVE_GMTIME_R)
+ return gmtime_r(&t, ptm);
+#else
+ struct tm* otm = gmtime(&t);
+ if (!otm) {
+ return false;
+ }
+ *ptm = *otm;
+ return true;
+#endif
+}
+
+/*
+ * Compute the offset in seconds from the current UTC time to the current local
+ * standard time (i.e. not including any offset due to DST).
+ *
+ * Examples:
+ *
+ * Suppose we are in California, USA on January 1, 2013 at 04:00 PST (UTC-8, no
+ * DST in effect), corresponding to 12:00 UTC. This function would then return
+ * -8 * SecondsPerHour, or -28800.
+ *
+ * Or suppose we are in Berlin, Germany on July 1, 2013 at 17:00 CEST (UTC+2,
+ * DST in effect), corresponding to 15:00 UTC. This function would then return
+ * +1 * SecondsPerHour, or +3600.
+ */
+static int32_t UTCToLocalStandardOffsetSeconds() {
+ using js::SecondsPerDay;
+ using js::SecondsPerHour;
+ using js::SecondsPerMinute;
+
+ // Get the current time.
+ time_t currentMaybeWithDST = time(nullptr);
+ if (currentMaybeWithDST == time_t(-1)) {
+ return 0;
+ }
+
+ // Break down the current time into its (locally-valued, maybe with DST)
+ // components.
+ struct tm local;
+ if (!ComputeLocalTime(currentMaybeWithDST, &local)) {
+ return 0;
+ }
+
+ // Compute a |time_t| corresponding to |local| interpreted without DST.
+ time_t currentNoDST;
+ if (local.tm_isdst == 0) {
+ // If |local| wasn't DST, we can use the same time.
+ currentNoDST = currentMaybeWithDST;
+ } else {
+ // If |local| respected DST, we need a time broken down into components
+ // ignoring DST. Turn off DST in the broken-down time. Create a fresh
+ // copy of |local|, because mktime() will reset tm_isdst = 1 and will
+ // adjust tm_hour and tm_hour accordingly.
+ struct tm localNoDST = local;
+ localNoDST.tm_isdst = 0;
+
+ // Compute a |time_t t| corresponding to the broken-down time with DST
+ // off. This has boundary-condition issues (for about the duration of
+ // a DST offset) near the time a location moves to a different time
+ // zone. But 1) errors will be transient; 2) locations rarely change
+ // time zone; and 3) in the absence of an API that provides the time
+ // zone offset directly, this may be the best we can do.
+ currentNoDST = mktime(&localNoDST);
+ if (currentNoDST == time_t(-1)) {
+ return 0;
+ }
+ }
+
+ // Break down the time corresponding to the no-DST |local| into UTC-based
+ // components.
+ struct tm utc;
+ if (!ComputeUTCTime(currentNoDST, &utc)) {
+ return 0;
+ }
+
+ // Finally, compare the seconds-based components of the local non-DST
+ // representation and the UTC representation to determine the actual
+ // difference.
+ int utc_secs =
+ utc.tm_hour * SecondsPerHour + utc.tm_min * int(SecondsPerMinute);
+ int local_secs =
+ local.tm_hour * SecondsPerHour + local.tm_min * int(SecondsPerMinute);
+
+ // Same-day? Just subtract the seconds counts.
+ if (utc.tm_mday == local.tm_mday) {
+ return local_secs - utc_secs;
+ }
+
+ // If we have more UTC seconds, move local seconds into the UTC seconds'
+ // frame of reference and then subtract.
+ if (utc_secs > local_secs) {
+ return (SecondsPerDay + local_secs) - utc_secs;
+ }
+
+ // Otherwise we have more local seconds, so move the UTC seconds into the
+ // local seconds' frame of reference and then subtract.
+ return local_secs - (utc_secs + SecondsPerDay);
+}
+
+void js::DateTimeInfo::internalResetTimeZone(ResetTimeZoneMode mode) {
+ // Nothing to do when an update request is already enqueued.
+ if (timeZoneStatus_ == TimeZoneStatus::NeedsUpdate) {
+ return;
+ }
+
+ // Mark the state as needing an update, but defer the actual update until it's
+ // actually needed to delay any system calls to the last possible moment. This
+ // is beneficial when this method is called during start-up, because it avoids
+ // main-thread I/O blocking the process.
+ if (mode == ResetTimeZoneMode::ResetEvenIfOffsetUnchanged) {
+ timeZoneStatus_ = TimeZoneStatus::NeedsUpdate;
+ } else {
+ timeZoneStatus_ = TimeZoneStatus::UpdateIfChanged;
+ }
+}
+
+void js::DateTimeInfo::updateTimeZone() {
+ MOZ_ASSERT(timeZoneStatus_ != TimeZoneStatus::Valid);
+
+ bool updateIfChanged = timeZoneStatus_ == TimeZoneStatus::UpdateIfChanged;
+
+ timeZoneStatus_ = TimeZoneStatus::Valid;
+
+ /*
+ * The difference between local standard time and UTC will never change for
+ * a given time zone.
+ */
+ int32_t newOffset = UTCToLocalStandardOffsetSeconds();
+
+ if (updateIfChanged && newOffset == utcToLocalStandardOffsetSeconds_) {
+ return;
+ }
+
+ utcToLocalStandardOffsetSeconds_ = newOffset;
+
+ dstRange_.reset();
+
+#if JS_HAS_INTL_API
+ utcRange_.reset();
+ localRange_.reset();
+
+ {
+ // Tell the analysis the |pFree| function pointer called by uprv_free
+ // cannot GC.
+ JS::AutoSuppressGCAnalysis nogc;
+
+ timeZone_ = nullptr;
+ }
+
+ standardName_ = nullptr;
+ daylightSavingsName_ = nullptr;
+#endif /* JS_HAS_INTL_API */
+
+ // Propagate the time zone change to ICU, too.
+ {
+ // Tell the analysis calling into ICU cannot GC.
+ JS::AutoSuppressGCAnalysis nogc;
+
+ internalResyncICUDefaultTimeZone();
+ }
+}
+
+js::DateTimeInfo::DateTimeInfo(bool forceUTC) : forceUTC_(forceUTC) {
+ // Set the time zone status into the invalid state, so we compute the actual
+ // defaults on first access. We don't yet want to initialize neither <ctime>
+ // nor ICU's time zone classes, because that may cause I/O operations slowing
+ // down the JS engine initialization, which we're currently in the middle of.
+ timeZoneStatus_ = TimeZoneStatus::NeedsUpdate;
+}
+
+js::DateTimeInfo::~DateTimeInfo() = default;
+
+int64_t js::DateTimeInfo::toClampedSeconds(int64_t milliseconds) {
+ int64_t seconds = milliseconds / int64_t(msPerSecond);
+ int64_t millis = milliseconds % int64_t(msPerSecond);
+
+ // Round towards the start of time.
+ if (millis < 0) {
+ seconds -= 1;
+ }
+
+ if (seconds > MaxTimeT) {
+ seconds = MaxTimeT;
+ } else if (seconds < MinTimeT) {
+ /* Go ahead a day to make localtime work (does not work with 0). */
+ seconds = SecondsPerDay;
+ }
+ return seconds;
+}
+
+int32_t js::DateTimeInfo::computeDSTOffsetMilliseconds(int64_t utcSeconds) {
+ MOZ_ASSERT(utcSeconds >= MinTimeT);
+ MOZ_ASSERT(utcSeconds <= MaxTimeT);
+
+#if JS_HAS_INTL_API
+ int64_t utcMilliseconds = utcSeconds * int64_t(msPerSecond);
+
+ return timeZone()->GetDSTOffsetMs(utcMilliseconds).unwrapOr(0);
+#else
+ struct tm tm;
+ if (!ComputeLocalTime(static_cast<time_t>(utcSeconds), &tm)) {
+ return 0;
+ }
+
+ // NB: The offset isn't computed correctly when the standard local offset
+ // at |utcSeconds| is different from |utcToLocalStandardOffsetSeconds|.
+ int32_t dayoff =
+ int32_t((utcSeconds + utcToLocalStandardOffsetSeconds_) % SecondsPerDay);
+ int32_t tmoff = tm.tm_sec + (tm.tm_min * SecondsPerMinute) +
+ (tm.tm_hour * SecondsPerHour);
+
+ int32_t diff = tmoff - dayoff;
+
+ if (diff < 0) {
+ diff += SecondsPerDay;
+ } else if (uint32_t(diff) >= SecondsPerDay) {
+ diff -= SecondsPerDay;
+ }
+
+ return diff * int32_t(msPerSecond);
+#endif /* JS_HAS_INTL_API */
+}
+
+int32_t js::DateTimeInfo::internalGetDSTOffsetMilliseconds(
+ int64_t utcMilliseconds) {
+ int64_t utcSeconds = toClampedSeconds(utcMilliseconds);
+ return getOrComputeValue(dstRange_, utcSeconds,
+ &DateTimeInfo::computeDSTOffsetMilliseconds);
+}
+
+int32_t js::DateTimeInfo::getOrComputeValue(RangeCache& range, int64_t seconds,
+ ComputeFn compute) {
+ range.sanityCheck();
+
+ auto checkSanity =
+ mozilla::MakeScopeExit([&range]() { range.sanityCheck(); });
+
+ // NB: Be aware of the initial range values when making changes to this
+ // code: the first call to this method, with those initial range
+ // values, must result in a cache miss.
+ MOZ_ASSERT(seconds != INT64_MIN);
+
+ if (range.startSeconds <= seconds && seconds <= range.endSeconds) {
+ return range.offsetMilliseconds;
+ }
+
+ if (range.oldStartSeconds <= seconds && seconds <= range.oldEndSeconds) {
+ return range.oldOffsetMilliseconds;
+ }
+
+ range.oldOffsetMilliseconds = range.offsetMilliseconds;
+ range.oldStartSeconds = range.startSeconds;
+ range.oldEndSeconds = range.endSeconds;
+
+ if (range.startSeconds <= seconds) {
+ int64_t newEndSeconds =
+ std::min({range.endSeconds + RangeExpansionAmount, MaxTimeT});
+ if (newEndSeconds >= seconds) {
+ int32_t endOffsetMilliseconds = (this->*compute)(newEndSeconds);
+ if (endOffsetMilliseconds == range.offsetMilliseconds) {
+ range.endSeconds = newEndSeconds;
+ return range.offsetMilliseconds;
+ }
+
+ range.offsetMilliseconds = (this->*compute)(seconds);
+ if (range.offsetMilliseconds == endOffsetMilliseconds) {
+ range.startSeconds = seconds;
+ range.endSeconds = newEndSeconds;
+ } else {
+ range.endSeconds = seconds;
+ }
+ return range.offsetMilliseconds;
+ }
+
+ range.offsetMilliseconds = (this->*compute)(seconds);
+ range.startSeconds = range.endSeconds = seconds;
+ return range.offsetMilliseconds;
+ }
+
+ int64_t newStartSeconds =
+ std::max<int64_t>({range.startSeconds - RangeExpansionAmount, MinTimeT});
+ if (newStartSeconds <= seconds) {
+ int32_t startOffsetMilliseconds = (this->*compute)(newStartSeconds);
+ if (startOffsetMilliseconds == range.offsetMilliseconds) {
+ range.startSeconds = newStartSeconds;
+ return range.offsetMilliseconds;
+ }
+
+ range.offsetMilliseconds = (this->*compute)(seconds);
+ if (range.offsetMilliseconds == startOffsetMilliseconds) {
+ range.startSeconds = newStartSeconds;
+ range.endSeconds = seconds;
+ } else {
+ range.startSeconds = seconds;
+ }
+ return range.offsetMilliseconds;
+ }
+
+ range.startSeconds = range.endSeconds = seconds;
+ range.offsetMilliseconds = (this->*compute)(seconds);
+ return range.offsetMilliseconds;
+}
+
+void js::DateTimeInfo::RangeCache::reset() {
+ // The initial range values are carefully chosen to result in a cache miss
+ // on first use given the range of possible values. Be careful to keep
+ // these values and the caching algorithm in sync!
+ offsetMilliseconds = 0;
+ startSeconds = endSeconds = INT64_MIN;
+ oldOffsetMilliseconds = 0;
+ oldStartSeconds = oldEndSeconds = INT64_MIN;
+
+ sanityCheck();
+}
+
+void js::DateTimeInfo::RangeCache::sanityCheck() {
+ auto assertRange = [](int64_t start, int64_t end) {
+ MOZ_ASSERT(start <= end);
+ MOZ_ASSERT_IF(start == INT64_MIN, end == INT64_MIN);
+ MOZ_ASSERT_IF(end == INT64_MIN, start == INT64_MIN);
+ MOZ_ASSERT_IF(start != INT64_MIN, start >= MinTimeT && end >= MinTimeT);
+ MOZ_ASSERT_IF(start != INT64_MIN, start <= MaxTimeT && end <= MaxTimeT);
+ };
+
+ assertRange(startSeconds, endSeconds);
+ assertRange(oldStartSeconds, oldEndSeconds);
+}
+
+#if JS_HAS_INTL_API
+int32_t js::DateTimeInfo::computeUTCOffsetMilliseconds(int64_t localSeconds) {
+ MOZ_ASSERT(localSeconds >= MinTimeT);
+ MOZ_ASSERT(localSeconds <= MaxTimeT);
+
+ int64_t localMilliseconds = localSeconds * int64_t(msPerSecond);
+
+ return timeZone()->GetUTCOffsetMs(localMilliseconds).unwrapOr(0);
+}
+
+int32_t js::DateTimeInfo::computeLocalOffsetMilliseconds(int64_t utcSeconds) {
+ MOZ_ASSERT(utcSeconds >= MinTimeT);
+ MOZ_ASSERT(utcSeconds <= MaxTimeT);
+
+ UDate utcMilliseconds = UDate(utcSeconds * int64_t(msPerSecond));
+
+ return timeZone()->GetOffsetMs(utcMilliseconds).unwrapOr(0);
+}
+
+int32_t js::DateTimeInfo::internalGetOffsetMilliseconds(int64_t milliseconds,
+ TimeZoneOffset offset) {
+ int64_t seconds = toClampedSeconds(milliseconds);
+ return offset == TimeZoneOffset::UTC
+ ? getOrComputeValue(localRange_, seconds,
+ &DateTimeInfo::computeLocalOffsetMilliseconds)
+ : getOrComputeValue(utcRange_, seconds,
+ &DateTimeInfo::computeUTCOffsetMilliseconds);
+}
+
+bool js::DateTimeInfo::internalTimeZoneDisplayName(char16_t* buf, size_t buflen,
+ int64_t utcMilliseconds,
+ const char* locale) {
+ MOZ_ASSERT(buf != nullptr);
+ MOZ_ASSERT(buflen > 0);
+ MOZ_ASSERT(locale != nullptr);
+
+ // Clear any previously cached names when the default locale changed.
+ if (!locale_ || std::strcmp(locale_.get(), locale) != 0) {
+ locale_ = DuplicateString(locale);
+ if (!locale_) {
+ return false;
+ }
+
+ standardName_.reset();
+ daylightSavingsName_.reset();
+ }
+
+ using DaylightSavings = mozilla::intl::TimeZone::DaylightSavings;
+
+ auto daylightSavings = internalGetDSTOffsetMilliseconds(utcMilliseconds) != 0
+ ? DaylightSavings::Yes
+ : DaylightSavings::No;
+
+ JS::UniqueTwoByteChars& cachedName = (daylightSavings == DaylightSavings::Yes)
+ ? daylightSavingsName_
+ : standardName_;
+ if (!cachedName) {
+ // Retrieve the display name for the given locale.
+
+ intl::FormatBuffer<char16_t, 0, js::SystemAllocPolicy> buffer;
+ if (timeZone()->GetDisplayName(locale, daylightSavings, buffer).isErr()) {
+ return false;
+ }
+
+ cachedName = buffer.extractStringZ();
+ if (!cachedName) {
+ return false;
+ }
+ }
+
+ // Return an empty string if the display name doesn't fit into the buffer.
+ size_t length = js_strlen(cachedName.get());
+ if (length < buflen) {
+ std::copy(cachedName.get(), cachedName.get() + length, buf);
+ } else {
+ length = 0;
+ }
+
+ buf[length] = '\0';
+ return true;
+}
+
+mozilla::intl::TimeZone* js::DateTimeInfo::timeZone() {
+ if (!timeZone_) {
+ // For resist finger printing mode we always use the UTC time zone.
+ mozilla::Maybe<mozilla::Span<const char16_t>> timeZoneOverride;
+ if (forceUTC_) {
+ timeZoneOverride = mozilla::Some(mozilla::MakeStringSpan(u"UTC"));
+ }
+
+ auto timeZone = mozilla::intl::TimeZone::TryCreate(timeZoneOverride);
+
+ // Creating the default or UTC time zone should never fail. If it should
+ // fail nonetheless for some reason, just crash because we don't have a way
+ // to propagate any errors.
+ MOZ_RELEASE_ASSERT(timeZone.isOk());
+
+ timeZone_ = timeZone.unwrap();
+ MOZ_ASSERT(timeZone_);
+ }
+
+ return timeZone_.get();
+}
+#endif /* JS_HAS_INTL_API */
+
+/* static */ js::ExclusiveData<js::DateTimeInfo>* js::DateTimeInfo::instance;
+/* static */ js::ExclusiveData<js::DateTimeInfo>* js::DateTimeInfo::instanceUTC;
+
+bool js::InitDateTimeState() {
+ MOZ_ASSERT(!DateTimeInfo::instance && !DateTimeInfo::instanceUTC,
+ "we should be initializing only once");
+
+ DateTimeInfo::instance =
+ js_new<ExclusiveData<DateTimeInfo>>(mutexid::DateTimeInfoMutex, false);
+ DateTimeInfo::instanceUTC =
+ js_new<ExclusiveData<DateTimeInfo>>(mutexid::DateTimeInfoMutex, true);
+ return DateTimeInfo::instance && DateTimeInfo::instanceUTC;
+}
+
+/* static */
+void js::FinishDateTimeState() {
+ js_delete(DateTimeInfo::instance);
+ DateTimeInfo::instance = nullptr;
+}
+
+void js::ResetTimeZoneInternal(ResetTimeZoneMode mode) {
+ js::DateTimeInfo::resetTimeZone(mode);
+}
+
+JS_PUBLIC_API void JS::ResetTimeZone() {
+ js::ResetTimeZoneInternal(js::ResetTimeZoneMode::ResetEvenIfOffsetUnchanged);
+}
+
+#if JS_HAS_INTL_API
+# if defined(XP_WIN)
+static bool IsOlsonCompatibleWindowsTimeZoneId(std::string_view tz) {
+ // ICU ignores the TZ environment variable on Windows and instead directly
+ // invokes Win API functions to retrieve the current time zone. But since
+ // we're still using the POSIX-derived localtime_s() function on Windows
+ // and localtime_s() does return a time zone adjusted value based on the
+ // TZ environment variable, we need to manually adjust the default ICU
+ // time zone if TZ is set.
+ //
+ // Windows supports the following format for TZ: tzn[+|-]hh[:mm[:ss]][dzn]
+ // where "tzn" is the time zone name for standard time, the time zone
+ // offset is positive for time zones west of GMT, and "dzn" is the
+ // optional time zone name when daylight savings are observed. Daylight
+ // savings are always based on the U.S. daylight saving rules, that means
+ // for example it's not possible to use "TZ=CET-1CEST" to select the IANA
+ // time zone "CET".
+ //
+ // When comparing this restricted format for TZ to all IANA time zone
+ // names, the following time zones are in the intersection of what's
+ // supported by Windows and is also a valid IANA time zone identifier.
+ //
+ // Even though the time zone offset is marked as mandatory on MSDN, it
+ // appears it defaults to zero when omitted. This in turn means we can
+ // also allow the time zone identifiers "UCT", "UTC", and "GMT".
+
+ static const char* const allowedIds[] = {
+ // From tzdata's "northamerica" file:
+ "EST5EDT",
+ "CST6CDT",
+ "MST7MDT",
+ "PST8PDT",
+
+ // From tzdata's "backward" file:
+ "GMT+0",
+ "GMT-0",
+ "GMT0",
+ "UCT",
+ "UTC",
+
+ // From tzdata's "etcetera" file:
+ "GMT",
+ };
+ for (const auto& allowedId : allowedIds) {
+ if (tz == allowedId) {
+ return true;
+ }
+ }
+ return false;
+}
+# else
+static std::string_view TZContainsAbsolutePath(std::string_view tzVar) {
+ // A TZ environment variable may be an absolute path. The path
+ // format of TZ may begin with a colon. (ICU handles relative paths.)
+ if (tzVar.length() > 1 && tzVar[0] == ':' && tzVar[1] == '/') {
+ return tzVar.substr(1);
+ }
+ if (tzVar.length() > 0 && tzVar[0] == '/') {
+ return tzVar;
+ }
+ return {};
+}
+
+/**
+ * Reject the input if it doesn't match the time zone id pattern or legacy time
+ * zone names.
+ *
+ * See <https://github.com/eggert/tz/blob/master/theory.html>.
+ */
+static bool IsTimeZoneId(std::string_view timeZone) {
+ size_t timeZoneLen = timeZone.length();
+
+ if (timeZoneLen == 0) {
+ return false;
+ }
+
+ for (size_t i = 0; i < timeZoneLen; i++) {
+ char c = timeZone[i];
+
+ // According to theory.html, '.' is allowed in time zone ids, but the
+ // accompanying zic.c file doesn't allow it. Assume the source file is
+ // correct and disallow '.' here, too.
+ if (mozilla::IsAsciiAlphanumeric(c) || c == '_' || c == '-' || c == '+') {
+ continue;
+ }
+
+ // Reject leading, trailing, or consecutive '/' characters.
+ if (c == '/' && i > 0 && i + 1 < timeZoneLen && timeZone[i + 1] != '/') {
+ continue;
+ }
+
+ return false;
+ }
+
+ return true;
+}
+
+using TimeZoneIdentifierVector =
+ js::Vector<char, mozilla::intl::TimeZone::TimeZoneIdentifierLength,
+ js::SystemAllocPolicy>;
+
+/**
+ * Given a presumptive path |tz| to a zoneinfo time zone file
+ * (e.g. /etc/localtime), attempt to compute the time zone encoded by that
+ * path by repeatedly resolving symlinks until a path containing "/zoneinfo/"
+ * followed by time zone looking components is found. If a symlink is broken,
+ * symlink-following recurs too deeply, non time zone looking components are
+ * encountered, or some other error is encountered, then the |result| buffer is
+ * left empty.
+ *
+ * If |result| is set to a non-empty string, it's only guaranteed to have
+ * certain syntactic validity. It might not actually *be* a time zone name.
+ *
+ * If there's an (OOM) error, |false| is returned.
+ */
+static bool ReadTimeZoneLink(std::string_view tz,
+ TimeZoneIdentifierVector& result) {
+ MOZ_ASSERT(!tz.empty());
+ MOZ_ASSERT(result.empty());
+
+ // The resolved link name can have different paths depending on the OS.
+ // Follow ICU and only search for "/zoneinfo/"; see $ICU/common/putil.cpp.
+ static constexpr char ZoneInfoPath[] = "/zoneinfo/";
+ constexpr size_t ZoneInfoPathLength = js_strlen(ZoneInfoPath);
+
+ // Stop following symlinks after a fixed depth, because some common time
+ // zones are stored in files whose name doesn't match an Olson time zone
+ // name. For example on Ubuntu, "/usr/share/zoneinfo/America/New_York" is a
+ // symlink to "/usr/share/zoneinfo/posixrules" and "posixrules" is not an
+ // Olson time zone name.
+ // Four hops should be a reasonable limit for most use cases.
+ constexpr uint32_t FollowDepthLimit = 4;
+
+# ifdef PATH_MAX
+ constexpr size_t PathMax = PATH_MAX;
+# else
+ constexpr size_t PathMax = 4096;
+# endif
+ static_assert(PathMax > 0, "PathMax should be larger than zero");
+
+ char linkName[PathMax];
+ constexpr size_t linkNameLen =
+ std::size(linkName) - 1; // -1 to null-terminate.
+
+ // Return if the TZ value is too large.
+ if (tz.length() > linkNameLen) {
+ return true;
+ }
+
+ tz.copy(linkName, tz.length());
+ linkName[tz.length()] = '\0';
+
+ char linkTarget[PathMax];
+ constexpr size_t linkTargetLen =
+ std::size(linkTarget) - 1; // -1 to null-terminate.
+
+ uint32_t depth = 0;
+
+ // Search until we find "/zoneinfo/" in the link name.
+ const char* timeZoneWithZoneInfo;
+ while (!(timeZoneWithZoneInfo = std::strstr(linkName, ZoneInfoPath))) {
+ // Return if the symlink nesting is too deep.
+ if (++depth > FollowDepthLimit) {
+ return true;
+ }
+
+ // Return on error or if the result was truncated.
+ ssize_t slen = readlink(linkName, linkTarget, linkTargetLen);
+ if (slen < 0 || size_t(slen) >= linkTargetLen) {
+ return true;
+ }
+
+ // Ensure linkTarget is null-terminated. (readlink may not necessarily
+ // null-terminate the string.)
+ size_t len = size_t(slen);
+ linkTarget[len] = '\0';
+
+ // If the target is absolute, continue with that.
+ if (linkTarget[0] == '/') {
+ std::strcpy(linkName, linkTarget);
+ continue;
+ }
+
+ // If the target is relative, it must be resolved against either the
+ // directory the link was in, or against the current working directory.
+ char* separator = std::strrchr(linkName, '/');
+
+ // If the link name is just something like "foo", resolve linkTarget
+ // against the current working directory.
+ if (!separator) {
+ std::strcpy(linkName, linkTarget);
+ continue;
+ }
+
+ // Remove everything after the final path separator in linkName.
+ separator[1] = '\0';
+
+ // Return if the concatenated path name is too large.
+ if (std::strlen(linkName) + len > linkNameLen) {
+ return true;
+ }
+
+ // Keep it simple and just concatenate the path names.
+ std::strcat(linkName, linkTarget);
+ }
+
+ std::string_view timeZone(timeZoneWithZoneInfo + ZoneInfoPathLength);
+ if (!IsTimeZoneId(timeZone)) {
+ return true;
+ }
+ return result.append(timeZone.data(), timeZone.length());
+}
+# endif /* defined(XP_WIN) */
+#endif /* JS_HAS_INTL_API */
+
+void js::DateTimeInfo::internalResyncICUDefaultTimeZone() {
+#if JS_HAS_INTL_API
+ // In the future we should not be setting a default ICU time zone at all,
+ // instead all accesses should go through the appropriate DateTimeInfo
+ // instance depending on the resist fingerprinting status. For now we return
+ // early to prevent overwriting the default time zone with the UTC time zone
+ // used by RFP.
+ if (forceUTC_) {
+ return;
+ }
+
+ if (const char* tzenv = std::getenv("TZ")) {
+ std::string_view tz(tzenv);
+
+ mozilla::Span<const char> tzid;
+
+# if defined(XP_WIN)
+ // If TZ is set and its value is valid under Windows' and IANA's time zone
+ // identifier rules, update the ICU default time zone to use this value.
+ if (IsOlsonCompatibleWindowsTimeZoneId(tz)) {
+ tzid = mozilla::Span(tz.data(), tz.length());
+ } else {
+ // If |tz| isn't a supported time zone identifier, use the default Windows
+ // time zone for ICU.
+ // TODO: Handle invalid time zone identifiers (bug 342068).
+ }
+# else
+ // The TZ environment variable allows both absolute and relative paths,
+ // optionally beginning with a colon (':'). (Relative paths, without the
+ // colon, are just Olson time zone names.) We need to handle absolute paths
+ // ourselves, including handling that they might be symlinks.
+ // <https://unicode-org.atlassian.net/browse/ICU-13694>
+ TimeZoneIdentifierVector tzidVector;
+ std::string_view tzlink = TZContainsAbsolutePath(tz);
+ if (!tzlink.empty()) {
+ if (!ReadTimeZoneLink(tzlink, tzidVector)) {
+ // Ignore OOM.
+ return;
+ }
+ tzid = tzidVector;
+ }
+
+# ifdef ANDROID
+ // ICU ignores the TZ environment variable on Android. If it doesn't contain
+ // an absolute path, try to parse it as a time zone name.
+ else if (IsTimeZoneId(tz)) {
+ tzid = mozilla::Span(tz.data(), tz.length());
+ }
+# endif
+# endif /* defined(XP_WIN) */
+
+ if (!tzid.empty()) {
+ auto result = mozilla::intl::TimeZone::SetDefaultTimeZone(tzid);
+ if (result.isErr()) {
+ // Intentionally ignore any errors, because we don't have a good way to
+ // report errors from this function.
+ return;
+ }
+
+ // Return if the default time zone was successfully updated.
+ if (result.unwrap()) {
+ return;
+ }
+
+ // If SetDefaultTimeZone() succeeded, but the default time zone wasn't
+ // changed, proceed to set the default time zone from the host time zone.
+ }
+ }
+
+ // Intentionally ignore any errors, because we don't have a good way to report
+ // errors from this function.
+ (void)mozilla::intl::TimeZone::SetDefaultTimeZoneFromHostTimeZone();
+#endif
+}