Coverage Report

Created: 2018-09-25 14:53

/src/mozilla-central/js/src/builtin/intl/Collator.cpp
Line
Count
Source (jump to first uncovered line)
1
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2
 * vim: set ts=8 sts=4 et sw=4 tw=99:
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.Collator implementation. */
8
9
#include "builtin/intl/Collator.h"
10
11
#include "mozilla/Assertions.h"
12
13
#include "jsapi.h"
14
15
#include "builtin/intl/CommonFunctions.h"
16
#include "builtin/intl/ICUStubs.h"
17
#include "builtin/intl/ScopedICUObject.h"
18
#include "builtin/intl/SharedIntlData.h"
19
#include "gc/FreeOp.h"
20
#include "js/CharacterEncoding.h"
21
#include "js/StableStringChars.h"
22
#include "js/TypeDecls.h"
23
#include "vm/GlobalObject.h"
24
#include "vm/JSContext.h"
25
#include "vm/Runtime.h"
26
#include "vm/StringType.h"
27
28
#include "vm/JSObject-inl.h"
29
30
using namespace js;
31
32
using JS::AutoStableStringChars;
33
34
using js::intl::GetAvailableLocales;
35
using js::intl::IcuLocale;
36
using js::intl::ReportInternalError;
37
using js::intl::SharedIntlData;
38
using js::intl::StringsAreEqual;
39
40
const ClassOps CollatorObject::classOps_ = {
41
    nullptr, /* addProperty */
42
    nullptr, /* delProperty */
43
    nullptr, /* enumerate */
44
    nullptr, /* newEnumerate */
45
    nullptr, /* resolve */
46
    nullptr, /* mayResolve */
47
    CollatorObject::finalize
48
};
49
50
const Class CollatorObject::class_ = {
51
    js_Object_str,
52
    JSCLASS_HAS_RESERVED_SLOTS(CollatorObject::SLOT_COUNT) |
53
    JSCLASS_FOREGROUND_FINALIZE,
54
    &CollatorObject::classOps_
55
};
56
57
static bool
58
collator_toSource(JSContext* cx, unsigned argc, Value* vp)
59
0
{
60
0
    CallArgs args = CallArgsFromVp(argc, vp);
61
0
    args.rval().setString(cx->names().Collator);
62
0
    return true;
63
0
}
64
65
static const JSFunctionSpec collator_static_methods[] = {
66
    JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_Collator_supportedLocalesOf", 1, 0),
67
    JS_FS_END
68
};
69
70
static const JSFunctionSpec collator_methods[] = {
71
    JS_SELF_HOSTED_FN("resolvedOptions", "Intl_Collator_resolvedOptions", 0, 0),
72
    JS_FN(js_toSource_str, collator_toSource, 0, 0),
73
    JS_FS_END
74
};
75
76
static const JSPropertySpec collator_properties[] = {
77
    JS_SELF_HOSTED_GET("compare", "Intl_Collator_compare_get", 0),
78
    JS_STRING_SYM_PS(toStringTag, "Object", JSPROP_READONLY),
79
    JS_PS_END
80
};
81
82
/**
83
 * 10.1.2 Intl.Collator([ locales [, options]])
84
 *
85
 * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b
86
 */
87
static bool
88
Collator(JSContext* cx, const CallArgs& args)
89
0
{
90
0
    // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code).
91
0
92
0
    // Steps 2-5 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
93
0
    RootedObject proto(cx);
94
0
    if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) {
95
0
        return false;
96
0
    }
97
0
98
0
    if (!proto) {
99
0
        proto = GlobalObject::getOrCreateCollatorPrototype(cx, cx->global());
100
0
        if (!proto) {
101
0
            return false;
102
0
        }
103
0
    }
104
0
105
0
    Rooted<CollatorObject*> collator(cx, NewObjectWithGivenProto<CollatorObject>(cx, proto));
106
0
    if (!collator) {
107
0
        return false;
108
0
    }
109
0
110
0
    collator->setReservedSlot(CollatorObject::INTERNALS_SLOT, NullValue());
111
0
    collator->setReservedSlot(CollatorObject::UCOLLATOR_SLOT, PrivateValue(nullptr));
112
0
113
0
    HandleValue locales = args.get(0);
114
0
    HandleValue options = args.get(1);
115
0
116
0
    // Step 6.
117
0
    if (!intl::InitializeObject(cx, collator, cx->names().InitializeCollator, locales, options)) {
118
0
        return false;
119
0
    }
120
0
121
0
    args.rval().setObject(*collator);
122
0
    return true;
123
0
}
124
125
static bool
126
Collator(JSContext* cx, unsigned argc, Value* vp)
127
0
{
128
0
    CallArgs args = CallArgsFromVp(argc, vp);
129
0
    return Collator(cx, args);
130
0
}
131
132
bool
133
js::intl_Collator(JSContext* cx, unsigned argc, Value* vp)
134
0
{
135
0
    CallArgs args = CallArgsFromVp(argc, vp);
136
0
    MOZ_ASSERT(args.length() == 2);
137
0
    MOZ_ASSERT(!args.isConstructing());
138
0
139
0
    return Collator(cx, args);
140
0
}
141
142
void
143
js::CollatorObject::finalize(FreeOp* fop, JSObject* obj)
144
0
{
145
0
    MOZ_ASSERT(fop->onMainThread());
146
0
147
0
    const Value& slot = obj->as<CollatorObject>().getReservedSlot(CollatorObject::UCOLLATOR_SLOT);
148
0
    if (UCollator* coll = static_cast<UCollator*>(slot.toPrivate())) {
149
0
        ucol_close(coll);
150
0
    }
151
0
}
152
153
JSObject*
154
js::CreateCollatorPrototype(JSContext* cx, HandleObject Intl, Handle<GlobalObject*> global)
155
0
{
156
0
    RootedFunction ctor(cx, GlobalObject::createConstructor(cx, &Collator, cx->names().Collator,
157
0
                                                            0));
158
0
    if (!ctor) {
159
0
        return nullptr;
160
0
    }
161
0
162
0
    RootedObject proto(cx, GlobalObject::createBlankPrototype<PlainObject>(cx, global));
163
0
    if (!proto) {
164
0
        return nullptr;
165
0
    }
166
0
167
0
    if (!LinkConstructorAndPrototype(cx, ctor, proto)) {
168
0
        return nullptr;
169
0
    }
170
0
171
0
    // 10.2.2
172
0
    if (!JS_DefineFunctions(cx, ctor, collator_static_methods)) {
173
0
        return nullptr;
174
0
    }
175
0
176
0
    // 10.3.5
177
0
    if (!JS_DefineFunctions(cx, proto, collator_methods)) {
178
0
        return nullptr;
179
0
    }
180
0
181
0
    // 10.3.2 and 10.3.3
182
0
    if (!JS_DefineProperties(cx, proto, collator_properties)) {
183
0
        return nullptr;
184
0
    }
185
0
186
0
    // 8.1
187
0
    RootedValue ctorValue(cx, ObjectValue(*ctor));
188
0
    if (!DefineDataProperty(cx, Intl, cx->names().Collator, ctorValue, 0)) {
189
0
        return nullptr;
190
0
    }
191
0
192
0
    return proto;
193
0
}
194
195
bool
196
js::intl_Collator_availableLocales(JSContext* cx, unsigned argc, Value* vp)
197
0
{
198
0
    CallArgs args = CallArgsFromVp(argc, vp);
199
0
    MOZ_ASSERT(args.length() == 0);
200
0
201
0
    RootedValue result(cx);
202
0
    if (!GetAvailableLocales(cx, ucol_countAvailable, ucol_getAvailable, &result)) {
203
0
        return false;
204
0
    }
205
0
    args.rval().set(result);
206
0
    return true;
207
0
}
208
209
bool
210
js::intl_availableCollations(JSContext* cx, unsigned argc, Value* vp)
211
0
{
212
0
    CallArgs args = CallArgsFromVp(argc, vp);
213
0
    MOZ_ASSERT(args.length() == 1);
214
0
    MOZ_ASSERT(args[0].isString());
215
0
216
0
    UniqueChars locale = intl::EncodeLocale(cx, args[0].toString());
217
0
    if (!locale) {
218
0
        return false;
219
0
    }
220
0
    UErrorCode status = U_ZERO_ERROR;
221
0
    UEnumeration* values = ucol_getKeywordValuesForLocale("co", locale.get(), false, &status);
222
0
    if (U_FAILURE(status)) {
223
0
        ReportInternalError(cx);
224
0
        return false;
225
0
    }
226
0
    ScopedICUObject<UEnumeration, uenum_close> toClose(values);
227
0
228
0
    uint32_t count = uenum_count(values, &status);
229
0
    if (U_FAILURE(status)) {
230
0
        ReportInternalError(cx);
231
0
        return false;
232
0
    }
233
0
234
0
    RootedObject collations(cx, NewDenseEmptyArray(cx));
235
0
    if (!collations) {
236
0
        return false;
237
0
    }
238
0
239
0
    uint32_t index = 0;
240
0
241
0
    // The first element of the collations array must be |null| per
242
0
    // ES2017 Intl, 10.2.3 Internal Slots.
243
0
    if (!DefineDataElement(cx, collations, index++, NullHandleValue)) {
244
0
        return false;
245
0
    }
246
0
247
0
    RootedValue element(cx);
248
0
    for (uint32_t i = 0; i < count; i++) {
249
0
        const char* collation = uenum_next(values, nullptr, &status);
250
0
        if (U_FAILURE(status)) {
251
0
            ReportInternalError(cx);
252
0
            return false;
253
0
        }
254
0
255
0
        // Per ECMA-402, 10.2.3, we don't include standard and search:
256
0
        // "The values 'standard' and 'search' must not be used as elements in
257
0
        // any [[sortLocaleData]][locale].co and [[searchLocaleData]][locale].co
258
0
        // array."
259
0
        if (StringsAreEqual(collation, "standard") || StringsAreEqual(collation, "search")) {
260
0
            continue;
261
0
        }
262
0
263
0
        // ICU returns old-style keyword values; map them to BCP 47 equivalents.
264
0
        collation = uloc_toUnicodeLocaleType("co", collation);
265
0
        if (!collation) {
266
0
            ReportInternalError(cx);
267
0
            return false;
268
0
        }
269
0
270
0
        JSString* jscollation = NewStringCopyZ<CanGC>(cx, collation);
271
0
        if (!jscollation) {
272
0
            return false;
273
0
        }
274
0
        element = StringValue(jscollation);
275
0
        if (!DefineDataElement(cx, collations, index++, element)) {
276
0
            return false;
277
0
        }
278
0
    }
279
0
280
0
    args.rval().setObject(*collations);
281
0
    return true;
282
0
}
283
284
/**
285
 * Returns a new UCollator with the locale and collation options
286
 * of the given Collator.
287
 */
288
static UCollator*
289
NewUCollator(JSContext* cx, Handle<CollatorObject*> collator)
290
0
{
291
0
    RootedValue value(cx);
292
0
293
0
    RootedObject internals(cx, intl::GetInternalsObject(cx, collator));
294
0
    if (!internals) {
295
0
        return nullptr;
296
0
    }
297
0
298
0
    if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) {
299
0
        return nullptr;
300
0
    }
301
0
    UniqueChars locale = intl::EncodeLocale(cx, value.toString());
302
0
    if (!locale) {
303
0
        return nullptr;
304
0
    }
305
0
306
0
    // UCollator options with default values.
307
0
    UColAttributeValue uStrength = UCOL_DEFAULT;
308
0
    UColAttributeValue uCaseLevel = UCOL_OFF;
309
0
    UColAttributeValue uAlternate = UCOL_DEFAULT;
310
0
    UColAttributeValue uNumeric = UCOL_OFF;
311
0
    // Normalization is always on to meet the canonical equivalence requirement.
312
0
    UColAttributeValue uNormalization = UCOL_ON;
313
0
    UColAttributeValue uCaseFirst = UCOL_DEFAULT;
314
0
315
0
    if (!GetProperty(cx, internals, internals, cx->names().usage, &value)) {
316
0
        return nullptr;
317
0
    }
318
0
319
0
    {
320
0
        JSLinearString* usage = value.toString()->ensureLinear(cx);
321
0
        if (!usage) {
322
0
            return nullptr;
323
0
        }
324
0
        if (StringEqualsAscii(usage, "search")) {
325
0
            // ICU expects search as a Unicode locale extension on locale.
326
0
            // Unicode locale extensions must occur before private use extensions.
327
0
            const char* oldLocale = locale.get();
328
0
            const char* p;
329
0
            size_t index;
330
0
            size_t localeLen = strlen(oldLocale);
331
0
            if ((p = strstr(oldLocale, "-x-"))) {
332
0
                index = p - oldLocale;
333
0
            } else {
334
0
                index = localeLen;
335
0
            }
336
0
337
0
            const char* insert;
338
0
            if ((p = strstr(oldLocale, "-u-")) && static_cast<size_t>(p - oldLocale) < index) {
339
0
                index = p - oldLocale + 2;
340
0
                insert = "-co-search";
341
0
            } else {
342
0
                insert = "-u-co-search";
343
0
            }
344
0
            size_t insertLen = strlen(insert);
345
0
            char* newLocale = cx->pod_malloc<char>(localeLen + insertLen + 1);
346
0
            if (!newLocale) {
347
0
                return nullptr;
348
0
            }
349
0
            memcpy(newLocale, oldLocale, index);
350
0
            memcpy(newLocale + index, insert, insertLen);
351
0
            memcpy(newLocale + index + insertLen, oldLocale + index, localeLen - index + 1); // '\0'
352
0
            locale = JS::UniqueChars(newLocale);
353
0
        } else {
354
0
            MOZ_ASSERT(StringEqualsAscii(usage, "sort"));
355
0
        }
356
0
    }
357
0
358
0
    // We don't need to look at the collation property - it can only be set
359
0
    // via the Unicode locale extension and is therefore already set on
360
0
    // locale.
361
0
362
0
    if (!GetProperty(cx, internals, internals, cx->names().sensitivity, &value)) {
363
0
        return nullptr;
364
0
    }
365
0
366
0
    {
367
0
        JSLinearString* sensitivity = value.toString()->ensureLinear(cx);
368
0
        if (!sensitivity) {
369
0
            return nullptr;
370
0
        }
371
0
        if (StringEqualsAscii(sensitivity, "base")) {
372
0
            uStrength = UCOL_PRIMARY;
373
0
        } else if (StringEqualsAscii(sensitivity, "accent")) {
374
0
            uStrength = UCOL_SECONDARY;
375
0
        } else if (StringEqualsAscii(sensitivity, "case")) {
376
0
            uStrength = UCOL_PRIMARY;
377
0
            uCaseLevel = UCOL_ON;
378
0
        } else {
379
0
            MOZ_ASSERT(StringEqualsAscii(sensitivity, "variant"));
380
0
            uStrength = UCOL_TERTIARY;
381
0
        }
382
0
    }
383
0
384
0
    if (!GetProperty(cx, internals, internals, cx->names().ignorePunctuation, &value)) {
385
0
        return nullptr;
386
0
    }
387
0
    // According to the ICU team, UCOL_SHIFTED causes punctuation to be
388
0
    // ignored. Looking at Unicode Technical Report 35, Unicode Locale Data
389
0
    // Markup Language, "shifted" causes whitespace and punctuation to be
390
0
    // ignored - that's a bit more than asked for, but there's no way to get
391
0
    // less.
392
0
    if (value.toBoolean()) {
393
0
        uAlternate = UCOL_SHIFTED;
394
0
    }
395
0
396
0
    if (!GetProperty(cx, internals, internals, cx->names().numeric, &value)) {
397
0
        return nullptr;
398
0
    }
399
0
    if (!value.isUndefined() && value.toBoolean()) {
400
0
        uNumeric = UCOL_ON;
401
0
    }
402
0
403
0
    if (!GetProperty(cx, internals, internals, cx->names().caseFirst, &value)) {
404
0
        return nullptr;
405
0
    }
406
0
    if (!value.isUndefined()) {
407
0
        JSLinearString* caseFirst = value.toString()->ensureLinear(cx);
408
0
        if (!caseFirst) {
409
0
            return nullptr;
410
0
        }
411
0
        if (StringEqualsAscii(caseFirst, "upper")) {
412
0
            uCaseFirst = UCOL_UPPER_FIRST;
413
0
        } else if (StringEqualsAscii(caseFirst, "lower")) {
414
0
            uCaseFirst = UCOL_LOWER_FIRST;
415
0
        } else {
416
0
            MOZ_ASSERT(StringEqualsAscii(caseFirst, "false"));
417
0
            uCaseFirst = UCOL_OFF;
418
0
        }
419
0
    }
420
0
421
0
    UErrorCode status = U_ZERO_ERROR;
422
0
    UCollator* coll = ucol_open(IcuLocale(locale.get()), &status);
423
0
    if (U_FAILURE(status)) {
424
0
        ReportInternalError(cx);
425
0
        return nullptr;
426
0
    }
427
0
428
0
    ucol_setAttribute(coll, UCOL_STRENGTH, uStrength, &status);
429
0
    ucol_setAttribute(coll, UCOL_CASE_LEVEL, uCaseLevel, &status);
430
0
    ucol_setAttribute(coll, UCOL_ALTERNATE_HANDLING, uAlternate, &status);
431
0
    ucol_setAttribute(coll, UCOL_NUMERIC_COLLATION, uNumeric, &status);
432
0
    ucol_setAttribute(coll, UCOL_NORMALIZATION_MODE, uNormalization, &status);
433
0
    ucol_setAttribute(coll, UCOL_CASE_FIRST, uCaseFirst, &status);
434
0
    if (U_FAILURE(status)) {
435
0
        ucol_close(coll);
436
0
        ReportInternalError(cx);
437
0
        return nullptr;
438
0
    }
439
0
440
0
    return coll;
441
0
}
442
443
static bool
444
intl_CompareStrings(JSContext* cx, UCollator* coll, HandleString str1, HandleString str2,
445
                    MutableHandleValue result)
446
0
{
447
0
    MOZ_ASSERT(str1);
448
0
    MOZ_ASSERT(str2);
449
0
450
0
    if (str1 == str2) {
451
0
        result.setInt32(0);
452
0
        return true;
453
0
    }
454
0
455
0
    AutoStableStringChars stableChars1(cx);
456
0
    if (!stableChars1.initTwoByte(cx, str1)) {
457
0
        return false;
458
0
    }
459
0
460
0
    AutoStableStringChars stableChars2(cx);
461
0
    if (!stableChars2.initTwoByte(cx, str2)) {
462
0
        return false;
463
0
    }
464
0
465
0
    mozilla::Range<const char16_t> chars1 = stableChars1.twoByteRange();
466
0
    mozilla::Range<const char16_t> chars2 = stableChars2.twoByteRange();
467
0
468
0
    UCollationResult uresult = ucol_strcoll(coll,
469
0
                                            chars1.begin().get(), chars1.length(),
470
0
                                            chars2.begin().get(), chars2.length());
471
0
    int32_t res;
472
0
    switch (uresult) {
473
0
        case UCOL_LESS: res = -1; break;
474
0
        case UCOL_EQUAL: res = 0; break;
475
0
        case UCOL_GREATER: res = 1; break;
476
0
        default: MOZ_CRASH("ucol_strcoll returned bad UCollationResult");
477
0
    }
478
0
    result.setInt32(res);
479
0
    return true;
480
0
}
481
482
bool
483
js::intl_CompareStrings(JSContext* cx, unsigned argc, Value* vp)
484
0
{
485
0
    CallArgs args = CallArgsFromVp(argc, vp);
486
0
    MOZ_ASSERT(args.length() == 3);
487
0
    MOZ_ASSERT(args[0].isObject());
488
0
    MOZ_ASSERT(args[1].isString());
489
0
    MOZ_ASSERT(args[2].isString());
490
0
491
0
    Rooted<CollatorObject*> collator(cx, &args[0].toObject().as<CollatorObject>());
492
0
493
0
    // Obtain a cached UCollator object.
494
0
    // XXX Does this handle Collator instances from other globals correctly?
495
0
    void* priv = collator->getReservedSlot(CollatorObject::UCOLLATOR_SLOT).toPrivate();
496
0
    UCollator* coll = static_cast<UCollator*>(priv);
497
0
    if (!coll) {
498
0
        coll = NewUCollator(cx, collator);
499
0
        if (!coll) {
500
0
            return false;
501
0
        }
502
0
        collator->setReservedSlot(CollatorObject::UCOLLATOR_SLOT, PrivateValue(coll));
503
0
    }
504
0
505
0
    // Use the UCollator to actually compare the strings.
506
0
    RootedString str1(cx, args[1].toString());
507
0
    RootedString str2(cx, args[2].toString());
508
0
    return intl_CompareStrings(cx, coll, str1, str2, args.rval());
509
0
}
510
511
bool
512
js::intl_isUpperCaseFirst(JSContext* cx, unsigned argc, Value* vp)
513
0
{
514
0
    CallArgs args = CallArgsFromVp(argc, vp);
515
0
    MOZ_ASSERT(args.length() == 1);
516
0
    MOZ_ASSERT(args[0].isString());
517
0
518
0
    SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
519
0
520
0
    RootedString locale(cx, args[0].toString());
521
0
    bool isUpperFirst;
522
0
    if (!sharedIntlData.isUpperCaseFirst(cx, locale, &isUpperFirst)) {
523
0
        return false;
524
0
    }
525
0
526
0
    args.rval().setBoolean(isUpperFirst);
527
0
    return true;
528
0
}