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 /* Implementation of the Intl object and its non-constructor properties. */
8 
9 #include "builtin/intl/IntlObject.h"
10 
11 #include "mozilla/Assertions.h"
12 #include "mozilla/Likely.h"
13 #include "mozilla/Range.h"
14 
15 #include "jsapi.h"
16 
17 #include "builtin/intl/Collator.h"
18 #include "builtin/intl/CommonFunctions.h"
19 #include "builtin/intl/DateTimeFormat.h"
20 #include "builtin/intl/ICUStubs.h"
21 #include "builtin/intl/NumberFormat.h"
22 #include "builtin/intl/PluralRules.h"
23 #include "builtin/intl/ScopedICUObject.h"
24 #include "js/Class.h"
25 #include "vm/GlobalObject.h"
26 #include "vm/JSContext.h"
27 #include "vm/JSObject.h"
28 #include "vm/StringType.h"
29 
30 #include "vm/JSObject-inl.h"
31 
32 using namespace js;
33 
34 using mozilla::Range;
35 using mozilla::RangedPtr;
36 
37 using js::intl::CallICU;
38 using js::intl::DateTimeFormatOptions;
39 using js::intl::IcuLocale;
40 
41 /******************** Intl ********************/
42 
intl_GetCalendarInfo(JSContext * cx,unsigned argc,Value * vp)43 bool js::intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp) {
44   CallArgs args = CallArgsFromVp(argc, vp);
45   MOZ_ASSERT(args.length() == 1);
46 
47   JSAutoByteString locale(cx, args[0].toString());
48   if (!locale) return false;
49 
50   UErrorCode status = U_ZERO_ERROR;
51   const UChar* uTimeZone = nullptr;
52   int32_t uTimeZoneLength = 0;
53   UCalendar* cal = ucal_open(uTimeZone, uTimeZoneLength, locale.ptr(),
54                              UCAL_DEFAULT, &status);
55   if (U_FAILURE(status)) {
56     intl::ReportInternalError(cx);
57     return false;
58   }
59   ScopedICUObject<UCalendar, ucal_close> toClose(cal);
60 
61   RootedObject info(cx, NewBuiltinClassInstance<PlainObject>(cx));
62   if (!info) return false;
63 
64   RootedValue v(cx);
65   int32_t firstDayOfWeek = ucal_getAttribute(cal, UCAL_FIRST_DAY_OF_WEEK);
66   v.setInt32(firstDayOfWeek);
67 
68   if (!DefineDataProperty(cx, info, cx->names().firstDayOfWeek, v))
69     return false;
70 
71   int32_t minDays = ucal_getAttribute(cal, UCAL_MINIMAL_DAYS_IN_FIRST_WEEK);
72   v.setInt32(minDays);
73   if (!DefineDataProperty(cx, info, cx->names().minDays, v)) return false;
74 
75   UCalendarWeekdayType prevDayType =
76       ucal_getDayOfWeekType(cal, UCAL_SATURDAY, &status);
77   if (U_FAILURE(status)) {
78     intl::ReportInternalError(cx);
79     return false;
80   }
81 
82   RootedValue weekendStart(cx), weekendEnd(cx);
83 
84   for (int i = UCAL_SUNDAY; i <= UCAL_SATURDAY; i++) {
85     UCalendarDaysOfWeek dayOfWeek = static_cast<UCalendarDaysOfWeek>(i);
86     UCalendarWeekdayType type = ucal_getDayOfWeekType(cal, dayOfWeek, &status);
87     if (U_FAILURE(status)) {
88       intl::ReportInternalError(cx);
89       return false;
90     }
91 
92     if (prevDayType != type) {
93       switch (type) {
94         case UCAL_WEEKDAY:
95           // If the first Weekday after Weekend is Sunday (1),
96           // then the last Weekend day is Saturday (7).
97           // Otherwise we'll just take the previous days number.
98           weekendEnd.setInt32(i == 1 ? 7 : i - 1);
99           break;
100         case UCAL_WEEKEND:
101           weekendStart.setInt32(i);
102           break;
103         case UCAL_WEEKEND_ONSET:
104         case UCAL_WEEKEND_CEASE:
105           // At the time this code was added, ICU apparently never behaves this
106           // way, so just throw, so that users will report a bug and we can
107           // decide what to do.
108           intl::ReportInternalError(cx);
109           return false;
110         default:
111           break;
112       }
113     }
114 
115     prevDayType = type;
116   }
117 
118   MOZ_ASSERT(weekendStart.isInt32());
119   MOZ_ASSERT(weekendEnd.isInt32());
120 
121   if (!DefineDataProperty(cx, info, cx->names().weekendStart, weekendStart))
122     return false;
123 
124   if (!DefineDataProperty(cx, info, cx->names().weekendEnd, weekendEnd))
125     return false;
126 
127   args.rval().setObject(*info);
128   return true;
129 }
130 
ReportBadKey(JSContext * cx,const Range<const JS::Latin1Char> & range)131 static void ReportBadKey(JSContext* cx,
132                          const Range<const JS::Latin1Char>& range) {
133   JS_ReportErrorNumberLatin1(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY,
134                              range.begin().get());
135 }
136 
ReportBadKey(JSContext * cx,const Range<const char16_t> & range)137 static void ReportBadKey(JSContext* cx, const Range<const char16_t>& range) {
138   JS_ReportErrorNumberUC(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY,
139                          range.begin().get());
140 }
141 
142 template <typename ConstChar>
MatchPart(RangedPtr<ConstChar> iter,const RangedPtr<ConstChar> end,const char * part,size_t partlen)143 static bool MatchPart(RangedPtr<ConstChar> iter, const RangedPtr<ConstChar> end,
144                       const char* part, size_t partlen) {
145   for (size_t i = 0; i < partlen; iter++, i++) {
146     if (iter == end || *iter != part[i]) return false;
147   }
148 
149   return true;
150 }
151 
152 template <typename ConstChar, size_t N>
MatchPart(RangedPtr<ConstChar> * iter,const RangedPtr<ConstChar> end,const char (& part)[N])153 inline bool MatchPart(RangedPtr<ConstChar>* iter,
154                       const RangedPtr<ConstChar> end, const char (&part)[N]) {
155   if (!MatchPart(*iter, end, part, N - 1)) return false;
156 
157   *iter += N - 1;
158   return true;
159 }
160 
161 enum class DisplayNameStyle {
162   Narrow,
163   Short,
164   Long,
165 };
166 
167 template <typename ConstChar>
ComputeSingleDisplayName(JSContext * cx,UDateFormat * fmt,UDateTimePatternGenerator * dtpg,DisplayNameStyle style,const Range<ConstChar> & pattern)168 static JSString* ComputeSingleDisplayName(JSContext* cx, UDateFormat* fmt,
169                                           UDateTimePatternGenerator* dtpg,
170                                           DisplayNameStyle style,
171                                           const Range<ConstChar>& pattern) {
172   RangedPtr<ConstChar> iter = pattern.begin();
173   const RangedPtr<ConstChar> end = pattern.end();
174 
175   auto MatchSlash = [cx, pattern, &iter, end]() {
176     if (MOZ_LIKELY(iter != end && *iter == '/')) {
177       iter++;
178       return true;
179     }
180 
181     ReportBadKey(cx, pattern);
182     return false;
183   };
184 
185   if (!MatchPart(&iter, end, "dates")) {
186     ReportBadKey(cx, pattern);
187     return nullptr;
188   }
189 
190   if (!MatchSlash()) return nullptr;
191 
192   if (MatchPart(&iter, end, "fields")) {
193     if (!MatchSlash()) return nullptr;
194 
195     UDateTimePatternField fieldType;
196 
197     if (MatchPart(&iter, end, "year")) {
198       fieldType = UDATPG_YEAR_FIELD;
199     } else if (MatchPart(&iter, end, "month")) {
200       fieldType = UDATPG_MONTH_FIELD;
201     } else if (MatchPart(&iter, end, "week")) {
202       fieldType = UDATPG_WEEK_OF_YEAR_FIELD;
203     } else if (MatchPart(&iter, end, "day")) {
204       fieldType = UDATPG_DAY_FIELD;
205     } else {
206       ReportBadKey(cx, pattern);
207       return nullptr;
208     }
209 
210     // This part must be the final part with no trailing data.
211     if (iter != end) {
212       ReportBadKey(cx, pattern);
213       return nullptr;
214     }
215 
216     int32_t resultSize;
217     const UChar* value = udatpg_getAppendItemName(dtpg, fieldType, &resultSize);
218     MOZ_ASSERT(resultSize >= 0);
219 
220     return NewStringCopyN<CanGC>(cx, value, size_t(resultSize));
221   }
222 
223   if (MatchPart(&iter, end, "gregorian")) {
224     if (!MatchSlash()) return nullptr;
225 
226     UDateFormatSymbolType symbolType;
227     int32_t index;
228 
229     if (MatchPart(&iter, end, "months")) {
230       if (!MatchSlash()) return nullptr;
231 
232       switch (style) {
233         case DisplayNameStyle::Narrow:
234           symbolType = UDAT_STANDALONE_NARROW_MONTHS;
235           break;
236 
237         case DisplayNameStyle::Short:
238           symbolType = UDAT_STANDALONE_SHORT_MONTHS;
239           break;
240 
241         case DisplayNameStyle::Long:
242           symbolType = UDAT_STANDALONE_MONTHS;
243           break;
244       }
245 
246       if (MatchPart(&iter, end, "january")) {
247         index = UCAL_JANUARY;
248       } else if (MatchPart(&iter, end, "february")) {
249         index = UCAL_FEBRUARY;
250       } else if (MatchPart(&iter, end, "march")) {
251         index = UCAL_MARCH;
252       } else if (MatchPart(&iter, end, "april")) {
253         index = UCAL_APRIL;
254       } else if (MatchPart(&iter, end, "may")) {
255         index = UCAL_MAY;
256       } else if (MatchPart(&iter, end, "june")) {
257         index = UCAL_JUNE;
258       } else if (MatchPart(&iter, end, "july")) {
259         index = UCAL_JULY;
260       } else if (MatchPart(&iter, end, "august")) {
261         index = UCAL_AUGUST;
262       } else if (MatchPart(&iter, end, "september")) {
263         index = UCAL_SEPTEMBER;
264       } else if (MatchPart(&iter, end, "october")) {
265         index = UCAL_OCTOBER;
266       } else if (MatchPart(&iter, end, "november")) {
267         index = UCAL_NOVEMBER;
268       } else if (MatchPart(&iter, end, "december")) {
269         index = UCAL_DECEMBER;
270       } else {
271         ReportBadKey(cx, pattern);
272         return nullptr;
273       }
274     } else if (MatchPart(&iter, end, "weekdays")) {
275       if (!MatchSlash()) return nullptr;
276 
277       switch (style) {
278         case DisplayNameStyle::Narrow:
279           symbolType = UDAT_STANDALONE_NARROW_WEEKDAYS;
280           break;
281 
282         case DisplayNameStyle::Short:
283           symbolType = UDAT_STANDALONE_SHORT_WEEKDAYS;
284           break;
285 
286         case DisplayNameStyle::Long:
287           symbolType = UDAT_STANDALONE_WEEKDAYS;
288           break;
289       }
290 
291       if (MatchPart(&iter, end, "monday")) {
292         index = UCAL_MONDAY;
293       } else if (MatchPart(&iter, end, "tuesday")) {
294         index = UCAL_TUESDAY;
295       } else if (MatchPart(&iter, end, "wednesday")) {
296         index = UCAL_WEDNESDAY;
297       } else if (MatchPart(&iter, end, "thursday")) {
298         index = UCAL_THURSDAY;
299       } else if (MatchPart(&iter, end, "friday")) {
300         index = UCAL_FRIDAY;
301       } else if (MatchPart(&iter, end, "saturday")) {
302         index = UCAL_SATURDAY;
303       } else if (MatchPart(&iter, end, "sunday")) {
304         index = UCAL_SUNDAY;
305       } else {
306         ReportBadKey(cx, pattern);
307         return nullptr;
308       }
309     } else if (MatchPart(&iter, end, "dayperiods")) {
310       if (!MatchSlash()) return nullptr;
311 
312       symbolType = UDAT_AM_PMS;
313 
314       if (MatchPart(&iter, end, "am")) {
315         index = UCAL_AM;
316       } else if (MatchPart(&iter, end, "pm")) {
317         index = UCAL_PM;
318       } else {
319         ReportBadKey(cx, pattern);
320         return nullptr;
321       }
322     } else {
323       ReportBadKey(cx, pattern);
324       return nullptr;
325     }
326 
327     // This part must be the final part with no trailing data.
328     if (iter != end) {
329       ReportBadKey(cx, pattern);
330       return nullptr;
331     }
332 
333     return CallICU(cx, [fmt, symbolType, index](UChar* chars, int32_t size,
334                                                 UErrorCode* status) {
335       return udat_getSymbols(fmt, symbolType, index, chars, size, status);
336     });
337   }
338 
339   ReportBadKey(cx, pattern);
340   return nullptr;
341 }
342 
intl_ComputeDisplayNames(JSContext * cx,unsigned argc,Value * vp)343 bool js::intl_ComputeDisplayNames(JSContext* cx, unsigned argc, Value* vp) {
344   CallArgs args = CallArgsFromVp(argc, vp);
345   MOZ_ASSERT(args.length() == 3);
346 
347   // 1. Assert: locale is a string.
348   RootedString str(cx, args[0].toString());
349   JSAutoByteString locale;
350   if (!locale.encodeUtf8(cx, str)) return false;
351 
352   // 2. Assert: style is a string.
353   DisplayNameStyle dnStyle;
354   {
355     JSLinearString* style = args[1].toString()->ensureLinear(cx);
356     if (!style) return false;
357 
358     if (StringEqualsAscii(style, "narrow")) {
359       dnStyle = DisplayNameStyle::Narrow;
360     } else if (StringEqualsAscii(style, "short")) {
361       dnStyle = DisplayNameStyle::Short;
362     } else {
363       MOZ_ASSERT(StringEqualsAscii(style, "long"));
364       dnStyle = DisplayNameStyle::Long;
365     }
366   }
367 
368   // 3. Assert: keys is an Array.
369   RootedArrayObject keys(cx, &args[2].toObject().as<ArrayObject>());
370   if (!keys) return false;
371 
372   // 4. Let result be ArrayCreate(0).
373   RootedArrayObject result(cx, NewDenseUnallocatedArray(cx, keys->length()));
374   if (!result) return false;
375 
376   UErrorCode status = U_ZERO_ERROR;
377 
378   UDateFormat* fmt =
379       udat_open(UDAT_DEFAULT, UDAT_DEFAULT, IcuLocale(locale.ptr()), nullptr, 0,
380                 nullptr, 0, &status);
381   if (U_FAILURE(status)) {
382     intl::ReportInternalError(cx);
383     return false;
384   }
385   ScopedICUObject<UDateFormat, udat_close> datToClose(fmt);
386 
387   // UDateTimePatternGenerator will be needed for translations of date and
388   // time fields like "month", "week", "day" etc.
389   UDateTimePatternGenerator* dtpg =
390       udatpg_open(IcuLocale(locale.ptr()), &status);
391   if (U_FAILURE(status)) {
392     intl::ReportInternalError(cx);
393     return false;
394   }
395   ScopedICUObject<UDateTimePatternGenerator, udatpg_close> datPgToClose(dtpg);
396 
397   // 5. For each element of keys,
398   RootedString keyValStr(cx);
399   RootedValue v(cx);
400   for (uint32_t i = 0; i < keys->length(); i++) {
401     if (!GetElement(cx, keys, keys, i, &v)) return false;
402 
403     keyValStr = v.toString();
404 
405     AutoStableStringChars stablePatternChars(cx);
406     if (!stablePatternChars.init(cx, keyValStr)) return false;
407 
408     // 5.a. Perform an implementation dependent algorithm to map a key to a
409     //      corresponding display name.
410     JSString* displayName =
411         stablePatternChars.isLatin1()
412             ? ComputeSingleDisplayName(cx, fmt, dtpg, dnStyle,
413                                        stablePatternChars.latin1Range())
414             : ComputeSingleDisplayName(cx, fmt, dtpg, dnStyle,
415                                        stablePatternChars.twoByteRange());
416     if (!displayName) return false;
417 
418     // 5.b. Append the result string to result.
419     v.setString(displayName);
420     if (!DefineDataElement(cx, result, i, v)) return false;
421   }
422 
423   // 6. Return result.
424   args.rval().setObject(*result);
425   return true;
426 }
427 
intl_GetLocaleInfo(JSContext * cx,unsigned argc,Value * vp)428 bool js::intl_GetLocaleInfo(JSContext* cx, unsigned argc, Value* vp) {
429   CallArgs args = CallArgsFromVp(argc, vp);
430   MOZ_ASSERT(args.length() == 1);
431 
432   JSAutoByteString locale(cx, args[0].toString());
433   if (!locale) return false;
434 
435   RootedObject info(cx, NewBuiltinClassInstance<PlainObject>(cx));
436   if (!info) return false;
437 
438   if (!DefineDataProperty(cx, info, cx->names().locale, args[0])) return false;
439 
440   bool rtl = uloc_isRightToLeft(IcuLocale(locale.ptr()));
441 
442   RootedValue dir(cx, StringValue(rtl ? cx->names().rtl : cx->names().ltr));
443 
444   if (!DefineDataProperty(cx, info, cx->names().direction, dir)) return false;
445 
446   args.rval().setObject(*info);
447   return true;
448 }
449 
450 const Class js::IntlClass = {js_Object_str,
451                              JSCLASS_HAS_CACHED_PROTO(JSProto_Intl)};
452 
intl_toSource(JSContext * cx,unsigned argc,Value * vp)453 static bool intl_toSource(JSContext* cx, unsigned argc, Value* vp) {
454   CallArgs args = CallArgsFromVp(argc, vp);
455   args.rval().setString(cx->names().Intl);
456   return true;
457 }
458 
459 static const JSFunctionSpec intl_static_methods[] = {
460     JS_FN(js_toSource_str, intl_toSource, 0, 0),
461     JS_SELF_HOSTED_FN("getCanonicalLocales", "Intl_getCanonicalLocales", 1, 0),
462     JS_FS_END};
463 
464 /**
465  * Initializes the Intl Object and its standard built-in properties.
466  * Spec: ECMAScript Internationalization API Specification, 8.0, 8.1
467  */
initIntlObject(JSContext * cx,Handle<GlobalObject * > global)468 /* static */ bool GlobalObject::initIntlObject(JSContext* cx,
469                                                Handle<GlobalObject*> global) {
470   RootedObject proto(cx, GlobalObject::getOrCreateObjectPrototype(cx, global));
471   if (!proto) return false;
472 
473   // The |Intl| object is just a plain object with some "static" function
474   // properties and some constructor properties.
475   RootedObject intl(
476       cx, NewObjectWithGivenProto(cx, &IntlClass, proto, SingletonObject));
477   if (!intl) return false;
478 
479   // Add the static functions.
480   if (!JS_DefineFunctions(cx, intl, intl_static_methods)) return false;
481 
482   // Add the constructor properties, computing and returning the relevant
483   // prototype objects needed below.
484   RootedObject collatorProto(cx, CreateCollatorPrototype(cx, intl, global));
485   if (!collatorProto) return false;
486   RootedObject dateTimeFormatProto(cx), dateTimeFormat(cx);
487   dateTimeFormatProto = CreateDateTimeFormatPrototype(
488       cx, intl, global, &dateTimeFormat, DateTimeFormatOptions::Standard);
489   if (!dateTimeFormatProto) return false;
490   RootedObject numberFormatProto(cx), numberFormat(cx);
491   numberFormatProto =
492       CreateNumberFormatPrototype(cx, intl, global, &numberFormat);
493   if (!numberFormatProto) return false;
494   RootedObject pluralRulesProto(cx,
495                                 CreatePluralRulesPrototype(cx, intl, global));
496   if (!pluralRulesProto) return false;
497 
498   // The |Intl| object is fully set up now, so define the global property.
499   RootedValue intlValue(cx, ObjectValue(*intl));
500   if (!DefineDataProperty(cx, global, cx->names().Intl, intlValue,
501                           JSPROP_RESOLVING))
502     return false;
503 
504   // Now that the |Intl| object is successfully added, we can OOM-safely fill
505   // in all relevant reserved global slots.
506 
507   // Cache the various prototypes, for use in creating instances of these
508   // objects with the proper [[Prototype]] as "the original value of
509   // |Intl.Collator.prototype|" and similar.  For builtin classes like
510   // |String.prototype| we have |JSProto_*| that enables
511   // |getPrototype(JSProto_*)|, but that has global-object-property-related
512   // baggage we don't need or want, so we use one-off reserved slots.
513   global->setReservedSlot(COLLATOR_PROTO, ObjectValue(*collatorProto));
514   global->setReservedSlot(DATE_TIME_FORMAT, ObjectValue(*dateTimeFormat));
515   global->setReservedSlot(DATE_TIME_FORMAT_PROTO,
516                           ObjectValue(*dateTimeFormatProto));
517   global->setReservedSlot(NUMBER_FORMAT, ObjectValue(*numberFormat));
518   global->setReservedSlot(NUMBER_FORMAT_PROTO, ObjectValue(*numberFormatProto));
519   global->setReservedSlot(PLURAL_RULES_PROTO, ObjectValue(*pluralRulesProto));
520 
521   // Also cache |Intl| to implement spec language that conditions behavior
522   // based on values being equal to "the standard built-in |Intl| object".
523   // Use |setConstructor| to correspond with |JSProto_Intl|.
524   //
525   // XXX We should possibly do a one-off reserved slot like above.
526   global->setConstructor(JSProto_Intl, ObjectValue(*intl));
527   return true;
528 }
529 
InitIntlClass(JSContext * cx,HandleObject obj)530 JSObject* js::InitIntlClass(JSContext* cx, HandleObject obj) {
531   Handle<GlobalObject*> global = obj.as<GlobalObject>();
532   if (!GlobalObject::initIntlObject(cx, global)) return nullptr;
533 
534   return &global->getConstructor(JSProto_Intl).toObject();
535 }
536