diff --git a/js/src/builtin/Intl.cpp b/js/src/builtin/Intl.cpp index f5375a2f7f..48f8791ac4 100644 --- a/js/src/builtin/Intl.cpp +++ b/js/src/builtin/Intl.cpp @@ -27,7 +27,7 @@ #include "builtin/intl/ICUHeader.h" #include "builtin/intl/NumberFormat.h" #include "builtin/intl/ScopedICUObject.h" -#include "builtin/IntlTimeZoneData.h" +#include "builtin/intl/SharedIntlData.h" #include "ds/Sort.h" #include "vm/DateTime.h" #include "vm/GlobalObject.h" @@ -51,6 +51,7 @@ using js::intl::CallICU; using js::intl::GetAvailableLocales; using js::intl::IcuLocale; using js::intl::INITIAL_CHAR_BUFFER_SIZE; +using js::intl::SharedIntlData; using js::intl::StringsAreEqual; /******************** DateTimeFormat ********************/ @@ -363,280 +364,6 @@ js::intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp) return true; } -template -static constexpr Char -ToUpperASCII(Char c) -{ - return ('a' <= c && c <= 'z') - ? (c & ~0x20) - : c; -} - -static_assert(ToUpperASCII('a') == 'A', "verifying 'a' uppercases correctly"); -static_assert(ToUpperASCII('m') == 'M', "verifying 'm' uppercases correctly"); -static_assert(ToUpperASCII('z') == 'Z', "verifying 'z' uppercases correctly"); -static_assert(ToUpperASCII(u'a') == u'A', "verifying u'a' uppercases correctly"); -static_assert(ToUpperASCII(u'k') == u'K', "verifying u'k' uppercases correctly"); -static_assert(ToUpperASCII(u'z') == u'Z', "verifying u'z' uppercases correctly"); - -template -static bool -EqualCharsIgnoreCaseASCII(const Char1* s1, const Char2* s2, size_t len) -{ - for (const Char1* s1end = s1 + len; s1 < s1end; s1++, s2++) { - if (ToUpperASCII(*s1) != ToUpperASCII(*s2)) - return false; - } - return true; -} - -template -static js::HashNumber -HashStringIgnoreCaseASCII(const Char* s, size_t length) -{ - uint32_t hash = 0; - for (size_t i = 0; i < length; i++) - hash = mozilla::AddToHash(hash, ToUpperASCII(s[i])); - return hash; -} - -js::SharedIntlData::TimeZoneHasher::Lookup::Lookup(JSFlatString* timeZone) - : isLatin1(timeZone->hasLatin1Chars()), length(timeZone->length()) -{ - if (isLatin1) { - latin1Chars = timeZone->latin1Chars(nogc); - hash = HashStringIgnoreCaseASCII(latin1Chars, length); - } else { - twoByteChars = timeZone->twoByteChars(nogc); - hash = HashStringIgnoreCaseASCII(twoByteChars, length); - } -} - -bool -js::SharedIntlData::TimeZoneHasher::match(TimeZoneName key, const Lookup& lookup) -{ - if (key->length() != lookup.length) - return false; - - // Compare time zone names ignoring ASCII case differences. - if (key->hasLatin1Chars()) { - const Latin1Char* keyChars = key->latin1Chars(lookup.nogc); - if (lookup.isLatin1) - return EqualCharsIgnoreCaseASCII(keyChars, lookup.latin1Chars, lookup.length); - return EqualCharsIgnoreCaseASCII(keyChars, lookup.twoByteChars, lookup.length); - } - - const char16_t* keyChars = key->twoByteChars(lookup.nogc); - if (lookup.isLatin1) - return EqualCharsIgnoreCaseASCII(lookup.latin1Chars, keyChars, lookup.length); - return EqualCharsIgnoreCaseASCII(keyChars, lookup.twoByteChars, lookup.length); -} - -static bool -IsLegacyICUTimeZone(const char* timeZone) -{ - for (const auto& legacyTimeZone : js::timezone::legacyICUTimeZones) { - if (StringsAreEqual(timeZone, legacyTimeZone)) - return true; - } - return false; -} - -bool -js::SharedIntlData::ensureTimeZones(JSContext* cx) -{ - if (timeZoneDataInitialized) - return true; - - // If initTimeZones() was called previously, but didn't complete due to - // OOM, clear all sets/maps and start from scratch. - if (availableTimeZones.initialized()) - availableTimeZones.finish(); - if (!availableTimeZones.init()) { - ReportOutOfMemory(cx); - return false; - } - - UErrorCode status = U_ZERO_ERROR; - UEnumeration* values = ucal_openTimeZones(&status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - ScopedICUObject toClose(values); - - RootedAtom timeZone(cx); - while (true) { - int32_t size; - const char* rawTimeZone = uenum_next(values, &size, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - if (rawTimeZone == nullptr) - break; - - // Skip legacy ICU time zone names. - if (IsLegacyICUTimeZone(rawTimeZone)) - continue; - - MOZ_ASSERT(size >= 0); - timeZone = Atomize(cx, rawTimeZone, size_t(size)); - if (!timeZone) - return false; - - TimeZoneHasher::Lookup lookup(timeZone); - TimeZoneSet::AddPtr p = availableTimeZones.lookupForAdd(lookup); - - // ICU shouldn't report any duplicate time zone names, but if it does, - // just ignore the duplicate name. - if (!p && !availableTimeZones.add(p, timeZone)) { - ReportOutOfMemory(cx); - return false; - } - } - - if (ianaZonesTreatedAsLinksByICU.initialized()) - ianaZonesTreatedAsLinksByICU.finish(); - if (!ianaZonesTreatedAsLinksByICU.init()) { - ReportOutOfMemory(cx); - return false; - } - - for (const char* rawTimeZone : timezone::ianaZonesTreatedAsLinksByICU) { - MOZ_ASSERT(rawTimeZone != nullptr); - timeZone = Atomize(cx, rawTimeZone, strlen(rawTimeZone)); - if (!timeZone) - return false; - - TimeZoneHasher::Lookup lookup(timeZone); - TimeZoneSet::AddPtr p = ianaZonesTreatedAsLinksByICU.lookupForAdd(lookup); - MOZ_ASSERT(!p, "Duplicate entry in timezone::ianaZonesTreatedAsLinksByICU"); - - if (!ianaZonesTreatedAsLinksByICU.add(p, timeZone)) { - ReportOutOfMemory(cx); - return false; - } - } - - if (ianaLinksCanonicalizedDifferentlyByICU.initialized()) - ianaLinksCanonicalizedDifferentlyByICU.finish(); - if (!ianaLinksCanonicalizedDifferentlyByICU.init()) { - ReportOutOfMemory(cx); - return false; - } - - RootedAtom linkName(cx); - RootedAtom& target = timeZone; - for (const auto& linkAndTarget : timezone::ianaLinksCanonicalizedDifferentlyByICU) { - const char* rawLinkName = linkAndTarget.link; - const char* rawTarget = linkAndTarget.target; - - MOZ_ASSERT(rawLinkName != nullptr); - linkName = Atomize(cx, rawLinkName, strlen(rawLinkName)); - if (!linkName) - return false; - - MOZ_ASSERT(rawTarget != nullptr); - target = Atomize(cx, rawTarget, strlen(rawTarget)); - if (!target) - return false; - - TimeZoneHasher::Lookup lookup(linkName); - TimeZoneMap::AddPtr p = ianaLinksCanonicalizedDifferentlyByICU.lookupForAdd(lookup); - MOZ_ASSERT(!p, "Duplicate entry in timezone::ianaLinksCanonicalizedDifferentlyByICU"); - - if (!ianaLinksCanonicalizedDifferentlyByICU.add(p, linkName, target)) { - ReportOutOfMemory(cx); - return false; - } - } - - MOZ_ASSERT(!timeZoneDataInitialized, "ensureTimeZones is neither reentrant nor thread-safe"); - timeZoneDataInitialized = true; - - return true; -} - -bool -js::SharedIntlData::validateTimeZoneName(JSContext* cx, HandleString timeZone, - MutableHandleString result) -{ - if (!ensureTimeZones(cx)) - return false; - - Rooted timeZoneFlat(cx, timeZone->ensureFlat(cx)); - if (!timeZoneFlat) - return false; - - TimeZoneHasher::Lookup lookup(timeZoneFlat); - if (TimeZoneSet::Ptr p = availableTimeZones.lookup(lookup)) - result.set(*p); - - return true; -} - -bool -js::SharedIntlData::tryCanonicalizeTimeZoneConsistentWithIANA(JSContext* cx, HandleString timeZone, - MutableHandleString result) -{ - if (!ensureTimeZones(cx)) - return false; - - Rooted timeZoneFlat(cx, timeZone->ensureFlat(cx)); - if (!timeZoneFlat) - return false; - - TimeZoneHasher::Lookup lookup(timeZoneFlat); - MOZ_ASSERT(availableTimeZones.has(lookup), "Invalid time zone name"); - - if (TimeZoneMap::Ptr p = ianaLinksCanonicalizedDifferentlyByICU.lookup(lookup)) { - // The effectively supported time zones aren't known at compile time, - // when - // 1. SpiderMonkey was compiled with "--with-system-icu". - // 2. ICU's dynamic time zone data loading feature was used. - // (ICU supports loading time zone files at runtime through the - // ICU_TIMEZONE_FILES_DIR environment variable.) - // Ensure ICU supports the new target zone before applying the update. - TimeZoneName targetTimeZone = p->value(); - TimeZoneHasher::Lookup targetLookup(targetTimeZone); - if (availableTimeZones.has(targetLookup)) - result.set(targetTimeZone); - } else if (TimeZoneSet::Ptr p = ianaZonesTreatedAsLinksByICU.lookup(lookup)) { - result.set(*p); - } - - return true; -} - -void -js::SharedIntlData::destroyInstance() -{ - availableTimeZones.finish(); - ianaZonesTreatedAsLinksByICU.finish(); - ianaLinksCanonicalizedDifferentlyByICU.finish(); -} - -void -js::SharedIntlData::trace(JSTracer* trc) -{ - // Atoms are always tenured. - if (!trc->runtime()->isHeapMinorCollecting()) { - availableTimeZones.trace(trc); - ianaZonesTreatedAsLinksByICU.trace(trc); - ianaLinksCanonicalizedDifferentlyByICU.trace(trc); - } -} - -size_t -js::SharedIntlData::sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const -{ - return availableTimeZones.sizeOfExcludingThis(mallocSizeOf) + - ianaZonesTreatedAsLinksByICU.sizeOfExcludingThis(mallocSizeOf) + - ianaLinksCanonicalizedDifferentlyByICU.sizeOfExcludingThis(mallocSizeOf); -} - bool js::intl_IsValidTimeZoneName(JSContext* cx, unsigned argc, Value* vp) { diff --git a/js/src/builtin/Intl.h b/js/src/builtin/Intl.h index f98d40f3e0..0f934debfb 100644 --- a/js/src/builtin/Intl.h +++ b/js/src/builtin/Intl.h @@ -31,140 +31,6 @@ namespace js { extern JSObject* InitIntlClass(JSContext* cx, HandleObject obj); -/** - * Stores Intl data which can be shared across compartments (but not contexts). - * - * Used for data which is expensive when computed repeatedly or is not - * available through ICU. - */ -class SharedIntlData -{ - /** - * Information tracking the set of the supported time zone names, derived - * from the IANA time zone database . - * - * There are two kinds of IANA time zone names: Zone and Link (denoted as - * such in database source files). Zone names are the canonical, preferred - * name for a time zone, e.g. Asia/Kolkata. Link names simply refer to - * target Zone names for their meaning, e.g. Asia/Calcutta targets - * Asia/Kolkata. That a name is a Link doesn't *necessarily* reflect a - * sense of deprecation: some Link names also exist partly for convenience, - * e.g. UTC and GMT as Link names targeting the Zone name Etc/UTC. - * - * Two data sources determine the time zone names we support: those ICU - * supports and IANA's zone information. - * - * Unfortunately the names ICU and IANA support, and their Link - * relationships from name to target, aren't identical, so we can't simply - * implicitly trust ICU's name handling. We must perform various - * preprocessing of user-provided zone names and post-processing of - * ICU-provided zone names to implement ECMA-402's IANA-consistent behavior. - * - * Also see and - * . - */ - - using TimeZoneName = JSAtom*; - - struct TimeZoneHasher - { - struct Lookup - { - union { - const JS::Latin1Char* latin1Chars; - const char16_t* twoByteChars; - }; - bool isLatin1; - size_t length; - JS::AutoCheckCannotGC nogc; - HashNumber hash; - - explicit Lookup(JSFlatString* timeZone); - }; - - static js::HashNumber hash(const Lookup& lookup) { return lookup.hash; } - static bool match(TimeZoneName key, const Lookup& lookup); - }; - - using TimeZoneSet = js::GCHashSet; - - using TimeZoneMap = js::GCHashMap; - - /** - * As a threshold matter, available time zones are those time zones ICU - * supports, via ucal_openTimeZones. But ICU supports additional non-IANA - * time zones described in intl/icu/source/tools/tzcode/icuzones (listed in - * IntlTimeZoneData.cpp's |legacyICUTimeZones|) for its own backwards - * compatibility purposes. This set consists of ICU's supported time zones, - * minus all backwards-compatibility time zones. - */ - TimeZoneSet availableTimeZones; - - /** - * IANA treats some time zone names as Zones, that ICU instead treats as - * Links. For example, IANA considers "America/Indiana/Indianapolis" to be - * a Zone and "America/Fort_Wayne" a Link that targets it, but ICU - * considers the former a Link that targets "America/Indianapolis" (which - * IANA treats as a Link). - * - * ECMA-402 requires that we respect IANA data, so if we're asked to - * canonicalize a time zone name in this set, we must *not* return ICU's - * canonicalization. - */ - TimeZoneSet ianaZonesTreatedAsLinksByICU; - - /** - * IANA treats some time zone names as Links to one target, that ICU - * instead treats as either Zones, or Links to different targets. An - * example of the former is "Asia/Calcutta, which IANA assigns the target - * "Asia/Kolkata" but ICU considers its own Zone. An example of the latter - * is "America/Virgin", which IANA assigns the target - * "America/Port_of_Spain" but ICU assigns the target "America/St_Thomas". - * - * ECMA-402 requires that we respect IANA data, so if we're asked to - * canonicalize a time zone name that's a key in this map, we *must* return - * the corresponding value and *must not* return ICU's canonicalization. - */ - TimeZoneMap ianaLinksCanonicalizedDifferentlyByICU; - - bool timeZoneDataInitialized = false; - - /** - * Precomputes the available time zone names, because it's too expensive to - * call ucal_openTimeZones() repeatedly. - */ - bool ensureTimeZones(JSContext* cx); - - public: - /** - * Returns the validated time zone name in |result|. If the input time zone - * isn't a valid IANA time zone name, |result| remains unchanged. - */ - bool validateTimeZoneName(JSContext* cx, JS::HandleString timeZone, - JS::MutableHandleString result); - - /** - * Returns the canonical time zone name in |result|. If no canonical name - * was found, |result| remains unchanged. - * - * This method only handles time zones which are canonicalized differently - * by ICU when compared to IANA. - */ - bool tryCanonicalizeTimeZoneConsistentWithIANA(JSContext* cx, JS::HandleString timeZone, - JS::MutableHandleString result); - - void destroyInstance(); - - void trace(JSTracer* trc); - - size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const; -}; - /* * The following functions are for use by self-hosted code. */ diff --git a/js/src/builtin/intl/Collator.cpp b/js/src/builtin/intl/Collator.cpp index 903917df89..426daedc5a 100644 --- a/js/src/builtin/intl/Collator.cpp +++ b/js/src/builtin/intl/Collator.cpp @@ -16,6 +16,7 @@ #include "builtin/intl/CommonFunctions.h" #include "builtin/intl/ICUHeader.h" #include "builtin/intl/ScopedICUObject.h" +#include "builtin/intl/SharedIntlData.h" #include "js/TypeDecls.h" #include "vm/GlobalObject.h" #include "vm/Runtime.h" @@ -27,6 +28,7 @@ using namespace js; using js::intl::GetAvailableLocales; using js::intl::IcuLocale; using js::intl::ReportInternalError; +using js::intl::SharedIntlData; using js::intl::StringsAreEqual; /******************** Collator ********************/ diff --git a/js/src/builtin/intl/SharedIntlData.cpp b/js/src/builtin/intl/SharedIntlData.cpp new file mode 100644 index 0000000000..f2da97b361 --- /dev/null +++ b/js/src/builtin/intl/SharedIntlData.cpp @@ -0,0 +1,300 @@ +/* -*- 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/. */ + +/* Runtime-wide Intl data shared across compartments. */ + +#include "builtin/intl/SharedIntlData.h" + +#include "mozilla/Assertions.h" +#include "mozilla/HashFunctions.h" + +#include + +#include "jsatom.h" +#include "jsstr.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/ICUHeader.h" +#include "builtin/intl/ScopedICUObject.h" +#include "builtin/IntlTimeZoneData.h" +#include "js/Utility.h" + +using js::HashNumber; +using js::intl::StringsAreEqual; + +template +static constexpr Char +ToUpperASCII(Char c) +{ + return ('a' <= c && c <= 'z') + ? (c & ~0x20) + : c; +} + +static_assert(ToUpperASCII('a') == 'A', "verifying 'a' uppercases correctly"); +static_assert(ToUpperASCII('m') == 'M', "verifying 'm' uppercases correctly"); +static_assert(ToUpperASCII('z') == 'Z', "verifying 'z' uppercases correctly"); +static_assert(ToUpperASCII(u'a') == u'A', "verifying u'a' uppercases correctly"); +static_assert(ToUpperASCII(u'k') == u'K', "verifying u'k' uppercases correctly"); +static_assert(ToUpperASCII(u'z') == u'Z', "verifying u'z' uppercases correctly"); + +template +static HashNumber +HashStringIgnoreCaseASCII(const Char* s, size_t length) +{ + uint32_t hash = 0; + for (size_t i = 0; i < length; i++) + hash = mozilla::AddToHash(hash, ToUpperASCII(s[i])); + return hash; +} + +template +static bool +EqualCharsIgnoreCaseASCII(const Char1* s1, const Char2* s2, size_t len) +{ + for (const Char1* s1end = s1 + len; s1 < s1end; s1++, s2++) { + if (ToUpperASCII(*s1) != ToUpperASCII(*s2)) + return false; + } + return true; +} + +js::intl::SharedIntlData::TimeZoneHasher::Lookup::Lookup(JSFlatString* timeZone) + : isLatin1(timeZone->hasLatin1Chars()), length(timeZone->length()) +{ + if (isLatin1) { + latin1Chars = timeZone->latin1Chars(nogc); + hash = HashStringIgnoreCaseASCII(latin1Chars, length); + } else { + twoByteChars = timeZone->twoByteChars(nogc); + hash = HashStringIgnoreCaseASCII(twoByteChars, length); + } +} + +bool +js::intl::SharedIntlData::TimeZoneHasher::match(TimeZoneName key, const Lookup& lookup) +{ + if (key->length() != lookup.length) + return false; + + // Compare time zone names ignoring ASCII case differences. + if (key->hasLatin1Chars()) { + const Latin1Char* keyChars = key->latin1Chars(lookup.nogc); + if (lookup.isLatin1) + return EqualCharsIgnoreCaseASCII(keyChars, lookup.latin1Chars, lookup.length); + return EqualCharsIgnoreCaseASCII(keyChars, lookup.twoByteChars, lookup.length); + } + + const char16_t* keyChars = key->twoByteChars(lookup.nogc); + if (lookup.isLatin1) + return EqualCharsIgnoreCaseASCII(lookup.latin1Chars, keyChars, lookup.length); + return EqualCharsIgnoreCaseASCII(keyChars, lookup.twoByteChars, lookup.length); +} + +static bool +IsLegacyICUTimeZone(const char* timeZone) +{ + for (const auto& legacyTimeZone : js::timezone::legacyICUTimeZones) { + if (StringsAreEqual(timeZone, legacyTimeZone)) + return true; + } + return false; +} + +bool +js::intl::SharedIntlData::ensureTimeZones(JSContext* cx) +{ + if (timeZoneDataInitialized) + return true; + + // If initTimeZones() was called previously, but didn't complete due to + // OOM, clear all sets/maps and start from scratch. + if (availableTimeZones.initialized()) + availableTimeZones.finish(); + if (!availableTimeZones.init()) { + ReportOutOfMemory(cx); + return false; + } + + UErrorCode status = U_ZERO_ERROR; + UEnumeration* values = ucal_openTimeZones(&status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(values); + + RootedAtom timeZone(cx); + while (true) { + int32_t size; + const char* rawTimeZone = uenum_next(values, &size, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + if (rawTimeZone == nullptr) + break; + + // Skip legacy ICU time zone names. + if (IsLegacyICUTimeZone(rawTimeZone)) + continue; + + MOZ_ASSERT(size >= 0); + timeZone = Atomize(cx, rawTimeZone, size_t(size)); + if (!timeZone) + return false; + + TimeZoneHasher::Lookup lookup(timeZone); + TimeZoneSet::AddPtr p = availableTimeZones.lookupForAdd(lookup); + + // ICU shouldn't report any duplicate time zone names, but if it does, + // just ignore the duplicate name. + if (!p && !availableTimeZones.add(p, timeZone)) { + ReportOutOfMemory(cx); + return false; + } + } + + if (ianaZonesTreatedAsLinksByICU.initialized()) + ianaZonesTreatedAsLinksByICU.finish(); + if (!ianaZonesTreatedAsLinksByICU.init()) { + ReportOutOfMemory(cx); + return false; + } + + for (const char* rawTimeZone : timezone::ianaZonesTreatedAsLinksByICU) { + MOZ_ASSERT(rawTimeZone != nullptr); + timeZone = Atomize(cx, rawTimeZone, strlen(rawTimeZone)); + if (!timeZone) + return false; + + TimeZoneHasher::Lookup lookup(timeZone); + TimeZoneSet::AddPtr p = ianaZonesTreatedAsLinksByICU.lookupForAdd(lookup); + MOZ_ASSERT(!p, "Duplicate entry in timezone::ianaZonesTreatedAsLinksByICU"); + + if (!ianaZonesTreatedAsLinksByICU.add(p, timeZone)) { + ReportOutOfMemory(cx); + return false; + } + } + + if (ianaLinksCanonicalizedDifferentlyByICU.initialized()) + ianaLinksCanonicalizedDifferentlyByICU.finish(); + if (!ianaLinksCanonicalizedDifferentlyByICU.init()) { + ReportOutOfMemory(cx); + return false; + } + + RootedAtom linkName(cx); + RootedAtom& target = timeZone; + for (const auto& linkAndTarget : timezone::ianaLinksCanonicalizedDifferentlyByICU) { + const char* rawLinkName = linkAndTarget.link; + const char* rawTarget = linkAndTarget.target; + + MOZ_ASSERT(rawLinkName != nullptr); + linkName = Atomize(cx, rawLinkName, strlen(rawLinkName)); + if (!linkName) + return false; + + MOZ_ASSERT(rawTarget != nullptr); + target = Atomize(cx, rawTarget, strlen(rawTarget)); + if (!target) + return false; + + TimeZoneHasher::Lookup lookup(linkName); + TimeZoneMap::AddPtr p = ianaLinksCanonicalizedDifferentlyByICU.lookupForAdd(lookup); + MOZ_ASSERT(!p, "Duplicate entry in timezone::ianaLinksCanonicalizedDifferentlyByICU"); + + if (!ianaLinksCanonicalizedDifferentlyByICU.add(p, linkName, target)) { + ReportOutOfMemory(cx); + return false; + } + } + + MOZ_ASSERT(!timeZoneDataInitialized, "ensureTimeZones is neither reentrant nor thread-safe"); + timeZoneDataInitialized = true; + + return true; +} + +bool +js::intl::SharedIntlData::validateTimeZoneName(JSContext* cx, HandleString timeZone, + MutableHandleString result) +{ + if (!ensureTimeZones(cx)) + return false; + + Rooted timeZoneFlat(cx, timeZone->ensureFlat(cx)); + if (!timeZoneFlat) + return false; + + TimeZoneHasher::Lookup lookup(timeZoneFlat); + if (TimeZoneSet::Ptr p = availableTimeZones.lookup(lookup)) + result.set(*p); + + return true; +} + +bool +js::intl::SharedIntlData::tryCanonicalizeTimeZoneConsistentWithIANA(JSContext* cx, HandleString timeZone, + MutableHandleString result) +{ + if (!ensureTimeZones(cx)) + return false; + + Rooted timeZoneFlat(cx, timeZone->ensureFlat(cx)); + if (!timeZoneFlat) + return false; + + TimeZoneHasher::Lookup lookup(timeZoneFlat); + MOZ_ASSERT(availableTimeZones.has(lookup), "Invalid time zone name"); + + if (TimeZoneMap::Ptr p = ianaLinksCanonicalizedDifferentlyByICU.lookup(lookup)) { + // The effectively supported time zones aren't known at compile time, + // when + // 1. SpiderMonkey was compiled with "--with-system-icu". + // 2. ICU's dynamic time zone data loading feature was used. + // (ICU supports loading time zone files at runtime through the + // ICU_TIMEZONE_FILES_DIR environment variable.) + // Ensure ICU supports the new target zone before applying the update. + TimeZoneName targetTimeZone = p->value(); + TimeZoneHasher::Lookup targetLookup(targetTimeZone); + if (availableTimeZones.has(targetLookup)) + result.set(targetTimeZone); + } else if (TimeZoneSet::Ptr p = ianaZonesTreatedAsLinksByICU.lookup(lookup)) { + result.set(*p); + } + + return true; +} + +void +js::intl::SharedIntlData::destroyInstance() +{ + availableTimeZones.finish(); + ianaZonesTreatedAsLinksByICU.finish(); + ianaLinksCanonicalizedDifferentlyByICU.finish(); +} + +void +js::intl::SharedIntlData::trace(JSTracer* trc) +{ + // Atoms are always tenured. + if (!trc->runtime()->isHeapMinorCollecting()) { + availableTimeZones.trace(trc); + ianaZonesTreatedAsLinksByICU.trace(trc); + ianaLinksCanonicalizedDifferentlyByICU.trace(trc); + } +} + +size_t +js::intl::SharedIntlData::sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const +{ + return availableTimeZones.sizeOfExcludingThis(mallocSizeOf) + + ianaZonesTreatedAsLinksByICU.sizeOfExcludingThis(mallocSizeOf) + + ianaLinksCanonicalizedDifferentlyByICU.sizeOfExcludingThis(mallocSizeOf); +} diff --git a/js/src/builtin/intl/SharedIntlData.h b/js/src/builtin/intl/SharedIntlData.h new file mode 100644 index 0000000000..c047de3801 --- /dev/null +++ b/js/src/builtin/intl/SharedIntlData.h @@ -0,0 +1,164 @@ +/* -*- 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/. */ + +#ifndef builtin_intl_SharedIntlData_h +#define builtin_intl_SharedIntlData_h + +#include "mozilla/MemoryReporting.h" + +#include + +#include "jsalloc.h" +#include "js/CharacterEncoding.h" +#include "js/GCAPI.h" +#include "js/GCHashTable.h" +#include "js/RootingAPI.h" +#include "js/Utility.h" +#include "vm/String.h" + +namespace js { + +namespace intl { + +/** + * Stores Intl data which can be shared across compartments (but not contexts). + * + * Used for data which is expensive when computed repeatedly or is not + * available through ICU. + */ +class SharedIntlData +{ + /** + * Information tracking the set of the supported time zone names, derived + * from the IANA time zone database . + * + * There are two kinds of IANA time zone names: Zone and Link (denoted as + * such in database source files). Zone names are the canonical, preferred + * name for a time zone, e.g. Asia/Kolkata. Link names simply refer to + * target Zone names for their meaning, e.g. Asia/Calcutta targets + * Asia/Kolkata. That a name is a Link doesn't *necessarily* reflect a + * sense of deprecation: some Link names also exist partly for convenience, + * e.g. UTC and GMT as Link names targeting the Zone name Etc/UTC. + * + * Two data sources determine the time zone names we support: those ICU + * supports and IANA's zone information. + * + * Unfortunately the names ICU and IANA support, and their Link + * relationships from name to target, aren't identical, so we can't simply + * implicitly trust ICU's name handling. We must perform various + * preprocessing of user-provided zone names and post-processing of + * ICU-provided zone names to implement ECMA-402's IANA-consistent behavior. + * + * Also see and + * . + */ + + using TimeZoneName = JSAtom*; + + struct TimeZoneHasher + { + struct Lookup + { + union { + const JS::Latin1Char* latin1Chars; + const char16_t* twoByteChars; + }; + bool isLatin1; + size_t length; + JS::AutoCheckCannotGC nogc; + HashNumber hash; + + explicit Lookup(JSFlatString* timeZone); + }; + + static js::HashNumber hash(const Lookup& lookup) { return lookup.hash; } + static bool match(TimeZoneName key, const Lookup& lookup); + }; + + using TimeZoneSet = js::GCHashSet; + + using TimeZoneMap = js::GCHashMap; + + /** + * As a threshold matter, available time zones are those time zones ICU + * supports, via ucal_openTimeZones. But ICU supports additional non-IANA + * time zones described in intl/icu/source/tools/tzcode/icuzones (listed in + * IntlTimeZoneData.cpp's |legacyICUTimeZones|) for its own backwards + * compatibility purposes. This set consists of ICU's supported time zones, + * minus all backwards-compatibility time zones. + */ + TimeZoneSet availableTimeZones; + + /** + * IANA treats some time zone names as Zones, that ICU instead treats as + * Links. For example, IANA considers "America/Indiana/Indianapolis" to be + * a Zone and "America/Fort_Wayne" a Link that targets it, but ICU + * considers the former a Link that targets "America/Indianapolis" (which + * IANA treats as a Link). + * + * ECMA-402 requires that we respect IANA data, so if we're asked to + * canonicalize a time zone name in this set, we must *not* return ICU's + * canonicalization. + */ + TimeZoneSet ianaZonesTreatedAsLinksByICU; + + /** + * IANA treats some time zone names as Links to one target, that ICU + * instead treats as either Zones, or Links to different targets. An + * example of the former is "Asia/Calcutta, which IANA assigns the target + * "Asia/Kolkata" but ICU considers its own Zone. An example of the latter + * is "America/Virgin", which IANA assigns the target + * "America/Port_of_Spain" but ICU assigns the target "America/St_Thomas". + * + * ECMA-402 requires that we respect IANA data, so if we're asked to + * canonicalize a time zone name that's a key in this map, we *must* return + * the corresponding value and *must not* return ICU's canonicalization. + */ + TimeZoneMap ianaLinksCanonicalizedDifferentlyByICU; + + bool timeZoneDataInitialized = false; + + /** + * Precomputes the available time zone names, because it's too expensive to + * call ucal_openTimeZones() repeatedly. + */ + bool ensureTimeZones(JSContext* cx); + + public: + /** + * Returns the validated time zone name in |result|. If the input time zone + * isn't a valid IANA time zone name, |result| remains unchanged. + */ + bool validateTimeZoneName(JSContext* cx, JS::HandleString timeZone, + JS::MutableHandleString result); + + /** + * Returns the canonical time zone name in |result|. If no canonical name + * was found, |result| remains unchanged. + * + * This method only handles time zones which are canonicalized differently + * by ICU when compared to IANA. + */ + bool tryCanonicalizeTimeZoneConsistentWithIANA(JSContext* cx, JS::HandleString timeZone, + JS::MutableHandleString result); + + void destroyInstance(); + + void trace(JSTracer* trc); + + size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const; +}; + +} // namespace intl + +} // namespace js + +#endif /* builtin_intl_SharedIntlData_h */ \ No newline at end of file diff --git a/js/src/moz.build b/js/src/moz.build index dce9e5ef92..0b80c31802 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -118,6 +118,7 @@ UNIFIED_SOURCES += [ 'builtin/intl/Collator.cpp', 'builtin/intl/CommonFunctions.cpp', 'builtin/intl/NumberFormat.cpp', + 'builtin/intl/SharedIntlData.cpp', 'builtin/MapObject.cpp', 'builtin/ModuleObject.cpp', 'builtin/Object.cpp', diff --git a/js/src/vm/Runtime.h b/js/src/vm/Runtime.h index 1bbe4658fb..0fc6e859e7 100644 --- a/js/src/vm/Runtime.h +++ b/js/src/vm/Runtime.h @@ -26,6 +26,7 @@ #endif #include "builtin/AtomicsObject.h" #include "builtin/Intl.h" +#include "builtin/intl/SharedIntlData.h" #include "builtin/Promise.h" #include "ds/FixedSizeHash.h" #include "frontend/NameCollections.h" @@ -810,7 +811,7 @@ struct JSRuntime : public JS::shadow::Runtime, const char* getDefaultLocale(); /* Shared Intl data for this runtime. */ - js::SharedIntlData sharedIntlData; + js::intl::SharedIntlData sharedIntlData; void traceSharedIntlData(JSTracer* trc);