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