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