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 };