Fix exception scope verification failures in IteratorOperations.h.
[WebKit-https.git] / Source / JavaScriptCore / runtime / IntlObject.cpp
1 /*
2  * Copyright (C) 2015 Andy VanWagoner (thetalecrafter@gmail.com)
3  * Copyright (C) 2015 Sukolsak Sakshuwong (sukolsak@gmail.com)
4  * Copyright (C) 2016 Apple Inc. All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  * 2. Redistributions in binary form must reproduce the above copyright
12  *    notice, this list of conditions and the following disclaimer in the
13  *    documentation and/or other materials provided with the distribution.
14  *
15  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
16  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
17  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
19  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
25  * THE POSSIBILITY OF SUCH DAMAGE.
26  */
27
28 #include "config.h"
29 #include "IntlObject.h"
30
31 #if ENABLE(INTL)
32
33 #include "Error.h"
34 #include "FunctionPrototype.h"
35 #include "IntlCollator.h"
36 #include "IntlCollatorConstructor.h"
37 #include "IntlCollatorPrototype.h"
38 #include "IntlDateTimeFormat.h"
39 #include "IntlDateTimeFormatConstructor.h"
40 #include "IntlDateTimeFormatPrototype.h"
41 #include "IntlNumberFormat.h"
42 #include "IntlNumberFormatConstructor.h"
43 #include "IntlNumberFormatPrototype.h"
44 #include "JSCInlines.h"
45 #include "JSCJSValueInlines.h"
46 #include "Lookup.h"
47 #include "ObjectPrototype.h"
48 #include <unicode/uloc.h>
49 #include <unicode/unumsys.h>
50 #include <wtf/Assertions.h>
51 #include <wtf/NeverDestroyed.h>
52 #include <wtf/PlatformUserPreferredLanguages.h>
53 #include <wtf/text/StringBuilder.h>
54
55 namespace JSC {
56
57 STATIC_ASSERT_IS_TRIVIALLY_DESTRUCTIBLE(IntlObject);
58
59 static EncodedJSValue JSC_HOST_CALL intlObjectFuncGetCanonicalLocales(ExecState*);
60
61 }
62
63 namespace JSC {
64
65 struct MatcherResult {
66     String locale;
67     String extension;
68     size_t extensionIndex;
69 };
70
71 const ClassInfo IntlObject::s_info = { "Object", &Base::s_info, 0, CREATE_METHOD_TABLE(IntlObject) };
72
73 IntlObject::IntlObject(VM& vm, Structure* structure)
74     : JSNonFinalObject(vm, structure)
75 {
76 }
77
78 IntlObject* IntlObject::create(VM& vm, JSGlobalObject* globalObject, Structure* structure)
79 {
80     IntlObject* object = new (NotNull, allocateCell<IntlObject>(vm.heap)) IntlObject(vm, structure);
81     object->finishCreation(vm, globalObject);
82     return object;
83 }
84
85 void IntlObject::finishCreation(VM& vm, JSGlobalObject* globalObject)
86 {
87     Base::finishCreation(vm);
88     ASSERT(inherits(info()));
89
90     // Set up Collator.
91     IntlCollatorPrototype* collatorPrototype = IntlCollatorPrototype::create(vm, globalObject, IntlCollatorPrototype::createStructure(vm, globalObject, globalObject->objectPrototype()));
92     Structure* collatorStructure = IntlCollator::createStructure(vm, globalObject, collatorPrototype);
93     IntlCollatorConstructor* collatorConstructor = IntlCollatorConstructor::create(vm, IntlCollatorConstructor::createStructure(vm, globalObject, globalObject->functionPrototype()), collatorPrototype, collatorStructure);
94
95     collatorPrototype->putDirectWithoutTransition(vm, vm.propertyNames->constructor, collatorConstructor, DontEnum);
96
97     // Set up NumberFormat.
98     IntlNumberFormatPrototype* numberFormatPrototype = IntlNumberFormatPrototype::create(vm, globalObject, IntlNumberFormatPrototype::createStructure(vm, globalObject, globalObject->objectPrototype()));
99     Structure* numberFormatStructure = IntlNumberFormat::createStructure(vm, globalObject, numberFormatPrototype);
100     IntlNumberFormatConstructor* numberFormatConstructor = IntlNumberFormatConstructor::create(vm, IntlNumberFormatConstructor::createStructure(vm, globalObject, globalObject->functionPrototype()), numberFormatPrototype, numberFormatStructure);
101
102     numberFormatPrototype->putDirectWithoutTransition(vm, vm.propertyNames->constructor, numberFormatConstructor, DontEnum);
103
104     // Set up DateTimeFormat.
105     IntlDateTimeFormatPrototype* dateTimeFormatPrototype = IntlDateTimeFormatPrototype::create(vm, globalObject, IntlDateTimeFormatPrototype::createStructure(vm, globalObject, globalObject->objectPrototype()));
106     Structure* dateTimeFormatStructure = IntlDateTimeFormat::createStructure(vm, globalObject, dateTimeFormatPrototype);
107     IntlDateTimeFormatConstructor* dateTimeFormatConstructor = IntlDateTimeFormatConstructor::create(vm, IntlDateTimeFormatConstructor::createStructure(vm, globalObject, globalObject->functionPrototype()), dateTimeFormatPrototype, dateTimeFormatStructure);
108
109     dateTimeFormatPrototype->putDirectWithoutTransition(vm, vm.propertyNames->constructor, dateTimeFormatConstructor, DontEnum);
110
111     // 8.1 Properties of the Intl Object (ECMA-402 2.0)
112     putDirectWithoutTransition(vm, vm.propertyNames->Collator, collatorConstructor, DontEnum);
113     putDirectWithoutTransition(vm, vm.propertyNames->NumberFormat, numberFormatConstructor, DontEnum);
114     putDirectWithoutTransition(vm, vm.propertyNames->DateTimeFormat, dateTimeFormatConstructor, DontEnum);
115
116     // 8.2 Function Properties of the Intl Object
117     // https://tc39.github.io/ecma402/#sec-function-properties-of-the-intl-object
118     putDirectNativeFunction(vm, globalObject, Identifier::fromString(&vm, "getCanonicalLocales"), 1, intlObjectFuncGetCanonicalLocales, NoIntrinsic, DontEnum);
119 }
120
121 Structure* IntlObject::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype)
122 {
123     return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info());
124 }
125
126 void convertICULocaleToBCP47LanguageTag(String& locale)
127 {
128     locale.replace('_', '-');
129 }
130
131 bool intlBooleanOption(ExecState& state, JSValue options, PropertyName property, bool& usesFallback)
132 {
133     VM& vm = state.vm();
134     auto scope = DECLARE_THROW_SCOPE(vm);
135
136     // 9.2.9 GetOption (options, property, type, values, fallback)
137     // For type="boolean". values is always undefined.
138
139     // 1. Let opts be ToObject(options).
140     JSObject* opts = options.toObject(&state);
141
142     // 2. ReturnIfAbrupt(opts).
143     RETURN_IF_EXCEPTION(scope, false);
144
145     // 3. Let value be Get(opts, property).
146     JSValue value = opts->get(&state, property);
147
148     // 4. ReturnIfAbrupt(value).
149     RETURN_IF_EXCEPTION(scope, false);
150
151     // 5. If value is not undefined, then
152     if (!value.isUndefined()) {
153         // a. Assert: type is "boolean" or "string".
154         // Function dedicated to "boolean".
155
156         // b. If type is "boolean", then
157         // i. Let value be ToBoolean(value).
158         bool booleanValue = value.toBoolean(&state);
159
160         // e. Return value.
161         usesFallback = false;
162         return booleanValue;
163     }
164
165     // 6. Else return fallback.
166     // Because fallback can be undefined, we let the caller handle it instead.
167     usesFallback = true;
168     return false;
169 }
170
171 String intlStringOption(ExecState& state, JSValue options, PropertyName property, std::initializer_list<const char*> values, const char* notFound, const char* fallback)
172 {
173     VM& vm = state.vm();
174     auto scope = DECLARE_THROW_SCOPE(vm);
175
176     // 9.2.9 GetOption (options, property, type, values, fallback)
177     // For type="string".
178
179     // 1. Let opts be ToObject(options).
180     JSObject* opts = options.toObject(&state);
181
182     // 2. ReturnIfAbrupt(opts).
183     RETURN_IF_EXCEPTION(scope, String());
184
185     // 3. Let value be Get(opts, property).
186     JSValue value = opts->get(&state, property);
187
188     // 4. ReturnIfAbrupt(value).
189     RETURN_IF_EXCEPTION(scope, String());
190
191     // 5. If value is not undefined, then
192     if (!value.isUndefined()) {
193         // a. Assert: type is "boolean" or "string".
194         // Function dedicated to "string".
195
196         // c. If type is "string", then
197         // i. Let value be ToString(value).
198         String stringValue = value.toWTFString(&state);
199
200         // ii. ReturnIfAbrupt(value).
201         RETURN_IF_EXCEPTION(scope, String());
202
203         // d. If values is not undefined, then
204         // i. If values does not contain an element equal to value, throw a RangeError exception.
205         if (values.size() && std::find(values.begin(), values.end(), stringValue) == values.end()) {
206             throwException(&state, scope, createRangeError(&state, notFound));
207             return { };
208         }
209
210         // e. Return value.
211         return stringValue;
212     }
213
214     // 6. Else return fallback.
215     return fallback;
216 }
217
218 unsigned intlNumberOption(ExecState& state, JSValue options, PropertyName property, unsigned minimum, unsigned maximum, unsigned fallback)
219 {
220     VM& vm = state.vm();
221     auto scope = DECLARE_THROW_SCOPE(vm);
222
223     // 9.2.9 GetNumberOption (options, property, minimum, maximum, fallback) (ECMA-402 2.0)
224     // 1. Let opts be ToObject(options).
225     JSObject* opts = options.toObject(&state);
226
227     // 2. ReturnIfAbrupt(opts).
228     RETURN_IF_EXCEPTION(scope, 0);
229
230     // 3. Let value be Get(opts, property).
231     JSValue value = opts->get(&state, property);
232
233     // 4. ReturnIfAbrupt(value).
234     RETURN_IF_EXCEPTION(scope, 0);
235
236     // 5. If value is not undefined, then
237     if (!value.isUndefined()) {
238         // a. Let value be ToNumber(value).
239         double doubleValue = value.toNumber(&state);
240         // b. ReturnIfAbrupt(value).
241         RETURN_IF_EXCEPTION(scope, 0);
242         // 1. If value is NaN or less than minimum or greater than maximum, throw a RangeError exception.
243         if (!(doubleValue >= minimum && doubleValue <= maximum)) {
244             throwException(&state, scope, createRangeError(&state, *property.publicName() + " is out of range"));
245             return 0;
246         }
247
248         // c. Return floor(value).
249         return static_cast<unsigned>(doubleValue);
250     }
251
252     // 6. Else return fallback.
253     return fallback;
254 }
255
256 static String privateUseLangTag(const Vector<String>& parts, size_t startIndex)
257 {
258     size_t numParts = parts.size();
259     size_t currentIndex = startIndex;
260
261     // Check for privateuse.
262     // privateuse = "x" 1*("-" (1*8alphanum))
263     StringBuilder privateuse;
264     while (currentIndex < numParts) {
265         const String& singleton = parts[currentIndex];
266         unsigned singletonLength = singleton.length();
267         bool isValid = (singletonLength == 1 && (singleton == "x" || singleton == "X"));
268         if (!isValid)
269             break;
270
271         if (currentIndex != startIndex)
272             privateuse.append('-');
273
274         ++currentIndex;
275         unsigned numExtParts = 0;
276         privateuse.append('x');
277         while (currentIndex < numParts) {
278             const String& extPart = parts[currentIndex];
279             unsigned extPartLength = extPart.length();
280
281             bool isValid = (extPartLength >= 1 && extPartLength <= 8 && extPart.isAllSpecialCharacters<isASCIIAlphanumeric>());
282             if (!isValid)
283                 break;
284
285             ++currentIndex;
286             ++numExtParts;
287             privateuse.append('-');
288             privateuse.append(extPart.convertToASCIILowercase());
289         }
290
291         // Requires at least one production.
292         if (!numExtParts)
293             return String();
294     }
295
296     // Leftovers makes it invalid.
297     if (currentIndex < numParts)
298         return String();
299
300     return privateuse.toString();
301 }
302
303 static String canonicalLangTag(const Vector<String>& parts)
304 {
305     ASSERT(!parts.isEmpty());
306
307     // Follows the grammar at https://www.rfc-editor.org/rfc/bcp/bcp47.txt
308     // langtag = language ["-" script] ["-" region] *("-" variant) *("-" extension) ["-" privateuse]
309
310     size_t numParts = parts.size();
311     // Check for language.
312     // language = 2*3ALPHA ["-" extlang] / 4ALPHA / 5*8ALPHA
313     size_t currentIndex = 0;
314     const String& language = parts[currentIndex];
315     unsigned languageLength = language.length();
316     bool canHaveExtlang = languageLength >= 2 && languageLength <= 3;
317     bool isValidLanguage = languageLength >= 2 && languageLength <= 8 && language.isAllSpecialCharacters<isASCIIAlpha>();
318     if (!isValidLanguage)
319         return String();
320
321     ++currentIndex;
322     StringBuilder canonical;
323     canonical.append(language.convertToASCIILowercase());
324
325     // Check for extlang.
326     // extlang = 3ALPHA *2("-" 3ALPHA)
327     if (canHaveExtlang) {
328         for (unsigned times = 0; times < 3 && currentIndex < numParts; ++times) {
329             const String& extlang = parts[currentIndex];
330             unsigned extlangLength = extlang.length();
331             if (extlangLength == 3 && extlang.isAllSpecialCharacters<isASCIIAlpha>()) {
332                 ++currentIndex;
333                 canonical.append('-');
334                 canonical.append(extlang.convertToASCIILowercase());
335             } else
336                 break;
337         }
338     }
339
340     // Check for script.
341     // script = 4ALPHA
342     if (currentIndex < numParts) {
343         const String& script = parts[currentIndex];
344         unsigned scriptLength = script.length();
345         if (scriptLength == 4 && script.isAllSpecialCharacters<isASCIIAlpha>()) {
346             ++currentIndex;
347             canonical.append('-');
348             canonical.append(toASCIIUpper(script[0]));
349             canonical.append(script.substring(1, 3).convertToASCIILowercase());
350         }
351     }
352
353     // Check for region.
354     // region = 2ALPHA / 3DIGIT
355     if (currentIndex < numParts) {
356         const String& region = parts[currentIndex];
357         unsigned regionLength = region.length();
358         bool isValidRegion = (
359             (regionLength == 2 && region.isAllSpecialCharacters<isASCIIAlpha>())
360             || (regionLength == 3 && region.isAllSpecialCharacters<isASCIIDigit>())
361         );
362         if (isValidRegion) {
363             ++currentIndex;
364             canonical.append('-');
365             canonical.append(region.convertToASCIIUppercase());
366         }
367     }
368
369     // Check for variant.
370     // variant = 5*8alphanum / (DIGIT 3alphanum)
371     HashSet<String> subtags;
372     while (currentIndex < numParts) {
373         const String& variant = parts[currentIndex];
374         unsigned variantLength = variant.length();
375         bool isValidVariant = (
376             (variantLength >= 5 && variantLength <= 8 && variant.isAllSpecialCharacters<isASCIIAlphanumeric>())
377             || (variantLength == 4 && isASCIIDigit(variant[0]) && variant.substring(1, 3).isAllSpecialCharacters<isASCIIAlphanumeric>())
378         );
379         if (!isValidVariant)
380             break;
381
382         // Cannot include duplicate subtags (case insensitive).
383         String lowerVariant = variant.convertToASCIILowercase();
384         if (!subtags.add(lowerVariant).isNewEntry)
385             return String();
386
387         ++currentIndex;
388
389         // Reordering variant subtags is not required in the spec.
390         canonical.append('-');
391         canonical.append(lowerVariant);
392     }
393
394     // Check for extension.
395     // extension = singleton 1*("-" (2*8alphanum))
396     // singleton = alphanum except x or X
397     subtags.clear();
398     Vector<String> extensions;
399     while (currentIndex < numParts) {
400         const String& possibleSingleton = parts[currentIndex];
401         unsigned singletonLength = possibleSingleton.length();
402         bool isValidSingleton = (singletonLength == 1 && possibleSingleton != "x" && possibleSingleton != "X" && isASCIIAlphanumeric(possibleSingleton[0]));
403         if (!isValidSingleton)
404             break;
405
406         // Cannot include duplicate singleton (case insensitive).
407         String singleton = possibleSingleton.convertToASCIILowercase();
408         if (!subtags.add(singleton).isNewEntry)
409             return String();
410
411         ++currentIndex;
412         int numExtParts = 0;
413         StringBuilder extension;
414         extension.append(singleton);
415         while (currentIndex < numParts) {
416             const String& extPart = parts[currentIndex];
417             unsigned extPartLength = extPart.length();
418
419             bool isValid = (extPartLength >= 2 && extPartLength <= 8 && extPart.isAllSpecialCharacters<isASCIIAlphanumeric>());
420             if (!isValid)
421                 break;
422
423             ++currentIndex;
424             ++numExtParts;
425             extension.append('-');
426             extension.append(extPart.convertToASCIILowercase());
427         }
428
429         // Requires at least one production.
430         if (!numExtParts)
431             return String();
432
433         extensions.append(extension.toString());
434     }
435
436     // Add extensions to canonical sorted by singleton.
437     std::sort(
438         extensions.begin(),
439         extensions.end(),
440         [] (const String& a, const String& b) -> bool {
441             return a[0] < b[0];
442         }
443     );
444     size_t numExtenstions = extensions.size();
445     for (size_t i = 0; i < numExtenstions; ++i) {
446         canonical.append('-');
447         canonical.append(extensions[i]);
448     }
449
450     // Check for privateuse.
451     if (currentIndex < numParts) {
452         String privateuse = privateUseLangTag(parts, currentIndex);
453         if (privateuse.isNull())
454             return String();
455         canonical.append('-');
456         canonical.append(privateuse);
457     }
458
459     // FIXME: Replace subtags with their preferred values.
460
461     return canonical.toString();
462 }
463
464 static String grandfatheredLangTag(const String& locale)
465 {
466     // grandfathered = irregular / regular
467     // FIXME: convert to a compile time hash table if this is causing performance issues.
468     HashMap<String, String> tagMap = {
469         // Irregular.
470         { ASCIILiteral("en-gb-oed"), ASCIILiteral("en-GB-oed") },
471         { ASCIILiteral("i-ami"), ASCIILiteral("ami") },
472         { ASCIILiteral("i-bnn"), ASCIILiteral("bnn") },
473         { ASCIILiteral("i-default"), ASCIILiteral("i-default") },
474         { ASCIILiteral("i-enochian"), ASCIILiteral("i-enochian") },
475         { ASCIILiteral("i-hak"), ASCIILiteral("hak") },
476         { ASCIILiteral("i-klingon"), ASCIILiteral("tlh") },
477         { ASCIILiteral("i-lux"), ASCIILiteral("lb") },
478         { ASCIILiteral("i-mingo"), ASCIILiteral("i-mingo") },
479         { ASCIILiteral("i-navajo"), ASCIILiteral("nv") },
480         { ASCIILiteral("i-pwn"), ASCIILiteral("pwn") },
481         { ASCIILiteral("i-tao"), ASCIILiteral("tao") },
482         { ASCIILiteral("i-tay"), ASCIILiteral("tay") },
483         { ASCIILiteral("i-tsu"), ASCIILiteral("tsu") },
484         { ASCIILiteral("sgn-be-fr"), ASCIILiteral("sfb") },
485         { ASCIILiteral("sgn-be-nl"), ASCIILiteral("vgt") },
486         { ASCIILiteral("sgn-ch-de"), ASCIILiteral("sgg") },
487         // Regular.
488         { ASCIILiteral("art-lojban"), ASCIILiteral("jbo") },
489         { ASCIILiteral("cel-gaulish"), ASCIILiteral("cel-gaulish") },
490         { ASCIILiteral("no-bok"), ASCIILiteral("nb") },
491         { ASCIILiteral("no-nyn"), ASCIILiteral("nn") },
492         { ASCIILiteral("zh-guoyu"), ASCIILiteral("cmn") },
493         { ASCIILiteral("zh-hakka"), ASCIILiteral("hak") },
494         { ASCIILiteral("zh-min"), ASCIILiteral("zh-min") },
495         { ASCIILiteral("zh-min-nan"), ASCIILiteral("nan") },
496         { ASCIILiteral("zh-xiang"), ASCIILiteral("hsn") }
497     };
498
499     return tagMap.get(locale.convertToASCIILowercase());
500 }
501
502 static String canonicalizeLanguageTag(const String& locale)
503 {
504     // 6.2.2 IsStructurallyValidLanguageTag (locale)
505     // 6.2.3 CanonicalizeLanguageTag (locale)
506     // These are done one after another in CanonicalizeLocaleList, so they are combined here to reduce duplication.
507     // https://www.rfc-editor.org/rfc/bcp/bcp47.txt
508
509     // Language-Tag = langtag / privateuse / grandfathered
510     String grandfather = grandfatheredLangTag(locale);
511     if (!grandfather.isNull())
512         return grandfather;
513
514     // FIXME: Replace redundant tags [RFC4647].
515
516     Vector<String> parts;
517     locale.split('-', true, parts);
518     if (!parts.isEmpty()) {
519         String langtag = canonicalLangTag(parts);
520         if (!langtag.isNull())
521             return langtag;
522
523         String privateuse = privateUseLangTag(parts, 0);
524         if (!privateuse.isNull())
525             return privateuse;
526     }
527
528     return String();
529 }
530
531 Vector<String> canonicalizeLocaleList(ExecState& state, JSValue locales)
532 {
533     // 9.2.1 CanonicalizeLocaleList (locales)
534     VM& vm = state.vm();
535     auto scope = DECLARE_THROW_SCOPE(vm);
536
537     JSGlobalObject* globalObject = state.callee()->globalObject();
538     Vector<String> seen;
539
540     // 1. If locales is undefined, then a. Return a new empty List.
541     if (locales.isUndefined())
542         return seen;
543
544     // 2. Let seen be an empty List.
545     // Done before to also return in step 1, if needed.
546
547     // 3. If Type(locales) is String, then
548     JSObject* localesObject;
549     if (locales.isString()) {
550         //  a. Let aLocales be CreateArrayFromList(«locales»).
551         JSArray* localesArray = JSArray::tryCreateUninitialized(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous), 1);
552         localesArray->initializeIndex(vm, 0, locales);
553         // 4. Let O be ToObject(aLocales).
554         localesObject = localesArray;
555     } else {
556         // 4. Let O be ToObject(aLocales).
557         localesObject = locales.toObject(&state);
558     }
559
560     // 5. ReturnIfAbrupt(O).
561     RETURN_IF_EXCEPTION(scope, Vector<String>());
562
563     // 6. Let len be ToLength(Get(O, "length")).
564     JSValue lengthProperty = localesObject->get(&state, vm.propertyNames->length);
565     RETURN_IF_EXCEPTION(scope, Vector<String>());
566
567     double length = lengthProperty.toLength(&state);
568     RETURN_IF_EXCEPTION(scope, Vector<String>());
569
570     // Keep track of locales that have been added to the list.
571     HashSet<String> seenSet;
572
573     // 7. Let k be 0.
574     // 8. Repeat, while k < len
575     for (double k = 0; k < length; ++k) {
576         // a. Let Pk be ToString(k).
577         // Not needed because hasProperty and get take an int for numeric key.
578
579         // b. Let kPresent be HasProperty(O, Pk).
580         bool kPresent = localesObject->hasProperty(&state, k);
581
582         // c. ReturnIfAbrupt(kPresent).
583         RETURN_IF_EXCEPTION(scope, Vector<String>());
584
585         // d. If kPresent is true, then
586         if (kPresent) {
587             // i. Let kValue be Get(O, Pk).
588             JSValue kValue = localesObject->get(&state, k);
589
590             // ii. ReturnIfAbrupt(kValue).
591             RETURN_IF_EXCEPTION(scope, Vector<String>());
592
593             // iii. If Type(kValue) is not String or Object, throw a TypeError exception.
594             if (!kValue.isString() && !kValue.isObject()) {
595                 throwTypeError(&state, scope, ASCIILiteral("locale value must be a string or object"));
596                 return Vector<String>();
597             }
598
599             // iv. Let tag be ToString(kValue).
600             JSString* tag = kValue.toString(&state);
601
602             // v. ReturnIfAbrupt(tag).
603             RETURN_IF_EXCEPTION(scope, Vector<String>());
604
605             // vi. If IsStructurallyValidLanguageTag(tag) is false, throw a RangeError exception.
606             // vii. Let canonicalizedTag be CanonicalizeLanguageTag(tag).
607             String canonicalizedTag = canonicalizeLanguageTag(tag->value(&state));
608             if (canonicalizedTag.isNull()) {
609                 throwException(&state, scope, createRangeError(&state, String::format("invalid language tag: %s", tag->value(&state).utf8().data())));
610                 return Vector<String>();
611             }
612
613             // viii. If canonicalizedTag is not an element of seen, append canonicalizedTag as the last element of seen.
614             if (seenSet.add(canonicalizedTag).isNewEntry)
615                 seen.append(canonicalizedTag);
616         }
617         // e. Increase k by 1.
618     }
619
620     return seen;
621 }
622
623 String bestAvailableLocale(const HashSet<String>& availableLocales, const String& locale)
624 {
625     // 9.2.2 BestAvailableLocale (availableLocales, locale)
626     // 1. Let candidate be locale.
627     String candidate = locale;
628
629     // 2. Repeat
630     while (!candidate.isEmpty()) {
631         // a. If availableLocales contains an element equal to candidate, then return candidate.
632         if (availableLocales.contains(candidate))
633             return candidate;
634
635         // b. Let pos be the character index of the last occurrence of "-" (U+002D) within candidate. If that character does not occur, return undefined.
636         size_t pos = candidate.reverseFind('-');
637         if (pos == notFound)
638             return String();
639
640         // c. If pos ≥ 2 and the character "-" occurs at index pos-2 of candidate, then decrease pos by 2.
641         if (pos >= 2 && candidate[pos - 2] == '-')
642             pos -= 2;
643
644         // d. Let candidate be the substring of candidate from position 0, inclusive, to position pos, exclusive.
645         candidate = candidate.substring(0, pos);
646     }
647
648     return String();
649 }
650
651 String defaultLocale(ExecState& state)
652 {
653     // 6.2.4 DefaultLocale ()
654     
655     // WebCore's global objects will have their own ideas of how to determine the language. It may
656     // be determined by WebCore-specific logic like some WK settings. Usually this will return the
657     // same thing as platformUserPreferredLanguages()[0].
658     if (auto defaultLanguage = state.callee()->globalObject()->globalObjectMethodTable()->defaultLanguage) {
659         String locale = defaultLanguage();
660         if (!locale.isEmpty())
661             return canonicalizeLanguageTag(locale);
662     }
663     
664     // If WebCore isn't around to tell us how to get the language then fall back to our own way of
665     // doing it, which mostly follows what WebCore would have done.
666     Vector<String> languages = platformUserPreferredLanguages();
667     if (!languages.isEmpty() && !languages[0].isEmpty())
668         return canonicalizeLanguageTag(languages[0]);
669     
670     // If all else fails, ask ICU. It will probably say something bogus like en_us even if the user
671     // has configured some other language, but being wrong is better than crashing.
672     String locale = uloc_getDefault();
673     convertICULocaleToBCP47LanguageTag(locale);
674     return locale;
675 }
676
677 String removeUnicodeLocaleExtension(const String& locale)
678 {
679     Vector<String> parts;
680     locale.split('-', parts);
681     StringBuilder builder;
682     size_t partsSize = parts.size();
683     if (partsSize > 0)
684         builder.append(parts[0]);
685     for (size_t p = 1; p < partsSize; ++p) {
686         if (parts[p] == "u") {
687             // Skip the u- and anything that follows until another singleton.
688             // While the next part is part of the unicode extension, skip it.
689             while (p + 1 < partsSize && parts[p + 1].length() > 1)
690                 ++p;
691         } else {
692             builder.append('-');
693             builder.append(parts[p]);
694         }
695     }
696     return builder.toString();
697 }
698
699 static MatcherResult lookupMatcher(ExecState& state, const HashSet<String>& availableLocales, const Vector<String>& requestedLocales)
700 {
701     // 9.2.3 LookupMatcher (availableLocales, requestedLocales) (ECMA-402 2.0)
702     String locale;
703     String noExtensionsLocale;
704     String availableLocale;
705     for (size_t i = 0; i < requestedLocales.size() && availableLocale.isNull(); ++i) {
706         locale = requestedLocales[i];
707         noExtensionsLocale = removeUnicodeLocaleExtension(locale);
708         availableLocale = bestAvailableLocale(availableLocales, noExtensionsLocale);
709     }
710
711     MatcherResult result;
712     if (!availableLocale.isNull()) {
713         result.locale = availableLocale;
714         if (locale != noExtensionsLocale) {
715             // i. Let extension be the String value consisting of the first substring of locale that is a Unicode locale extension sequence.
716             // ii. Let extensionIndex be the character position of the initial "-" extension sequence within locale.
717             size_t extensionIndex = locale.find("-u-");
718             RELEASE_ASSERT(extensionIndex != notFound);
719
720             size_t extensionLength = locale.length() - extensionIndex;
721             size_t end = extensionIndex + 3;
722             while (end < locale.length()) {
723                 end = locale.find('-', end);
724                 if (end == notFound)
725                     break;
726                 if (end + 2 < locale.length() && locale[end + 2] == '-') {
727                     extensionLength = end - extensionIndex;
728                     break;
729                 }
730                 end++;
731             }
732             result.extension = locale.substring(extensionIndex, extensionLength);
733             result.extensionIndex = extensionIndex;
734         }
735     } else
736         result.locale = defaultLocale(state);
737     return result;
738 }
739
740 static MatcherResult bestFitMatcher(ExecState& state, const HashSet<String>& availableLocales, const Vector<String>& requestedLocales)
741 {
742     // 9.2.4 BestFitMatcher (availableLocales, requestedLocales) (ECMA-402 2.0)
743     // FIXME: Implement something better than lookup.
744     return lookupMatcher(state, availableLocales, requestedLocales);
745 }
746
747 HashMap<String, String> resolveLocale(ExecState& state, const HashSet<String>& availableLocales, const Vector<String>& requestedLocales, const HashMap<String, String>& options, const char* const relevantExtensionKeys[], size_t relevantExtensionKeyCount, Vector<String> (*localeData)(const String&, size_t))
748 {
749     // 9.2.5 ResolveLocale (availableLocales, requestedLocales, options, relevantExtensionKeys, localeData) (ECMA-402 2.0)
750     // 1. Let matcher be the value of options.[[localeMatcher]].
751     const String& matcher = options.get(ASCIILiteral("localeMatcher"));
752
753     // 2. If matcher is "lookup", then
754     MatcherResult (*matcherOperation)(ExecState&, const HashSet<String>&, const Vector<String>&);
755     if (matcher == "lookup") {
756         // a. Let MatcherOperation be the abstract operation LookupMatcher.
757         matcherOperation = lookupMatcher;
758     } else { // 3. Else
759         // a. Let MatcherOperation be the abstract operation BestFitMatcher.
760         matcherOperation = bestFitMatcher;
761     }
762
763     // 4. Let r be MatcherOperation(availableLocales, requestedLocales).
764     MatcherResult matcherResult = matcherOperation(state, availableLocales, requestedLocales);
765
766     // 5. Let foundLocale be the value of r.[[locale]].
767     String foundLocale = matcherResult.locale;
768
769     // 6. If r has an [[extension]] field, then
770     Vector<String> extensionSubtags;
771     if (!matcherResult.extension.isNull()) {
772         // a. Let extension be the value of r.[[extension]].
773         // b. Let extensionIndex be the value of r.[[extensionIndex]].
774         // c. Let extensionSubtags be Call(%StringProto_split%, extension, «"-"») .
775         // d. Let extensionSubtagsLength be Get(CreateArrayFromList(extensionSubtags), "length").
776         matcherResult.extension.split('-', extensionSubtags);
777     }
778
779     // 7. Let result be a new Record.
780     HashMap<String, String> result;
781
782     // 8. Set result.[[dataLocale]] to foundLocale.
783     result.add(ASCIILiteral("dataLocale"), foundLocale);
784
785     // 9. Let supportedExtension be "-u".
786     String supportedExtension = ASCIILiteral("-u");
787
788     // 10. Let k be 0.
789     // 11. Let rExtensionKeys be ToObject(CreateArrayFromList(relevantExtensionKeys)).
790     // 12. ReturnIfAbrupt(rExtensionKeys).
791     // 13. Let len be ToLength(Get(rExtensionKeys, "length")).
792     // 14. Repeat while k < len
793     for (size_t keyIndex = 0; keyIndex < relevantExtensionKeyCount; ++keyIndex) {
794         // a. Let key be Get(rExtensionKeys, ToString(k)).
795         // b. ReturnIfAbrupt(key).
796         const char* key = relevantExtensionKeys[keyIndex];
797
798         // c. Let foundLocaleData be Get(localeData, foundLocale).
799         // d. ReturnIfAbrupt(foundLocaleData).
800         // e. Let keyLocaleData be ToObject(Get(foundLocaleData, key)).
801         // f. ReturnIfAbrupt(keyLocaleData).
802         Vector<String> keyLocaleData = localeData(foundLocale, keyIndex);
803
804         // g. Let value be ToString(Get(keyLocaleData, "0")).
805         // h. ReturnIfAbrupt(value).
806         ASSERT(!keyLocaleData.isEmpty());
807         String value = keyLocaleData[0];
808
809         // i. Let supportedExtensionAddition be "".
810         String supportedExtensionAddition;
811
812         // j. If extensionSubtags is not undefined, then
813         if (!extensionSubtags.isEmpty()) {
814             // i. Let keyPos be Call(%ArrayProto_indexOf%, extensionSubtags, «key») .
815             size_t keyPos = extensionSubtags.find(key);
816             // ii. If keyPos != -1, then
817             if (keyPos != notFound) {
818                 // FIXME: https://github.com/tc39/ecma402/issues/59
819                 // 1. If keyPos + 1 < extensionSubtagsLength and the length of the result of Get(extensionSubtags, ToString(keyPos +1)) is greater than 2, then
820                 if (keyPos + 1 < extensionSubtags.size() && extensionSubtags[keyPos + 1].length() > 2) {
821                     const String& requestedValue = extensionSubtags[keyPos + 1];
822                     if (keyLocaleData.contains(requestedValue)) {
823                         value = requestedValue;
824                         supportedExtensionAddition = makeString('-', key, '-', value);
825                     }
826                 } else if (keyLocaleData.contains(static_cast<String>(ASCIILiteral("true")))) {
827                     // 2. Else, if the result of Call(%StringProto_includes%, keyLocaleData, «"true"») is true, then
828                     value = ASCIILiteral("true");
829                 }
830             }
831         }
832
833         // k. If options has a field [[<key>]], then
834         HashMap<String, String>::const_iterator iterator = options.find(key);
835         if (iterator != options.end()) {
836             // i. Let optionsValue be the value of ToString(options.[[<key>]]).
837             // ii. ReturnIfAbrupt(optionsValue).
838             const String& optionsValue = iterator->value;
839             // iii. If the result of Call(%StringProto_includes%, keyLocaleData, «optionsValue») is true, then
840             if (!optionsValue.isNull() && keyLocaleData.contains(optionsValue)) {
841                 // 1. If optionsValue is not equal to value, then
842                 if (optionsValue != value) {
843                     value = optionsValue;
844                     supportedExtensionAddition = String();
845                 }
846             }
847         }
848
849         // l. Set result.[[<key>]] to value.
850         result.add(key, value);
851
852         // m. Append supportedExtensionAddition to supportedExtension.
853         supportedExtension.append(supportedExtensionAddition);
854
855         // n. Increase k by 1.
856     }
857
858     // 15. If the number of elements in supportedExtension is greater than 2, then
859     if (supportedExtension.length() > 2) {
860         // a. Let preExtension be the substring of foundLocale from position 0, inclusive, to position extensionIndex, exclusive.
861         // b. Let postExtension be the substring of foundLocale from position extensionIndex to the end of the string.
862         // c. Let foundLocale be the concatenation of preExtension, supportedExtension, and postExtension.
863         String preExtension = foundLocale.substring(0, matcherResult.extensionIndex);
864         String postExtension = foundLocale.substring(matcherResult.extensionIndex);
865         foundLocale = preExtension + supportedExtension + postExtension;
866     }
867
868     // 16. Set result.[[locale]] to foundLocale.
869     result.add(ASCIILiteral("locale"), foundLocale);
870
871     // 17. Return result.
872     return result;
873 }
874
875 static JSArray* lookupSupportedLocales(ExecState& state, const HashSet<String>& availableLocales, const Vector<String>& requestedLocales)
876 {
877     VM& vm = state.vm();
878     auto scope = DECLARE_THROW_SCOPE(vm);
879
880     // 9.2.6 LookupSupportedLocales (availableLocales, requestedLocales)
881
882     // 1. Let rLocales be CreateArrayFromList(requestedLocales).
883     // Already an array.
884
885     // 2. Let len be ToLength(Get(rLocales, "length")).
886     size_t len = requestedLocales.size();
887
888     // 3. Let subset be an empty List.
889     JSGlobalObject* globalObject = state.callee()->globalObject();
890     JSArray* subset = JSArray::tryCreateUninitialized(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithUndecided), 0);
891     if (!subset) {
892         throwOutOfMemoryError(&state, scope);
893         return nullptr;
894     }
895
896     // 4. Let k be 0.
897     // 5. Repeat while k < len
898     for (size_t k = 0; k < len; ++k) {
899         // a. Let Pk be ToString(k).
900         // b. Let locale be Get(rLocales, Pk).
901         // c. ReturnIfAbrupt(locale).
902         const String& locale = requestedLocales[k];
903
904         // d. Let noExtensionsLocale be the String value that is locale with all Unicode locale extension sequences removed.
905         String noExtensionsLocale = removeUnicodeLocaleExtension(locale);
906
907         // e. Let availableLocale be BestAvailableLocale(availableLocales, noExtensionsLocale).
908         String availableLocale = bestAvailableLocale(availableLocales, noExtensionsLocale);
909
910         // f. If availableLocale is not undefined, then append locale to the end of subset.
911         if (!availableLocale.isNull())
912             subset->push(&state, jsString(&state, locale));
913
914         // g. Increment k by 1.
915     }
916
917     // 6. Return subset.
918     return subset;
919 }
920
921 static JSArray* bestFitSupportedLocales(ExecState& state, const HashSet<String>& availableLocales, const Vector<String>& requestedLocales)
922 {
923     // 9.2.7 BestFitSupportedLocales (availableLocales, requestedLocales)
924     // FIXME: Implement something better than lookup.
925     return lookupSupportedLocales(state, availableLocales, requestedLocales);
926 }
927
928 JSValue supportedLocales(ExecState& state, const HashSet<String>& availableLocales, const Vector<String>& requestedLocales, JSValue options)
929 {
930     // 9.2.8 SupportedLocales (availableLocales, requestedLocales, options)
931     VM& vm = state.vm();
932     auto scope = DECLARE_THROW_SCOPE(vm);
933     String matcher;
934
935     // 1. If options is not undefined, then
936     if (!options.isUndefined()) {
937         // a. Let matcher be GetOption(options, "localeMatcher", "string", « "lookup", "best fit" », "best fit").
938         matcher = intlStringOption(state, options, vm.propertyNames->localeMatcher, { "lookup", "best fit" }, "localeMatcher must be either \"lookup\" or \"best fit\"", "best fit");
939         // b. ReturnIfAbrupt(matcher).
940         RETURN_IF_EXCEPTION(scope, JSValue());
941     } else {
942         // 2. Else, let matcher be "best fit".
943         matcher = ASCIILiteral("best fit");
944     }
945
946     JSArray* supportedLocales;
947     // 3. If matcher is "best fit",
948     if (matcher == "best fit") {
949         // a. Let MatcherOperation be the abstract operation BestFitSupportedLocales.
950         // 5. Let supportedLocales be MatcherOperation(availableLocales, requestedLocales).
951         supportedLocales = bestFitSupportedLocales(state, availableLocales, requestedLocales);
952     } else {
953         // 4. Else
954         // a. Let MatcherOperation be the abstract operation LookupSupportedLocales.
955         // 5. Let supportedLocales be MatcherOperation(availableLocales, requestedLocales).
956         supportedLocales = lookupSupportedLocales(state, availableLocales, requestedLocales);
957     }
958
959     RETURN_IF_EXCEPTION(scope, JSValue());
960
961     // 6. Let subset be CreateArrayFromList(supportedLocales).
962     // Already an array.
963
964     // 7. Let keys be subset.[[OwnPropertyKeys]]().
965     PropertyNameArray keys(&state, PropertyNameMode::Strings);
966     supportedLocales->getOwnPropertyNames(supportedLocales, &state, keys, EnumerationMode());
967     RETURN_IF_EXCEPTION(scope, JSValue());
968
969     PropertyDescriptor desc;
970     desc.setConfigurable(false);
971     desc.setWritable(false);
972
973     // 8. Repeat for each element P of keys in List order,
974     size_t len = keys.size();
975     for (size_t i = 0; i < len; ++i) {
976         // a. Let desc be PropertyDescriptor { [[Configurable]]: false, [[Writable]]: false }.
977         // Created above for reuse.
978
979         // b. Let status be DefinePropertyOrThrow(subset, P, desc).
980         supportedLocales->defineOwnProperty(supportedLocales, &state, keys[i], desc, true);
981
982         // c. Assert: status is not abrupt completion.
983         RETURN_IF_EXCEPTION(scope, JSValue());
984     }
985
986     // 9. Return subset.
987     return supportedLocales;
988 }
989
990 Vector<String> numberingSystemsForLocale(const String& locale)
991 {
992     static NeverDestroyed<Vector<String>> cachedNumberingSystems;
993     Vector<String>& availableNumberingSystems = cachedNumberingSystems.get();
994     if (availableNumberingSystems.isEmpty()) {
995         UErrorCode status = U_ZERO_ERROR;
996         UEnumeration* numberingSystemNames = unumsys_openAvailableNames(&status);
997         ASSERT(U_SUCCESS(status));
998
999         int32_t resultLength;
1000         // Numbering system names are always ASCII, so use char[].
1001         while (const char* result = uenum_next(numberingSystemNames, &resultLength, &status)) {
1002             ASSERT(U_SUCCESS(status));
1003             availableNumberingSystems.append(String(result, resultLength));
1004         }
1005         uenum_close(numberingSystemNames);
1006     }
1007
1008     UErrorCode status = U_ZERO_ERROR;
1009     UNumberingSystem* defaultSystem = unumsys_open(locale.utf8().data(), &status);
1010     ASSERT(U_SUCCESS(status));
1011     String defaultSystemName(unumsys_getName(defaultSystem));
1012     unumsys_close(defaultSystem);
1013
1014     Vector<String> numberingSystems({ defaultSystemName });
1015     numberingSystems.appendVector(availableNumberingSystems);
1016     return numberingSystems;
1017 }
1018
1019 EncodedJSValue JSC_HOST_CALL intlObjectFuncGetCanonicalLocales(ExecState* state)
1020 {
1021     VM& vm = state->vm();
1022     auto scope = DECLARE_THROW_SCOPE(vm);
1023     // https://tc39.github.io/ecma402/#sec-intl.getcanonicallocales
1024     // 8.2.1 Intl.getCanonicalLocales(locales) (ECMA-402 4.0)
1025
1026     // 1. Let ll be ? CanonicalizeLocaleList(locales).
1027     Vector<String> localeList = canonicalizeLocaleList(*state, state->argument(0));
1028     RETURN_IF_EXCEPTION(scope, encodedJSValue());
1029
1030     // 2. Return CreateArrayFromList(ll).
1031     JSGlobalObject* globalObject = state->callee()->globalObject();
1032     JSArray* localeArray = JSArray::tryCreateUninitialized(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous), localeList.size());
1033     if (!localeArray) {
1034         throwOutOfMemoryError(state, scope);
1035         return encodedJSValue();
1036     }
1037
1038     auto length = localeList.size();
1039     for (size_t i = 0; i < length; ++i) {
1040         localeArray->initializeIndex(vm, i, jsString(state, localeList[i]));
1041         RETURN_IF_EXCEPTION(scope, encodedJSValue());
1042     }
1043     return JSValue::encode(localeArray);
1044 }
1045
1046 } // namespace JSC
1047
1048 #endif // ENABLE(INTL)