Source code

Revision control

Other Tools

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/Casting.h"
14
#include "mozilla/Maybe.h"
15
#include "mozilla/Span.h"
16
#include "mozilla/TextUtils.h"
17
18
#include <algorithm>
19
#include <iterator>
20
#include <string>
21
#include <string.h>
22
#include <utility>
23
24
#include "jsapi.h"
25
#include "jsfriendapi.h"
26
27
#include "builtin/intl/CommonFunctions.h"
28
#include "builtin/intl/LanguageTag.h"
29
#include "builtin/String.h"
30
#include "gc/Rooting.h"
31
#include "js/Conversions.h"
32
#include "js/TypeDecls.h"
33
#include "js/Wrapper.h"
34
#include "util/StringBuffer.h"
35
#include "vm/GlobalObject.h"
36
#include "vm/JSContext.h"
37
#include "vm/Printer.h"
38
#include "vm/StringType.h"
39
40
#include "vm/JSObject-inl.h"
41
#include "vm/NativeObject-inl.h"
42
43
using namespace js;
44
using namespace js::intl::LanguageTagLimits;
45
46
using intl::LanguageTag;
47
using intl::LanguageTagParser;
48
49
const JSClass LocaleObject::class_ = {
50
js_Object_str,
51
JSCLASS_HAS_RESERVED_SLOTS(LocaleObject::SLOT_COUNT) |
52
JSCLASS_HAS_CACHED_PROTO(JSProto_Locale),
53
JS_NULL_CLASS_OPS, &LocaleObject::classSpec_};
54
55
const JSClass& LocaleObject::protoClass_ = PlainObject::class_;
56
57
static inline bool IsLocale(HandleValue v) {
58
return v.isObject() && v.toObject().is<LocaleObject>();
59
}
60
61
// Return the length of the base-name subtags.
62
static size_t BaseNameLength(const LanguageTag& tag) {
63
size_t baseNameLength = tag.language().length();
64
if (tag.script().present()) {
65
baseNameLength += 1 + tag.script().length();
66
}
67
if (tag.region().present()) {
68
baseNameLength += 1 + tag.region().length();
69
}
70
for (const auto& variant : tag.variants()) {
71
baseNameLength += 1 + strlen(variant.get());
72
}
73
return baseNameLength;
74
}
75
76
struct IndexAndLength {
77
size_t index;
78
size_t length;
79
80
IndexAndLength(size_t index, size_t length) : index(index), length(length){};
81
82
template <typename T>
83
mozilla::Span<const T> spanOf(const T* ptr) const {
84
return {ptr + index, length};
85
}
86
};
87
88
// Compute the Unicode extension's index and length in the extension subtag.
89
static mozilla::Maybe<IndexAndLength> UnicodeExtensionPosition(
90
const LanguageTag& tag) {
91
size_t index = 0;
92
for (const auto& extension : tag.extensions()) {
93
MOZ_ASSERT(!mozilla::IsAsciiUppercaseAlpha(extension[0]),
94
"extensions are case normalized to lowercase");
95
96
size_t extensionLength = strlen(extension.get());
97
if (extension[0] == 'u') {
98
return mozilla::Some(IndexAndLength{index, extensionLength});
99
}
100
101
// Add +1 to skip over the preceding separator.
102
index += 1 + extensionLength;
103
}
104
return mozilla::Nothing();
105
}
106
107
static LocaleObject* CreateLocaleObject(JSContext* cx, HandleObject prototype,
108
const LanguageTag& tag) {
109
RootedString tagStr(cx, tag.toString(cx));
110
if (!tagStr) {
111
return nullptr;
112
}
113
114
size_t baseNameLength = BaseNameLength(tag);
115
116
RootedString baseName(cx, NewDependentString(cx, tagStr, 0, baseNameLength));
117
if (!baseName) {
118
return nullptr;
119
}
120
121
RootedValue unicodeExtension(cx, UndefinedValue());
122
if (auto result = UnicodeExtensionPosition(tag)) {
123
JSString* str = NewDependentString(
124
cx, tagStr, baseNameLength + 1 + result->index, result->length);
125
if (!str) {
126
return nullptr;
127
}
128
129
unicodeExtension.setString(str);
130
}
131
132
auto* locale = NewObjectWithClassProto<LocaleObject>(cx, prototype);
133
if (!locale) {
134
return nullptr;
135
}
136
137
locale->setFixedSlot(LocaleObject::LANGUAGE_TAG_SLOT, StringValue(tagStr));
138
locale->setFixedSlot(LocaleObject::BASENAME_SLOT, StringValue(baseName));
139
locale->setFixedSlot(LocaleObject::UNICODE_EXTENSION_SLOT, unicodeExtension);
140
141
return locale;
142
}
143
144
static inline bool IsValidUnicodeExtensionValue(JSLinearString* linear) {
145
return linear->length() > 0 &&
146
LanguageTagParser::canParseUnicodeExtensionType(linear);
147
}
148
149
/** Iterate through (sep keyword) in a valid, lowercased Unicode extension. */
150
template <typename CharT>
151
class SepKeywordIterator {
152
const CharT* iter_;
153
const CharT* const end_;
154
155
public:
156
SepKeywordIterator(const CharT* unicodeExtensionBegin,
157
const CharT* unicodeExtensionEnd)
158
: iter_(unicodeExtensionBegin), end_(unicodeExtensionEnd) {}
159
160
/**
161
* Return (sep keyword) in the Unicode locale extension from begin to end.
162
* The first call after all (sep keyword) are consumed returns |nullptr|; no
163
* further calls are allowed.
164
*/
165
const CharT* next() {
166
MOZ_ASSERT(iter_ != nullptr,
167
"can't call next() once it's returned nullptr");
168
169
constexpr size_t SepKeyLength = 1 + UnicodeKeyLength; // "-co"/"-nu"/etc.
170
171
MOZ_ASSERT(iter_ + SepKeyLength <= end_,
172
"overall Unicode locale extension or non-leading subtags must "
173
"be at least key-sized");
174
175
MOZ_ASSERT((iter_[0] == 'u' && iter_[1] == '-') || iter_[0] == '-');
176
177
while (true) {
178
// Skip past '-' so |std::char_traits::find| makes progress. Skipping
179
// 'u' is harmless -- skip or not, |find| returns the first '-'.
180
iter_++;
181
182
// Find the next separator.
183
iter_ = std::char_traits<CharT>::find(
184
iter_, mozilla::PointerRangeSize(iter_, end_), CharT('-'));
185
if (!iter_) {
186
return nullptr;
187
}
188
189
MOZ_ASSERT(iter_ + SepKeyLength <= end_,
190
"non-leading subtags in a Unicode locale extension are all "
191
"at least as long as a key");
192
193
if (iter_ + SepKeyLength == end_ || // key is terminal subtag
194
iter_[SepKeyLength] == '-') { // key is followed by more subtags
195
break;
196
}
197
}
198
199
MOZ_ASSERT(iter_[0] == '-');
200
MOZ_ASSERT(mozilla::IsAsciiLowercaseAlpha(iter_[1]) ||
201
mozilla::IsAsciiDigit(iter_[1]));
202
MOZ_ASSERT(mozilla::IsAsciiLowercaseAlpha(iter_[2]));
203
MOZ_ASSERT_IF(iter_ + SepKeyLength < end_, iter_[SepKeyLength] == '-');
204
return iter_;
205
}
206
};
207
208
/**
209
* 9.2.10 GetOption ( options, property, type, values, fallback )
210
*
211
* If the requested property is present and not-undefined, set the result string
212
* to |ToString(value)|. Otherwise set the result string to nullptr.
213
*/
214
static bool GetStringOption(JSContext* cx, HandleObject options,
215
HandlePropertyName name,
216
MutableHandle<JSLinearString*> string) {
217
// Step 1.
218
RootedValue option(cx);
219
if (!GetProperty(cx, options, options, name, &option)) {
220
return false;
221
}
222
223
// Step 2.
224
JSLinearString* linear = nullptr;
225
if (!option.isUndefined()) {
226
// Steps 2.a-b, 2.d (not applicable).
227
228
// Steps 2.c, 2.e.
229
JSString* str = ToString(cx, option);
230
if (!str) {
231
return false;
232
}
233
linear = str->ensureLinear(cx);
234
if (!linear) {
235
return false;
236
}
237
}
238
239
// Step 3.
240
string.set(linear);
241
return true;
242
}
243
244
/**
245
* 9.2.10 GetOption ( options, property, type, values, fallback )
246
*
247
* If the requested property is present and not-undefined, set the result string
248
* to |ToString(ToBoolean(value))|. Otherwise set the result string to nullptr.
249
*/
250
static bool GetBooleanOption(JSContext* cx, HandleObject options,
251
HandlePropertyName name,
252
MutableHandle<JSLinearString*> string) {
253
// Step 1.
254
RootedValue option(cx);
255
if (!GetProperty(cx, options, options, name, &option)) {
256
return false;
257
}
258
259
// Step 2.
260
JSLinearString* linear = nullptr;
261
if (!option.isUndefined()) {
262
// Steps 2.a, 2.c-d (not applicable).
263
264
// Steps 2.c, 2.e.
265
JSString* str = BooleanToString(cx, ToBoolean(option));
266
MOZ_ALWAYS_TRUE(linear = str->ensureLinear(cx));
267
}
268
269
// Step 3.
270
string.set(linear);
271
return true;
272
}
273
274
/**
275
* ApplyOptionsToTag ( tag, options )
276
*/
277
static bool ApplyOptionsToTag(JSContext* cx, LanguageTag& tag,
278
HandleObject options) {
279
// Steps 1-2 (Already performed in caller).
280
281
RootedLinearString option(cx);
282
283
// Step 3.
284
if (!GetStringOption(cx, options, cx->names().language, &option)) {
285
return false;
286
}
287
288
// Step 4.
289
intl::LanguageSubtag language;
290
if (option && !intl::ParseStandaloneLanguageTag(option, language)) {
291
if (UniqueChars str = QuoteString(cx, option, '"')) {
292
JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
293
JSMSG_INVALID_OPTION_VALUE, "language",
294
str.get());
295
}
296
return false;
297
}
298
299
// Step 5.
300
if (!GetStringOption(cx, options, cx->names().script, &option)) {
301
return false;
302
}
303
304
// Step 6.
305
intl::ScriptSubtag script;
306
if (option && !intl::ParseStandaloneScriptTag(option, script)) {
307
if (UniqueChars str = QuoteString(cx, option, '"')) {
308
JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
309
JSMSG_INVALID_OPTION_VALUE, "script",
310
str.get());
311
}
312
return false;
313
}
314
315
// Step 7.
316
if (!GetStringOption(cx, options, cx->names().region, &option)) {
317
return false;
318
}
319
320
// Step 8.
321
intl::RegionSubtag region;
322
if (option && !intl::ParseStandaloneRegionTag(option, region)) {
323
if (UniqueChars str = QuoteString(cx, option, '"')) {
324
JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
325
JSMSG_INVALID_OPTION_VALUE, "region",
326
str.get());
327
}
328
return false;
329
}
330
331
// Step 9 (Already performed in caller).
332
333
// Skip steps 10-13 when no subtags were modified.
334
if (language.present() || script.present() || region.present()) {
335
// Step 10.
336
if (language.present()) {
337
tag.setLanguage(language);
338
}
339
340
// Step 11.
341
if (script.present()) {
342
tag.setScript(script);
343
}
344
345
// Step 12.
346
if (region.present()) {
347
tag.setRegion(region);
348
}
349
350
// Step 13.
351
// Optimized to only canonicalize the base-name subtags. All other
352
// canonicalization steps will happen later.
353
if (!tag.canonicalizeBaseName(cx)) {
354
return true;
355
}
356
}
357
358
return true;
359
}
360
361
/**
362
* ApplyUnicodeExtensionToTag( tag, options, relevantExtensionKeys )
363
*/
364
bool js::intl::ApplyUnicodeExtensionToTag(
365
JSContext* cx, LanguageTag& tag,
366
JS::HandleVector<intl::UnicodeExtensionKeyword> keywords) {
367
// If no Unicode extensions were present in the options object, we can skip
368
// everything below and directly return.
369
if (keywords.length() == 0) {
370
return true;
371
}
372
373
Vector<char, 32> newExtension(cx);
374
if (!newExtension.append('u')) {
375
return false;
376
}
377
378
// Check if there's an existing Unicode extension subtag.
379
380
const char* unicodeExtensionEnd = nullptr;
381
const char* unicodeExtensionKeywords = nullptr;
382
if (const char* unicodeExtension = tag.unicodeExtension()) {
383
unicodeExtensionEnd = unicodeExtension + strlen(unicodeExtension);
384
385
SepKeywordIterator<char> iter(unicodeExtension, unicodeExtensionEnd);
386
387
// Find the start of the first keyword.
388
unicodeExtensionKeywords = iter.next();
389
390
// Copy any attributes present before the first keyword.
391
const char* attributesEnd = unicodeExtensionKeywords
392
? unicodeExtensionKeywords
393
: unicodeExtensionEnd;
394
if (!newExtension.append(unicodeExtension + 1, attributesEnd)) {
395
return false;
396
}
397
}
398
399
// Append the new keywords before any existing keywords. That way any previous
400
// keyword with the same key is detected as a duplicate when canonicalizing
401
// the Unicode extension subtag and gets discarded.
402
403
for (const auto& keyword : keywords) {
404
UnicodeExtensionKeyword::UnicodeKeySpan key = keyword.key();
405
if (!newExtension.append('-')) {
406
return false;
407
}
408
if (!newExtension.append(key.data(), key.size())) {
409
return false;
410
}
411
if (!newExtension.append('-')) {
412
return false;
413
}
414
415
JS::AutoCheckCannotGC nogc;
416
JSLinearString* type = keyword.type();
417
if (type->hasLatin1Chars()) {
418
if (!newExtension.append(type->latin1Chars(nogc), type->length())) {
419
return false;
420
}
421
} else {
422
if (!newExtension.append(type->twoByteChars(nogc), type->length())) {
423
return false;
424
}
425
}
426
}
427
428
// Append the remaining keywords from the previous Unicode extension subtag.
429
if (unicodeExtensionKeywords) {
430
if (!newExtension.append(unicodeExtensionKeywords, unicodeExtensionEnd)) {
431
return false;
432
}
433
}
434
435
// Null-terminate the new Unicode extension string.
436
if (!newExtension.append('\0')) {
437
return false;
438
}
439
440
// Insert the new Unicode extension string into the language tag.
441
UniqueChars newExtensionChars(newExtension.extractOrCopyRawBuffer());
442
if (!newExtensionChars) {
443
return false;
444
}
445
return tag.setUnicodeExtension(std::move(newExtensionChars));
446
}
447
448
static JS::Result<JSString*> LanguageTagFromMaybeWrappedLocale(JSContext* cx,
449
JSObject* obj) {
450
if (obj->is<LocaleObject>()) {
451
return obj->as<LocaleObject>().languageTag();
452
}
453
454
JSObject* unwrapped = CheckedUnwrapStatic(obj);
455
if (!unwrapped) {
456
ReportAccessDenied(cx);
457
return cx->alreadyReportedError();
458
}
459
460
if (!unwrapped->is<LocaleObject>()) {
461
return nullptr;
462
}
463
464
RootedString tagStr(cx, unwrapped->as<LocaleObject>().languageTag());
465
if (!cx->compartment()->wrap(cx, &tagStr)) {
466
return cx->alreadyReportedError();
467
}
468
return tagStr.get();
469
}
470
471
/**
472
* Intl.Locale( tag[, options] )
473
*/
474
static bool Locale(JSContext* cx, unsigned argc, Value* vp) {
475
CallArgs args = CallArgsFromVp(argc, vp);
476
477
// Step 1.
478
if (!ThrowIfNotConstructing(cx, args, "Intl.Locale")) {
479
return false;
480
}
481
482
// Steps 2-6 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
483
RootedObject proto(cx);
484
if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Locale, &proto)) {
485
return false;
486
}
487
488
// Steps 7-9.
489
HandleValue tagValue = args.get(0);
490
JSString* tagStr;
491
if (tagValue.isObject()) {
492
JS_TRY_VAR_OR_RETURN_FALSE(
493
cx, tagStr,
494
LanguageTagFromMaybeWrappedLocale(cx, &tagValue.toObject()));
495
if (!tagStr) {
496
tagStr = ToString(cx, tagValue);
497
if (!tagStr) {
498
return false;
499
}
500
}
501
} else if (tagValue.isString()) {
502
tagStr = tagValue.toString();
503
} else {
504
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
505
JSMSG_INVALID_LOCALES_ELEMENT);
506
return false;
507
}
508
509
RootedLinearString tagLinearStr(cx, tagStr->ensureLinear(cx));
510
if (!tagLinearStr) {
511
return false;
512
}
513
514
// ApplyOptionsToTag, steps 2 and 9.
515
LanguageTag tag(cx);
516
if (!LanguageTagParser::parse(cx, tagLinearStr, tag)) {
517
return false;
518
}
519
520
if (!tag.canonicalizeBaseName(cx)) {
521
return false;
522
}
523
524
// Steps 10-11.
525
if (args.hasDefined(1)) {
526
RootedObject options(cx, ToObject(cx, args[1]));
527
if (!options) {
528
return false;
529
}
530
531
// Step 12.
532
if (!ApplyOptionsToTag(cx, tag, options)) {
533
return false;
534
}
535
536
// Step 13.
537
JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx);
538
539
// Step 14.
540
RootedLinearString calendar(cx);
541
if (!GetStringOption(cx, options, cx->names().calendar, &calendar)) {
542
return false;
543
}
544
545
// Steps 15-16.
546
if (calendar) {
547
if (!IsValidUnicodeExtensionValue(calendar)) {
548
if (UniqueChars str = QuoteString(cx, calendar, '"')) {
549
JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
550
JSMSG_INVALID_OPTION_VALUE, "calendar",
551
str.get());
552
}
553
return false;
554
}
555
556
if (!keywords.emplaceBack("ca", calendar)) {
557
return false;
558
}
559
}
560
561
// Step 17.
562
RootedLinearString collation(cx);
563
if (!GetStringOption(cx, options, cx->names().collation, &collation)) {
564
return false;
565
}
566
567
// Steps 18-19.
568
if (collation) {
569
if (!IsValidUnicodeExtensionValue(collation)) {
570
if (UniqueChars str = QuoteString(cx, collation, '"')) {
571
JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
572
JSMSG_INVALID_OPTION_VALUE, "collation",
573
str.get());
574
}
575
return false;
576
}
577
578
if (!keywords.emplaceBack("co", collation)) {
579
return false;
580
}
581
}
582
583
// Step 20 (without validation).
584
RootedLinearString hourCycle(cx);
585
if (!GetStringOption(cx, options, cx->names().hourCycle, &hourCycle)) {
586
return false;
587
}
588
589
// Steps 20-21.
590
if (hourCycle) {
591
if (!StringEqualsLiteral(hourCycle, "h11") &&
592
!StringEqualsLiteral(hourCycle, "h12") &&
593
!StringEqualsLiteral(hourCycle, "h23") &&
594
!StringEqualsLiteral(hourCycle, "h24")) {
595
if (UniqueChars str = QuoteString(cx, hourCycle, '"')) {
596
JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
597
JSMSG_INVALID_OPTION_VALUE, "hourCycle",
598
str.get());
599
}
600
return false;
601
}
602
603
if (!keywords.emplaceBack("hc", hourCycle)) {
604
return false;
605
}
606
}
607
608
// Step 22 (without validation).
609
RootedLinearString caseFirst(cx);
610
if (!GetStringOption(cx, options, cx->names().caseFirst, &caseFirst)) {
611
return false;
612
}
613
614
// Steps 22-23.
615
if (caseFirst) {
616
if (!StringEqualsLiteral(caseFirst, "upper") &&
617
!StringEqualsLiteral(caseFirst, "lower") &&
618
!StringEqualsLiteral(caseFirst, "false")) {
619
if (UniqueChars str = QuoteString(cx, caseFirst, '"')) {
620
JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
621
JSMSG_INVALID_OPTION_VALUE, "caseFirst",
622
str.get());
623
}
624
return false;
625
}
626
627
if (!keywords.emplaceBack("kf", caseFirst)) {
628
return false;
629
}
630
}
631
632
// Steps 24-25.
633
RootedLinearString numeric(cx);
634
if (!GetBooleanOption(cx, options, cx->names().numeric, &numeric)) {
635
return false;
636
}
637
638
// Step 26.
639
if (numeric) {
640
if (!keywords.emplaceBack("kn", numeric)) {
641
return false;
642
}
643
}
644
645
// Step 27.
646
RootedLinearString numberingSystem(cx);
647
if (!GetStringOption(cx, options, cx->names().numberingSystem,
648
&numberingSystem)) {
649
return false;
650
}
651
652
// Steps 28-29.
653
if (numberingSystem) {
654
if (!IsValidUnicodeExtensionValue(numberingSystem)) {
655
if (UniqueChars str = QuoteString(cx, numberingSystem, '"')) {
656
JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
657
JSMSG_INVALID_OPTION_VALUE,
658
"numberingSystem", str.get());
659
}
660
return false;
661
}
662
663
if (!keywords.emplaceBack("nu", numberingSystem)) {
664
return false;
665
}
666
}
667
668
// Step 30.
669
if (!ApplyUnicodeExtensionToTag(cx, tag, keywords)) {
670
return false;
671
}
672
}
673
674
// ApplyOptionsToTag, steps 9 and 13.
675
// ApplyUnicodeExtensionToTag, step 8.
676
if (!tag.canonicalizeExtensions(
677
cx, LanguageTag::UnicodeExtensionCanonicalForm::Yes)) {
678
return false;
679
}
680
681
// Steps 6, 31-37.
682
JSObject* obj = CreateLocaleObject(cx, proto, tag);
683
if (!obj) {
684
return false;
685
}
686
687
// Step 38.
688
args.rval().setObject(*obj);
689
return true;
690
}
691
692
using UnicodeKey = const char (&)[UnicodeKeyLength + 1];
693
694
// Returns the tuple [index, length] of the `type` in the `keyword` in Unicode
695
// locale extension |extension| that has |key| as its `key`. If `keyword` lacks
696
// a type, the returned |index| will be where `type` would have been, and
697
// |length| will be set to zero.
698
template <typename CharT>
699
static mozilla::Maybe<IndexAndLength> FindUnicodeExtensionType(
700
const CharT* extension, size_t length, UnicodeKey key) {
701
MOZ_ASSERT(extension[0] == 'u');
702
MOZ_ASSERT(extension[1] == '-');
703
704
const CharT* end = extension + length;
705
706
SepKeywordIterator<CharT> iter(extension, end);
707
708
// Search all keywords until a match was found.
709
const CharT* beginKey;
710
while (true) {
711
beginKey = iter.next();
712
if (!beginKey) {
713
return mozilla::Nothing();
714
}
715
716
// Add +1 to skip over the separator preceding the keyword.
717
MOZ_ASSERT(beginKey[0] == '-');
718
beginKey++;
719
720
// Exit the loop on the first match.
721
if (std::equal(beginKey, beginKey + UnicodeKeyLength, key)) {
722
break;
723
}
724
}
725
726
// Skip over the key.
727
const CharT* beginType = beginKey + UnicodeKeyLength;
728
729
// Find the start of the next keyword.
730
const CharT* endType = iter.next();
731
732
// No further keyword present, the current keyword ends the Unicode extension.
733
if (!endType) {
734
endType = end;
735
}
736
737
// If the keyword has a type, skip over the separator preceding the type.
738
if (beginType != endType) {
739
MOZ_ASSERT(beginType[0] == '-');
740
beginType++;
741
}
742
return mozilla::Some(IndexAndLength{size_t(beginType - extension),
743
size_t(endType - beginType)});
744
}
745
746
static inline auto FindUnicodeExtensionType(JSLinearString* unicodeExtension,
747
UnicodeKey key) {
748
JS::AutoCheckCannotGC nogc;
749
return unicodeExtension->hasLatin1Chars()
750
? FindUnicodeExtensionType(unicodeExtension->latin1Chars(nogc),
751
unicodeExtension->length(), key)
752
: FindUnicodeExtensionType(unicodeExtension->twoByteChars(nogc),
753
unicodeExtension->length(), key);
754
}
755
756
// Return the sequence of types for the Unicode extension keyword specified by
757
// key or undefined when the keyword isn't present.
758
static bool GetUnicodeExtension(JSContext* cx, LocaleObject* locale,
759
UnicodeKey key, MutableHandleValue value) {
760
// Return undefined when no Unicode extension subtag is present.
761
const Value& unicodeExtensionValue = locale->unicodeExtension();
762
if (unicodeExtensionValue.isUndefined()) {
763
value.setUndefined();
764
return true;
765
}
766
767
JSLinearString* unicodeExtension =
768
unicodeExtensionValue.toString()->ensureLinear(cx);
769
if (!unicodeExtension) {
770
return false;
771
}
772
773
// Find the type of the requested key in the Unicode extension subtag.
774
auto result = FindUnicodeExtensionType(unicodeExtension, key);
775
776
// Return undefined if the requested key isn't present in the extension.
777
if (!result) {
778
value.setUndefined();
779
return true;
780
}
781
782
size_t index = result->index;
783
size_t length = result->length;
784
785
// Otherwise return the type value of the found keyword.
786
JSString* str = NewDependentString(cx, unicodeExtension, index, length);
787
if (!str) {
788
return false;
789
}
790
value.setString(str);
791
return true;
792
}
793
794
struct BaseNamePartsResult {
795
IndexAndLength language;
796
mozilla::Maybe<IndexAndLength> script;
797
mozilla::Maybe<IndexAndLength> region;
798
};
799
800
// Returns [language-length, script-index, region-index, region-length].
801
template <typename CharT>
802
static BaseNamePartsResult BaseNameParts(const CharT* baseName, size_t length) {
803
size_t languageLength;
804
size_t scriptIndex = 0;
805
size_t regionIndex = 0;
806
size_t regionLength = 0;
807
808
// Search the first separator to find the end of the language subtag.
809
if (const CharT* sep = std::char_traits<CharT>::find(baseName, length, '-')) {
810
languageLength = sep - baseName;
811
812
// Add +1 to skip over the separator character.
813
size_t nextSubtag = languageLength + 1;
814
815
// Script subtags are always four characters long, but take care for a four
816
// character long variant subtag. These start with a digit.
817
if ((nextSubtag + ScriptLength == length ||
818
(nextSubtag + ScriptLength < length &&
819
baseName[nextSubtag + ScriptLength] == '-')) &&
820
mozilla::IsAsciiAlpha(baseName[nextSubtag])) {
821
scriptIndex = nextSubtag;
822
nextSubtag = scriptIndex + ScriptLength + 1;
823
}
824
825
// Region subtags can be either two or three characters long.
826
if (nextSubtag < length) {
827
for (size_t rlen : {AlphaRegionLength, DigitRegionLength}) {
828
MOZ_ASSERT(nextSubtag + rlen <= length);
829
if (nextSubtag + rlen == length || baseName[nextSubtag + rlen] == '-') {
830
regionIndex = nextSubtag;
831
regionLength = rlen;
832
break;
833
}
834
}
835
}
836
} else {
837
// No separator found, the base-name consists of just a language subtag.
838
languageLength = length;
839
}
840
841
IndexAndLength language{0, languageLength};
842
MOZ_ASSERT(intl::IsStructurallyValidLanguageTag(language.spanOf(baseName)));
843
844
mozilla::Maybe<IndexAndLength> script{};
845
if (scriptIndex) {
846
script.emplace(scriptIndex, ScriptLength);
847
MOZ_ASSERT(intl::IsStructurallyValidScriptTag(script->spanOf(baseName)));
848
}
849
850
mozilla::Maybe<IndexAndLength> region{};
851
if (regionIndex) {
852
region.emplace(regionIndex, regionLength);
853
MOZ_ASSERT(intl::IsStructurallyValidRegionTag(region->spanOf(baseName)));
854
}
855
856
return {language, script, region};
857
}
858
859
static inline auto BaseNameParts(JSLinearString* baseName) {
860
JS::AutoCheckCannotGC nogc;
861
return baseName->hasLatin1Chars()
862
? BaseNameParts(baseName->latin1Chars(nogc), baseName->length())
863
: BaseNameParts(baseName->twoByteChars(nogc), baseName->length());
864
}
865
866
// Intl.Locale.prototype.maximize ()
867
static bool Locale_maximize(JSContext* cx, const CallArgs& args) {
868
MOZ_ASSERT(IsLocale(args.thisv()));
869
870
// Step 3.
871
auto* locale = &args.thisv().toObject().as<LocaleObject>();
872
RootedLinearString tagStr(cx, locale->languageTag()->ensureLinear(cx));
873
if (!tagStr) {
874
return false;
875
}
876
877
LanguageTag tag(cx);
878
if (!LanguageTagParser::parse(cx, tagStr, tag)) {
879
return false;
880
}
881
882
if (!tag.addLikelySubtags(cx)) {
883
return false;
884
}
885
886
// Step 4.
887
auto* result = CreateLocaleObject(cx, nullptr, tag);
888
if (!result) {
889
return false;
890
}
891
args.rval().setObject(*result);
892
return true;
893
}
894
895
// Intl.Locale.prototype.maximize ()
896
static bool Locale_maximize(JSContext* cx, unsigned argc, Value* vp) {
897
// Steps 1-2.
898
CallArgs args = CallArgsFromVp(argc, vp);
899
return CallNonGenericMethod<IsLocale, Locale_maximize>(cx, args);
900
}
901
902
// Intl.Locale.prototype.minimize ()
903
static bool Locale_minimize(JSContext* cx, const CallArgs& args) {
904
MOZ_ASSERT(IsLocale(args.thisv()));
905
906
// Step 3.
907
auto* locale = &args.thisv().toObject().as<LocaleObject>();
908
RootedLinearString tagStr(cx, locale->languageTag()->ensureLinear(cx));
909
if (!tagStr) {
910
return false;
911
}
912
913
LanguageTag tag(cx);
914
if (!LanguageTagParser::parse(cx, tagStr, tag)) {
915
return false;
916
}
917
918
if (!tag.removeLikelySubtags(cx)) {
919
return false;
920
}
921
922
// Step 4.
923
auto* result = CreateLocaleObject(cx, nullptr, tag);
924
if (!result) {
925
return false;
926
}
927
args.rval().setObject(*result);
928
return true;
929
}
930
931
// Intl.Locale.prototype.minimize ()
932
static bool Locale_minimize(JSContext* cx, unsigned argc, Value* vp) {
933
// Steps 1-2.
934
CallArgs args = CallArgsFromVp(argc, vp);
935
return CallNonGenericMethod<IsLocale, Locale_minimize>(cx, args);
936
}
937
938
// Intl.Locale.prototype.toString ()
939
static bool Locale_toString(JSContext* cx, const CallArgs& args) {
940
MOZ_ASSERT(IsLocale(args.thisv()));
941
942
// Step 3.
943
auto* locale = &args.thisv().toObject().as<LocaleObject>();
944
args.rval().setString(locale->languageTag());
945
return true;
946
}
947
948
// Intl.Locale.prototype.toString ()
949
static bool Locale_toString(JSContext* cx, unsigned argc, Value* vp) {
950
// Steps 1-2.
951
CallArgs args = CallArgsFromVp(argc, vp);
952
return CallNonGenericMethod<IsLocale, Locale_toString>(cx, args);
953
}
954
955
// get Intl.Locale.prototype.baseName
956
static bool Locale_baseName(JSContext* cx, const CallArgs& args) {
957
MOZ_ASSERT(IsLocale(args.thisv()));
958
959
// FIXME: spec bug - invalid assertion in step 4.
960
// FIXME: spec bug - subtag production names not updated.
961
962
// Steps 3, 5.
963
auto* locale = &args.thisv().toObject().as<LocaleObject>();
964
args.rval().setString(locale->baseName());
965
return true;
966
}
967
968
// get Intl.Locale.prototype.baseName
969
static bool Locale_baseName(JSContext* cx, unsigned argc, Value* vp) {
970
// Steps 1-2.
971
CallArgs args = CallArgsFromVp(argc, vp);
972
return CallNonGenericMethod<IsLocale, Locale_baseName>(cx, args);
973
}
974
975
// get Intl.Locale.prototype.calendar
976
static bool Locale_calendar(JSContext* cx, const CallArgs& args) {
977
MOZ_ASSERT(IsLocale(args.thisv()));
978
979
// Step 3.
980
auto* locale = &args.thisv().toObject().as<LocaleObject>();
981
return GetUnicodeExtension(cx, locale, "ca", args.rval());
982
}
983
984
// get Intl.Locale.prototype.calendar
985
static bool Locale_calendar(JSContext* cx, unsigned argc, Value* vp) {
986
// Steps 1-2.
987
CallArgs args = CallArgsFromVp(argc, vp);
988
return CallNonGenericMethod<IsLocale, Locale_calendar>(cx, args);
989
}
990
991
// get Intl.Locale.prototype.collation
992
static bool Locale_collation(JSContext* cx, const CallArgs& args) {
993
MOZ_ASSERT(IsLocale(args.thisv()));
994
995
// Step 3.
996
auto* locale = &args.thisv().toObject().as<LocaleObject>();
997
return GetUnicodeExtension(cx, locale, "co", args.rval());
998
}
999
1000
// get Intl.Locale.prototype.collation
1001
static bool Locale_collation(JSContext* cx, unsigned argc, Value* vp) {
1002
// Steps 1-2.
1003
CallArgs args = CallArgsFromVp(argc, vp);
1004
return CallNonGenericMethod<IsLocale, Locale_collation>(cx, args);
1005
}
1006
1007
// get Intl.Locale.prototype.hourCycle
1008
static bool Locale_hourCycle(JSContext* cx, const CallArgs& args) {
1009
MOZ_ASSERT(IsLocale(args.thisv()));
1010
1011
// Step 3.
1012
auto* locale = &args.thisv().toObject().as<LocaleObject>();
1013
return GetUnicodeExtension(cx, locale, "hc", args.rval());
1014
}
1015
1016
// get Intl.Locale.prototype.hourCycle
1017
static bool Locale_hourCycle(JSContext* cx, unsigned argc, Value* vp) {
1018
// Steps 1-2.
1019
CallArgs args = CallArgsFromVp(argc, vp);
1020
return CallNonGenericMethod<IsLocale, Locale_hourCycle>(cx, args);
1021
}
1022
1023
// get Intl.Locale.prototype.caseFirst
1024
static bool Locale_caseFirst(JSContext* cx, const CallArgs& args) {
1025
MOZ_ASSERT(IsLocale(args.thisv()));
1026
1027
// Step 3.
1028
auto* locale = &args.thisv().toObject().as<LocaleObject>();
1029
return GetUnicodeExtension(cx, locale, "kf", args.rval());
1030
}
1031
1032
// get Intl.Locale.prototype.caseFirst
1033
static bool Locale_caseFirst(JSContext* cx, unsigned argc, Value* vp) {
1034
// Steps 1-2.
1035
CallArgs args = CallArgsFromVp(argc, vp);
1036
return CallNonGenericMethod<IsLocale, Locale_caseFirst>(cx, args);
1037
}
1038
1039
// get Intl.Locale.prototype.numeric
1040
static bool Locale_numeric(JSContext* cx, const CallArgs& args) {
1041
MOZ_ASSERT(IsLocale(args.thisv()));
1042
1043
// Step 3.
1044
auto* locale = &args.thisv().toObject().as<LocaleObject>();
1045
RootedValue value(cx);
1046
if (!GetUnicodeExtension(cx, locale, "kn", &value)) {
1047
return false;
1048
}
1049
1050
// FIXME: spec bug - comparison should be against the empty string, too.
1051
MOZ_ASSERT(value.isUndefined() || value.isString());
1052
args.rval().setBoolean(value.isString() && value.toString()->empty());
1053
return true;
1054
}
1055
1056
// get Intl.Locale.prototype.numeric
1057
static bool Locale_numeric(JSContext* cx, unsigned argc, Value* vp) {
1058
// Steps 1-2.
1059
CallArgs args = CallArgsFromVp(argc, vp);
1060
return CallNonGenericMethod<IsLocale, Locale_numeric>(cx, args);
1061
}
1062
1063
// get Intl.Locale.prototype.numberingSystem
1064
static bool Intl_Locale_numberingSystem(JSContext* cx, const CallArgs& args) {
1065
MOZ_ASSERT(IsLocale(args.thisv()));
1066
1067
// Step 3.
1068
auto* locale = &args.thisv().toObject().as<LocaleObject>();
1069
return GetUnicodeExtension(cx, locale, "nu", args.rval());
1070
}
1071
1072
// get Intl.Locale.prototype.numberingSystem
1073
static bool Locale_numberingSystem(JSContext* cx, unsigned argc, Value* vp) {
1074
// Steps 1-2.
1075
CallArgs args = CallArgsFromVp(argc, vp);
1076
return CallNonGenericMethod<IsLocale, Intl_Locale_numberingSystem>(cx, args);
1077
}
1078
1079
// get Intl.Locale.prototype.language
1080
static bool Locale_language(JSContext* cx, const CallArgs& args) {
1081
MOZ_ASSERT(IsLocale(args.thisv()));
1082
1083
// Step 3.
1084
auto* locale = &args.thisv().toObject().as<LocaleObject>();
1085
JSLinearString* baseName = locale->baseName()->ensureLinear(cx);
1086
if (!baseName) {
1087
return false;
1088
}
1089
1090
// Step 4 (Unnecessary assertion).
1091
1092
auto language = BaseNameParts(baseName).language;
1093
1094
size_t index = language.index;
1095
size_t length = language.length;
1096
1097
// Step 5.
1098
// FIXME: spec bug - not all production names updated.
1099
JSString* str = NewDependentString(cx, baseName, index, length);
1100
if (!str) {
1101
return false;
1102
}
1103
1104
args.rval().setString(str);
1105
return true;
1106
}
1107
1108
// get Intl.Locale.prototype.language
1109
static bool Locale_language(JSContext* cx, unsigned argc, Value* vp) {
1110
// Steps 1-2.
1111
CallArgs args = CallArgsFromVp(argc, vp);
1112
return CallNonGenericMethod<IsLocale, Locale_language>(cx, args);
1113
}
1114
1115
// get Intl.Locale.prototype.script
1116
static bool Locale_script(JSContext* cx, const CallArgs& args) {
1117
MOZ_ASSERT(IsLocale(args.thisv()));
1118
1119
// Step 3.
1120
auto* locale = &args.thisv().toObject().as<LocaleObject>();
1121
JSLinearString* baseName = locale->baseName()->ensureLinear(cx);
1122
if (!baseName) {
1123
return false;
1124
}
1125
1126
// Step 4 (Unnecessary assertion).
1127
1128
auto script = BaseNameParts(baseName).script;
1129
1130
// Step 5.
1131
// FIXME: spec bug - not all production names updated.
1132
if (!script) {
1133
args.rval().setUndefined();
1134
return true;
1135
}
1136
1137
size_t index = script->index;
1138
size_t length = script->length;
1139
1140
// Step 6.
1141
JSString* str = NewDependentString(cx, baseName, index, length);
1142
if (!str) {
1143
return false;
1144
}
1145
1146
args.rval().setString(str);
1147
return true;
1148
}
1149
1150
// get Intl.Locale.prototype.script
1151
static bool Locale_script(JSContext* cx, unsigned argc, Value* vp) {
1152
// Steps 1-2.
1153
CallArgs args = CallArgsFromVp(argc, vp);
1154
return CallNonGenericMethod<IsLocale, Locale_script>(cx, args);
1155
}
1156
1157
// get Intl.Locale.prototype.region
1158
static bool Locale_region(JSContext* cx, const CallArgs& args) {
1159
MOZ_ASSERT(IsLocale(args.thisv()));
1160
1161
// Step 3.
1162
auto* locale = &args.thisv().toObject().as<LocaleObject>();
1163
JSLinearString* baseName = locale->baseName()->ensureLinear(cx);
1164
if (!baseName) {
1165
return false;
1166
}
1167
1168
// Step 4 (Unnecessary assertion).
1169
1170
auto region = BaseNameParts(baseName).region;
1171
1172
// Step 5.
1173
if (!region) {
1174
args.rval().setUndefined();
1175
return true;
1176
}
1177
1178
size_t index = region->index;
1179
size_t length = region->length;
1180
1181
// Step 6.
1182
JSString* str = NewDependentString(cx, baseName, index, length);
1183
if (!str) {
1184
return false;
1185
}
1186
1187
args.rval().setString(str);
1188
return true;
1189
}
1190
1191
// get Intl.Locale.prototype.region
1192
static bool Locale_region(JSContext* cx, unsigned argc, Value* vp) {
1193
// Steps 1-2.
1194
CallArgs args = CallArgsFromVp(argc, vp);
1195
return CallNonGenericMethod<IsLocale, Locale_region>(cx, args);
1196
}
1197
1198
static bool Locale_toSource(JSContext* cx, unsigned argc, Value* vp) {
1199
CallArgs args = CallArgsFromVp(argc, vp);
1200
args.rval().setString(cx->names().Locale);
1201
return true;
1202
}
1203
1204
static const JSFunctionSpec locale_methods[] = {
1205
JS_FN("maximize", Locale_maximize, 0, 0),
1206
JS_FN("minimize", Locale_minimize, 0, 0),
1207
JS_FN(js_toString_str, Locale_toString, 0, 0),
1208
JS_FN(js_toSource_str, Locale_toSource, 0, 0), JS_FS_END};
1209
1210
static const JSPropertySpec locale_properties[] = {
1211
JS_PSG("baseName", Locale_baseName, 0),
1212
JS_PSG("calendar", Locale_calendar, 0),
1213
JS_PSG("collation", Locale_collation, 0),
1214
JS_PSG("hourCycle", Locale_hourCycle, 0),
1215
JS_PSG("caseFirst", Locale_caseFirst, 0),
1216
JS_PSG("numeric", Locale_numeric, 0),
1217
JS_PSG("numberingSystem", Locale_numberingSystem, 0),
1218
JS_PSG("language", Locale_language, 0),
1219
JS_PSG("script", Locale_script, 0),
1220
JS_PSG("region", Locale_region, 0),
1221
JS_STRING_SYM_PS(toStringTag, "Intl.Locale", JSPROP_READONLY),
1222
JS_PS_END};
1223
1224
const ClassSpec LocaleObject::classSpec_ = {
1225
GenericCreateConstructor<Locale, 1, gc::AllocKind::FUNCTION>,
1226
GenericCreatePrototype<LocaleObject>,
1227
nullptr,
1228
nullptr,
1229
locale_methods,
1230
locale_properties,
1231
nullptr,
1232
ClassSpec::DontDefineConstructor};
1233
1234
bool js::AddLocaleConstructor(JSContext* cx, JS::Handle<JSObject*> intl) {
1235
JSObject* ctor = GlobalObject::getOrCreateConstructor(cx, JSProto_Locale);
1236
if (!ctor) {
1237
return false;
1238
}
1239
1240
RootedValue ctorValue(cx, ObjectValue(*ctor));
1241
return DefineDataProperty(cx, intl, cx->names().Locale, ctorValue, 0);
1242
}
1243
1244
bool js::intl_ValidateAndCanonicalizeLanguageTag(JSContext* cx, unsigned argc,
1245
Value* vp) {
1246
CallArgs args = CallArgsFromVp(argc, vp);
1247
MOZ_ASSERT(args.length() == 2);
1248
1249
HandleValue tagValue = args[0];
1250
bool applyToString = args[1].toBoolean();
1251
1252
if (tagValue.isObject()) {
1253
JSString* tagStr;
1254
JS_TRY_VAR_OR_RETURN_FALSE(
1255
cx, tagStr,
1256
LanguageTagFromMaybeWrappedLocale(cx, &tagValue.toObject()));
1257
if (tagStr) {
1258
args.rval().setString(tagStr);
1259
return true;
1260
}
1261
}
1262
1263
if (!applyToString && !tagValue.isString()) {
1264
args.rval().setNull();
1265
return true;
1266
}
1267
1268
JSString* tagStr = ToString(cx, tagValue);
1269
if (!tagStr) {
1270
return false;
1271
}
1272
1273
RootedLinearString tagLinearStr(cx, tagStr->ensureLinear(cx));
1274
if (!tagLinearStr) {
1275
return false;
1276
}
1277
1278
// Handle the common case (a standalone language) first.
1279
// Only the following Unicode BCP 47 locale identifier subset is accepted:
1280
// unicode_locale_id = unicode_language_id
1281
// unicode_language_id = unicode_language_subtag
1282
// unicode_language_subtag = alpha{2,3}
1283
JSString* language;
1284
JS_TRY_VAR_OR_RETURN_FALSE(
1285
cx, language, intl::ParseStandaloneISO639LanguageTag(cx, tagLinearStr));
1286
if (language) {
1287
args.rval().setString(language);
1288
return true;
1289
}
1290
1291
LanguageTag tag(cx);
1292
if (!LanguageTagParser::parse(cx, tagLinearStr, tag)) {
1293
return false;
1294
}
1295
1296
if (!tag.canonicalize(cx, LanguageTag::UnicodeExtensionCanonicalForm::No)) {
1297
return false;
1298
}
1299
1300
JSString* resultStr = tag.toString(cx);
1301
if (!resultStr) {
1302
return false;
1303
}
1304
args.rval().setString(resultStr);
1305
return true;
1306
}
1307
1308
bool js::intl_TryValidateAndCanonicalizeLanguageTag(JSContext* cx,
1309
unsigned argc, Value* vp) {
1310
CallArgs args = CallArgsFromVp(argc, vp);
1311
MOZ_ASSERT(args.length() == 1);
1312
1313
RootedLinearString linear(cx, args[0].toString()->ensureLinear(cx));
1314
if (!linear) {
1315
return false;
1316
}
1317
1318
LanguageTag tag(cx);
1319
bool ok;
1320
JS_TRY_VAR_OR_RETURN_FALSE(cx, ok,
1321
LanguageTagParser::tryParse(cx, linear, tag));
1322
1323
// The caller handles invalid inputs.
1324
if (!ok) {
1325
args.rval().setNull();
1326
return true;
1327
}
1328
1329
if (!tag.canonicalize(cx, LanguageTag::UnicodeExtensionCanonicalForm::No)) {
1330
return false;
1331
}
1332
1333
JSString* resultStr = tag.toString(cx);
1334
if (!resultStr) {
1335
return false;
1336
}
1337
args.rval().setString(resultStr);
1338
return true;
1339
}
1340
1341
bool js::intl_ValidateAndCanonicalizeUnicodeExtensionType(JSContext* cx,
1342
unsigned argc,
1343
Value* vp) {
1344
CallArgs args = CallArgsFromVp(argc, vp);
1345
MOZ_ASSERT(args.length() == 2);
1346
1347
RootedLinearString unicodeType(cx, args[0].toString()->ensureLinear(cx));
1348
if (!unicodeType) {
1349
return false;
1350
}
1351
1352
RootedLinearString option(cx, args[1].toString()->ensureLinear(cx));
1353
if (!option) {
1354
return false;
1355
}
1356
1357
if (!IsValidUnicodeExtensionValue(unicodeType)) {
1358
UniqueChars optionChars = EncodeAscii(cx, option);
1359
if (!optionChars) {
1360
return false;
1361
}
1362
1363
UniqueChars unicodeTypeChars = QuoteString(cx, unicodeType, '"');
1364
if (!unicodeTypeChars) {
1365
return false;
1366
}
1367
1368
JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
1369
JSMSG_INVALID_OPTION_VALUE, optionChars.get(),
1370
unicodeTypeChars.get());
1371
return false;
1372
}
1373
1374
JSString* result = StringToLowerCase(cx, unicodeType);
1375
if (!result) {
1376
return false;
1377
}
1378
1379
args.rval().setString(result);
1380
return true;
1381
}