/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- * vim: set ts=8 sts=4 et sw=4 tw=99: * 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" #include "mozilla/Atomics.h" #include #include "jsutil.h" #include "js/Date.h" #if ENABLE_INTL_API #include "unicode/timezone.h" #endif using mozilla::Atomic; using mozilla::ReleaseAcquire; using mozilla::UnspecifiedNaN; /* static */ js::DateTimeInfo js::DateTimeInfo::instance; /* static */ mozilla::Atomic js::DateTimeInfo::AcquireLock::spinLock; static bool ComputeLocalTime(time_t local, struct tm* ptm) { #if defined(_WIN32) return localtime_s(ptm, &local) == 0; #elif defined(HAVE_LOCALTIME_R) 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; #if defined(XP_WIN) // Windows doesn't follow POSIX: updates to the TZ environment variable are // not reflected immediately on that platform as they are on other systems // without this call. _tzset(); #endif // 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. local.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(&local); 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 * SecondsPerMinute; int local_secs = local.tm_hour * SecondsPerHour + local.tm_min * 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::internalUpdateTimeZoneAdjustment() { /* * The difference between local standard time and UTC will never change for * a given time zone. */ utcToLocalStandardOffsetSeconds = UTCToLocalStandardOffsetSeconds(); double newTZA = utcToLocalStandardOffsetSeconds * msPerSecond; if (newTZA == localTZA_) return; localTZA_ = newTZA; /* * 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; rangeStartSeconds = rangeEndSeconds = INT64_MIN; oldOffsetMilliseconds = 0; oldRangeStartSeconds = oldRangeEndSeconds = INT64_MIN; sanityCheck(); } /* * Since getDSTOffsetMilliseconds guarantees that all times seen will be * positive, we can initialize the range at construction time with large * negative numbers to ensure the first computation is always a cache miss and * doesn't return a bogus offset. */ /* static */ void js::DateTimeInfo::init() { DateTimeInfo* dtInfo = &DateTimeInfo::instance; MOZ_ASSERT(dtInfo->localTZA_ == 0, "we should be initializing only once, and the static instance " "should have started out zeroed"); // Set to a totally impossible TZA so that the comparison above will fail // and all fields will be properly initialized. dtInfo->localTZA_ = UnspecifiedNaN(); dtInfo->internalUpdateTimeZoneAdjustment(); } int64_t js::DateTimeInfo::computeDSTOffsetMilliseconds(int64_t utcSeconds) { MOZ_ASSERT(utcSeconds >= 0); MOZ_ASSERT(utcSeconds <= MaxUnixTimeT); #if defined(XP_WIN) // Windows does not follow POSIX. Updates to the TZ environment variable // are not reflected immediately on that platform as they are on UNIX // systems without this call. _tzset(); #endif struct tm tm; if (!ComputeLocalTime(static_cast(utcSeconds), &tm)) return 0; 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; return diff * msPerSecond; } int64_t js::DateTimeInfo::internalGetDSTOffsetMilliseconds(int64_t utcMilliseconds) { sanityCheck(); int64_t utcSeconds = utcMilliseconds / msPerSecond; if (utcSeconds > MaxUnixTimeT) { utcSeconds = MaxUnixTimeT; } else if (utcSeconds < 0) { /* Go ahead a day to make localtime work (does not work with 0). */ utcSeconds = SecondsPerDay; } /* * 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. */ if (rangeStartSeconds <= utcSeconds && utcSeconds <= rangeEndSeconds) return offsetMilliseconds; if (oldRangeStartSeconds <= utcSeconds && utcSeconds <= oldRangeEndSeconds) return oldOffsetMilliseconds; oldOffsetMilliseconds = offsetMilliseconds; oldRangeStartSeconds = rangeStartSeconds; oldRangeEndSeconds = rangeEndSeconds; if (rangeStartSeconds <= utcSeconds) { int64_t newEndSeconds = Min(rangeEndSeconds + RangeExpansionAmount, MaxUnixTimeT); if (newEndSeconds >= utcSeconds) { int64_t endOffsetMilliseconds = computeDSTOffsetMilliseconds(newEndSeconds); if (endOffsetMilliseconds == offsetMilliseconds) { rangeEndSeconds = newEndSeconds; return offsetMilliseconds; } offsetMilliseconds = computeDSTOffsetMilliseconds(utcSeconds); if (offsetMilliseconds == endOffsetMilliseconds) { rangeStartSeconds = utcSeconds; rangeEndSeconds = newEndSeconds; } else { rangeEndSeconds = utcSeconds; } return offsetMilliseconds; } offsetMilliseconds = computeDSTOffsetMilliseconds(utcSeconds); rangeStartSeconds = rangeEndSeconds = utcSeconds; return offsetMilliseconds; } int64_t newStartSeconds = Max(rangeStartSeconds - RangeExpansionAmount, 0); if (newStartSeconds <= utcSeconds) { int64_t startOffsetMilliseconds = computeDSTOffsetMilliseconds(newStartSeconds); if (startOffsetMilliseconds == offsetMilliseconds) { rangeStartSeconds = newStartSeconds; return offsetMilliseconds; } offsetMilliseconds = computeDSTOffsetMilliseconds(utcSeconds); if (offsetMilliseconds == startOffsetMilliseconds) { rangeStartSeconds = newStartSeconds; rangeEndSeconds = utcSeconds; } else { rangeStartSeconds = utcSeconds; } return offsetMilliseconds; } rangeStartSeconds = rangeEndSeconds = utcSeconds; offsetMilliseconds = computeDSTOffsetMilliseconds(utcSeconds); return offsetMilliseconds; } void js::DateTimeInfo::sanityCheck() { MOZ_ASSERT(rangeStartSeconds <= rangeEndSeconds); MOZ_ASSERT_IF(rangeStartSeconds == INT64_MIN, rangeEndSeconds == INT64_MIN); MOZ_ASSERT_IF(rangeEndSeconds == INT64_MIN, rangeStartSeconds == INT64_MIN); MOZ_ASSERT_IF(rangeStartSeconds != INT64_MIN, rangeStartSeconds >= 0 && rangeEndSeconds >= 0); MOZ_ASSERT_IF(rangeStartSeconds != INT64_MIN, rangeStartSeconds <= MaxUnixTimeT && rangeEndSeconds <= MaxUnixTimeT); } static struct IcuTimeZoneInfo { Atomic locked; enum { Valid = 0, NeedsUpdate } status; void acquire() { while (!locked.compareExchange(false, true)) continue; } void release() { MOZ_ASSERT(locked, "should have been acquired"); locked = false; } } TZInfo; JS_PUBLIC_API(void) JS::ResetTimeZone() { js::DateTimeInfo::updateTimeZoneAdjustment(); #if ENABLE_INTL_API && defined(ICU_TZ_HAS_RECREATE_DEFAULT) TZInfo.acquire(); TZInfo.status = IcuTimeZoneInfo::NeedsUpdate; TZInfo.release(); #endif } void js::ResyncICUDefaultTimeZone() { #if ENABLE_INTL_API && defined(ICU_TZ_HAS_RECREATE_DEFAULT) TZInfo.acquire(); if (TZInfo.status == IcuTimeZoneInfo::NeedsUpdate) { icu::TimeZone::recreateDefault(); TZInfo.status = IcuTimeZoneInfo::Valid; } TZInfo.release(); #endif }