diff options
Diffstat (limited to 'js/src/builtin/intl/IntlObject.cpp')
-rw-r--r-- | js/src/builtin/intl/IntlObject.cpp | 910 |
1 files changed, 910 insertions, 0 deletions
diff --git a/js/src/builtin/intl/IntlObject.cpp b/js/src/builtin/intl/IntlObject.cpp new file mode 100644 index 0000000000..299964d07b --- /dev/null +++ b/js/src/builtin/intl/IntlObject.cpp @@ -0,0 +1,910 @@ +/* -*- 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/. */ + +/* Implementation of the Intl object and its non-constructor properties. */ + +#include "builtin/intl/IntlObject.h" + +#include "mozilla/Assertions.h" +#include "mozilla/intl/Calendar.h" +#include "mozilla/intl/Collator.h" +#include "mozilla/intl/Currency.h" +#include "mozilla/intl/Locale.h" +#include "mozilla/intl/MeasureUnitGenerated.h" +#include "mozilla/intl/TimeZone.h" + +#include <algorithm> +#include <array> +#include <cstring> +#include <iterator> +#include <string_view> + +#include "builtin/Array.h" +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/FormatBuffer.h" +#include "builtin/intl/NumberingSystemsGenerated.h" +#include "builtin/intl/SharedIntlData.h" +#include "builtin/intl/StringAsciiChars.h" +#include "ds/Sort.h" +#include "js/Class.h" +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/GCAPI.h" +#include "js/GCVector.h" +#include "js/PropertySpec.h" +#include "js/Result.h" +#include "js/StableStringChars.h" +#include "vm/GlobalObject.h" +#include "vm/JSAtom.h" +#include "vm/JSContext.h" +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/StringType.h" +#include "vm/WellKnownAtom.h" // js_*_str + +#include "vm/JSObject-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; + +/******************** Intl ********************/ + +bool js::intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + + UniqueChars locale = intl::EncodeLocale(cx, args[0].toString()); + if (!locale) { + return false; + } + + auto result = mozilla::intl::Calendar::TryCreate(locale.get()); + if (result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return false; + } + auto calendar = result.unwrap(); + + RootedObject info(cx, NewPlainObject(cx)); + if (!info) { + return false; + } + + RootedValue v(cx); + + v.setInt32(static_cast<int32_t>(calendar->GetFirstDayOfWeek())); + if (!DefineDataProperty(cx, info, cx->names().firstDayOfWeek, v)) { + return false; + } + + v.setInt32(calendar->GetMinimalDaysInFirstWeek()); + if (!DefineDataProperty(cx, info, cx->names().minDays, v)) { + return false; + } + + Rooted<ArrayObject*> weekendArray(cx, NewDenseEmptyArray(cx)); + if (!weekendArray) { + return false; + } + + auto weekend = calendar->GetWeekend(); + if (weekend.isErr()) { + intl::ReportInternalError(cx, weekend.unwrapErr()); + return false; + } + + for (auto day : weekend.unwrap()) { + if (!NewbornArrayPush(cx, weekendArray, + Int32Value(static_cast<int32_t>(day)))) { + return false; + } + } + + v.setObject(*weekendArray); + if (!DefineDataProperty(cx, info, cx->names().weekend, v)) { + return false; + } + + args.rval().setObject(*info); + return true; +} + +static void ReportBadKey(JSContext* cx, JSString* key) { + if (UniqueChars chars = QuoteString(cx, key, '"')) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, + chars.get()); + } +} + +static bool SameOrParentLocale(JSLinearString* locale, + JSLinearString* otherLocale) { + // Return true if |locale| is the same locale as |otherLocale|. + if (locale->length() == otherLocale->length()) { + return EqualStrings(locale, otherLocale); + } + + // Also return true if |locale| is the parent locale of |otherLocale|. + if (locale->length() < otherLocale->length()) { + return HasSubstringAt(otherLocale, locale, 0) && + otherLocale->latin1OrTwoByteChar(locale->length()) == '-'; + } + + return false; +} + +using SupportedLocaleKind = js::intl::SharedIntlData::SupportedLocaleKind; + +// 9.2.2 BestAvailableLocale ( availableLocales, locale ) +static JS::Result<JSLinearString*> BestAvailableLocale( + JSContext* cx, SupportedLocaleKind kind, Handle<JSLinearString*> locale, + Handle<JSLinearString*> defaultLocale) { + // In the spec, [[availableLocales]] is formally a list of all available + // locales. But in our implementation, it's an *incomplete* list, not + // necessarily including the default locale (and all locales implied by it, + // e.g. "de" implied by "de-CH"), if that locale isn't in every + // [[availableLocales]] list (because that locale is supported through + // fallback, e.g. "de-CH" supported through "de"). + // + // If we're considering the default locale, augment the spec loop with + // additional checks to also test whether the current prefix is a prefix of + // the default locale. + + intl::SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); + + auto findLast = [](const auto* chars, size_t length) { + auto rbegin = std::make_reverse_iterator(chars + length); + auto rend = std::make_reverse_iterator(chars); + auto p = std::find(rbegin, rend, '-'); + + // |dist(chars, p.base())| is equal to |dist(p, rend)|, pick whichever you + // find easier to reason about when using reserve iterators. + ptrdiff_t r = std::distance(chars, p.base()); + MOZ_ASSERT(r == std::distance(p, rend)); + + // But always subtract one to convert from the reverse iterator result to + // the correspoding forward iterator value, because reserve iterators point + // to one element past the forward iterator value. + return r - 1; + }; + + // Step 1. + Rooted<JSLinearString*> candidate(cx, locale); + + // Step 2. + while (true) { + // Step 2.a. + bool supported = false; + if (!sharedIntlData.isSupportedLocale(cx, kind, candidate, &supported)) { + return cx->alreadyReportedError(); + } + if (supported) { + return candidate.get(); + } + + if (defaultLocale && SameOrParentLocale(candidate, defaultLocale)) { + return candidate.get(); + } + + // Step 2.b. + ptrdiff_t pos; + if (candidate->hasLatin1Chars()) { + JS::AutoCheckCannotGC nogc; + pos = findLast(candidate->latin1Chars(nogc), candidate->length()); + } else { + JS::AutoCheckCannotGC nogc; + pos = findLast(candidate->twoByteChars(nogc), candidate->length()); + } + + if (pos < 0) { + return nullptr; + } + + // Step 2.c. + size_t length = size_t(pos); + if (length >= 2 && candidate->latin1OrTwoByteChar(length - 2) == '-') { + length -= 2; + } + + // Step 2.d. + candidate = NewDependentString(cx, candidate, 0, length); + if (!candidate) { + return cx->alreadyReportedError(); + } + } +} + +// 9.2.2 BestAvailableLocale ( availableLocales, locale ) +// +// Carries an additional third argument in our implementation to provide the +// default locale. See the doc-comment in the header file. +bool js::intl_BestAvailableLocale(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + + SupportedLocaleKind kind; + { + JSLinearString* typeStr = args[0].toString()->ensureLinear(cx); + if (!typeStr) { + return false; + } + + if (StringEqualsLiteral(typeStr, "Collator")) { + kind = SupportedLocaleKind::Collator; + } else if (StringEqualsLiteral(typeStr, "DateTimeFormat")) { + kind = SupportedLocaleKind::DateTimeFormat; + } else if (StringEqualsLiteral(typeStr, "DisplayNames")) { + kind = SupportedLocaleKind::DisplayNames; + } else if (StringEqualsLiteral(typeStr, "ListFormat")) { + kind = SupportedLocaleKind::ListFormat; + } else if (StringEqualsLiteral(typeStr, "NumberFormat")) { + kind = SupportedLocaleKind::NumberFormat; + } else if (StringEqualsLiteral(typeStr, "PluralRules")) { + kind = SupportedLocaleKind::PluralRules; + } else { + MOZ_ASSERT(StringEqualsLiteral(typeStr, "RelativeTimeFormat")); + kind = SupportedLocaleKind::RelativeTimeFormat; + } + } + + Rooted<JSLinearString*> locale(cx, args[1].toString()->ensureLinear(cx)); + if (!locale) { + return false; + } + +#ifdef DEBUG + { + MOZ_ASSERT(StringIsAscii(locale), "language tags are ASCII-only"); + + // |locale| is a structurally valid language tag. + mozilla::intl::Locale tag; + + using ParserError = mozilla::intl::LocaleParser::ParserError; + mozilla::Result<mozilla::Ok, ParserError> parse_result = Ok(); + { + intl::StringAsciiChars chars(locale); + if (!chars.init(cx)) { + return false; + } + + parse_result = mozilla::intl::LocaleParser::TryParse(chars, tag); + } + + if (parse_result.isErr()) { + MOZ_ASSERT(parse_result.unwrapErr() == ParserError::OutOfMemory, + "locale is a structurally valid language tag"); + + intl::ReportInternalError(cx); + return false; + } + + MOZ_ASSERT(!tag.GetUnicodeExtension(), + "locale must contain no Unicode extensions"); + + if (auto result = tag.Canonicalize(); result.isErr()) { + MOZ_ASSERT( + result.unwrapErr() != + mozilla::intl::Locale::CanonicalizationError::DuplicateVariant); + intl::ReportInternalError(cx); + return false; + } + + intl::FormatBuffer<char, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx); + if (auto result = tag.ToString(buffer); result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return false; + } + + JSLinearString* tagStr = buffer.toString(cx); + if (!tagStr) { + return false; + } + + MOZ_ASSERT(EqualStrings(locale, tagStr), + "locale is a canonicalized language tag"); + } +#endif + + MOZ_ASSERT(args[2].isNull() || args[2].isString()); + + Rooted<JSLinearString*> defaultLocale(cx); + if (args[2].isString()) { + defaultLocale = args[2].toString()->ensureLinear(cx); + if (!defaultLocale) { + return false; + } + } + + JSString* result; + JS_TRY_VAR_OR_RETURN_FALSE( + cx, result, BestAvailableLocale(cx, kind, locale, defaultLocale)); + + if (result) { + args.rval().setString(result); + } else { + args.rval().setUndefined(); + } + return true; +} + +bool js::intl_supportedLocaleOrFallback(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + + Rooted<JSLinearString*> locale(cx, args[0].toString()->ensureLinear(cx)); + if (!locale) { + return false; + } + + mozilla::intl::Locale tag; + bool canParseLocale = false; + if (StringIsAscii(locale)) { + intl::StringAsciiChars chars(locale); + if (!chars.init(cx)) { + return false; + } + + // Tell the analysis the |tag.canonicalize()| method can't GC. + JS::AutoSuppressGCAnalysis nogc; + + canParseLocale = mozilla::intl::LocaleParser::TryParse(chars, tag).isOk() && + tag.Canonicalize().isOk(); + } + + Rooted<JSLinearString*> candidate(cx); + if (!canParseLocale) { + candidate = NewStringCopyZ<CanGC>(cx, intl::LastDitchLocale()); + if (!candidate) { + return false; + } + } else { + // The default locale must be in [[AvailableLocales]], and that list must + // not contain any locales with Unicode extension sequences, so remove any + // present in the candidate. + tag.ClearUnicodeExtension(); + + intl::FormatBuffer<char, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx); + if (auto result = tag.ToString(buffer); result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return false; + } + + candidate = buffer.toAsciiString(cx); + if (!candidate) { + return false; + } + + // Certain old-style language tags lack a script code, but in current + // usage they *would* include a script code. Map these over to modern + // forms. + for (const auto& mapping : js::intl::oldStyleLanguageTagMappings) { + const char* oldStyle = mapping.oldStyle; + const char* modernStyle = mapping.modernStyle; + + if (StringEqualsAscii(candidate, oldStyle)) { + candidate = NewStringCopyZ<CanGC>(cx, modernStyle); + if (!candidate) { + return false; + } + break; + } + } + } + + // 9.1 Internal slots of Service Constructors + // + // - [[AvailableLocales]] is a List [...]. The list must include the value + // returned by the DefaultLocale abstract operation (6.2.4), [...]. + // + // That implies we must ignore any candidate which isn't supported by all + // Intl service constructors. + + Rooted<JSLinearString*> supportedCollator(cx); + JS_TRY_VAR_OR_RETURN_FALSE( + cx, supportedCollator, + BestAvailableLocale(cx, SupportedLocaleKind::Collator, candidate, + nullptr)); + + Rooted<JSLinearString*> supportedDateTimeFormat(cx); + JS_TRY_VAR_OR_RETURN_FALSE( + cx, supportedDateTimeFormat, + BestAvailableLocale(cx, SupportedLocaleKind::DateTimeFormat, candidate, + nullptr)); + +#ifdef DEBUG + // Note: We don't test the supported locales of the remaining Intl service + // constructors, because the set of supported locales is exactly equal to + // the set of supported locales of Intl.DateTimeFormat. + for (auto kind : + {SupportedLocaleKind::DisplayNames, SupportedLocaleKind::ListFormat, + SupportedLocaleKind::NumberFormat, SupportedLocaleKind::PluralRules, + SupportedLocaleKind::RelativeTimeFormat}) { + JSLinearString* supported; + JS_TRY_VAR_OR_RETURN_FALSE( + cx, supported, BestAvailableLocale(cx, kind, candidate, nullptr)); + + MOZ_ASSERT(!!supported == !!supportedDateTimeFormat); + MOZ_ASSERT_IF(supported, EqualStrings(supported, supportedDateTimeFormat)); + } +#endif + + // Accept the candidate locale if it is supported by all Intl service + // constructors. + if (supportedCollator && supportedDateTimeFormat) { + // Use the actually supported locale instead of the candidate locale. For + // example when the candidate locale "en-US-posix" is supported through + // "en-US", use "en-US" as the default locale. + // + // Also prefer the supported locale with more subtags. For example when + // requesting "de-CH" and Intl.DateTimeFormat supports "de-CH", but + // Intl.Collator only "de", still return "de-CH" as the result. + if (SameOrParentLocale(supportedCollator, supportedDateTimeFormat)) { + candidate = supportedDateTimeFormat; + } else { + candidate = supportedCollator; + } + } else { + candidate = NewStringCopyZ<CanGC>(cx, intl::LastDitchLocale()); + if (!candidate) { + return false; + } + } + + args.rval().setString(candidate); + return true; +} + +using StringList = GCVector<JSLinearString*>; + +/** + * Create a sorted array from a list of strings. + */ +static ArrayObject* CreateArrayFromList(JSContext* cx, + MutableHandle<StringList> list) { + // Reserve scratch space for MergeSort(). + size_t initialLength = list.length(); + if (!list.growBy(initialLength)) { + return nullptr; + } + + // Sort all strings in alphabetical order. + MOZ_ALWAYS_TRUE( + MergeSort(list.begin(), initialLength, list.begin() + initialLength, + [](const auto* a, const auto* b, bool* lessOrEqual) { + *lessOrEqual = CompareStrings(a, b) <= 0; + return true; + })); + + // Ensure we don't add duplicate entries to the array. + auto* end = std::unique( + list.begin(), list.begin() + initialLength, + [](const auto* a, const auto* b) { return EqualStrings(a, b); }); + + // std::unique leaves the elements after |end| with an unspecified value, so + // remove them first. And also delete the elements in the scratch space. + list.shrinkBy(std::distance(end, list.end())); + + // And finally copy the strings into the result array. + auto* array = NewDenseFullyAllocatedArray(cx, list.length()); + if (!array) { + return nullptr; + } + array->setDenseInitializedLength(list.length()); + + for (size_t i = 0; i < list.length(); ++i) { + array->initDenseElement(i, StringValue(list[i])); + } + + return array; +} + +/** + * Create an array from a sorted list of strings. + */ +template <size_t N> +static ArrayObject* CreateArrayFromSortedList( + JSContext* cx, const std::array<const char*, N>& list) { + // Ensure the list is sorted and doesn't contain duplicates. +#ifdef DEBUG + // See bug 1583449 for why the lambda can't be in the MOZ_ASSERT. + auto isLargerThanOrEqual = [](const auto& a, const auto& b) { + return std::strcmp(a, b) >= 0; + }; +#endif + MOZ_ASSERT(std::adjacent_find(std::begin(list), std::end(list), + isLargerThanOrEqual) == std::end(list)); + + size_t length = std::size(list); + + Rooted<ArrayObject*> array(cx, NewDenseFullyAllocatedArray(cx, length)); + if (!array) { + return nullptr; + } + array->ensureDenseInitializedLength(0, length); + + for (size_t i = 0; i < length; ++i) { + auto* str = NewStringCopyZ<CanGC>(cx, list[i]); + if (!str) { + return nullptr; + } + array->initDenseElement(i, StringValue(str)); + } + return array; +} + +/** + * Create an array from an intl::Enumeration. + */ +template <const auto& unsupported, class Enumeration> +static bool EnumerationIntoList(JSContext* cx, Enumeration values, + MutableHandle<StringList> list) { + for (auto value : values) { + if (value.isErr()) { + intl::ReportInternalError(cx); + return false; + } + auto span = value.unwrap(); + + // Skip over known, unsupported values. + std::string_view sv(span.data(), span.size()); + if (std::any_of(std::begin(unsupported), std::end(unsupported), + [sv](const auto& e) { return sv == e; })) { + continue; + } + + auto* string = NewStringCopy<CanGC>(cx, span); + if (!string) { + return false; + } + if (!list.append(string)) { + return false; + } + } + + return true; +} + +/** + * Returns the list of calendar types which mustn't be returned by + * |Intl.supportedValuesOf()|. + */ +static constexpr auto UnsupportedCalendars() { + // No calendar values are currently unsupported. + return std::array<const char*, 0>{}; +} + +// Defined outside of the function to workaround bugs in GCC<9. +// Also see <https://gcc.gnu.org/bugzilla/show_bug.cgi?id=85589>. +static constexpr auto UnsupportedCalendarsArray = UnsupportedCalendars(); + +/** + * AvailableCalendars ( ) + */ +static ArrayObject* AvailableCalendars(JSContext* cx) { + Rooted<StringList> list(cx, StringList(cx)); + + { + // Hazard analysis complains that the mozilla::Result destructor calls a + // GC function, which is unsound when returning an unrooted value. Work + // around this issue by restricting the lifetime of |keywords| to a + // separate block. + auto keywords = mozilla::intl::Calendar::GetBcp47KeywordValuesForLocale(""); + if (keywords.isErr()) { + intl::ReportInternalError(cx, keywords.unwrapErr()); + return nullptr; + } + + static constexpr auto& unsupported = UnsupportedCalendarsArray; + + if (!EnumerationIntoList<unsupported>(cx, keywords.unwrap(), &list)) { + return nullptr; + } + } + + return CreateArrayFromList(cx, &list); +} + +/** + * Returns the list of collation types which mustn't be returned by + * |Intl.supportedValuesOf()|. + */ +static constexpr auto UnsupportedCollations() { + return std::array{ + "search", + "standard", + }; +} + +// Defined outside of the function to workaround bugs in GCC<9. +// Also see <https://gcc.gnu.org/bugzilla/show_bug.cgi?id=85589>. +static constexpr auto UnsupportedCollationsArray = UnsupportedCollations(); + +/** + * AvailableCollations ( ) + */ +static ArrayObject* AvailableCollations(JSContext* cx) { + Rooted<StringList> list(cx, StringList(cx)); + + { + // Hazard analysis complains that the mozilla::Result destructor calls a + // GC function, which is unsound when returning an unrooted value. Work + // around this issue by restricting the lifetime of |keywords| to a + // separate block. + auto keywords = mozilla::intl::Collator::GetBcp47KeywordValues(); + if (keywords.isErr()) { + intl::ReportInternalError(cx, keywords.unwrapErr()); + return nullptr; + } + + static constexpr auto& unsupported = UnsupportedCollationsArray; + + if (!EnumerationIntoList<unsupported>(cx, keywords.unwrap(), &list)) { + return nullptr; + } + } + + return CreateArrayFromList(cx, &list); +} + +/** + * Returns a list of known, unsupported currencies which are returned by + * |Currency::GetISOCurrencies()|. + */ +static constexpr auto UnsupportedCurrencies() { + // "MVP" is also marked with "questionable, remove?" in ucurr.cpp, but only + // this single currency code isn't supported by |Intl.DisplayNames| and + // therefore must be excluded by |Intl.supportedValuesOf|. + return std::array{ + "LSM", // https://unicode-org.atlassian.net/browse/ICU-21687 + }; +} + +/** + * Return a list of known, missing currencies which aren't returned by + * |Currency::GetISOCurrencies()|. + */ +static constexpr auto MissingCurrencies() { + return std::array{ + "SLE", // https://unicode-org.atlassian.net/browse/ICU-21989 + "VED", // https://unicode-org.atlassian.net/browse/ICU-21989 + }; +} + +// Defined outside of the function to workaround bugs in GCC<9. +// Also see <https://gcc.gnu.org/bugzilla/show_bug.cgi?id=85589>. +static constexpr auto UnsupportedCurrenciesArray = UnsupportedCurrencies(); +static constexpr auto MissingCurrenciesArray = MissingCurrencies(); + +/** + * AvailableCurrencies ( ) + */ +static ArrayObject* AvailableCurrencies(JSContext* cx) { + Rooted<StringList> list(cx, StringList(cx)); + + { + // Hazard analysis complains that the mozilla::Result destructor calls a + // GC function, which is unsound when returning an unrooted value. Work + // around this issue by restricting the lifetime of |currencies| to a + // separate block. + auto currencies = mozilla::intl::Currency::GetISOCurrencies(); + if (currencies.isErr()) { + intl::ReportInternalError(cx, currencies.unwrapErr()); + return nullptr; + } + + static constexpr auto& unsupported = UnsupportedCurrenciesArray; + + if (!EnumerationIntoList<unsupported>(cx, currencies.unwrap(), &list)) { + return nullptr; + } + } + + // Add known missing values. + for (const char* value : MissingCurrenciesArray) { + auto* string = NewStringCopyZ<CanGC>(cx, value); + if (!string) { + return nullptr; + } + if (!list.append(string)) { + return nullptr; + } + } + + return CreateArrayFromList(cx, &list); +} + +/** + * AvailableNumberingSystems ( ) + */ +static ArrayObject* AvailableNumberingSystems(JSContext* cx) { + static constexpr std::array numberingSystems = { + NUMBERING_SYSTEMS_WITH_SIMPLE_DIGIT_MAPPINGS}; + + return CreateArrayFromSortedList(cx, numberingSystems); +} + +/** + * AvailableTimeZones ( ) + */ +static ArrayObject* AvailableTimeZones(JSContext* cx) { + // Unsorted list of canonical time zone names, possibly containing + // duplicates. + Rooted<StringList> timeZones(cx, StringList(cx)); + + intl::SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); + auto iterResult = sharedIntlData.availableTimeZonesIteration(cx); + if (iterResult.isErr()) { + return nullptr; + } + auto iter = iterResult.unwrap(); + + Rooted<JSAtom*> validatedTimeZone(cx); + Rooted<JSAtom*> ianaTimeZone(cx); + for (; !iter.done(); iter.next()) { + validatedTimeZone = iter.get(); + + // Canonicalize the time zone before adding it to the result array. + + // Some time zone names are canonicalized differently by ICU -- handle + // those first. + ianaTimeZone.set(nullptr); + if (!sharedIntlData.tryCanonicalizeTimeZoneConsistentWithIANA( + cx, validatedTimeZone, &ianaTimeZone)) { + return nullptr; + } + + JSLinearString* timeZone; + if (ianaTimeZone) { + cx->markAtom(ianaTimeZone); + + timeZone = ianaTimeZone; + } else { + // Call into ICU to canonicalize the time zone. + + JS::AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, validatedTimeZone)) { + return nullptr; + } + + intl::FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> + canonicalTimeZone(cx); + auto result = mozilla::intl::TimeZone::GetCanonicalTimeZoneID( + stableChars.twoByteRange(), canonicalTimeZone); + if (result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return nullptr; + } + + timeZone = canonicalTimeZone.toString(cx); + if (!timeZone) { + return nullptr; + } + + // Canonicalize both to "UTC" per CanonicalizeTimeZoneName(). + if (StringEqualsLiteral(timeZone, "Etc/UTC") || + StringEqualsLiteral(timeZone, "Etc/GMT")) { + timeZone = cx->names().UTC; + } + } + + if (!timeZones.append(timeZone)) { + return nullptr; + } + } + + return CreateArrayFromList(cx, &timeZones); +} + +template <size_t N> +constexpr auto MeasurementUnitNames( + const mozilla::intl::SimpleMeasureUnit (&units)[N]) { + std::array<const char*, N> array = {}; + for (size_t i = 0; i < N; ++i) { + array[i] = units[i].name; + } + return array; +} + +/** + * AvailableUnits ( ) + */ +static ArrayObject* AvailableUnits(JSContext* cx) { + static constexpr auto simpleMeasureUnitNames = + MeasurementUnitNames(mozilla::intl::simpleMeasureUnits); + + return CreateArrayFromSortedList(cx, simpleMeasureUnitNames); +} + +bool js::intl_SupportedValuesOf(JSContext* cx, unsigned argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + JSLinearString* key = args[0].toString()->ensureLinear(cx); + if (!key) { + return false; + } + + ArrayObject* list; + if (StringEqualsLiteral(key, "calendar")) { + list = AvailableCalendars(cx); + } else if (StringEqualsLiteral(key, "collation")) { + list = AvailableCollations(cx); + } else if (StringEqualsLiteral(key, "currency")) { + list = AvailableCurrencies(cx); + } else if (StringEqualsLiteral(key, "numberingSystem")) { + list = AvailableNumberingSystems(cx); + } else if (StringEqualsLiteral(key, "timeZone")) { + list = AvailableTimeZones(cx); + } else if (StringEqualsLiteral(key, "unit")) { + list = AvailableUnits(cx); + } else { + ReportBadKey(cx, key); + return false; + } + if (!list) { + return false; + } + + args.rval().setObject(*list); + return true; +} + +static bool intl_toSource(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().Intl); + return true; +} + +static const JSFunctionSpec intl_static_methods[] = { + JS_FN(js_toSource_str, intl_toSource, 0, 0), + JS_SELF_HOSTED_FN("getCanonicalLocales", "Intl_getCanonicalLocales", 1, 0), + JS_SELF_HOSTED_FN("supportedValuesOf", "Intl_supportedValuesOf", 1, 0), + JS_FS_END}; + +static const JSPropertySpec intl_static_properties[] = { + JS_STRING_SYM_PS(toStringTag, "Intl", JSPROP_READONLY), JS_PS_END}; + +static JSObject* CreateIntlObject(JSContext* cx, JSProtoKey key) { + RootedObject proto(cx, &cx->global()->getObjectPrototype()); + + // The |Intl| object is just a plain object with some "static" function + // properties and some constructor properties. + return NewTenuredObjectWithGivenProto(cx, &IntlClass, proto); +} + +/** + * Initializes the Intl Object and its standard built-in properties. + * Spec: ECMAScript Internationalization API Specification, 8.0, 8.1 + */ +static bool IntlClassFinish(JSContext* cx, HandleObject intl, + HandleObject proto) { + // Add the constructor properties. + RootedId ctorId(cx); + RootedValue ctorValue(cx); + for (const auto& protoKey : + {JSProto_Collator, JSProto_DateTimeFormat, JSProto_DisplayNames, + JSProto_ListFormat, JSProto_Locale, JSProto_NumberFormat, + JSProto_PluralRules, JSProto_RelativeTimeFormat}) { + JSObject* ctor = GlobalObject::getOrCreateConstructor(cx, protoKey); + if (!ctor) { + return false; + } + + ctorId = NameToId(ClassName(protoKey, cx)); + ctorValue.setObject(*ctor); + if (!DefineDataProperty(cx, intl, ctorId, ctorValue, 0)) { + return false; + } + } + + return true; +} + +static const ClassSpec IntlClassSpec = { + CreateIntlObject, nullptr, intl_static_methods, intl_static_properties, + nullptr, nullptr, IntlClassFinish}; + +const JSClass js::IntlClass = {"Intl", JSCLASS_HAS_CACHED_PROTO(JSProto_Intl), + JS_NULL_CLASS_OPS, &IntlClassSpec}; |