tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

Locale.cpp (50413B)


      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 /* Intl.Locale implementation. */
      8 
      9 #include "builtin/intl/Locale.h"
     10 
     11 #include "mozilla/ArrayUtils.h"
     12 #include "mozilla/Assertions.h"
     13 #include "mozilla/intl/Locale.h"
     14 #include "mozilla/Maybe.h"
     15 #include "mozilla/Span.h"
     16 #include "mozilla/TextUtils.h"
     17 
     18 #include <algorithm>
     19 #include <string>
     20 #include <string.h>
     21 #include <utility>
     22 
     23 #include "builtin/Array.h"
     24 #include "builtin/Boolean.h"
     25 #include "builtin/intl/CommonFunctions.h"
     26 #include "builtin/intl/FormatBuffer.h"
     27 #include "builtin/intl/LanguageTag.h"
     28 #include "builtin/intl/LocaleNegotiation.h"
     29 #include "builtin/intl/StringAsciiChars.h"
     30 #include "builtin/String.h"
     31 #include "js/Conversions.h"
     32 #include "js/friend/ErrorMessages.h"  // js::GetErrorMessage, JSMSG_*
     33 #include "js/Printer.h"
     34 #include "js/TypeDecls.h"
     35 #include "js/Wrapper.h"
     36 #include "vm/Compartment.h"
     37 #include "vm/GlobalObject.h"
     38 #include "vm/JSContext.h"
     39 #include "vm/PlainObject.h"  // js::PlainObject
     40 #include "vm/StringType.h"
     41 
     42 #include "vm/JSObject-inl.h"
     43 #include "vm/NativeObject-inl.h"
     44 
     45 using namespace js;
     46 using namespace mozilla::intl::LanguageTagLimits;
     47 
     48 const JSClass LocaleObject::class_ = {
     49    "Intl.Locale",
     50    JSCLASS_HAS_RESERVED_SLOTS(LocaleObject::SLOT_COUNT) |
     51        JSCLASS_HAS_CACHED_PROTO(JSProto_Locale),
     52    JS_NULL_CLASS_OPS,
     53    &LocaleObject::classSpec_,
     54 };
     55 
     56 const JSClass& LocaleObject::protoClass_ = PlainObject::class_;
     57 
     58 static inline bool IsLocale(HandleValue v) {
     59  return v.isObject() && v.toObject().is<LocaleObject>();
     60 }
     61 
     62 // Return the length of the base-name subtags.
     63 static size_t BaseNameLength(const mozilla::intl::Locale& tag) {
     64  size_t baseNameLength = tag.Language().Length();
     65  if (tag.Script().Present()) {
     66    baseNameLength += 1 + tag.Script().Length();
     67  }
     68  if (tag.Region().Present()) {
     69    baseNameLength += 1 + tag.Region().Length();
     70  }
     71  for (const auto& variant : tag.Variants()) {
     72    baseNameLength += 1 + variant.size();
     73  }
     74  return baseNameLength;
     75 }
     76 
     77 struct IndexAndLength {
     78  size_t index;
     79  size_t length;
     80 
     81  IndexAndLength(size_t index, size_t length) : index(index), length(length) {};
     82 
     83  template <typename T>
     84  mozilla::Span<const T> spanOf(const T* ptr) const {
     85    return {ptr + index, length};
     86  }
     87 };
     88 
     89 // Compute the Unicode extension's index and length in the extension subtag.
     90 static mozilla::Maybe<IndexAndLength> UnicodeExtensionPosition(
     91    const mozilla::intl::Locale& tag) {
     92  size_t index = 0;
     93  for (const auto& extension : tag.Extensions()) {
     94    MOZ_ASSERT(!mozilla::IsAsciiUppercaseAlpha(extension[0]),
     95               "extensions are case normalized to lowercase");
     96 
     97    size_t extensionLength = extension.size();
     98    if (extension[0] == 'u') {
     99      return mozilla::Some(IndexAndLength{index, extensionLength});
    100    }
    101 
    102    // Add +1 to skip over the preceding separator.
    103    index += 1 + extensionLength;
    104  }
    105  return mozilla::Nothing();
    106 }
    107 
    108 static LocaleObject* CreateLocaleObject(JSContext* cx, HandleObject prototype,
    109                                        const mozilla::intl::Locale& tag) {
    110  intl::FormatBuffer<char, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
    111  if (auto result = tag.ToString(buffer); result.isErr()) {
    112    intl::ReportInternalError(cx, result.unwrapErr());
    113    return nullptr;
    114  }
    115 
    116  RootedString tagStr(cx, buffer.toAsciiString(cx));
    117  if (!tagStr) {
    118    return nullptr;
    119  }
    120 
    121  size_t baseNameLength = BaseNameLength(tag);
    122 
    123  RootedString baseName(cx, NewDependentString(cx, tagStr, 0, baseNameLength));
    124  if (!baseName) {
    125    return nullptr;
    126  }
    127 
    128  RootedValue unicodeExtension(cx, UndefinedValue());
    129  if (auto result = UnicodeExtensionPosition(tag)) {
    130    JSString* str = NewDependentString(
    131        cx, tagStr, baseNameLength + 1 + result->index, result->length);
    132    if (!str) {
    133      return nullptr;
    134    }
    135 
    136    unicodeExtension.setString(str);
    137  }
    138 
    139  auto* locale = NewObjectWithClassProto<LocaleObject>(cx, prototype);
    140  if (!locale) {
    141    return nullptr;
    142  }
    143 
    144  locale->initFixedSlot(LocaleObject::LANGUAGE_TAG_SLOT, StringValue(tagStr));
    145  locale->initFixedSlot(LocaleObject::BASENAME_SLOT, StringValue(baseName));
    146  locale->initFixedSlot(LocaleObject::UNICODE_EXTENSION_SLOT, unicodeExtension);
    147 
    148  return locale;
    149 }
    150 
    151 static inline bool IsValidUnicodeExtensionValue(JSContext* cx,
    152                                                const JSLinearString* linear,
    153                                                bool* isValid) {
    154  if (linear->length() == 0) {
    155    *isValid = false;
    156    return true;
    157  }
    158 
    159  if (!StringIsAscii(linear)) {
    160    *isValid = false;
    161    return true;
    162  }
    163 
    164  intl::StringAsciiChars chars(linear);
    165  if (!chars.init(cx)) {
    166    return false;
    167  }
    168 
    169  *isValid =
    170      mozilla::intl::LocaleParser::CanParseUnicodeExtensionType(chars).isOk();
    171  return true;
    172 }
    173 
    174 /**
    175 * Iterate through (sep keyword) in a valid Unicode extension.
    176 *
    177 * The Unicode extension value is not required to be in canonical case.
    178 */
    179 template <typename CharT>
    180 class SepKeywordIterator {
    181  const CharT* iter_;
    182  const CharT* const end_;
    183 
    184 public:
    185  SepKeywordIterator(const CharT* unicodeExtensionBegin,
    186                     const CharT* unicodeExtensionEnd)
    187      : iter_(unicodeExtensionBegin), end_(unicodeExtensionEnd) {}
    188 
    189  /**
    190   * Return (sep keyword) in the Unicode locale extension from begin to end.
    191   * The first call after all (sep keyword) are consumed returns |nullptr|; no
    192   * further calls are allowed.
    193   */
    194  const CharT* next() {
    195    MOZ_ASSERT(iter_ != nullptr,
    196               "can't call next() once it's returned nullptr");
    197 
    198    constexpr size_t SepKeyLength = 1 + UnicodeKeyLength;  // "-co"/"-nu"/etc.
    199 
    200    MOZ_ASSERT(iter_ + SepKeyLength <= end_,
    201               "overall Unicode locale extension or non-leading subtags must "
    202               "be at least key-sized");
    203 
    204    MOZ_ASSERT(((iter_[0] == 'u' || iter_[0] == 'U') && iter_[1] == '-') ||
    205               iter_[0] == '-');
    206 
    207    while (true) {
    208      // Skip past '-' so |std::char_traits::find| makes progress. Skipping
    209      // 'u' is harmless -- skip or not, |find| returns the first '-'.
    210      iter_++;
    211 
    212      // Find the next separator.
    213      iter_ = std::char_traits<CharT>::find(
    214          iter_, mozilla::PointerRangeSize(iter_, end_), CharT('-'));
    215      if (!iter_) {
    216        return nullptr;
    217      }
    218 
    219      MOZ_ASSERT(iter_ + SepKeyLength <= end_,
    220                 "non-leading subtags in a Unicode locale extension are all "
    221                 "at least as long as a key");
    222 
    223      if (iter_ + SepKeyLength == end_ ||  // key is terminal subtag
    224          iter_[SepKeyLength] == '-') {    // key is followed by more subtags
    225        break;
    226      }
    227    }
    228 
    229    MOZ_ASSERT(iter_[0] == '-');
    230    MOZ_ASSERT(mozilla::IsAsciiAlphanumeric(iter_[1]));
    231    MOZ_ASSERT(mozilla::IsAsciiAlpha(iter_[2]));
    232    MOZ_ASSERT_IF(iter_ + SepKeyLength < end_, iter_[SepKeyLength] == '-');
    233    return iter_;
    234  }
    235 };
    236 
    237 /**
    238 * 9.2.10 GetOption ( options, property, type, values, fallback )
    239 *
    240 * If the requested property is present and not-undefined, set the result string
    241 * to |ToString(value)|. Otherwise set the result string to nullptr.
    242 */
    243 static bool GetStringOption(JSContext* cx, HandleObject options,
    244                            Handle<PropertyName*> name,
    245                            MutableHandle<JSLinearString*> string) {
    246  // Step 1.
    247  RootedValue option(cx);
    248  if (!GetProperty(cx, options, options, name, &option)) {
    249    return false;
    250  }
    251 
    252  // Step 2.
    253  JSLinearString* linear = nullptr;
    254  if (!option.isUndefined()) {
    255    // Steps 2.a-b, 2.d (not applicable).
    256 
    257    // Steps 2.c, 2.e.
    258    JSString* str = ToString(cx, option);
    259    if (!str) {
    260      return false;
    261    }
    262    linear = str->ensureLinear(cx);
    263    if (!linear) {
    264      return false;
    265    }
    266  }
    267 
    268  // Step 3.
    269  string.set(linear);
    270  return true;
    271 }
    272 
    273 /**
    274 * 9.2.10 GetOption ( options, property, type, values, fallback )
    275 *
    276 * If the requested property is present and not-undefined, set the result string
    277 * to |ToString(ToBoolean(value))|. Otherwise set the result string to nullptr.
    278 */
    279 static bool GetBooleanOption(JSContext* cx, HandleObject options,
    280                             Handle<PropertyName*> name,
    281                             MutableHandle<JSLinearString*> string) {
    282  // Step 1.
    283  RootedValue option(cx);
    284  if (!GetProperty(cx, options, options, name, &option)) {
    285    return false;
    286  }
    287 
    288  // Step 2.
    289  JSLinearString* linear = nullptr;
    290  if (!option.isUndefined()) {
    291    // Steps 2.a, 2.c-d (not applicable).
    292 
    293    // Steps 2.c, 2.e.
    294    linear = BooleanToString(cx, ToBoolean(option));
    295  }
    296 
    297  // Step 3.
    298  string.set(linear);
    299  return true;
    300 }
    301 
    302 /**
    303 * ApplyOptionsToTag ( tag, options )
    304 */
    305 static bool ApplyOptionsToTag(JSContext* cx, mozilla::intl::Locale& tag,
    306                              HandleObject options) {
    307  // Step 1. (Not applicable in our implementation.)
    308 
    309  Rooted<JSLinearString*> option(cx);
    310 
    311  // Step 2.
    312  if (!GetStringOption(cx, options, cx->names().language, &option)) {
    313    return false;
    314  }
    315 
    316  // Step 3.
    317  mozilla::intl::LanguageSubtag language;
    318  if (option && !intl::ParseStandaloneLanguageTag(option, language)) {
    319    if (UniqueChars str = QuoteString(cx, option, '"')) {
    320      JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
    321                                JSMSG_INVALID_OPTION_VALUE, "language",
    322                                str.get());
    323    }
    324    return false;
    325  }
    326 
    327  // Step 4.
    328  if (!GetStringOption(cx, options, cx->names().script, &option)) {
    329    return false;
    330  }
    331 
    332  // Step 5.
    333  mozilla::intl::ScriptSubtag script;
    334  if (option && !intl::ParseStandaloneScriptTag(option, script)) {
    335    if (UniqueChars str = QuoteString(cx, option, '"')) {
    336      JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
    337                                JSMSG_INVALID_OPTION_VALUE, "script",
    338                                str.get());
    339    }
    340    return false;
    341  }
    342 
    343  // Step 6.
    344  if (!GetStringOption(cx, options, cx->names().region, &option)) {
    345    return false;
    346  }
    347 
    348  // Step 7.
    349  mozilla::intl::RegionSubtag region;
    350  if (option && !intl::ParseStandaloneRegionTag(option, region)) {
    351    if (UniqueChars str = QuoteString(cx, option, '"')) {
    352      JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
    353                                JSMSG_INVALID_OPTION_VALUE, "region",
    354                                str.get());
    355    }
    356    return false;
    357  }
    358 
    359  // Step 8.
    360  if (!GetStringOption(cx, options, cx->names().variants, &option)) {
    361    return false;
    362  }
    363 
    364  // Step 9.
    365  mozilla::intl::Locale::VariantsVector variants;
    366  if (option) {
    367    bool ok;
    368    if (!intl::ParseStandaloneVariantTag(option, variants, &ok)) {
    369      ReportOutOfMemory(cx);
    370      return false;
    371    }
    372    if (!ok) {
    373      if (UniqueChars str = QuoteString(cx, option, '"')) {
    374        JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
    375                                  JSMSG_INVALID_OPTION_VALUE, "variants",
    376                                  str.get());
    377      }
    378      return false;
    379    }
    380  }
    381 
    382  // Skip steps 10-15 when no subtags were modified.
    383  if (language.Present() || script.Present() || region.Present() ||
    384      !variants.empty()) {
    385    // Step 10. (Not applicable in our implementation.)
    386 
    387    // Step 11.
    388    if (language.Present()) {
    389      tag.SetLanguage(language);
    390    }
    391 
    392    // Step 12.
    393    if (script.Present()) {
    394      tag.SetScript(script);
    395    }
    396 
    397    // Step 13.
    398    if (region.Present()) {
    399      tag.SetRegion(region);
    400    }
    401 
    402    // Step 14.
    403    if (!variants.empty()) {
    404      tag.SetVariants(std::move(variants));
    405    }
    406 
    407    // Step 15.
    408    //
    409    // Optimization to perform base-name canonicalization early. This avoids
    410    // extra work later on.
    411    auto result = tag.CanonicalizeBaseName();
    412    if (result.isErr()) {
    413      if (result.unwrapErr() ==
    414          mozilla::intl::Locale::CanonicalizationError::DuplicateVariant) {
    415        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
    416                                  JSMSG_DUPLICATE_VARIANT_SUBTAG);
    417      } else {
    418        intl::ReportInternalError(cx);
    419      }
    420      return false;
    421    }
    422  }
    423 
    424  // Step 16.
    425  return true;
    426 }
    427 
    428 /**
    429 * ApplyUnicodeExtensionToTag( tag, options, relevantExtensionKeys )
    430 */
    431 bool js::intl::ApplyUnicodeExtensionToTag(
    432    JSContext* cx, mozilla::intl::Locale& tag,
    433    JS::HandleVector<intl::UnicodeExtensionKeyword> keywords) {
    434  // If no Unicode extensions were present in the options object, we can skip
    435  // everything below and directly return.
    436  if (keywords.length() == 0) {
    437    return true;
    438  }
    439 
    440  Vector<char, 32> newExtension(cx);
    441  if (!newExtension.append('u')) {
    442    return false;
    443  }
    444 
    445  // Check if there's an existing Unicode extension subtag.
    446 
    447  const char* unicodeExtensionEnd = nullptr;
    448  const char* unicodeExtensionKeywords = nullptr;
    449  if (auto unicodeExtension = tag.GetUnicodeExtension()) {
    450    const char* unicodeExtensionBegin = unicodeExtension->data();
    451    unicodeExtensionEnd = unicodeExtensionBegin + unicodeExtension->size();
    452 
    453    SepKeywordIterator<char> iter(unicodeExtensionBegin, unicodeExtensionEnd);
    454 
    455    // Find the start of the first keyword.
    456    unicodeExtensionKeywords = iter.next();
    457 
    458    // Copy any attributes present before the first keyword.
    459    const char* attributesEnd = unicodeExtensionKeywords
    460                                    ? unicodeExtensionKeywords
    461                                    : unicodeExtensionEnd;
    462    if (!newExtension.append(unicodeExtensionBegin + 1, attributesEnd)) {
    463      return false;
    464    }
    465  }
    466 
    467  // Append the new keywords before any existing keywords. That way any previous
    468  // keyword with the same key is detected as a duplicate when canonicalizing
    469  // the Unicode extension subtag and gets discarded.
    470 
    471  for (const auto& keyword : keywords) {
    472    UnicodeExtensionKeyword::UnicodeKeySpan key = keyword.key();
    473    if (!newExtension.append('-')) {
    474      return false;
    475    }
    476    if (!newExtension.append(key.data(), key.size())) {
    477      return false;
    478    }
    479    if (!newExtension.append('-')) {
    480      return false;
    481    }
    482 
    483    JS::AutoCheckCannotGC nogc;
    484    JSLinearString* type = keyword.type();
    485    if (type->hasLatin1Chars()) {
    486      if (!newExtension.append(type->latin1Chars(nogc), type->length())) {
    487        return false;
    488      }
    489    } else {
    490      if (!newExtension.append(type->twoByteChars(nogc), type->length())) {
    491        return false;
    492      }
    493    }
    494  }
    495 
    496  // Append the remaining keywords from the previous Unicode extension subtag.
    497  if (unicodeExtensionKeywords) {
    498    if (!newExtension.append(unicodeExtensionKeywords, unicodeExtensionEnd)) {
    499      return false;
    500    }
    501  }
    502 
    503  if (auto res = tag.SetUnicodeExtension(newExtension); res.isErr()) {
    504    intl::ReportInternalError(cx, res.unwrapErr());
    505    return false;
    506  }
    507 
    508  return true;
    509 }
    510 
    511 static JS::Result<JSString*> LanguageTagFromMaybeWrappedLocale(JSContext* cx,
    512                                                               JSObject* obj) {
    513  if (obj->is<LocaleObject>()) {
    514    return obj->as<LocaleObject>().languageTag();
    515  }
    516 
    517  JSObject* unwrapped = CheckedUnwrapStatic(obj);
    518  if (!unwrapped) {
    519    ReportAccessDenied(cx);
    520    return cx->alreadyReportedError();
    521  }
    522 
    523  if (!unwrapped->is<LocaleObject>()) {
    524    return nullptr;
    525  }
    526 
    527  RootedString tagStr(cx, unwrapped->as<LocaleObject>().languageTag());
    528  if (!cx->compartment()->wrap(cx, &tagStr)) {
    529    return cx->alreadyReportedError();
    530  }
    531  return tagStr.get();
    532 }
    533 
    534 /**
    535 * Intl.Locale( tag[, options] )
    536 */
    537 static bool Locale(JSContext* cx, unsigned argc, Value* vp) {
    538  CallArgs args = CallArgsFromVp(argc, vp);
    539 
    540  // Step 1.
    541  if (!ThrowIfNotConstructing(cx, args, "Intl.Locale")) {
    542    return false;
    543  }
    544 
    545  // Steps 2-6 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
    546  RootedObject proto(cx);
    547  if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Locale, &proto)) {
    548    return false;
    549  }
    550 
    551  // Steps 7-9.
    552  HandleValue tagValue = args.get(0);
    553  JSString* tagStr;
    554  if (tagValue.isObject()) {
    555    JS_TRY_VAR_OR_RETURN_FALSE(
    556        cx, tagStr,
    557        LanguageTagFromMaybeWrappedLocale(cx, &tagValue.toObject()));
    558    if (!tagStr) {
    559      tagStr = ToString(cx, tagValue);
    560      if (!tagStr) {
    561        return false;
    562      }
    563    }
    564  } else if (tagValue.isString()) {
    565    tagStr = tagValue.toString();
    566  } else {
    567    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
    568                              JSMSG_INVALID_LOCALES_ELEMENT);
    569    return false;
    570  }
    571 
    572  Rooted<JSLinearString*> tagLinearStr(cx, tagStr->ensureLinear(cx));
    573  if (!tagLinearStr) {
    574    return false;
    575  }
    576 
    577  // Step 10.
    578  RootedObject options(cx);
    579  if (args.hasDefined(1)) {
    580    options = ToObject(cx, args[1]);
    581    if (!options) {
    582      return false;
    583    }
    584  }
    585 
    586  // Step 11.
    587  mozilla::intl::Locale tag;
    588  if (!intl::ParseLocale(cx, tagLinearStr, tag)) {
    589    return false;
    590  }
    591 
    592  if (tag.Language().Length() > 4) {
    593    MOZ_ASSERT(cx->global());
    594    cx->runtime()->setUseCounter(cx->global(),
    595                                 JSUseCounter::LEGACY_LANG_SUBTAG);
    596  }
    597 
    598  // Step 12. (Optimized to only perform base-name canonicalization.)
    599  if (auto result = tag.CanonicalizeBaseName(); result.isErr()) {
    600    if (result.unwrapErr() ==
    601        mozilla::intl::Locale::CanonicalizationError::DuplicateVariant) {
    602      JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
    603                                JSMSG_DUPLICATE_VARIANT_SUBTAG);
    604    } else {
    605      intl::ReportInternalError(cx);
    606    }
    607    return false;
    608  }
    609 
    610  if (options) {
    611    // Step 13.
    612    if (!ApplyOptionsToTag(cx, tag, options)) {
    613      return false;
    614    }
    615 
    616    // Step 14.
    617    JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx);
    618 
    619    // Step 15.
    620    Rooted<JSLinearString*> calendar(cx);
    621    if (!GetStringOption(cx, options, cx->names().calendar, &calendar)) {
    622      return false;
    623    }
    624 
    625    // Steps 16-17.
    626    if (calendar) {
    627      bool isValid;
    628      if (!IsValidUnicodeExtensionValue(cx, calendar, &isValid)) {
    629        return false;
    630      }
    631 
    632      if (!isValid) {
    633        if (UniqueChars str = QuoteString(cx, calendar, '"')) {
    634          JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
    635                                    JSMSG_INVALID_OPTION_VALUE, "calendar",
    636                                    str.get());
    637        }
    638        return false;
    639      }
    640 
    641      if (!keywords.emplaceBack("ca", calendar)) {
    642        return false;
    643      }
    644    }
    645 
    646    // Step 18.
    647    Rooted<JSLinearString*> collation(cx);
    648    if (!GetStringOption(cx, options, cx->names().collation, &collation)) {
    649      return false;
    650    }
    651 
    652    // Steps 19-20.
    653    if (collation) {
    654      bool isValid;
    655      if (!IsValidUnicodeExtensionValue(cx, collation, &isValid)) {
    656        return false;
    657      }
    658 
    659      if (!isValid) {
    660        if (UniqueChars str = QuoteString(cx, collation, '"')) {
    661          JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
    662                                    JSMSG_INVALID_OPTION_VALUE, "collation",
    663                                    str.get());
    664        }
    665        return false;
    666      }
    667 
    668      if (!keywords.emplaceBack("co", collation)) {
    669        return false;
    670      }
    671    }
    672 
    673    // Step 21 (without validation).
    674    Rooted<JSLinearString*> hourCycle(cx);
    675    if (!GetStringOption(cx, options, cx->names().hourCycle, &hourCycle)) {
    676      return false;
    677    }
    678 
    679    // Steps 21-22.
    680    if (hourCycle) {
    681      if (!StringEqualsLiteral(hourCycle, "h11") &&
    682          !StringEqualsLiteral(hourCycle, "h12") &&
    683          !StringEqualsLiteral(hourCycle, "h23") &&
    684          !StringEqualsLiteral(hourCycle, "h24")) {
    685        if (UniqueChars str = QuoteString(cx, hourCycle, '"')) {
    686          JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
    687                                    JSMSG_INVALID_OPTION_VALUE, "hourCycle",
    688                                    str.get());
    689        }
    690        return false;
    691      }
    692 
    693      if (!keywords.emplaceBack("hc", hourCycle)) {
    694        return false;
    695      }
    696    }
    697 
    698    // Step 23 (without validation).
    699    Rooted<JSLinearString*> caseFirst(cx);
    700    if (!GetStringOption(cx, options, cx->names().caseFirst, &caseFirst)) {
    701      return false;
    702    }
    703 
    704    // Steps 23-24.
    705    if (caseFirst) {
    706      if (!StringEqualsLiteral(caseFirst, "upper") &&
    707          !StringEqualsLiteral(caseFirst, "lower") &&
    708          !StringEqualsLiteral(caseFirst, "false")) {
    709        if (UniqueChars str = QuoteString(cx, caseFirst, '"')) {
    710          JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
    711                                    JSMSG_INVALID_OPTION_VALUE, "caseFirst",
    712                                    str.get());
    713        }
    714        return false;
    715      }
    716 
    717      if (!keywords.emplaceBack("kf", caseFirst)) {
    718        return false;
    719      }
    720    }
    721 
    722    // Steps 25-26.
    723    Rooted<JSLinearString*> numeric(cx);
    724    if (!GetBooleanOption(cx, options, cx->names().numeric, &numeric)) {
    725      return false;
    726    }
    727 
    728    // Step 27.
    729    if (numeric) {
    730      if (!keywords.emplaceBack("kn", numeric)) {
    731        return false;
    732      }
    733    }
    734 
    735    // Step 28.
    736    Rooted<JSLinearString*> numberingSystem(cx);
    737    if (!GetStringOption(cx, options, cx->names().numberingSystem,
    738                         &numberingSystem)) {
    739      return false;
    740    }
    741 
    742    // Steps 29-30.
    743    if (numberingSystem) {
    744      bool isValid;
    745      if (!IsValidUnicodeExtensionValue(cx, numberingSystem, &isValid)) {
    746        return false;
    747      }
    748      if (!isValid) {
    749        if (UniqueChars str = QuoteString(cx, numberingSystem, '"')) {
    750          JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
    751                                    JSMSG_INVALID_OPTION_VALUE,
    752                                    "numberingSystem", str.get());
    753        }
    754        return false;
    755      }
    756 
    757      if (!keywords.emplaceBack("nu", numberingSystem)) {
    758        return false;
    759      }
    760    }
    761 
    762    // Step 31.
    763    if (!ApplyUnicodeExtensionToTag(cx, tag, keywords)) {
    764      return false;
    765    }
    766  }
    767 
    768  // ApplyUnicodeExtensionToTag, steps 6-7.
    769  if (auto result = tag.CanonicalizeExtensions(); result.isErr()) {
    770    if (result.unwrapErr() ==
    771        mozilla::intl::Locale::CanonicalizationError::DuplicateVariant) {
    772      JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
    773                                JSMSG_DUPLICATE_VARIANT_SUBTAG);
    774    } else {
    775      intl::ReportInternalError(cx);
    776    }
    777    return false;
    778  }
    779 
    780  // Steps 6, 32-38.
    781  JSObject* obj = CreateLocaleObject(cx, proto, tag);
    782  if (!obj) {
    783    return false;
    784  }
    785 
    786  // Step 39.
    787  args.rval().setObject(*obj);
    788  return true;
    789 }
    790 
    791 using UnicodeKey = const char (&)[UnicodeKeyLength + 1];
    792 
    793 // Returns the tuple [index, length] of the `type` in the `keyword` in Unicode
    794 // locale extension |extension| that has |key| as its `key`. If `keyword` lacks
    795 // a type, the returned |index| will be where `type` would have been, and
    796 // |length| will be set to zero.
    797 template <typename CharT>
    798 static mozilla::Maybe<IndexAndLength> FindUnicodeExtensionType(
    799    const CharT* extension, size_t length, UnicodeKey key) {
    800  MOZ_ASSERT(extension[0] == 'u');
    801  MOZ_ASSERT(extension[1] == '-');
    802 
    803  const CharT* end = extension + length;
    804 
    805  SepKeywordIterator<CharT> iter(extension, end);
    806 
    807  // Search all keywords until a match was found.
    808  const CharT* beginKey;
    809  while (true) {
    810    beginKey = iter.next();
    811    if (!beginKey) {
    812      return mozilla::Nothing();
    813    }
    814 
    815    // Add +1 to skip over the separator preceding the keyword.
    816    MOZ_ASSERT(beginKey[0] == '-');
    817    beginKey++;
    818 
    819    // Exit the loop on the first match.
    820    if (std::equal(beginKey, beginKey + UnicodeKeyLength, key)) {
    821      break;
    822    }
    823  }
    824 
    825  // Skip over the key.
    826  const CharT* beginType = beginKey + UnicodeKeyLength;
    827 
    828  // Find the start of the next keyword.
    829  const CharT* endType = iter.next();
    830 
    831  // No further keyword present, the current keyword ends the Unicode extension.
    832  if (!endType) {
    833    endType = end;
    834  }
    835 
    836  // If the keyword has a type, skip over the separator preceding the type.
    837  if (beginType != endType) {
    838    MOZ_ASSERT(beginType[0] == '-');
    839    beginType++;
    840  }
    841  return mozilla::Some(IndexAndLength{size_t(beginType - extension),
    842                                      size_t(endType - beginType)});
    843 }
    844 
    845 static inline auto FindUnicodeExtensionType(
    846    const JSLinearString* unicodeExtension, UnicodeKey key) {
    847  JS::AutoCheckCannotGC nogc;
    848  return unicodeExtension->hasLatin1Chars()
    849             ? FindUnicodeExtensionType(
    850                   reinterpret_cast<const char*>(
    851                       unicodeExtension->latin1Chars(nogc)),
    852                   unicodeExtension->length(), key)
    853             : FindUnicodeExtensionType(unicodeExtension->twoByteChars(nogc),
    854                                        unicodeExtension->length(), key);
    855 }
    856 
    857 // Return the sequence of types for the Unicode extension keyword specified by
    858 // key or undefined when the keyword isn't present.
    859 static bool GetUnicodeExtension(JSContext* cx, LocaleObject* locale,
    860                                UnicodeKey key, MutableHandleValue value) {
    861  // Return undefined when no Unicode extension subtag is present.
    862  const Value& unicodeExtensionValue = locale->unicodeExtension();
    863  if (unicodeExtensionValue.isUndefined()) {
    864    value.setUndefined();
    865    return true;
    866  }
    867 
    868  JSLinearString* unicodeExtension =
    869      unicodeExtensionValue.toString()->ensureLinear(cx);
    870  if (!unicodeExtension) {
    871    return false;
    872  }
    873 
    874  // Find the type of the requested key in the Unicode extension subtag.
    875  auto result = FindUnicodeExtensionType(unicodeExtension, key);
    876 
    877  // Return undefined if the requested key isn't present in the extension.
    878  if (!result) {
    879    value.setUndefined();
    880    return true;
    881  }
    882 
    883  size_t index = result->index;
    884  size_t length = result->length;
    885 
    886  // Otherwise return the type value of the found keyword.
    887  JSString* str = NewDependentString(cx, unicodeExtension, index, length);
    888  if (!str) {
    889    return false;
    890  }
    891  value.setString(str);
    892  return true;
    893 }
    894 
    895 struct BaseNamePartsResult {
    896  IndexAndLength language;
    897  mozilla::Maybe<IndexAndLength> script;
    898  mozilla::Maybe<IndexAndLength> region;
    899 };
    900 
    901 // Returns [language-length, script-index, region-index, region-length].
    902 template <typename CharT>
    903 static BaseNamePartsResult BaseNameParts(const CharT* baseName, size_t length) {
    904  size_t languageLength;
    905  size_t scriptIndex = 0;
    906  size_t regionIndex = 0;
    907  size_t regionLength = 0;
    908 
    909  // Search the first separator to find the end of the language subtag.
    910  if (const CharT* sep = std::char_traits<CharT>::find(baseName, length, '-')) {
    911    languageLength = sep - baseName;
    912 
    913    // Add +1 to skip over the separator character.
    914    size_t nextSubtag = languageLength + 1;
    915 
    916    // Script subtags are always four characters long, but take care for a four
    917    // character long variant subtag. These start with a digit.
    918    if ((nextSubtag + ScriptLength == length ||
    919         (nextSubtag + ScriptLength < length &&
    920          baseName[nextSubtag + ScriptLength] == '-')) &&
    921        mozilla::IsAsciiAlpha(baseName[nextSubtag])) {
    922      scriptIndex = nextSubtag;
    923      nextSubtag = scriptIndex + ScriptLength + 1;
    924    }
    925 
    926    // Region subtags can be either two or three characters long.
    927    if (nextSubtag < length) {
    928      for (size_t rlen : {AlphaRegionLength, DigitRegionLength}) {
    929        MOZ_ASSERT(nextSubtag + rlen <= length);
    930        if (nextSubtag + rlen == length || baseName[nextSubtag + rlen] == '-') {
    931          regionIndex = nextSubtag;
    932          regionLength = rlen;
    933          break;
    934        }
    935      }
    936    }
    937  } else {
    938    // No separator found, the base-name consists of just a language subtag.
    939    languageLength = length;
    940  }
    941 
    942  // Tell the analysis the |IsStructurallyValid*Tag| functions can't GC.
    943  JS::AutoSuppressGCAnalysis nogc;
    944 
    945  IndexAndLength language{0, languageLength};
    946  MOZ_ASSERT(
    947      mozilla::intl::IsStructurallyValidLanguageTag(language.spanOf(baseName)));
    948 
    949  mozilla::Maybe<IndexAndLength> script{};
    950  if (scriptIndex) {
    951    script.emplace(scriptIndex, ScriptLength);
    952    MOZ_ASSERT(
    953        mozilla::intl::IsStructurallyValidScriptTag(script->spanOf(baseName)));
    954  }
    955 
    956  mozilla::Maybe<IndexAndLength> region{};
    957  if (regionIndex) {
    958    region.emplace(regionIndex, regionLength);
    959    MOZ_ASSERT(
    960        mozilla::intl::IsStructurallyValidRegionTag(region->spanOf(baseName)));
    961  }
    962 
    963  return {language, script, region};
    964 }
    965 
    966 static inline auto BaseNameParts(const JSLinearString* baseName) {
    967  JS::AutoCheckCannotGC nogc;
    968  return baseName->hasLatin1Chars()
    969             ? BaseNameParts(
    970                   reinterpret_cast<const char*>(baseName->latin1Chars(nogc)),
    971                   baseName->length())
    972             : BaseNameParts(baseName->twoByteChars(nogc), baseName->length());
    973 }
    974 
    975 // Intl.Locale.prototype.maximize ()
    976 static bool Locale_maximize(JSContext* cx, const CallArgs& args) {
    977  MOZ_ASSERT(IsLocale(args.thisv()));
    978 
    979  // Step 3.
    980  auto* locale = &args.thisv().toObject().as<LocaleObject>();
    981  Rooted<JSLinearString*> tagStr(cx, locale->languageTag()->ensureLinear(cx));
    982  if (!tagStr) {
    983    return false;
    984  }
    985 
    986  mozilla::intl::Locale tag;
    987  if (!intl::ParseLocale(cx, tagStr, tag)) {
    988    return false;
    989  }
    990 
    991  if (auto result = tag.AddLikelySubtags(); result.isErr()) {
    992    intl::ReportInternalError(cx, result.unwrapErr());
    993    return false;
    994  }
    995 
    996  // Step 4.
    997  auto* result = CreateLocaleObject(cx, nullptr, tag);
    998  if (!result) {
    999    return false;
   1000  }
   1001  args.rval().setObject(*result);
   1002  return true;
   1003 }
   1004 
   1005 // Intl.Locale.prototype.maximize ()
   1006 static bool Locale_maximize(JSContext* cx, unsigned argc, Value* vp) {
   1007  // Steps 1-2.
   1008  CallArgs args = CallArgsFromVp(argc, vp);
   1009  return CallNonGenericMethod<IsLocale, Locale_maximize>(cx, args);
   1010 }
   1011 
   1012 // Intl.Locale.prototype.minimize ()
   1013 static bool Locale_minimize(JSContext* cx, const CallArgs& args) {
   1014  MOZ_ASSERT(IsLocale(args.thisv()));
   1015 
   1016  // Step 3.
   1017  auto* locale = &args.thisv().toObject().as<LocaleObject>();
   1018  Rooted<JSLinearString*> tagStr(cx, locale->languageTag()->ensureLinear(cx));
   1019  if (!tagStr) {
   1020    return false;
   1021  }
   1022 
   1023  mozilla::intl::Locale tag;
   1024  if (!intl::ParseLocale(cx, tagStr, tag)) {
   1025    return false;
   1026  }
   1027 
   1028  if (auto result = tag.RemoveLikelySubtags(); result.isErr()) {
   1029    intl::ReportInternalError(cx, result.unwrapErr());
   1030    return false;
   1031  }
   1032 
   1033  // Step 4.
   1034  auto* result = CreateLocaleObject(cx, nullptr, tag);
   1035  if (!result) {
   1036    return false;
   1037  }
   1038  args.rval().setObject(*result);
   1039  return true;
   1040 }
   1041 
   1042 // Intl.Locale.prototype.minimize ()
   1043 static bool Locale_minimize(JSContext* cx, unsigned argc, Value* vp) {
   1044  // Steps 1-2.
   1045  CallArgs args = CallArgsFromVp(argc, vp);
   1046  return CallNonGenericMethod<IsLocale, Locale_minimize>(cx, args);
   1047 }
   1048 
   1049 // Intl.Locale.prototype.toString ()
   1050 static bool Locale_toString(JSContext* cx, const CallArgs& args) {
   1051  MOZ_ASSERT(IsLocale(args.thisv()));
   1052 
   1053  // Step 3.
   1054  auto* locale = &args.thisv().toObject().as<LocaleObject>();
   1055  args.rval().setString(locale->languageTag());
   1056  return true;
   1057 }
   1058 
   1059 // Intl.Locale.prototype.toString ()
   1060 static bool Locale_toString(JSContext* cx, unsigned argc, Value* vp) {
   1061  // Steps 1-2.
   1062  CallArgs args = CallArgsFromVp(argc, vp);
   1063  return CallNonGenericMethod<IsLocale, Locale_toString>(cx, args);
   1064 }
   1065 
   1066 // get Intl.Locale.prototype.baseName
   1067 static bool Locale_baseName(JSContext* cx, const CallArgs& args) {
   1068  MOZ_ASSERT(IsLocale(args.thisv()));
   1069 
   1070  // Steps 3-4.
   1071  auto* locale = &args.thisv().toObject().as<LocaleObject>();
   1072  args.rval().setString(locale->baseName());
   1073  return true;
   1074 }
   1075 
   1076 // get Intl.Locale.prototype.baseName
   1077 static bool Locale_baseName(JSContext* cx, unsigned argc, Value* vp) {
   1078  // Steps 1-2.
   1079  CallArgs args = CallArgsFromVp(argc, vp);
   1080  return CallNonGenericMethod<IsLocale, Locale_baseName>(cx, args);
   1081 }
   1082 
   1083 // get Intl.Locale.prototype.calendar
   1084 static bool Locale_calendar(JSContext* cx, const CallArgs& args) {
   1085  MOZ_ASSERT(IsLocale(args.thisv()));
   1086 
   1087  // Step 3.
   1088  auto* locale = &args.thisv().toObject().as<LocaleObject>();
   1089  return GetUnicodeExtension(cx, locale, "ca", args.rval());
   1090 }
   1091 
   1092 // get Intl.Locale.prototype.calendar
   1093 static bool Locale_calendar(JSContext* cx, unsigned argc, Value* vp) {
   1094  // Steps 1-2.
   1095  CallArgs args = CallArgsFromVp(argc, vp);
   1096  return CallNonGenericMethod<IsLocale, Locale_calendar>(cx, args);
   1097 }
   1098 
   1099 // get Intl.Locale.prototype.caseFirst
   1100 static bool Locale_caseFirst(JSContext* cx, const CallArgs& args) {
   1101  MOZ_ASSERT(IsLocale(args.thisv()));
   1102 
   1103  // Step 3.
   1104  auto* locale = &args.thisv().toObject().as<LocaleObject>();
   1105  return GetUnicodeExtension(cx, locale, "kf", args.rval());
   1106 }
   1107 
   1108 // get Intl.Locale.prototype.caseFirst
   1109 static bool Locale_caseFirst(JSContext* cx, unsigned argc, Value* vp) {
   1110  // Steps 1-2.
   1111  CallArgs args = CallArgsFromVp(argc, vp);
   1112  return CallNonGenericMethod<IsLocale, Locale_caseFirst>(cx, args);
   1113 }
   1114 
   1115 // get Intl.Locale.prototype.collation
   1116 static bool Locale_collation(JSContext* cx, const CallArgs& args) {
   1117  MOZ_ASSERT(IsLocale(args.thisv()));
   1118 
   1119  // Step 3.
   1120  auto* locale = &args.thisv().toObject().as<LocaleObject>();
   1121  return GetUnicodeExtension(cx, locale, "co", args.rval());
   1122 }
   1123 
   1124 // get Intl.Locale.prototype.collation
   1125 static bool Locale_collation(JSContext* cx, unsigned argc, Value* vp) {
   1126  // Steps 1-2.
   1127  CallArgs args = CallArgsFromVp(argc, vp);
   1128  return CallNonGenericMethod<IsLocale, Locale_collation>(cx, args);
   1129 }
   1130 
   1131 // get Intl.Locale.prototype.hourCycle
   1132 static bool Locale_hourCycle(JSContext* cx, const CallArgs& args) {
   1133  MOZ_ASSERT(IsLocale(args.thisv()));
   1134 
   1135  // Step 3.
   1136  auto* locale = &args.thisv().toObject().as<LocaleObject>();
   1137  return GetUnicodeExtension(cx, locale, "hc", args.rval());
   1138 }
   1139 
   1140 // get Intl.Locale.prototype.hourCycle
   1141 static bool Locale_hourCycle(JSContext* cx, unsigned argc, Value* vp) {
   1142  // Steps 1-2.
   1143  CallArgs args = CallArgsFromVp(argc, vp);
   1144  return CallNonGenericMethod<IsLocale, Locale_hourCycle>(cx, args);
   1145 }
   1146 
   1147 // get Intl.Locale.prototype.numeric
   1148 static bool Locale_numeric(JSContext* cx, const CallArgs& args) {
   1149  MOZ_ASSERT(IsLocale(args.thisv()));
   1150 
   1151  // Step 3.
   1152  auto* locale = &args.thisv().toObject().as<LocaleObject>();
   1153  RootedValue value(cx);
   1154  if (!GetUnicodeExtension(cx, locale, "kn", &value)) {
   1155    return false;
   1156  }
   1157 
   1158  // Compare against the empty string per Intl.Locale, step 36.a. The Unicode
   1159  // extension is already canonicalized, so we don't need to compare against
   1160  // "true" at this point.
   1161  MOZ_ASSERT(value.isUndefined() || value.isString());
   1162  MOZ_ASSERT_IF(value.isString(),
   1163                !StringEqualsLiteral(&value.toString()->asLinear(), "true"));
   1164 
   1165  args.rval().setBoolean(value.isString() && value.toString()->empty());
   1166  return true;
   1167 }
   1168 
   1169 // get Intl.Locale.prototype.numeric
   1170 static bool Locale_numeric(JSContext* cx, unsigned argc, Value* vp) {
   1171  // Steps 1-2.
   1172  CallArgs args = CallArgsFromVp(argc, vp);
   1173  return CallNonGenericMethod<IsLocale, Locale_numeric>(cx, args);
   1174 }
   1175 
   1176 // get Intl.Locale.prototype.numberingSystem
   1177 static bool Intl_Locale_numberingSystem(JSContext* cx, const CallArgs& args) {
   1178  MOZ_ASSERT(IsLocale(args.thisv()));
   1179 
   1180  // Step 3.
   1181  auto* locale = &args.thisv().toObject().as<LocaleObject>();
   1182  return GetUnicodeExtension(cx, locale, "nu", args.rval());
   1183 }
   1184 
   1185 // get Intl.Locale.prototype.numberingSystem
   1186 static bool Locale_numberingSystem(JSContext* cx, unsigned argc, Value* vp) {
   1187  // Steps 1-2.
   1188  CallArgs args = CallArgsFromVp(argc, vp);
   1189  return CallNonGenericMethod<IsLocale, Intl_Locale_numberingSystem>(cx, args);
   1190 }
   1191 
   1192 // get Intl.Locale.prototype.language
   1193 static bool Locale_language(JSContext* cx, const CallArgs& args) {
   1194  MOZ_ASSERT(IsLocale(args.thisv()));
   1195 
   1196  // Step 3.
   1197  auto* locale = &args.thisv().toObject().as<LocaleObject>();
   1198  JSLinearString* baseName = locale->baseName()->ensureLinear(cx);
   1199  if (!baseName) {
   1200    return false;
   1201  }
   1202 
   1203  // Step 4 (Unnecessary assertion).
   1204 
   1205  auto language = BaseNameParts(baseName).language;
   1206 
   1207  size_t index = language.index;
   1208  size_t length = language.length;
   1209 
   1210  // Step 5.
   1211  JSString* str = NewDependentString(cx, baseName, index, length);
   1212  if (!str) {
   1213    return false;
   1214  }
   1215 
   1216  args.rval().setString(str);
   1217  return true;
   1218 }
   1219 
   1220 // get Intl.Locale.prototype.language
   1221 static bool Locale_language(JSContext* cx, unsigned argc, Value* vp) {
   1222  // Steps 1-2.
   1223  CallArgs args = CallArgsFromVp(argc, vp);
   1224  return CallNonGenericMethod<IsLocale, Locale_language>(cx, args);
   1225 }
   1226 
   1227 // get Intl.Locale.prototype.script
   1228 static bool Locale_script(JSContext* cx, const CallArgs& args) {
   1229  MOZ_ASSERT(IsLocale(args.thisv()));
   1230 
   1231  // Step 3.
   1232  auto* locale = &args.thisv().toObject().as<LocaleObject>();
   1233  JSLinearString* baseName = locale->baseName()->ensureLinear(cx);
   1234  if (!baseName) {
   1235    return false;
   1236  }
   1237 
   1238  // Step 4 (Unnecessary assertion).
   1239 
   1240  auto script = BaseNameParts(baseName).script;
   1241 
   1242  // Step 5.
   1243  if (!script) {
   1244    args.rval().setUndefined();
   1245    return true;
   1246  }
   1247 
   1248  size_t index = script->index;
   1249  size_t length = script->length;
   1250 
   1251  // Step 6.
   1252  JSString* str = NewDependentString(cx, baseName, index, length);
   1253  if (!str) {
   1254    return false;
   1255  }
   1256 
   1257  args.rval().setString(str);
   1258  return true;
   1259 }
   1260 
   1261 // get Intl.Locale.prototype.script
   1262 static bool Locale_script(JSContext* cx, unsigned argc, Value* vp) {
   1263  // Steps 1-2.
   1264  CallArgs args = CallArgsFromVp(argc, vp);
   1265  return CallNonGenericMethod<IsLocale, Locale_script>(cx, args);
   1266 }
   1267 
   1268 // get Intl.Locale.prototype.region
   1269 static bool Locale_region(JSContext* cx, const CallArgs& args) {
   1270  MOZ_ASSERT(IsLocale(args.thisv()));
   1271 
   1272  // Step 3.
   1273  auto* locale = &args.thisv().toObject().as<LocaleObject>();
   1274  JSLinearString* baseName = locale->baseName()->ensureLinear(cx);
   1275  if (!baseName) {
   1276    return false;
   1277  }
   1278 
   1279  // Step 4 (Unnecessary assertion).
   1280 
   1281  auto region = BaseNameParts(baseName).region;
   1282 
   1283  // Step 5.
   1284  if (!region) {
   1285    args.rval().setUndefined();
   1286    return true;
   1287  }
   1288 
   1289  size_t index = region->index;
   1290  size_t length = region->length;
   1291 
   1292  // Step 6.
   1293  JSString* str = NewDependentString(cx, baseName, index, length);
   1294  if (!str) {
   1295    return false;
   1296  }
   1297 
   1298  args.rval().setString(str);
   1299  return true;
   1300 }
   1301 
   1302 // get Intl.Locale.prototype.region
   1303 static bool Locale_region(JSContext* cx, unsigned argc, Value* vp) {
   1304  // Steps 1-2.
   1305  CallArgs args = CallArgsFromVp(argc, vp);
   1306  return CallNonGenericMethod<IsLocale, Locale_region>(cx, args);
   1307 }
   1308 
   1309 // get Intl.Locale.prototype.variants
   1310 static bool Locale_variants(JSContext* cx, const CallArgs& args) {
   1311  MOZ_ASSERT(IsLocale(args.thisv()));
   1312 
   1313  // Step 3.
   1314  auto* locale = &args.thisv().toObject().as<LocaleObject>();
   1315  JSLinearString* baseName = locale->baseName()->ensureLinear(cx);
   1316  if (!baseName) {
   1317    return false;
   1318  }
   1319 
   1320  auto parts = BaseNameParts(baseName);
   1321 
   1322  // Variants are the trailing subtags in the base-name. Find which subtag
   1323  // precedes the variants.
   1324  auto precedingSubtag = parts.region   ? *parts.region
   1325                         : parts.script ? *parts.script
   1326                                        : parts.language;
   1327 
   1328  // Index of the next subtag, including the leading '-' character.
   1329  size_t index = precedingSubtag.index + precedingSubtag.length;
   1330 
   1331  // Length of the variant subtags, including the leading '-' character.
   1332  size_t length = baseName->length() - index;
   1333 
   1334  // No variant subtags present when |length| is zero.
   1335  if (length == 0) {
   1336    args.rval().setUndefined();
   1337    return true;
   1338  }
   1339  MOZ_ASSERT(baseName->latin1OrTwoByteChar(index) == '-',
   1340             "missing '-' separator after precedingSubtag");
   1341  MOZ_ASSERT(length >= 4 + 1,
   1342             "variant subtag is at least four characters long");
   1343 
   1344  JSString* str = NewDependentString(cx, baseName, index + 1, length - 1);
   1345  if (!str) {
   1346    return false;
   1347  }
   1348 
   1349  args.rval().setString(str);
   1350  return true;
   1351 }
   1352 
   1353 // get Intl.Locale.prototype.variants
   1354 static bool Locale_variants(JSContext* cx, unsigned argc, Value* vp) {
   1355  // Steps 1-2.
   1356  CallArgs args = CallArgsFromVp(argc, vp);
   1357  return CallNonGenericMethod<IsLocale, Locale_variants>(cx, args);
   1358 }
   1359 
   1360 static bool Locale_toSource(JSContext* cx, unsigned argc, Value* vp) {
   1361  CallArgs args = CallArgsFromVp(argc, vp);
   1362  args.rval().setString(cx->names().Locale);
   1363  return true;
   1364 }
   1365 
   1366 static const JSFunctionSpec locale_methods[] = {
   1367    JS_FN("maximize", Locale_maximize, 0, 0),
   1368    JS_FN("minimize", Locale_minimize, 0, 0),
   1369    JS_FN("toString", Locale_toString, 0, 0),
   1370    JS_FN("toSource", Locale_toSource, 0, 0),
   1371    JS_FS_END,
   1372 };
   1373 
   1374 static const JSPropertySpec locale_properties[] = {
   1375    JS_PSG("baseName", Locale_baseName, 0),
   1376    JS_PSG("calendar", Locale_calendar, 0),
   1377    JS_PSG("caseFirst", Locale_caseFirst, 0),
   1378    JS_PSG("collation", Locale_collation, 0),
   1379    JS_PSG("hourCycle", Locale_hourCycle, 0),
   1380    JS_PSG("numeric", Locale_numeric, 0),
   1381    JS_PSG("numberingSystem", Locale_numberingSystem, 0),
   1382    JS_PSG("language", Locale_language, 0),
   1383    JS_PSG("script", Locale_script, 0),
   1384    JS_PSG("region", Locale_region, 0),
   1385    JS_PSG("variants", Locale_variants, 0),
   1386    JS_STRING_SYM_PS(toStringTag, "Intl.Locale", JSPROP_READONLY),
   1387    JS_PS_END,
   1388 };
   1389 
   1390 const ClassSpec LocaleObject::classSpec_ = {
   1391    GenericCreateConstructor<Locale, 1, gc::AllocKind::FUNCTION>,
   1392    GenericCreatePrototype<LocaleObject>,
   1393    nullptr,
   1394    nullptr,
   1395    locale_methods,
   1396    locale_properties,
   1397    nullptr,
   1398    ClassSpec::DontDefineConstructor,
   1399 };
   1400 
   1401 static JSLinearString* ValidateAndCanonicalizeLanguageTag(
   1402    JSContext* cx, Handle<JSLinearString*> string) {
   1403  // Handle the common case (a standalone language) first.
   1404  // Only the following Unicode BCP 47 locale identifier subset is accepted:
   1405  //   unicode_locale_id = unicode_language_id
   1406  //   unicode_language_id = unicode_language_subtag
   1407  //   unicode_language_subtag = alpha{2,3}
   1408  JSLinearString* language;
   1409  JS_TRY_VAR_OR_RETURN_NULL(cx, language,
   1410                            intl::ParseStandaloneISO639LanguageTag(cx, string));
   1411  if (language) {
   1412    return language;
   1413  }
   1414 
   1415  mozilla::intl::Locale tag;
   1416  if (!intl::ParseLocale(cx, string, tag)) {
   1417    return nullptr;
   1418  }
   1419 
   1420  auto result = tag.Canonicalize();
   1421  if (result.isErr()) {
   1422    if (result.unwrapErr() ==
   1423        mozilla::intl::Locale::CanonicalizationError::DuplicateVariant) {
   1424      JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
   1425                                JSMSG_DUPLICATE_VARIANT_SUBTAG);
   1426    } else {
   1427      intl::ReportInternalError(cx);
   1428    }
   1429    return nullptr;
   1430  }
   1431 
   1432  intl::FormatBuffer<char, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
   1433  if (auto result = tag.ToString(buffer); result.isErr()) {
   1434    intl::ReportInternalError(cx, result.unwrapErr());
   1435    return nullptr;
   1436  }
   1437 
   1438  return buffer.toAsciiString(cx);
   1439 }
   1440 
   1441 static JSLinearString* ValidateAndCanonicalizeLanguageTag(
   1442    JSContext* cx, Handle<Value> tagValue) {
   1443  if (tagValue.isObject()) {
   1444    JSString* tagStr;
   1445    JS_TRY_VAR_OR_RETURN_NULL(
   1446        cx, tagStr,
   1447        LanguageTagFromMaybeWrappedLocale(cx, &tagValue.toObject()));
   1448    if (tagStr) {
   1449      return tagStr->ensureLinear(cx);
   1450    }
   1451  }
   1452 
   1453  JSString* tagStr = ToString(cx, tagValue);
   1454  if (!tagStr) {
   1455    return nullptr;
   1456  }
   1457 
   1458  Rooted<JSLinearString*> tagLinearStr(cx, tagStr->ensureLinear(cx));
   1459  if (!tagLinearStr) {
   1460    return nullptr;
   1461  }
   1462  return ValidateAndCanonicalizeLanguageTag(cx, tagLinearStr);
   1463 }
   1464 
   1465 bool js::intl_ValidateAndCanonicalizeLanguageTag(JSContext* cx, unsigned argc,
   1466                                                 Value* vp) {
   1467  CallArgs args = CallArgsFromVp(argc, vp);
   1468  MOZ_ASSERT(args.length() == 2);
   1469 
   1470  HandleValue tagValue = args[0];
   1471  bool applyToString = args[1].toBoolean();
   1472 
   1473  if (tagValue.isObject()) {
   1474    JSString* tagStr;
   1475    JS_TRY_VAR_OR_RETURN_FALSE(
   1476        cx, tagStr,
   1477        LanguageTagFromMaybeWrappedLocale(cx, &tagValue.toObject()));
   1478    if (tagStr) {
   1479      args.rval().setString(tagStr);
   1480      return true;
   1481    }
   1482  }
   1483 
   1484  if (!applyToString && !tagValue.isString()) {
   1485    args.rval().setNull();
   1486    return true;
   1487  }
   1488 
   1489  auto* resultStr = ValidateAndCanonicalizeLanguageTag(cx, tagValue);
   1490  if (!resultStr) {
   1491    return false;
   1492  }
   1493  args.rval().setString(resultStr);
   1494  return true;
   1495 }
   1496 
   1497 bool js::intl_TryValidateAndCanonicalizeLanguageTag(JSContext* cx,
   1498                                                    unsigned argc, Value* vp) {
   1499  CallArgs args = CallArgsFromVp(argc, vp);
   1500  MOZ_ASSERT(args.length() == 1);
   1501 
   1502  Rooted<JSLinearString*> linear(cx, args[0].toString()->ensureLinear(cx));
   1503  if (!linear) {
   1504    return false;
   1505  }
   1506 
   1507  mozilla::intl::Locale tag;
   1508  {
   1509    if (!StringIsAscii(linear)) {
   1510      // The caller handles invalid inputs.
   1511      args.rval().setNull();
   1512      return true;
   1513    }
   1514 
   1515    intl::StringAsciiChars chars(linear);
   1516    if (!chars.init(cx)) {
   1517      return false;
   1518    }
   1519 
   1520    if (mozilla::intl::LocaleParser::TryParse(chars, tag).isErr()) {
   1521      // The caller handles invalid inputs.
   1522      args.rval().setNull();
   1523      return true;
   1524    }
   1525  }
   1526 
   1527  auto result = tag.Canonicalize();
   1528  if (result.isErr()) {
   1529    if (result.unwrapErr() ==
   1530        mozilla::intl::Locale::CanonicalizationError::DuplicateVariant) {
   1531      JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
   1532                                JSMSG_DUPLICATE_VARIANT_SUBTAG);
   1533    } else {
   1534      intl::ReportInternalError(cx);
   1535    }
   1536    return false;
   1537  }
   1538 
   1539  intl::FormatBuffer<char, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
   1540  if (auto result = tag.ToString(buffer); result.isErr()) {
   1541    intl::ReportInternalError(cx, result.unwrapErr());
   1542    return false;
   1543  }
   1544 
   1545  JSString* resultStr = buffer.toAsciiString(cx);
   1546  if (!resultStr) {
   1547    return false;
   1548  }
   1549  args.rval().setString(resultStr);
   1550  return true;
   1551 }
   1552 
   1553 bool js::intl_ValidateAndCanonicalizeUnicodeExtensionType(JSContext* cx,
   1554                                                          unsigned argc,
   1555                                                          Value* vp) {
   1556  CallArgs args = CallArgsFromVp(argc, vp);
   1557  MOZ_ASSERT(args.length() == 3);
   1558 
   1559  HandleValue typeArg = args[0];
   1560  MOZ_ASSERT(typeArg.isString(), "type must be a string");
   1561 
   1562  HandleValue optionArg = args[1];
   1563  MOZ_ASSERT(optionArg.isString(), "option name must be a string");
   1564 
   1565  HandleValue keyArg = args[2];
   1566  MOZ_ASSERT(keyArg.isString(), "key must be a string");
   1567 
   1568  Rooted<JSLinearString*> unicodeType(cx, typeArg.toString()->ensureLinear(cx));
   1569  if (!unicodeType) {
   1570    return false;
   1571  }
   1572 
   1573  bool isValid;
   1574  if (!IsValidUnicodeExtensionValue(cx, unicodeType, &isValid)) {
   1575    return false;
   1576  }
   1577  if (!isValid) {
   1578    UniqueChars optionChars = EncodeAscii(cx, optionArg.toString());
   1579    if (!optionChars) {
   1580      return false;
   1581    }
   1582 
   1583    UniqueChars unicodeTypeChars = QuoteString(cx, unicodeType, '"');
   1584    if (!unicodeTypeChars) {
   1585      return false;
   1586    }
   1587 
   1588    JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
   1589                              JSMSG_INVALID_OPTION_VALUE, optionChars.get(),
   1590                              unicodeTypeChars.get());
   1591    return false;
   1592  }
   1593 
   1594  char unicodeKey[UnicodeKeyLength];
   1595  {
   1596    JSLinearString* str = keyArg.toString()->ensureLinear(cx);
   1597    if (!str) {
   1598      return false;
   1599    }
   1600    MOZ_ASSERT(str->length() == UnicodeKeyLength);
   1601 
   1602    for (size_t i = 0; i < UnicodeKeyLength; i++) {
   1603      char16_t ch = str->latin1OrTwoByteChar(i);
   1604      MOZ_ASSERT(mozilla::IsAscii(ch));
   1605      unicodeKey[i] = char(ch);
   1606    }
   1607  }
   1608 
   1609  UniqueChars unicodeTypeChars = EncodeAscii(cx, unicodeType);
   1610  if (!unicodeTypeChars) {
   1611    return false;
   1612  }
   1613 
   1614  size_t unicodeTypeLength = unicodeType->length();
   1615  MOZ_ASSERT(strlen(unicodeTypeChars.get()) == unicodeTypeLength);
   1616 
   1617  // Convert into canonical case before searching for replacements.
   1618  mozilla::intl::AsciiToLowerCase(unicodeTypeChars.get(), unicodeTypeLength,
   1619                                  unicodeTypeChars.get());
   1620 
   1621  auto key = mozilla::Span(unicodeKey, UnicodeKeyLength);
   1622  auto type = mozilla::Span(unicodeTypeChars.get(), unicodeTypeLength);
   1623 
   1624  // Search if there's a replacement for the current Unicode keyword.
   1625  JSString* result;
   1626  if (const char* replacement =
   1627          mozilla::intl::Locale::ReplaceUnicodeExtensionType(key, type)) {
   1628    result = NewStringCopyZ<CanGC>(cx, replacement);
   1629  } else {
   1630    result = StringToLowerCase(cx, unicodeType);
   1631  }
   1632  if (!result) {
   1633    return false;
   1634  }
   1635 
   1636  args.rval().setString(result);
   1637  return true;
   1638 }
   1639 
   1640 /**
   1641 * Canonicalizes a locale list.
   1642 *
   1643 * Spec: ECMAScript Internationalization API Specification, 9.2.1.
   1644 */
   1645 bool js::intl::CanonicalizeLocaleList(JSContext* cx, Handle<Value> locales,
   1646                                      MutableHandle<LocalesList> result) {
   1647  MOZ_ASSERT(result.empty());
   1648 
   1649  // Step 1.
   1650  if (locales.isUndefined()) {
   1651    return true;
   1652  }
   1653 
   1654  // Step 3 (and the remaining steps).
   1655  if (locales.isString()) {
   1656    Rooted<JSLinearString*> linear(cx, locales.toString()->ensureLinear(cx));
   1657    if (!linear) {
   1658      return false;
   1659    }
   1660 
   1661    auto* languageTag = ValidateAndCanonicalizeLanguageTag(cx, linear);
   1662    if (!languageTag) {
   1663      return false;
   1664    }
   1665    return result.append(languageTag);
   1666  }
   1667 
   1668  if (locales.isObject()) {
   1669    JSString* languageTag;
   1670    JS_TRY_VAR_OR_RETURN_FALSE(
   1671        cx, languageTag,
   1672        LanguageTagFromMaybeWrappedLocale(cx, &locales.toObject()));
   1673    if (languageTag) {
   1674      auto* linear = languageTag->ensureLinear(cx);
   1675      if (!linear) {
   1676        return false;
   1677      }
   1678      return result.append(linear);
   1679    }
   1680  }
   1681 
   1682  // Step 2. (Implicit)
   1683 
   1684  // Step 4.
   1685  Rooted<JSObject*> obj(cx, ToObject(cx, locales));
   1686  if (!obj) {
   1687    return false;
   1688  }
   1689 
   1690  // Step 5.
   1691  uint64_t length;
   1692  if (!GetLengthProperty(cx, obj, &length)) {
   1693    return false;
   1694  }
   1695 
   1696  // Steps 6-7.
   1697  Rooted<Value> value(cx);
   1698  for (uint64_t k = 0; k < length; k++) {
   1699    // Step 7.a-c.
   1700    bool hole;
   1701    if (!CheckForInterrupt(cx) ||
   1702        !HasAndGetElement(cx, obj, k, &hole, &value)) {
   1703      return false;
   1704    }
   1705 
   1706    if (!hole) {
   1707      // Step 7.c.ii.
   1708      if (!value.isString() && !value.isObject()) {
   1709        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
   1710                                  JSMSG_INVALID_LOCALES_ELEMENT);
   1711        return false;
   1712      }
   1713 
   1714      // Step 7.c.iii-iv.
   1715      JSLinearString* tag = ValidateAndCanonicalizeLanguageTag(cx, value);
   1716      if (!tag) {
   1717        return false;
   1718      }
   1719 
   1720      // Step 7.c.v.
   1721      bool addToResult =
   1722          std::none_of(result.begin(), result.end(),
   1723                       [tag](auto* other) { return EqualStrings(tag, other); });
   1724      if (addToResult && !result.append(tag)) {
   1725        return false;
   1726      }
   1727    }
   1728  }
   1729 
   1730  // Step 8.
   1731  return true;
   1732 }