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 }