Collator.cpp (15551B)
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.Collator implementation. */ 8 9 #include "builtin/intl/Collator.h" 10 11 #include "mozilla/Assertions.h" 12 #include "mozilla/intl/Collator.h" 13 #include "mozilla/intl/Locale.h" 14 #include "mozilla/Span.h" 15 16 #include "builtin/Array.h" 17 #include "builtin/intl/CommonFunctions.h" 18 #include "builtin/intl/FormatBuffer.h" 19 #include "builtin/intl/LanguageTag.h" 20 #include "builtin/intl/LocaleNegotiation.h" 21 #include "builtin/intl/SharedIntlData.h" 22 #include "gc/GCContext.h" 23 #include "js/PropertySpec.h" 24 #include "js/StableStringChars.h" 25 #include "js/TypeDecls.h" 26 #include "vm/GlobalObject.h" 27 #include "vm/JSContext.h" 28 #include "vm/PlainObject.h" // js::PlainObject 29 #include "vm/Runtime.h" 30 #include "vm/StringType.h" 31 32 #include "vm/GeckoProfiler-inl.h" 33 #include "vm/JSObject-inl.h" 34 35 using namespace js; 36 using namespace js::intl; 37 38 using JS::AutoStableStringChars; 39 40 using js::intl::ReportInternalError; 41 using js::intl::SharedIntlData; 42 43 const JSClassOps CollatorObject::classOps_ = { 44 nullptr, // addProperty 45 nullptr, // delProperty 46 nullptr, // enumerate 47 nullptr, // newEnumerate 48 nullptr, // resolve 49 nullptr, // mayResolve 50 CollatorObject::finalize, // finalize 51 nullptr, // call 52 nullptr, // construct 53 nullptr, // trace 54 }; 55 56 const JSClass CollatorObject::class_ = { 57 "Intl.Collator", 58 JSCLASS_HAS_RESERVED_SLOTS(CollatorObject::SLOT_COUNT) | 59 JSCLASS_HAS_CACHED_PROTO(JSProto_Collator) | 60 JSCLASS_FOREGROUND_FINALIZE, 61 &CollatorObject::classOps_, 62 &CollatorObject::classSpec_, 63 }; 64 65 const JSClass& CollatorObject::protoClass_ = PlainObject::class_; 66 67 static bool collator_supportedLocalesOf(JSContext* cx, unsigned argc, 68 Value* vp); 69 70 static bool collator_toSource(JSContext* cx, unsigned argc, Value* vp) { 71 CallArgs args = CallArgsFromVp(argc, vp); 72 args.rval().setString(cx->names().Collator); 73 return true; 74 } 75 76 static const JSFunctionSpec collator_static_methods[] = { 77 JS_FN("supportedLocalesOf", collator_supportedLocalesOf, 1, 0), 78 JS_FS_END, 79 }; 80 81 static const JSFunctionSpec collator_methods[] = { 82 JS_SELF_HOSTED_FN("resolvedOptions", "Intl_Collator_resolvedOptions", 0, 0), 83 JS_FN("toSource", collator_toSource, 0, 0), 84 JS_FS_END, 85 }; 86 87 static const JSPropertySpec collator_properties[] = { 88 JS_SELF_HOSTED_GET("compare", "$Intl_Collator_compare_get", 0), 89 JS_STRING_SYM_PS(toStringTag, "Intl.Collator", JSPROP_READONLY), 90 JS_PS_END, 91 }; 92 93 static bool Collator(JSContext* cx, unsigned argc, Value* vp); 94 95 const ClassSpec CollatorObject::classSpec_ = { 96 GenericCreateConstructor<Collator, 0, gc::AllocKind::FUNCTION>, 97 GenericCreatePrototype<CollatorObject>, 98 collator_static_methods, 99 nullptr, 100 collator_methods, 101 collator_properties, 102 nullptr, 103 ClassSpec::DontDefineConstructor, 104 }; 105 106 /** 107 * 10.1.2 Intl.Collator([ locales [, options]]) 108 * 109 * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b 110 */ 111 static bool Collator(JSContext* cx, unsigned argc, Value* vp) { 112 AutoJSConstructorProfilerEntry pseudoFrame(cx, "Intl.Collator"); 113 CallArgs args = CallArgsFromVp(argc, vp); 114 115 // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code). 116 117 // Steps 2-5 (Inlined 9.1.14, OrdinaryCreateFromConstructor). 118 RootedObject proto(cx); 119 if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Collator, &proto)) { 120 return false; 121 } 122 123 Rooted<CollatorObject*> collator( 124 cx, NewObjectWithClassProto<CollatorObject>(cx, proto)); 125 if (!collator) { 126 return false; 127 } 128 129 HandleValue locales = args.get(0); 130 HandleValue options = args.get(1); 131 132 // Step 6. 133 if (!intl::InitializeObject(cx, collator, cx->names().InitializeCollator, 134 locales, options)) { 135 return false; 136 } 137 138 args.rval().setObject(*collator); 139 return true; 140 } 141 142 CollatorObject* js::intl::CreateCollator(JSContext* cx, Handle<Value> locales, 143 Handle<Value> options) { 144 Rooted<CollatorObject*> collator(cx, 145 NewBuiltinClassInstance<CollatorObject>(cx)); 146 if (!collator) { 147 return nullptr; 148 } 149 150 if (!InitializeObject(cx, collator, cx->names().InitializeCollator, locales, 151 options)) { 152 return nullptr; 153 } 154 155 return collator; 156 } 157 158 CollatorObject* js::intl::GetOrCreateCollator(JSContext* cx, 159 Handle<Value> locales, 160 Handle<Value> options) { 161 // Try to use a cached instance when |locales| is either undefined or a 162 // string, and |options| is undefined. 163 if ((locales.isUndefined() || locales.isString()) && options.isUndefined()) { 164 Rooted<JSLinearString*> locale(cx); 165 if (locales.isString()) { 166 locale = locales.toString()->ensureLinear(cx); 167 if (!locale) { 168 return nullptr; 169 } 170 } 171 return cx->global()->globalIntlData().getOrCreateCollator(cx, locale); 172 } 173 174 // Create a new Intl.Collator instance. 175 return CreateCollator(cx, locales, options); 176 } 177 178 void js::CollatorObject::finalize(JS::GCContext* gcx, JSObject* obj) { 179 MOZ_ASSERT(gcx->onMainThread()); 180 181 if (mozilla::intl::Collator* coll = obj->as<CollatorObject>().getCollator()) { 182 intl::RemoveICUCellMemory(gcx, obj, CollatorObject::EstimatedMemoryUse); 183 delete coll; 184 } 185 } 186 187 bool js::intl_availableCollations(JSContext* cx, unsigned argc, Value* vp) { 188 CallArgs args = CallArgsFromVp(argc, vp); 189 MOZ_ASSERT(args.length() == 1); 190 MOZ_ASSERT(args[0].isString()); 191 192 UniqueChars locale = intl::EncodeLocale(cx, args[0].toString()); 193 if (!locale) { 194 return false; 195 } 196 auto keywords = 197 mozilla::intl::Collator::GetBcp47KeywordValuesForLocale(locale.get()); 198 if (keywords.isErr()) { 199 ReportInternalError(cx, keywords.unwrapErr()); 200 return false; 201 } 202 203 RootedObject collations(cx, NewDenseEmptyArray(cx)); 204 if (!collations) { 205 return false; 206 } 207 208 // The first element of the collations array must be |null| per 209 // ES2017 Intl, 10.2.3 Internal Slots. 210 if (!NewbornArrayPush(cx, collations, NullValue())) { 211 return false; 212 } 213 214 for (auto result : keywords.unwrap()) { 215 if (result.isErr()) { 216 ReportInternalError(cx); 217 return false; 218 } 219 mozilla::Span<const char> collation = result.unwrap(); 220 221 // Per ECMA-402, 10.2.3, we don't include standard and search: 222 // "The values 'standard' and 'search' must not be used as elements in 223 // any [[sortLocaleData]][locale].co and [[searchLocaleData]][locale].co 224 // array." 225 static constexpr auto standard = mozilla::MakeStringSpan("standard"); 226 static constexpr auto search = mozilla::MakeStringSpan("search"); 227 if (collation == standard || collation == search) { 228 continue; 229 } 230 231 JSString* jscollation = NewStringCopy<CanGC>(cx, collation); 232 if (!jscollation) { 233 return false; 234 } 235 if (!NewbornArrayPush(cx, collations, StringValue(jscollation))) { 236 return false; 237 } 238 } 239 240 args.rval().setObject(*collations); 241 return true; 242 } 243 244 /** 245 * Returns a new mozilla::intl::Collator with the locale and collation options 246 * of the given Collator. 247 */ 248 static mozilla::intl::Collator* NewIntlCollator( 249 JSContext* cx, Handle<CollatorObject*> collator) { 250 RootedValue value(cx); 251 RootedObject internals(cx, intl::GetInternalsObject(cx, collator)); 252 if (!internals) { 253 return nullptr; 254 } 255 256 using mozilla::intl::Collator; 257 258 Collator::Options options{}; 259 260 if (!GetProperty(cx, internals, internals, cx->names().usage, &value)) { 261 return nullptr; 262 } 263 264 enum class Usage { Search, Sort }; 265 266 Usage usage; 267 { 268 JSLinearString* str = value.toString()->ensureLinear(cx); 269 if (!str) { 270 return nullptr; 271 } 272 273 if (StringEqualsLiteral(str, "search")) { 274 usage = Usage::Search; 275 } else { 276 MOZ_ASSERT(StringEqualsLiteral(str, "sort")); 277 usage = Usage::Sort; 278 } 279 } 280 281 JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx); 282 283 // ICU expects collation as Unicode locale extensions on locale. 284 if (usage == Usage::Search) { 285 if (!keywords.emplaceBack("co", cx->names().search)) { 286 return nullptr; 287 } 288 289 // Search collations can't select a different collation, so the collation 290 // property is guaranteed to be "default". 291 #ifdef DEBUG 292 if (!GetProperty(cx, internals, internals, cx->names().collation, &value)) { 293 return nullptr; 294 } 295 296 JSLinearString* collation = value.toString()->ensureLinear(cx); 297 if (!collation) { 298 return nullptr; 299 } 300 301 MOZ_ASSERT(StringEqualsLiteral(collation, "default")); 302 #endif 303 } else { 304 if (!GetProperty(cx, internals, internals, cx->names().collation, &value)) { 305 return nullptr; 306 } 307 308 JSLinearString* collation = value.toString()->ensureLinear(cx); 309 if (!collation) { 310 return nullptr; 311 } 312 313 // Set collation as a Unicode locale extension when it was specified. 314 if (!StringEqualsLiteral(collation, "default")) { 315 if (!keywords.emplaceBack("co", collation)) { 316 return nullptr; 317 } 318 } 319 } 320 321 UniqueChars locale = intl::FormatLocale(cx, internals, keywords); 322 if (!locale) { 323 return nullptr; 324 } 325 326 if (!GetProperty(cx, internals, internals, cx->names().sensitivity, &value)) { 327 return nullptr; 328 } 329 330 { 331 JSLinearString* sensitivity = value.toString()->ensureLinear(cx); 332 if (!sensitivity) { 333 return nullptr; 334 } 335 if (StringEqualsLiteral(sensitivity, "base")) { 336 options.sensitivity = Collator::Sensitivity::Base; 337 } else if (StringEqualsLiteral(sensitivity, "accent")) { 338 options.sensitivity = Collator::Sensitivity::Accent; 339 } else if (StringEqualsLiteral(sensitivity, "case")) { 340 options.sensitivity = Collator::Sensitivity::Case; 341 } else { 342 MOZ_ASSERT(StringEqualsLiteral(sensitivity, "variant")); 343 options.sensitivity = Collator::Sensitivity::Variant; 344 } 345 } 346 347 if (!GetProperty(cx, internals, internals, cx->names().ignorePunctuation, 348 &value)) { 349 return nullptr; 350 } 351 options.ignorePunctuation = value.toBoolean(); 352 353 if (!GetProperty(cx, internals, internals, cx->names().numeric, &value)) { 354 return nullptr; 355 } 356 if (!value.isUndefined()) { 357 options.numeric = value.toBoolean(); 358 } 359 360 if (!GetProperty(cx, internals, internals, cx->names().caseFirst, &value)) { 361 return nullptr; 362 } 363 if (!value.isUndefined()) { 364 JSLinearString* caseFirst = value.toString()->ensureLinear(cx); 365 if (!caseFirst) { 366 return nullptr; 367 } 368 if (StringEqualsLiteral(caseFirst, "upper")) { 369 options.caseFirst = Collator::CaseFirst::Upper; 370 } else if (StringEqualsLiteral(caseFirst, "lower")) { 371 options.caseFirst = Collator::CaseFirst::Lower; 372 } else { 373 MOZ_ASSERT(StringEqualsLiteral(caseFirst, "false")); 374 options.caseFirst = Collator::CaseFirst::False; 375 } 376 } 377 378 auto collResult = Collator::TryCreate(locale.get()); 379 if (collResult.isErr()) { 380 ReportInternalError(cx, collResult.unwrapErr()); 381 return nullptr; 382 } 383 auto coll = collResult.unwrap(); 384 385 auto optResult = coll->SetOptions(options); 386 if (optResult.isErr()) { 387 ReportInternalError(cx, optResult.unwrapErr()); 388 return nullptr; 389 } 390 391 return coll.release(); 392 } 393 394 static mozilla::intl::Collator* GetOrCreateCollator( 395 JSContext* cx, Handle<CollatorObject*> collator) { 396 // Obtain a cached mozilla::intl::Collator object. 397 mozilla::intl::Collator* coll = collator->getCollator(); 398 if (coll) { 399 return coll; 400 } 401 402 coll = NewIntlCollator(cx, collator); 403 if (!coll) { 404 return nullptr; 405 } 406 collator->setCollator(coll); 407 408 intl::AddICUCellMemory(collator, CollatorObject::EstimatedMemoryUse); 409 return coll; 410 } 411 412 static bool intl_CompareStrings(JSContext* cx, mozilla::intl::Collator* coll, 413 JSString* str1, JSString* str2, 414 MutableHandleValue result) { 415 MOZ_ASSERT(str1); 416 MOZ_ASSERT(str2); 417 418 if (str1 == str2) { 419 result.setInt32(0); 420 return true; 421 } 422 423 AutoStableStringChars stableChars1(cx); 424 if (!stableChars1.initTwoByte(cx, str1)) { 425 return false; 426 } 427 428 AutoStableStringChars stableChars2(cx); 429 if (!stableChars2.initTwoByte(cx, str2)) { 430 return false; 431 } 432 433 mozilla::Range<const char16_t> chars1 = stableChars1.twoByteRange(); 434 mozilla::Range<const char16_t> chars2 = stableChars2.twoByteRange(); 435 436 result.setInt32(coll->CompareStrings(chars1, chars2)); 437 return true; 438 } 439 440 bool js::intl_CompareStrings(JSContext* cx, unsigned argc, Value* vp) { 441 CallArgs args = CallArgsFromVp(argc, vp); 442 MOZ_ASSERT(args.length() == 3); 443 MOZ_ASSERT(args[0].isObject()); 444 MOZ_ASSERT(args[1].isString()); 445 MOZ_ASSERT(args[2].isString()); 446 447 Rooted<CollatorObject*> collator(cx, 448 &args[0].toObject().as<CollatorObject>()); 449 450 mozilla::intl::Collator* coll = GetOrCreateCollator(cx, collator); 451 if (!coll) { 452 return false; 453 } 454 455 // Use the UCollator to actually compare the strings. 456 JSString* str1 = args[1].toString(); 457 JSString* str2 = args[2].toString(); 458 return intl_CompareStrings(cx, coll, str1, str2, args.rval()); 459 } 460 461 bool js::intl::CompareStrings(JSContext* cx, Handle<CollatorObject*> collator, 462 Handle<JSString*> str1, Handle<JSString*> str2, 463 MutableHandle<Value> result) { 464 mozilla::intl::Collator* coll = GetOrCreateCollator(cx, collator); 465 if (!coll) { 466 return false; 467 } 468 return intl_CompareStrings(cx, coll, str1, str2, result); 469 } 470 471 bool js::intl_isUpperCaseFirst(JSContext* cx, unsigned argc, Value* vp) { 472 CallArgs args = CallArgsFromVp(argc, vp); 473 MOZ_ASSERT(args.length() == 1); 474 MOZ_ASSERT(args[0].isString()); 475 476 SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); 477 478 Rooted<JSLinearString*> locale(cx, args[0].toString()->ensureLinear(cx)); 479 if (!locale) { 480 return false; 481 } 482 483 bool isUpperFirst; 484 if (!sharedIntlData.isUpperCaseFirst(cx, locale, &isUpperFirst)) { 485 return false; 486 } 487 488 args.rval().setBoolean(isUpperFirst); 489 return true; 490 } 491 492 bool js::intl_isIgnorePunctuation(JSContext* cx, unsigned argc, Value* vp) { 493 CallArgs args = CallArgsFromVp(argc, vp); 494 MOZ_ASSERT(args.length() == 1); 495 MOZ_ASSERT(args[0].isString()); 496 497 SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); 498 499 Rooted<JSLinearString*> locale(cx, args[0].toString()->ensureLinear(cx)); 500 if (!locale) { 501 return false; 502 } 503 504 bool isIgnorePunctuation; 505 if (!sharedIntlData.isIgnorePunctuation(cx, locale, &isIgnorePunctuation)) { 506 return false; 507 } 508 509 args.rval().setBoolean(isIgnorePunctuation); 510 return true; 511 } 512 513 /** 514 * Intl.Collator.supportedLocalesOf ( locales [ , options ] ) 515 */ 516 static bool collator_supportedLocalesOf(JSContext* cx, unsigned argc, 517 Value* vp) { 518 CallArgs args = CallArgsFromVp(argc, vp); 519 520 // Steps 1-3. 521 auto* array = SupportedLocalesOf(cx, AvailableLocaleKind::Collator, 522 args.get(0), args.get(1)); 523 if (!array) { 524 return false; 525 } 526 args.rval().setObject(*array); 527 return true; 528 }