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.DisplayNames implementation. */
8
9 #include "builtin/intl/DisplayNames.h"
10
11 #include "mozilla/Assertions.h"
12 #include "mozilla/intl/DisplayNames.h"
13 #include "mozilla/PodOperations.h"
14 #include "mozilla/Span.h"
15 #include "mozilla/TextUtils.h"
16
17 #include <algorithm>
18 #include <cstring>
19 #include <iterator>
20
21 #include "jsnum.h"
22 #include "jspubtd.h"
23
24 #include "builtin/intl/CommonFunctions.h"
25 #include "builtin/intl/FormatBuffer.h"
26 #include "builtin/intl/StringAsciiChars.h"
27 #include "builtin/String.h"
28 #include "gc/AllocKind.h"
29 #include "gc/FreeOp.h"
30 #include "gc/Rooting.h"
31 #include "js/CallArgs.h"
32 #include "js/Class.h"
33 #include "js/experimental/Intl.h" // JS::AddMozDisplayNamesConstructor
34 #include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
35 #include "js/GCVector.h"
36 #include "js/PropertyAndElement.h" // JS_DefineFunctions, JS_DefineProperties
37 #include "js/PropertyDescriptor.h"
38 #include "js/PropertySpec.h"
39 #include "js/Result.h"
40 #include "js/RootingAPI.h"
41 #include "js/TypeDecls.h"
42 #include "js/Utility.h"
43 #include "vm/GlobalObject.h"
44 #include "vm/JSAtom.h"
45 #include "vm/JSContext.h"
46 #include "vm/JSObject.h"
47 #include "vm/Printer.h"
48 #include "vm/Runtime.h"
49 #include "vm/SelfHosting.h"
50 #include "vm/Stack.h"
51 #include "vm/StaticStrings.h"
52 #include "vm/StringType.h"
53 #include "vm/WellKnownAtom.h" // js_*_str
54
55 #include "vm/JSObject-inl.h"
56 #include "vm/NativeObject-inl.h"
57
58 using namespace js;
59
60 const JSClassOps DisplayNamesObject::classOps_ = {nullptr, /* addProperty */
61 nullptr, /* delProperty */
62 nullptr, /* enumerate */
63 nullptr, /* newEnumerate */
64 nullptr, /* resolve */
65 nullptr, /* mayResolve */
66 DisplayNamesObject::finalize};
67
68 const JSClass DisplayNamesObject::class_ = {
69 "Intl.DisplayNames",
70 JSCLASS_HAS_RESERVED_SLOTS(DisplayNamesObject::SLOT_COUNT) |
71 JSCLASS_HAS_CACHED_PROTO(JSProto_DisplayNames) |
72 JSCLASS_FOREGROUND_FINALIZE,
73 &DisplayNamesObject::classOps_, &DisplayNamesObject::classSpec_};
74
75 const JSClass& DisplayNamesObject::protoClass_ = PlainObject::class_;
76
displayNames_toSource(JSContext * cx,unsigned argc,Value * vp)77 static bool displayNames_toSource(JSContext* cx, unsigned argc, Value* vp) {
78 CallArgs args = CallArgsFromVp(argc, vp);
79 args.rval().setString(cx->names().DisplayNames);
80 return true;
81 }
82
83 static const JSFunctionSpec displayNames_static_methods[] = {
84 JS_SELF_HOSTED_FN("supportedLocalesOf",
85 "Intl_DisplayNames_supportedLocalesOf", 1, 0),
86 JS_FS_END};
87
88 static const JSFunctionSpec displayNames_methods[] = {
89 JS_SELF_HOSTED_FN("of", "Intl_DisplayNames_of", 1, 0),
90 JS_SELF_HOSTED_FN("resolvedOptions", "Intl_DisplayNames_resolvedOptions", 0,
91 0),
92 JS_FN(js_toSource_str, displayNames_toSource, 0, 0), JS_FS_END};
93
94 static const JSPropertySpec displayNames_properties[] = {
95 JS_STRING_SYM_PS(toStringTag, "Intl.DisplayNames", JSPROP_READONLY),
96 JS_PS_END};
97
98 static bool DisplayNames(JSContext* cx, unsigned argc, Value* vp);
99
100 const ClassSpec DisplayNamesObject::classSpec_ = {
101 GenericCreateConstructor<DisplayNames, 2, gc::AllocKind::FUNCTION>,
102 GenericCreatePrototype<DisplayNamesObject>,
103 displayNames_static_methods,
104 nullptr,
105 displayNames_methods,
106 displayNames_properties,
107 nullptr,
108 ClassSpec::DontDefineConstructor};
109
110 enum class DisplayNamesOptions {
111 Standard,
112
113 // Calendar display names are no longer available with the current spec
114 // proposal text, but may be re-enabled in the future. For our internal use
115 // we still need to have them present, so use a feature guard for now.
116 EnableMozExtensions,
117 };
118
119 /**
120 * Initialize a new Intl.DisplayNames object using the named self-hosted
121 * function.
122 */
InitializeDisplayNamesObject(JSContext * cx,HandleObject obj,HandlePropertyName initializer,HandleValue locales,HandleValue options,DisplayNamesOptions dnoptions)123 static bool InitializeDisplayNamesObject(JSContext* cx, HandleObject obj,
124 HandlePropertyName initializer,
125 HandleValue locales,
126 HandleValue options,
127 DisplayNamesOptions dnoptions) {
128 FixedInvokeArgs<4> args(cx);
129
130 args[0].setObject(*obj);
131 args[1].set(locales);
132 args[2].set(options);
133 args[3].setBoolean(dnoptions == DisplayNamesOptions::EnableMozExtensions);
134
135 RootedValue ignored(cx);
136 if (!CallSelfHostedFunction(cx, initializer, NullHandleValue, args,
137 &ignored)) {
138 return false;
139 }
140
141 MOZ_ASSERT(ignored.isUndefined(),
142 "Unexpected return value from non-legacy Intl object initializer");
143 return true;
144 }
145
146 /**
147 * Intl.DisplayNames ([ locales [ , options ]])
148 */
DisplayNames(JSContext * cx,const CallArgs & args,DisplayNamesOptions dnoptions)149 static bool DisplayNames(JSContext* cx, const CallArgs& args,
150 DisplayNamesOptions dnoptions) {
151 // Step 1.
152 if (!ThrowIfNotConstructing(cx, args, "Intl.DisplayNames")) {
153 return false;
154 }
155
156 // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
157 RootedObject proto(cx);
158 if (dnoptions == DisplayNamesOptions::Standard) {
159 if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_DisplayNames,
160 &proto)) {
161 return false;
162 }
163 } else {
164 RootedObject newTarget(cx, &args.newTarget().toObject());
165 if (!GetPrototypeFromConstructor(cx, newTarget, JSProto_Null, &proto)) {
166 return false;
167 }
168 }
169
170 Rooted<DisplayNamesObject*> displayNames(cx);
171 displayNames = NewObjectWithClassProto<DisplayNamesObject>(cx, proto);
172 if (!displayNames) {
173 return false;
174 }
175
176 HandleValue locales = args.get(0);
177 HandleValue options = args.get(1);
178
179 // Steps 3-26.
180 if (!InitializeDisplayNamesObject(cx, displayNames,
181 cx->names().InitializeDisplayNames, locales,
182 options, dnoptions)) {
183 return false;
184 }
185
186 // Step 27.
187 args.rval().setObject(*displayNames);
188 return true;
189 }
190
DisplayNames(JSContext * cx,unsigned argc,Value * vp)191 static bool DisplayNames(JSContext* cx, unsigned argc, Value* vp) {
192 CallArgs args = CallArgsFromVp(argc, vp);
193 return DisplayNames(cx, args, DisplayNamesOptions::Standard);
194 }
195
MozDisplayNames(JSContext * cx,unsigned argc,Value * vp)196 static bool MozDisplayNames(JSContext* cx, unsigned argc, Value* vp) {
197 CallArgs args = CallArgsFromVp(argc, vp);
198 return DisplayNames(cx, args, DisplayNamesOptions::EnableMozExtensions);
199 }
200
finalize(JSFreeOp * fop,JSObject * obj)201 void js::DisplayNamesObject::finalize(JSFreeOp* fop, JSObject* obj) {
202 MOZ_ASSERT(fop->onMainThread());
203
204 if (mozilla::intl::DisplayNames* displayNames =
205 obj->as<DisplayNamesObject>().getDisplayNames()) {
206 intl::RemoveICUCellMemory(fop, obj, DisplayNamesObject::EstimatedMemoryUse);
207 delete displayNames;
208 }
209 }
210
AddMozDisplayNamesConstructor(JSContext * cx,HandleObject intl)211 bool JS::AddMozDisplayNamesConstructor(JSContext* cx, HandleObject intl) {
212 RootedObject ctor(cx, GlobalObject::createConstructor(
213 cx, MozDisplayNames, cx->names().DisplayNames, 2));
214 if (!ctor) {
215 return false;
216 }
217
218 RootedObject proto(
219 cx, GlobalObject::createBlankPrototype<PlainObject>(cx, cx->global()));
220 if (!proto) {
221 return false;
222 }
223
224 if (!LinkConstructorAndPrototype(cx, ctor, proto)) {
225 return false;
226 }
227
228 if (!JS_DefineFunctions(cx, ctor, displayNames_static_methods)) {
229 return false;
230 }
231
232 if (!JS_DefineFunctions(cx, proto, displayNames_methods)) {
233 return false;
234 }
235
236 if (!JS_DefineProperties(cx, proto, displayNames_properties)) {
237 return false;
238 }
239
240 RootedValue ctorValue(cx, ObjectValue(*ctor));
241 return DefineDataProperty(cx, intl, cx->names().DisplayNames, ctorValue, 0);
242 }
243
NewDisplayNames(JSContext * cx,const char * locale,mozilla::intl::DisplayNames::Options & options)244 static mozilla::intl::DisplayNames* NewDisplayNames(
245 JSContext* cx, const char* locale,
246 mozilla::intl::DisplayNames::Options& options) {
247 auto result = mozilla::intl::DisplayNames::TryCreate(locale, options);
248 if (result.isErr()) {
249 intl::ReportInternalError(cx, result.unwrapErr());
250 return nullptr;
251 }
252 return result.unwrap().release();
253 }
254
GetOrCreateDisplayNames(JSContext * cx,Handle<DisplayNamesObject * > displayNames,const char * locale,mozilla::intl::DisplayNames::Options & options)255 static mozilla::intl::DisplayNames* GetOrCreateDisplayNames(
256 JSContext* cx, Handle<DisplayNamesObject*> displayNames, const char* locale,
257 mozilla::intl::DisplayNames::Options& options) {
258 // Obtain a cached mozilla::intl::DisplayNames object.
259 mozilla::intl::DisplayNames* dn = displayNames->getDisplayNames();
260 if (!dn) {
261 dn = NewDisplayNames(cx, locale, options);
262 if (!dn) {
263 return nullptr;
264 }
265 displayNames->setDisplayNames(dn);
266
267 intl::AddICUCellMemory(displayNames,
268 DisplayNamesObject::EstimatedMemoryUse);
269 }
270 return dn;
271 }
272
ReportInvalidOptionError(JSContext * cx,HandleString type,HandleString option)273 static void ReportInvalidOptionError(JSContext* cx, HandleString type,
274 HandleString option) {
275 if (UniqueChars optionStr = QuoteString(cx, option, '"')) {
276 if (UniqueChars typeStr = QuoteString(cx, type)) {
277 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
278 JSMSG_INVALID_OPTION_VALUE, typeStr.get(),
279 optionStr.get());
280 }
281 }
282 }
283
ReportInvalidOptionError(JSContext * cx,const char * type,HandleString option)284 static void ReportInvalidOptionError(JSContext* cx, const char* type,
285 HandleString option) {
286 if (UniqueChars str = QuoteString(cx, option, '"')) {
287 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
288 JSMSG_INVALID_OPTION_VALUE, type, str.get());
289 }
290 }
291
ReportInvalidOptionError(JSContext * cx,const char * type,double option)292 static void ReportInvalidOptionError(JSContext* cx, const char* type,
293 double option) {
294 ToCStringBuf cbuf;
295 if (const char* str = NumberToCString(cx, &cbuf, option)) {
296 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
297 JSMSG_INVALID_DIGITS_VALUE, str);
298 }
299 }
300
301 /**
302 * intl_ComputeDisplayName(displayNames, locale, calendar, style,
303 * languageDisplay, fallback, type, code)
304 */
intl_ComputeDisplayName(JSContext * cx,unsigned argc,Value * vp)305 bool js::intl_ComputeDisplayName(JSContext* cx, unsigned argc, Value* vp) {
306 CallArgs args = CallArgsFromVp(argc, vp);
307 MOZ_ASSERT(args.length() == 8);
308
309 Rooted<DisplayNamesObject*> displayNames(
310 cx, &args[0].toObject().as<DisplayNamesObject>());
311
312 UniqueChars locale = intl::EncodeLocale(cx, args[1].toString());
313 if (!locale) {
314 return false;
315 }
316
317 RootedLinearString calendar(cx, args[2].toString()->ensureLinear(cx));
318 if (!calendar) {
319 return false;
320 }
321
322 RootedLinearString code(cx, args[7].toString()->ensureLinear(cx));
323 if (!code) {
324 return false;
325 }
326
327 mozilla::intl::DisplayNames::Style style;
328 {
329 JSLinearString* styleStr = args[3].toString()->ensureLinear(cx);
330 if (!styleStr) {
331 return false;
332 }
333
334 if (StringEqualsLiteral(styleStr, "long")) {
335 style = mozilla::intl::DisplayNames::Style::Long;
336 } else if (StringEqualsLiteral(styleStr, "short")) {
337 style = mozilla::intl::DisplayNames::Style::Short;
338 } else if (StringEqualsLiteral(styleStr, "narrow")) {
339 style = mozilla::intl::DisplayNames::Style::Narrow;
340 } else {
341 MOZ_ASSERT(StringEqualsLiteral(styleStr, "abbreviated"));
342 style = mozilla::intl::DisplayNames::Style::Abbreviated;
343 }
344 }
345
346 mozilla::intl::DisplayNames::LanguageDisplay languageDisplay;
347 {
348 JSLinearString* language = args[4].toString()->ensureLinear(cx);
349 if (!language) {
350 return false;
351 }
352
353 if (StringEqualsLiteral(language, "dialect")) {
354 languageDisplay = mozilla::intl::DisplayNames::LanguageDisplay::Dialect;
355 } else {
356 MOZ_ASSERT(language->empty() ||
357 StringEqualsLiteral(language, "standard"));
358 languageDisplay = mozilla::intl::DisplayNames::LanguageDisplay::Standard;
359 }
360 }
361
362 mozilla::intl::DisplayNames::Fallback fallback;
363 {
364 JSLinearString* fallbackStr = args[5].toString()->ensureLinear(cx);
365 if (!fallbackStr) {
366 return false;
367 }
368
369 if (StringEqualsLiteral(fallbackStr, "none")) {
370 fallback = mozilla::intl::DisplayNames::Fallback::None;
371 } else {
372 MOZ_ASSERT(StringEqualsLiteral(fallbackStr, "code"));
373 fallback = mozilla::intl::DisplayNames::Fallback::Code;
374 }
375 }
376
377 RootedLinearString type(cx, args[6].toString()->ensureLinear(cx));
378 if (!type) {
379 return false;
380 }
381
382 mozilla::intl::DisplayNames::Options options{
383 style,
384 languageDisplay,
385 };
386
387 // If a calendar exists, set it as an option.
388 JS::UniqueChars calendarChars = nullptr;
389 if (!calendar->empty()) {
390 calendarChars = JS_EncodeStringToUTF8(cx, calendar);
391 if (!calendarChars) {
392 return false;
393 }
394 }
395
396 mozilla::intl::DisplayNames* dn =
397 GetOrCreateDisplayNames(cx, displayNames, locale.get(), options);
398 if (!dn) {
399 return false;
400 }
401
402 // The "code" is usually a small ASCII string, so try to avoid an allocation
403 // by copying it to the stack. Unfortunately we can't pass a string span of
404 // the JSString directly to the unified DisplayNames API, as the
405 // intl::FormatBuffer will be written to. This writing can trigger a GC and
406 // invalidate the span, creating a nogc rooting hazard.
407 JS::UniqueChars utf8 = nullptr;
408 unsigned char ascii[32];
409 mozilla::Span<const char> codeSpan = nullptr;
410 if (code->length() < 32 && code->hasLatin1Chars() && StringIsAscii(code)) {
411 JS::AutoCheckCannotGC nogc;
412 mozilla::PodCopy(ascii, code->latin1Chars(nogc), code->length());
413 codeSpan =
414 mozilla::Span(reinterpret_cast<const char*>(ascii), code->length());
415 } else {
416 utf8 = JS_EncodeStringToUTF8(cx, code);
417 if (!utf8) {
418 return false;
419 }
420 codeSpan = mozilla::MakeStringSpan(utf8.get());
421 }
422
423 intl::FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
424 mozilla::Result<mozilla::Ok, mozilla::intl::DisplayNamesError> result =
425 mozilla::Ok{};
426
427 if (StringEqualsLiteral(type, "language")) {
428 result = dn->GetLanguage(buffer, codeSpan, fallback);
429 } else if (StringEqualsLiteral(type, "script")) {
430 result = dn->GetScript(buffer, codeSpan, fallback);
431 } else if (StringEqualsLiteral(type, "region")) {
432 result = dn->GetRegion(buffer, codeSpan, fallback);
433 } else if (StringEqualsLiteral(type, "currency")) {
434 result = dn->GetCurrency(buffer, codeSpan, fallback);
435 } else if (StringEqualsLiteral(type, "calendar")) {
436 result = dn->GetCalendar(buffer, codeSpan, fallback);
437 } else if (StringEqualsLiteral(type, "weekday")) {
438 double d;
439 if (!StringToNumber(cx, code, &d)) {
440 return false;
441 }
442 if (!IsInteger(d) || d < 1 || d > 7) {
443 ReportInvalidOptionError(cx, "weekday", d);
444 return false;
445 }
446 result =
447 dn->GetWeekday(buffer, static_cast<mozilla::intl::Weekday>(d),
448 mozilla::MakeStringSpan(calendarChars.get()), fallback);
449 } else if (StringEqualsLiteral(type, "month")) {
450 double d;
451 if (!StringToNumber(cx, code, &d)) {
452 return false;
453 }
454
455 if (!IsInteger(d) || d < 1 || d > 13) {
456 ReportInvalidOptionError(cx, "month", d);
457 return false;
458 }
459
460 result =
461 dn->GetMonth(buffer, static_cast<mozilla::intl::Month>(d),
462 mozilla::MakeStringSpan(calendarChars.get()), fallback);
463
464 } else if (StringEqualsLiteral(type, "quarter")) {
465 double d;
466 if (!StringToNumber(cx, code, &d)) {
467 return false;
468 }
469
470 // Inlined implementation of `IsValidQuarterCode ( quarter )`.
471 if (!IsInteger(d) || d < 1 || d > 4) {
472 ReportInvalidOptionError(cx, "quarter", d);
473 return false;
474 }
475
476 result =
477 dn->GetQuarter(buffer, static_cast<mozilla::intl::Quarter>(d),
478 mozilla::MakeStringSpan(calendarChars.get()), fallback);
479
480 } else if (StringEqualsLiteral(type, "dayPeriod")) {
481 mozilla::intl::DayPeriod dayPeriod;
482 if (StringEqualsLiteral(code, "am")) {
483 dayPeriod = mozilla::intl::DayPeriod::AM;
484 } else if (StringEqualsLiteral(code, "pm")) {
485 dayPeriod = mozilla::intl::DayPeriod::PM;
486 } else {
487 ReportInvalidOptionError(cx, "dayPeriod", code);
488 return false;
489 }
490 result = dn->GetDayPeriod(buffer, dayPeriod,
491 mozilla::MakeStringSpan(calendarChars.get()),
492 fallback);
493
494 } else {
495 MOZ_ASSERT(StringEqualsLiteral(type, "dateTimeField"));
496 mozilla::intl::DateTimeField field;
497 if (StringEqualsLiteral(code, "era")) {
498 field = mozilla::intl::DateTimeField::Era;
499 } else if (StringEqualsLiteral(code, "year")) {
500 field = mozilla::intl::DateTimeField::Year;
501 } else if (StringEqualsLiteral(code, "quarter")) {
502 field = mozilla::intl::DateTimeField::Quarter;
503 } else if (StringEqualsLiteral(code, "month")) {
504 field = mozilla::intl::DateTimeField::Month;
505 } else if (StringEqualsLiteral(code, "weekOfYear")) {
506 field = mozilla::intl::DateTimeField::WeekOfYear;
507 } else if (StringEqualsLiteral(code, "weekday")) {
508 field = mozilla::intl::DateTimeField::Weekday;
509 } else if (StringEqualsLiteral(code, "day")) {
510 field = mozilla::intl::DateTimeField::Day;
511 } else if (StringEqualsLiteral(code, "dayPeriod")) {
512 field = mozilla::intl::DateTimeField::DayPeriod;
513 } else if (StringEqualsLiteral(code, "hour")) {
514 field = mozilla::intl::DateTimeField::Hour;
515 } else if (StringEqualsLiteral(code, "minute")) {
516 field = mozilla::intl::DateTimeField::Minute;
517 } else if (StringEqualsLiteral(code, "second")) {
518 field = mozilla::intl::DateTimeField::Second;
519 } else if (StringEqualsLiteral(code, "timeZoneName")) {
520 field = mozilla::intl::DateTimeField::TimeZoneName;
521 } else {
522 ReportInvalidOptionError(cx, "dateTimeField", code);
523 return false;
524 }
525
526 intl::SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
527 mozilla::intl::DateTimePatternGenerator* dtpgen =
528 sharedIntlData.getDateTimePatternGenerator(cx, locale.get());
529 if (!dtpgen) {
530 return false;
531 }
532
533 result = dn->GetDateTimeField(buffer, field, *dtpgen, fallback);
534 }
535
536 if (result.isErr()) {
537 switch (result.unwrapErr()) {
538 case mozilla::intl::DisplayNamesError::InternalError:
539 intl::ReportInternalError(cx);
540 break;
541 case mozilla::intl::DisplayNamesError::OutOfMemory:
542 ReportOutOfMemory(cx);
543 break;
544 case mozilla::intl::DisplayNamesError::InvalidOption:
545 ReportInvalidOptionError(cx, type, code);
546 break;
547 case mozilla::intl::DisplayNamesError::DuplicateVariantSubtag:
548 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
549 JSMSG_DUPLICATE_VARIANT_SUBTAG);
550 break;
551 case mozilla::intl::DisplayNamesError::InvalidLanguageTag:
552 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
553 JSMSG_INVALID_LANGUAGE_TAG);
554 break;
555 }
556 return false;
557 }
558
559 JSString* str = buffer.toString(cx);
560 if (!str) {
561 return false;
562 }
563
564 if (str->empty()) {
565 args.rval().setUndefined();
566 } else {
567 args.rval().setString(str);
568 }
569
570 return true;
571 }
572