tor-browser

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

IntlObject.cpp (17316B)


      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 /* Implementation of the Intl object and its non-constructor properties. */
      8 
      9 #include "builtin/intl/IntlObject.h"
     10 
     11 #include "mozilla/Assertions.h"
     12 #include "mozilla/intl/Calendar.h"
     13 #include "mozilla/intl/Collator.h"
     14 #include "mozilla/intl/Currency.h"
     15 #include "mozilla/intl/MeasureUnitGenerated.h"
     16 #include "mozilla/intl/TimeZone.h"
     17 
     18 #include <algorithm>
     19 #include <array>
     20 #include <cstring>
     21 #include <iterator>
     22 #include <string_view>
     23 
     24 #include "builtin/Array.h"
     25 #include "builtin/intl/CommonFunctions.h"
     26 #include "builtin/intl/LocaleNegotiation.h"
     27 #include "builtin/intl/NumberingSystemsGenerated.h"
     28 #include "builtin/intl/SharedIntlData.h"
     29 #include "ds/Sort.h"
     30 #include "js/Class.h"
     31 #include "js/friend/ErrorMessages.h"  // js::GetErrorMessage, JSMSG_*
     32 #include "js/GCAPI.h"
     33 #include "js/GCVector.h"
     34 #include "js/PropertySpec.h"
     35 #include "vm/GlobalObject.h"
     36 #include "vm/JSAtomUtils.h"  // ClassName
     37 #include "vm/JSContext.h"
     38 #include "vm/PlainObject.h"  // js::PlainObject
     39 #include "vm/StringType.h"
     40 
     41 #include "vm/JSObject-inl.h"
     42 #include "vm/NativeObject-inl.h"
     43 
     44 using namespace js;
     45 using namespace js::intl;
     46 
     47 /******************** Intl ********************/
     48 
     49 bool js::intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp) {
     50  CallArgs args = CallArgsFromVp(argc, vp);
     51  MOZ_ASSERT(args.length() == 1);
     52 
     53  UniqueChars locale = intl::EncodeLocale(cx, args[0].toString());
     54  if (!locale) {
     55    return false;
     56  }
     57 
     58  auto result = mozilla::intl::Calendar::TryCreate(locale.get());
     59  if (result.isErr()) {
     60    intl::ReportInternalError(cx, result.unwrapErr());
     61    return false;
     62  }
     63  auto calendar = result.unwrap();
     64 
     65  RootedObject info(cx, NewPlainObject(cx));
     66  if (!info) {
     67    return false;
     68  }
     69 
     70  RootedValue v(cx);
     71 
     72  v.setInt32(static_cast<int32_t>(calendar->GetFirstDayOfWeek()));
     73  if (!DefineDataProperty(cx, info, cx->names().firstDayOfWeek, v)) {
     74    return false;
     75  }
     76 
     77  v.setInt32(calendar->GetMinimalDaysInFirstWeek());
     78  if (!DefineDataProperty(cx, info, cx->names().minDays, v)) {
     79    return false;
     80  }
     81 
     82  Rooted<ArrayObject*> weekendArray(cx, NewDenseEmptyArray(cx));
     83  if (!weekendArray) {
     84    return false;
     85  }
     86 
     87  auto weekend = calendar->GetWeekend();
     88  if (weekend.isErr()) {
     89    intl::ReportInternalError(cx, weekend.unwrapErr());
     90    return false;
     91  }
     92 
     93  for (auto day : weekend.unwrap()) {
     94    if (!NewbornArrayPush(cx, weekendArray,
     95                          Int32Value(static_cast<int32_t>(day)))) {
     96      return false;
     97    }
     98  }
     99 
    100  v.setObject(*weekendArray);
    101  if (!DefineDataProperty(cx, info, cx->names().weekend, v)) {
    102    return false;
    103  }
    104 
    105  args.rval().setObject(*info);
    106  return true;
    107 }
    108 
    109 // 9.2.2 BestAvailableLocale ( availableLocales, locale )
    110 //
    111 // Carries an additional third argument in our implementation to provide the
    112 // default locale. See the doc-comment in the header file.
    113 bool js::intl_BestAvailableLocale(JSContext* cx, unsigned argc, Value* vp) {
    114  CallArgs args = CallArgsFromVp(argc, vp);
    115  MOZ_ASSERT(args.length() == 3);
    116 
    117  using AvailableLocaleKind = js::intl::AvailableLocaleKind;
    118 
    119  AvailableLocaleKind kind;
    120  {
    121    JSLinearString* typeStr = args[0].toString()->ensureLinear(cx);
    122    if (!typeStr) {
    123      return false;
    124    }
    125 
    126    if (StringEqualsLiteral(typeStr, "Collator")) {
    127      kind = AvailableLocaleKind::Collator;
    128    } else if (StringEqualsLiteral(typeStr, "DateTimeFormat")) {
    129      kind = AvailableLocaleKind::DateTimeFormat;
    130    } else if (StringEqualsLiteral(typeStr, "DisplayNames")) {
    131      kind = AvailableLocaleKind::DisplayNames;
    132    } else if (StringEqualsLiteral(typeStr, "DurationFormat")) {
    133      kind = AvailableLocaleKind::DurationFormat;
    134    } else if (StringEqualsLiteral(typeStr, "ListFormat")) {
    135      kind = AvailableLocaleKind::ListFormat;
    136    } else if (StringEqualsLiteral(typeStr, "NumberFormat")) {
    137      kind = AvailableLocaleKind::NumberFormat;
    138    } else if (StringEqualsLiteral(typeStr, "PluralRules")) {
    139      kind = AvailableLocaleKind::PluralRules;
    140    } else if (StringEqualsLiteral(typeStr, "RelativeTimeFormat")) {
    141      kind = AvailableLocaleKind::RelativeTimeFormat;
    142    } else {
    143      MOZ_ASSERT(StringEqualsLiteral(typeStr, "Segmenter"));
    144      kind = AvailableLocaleKind::Segmenter;
    145    }
    146  }
    147 
    148  Rooted<JSLinearString*> locale(cx, args[1].toString()->ensureLinear(cx));
    149  if (!locale) {
    150    return false;
    151  }
    152 
    153  MOZ_ASSERT(args[2].isNull() || args[2].isString());
    154 
    155  Rooted<JSLinearString*> defaultLocale(cx);
    156  if (args[2].isString()) {
    157    defaultLocale = args[2].toString()->ensureLinear(cx);
    158    if (!defaultLocale) {
    159      return false;
    160    }
    161  }
    162 
    163  Rooted<JSLinearString*> result(cx);
    164  if (!intl::BestAvailableLocale(cx, kind, locale, defaultLocale, &result)) {
    165    return false;
    166  }
    167  if (result) {
    168    args.rval().setString(result);
    169  } else {
    170    args.rval().setUndefined();
    171  }
    172  return true;
    173 }
    174 
    175 using StringList = GCVector<JSLinearString*>;
    176 
    177 /**
    178 * Create a sorted array from a list of strings.
    179 */
    180 static ArrayObject* CreateArrayFromList(JSContext* cx,
    181                                        MutableHandle<StringList> list) {
    182  // Reserve scratch space for MergeSort().
    183  size_t initialLength = list.length();
    184  if (!list.growBy(initialLength)) {
    185    return nullptr;
    186  }
    187 
    188  // Sort all strings in alphabetical order.
    189  MOZ_ALWAYS_TRUE(
    190      MergeSort(list.begin(), initialLength, list.begin() + initialLength,
    191                [](const auto* a, const auto* b, bool* lessOrEqual) {
    192                  *lessOrEqual = CompareStrings(a, b) <= 0;
    193                  return true;
    194                }));
    195 
    196  // Ensure we don't add duplicate entries to the array.
    197  auto* end = std::unique(
    198      list.begin(), list.begin() + initialLength,
    199      [](const auto* a, const auto* b) { return EqualStrings(a, b); });
    200 
    201  // std::unique leaves the elements after |end| with an unspecified value, so
    202  // remove them first. And also delete the elements in the scratch space.
    203  list.shrinkBy(std::distance(end, list.end()));
    204 
    205  // And finally copy the strings into the result array.
    206  auto* array = NewDenseFullyAllocatedArray(cx, list.length());
    207  if (!array) {
    208    return nullptr;
    209  }
    210  array->setDenseInitializedLength(list.length());
    211 
    212  for (size_t i = 0; i < list.length(); ++i) {
    213    array->initDenseElement(i, StringValue(list[i]));
    214  }
    215 
    216  return array;
    217 }
    218 
    219 /**
    220 * Create an array from a sorted list of strings.
    221 */
    222 template <size_t N>
    223 static ArrayObject* CreateArrayFromSortedList(
    224    JSContext* cx, const std::array<const char*, N>& list) {
    225  // Ensure the list is sorted and doesn't contain duplicates.
    226  MOZ_ASSERT(std::adjacent_find(std::begin(list), std::end(list),
    227                                [](const auto& a, const auto& b) {
    228                                  return std::strcmp(a, b) >= 0;
    229                                }) == std::end(list));
    230 
    231  size_t length = std::size(list);
    232 
    233  Rooted<ArrayObject*> array(cx, NewDenseFullyAllocatedArray(cx, length));
    234  if (!array) {
    235    return nullptr;
    236  }
    237  array->ensureDenseInitializedLength(0, length);
    238 
    239  for (size_t i = 0; i < length; ++i) {
    240    auto* str = NewStringCopyZ<CanGC>(cx, list[i]);
    241    if (!str) {
    242      return nullptr;
    243    }
    244    array->initDenseElement(i, StringValue(str));
    245  }
    246  return array;
    247 }
    248 
    249 /**
    250 * Create an array from an intl::Enumeration.
    251 */
    252 template <const auto& unsupported>
    253 static bool EnumerationIntoList(JSContext* cx, auto values,
    254                                MutableHandle<StringList> list) {
    255  for (auto value : values) {
    256    if (value.isErr()) {
    257      intl::ReportInternalError(cx);
    258      return false;
    259    }
    260    auto span = value.unwrap();
    261 
    262    // Skip over known, unsupported values.
    263    std::string_view sv(span.data(), span.size());
    264    if (std::any_of(std::begin(unsupported), std::end(unsupported),
    265                    [sv](const auto& e) { return sv == e; })) {
    266      continue;
    267    }
    268 
    269    auto* string = NewStringCopy<CanGC>(cx, span);
    270    if (!string) {
    271      return false;
    272    }
    273    if (!list.append(string)) {
    274      return false;
    275    }
    276  }
    277 
    278  return true;
    279 }
    280 
    281 /**
    282 * Returns the list of calendar types which mustn't be returned by
    283 * |Intl.supportedValuesOf()|.
    284 */
    285 static constexpr auto UnsupportedCalendars() {
    286  return std::array{
    287      "islamic",
    288      "islamic-rgsa",
    289  };
    290 }
    291 
    292 /**
    293 * AvailableCalendars ( )
    294 */
    295 static ArrayObject* AvailableCalendars(JSContext* cx) {
    296  Rooted<StringList> list(cx, StringList(cx));
    297 
    298  {
    299    // Hazard analysis complains that the mozilla::Result destructor calls a
    300    // GC function, which is unsound when returning an unrooted value. Work
    301    // around this issue by restricting the lifetime of |keywords| to a
    302    // separate block.
    303    auto keywords = mozilla::intl::Calendar::GetBcp47KeywordValuesForLocale("");
    304    if (keywords.isErr()) {
    305      intl::ReportInternalError(cx, keywords.unwrapErr());
    306      return nullptr;
    307    }
    308 
    309    static constexpr auto unsupported = UnsupportedCalendars();
    310 
    311    if (!EnumerationIntoList<unsupported>(cx, keywords.unwrap(), &list)) {
    312      return nullptr;
    313    }
    314  }
    315 
    316  return CreateArrayFromList(cx, &list);
    317 }
    318 
    319 /**
    320 * Returns the list of collation types which mustn't be returned by
    321 * |Intl.supportedValuesOf()|.
    322 */
    323 static constexpr auto UnsupportedCollations() {
    324  return std::array{
    325      "search",
    326      "standard",
    327  };
    328 }
    329 
    330 /**
    331 * AvailableCollations ( )
    332 */
    333 static ArrayObject* AvailableCollations(JSContext* cx) {
    334  Rooted<StringList> list(cx, StringList(cx));
    335 
    336  {
    337    // Hazard analysis complains that the mozilla::Result destructor calls a
    338    // GC function, which is unsound when returning an unrooted value. Work
    339    // around this issue by restricting the lifetime of |keywords| to a
    340    // separate block.
    341    auto keywords = mozilla::intl::Collator::GetBcp47KeywordValues();
    342    if (keywords.isErr()) {
    343      intl::ReportInternalError(cx, keywords.unwrapErr());
    344      return nullptr;
    345    }
    346 
    347    static constexpr auto unsupported = UnsupportedCollations();
    348 
    349    if (!EnumerationIntoList<unsupported>(cx, keywords.unwrap(), &list)) {
    350      return nullptr;
    351    }
    352  }
    353 
    354  return CreateArrayFromList(cx, &list);
    355 }
    356 
    357 /**
    358 * Returns a list of known, unsupported currencies which are returned by
    359 * |Currency::GetISOCurrencies()|.
    360 */
    361 static constexpr auto UnsupportedCurrencies() {
    362  // "MVP" is also marked with "questionable, remove?" in ucurr.cpp, but only
    363  // this single currency code isn't supported by |Intl.DisplayNames| and
    364  // therefore must be excluded by |Intl.supportedValuesOf|.
    365  return std::array{
    366      "LSM",  // https://unicode-org.atlassian.net/browse/ICU-21687
    367  };
    368 }
    369 
    370 /**
    371 * AvailableCurrencies ( )
    372 */
    373 static ArrayObject* AvailableCurrencies(JSContext* cx) {
    374  Rooted<StringList> list(cx, StringList(cx));
    375 
    376  {
    377    // Hazard analysis complains that the mozilla::Result destructor calls a
    378    // GC function, which is unsound when returning an unrooted value. Work
    379    // around this issue by restricting the lifetime of |currencies| to a
    380    // separate block.
    381    auto currencies = mozilla::intl::Currency::GetISOCurrencies();
    382    if (currencies.isErr()) {
    383      intl::ReportInternalError(cx, currencies.unwrapErr());
    384      return nullptr;
    385    }
    386 
    387    static constexpr auto unsupported = UnsupportedCurrencies();
    388 
    389    if (!EnumerationIntoList<unsupported>(cx, currencies.unwrap(), &list)) {
    390      return nullptr;
    391    }
    392  }
    393 
    394  return CreateArrayFromList(cx, &list);
    395 }
    396 
    397 /**
    398 * AvailableNumberingSystems ( )
    399 */
    400 static ArrayObject* AvailableNumberingSystems(JSContext* cx) {
    401  static constexpr std::array numberingSystems = {
    402      NUMBERING_SYSTEMS_WITH_SIMPLE_DIGIT_MAPPINGS};
    403 
    404  return CreateArrayFromSortedList(cx, numberingSystems);
    405 }
    406 
    407 /**
    408 * AvailableTimeZones ( )
    409 */
    410 static ArrayObject* AvailableTimeZones(JSContext* cx) {
    411  // Unsorted list of canonical time zone names, possibly containing duplicates.
    412  Rooted<StringList> timeZones(cx, StringList(cx));
    413 
    414  intl::SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
    415  auto iterResult = sharedIntlData.availableTimeZonesIteration(cx);
    416  if (iterResult.isErr()) {
    417    return nullptr;
    418  }
    419  auto iter = iterResult.unwrap();
    420 
    421  Rooted<JSAtom*> validatedTimeZone(cx);
    422  for (; !iter.done(); iter.next()) {
    423    validatedTimeZone = iter.get();
    424 
    425    // Canonicalize the time zone before adding it to the result array.
    426    auto* timeZone = sharedIntlData.canonicalizeTimeZone(cx, validatedTimeZone);
    427    if (!timeZone) {
    428      return nullptr;
    429    }
    430 
    431    if (!timeZones.append(timeZone)) {
    432      return nullptr;
    433    }
    434  }
    435 
    436  return CreateArrayFromList(cx, &timeZones);
    437 }
    438 
    439 template <size_t N>
    440 constexpr auto MeasurementUnitNames(
    441    const mozilla::intl::SimpleMeasureUnit (&units)[N]) {
    442  std::array<const char*, N> array = {};
    443  for (size_t i = 0; i < N; ++i) {
    444    array[i] = units[i].name;
    445  }
    446  return array;
    447 }
    448 
    449 /**
    450 * AvailableUnits ( )
    451 */
    452 static ArrayObject* AvailableUnits(JSContext* cx) {
    453  static constexpr auto simpleMeasureUnitNames =
    454      MeasurementUnitNames(mozilla::intl::simpleMeasureUnits);
    455 
    456  return CreateArrayFromSortedList(cx, simpleMeasureUnitNames);
    457 }
    458 
    459 /**
    460 * Intl.getCanonicalLocales ( locales )
    461 */
    462 static bool intl_getCanonicalLocales(JSContext* cx, unsigned argc, Value* vp) {
    463  CallArgs args = CallArgsFromVp(argc, vp);
    464 
    465  // Step 1.
    466  Rooted<LocalesList> locales(cx, cx);
    467  if (!CanonicalizeLocaleList(cx, args.get(0), &locales)) {
    468    return false;
    469  }
    470 
    471  // Step 2.
    472  auto* array = LocalesListToArray(cx, locales);
    473  if (!array) {
    474    return false;
    475  }
    476  args.rval().setObject(*array);
    477  return true;
    478 }
    479 
    480 /**
    481 * Intl.supportedValuesOf ( key )
    482 */
    483 static bool intl_supportedValuesOf(JSContext* cx, unsigned argc, Value* vp) {
    484  CallArgs args = CallArgsFromVp(argc, vp);
    485 
    486  // Step 1.
    487  auto* key = ToString(cx, args.get(0));
    488  if (!key) {
    489    return false;
    490  }
    491 
    492  auto* linearKey = key->ensureLinear(cx);
    493  if (!linearKey) {
    494    return false;
    495  }
    496 
    497  // Steps 2-8.
    498  ArrayObject* list;
    499  if (StringEqualsLiteral(linearKey, "calendar")) {
    500    list = AvailableCalendars(cx);
    501  } else if (StringEqualsLiteral(linearKey, "collation")) {
    502    list = AvailableCollations(cx);
    503  } else if (StringEqualsLiteral(linearKey, "currency")) {
    504    list = AvailableCurrencies(cx);
    505  } else if (StringEqualsLiteral(linearKey, "numberingSystem")) {
    506    list = AvailableNumberingSystems(cx);
    507  } else if (StringEqualsLiteral(linearKey, "timeZone")) {
    508    list = AvailableTimeZones(cx);
    509  } else if (StringEqualsLiteral(linearKey, "unit")) {
    510    list = AvailableUnits(cx);
    511  } else {
    512    if (UniqueChars chars = QuoteString(cx, linearKey, '"')) {
    513      JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY,
    514                                chars.get());
    515    }
    516    return false;
    517  }
    518  if (!list) {
    519    return false;
    520  }
    521 
    522  // Step 9.
    523  args.rval().setObject(*list);
    524  return true;
    525 }
    526 
    527 static bool intl_toSource(JSContext* cx, unsigned argc, Value* vp) {
    528  CallArgs args = CallArgsFromVp(argc, vp);
    529  args.rval().setString(cx->names().Intl);
    530  return true;
    531 }
    532 
    533 static const JSFunctionSpec intl_static_methods[] = {
    534    JS_FN("toSource", intl_toSource, 0, 0),
    535    JS_FN("getCanonicalLocales", intl_getCanonicalLocales, 1, 0),
    536    JS_FN("supportedValuesOf", intl_supportedValuesOf, 1, 0),
    537    JS_FS_END,
    538 };
    539 
    540 static const JSPropertySpec intl_static_properties[] = {
    541    JS_STRING_SYM_PS(toStringTag, "Intl", JSPROP_READONLY),
    542    JS_PS_END,
    543 };
    544 
    545 static JSObject* CreateIntlObject(JSContext* cx, JSProtoKey key) {
    546  RootedObject proto(cx, &cx->global()->getObjectPrototype());
    547 
    548  // The |Intl| object is just a plain object with some "static" function
    549  // properties and some constructor properties.
    550  return NewTenuredObjectWithGivenProto(cx, &IntlClass, proto);
    551 }
    552 
    553 /**
    554 * Initializes the Intl Object and its standard built-in properties.
    555 * Spec: ECMAScript Internationalization API Specification, 8.0, 8.1
    556 */
    557 static bool IntlClassFinish(JSContext* cx, HandleObject intl,
    558                            HandleObject proto) {
    559  // Add the constructor properties.
    560  RootedId ctorId(cx);
    561  RootedValue ctorValue(cx);
    562  for (const auto& protoKey : {
    563           JSProto_Collator,
    564           JSProto_DateTimeFormat,
    565           JSProto_DisplayNames,
    566           JSProto_DurationFormat,
    567           JSProto_ListFormat,
    568           JSProto_Locale,
    569           JSProto_NumberFormat,
    570           JSProto_PluralRules,
    571           JSProto_RelativeTimeFormat,
    572           JSProto_Segmenter,
    573       }) {
    574    if (GlobalObject::skipDeselectedConstructor(cx, protoKey)) {
    575      continue;
    576    }
    577 
    578    JSObject* ctor = GlobalObject::getOrCreateConstructor(cx, protoKey);
    579    if (!ctor) {
    580      return false;
    581    }
    582 
    583    ctorId = NameToId(ClassName(protoKey, cx));
    584    ctorValue.setObject(*ctor);
    585    if (!DefineDataProperty(cx, intl, ctorId, ctorValue, 0)) {
    586      return false;
    587    }
    588  }
    589 
    590  return true;
    591 }
    592 
    593 static const ClassSpec IntlClassSpec = {
    594    CreateIntlObject, nullptr, intl_static_methods, intl_static_properties,
    595    nullptr,          nullptr, IntlClassFinish,
    596 };
    597 
    598 const JSClass js::IntlClass = {
    599    "Intl",
    600    JSCLASS_HAS_CACHED_PROTO(JSProto_Intl),
    601    JS_NULL_CLASS_OPS,
    602    &IntlClassSpec,
    603 };