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.DateTimeFormat implementation. */
8 
9 #include "builtin/intl/DateTimeFormat.h"
10 
11 #include "mozilla/Assertions.h"
12 #include "mozilla/EnumSet.h"
13 #include "mozilla/intl/Calendar.h"
14 #include "mozilla/intl/DateIntervalFormat.h"
15 #include "mozilla/intl/DateTimeFormat.h"
16 #include "mozilla/intl/DateTimePart.h"
17 #include "mozilla/intl/DateTimePatternGenerator.h"
18 #include "mozilla/intl/Locale.h"
19 #include "mozilla/intl/TimeZone.h"
20 #include "mozilla/Range.h"
21 #include "mozilla/Span.h"
22 
23 #include "builtin/Array.h"
24 #include "builtin/intl/CommonFunctions.h"
25 #include "builtin/intl/FormatBuffer.h"
26 #include "builtin/intl/LanguageTag.h"
27 #include "builtin/intl/SharedIntlData.h"
28 #include "builtin/intl/TimeZoneDataGenerated.h"
29 #include "gc/FreeOp.h"
30 #include "js/CharacterEncoding.h"
31 #include "js/Date.h"
32 #include "js/experimental/Intl.h"     // JS::AddMozDateTimeFormatConstructor
33 #include "js/friend/ErrorMessages.h"  // js::GetErrorMessage, JSMSG_*
34 #include "js/GCAPI.h"
35 #include "js/PropertyAndElement.h"  // JS_DefineFunctions, JS_DefineProperties
36 #include "js/PropertySpec.h"
37 #include "js/StableStringChars.h"
38 #include "vm/DateTime.h"
39 #include "vm/GlobalObject.h"
40 #include "vm/JSContext.h"
41 #include "vm/PlainObject.h"  // js::PlainObject
42 #include "vm/Runtime.h"
43 #include "vm/WellKnownAtom.h"  // js_*_str
44 
45 #include "vm/JSObject-inl.h"
46 #include "vm/NativeObject-inl.h"
47 
48 using namespace js;
49 
50 using JS::AutoStableStringChars;
51 using JS::ClippedTime;
52 using JS::TimeClip;
53 
54 using js::intl::DateTimeFormatOptions;
55 using js::intl::FormatBuffer;
56 using js::intl::INITIAL_CHAR_BUFFER_SIZE;
57 using js::intl::SharedIntlData;
58 
59 const JSClassOps DateTimeFormatObject::classOps_ = {
60     nullptr,                         // addProperty
61     nullptr,                         // delProperty
62     nullptr,                         // enumerate
63     nullptr,                         // newEnumerate
64     nullptr,                         // resolve
65     nullptr,                         // mayResolve
66     DateTimeFormatObject::finalize,  // finalize
67     nullptr,                         // call
68     nullptr,                         // hasInstance
69     nullptr,                         // construct
70     nullptr,                         // trace
71 };
72 
73 const JSClass DateTimeFormatObject::class_ = {
74     "Intl.DateTimeFormat",
75     JSCLASS_HAS_RESERVED_SLOTS(DateTimeFormatObject::SLOT_COUNT) |
76         JSCLASS_HAS_CACHED_PROTO(JSProto_DateTimeFormat) |
77         JSCLASS_FOREGROUND_FINALIZE,
78     &DateTimeFormatObject::classOps_, &DateTimeFormatObject::classSpec_};
79 
80 const JSClass& DateTimeFormatObject::protoClass_ = PlainObject::class_;
81 
dateTimeFormat_toSource(JSContext * cx,unsigned argc,Value * vp)82 static bool dateTimeFormat_toSource(JSContext* cx, unsigned argc, Value* vp) {
83   CallArgs args = CallArgsFromVp(argc, vp);
84   args.rval().setString(cx->names().DateTimeFormat);
85   return true;
86 }
87 
88 static const JSFunctionSpec dateTimeFormat_static_methods[] = {
89     JS_SELF_HOSTED_FN("supportedLocalesOf",
90                       "Intl_DateTimeFormat_supportedLocalesOf", 1, 0),
91     JS_FS_END};
92 
93 static const JSFunctionSpec dateTimeFormat_methods[] = {
94     JS_SELF_HOSTED_FN("resolvedOptions", "Intl_DateTimeFormat_resolvedOptions",
95                       0, 0),
96     JS_SELF_HOSTED_FN("formatToParts", "Intl_DateTimeFormat_formatToParts", 1,
97                       0),
98     JS_SELF_HOSTED_FN("formatRange", "Intl_DateTimeFormat_formatRange", 2, 0),
99     JS_SELF_HOSTED_FN("formatRangeToParts",
100                       "Intl_DateTimeFormat_formatRangeToParts", 2, 0),
101     JS_FN(js_toSource_str, dateTimeFormat_toSource, 0, 0),
102     JS_FS_END};
103 
104 static const JSPropertySpec dateTimeFormat_properties[] = {
105     JS_SELF_HOSTED_GET("format", "$Intl_DateTimeFormat_format_get", 0),
106     JS_STRING_SYM_PS(toStringTag, "Intl.DateTimeFormat", JSPROP_READONLY),
107     JS_PS_END};
108 
109 static bool DateTimeFormat(JSContext* cx, unsigned argc, Value* vp);
110 
111 const ClassSpec DateTimeFormatObject::classSpec_ = {
112     GenericCreateConstructor<DateTimeFormat, 0, gc::AllocKind::FUNCTION>,
113     GenericCreatePrototype<DateTimeFormatObject>,
114     dateTimeFormat_static_methods,
115     nullptr,
116     dateTimeFormat_methods,
117     dateTimeFormat_properties,
118     nullptr,
119     ClassSpec::DontDefineConstructor};
120 
121 /**
122  * 12.2.1 Intl.DateTimeFormat([ locales [, options]])
123  *
124  * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b
125  */
DateTimeFormat(JSContext * cx,const CallArgs & args,bool construct,DateTimeFormatOptions dtfOptions)126 static bool DateTimeFormat(JSContext* cx, const CallArgs& args, bool construct,
127                            DateTimeFormatOptions dtfOptions) {
128   // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code).
129 
130   // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
131   JSProtoKey protoKey = dtfOptions == DateTimeFormatOptions::Standard
132                             ? JSProto_DateTimeFormat
133                             : JSProto_Null;
134   RootedObject proto(cx);
135   if (!GetPrototypeFromBuiltinConstructor(cx, args, protoKey, &proto)) {
136     return false;
137   }
138 
139   Rooted<DateTimeFormatObject*> dateTimeFormat(cx);
140   dateTimeFormat = NewObjectWithClassProto<DateTimeFormatObject>(cx, proto);
141   if (!dateTimeFormat) {
142     return false;
143   }
144 
145   RootedValue thisValue(
146       cx, construct ? ObjectValue(*dateTimeFormat) : args.thisv());
147   HandleValue locales = args.get(0);
148   HandleValue options = args.get(1);
149 
150   // Step 3.
151   return intl::LegacyInitializeObject(
152       cx, dateTimeFormat, cx->names().InitializeDateTimeFormat, thisValue,
153       locales, options, dtfOptions, args.rval());
154 }
155 
DateTimeFormat(JSContext * cx,unsigned argc,Value * vp)156 static bool DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) {
157   CallArgs args = CallArgsFromVp(argc, vp);
158   return DateTimeFormat(cx, args, args.isConstructing(),
159                         DateTimeFormatOptions::Standard);
160 }
161 
MozDateTimeFormat(JSContext * cx,unsigned argc,Value * vp)162 static bool MozDateTimeFormat(JSContext* cx, unsigned argc, Value* vp) {
163   CallArgs args = CallArgsFromVp(argc, vp);
164 
165   // Don't allow to call mozIntl.DateTimeFormat as a function. That way we
166   // don't need to worry how to handle the legacy initialization semantics
167   // when applied on mozIntl.DateTimeFormat.
168   if (!ThrowIfNotConstructing(cx, args, "mozIntl.DateTimeFormat")) {
169     return false;
170   }
171 
172   return DateTimeFormat(cx, args, true,
173                         DateTimeFormatOptions::EnableMozExtensions);
174 }
175 
intl_DateTimeFormat(JSContext * cx,unsigned argc,Value * vp)176 bool js::intl_DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) {
177   CallArgs args = CallArgsFromVp(argc, vp);
178   MOZ_ASSERT(args.length() == 2);
179   MOZ_ASSERT(!args.isConstructing());
180   // intl_DateTimeFormat is an intrinsic for self-hosted JavaScript, so it
181   // cannot be used with "new", but it still has to be treated as a
182   // constructor.
183   return DateTimeFormat(cx, args, true, DateTimeFormatOptions::Standard);
184 }
185 
finalize(JSFreeOp * fop,JSObject * obj)186 void js::DateTimeFormatObject::finalize(JSFreeOp* fop, JSObject* obj) {
187   MOZ_ASSERT(fop->onMainThread());
188 
189   auto* dateTimeFormat = &obj->as<DateTimeFormatObject>();
190   mozilla::intl::DateTimeFormat* df = dateTimeFormat->getDateFormat();
191   mozilla::intl::DateIntervalFormat* dif =
192       dateTimeFormat->getDateIntervalFormat();
193 
194   if (df) {
195     intl::RemoveICUCellMemory(
196         fop, obj, DateTimeFormatObject::UDateFormatEstimatedMemoryUse);
197 
198     delete df;
199   }
200 
201   if (dif) {
202     intl::RemoveICUCellMemory(
203         fop, obj, DateTimeFormatObject::UDateIntervalFormatEstimatedMemoryUse);
204 
205     delete dif;
206   }
207 }
208 
AddMozDateTimeFormatConstructor(JSContext * cx,JS::Handle<JSObject * > intl)209 bool JS::AddMozDateTimeFormatConstructor(JSContext* cx,
210                                          JS::Handle<JSObject*> intl) {
211   RootedObject ctor(
212       cx, GlobalObject::createConstructor(cx, MozDateTimeFormat,
213                                           cx->names().DateTimeFormat, 0));
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   // 12.3.2
229   if (!JS_DefineFunctions(cx, ctor, dateTimeFormat_static_methods)) {
230     return false;
231   }
232 
233   // 12.4.4 and 12.4.5
234   if (!JS_DefineFunctions(cx, proto, dateTimeFormat_methods)) {
235     return false;
236   }
237 
238   // 12.4.2 and 12.4.3
239   if (!JS_DefineProperties(cx, proto, dateTimeFormat_properties)) {
240     return false;
241   }
242 
243   RootedValue ctorValue(cx, ObjectValue(*ctor));
244   return DefineDataProperty(cx, intl, cx->names().DateTimeFormat, ctorValue, 0);
245 }
246 
DefaultCalendar(JSContext * cx,const UniqueChars & locale,MutableHandleValue rval)247 static bool DefaultCalendar(JSContext* cx, const UniqueChars& locale,
248                             MutableHandleValue rval) {
249   auto calendar = mozilla::intl::Calendar::TryCreate(locale.get());
250   if (calendar.isErr()) {
251     intl::ReportInternalError(cx, calendar.unwrapErr());
252     return false;
253   }
254 
255   auto type = calendar.unwrap()->GetBcp47Type();
256   if (type.isErr()) {
257     intl::ReportInternalError(cx, type.unwrapErr());
258     return false;
259   }
260 
261   JSString* str = NewStringCopy<CanGC>(cx, type.unwrap());
262   if (!str) {
263     return false;
264   }
265 
266   rval.setString(str);
267   return true;
268 }
269 
intl_availableCalendars(JSContext * cx,unsigned argc,Value * vp)270 bool js::intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp) {
271   CallArgs args = CallArgsFromVp(argc, vp);
272   MOZ_ASSERT(args.length() == 1);
273   MOZ_ASSERT(args[0].isString());
274 
275   UniqueChars locale = intl::EncodeLocale(cx, args[0].toString());
276   if (!locale) {
277     return false;
278   }
279 
280   RootedObject calendars(cx, NewDenseEmptyArray(cx));
281   if (!calendars) {
282     return false;
283   }
284 
285   // We need the default calendar for the locale as the first result.
286   RootedValue defaultCalendar(cx);
287   if (!DefaultCalendar(cx, locale, &defaultCalendar)) {
288     return false;
289   }
290 
291   if (!NewbornArrayPush(cx, calendars, defaultCalendar)) {
292     return false;
293   }
294 
295   // Now get the calendars that "would make a difference", i.e., not the
296   // default.
297   auto keywords =
298       mozilla::intl::Calendar::GetBcp47KeywordValuesForLocale(locale.get());
299   if (keywords.isErr()) {
300     intl::ReportInternalError(cx, keywords.unwrapErr());
301     return false;
302   }
303 
304   for (auto keyword : keywords.unwrap()) {
305     if (keyword.isErr()) {
306       intl::ReportInternalError(cx);
307       return false;
308     }
309 
310     JSString* jscalendar = NewStringCopy<CanGC>(cx, keyword.unwrap());
311     if (!jscalendar) {
312       return false;
313     }
314     if (!NewbornArrayPush(cx, calendars, StringValue(jscalendar))) {
315       return false;
316     }
317   }
318 
319   args.rval().setObject(*calendars);
320   return true;
321 }
322 
intl_defaultCalendar(JSContext * cx,unsigned argc,Value * vp)323 bool js::intl_defaultCalendar(JSContext* cx, unsigned argc, Value* vp) {
324   CallArgs args = CallArgsFromVp(argc, vp);
325   MOZ_ASSERT(args.length() == 1);
326   MOZ_ASSERT(args[0].isString());
327 
328   UniqueChars locale = intl::EncodeLocale(cx, args[0].toString());
329   if (!locale) {
330     return false;
331   }
332 
333   return DefaultCalendar(cx, locale, args.rval());
334 }
335 
intl_IsValidTimeZoneName(JSContext * cx,unsigned argc,Value * vp)336 bool js::intl_IsValidTimeZoneName(JSContext* cx, unsigned argc, Value* vp) {
337   CallArgs args = CallArgsFromVp(argc, vp);
338   MOZ_ASSERT(args.length() == 1);
339   MOZ_ASSERT(args[0].isString());
340 
341   SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
342 
343   RootedString timeZone(cx, args[0].toString());
344   RootedAtom validatedTimeZone(cx);
345   if (!sharedIntlData.validateTimeZoneName(cx, timeZone, &validatedTimeZone)) {
346     return false;
347   }
348 
349   if (validatedTimeZone) {
350     cx->markAtom(validatedTimeZone);
351     args.rval().setString(validatedTimeZone);
352   } else {
353     args.rval().setNull();
354   }
355 
356   return true;
357 }
358 
intl_canonicalizeTimeZone(JSContext * cx,unsigned argc,Value * vp)359 bool js::intl_canonicalizeTimeZone(JSContext* cx, unsigned argc, Value* vp) {
360   CallArgs args = CallArgsFromVp(argc, vp);
361   MOZ_ASSERT(args.length() == 1);
362   MOZ_ASSERT(args[0].isString());
363 
364   SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
365 
366   // Some time zone names are canonicalized differently by ICU -- handle
367   // those first:
368   RootedString timeZone(cx, args[0].toString());
369   RootedAtom ianaTimeZone(cx);
370   if (!sharedIntlData.tryCanonicalizeTimeZoneConsistentWithIANA(
371           cx, timeZone, &ianaTimeZone)) {
372     return false;
373   }
374 
375   if (ianaTimeZone) {
376     cx->markAtom(ianaTimeZone);
377     args.rval().setString(ianaTimeZone);
378     return true;
379   }
380 
381   AutoStableStringChars stableChars(cx);
382   if (!stableChars.initTwoByte(cx, timeZone)) {
383     return false;
384   }
385 
386   FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> canonicalTimeZone(cx);
387   auto result = mozilla::intl::TimeZone::GetCanonicalTimeZoneID(
388       stableChars.twoByteRange(), canonicalTimeZone);
389   if (result.isErr()) {
390     intl::ReportInternalError(cx, result.unwrapErr());
391     return false;
392   }
393 
394   JSString* str = canonicalTimeZone.toString(cx);
395   if (!str) {
396     return false;
397   }
398 
399   args.rval().setString(str);
400   return true;
401 }
402 
intl_defaultTimeZone(JSContext * cx,unsigned argc,Value * vp)403 bool js::intl_defaultTimeZone(JSContext* cx, unsigned argc, Value* vp) {
404   CallArgs args = CallArgsFromVp(argc, vp);
405   MOZ_ASSERT(args.length() == 0);
406 
407   // The current default might be stale, because JS::ResetTimeZone() doesn't
408   // immediately update ICU's default time zone. So perform an update if
409   // needed.
410   js::ResyncICUDefaultTimeZone();
411 
412   FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> timeZone(cx);
413   auto result = mozilla::intl::TimeZone::GetDefaultTimeZone(timeZone);
414   if (result.isErr()) {
415     intl::ReportInternalError(cx, result.unwrapErr());
416     return false;
417   }
418 
419   JSString* str = timeZone.toString(cx);
420   if (!str) {
421     return false;
422   }
423 
424   args.rval().setString(str);
425   return true;
426 }
427 
intl_defaultTimeZoneOffset(JSContext * cx,unsigned argc,Value * vp)428 bool js::intl_defaultTimeZoneOffset(JSContext* cx, unsigned argc, Value* vp) {
429   CallArgs args = CallArgsFromVp(argc, vp);
430   MOZ_ASSERT(args.length() == 0);
431 
432   auto timeZone = mozilla::intl::TimeZone::TryCreate();
433   if (timeZone.isErr()) {
434     intl::ReportInternalError(cx, timeZone.unwrapErr());
435     return false;
436   }
437 
438   auto offset = timeZone.unwrap()->GetRawOffsetMs();
439   if (offset.isErr()) {
440     intl::ReportInternalError(cx, offset.unwrapErr());
441     return false;
442   }
443 
444   args.rval().setInt32(offset.unwrap());
445   return true;
446 }
447 
intl_isDefaultTimeZone(JSContext * cx,unsigned argc,Value * vp)448 bool js::intl_isDefaultTimeZone(JSContext* cx, unsigned argc, Value* vp) {
449   CallArgs args = CallArgsFromVp(argc, vp);
450   MOZ_ASSERT(args.length() == 1);
451   MOZ_ASSERT(args[0].isString() || args[0].isUndefined());
452 
453   // |undefined| is the default value when the Intl runtime caches haven't
454   // yet been initialized. Handle it the same way as a cache miss.
455   if (args[0].isUndefined()) {
456     args.rval().setBoolean(false);
457     return true;
458   }
459 
460   // The current default might be stale, because JS::ResetTimeZone() doesn't
461   // immediately update ICU's default time zone. So perform an update if
462   // needed.
463   js::ResyncICUDefaultTimeZone();
464 
465   FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> chars(cx);
466   auto result = mozilla::intl::TimeZone::GetDefaultTimeZone(chars);
467   if (result.isErr()) {
468     intl::ReportInternalError(cx, result.unwrapErr());
469     return false;
470   }
471 
472   JSLinearString* str = args[0].toString()->ensureLinear(cx);
473   if (!str) {
474     return false;
475   }
476 
477   bool equals;
478   if (str->length() == chars.length()) {
479     JS::AutoCheckCannotGC nogc;
480     equals =
481         str->hasLatin1Chars()
482             ? EqualChars(str->latin1Chars(nogc), chars.data(), str->length())
483             : EqualChars(str->twoByteChars(nogc), chars.data(), str->length());
484   } else {
485     equals = false;
486   }
487 
488   args.rval().setBoolean(equals);
489   return true;
490 }
491 
492 enum class HourCycle {
493   // 12 hour cycle, from 0 to 11.
494   H11,
495 
496   // 12 hour cycle, from 1 to 12.
497   H12,
498 
499   // 24 hour cycle, from 0 to 23.
500   H23,
501 
502   // 24 hour cycle, from 1 to 24.
503   H24
504 };
505 
DateTimeFormatLocale(JSContext * cx,HandleObject internals,mozilla::Maybe<mozilla::intl::DateTimeFormat::HourCycle> hourCycle=mozilla::Nothing ())506 static UniqueChars DateTimeFormatLocale(
507     JSContext* cx, HandleObject internals,
508     mozilla::Maybe<mozilla::intl::DateTimeFormat::HourCycle> hourCycle =
509         mozilla::Nothing()) {
510   RootedValue value(cx);
511   if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) {
512     return nullptr;
513   }
514 
515   // ICU expects calendar, numberingSystem, and hourCycle as Unicode locale
516   // extensions on locale.
517 
518   mozilla::intl::Locale tag;
519   {
520     RootedLinearString locale(cx, value.toString()->ensureLinear(cx));
521     if (!locale) {
522       return nullptr;
523     }
524 
525     if (!intl::ParseLocale(cx, locale, tag)) {
526       return nullptr;
527     }
528   }
529 
530   JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx);
531 
532   if (!GetProperty(cx, internals, internals, cx->names().calendar, &value)) {
533     return nullptr;
534   }
535 
536   {
537     JSLinearString* calendar = value.toString()->ensureLinear(cx);
538     if (!calendar) {
539       return nullptr;
540     }
541 
542     if (!keywords.emplaceBack("ca", calendar)) {
543       return nullptr;
544     }
545   }
546 
547   if (!GetProperty(cx, internals, internals, cx->names().numberingSystem,
548                    &value)) {
549     return nullptr;
550   }
551 
552   {
553     JSLinearString* numberingSystem = value.toString()->ensureLinear(cx);
554     if (!numberingSystem) {
555       return nullptr;
556     }
557 
558     if (!keywords.emplaceBack("nu", numberingSystem)) {
559       return nullptr;
560     }
561   }
562 
563   if (hourCycle) {
564     JSAtom* hourCycleStr;
565     switch (*hourCycle) {
566       case mozilla::intl::DateTimeFormat::HourCycle::H11:
567         hourCycleStr = cx->names().h11;
568         break;
569       case mozilla::intl::DateTimeFormat::HourCycle::H12:
570         hourCycleStr = cx->names().h12;
571         break;
572       case mozilla::intl::DateTimeFormat::HourCycle::H23:
573         hourCycleStr = cx->names().h23;
574         break;
575       case mozilla::intl::DateTimeFormat::HourCycle::H24:
576         hourCycleStr = cx->names().h24;
577         break;
578     }
579 
580     if (!keywords.emplaceBack("hc", hourCycleStr)) {
581       return nullptr;
582     }
583   }
584 
585   // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of
586   // the Unicode extension subtag. We're then relying on ICU to follow RFC
587   // 6067, which states that any trailing keywords using the same key
588   // should be ignored.
589   if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) {
590     return nullptr;
591   }
592 
593   FormatBuffer<char> buffer(cx);
594   if (auto result = tag.ToString(buffer); result.isErr()) {
595     intl::ReportInternalError(cx, result.unwrapErr());
596     return nullptr;
597   }
598   return buffer.extractStringZ();
599 }
600 
AssignTextComponent(JSContext * cx,HandleObject internals,HandlePropertyName property,mozilla::Maybe<mozilla::intl::DateTimeFormat::Text> * text)601 static bool AssignTextComponent(
602     JSContext* cx, HandleObject internals, HandlePropertyName property,
603     mozilla::Maybe<mozilla::intl::DateTimeFormat::Text>* text) {
604   RootedValue value(cx);
605   if (!GetProperty(cx, internals, internals, property, &value)) {
606     return false;
607   }
608 
609   if (value.isString()) {
610     JSLinearString* string = value.toString()->ensureLinear(cx);
611     if (!string) {
612       return false;
613     }
614     if (StringEqualsLiteral(string, "narrow")) {
615       *text = mozilla::Some(mozilla::intl::DateTimeFormat::Text::Narrow);
616     } else if (StringEqualsLiteral(string, "short")) {
617       *text = mozilla::Some(mozilla::intl::DateTimeFormat::Text::Short);
618     } else {
619       MOZ_ASSERT(StringEqualsLiteral(string, "long"));
620       *text = mozilla::Some(mozilla::intl::DateTimeFormat::Text::Long);
621     }
622   } else {
623     MOZ_ASSERT(value.isUndefined());
624   }
625 
626   return true;
627 }
628 
AssignNumericComponent(JSContext * cx,HandleObject internals,HandlePropertyName property,mozilla::Maybe<mozilla::intl::DateTimeFormat::Numeric> * numeric)629 static bool AssignNumericComponent(
630     JSContext* cx, HandleObject internals, HandlePropertyName property,
631     mozilla::Maybe<mozilla::intl::DateTimeFormat::Numeric>* numeric) {
632   RootedValue value(cx);
633   if (!GetProperty(cx, internals, internals, property, &value)) {
634     return false;
635   }
636 
637   if (value.isString()) {
638     JSLinearString* string = value.toString()->ensureLinear(cx);
639     if (!string) {
640       return false;
641     }
642     if (StringEqualsLiteral(string, "numeric")) {
643       *numeric = mozilla::Some(mozilla::intl::DateTimeFormat::Numeric::Numeric);
644     } else {
645       MOZ_ASSERT(StringEqualsLiteral(string, "2-digit"));
646       *numeric =
647           mozilla::Some(mozilla::intl::DateTimeFormat::Numeric::TwoDigit);
648     }
649   } else {
650     MOZ_ASSERT(value.isUndefined());
651   }
652 
653   return true;
654 }
655 
AssignMonthComponent(JSContext * cx,HandleObject internals,HandlePropertyName property,mozilla::Maybe<mozilla::intl::DateTimeFormat::Month> * month)656 static bool AssignMonthComponent(
657     JSContext* cx, HandleObject internals, HandlePropertyName property,
658     mozilla::Maybe<mozilla::intl::DateTimeFormat::Month>* month) {
659   RootedValue value(cx);
660   if (!GetProperty(cx, internals, internals, property, &value)) {
661     return false;
662   }
663 
664   if (value.isString()) {
665     JSLinearString* string = value.toString()->ensureLinear(cx);
666     if (!string) {
667       return false;
668     }
669     if (StringEqualsLiteral(string, "numeric")) {
670       *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::Numeric);
671     } else if (StringEqualsLiteral(string, "2-digit")) {
672       *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::TwoDigit);
673     } else if (StringEqualsLiteral(string, "long")) {
674       *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::Long);
675     } else if (StringEqualsLiteral(string, "short")) {
676       *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::Short);
677     } else {
678       MOZ_ASSERT(StringEqualsLiteral(string, "narrow"));
679       *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::Narrow);
680     }
681   } else {
682     MOZ_ASSERT(value.isUndefined());
683   }
684 
685   return true;
686 }
687 
AssignTimeZoneNameComponent(JSContext * cx,HandleObject internals,HandlePropertyName property,mozilla::Maybe<mozilla::intl::DateTimeFormat::TimeZoneName> * tzName)688 static bool AssignTimeZoneNameComponent(
689     JSContext* cx, HandleObject internals, HandlePropertyName property,
690     mozilla::Maybe<mozilla::intl::DateTimeFormat::TimeZoneName>* tzName) {
691   RootedValue value(cx);
692   if (!GetProperty(cx, internals, internals, property, &value)) {
693     return false;
694   }
695 
696   if (value.isString()) {
697     JSLinearString* string = value.toString()->ensureLinear(cx);
698     if (!string) {
699       return false;
700     }
701     if (StringEqualsLiteral(string, "long")) {
702       *tzName =
703           mozilla::Some(mozilla::intl::DateTimeFormat::TimeZoneName::Long);
704     } else if (StringEqualsLiteral(string, "short")) {
705       *tzName =
706           mozilla::Some(mozilla::intl::DateTimeFormat::TimeZoneName::Short);
707     } else if (StringEqualsLiteral(string, "shortOffset")) {
708       *tzName = mozilla::Some(
709           mozilla::intl::DateTimeFormat::TimeZoneName::ShortOffset);
710     } else if (StringEqualsLiteral(string, "longOffset")) {
711       *tzName = mozilla::Some(
712           mozilla::intl::DateTimeFormat::TimeZoneName::LongOffset);
713     } else if (StringEqualsLiteral(string, "shortGeneric")) {
714       *tzName = mozilla::Some(
715           mozilla::intl::DateTimeFormat::TimeZoneName::ShortGeneric);
716     } else {
717       MOZ_ASSERT(StringEqualsLiteral(string, "longGeneric"));
718       *tzName = mozilla::Some(
719           mozilla::intl::DateTimeFormat::TimeZoneName::LongGeneric);
720     }
721   } else {
722     MOZ_ASSERT(value.isUndefined());
723   }
724 
725   return true;
726 }
727 
AssignHourCycleComponent(JSContext * cx,HandleObject internals,HandlePropertyName property,mozilla::Maybe<mozilla::intl::DateTimeFormat::HourCycle> * hourCycle)728 static bool AssignHourCycleComponent(
729     JSContext* cx, HandleObject internals, HandlePropertyName property,
730     mozilla::Maybe<mozilla::intl::DateTimeFormat::HourCycle>* hourCycle) {
731   RootedValue value(cx);
732   if (!GetProperty(cx, internals, internals, property, &value)) {
733     return false;
734   }
735 
736   if (value.isString()) {
737     JSLinearString* string = value.toString()->ensureLinear(cx);
738     if (!string) {
739       return false;
740     }
741     if (StringEqualsLiteral(string, "h11")) {
742       *hourCycle = mozilla::Some(mozilla::intl::DateTimeFormat::HourCycle::H11);
743     } else if (StringEqualsLiteral(string, "h12")) {
744       *hourCycle = mozilla::Some(mozilla::intl::DateTimeFormat::HourCycle::H12);
745     } else if (StringEqualsLiteral(string, "h23")) {
746       *hourCycle = mozilla::Some(mozilla::intl::DateTimeFormat::HourCycle::H23);
747     } else {
748       MOZ_ASSERT(StringEqualsLiteral(string, "h24"));
749       *hourCycle = mozilla::Some(mozilla::intl::DateTimeFormat::HourCycle::H24);
750     }
751   } else {
752     MOZ_ASSERT(value.isUndefined());
753   }
754 
755   return true;
756 }
757 
AssignHour12Component(JSContext * cx,HandleObject internals,mozilla::Maybe<bool> * hour12)758 static bool AssignHour12Component(JSContext* cx, HandleObject internals,
759                                   mozilla::Maybe<bool>* hour12) {
760   RootedValue value(cx);
761   if (!GetProperty(cx, internals, internals, cx->names().hour12, &value)) {
762     return false;
763   }
764   if (value.isBoolean()) {
765     *hour12 = mozilla::Some(value.toBoolean());
766   } else {
767     MOZ_ASSERT(value.isUndefined());
768   }
769 
770   return true;
771 }
772 
AssignDateTimeLength(JSContext * cx,HandleObject internals,HandlePropertyName property,mozilla::Maybe<mozilla::intl::DateTimeFormat::Style> * style)773 static bool AssignDateTimeLength(
774     JSContext* cx, HandleObject internals, HandlePropertyName property,
775     mozilla::Maybe<mozilla::intl::DateTimeFormat::Style>* style) {
776   RootedValue value(cx);
777   if (!GetProperty(cx, internals, internals, property, &value)) {
778     return false;
779   }
780 
781   if (value.isString()) {
782     JSLinearString* string = value.toString()->ensureLinear(cx);
783     if (!string) {
784       return false;
785     }
786     if (StringEqualsLiteral(string, "full")) {
787       *style = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Full);
788     } else if (StringEqualsLiteral(string, "long")) {
789       *style = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Long);
790     } else if (StringEqualsLiteral(string, "medium")) {
791       *style = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Medium);
792     } else {
793       MOZ_ASSERT(StringEqualsLiteral(string, "short"));
794       *style = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short);
795     }
796   } else {
797     MOZ_ASSERT(value.isUndefined());
798   }
799 
800   return true;
801 }
802 
803 /**
804  * Returns a new mozilla::intl::DateTimeFormat with the locale and date-time
805  * formatting options of the given DateTimeFormat.
806  */
NewDateTimeFormat(JSContext * cx,Handle<DateTimeFormatObject * > dateTimeFormat)807 static mozilla::intl::DateTimeFormat* NewDateTimeFormat(
808     JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat) {
809   RootedValue value(cx);
810 
811   RootedObject internals(cx, intl::GetInternalsObject(cx, dateTimeFormat));
812   if (!internals) {
813     return nullptr;
814   }
815 
816   UniqueChars locale = DateTimeFormatLocale(cx, internals);
817   if (!locale) {
818     return nullptr;
819   }
820 
821   if (!GetProperty(cx, internals, internals, cx->names().timeZone, &value)) {
822     return nullptr;
823   }
824 
825   AutoStableStringChars timeZone(cx);
826   if (!timeZone.initTwoByte(cx, value.toString())) {
827     return nullptr;
828   }
829 
830   mozilla::Range<const char16_t> timeZoneChars = timeZone.twoByteRange();
831 
832   if (!GetProperty(cx, internals, internals, cx->names().pattern, &value)) {
833     return nullptr;
834   }
835   bool hasPattern = value.isString();
836 
837   if (!GetProperty(cx, internals, internals, cx->names().timeStyle, &value)) {
838     return nullptr;
839   }
840   bool hasStyle = value.isString();
841   if (!hasStyle) {
842     if (!GetProperty(cx, internals, internals, cx->names().dateStyle, &value)) {
843       return nullptr;
844     }
845     hasStyle = value.isString();
846   }
847 
848   mozilla::UniquePtr<mozilla::intl::DateTimeFormat> df = nullptr;
849   if (hasPattern) {
850     // This is a DateTimeFormat defined by a pattern option. This is internal
851     // to Mozilla, and not part of the ECMA-402 API.
852     if (!GetProperty(cx, internals, internals, cx->names().pattern, &value)) {
853       return nullptr;
854     }
855 
856     AutoStableStringChars pattern(cx);
857     if (!pattern.initTwoByte(cx, value.toString())) {
858       return nullptr;
859     }
860 
861     auto dfResult = mozilla::intl::DateTimeFormat::TryCreateFromPattern(
862         mozilla::MakeStringSpan(locale.get()), pattern.twoByteRange(),
863         mozilla::Some(timeZoneChars));
864     if (dfResult.isErr()) {
865       intl::ReportInternalError(cx, dfResult.unwrapErr());
866       return nullptr;
867     }
868 
869     df = dfResult.unwrap();
870   } else if (hasStyle) {
871     // This is a DateTimeFormat defined by a time style or date style.
872     mozilla::intl::DateTimeFormat::StyleBag style;
873     if (!AssignDateTimeLength(cx, internals, cx->names().timeStyle,
874                               &style.time)) {
875       return nullptr;
876     }
877     if (!AssignDateTimeLength(cx, internals, cx->names().dateStyle,
878                               &style.date)) {
879       return nullptr;
880     }
881     if (!AssignHourCycleComponent(cx, internals, cx->names().hourCycle,
882                                   &style.hourCycle)) {
883       return nullptr;
884     }
885 
886     if (!AssignHour12Component(cx, internals, &style.hour12)) {
887       return nullptr;
888     }
889 
890     SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
891 
892     mozilla::intl::DateTimePatternGenerator* gen =
893         sharedIntlData.getDateTimePatternGenerator(cx, locale.get());
894     if (!gen) {
895       return nullptr;
896     }
897     auto dfResult = mozilla::intl::DateTimeFormat::TryCreateFromStyle(
898         mozilla::MakeStringSpan(locale.get()), style, gen,
899         mozilla::Some(timeZoneChars));
900     if (dfResult.isErr()) {
901       intl::ReportInternalError(cx, dfResult.unwrapErr());
902       return nullptr;
903     }
904     df = dfResult.unwrap();
905   } else {
906     // This is a DateTimeFormat defined by a components bag.
907     mozilla::intl::DateTimeFormat::ComponentsBag bag;
908 
909     if (!AssignTextComponent(cx, internals, cx->names().era, &bag.era)) {
910       return nullptr;
911     }
912     if (!AssignNumericComponent(cx, internals, cx->names().year, &bag.year)) {
913       return nullptr;
914     }
915     if (!AssignMonthComponent(cx, internals, cx->names().month, &bag.month)) {
916       return nullptr;
917     }
918     if (!AssignNumericComponent(cx, internals, cx->names().day, &bag.day)) {
919       return nullptr;
920     }
921     if (!AssignTextComponent(cx, internals, cx->names().weekday,
922                              &bag.weekday)) {
923       return nullptr;
924     }
925     if (!AssignNumericComponent(cx, internals, cx->names().hour, &bag.hour)) {
926       return nullptr;
927     }
928     if (!AssignNumericComponent(cx, internals, cx->names().minute,
929                                 &bag.minute)) {
930       return nullptr;
931     }
932     if (!AssignNumericComponent(cx, internals, cx->names().second,
933                                 &bag.second)) {
934       return nullptr;
935     }
936     if (!AssignTimeZoneNameComponent(cx, internals, cx->names().timeZoneName,
937                                      &bag.timeZoneName)) {
938       return nullptr;
939     }
940     if (!AssignHourCycleComponent(cx, internals, cx->names().hourCycle,
941                                   &bag.hourCycle)) {
942       return nullptr;
943     }
944     if (!AssignTextComponent(cx, internals, cx->names().dayPeriod,
945                              &bag.dayPeriod)) {
946       return nullptr;
947     }
948     if (!AssignHour12Component(cx, internals, &bag.hour12)) {
949       return nullptr;
950     }
951 
952     if (!GetProperty(cx, internals, internals,
953                      cx->names().fractionalSecondDigits, &value)) {
954       return nullptr;
955     }
956     if (value.isInt32()) {
957       bag.fractionalSecondDigits = mozilla::Some(value.toInt32());
958     } else {
959       MOZ_ASSERT(value.isUndefined());
960     }
961 
962     SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
963     auto* dtpg = sharedIntlData.getDateTimePatternGenerator(cx, locale.get());
964     if (!dtpg) {
965       return nullptr;
966     }
967 
968     auto dfResult = mozilla::intl::DateTimeFormat::TryCreateFromComponents(
969         mozilla::MakeStringSpan(locale.get()), bag, dtpg,
970         mozilla::Some(timeZoneChars));
971     if (dfResult.isErr()) {
972       intl::ReportInternalError(cx, dfResult.unwrapErr());
973       return nullptr;
974     }
975     df = dfResult.unwrap();
976   }
977 
978   // ECMAScript requires the Gregorian calendar to be used from the beginning
979   // of ECMAScript time.
980   df->SetStartTimeIfGregorian(StartOfTime);
981 
982   return df.release();
983 }
984 
GetOrCreateDateTimeFormat(JSContext * cx,Handle<DateTimeFormatObject * > dateTimeFormat)985 static mozilla::intl::DateTimeFormat* GetOrCreateDateTimeFormat(
986     JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat) {
987   // Obtain a cached mozilla::intl::DateTimeFormat object.
988   mozilla::intl::DateTimeFormat* df = dateTimeFormat->getDateFormat();
989   if (df) {
990     return df;
991   }
992 
993   df = NewDateTimeFormat(cx, dateTimeFormat);
994   if (!df) {
995     return nullptr;
996   }
997   dateTimeFormat->setDateFormat(df);
998 
999   intl::AddICUCellMemory(dateTimeFormat,
1000                          DateTimeFormatObject::UDateFormatEstimatedMemoryUse);
1001   return df;
1002 }
1003 
1004 template <typename T>
SetResolvedProperty(JSContext * cx,HandleObject resolved,HandlePropertyName name,mozilla::Maybe<T> intlProp)1005 static bool SetResolvedProperty(JSContext* cx, HandleObject resolved,
1006                                 HandlePropertyName name,
1007                                 mozilla::Maybe<T> intlProp) {
1008   if (!intlProp) {
1009     return true;
1010   }
1011   JSString* str = NewStringCopyZ<CanGC>(
1012       cx, mozilla::intl::DateTimeFormat::ToString(*intlProp));
1013   if (!str) {
1014     return false;
1015   }
1016   RootedValue value(cx, StringValue(str));
1017   return DefineDataProperty(cx, resolved, name, value);
1018 }
1019 
intl_resolveDateTimeFormatComponents(JSContext * cx,unsigned argc,Value * vp)1020 bool js::intl_resolveDateTimeFormatComponents(JSContext* cx, unsigned argc,
1021                                               Value* vp) {
1022   CallArgs args = CallArgsFromVp(argc, vp);
1023   MOZ_ASSERT(args.length() == 3);
1024   MOZ_ASSERT(args[0].isObject());
1025   MOZ_ASSERT(args[1].isObject());
1026   MOZ_ASSERT(args[2].isBoolean());
1027 
1028   Rooted<DateTimeFormatObject*> dateTimeFormat(cx);
1029   dateTimeFormat = &args[0].toObject().as<DateTimeFormatObject>();
1030 
1031   RootedObject resolved(cx, &args[1].toObject());
1032 
1033   bool includeDateTimeFields = args[2].toBoolean();
1034 
1035   mozilla::intl::DateTimeFormat* df =
1036       GetOrCreateDateTimeFormat(cx, dateTimeFormat);
1037   if (!df) {
1038     return false;
1039   }
1040 
1041   auto result = df->ResolveComponents();
1042   if (result.isErr()) {
1043     intl::ReportInternalError(cx, result.unwrapErr());
1044     return false;
1045   }
1046 
1047   mozilla::intl::DateTimeFormat::ComponentsBag components = result.unwrap();
1048 
1049   // Map the resolved mozilla::intl::DateTimeFormat::ComponentsBag to the
1050   // options object as returned by DateTimeFormat.prototype.resolvedOptions.
1051   //
1052   // Resolved options must match the ordering as defined in:
1053   // https://tc39.es/ecma402/#sec-intl.datetimeformat.prototype.resolvedoptions
1054 
1055   if (!SetResolvedProperty(cx, resolved, cx->names().hourCycle,
1056                            components.hourCycle)) {
1057     return false;
1058   }
1059 
1060   if (components.hour12) {
1061     RootedValue value(cx, BooleanValue(*components.hour12));
1062     if (!DefineDataProperty(cx, resolved, cx->names().hour12, value)) {
1063       return false;
1064     }
1065   }
1066 
1067   if (!includeDateTimeFields) {
1068     args.rval().setUndefined();
1069     // Do not include date time fields.
1070     return true;
1071   }
1072 
1073   if (!SetResolvedProperty(cx, resolved, cx->names().weekday,
1074                            components.weekday)) {
1075     return false;
1076   }
1077   if (!SetResolvedProperty(cx, resolved, cx->names().era, components.era)) {
1078     return false;
1079   }
1080   if (!SetResolvedProperty(cx, resolved, cx->names().year, components.year)) {
1081     return false;
1082   }
1083   if (!SetResolvedProperty(cx, resolved, cx->names().month, components.month)) {
1084     return false;
1085   }
1086   if (!SetResolvedProperty(cx, resolved, cx->names().day, components.day)) {
1087     return false;
1088   }
1089   if (!SetResolvedProperty(cx, resolved, cx->names().dayPeriod,
1090                            components.dayPeriod)) {
1091     return false;
1092   }
1093   if (!SetResolvedProperty(cx, resolved, cx->names().hour, components.hour)) {
1094     return false;
1095   }
1096   if (!SetResolvedProperty(cx, resolved, cx->names().minute,
1097                            components.minute)) {
1098     return false;
1099   }
1100   if (!SetResolvedProperty(cx, resolved, cx->names().second,
1101                            components.second)) {
1102     return false;
1103   }
1104   if (!SetResolvedProperty(cx, resolved, cx->names().timeZoneName,
1105                            components.timeZoneName)) {
1106     return false;
1107   }
1108 
1109   if (components.fractionalSecondDigits) {
1110     RootedValue value(cx, Int32Value(*components.fractionalSecondDigits));
1111     if (!DefineDataProperty(cx, resolved, cx->names().fractionalSecondDigits,
1112                             value)) {
1113       return false;
1114     }
1115   }
1116 
1117   args.rval().setUndefined();
1118   return true;
1119 }
1120 
intl_FormatDateTime(JSContext * cx,const mozilla::intl::DateTimeFormat * df,ClippedTime x,MutableHandleValue result)1121 static bool intl_FormatDateTime(JSContext* cx,
1122                                 const mozilla::intl::DateTimeFormat* df,
1123                                 ClippedTime x, MutableHandleValue result) {
1124   MOZ_ASSERT(x.isValid());
1125 
1126   FormatBuffer<char16_t, INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
1127   auto dfResult = df->TryFormat(x.toDouble(), buffer);
1128   if (dfResult.isErr()) {
1129     intl::ReportInternalError(cx, dfResult.unwrapErr());
1130     return false;
1131   }
1132 
1133   JSString* str = buffer.toString(cx);
1134   if (!str) {
1135     return false;
1136   }
1137 
1138   result.setString(str);
1139   return true;
1140 }
1141 
1142 using FieldType = js::ImmutablePropertyNamePtr JSAtomState::*;
1143 
GetFieldTypeForPartType(mozilla::intl::DateTimePartType type)1144 static FieldType GetFieldTypeForPartType(mozilla::intl::DateTimePartType type) {
1145   switch (type) {
1146     case mozilla::intl::DateTimePartType::Literal:
1147       return &JSAtomState::literal;
1148     case mozilla::intl::DateTimePartType::Era:
1149       return &JSAtomState::era;
1150     case mozilla::intl::DateTimePartType::Year:
1151       return &JSAtomState::year;
1152     case mozilla::intl::DateTimePartType::YearName:
1153       return &JSAtomState::yearName;
1154     case mozilla::intl::DateTimePartType::RelatedYear:
1155       return &JSAtomState::relatedYear;
1156     case mozilla::intl::DateTimePartType::Month:
1157       return &JSAtomState::month;
1158     case mozilla::intl::DateTimePartType::Day:
1159       return &JSAtomState::day;
1160     case mozilla::intl::DateTimePartType::Hour:
1161       return &JSAtomState::hour;
1162     case mozilla::intl::DateTimePartType::Minute:
1163       return &JSAtomState::minute;
1164     case mozilla::intl::DateTimePartType::Second:
1165       return &JSAtomState::second;
1166     case mozilla::intl::DateTimePartType::Weekday:
1167       return &JSAtomState::weekday;
1168     case mozilla::intl::DateTimePartType::DayPeriod:
1169       return &JSAtomState::dayPeriod;
1170     case mozilla::intl::DateTimePartType::TimeZoneName:
1171       return &JSAtomState::timeZoneName;
1172     case mozilla::intl::DateTimePartType::FractionalSecondDigits:
1173       return &JSAtomState::fractionalSecond;
1174     case mozilla::intl::DateTimePartType::Unknown:
1175       return &JSAtomState::unknown;
1176   }
1177 
1178   MOZ_CRASH(
1179       "unenumerated, undocumented format field returned "
1180       "by iterator");
1181 }
1182 
GetFieldTypeForPartSource(mozilla::intl::DateTimePartSource source)1183 static FieldType GetFieldTypeForPartSource(
1184     mozilla::intl::DateTimePartSource source) {
1185   switch (source) {
1186     case mozilla::intl::DateTimePartSource::Shared:
1187       return &JSAtomState::shared;
1188     case mozilla::intl::DateTimePartSource::StartRange:
1189       return &JSAtomState::startRange;
1190     case mozilla::intl::DateTimePartSource::EndRange:
1191       return &JSAtomState::endRange;
1192   }
1193 
1194   MOZ_CRASH(
1195       "unenumerated, undocumented format field returned "
1196       "by iterator");
1197 }
1198 
1199 // A helper function to create an ArrayObject from DateTimePart objects.
1200 // When hasNoSource is true, we don't need to create the ||Source|| property for
1201 // the DateTimePart object.
CreateDateTimePartArray(JSContext * cx,mozilla::Span<const char16_t> formattedSpan,bool hasNoSource,const mozilla::intl::DateTimePartVector & parts,MutableHandleValue result)1202 static bool CreateDateTimePartArray(
1203     JSContext* cx, mozilla::Span<const char16_t> formattedSpan,
1204     bool hasNoSource, const mozilla::intl::DateTimePartVector& parts,
1205     MutableHandleValue result) {
1206   RootedString overallResult(cx, NewStringCopy<CanGC>(cx, formattedSpan));
1207   if (!overallResult) {
1208     return false;
1209   }
1210 
1211   RootedArrayObject partsArray(cx,
1212                                NewDenseFullyAllocatedArray(cx, parts.length()));
1213   if (!partsArray) {
1214     return false;
1215   }
1216   partsArray->ensureDenseInitializedLength(0, parts.length());
1217 
1218   if (overallResult->length() == 0) {
1219     // An empty string contains no parts, so avoid extra work below.
1220     result.setObject(*partsArray);
1221     return true;
1222   }
1223 
1224   RootedObject singlePart(cx);
1225   RootedValue val(cx);
1226 
1227   size_t index = 0;
1228   size_t beginIndex = 0;
1229   for (const mozilla::intl::DateTimePart& part : parts) {
1230     singlePart = NewPlainObject(cx);
1231     if (!singlePart) {
1232       return false;
1233     }
1234 
1235     FieldType type = GetFieldTypeForPartType(part.mType);
1236     val = StringValue(cx->names().*type);
1237     if (!DefineDataProperty(cx, singlePart, cx->names().type, val)) {
1238       return false;
1239     }
1240 
1241     MOZ_ASSERT(part.mEndIndex > beginIndex);
1242     JSLinearString* partStr = NewDependentString(cx, overallResult, beginIndex,
1243                                                  part.mEndIndex - beginIndex);
1244     if (!partStr) {
1245       return false;
1246     }
1247     val = StringValue(partStr);
1248     if (!DefineDataProperty(cx, singlePart, cx->names().value, val)) {
1249       return false;
1250     }
1251 
1252     if (!hasNoSource) {
1253       FieldType source = GetFieldTypeForPartSource(part.mSource);
1254       val = StringValue(cx->names().*source);
1255       if (!DefineDataProperty(cx, singlePart, cx->names().source, val)) {
1256         return false;
1257       }
1258     }
1259 
1260     beginIndex = part.mEndIndex;
1261     partsArray->initDenseElement(index++, ObjectValue(*singlePart));
1262   }
1263 
1264   MOZ_ASSERT(index == parts.length());
1265   MOZ_ASSERT(beginIndex == formattedSpan.size());
1266   result.setObject(*partsArray);
1267   return true;
1268 }
1269 
intl_FormatToPartsDateTime(JSContext * cx,const mozilla::intl::DateTimeFormat * df,ClippedTime x,bool hasNoSource,MutableHandleValue result)1270 static bool intl_FormatToPartsDateTime(JSContext* cx,
1271                                        const mozilla::intl::DateTimeFormat* df,
1272                                        ClippedTime x, bool hasNoSource,
1273                                        MutableHandleValue result) {
1274   MOZ_ASSERT(x.isValid());
1275 
1276   FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
1277   mozilla::intl::DateTimePartVector parts;
1278   auto r = df->TryFormatToParts(x.toDouble(), buffer, parts);
1279   if (r.isErr()) {
1280     intl::ReportInternalError(cx, r.unwrapErr());
1281     return false;
1282   }
1283 
1284   return CreateDateTimePartArray(cx, buffer, hasNoSource, parts, result);
1285 }
1286 
intl_FormatDateTime(JSContext * cx,unsigned argc,Value * vp)1287 bool js::intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp) {
1288   CallArgs args = CallArgsFromVp(argc, vp);
1289   MOZ_ASSERT(args.length() == 3);
1290   MOZ_ASSERT(args[0].isObject());
1291   MOZ_ASSERT(args[1].isNumber());
1292   MOZ_ASSERT(args[2].isBoolean());
1293 
1294   Rooted<DateTimeFormatObject*> dateTimeFormat(cx);
1295   dateTimeFormat = &args[0].toObject().as<DateTimeFormatObject>();
1296 
1297   bool formatToParts = args[2].toBoolean();
1298 
1299   ClippedTime x = TimeClip(args[1].toNumber());
1300   if (!x.isValid()) {
1301     JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
1302                               JSMSG_DATE_NOT_FINITE, "DateTimeFormat",
1303                               formatToParts ? "formatToParts" : "format");
1304     return false;
1305   }
1306 
1307   mozilla::intl::DateTimeFormat* df =
1308       GetOrCreateDateTimeFormat(cx, dateTimeFormat);
1309   if (!df) {
1310     return false;
1311   }
1312 
1313   // Use the DateTimeFormat to actually format the time stamp.
1314   return formatToParts ? intl_FormatToPartsDateTime(
1315                              cx, df, x, /* hasNoSource */ true, args.rval())
1316                        : intl_FormatDateTime(cx, df, x, args.rval());
1317 }
1318 
1319 /**
1320  * Returns a new DateIntervalFormat with the locale and date-time formatting
1321  * options of the given DateTimeFormat.
1322  */
NewDateIntervalFormat(JSContext * cx,Handle<DateTimeFormatObject * > dateTimeFormat,mozilla::intl::DateTimeFormat & mozDtf)1323 static mozilla::intl::DateIntervalFormat* NewDateIntervalFormat(
1324     JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat,
1325     mozilla::intl::DateTimeFormat& mozDtf) {
1326   RootedValue value(cx);
1327   RootedObject internals(cx, intl::GetInternalsObject(cx, dateTimeFormat));
1328   if (!internals) {
1329     return nullptr;
1330   }
1331 
1332   FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> pattern(cx);
1333   auto result = mozDtf.GetPattern(pattern);
1334   if (result.isErr()) {
1335     intl::ReportInternalError(cx, result.unwrapErr());
1336     return nullptr;
1337   }
1338 
1339   // Determine the hour cycle used in the resolved pattern. This is needed to
1340   // workaround <https://unicode-org.atlassian.net/browse/ICU-21154> and
1341   // <https://unicode-org.atlassian.net/browse/ICU-21155>.
1342   mozilla::Maybe<mozilla::intl::DateTimeFormat::HourCycle> hcPattern =
1343       mozilla::intl::DateTimeFormat::HourCycleFromPattern(pattern);
1344 
1345   UniqueChars locale = DateTimeFormatLocale(cx, internals, hcPattern);
1346   if (!locale) {
1347     return nullptr;
1348   }
1349 
1350   if (!GetProperty(cx, internals, internals, cx->names().timeZone, &value)) {
1351     return nullptr;
1352   }
1353 
1354   AutoStableStringChars timeZone(cx);
1355   if (!timeZone.initTwoByte(cx, value.toString())) {
1356     return nullptr;
1357   }
1358   mozilla::Span<const char16_t> timeZoneChars = timeZone.twoByteRange();
1359 
1360   FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> skeleton(cx);
1361   auto skelResult = mozDtf.GetOriginalSkeleton(skeleton, hcPattern);
1362   if (skelResult.isErr()) {
1363     intl::ReportInternalError(cx, skelResult.unwrapErr());
1364     return nullptr;
1365   }
1366 
1367   auto dif = mozilla::intl::DateIntervalFormat::TryCreate(
1368       mozilla::MakeStringSpan(locale.get()), skeleton, timeZoneChars);
1369 
1370   if (dif.isErr()) {
1371     js::intl::ReportInternalError(cx, dif.unwrapErr());
1372     return nullptr;
1373   }
1374 
1375   return dif.unwrap().release();
1376 }
1377 
GetOrCreateDateIntervalFormat(JSContext * cx,Handle<DateTimeFormatObject * > dateTimeFormat,mozilla::intl::DateTimeFormat & mozDtf)1378 static mozilla::intl::DateIntervalFormat* GetOrCreateDateIntervalFormat(
1379     JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat,
1380     mozilla::intl::DateTimeFormat& mozDtf) {
1381   // Obtain a cached DateIntervalFormat object.
1382   mozilla::intl::DateIntervalFormat* dif =
1383       dateTimeFormat->getDateIntervalFormat();
1384   if (dif) {
1385     return dif;
1386   }
1387 
1388   dif = NewDateIntervalFormat(cx, dateTimeFormat, mozDtf);
1389   if (!dif) {
1390     return nullptr;
1391   }
1392   dateTimeFormat->setDateIntervalFormat(dif);
1393 
1394   intl::AddICUCellMemory(
1395       dateTimeFormat,
1396       DateTimeFormatObject::UDateIntervalFormatEstimatedMemoryUse);
1397   return dif;
1398 }
1399 
1400 /**
1401  * PartitionDateTimeRangePattern ( dateTimeFormat, x, y )
1402  */
PartitionDateTimeRangePattern(JSContext * cx,const mozilla::intl::DateTimeFormat * df,const mozilla::intl::DateIntervalFormat * dif,mozilla::intl::AutoFormattedDateInterval & formatted,ClippedTime x,ClippedTime y,bool * equal)1403 static bool PartitionDateTimeRangePattern(
1404     JSContext* cx, const mozilla::intl::DateTimeFormat* df,
1405     const mozilla::intl::DateIntervalFormat* dif,
1406     mozilla::intl::AutoFormattedDateInterval& formatted, ClippedTime x,
1407     ClippedTime y, bool* equal) {
1408   MOZ_ASSERT(x.isValid());
1409   MOZ_ASSERT(y.isValid());
1410   MOZ_ASSERT(x.toDouble() <= y.toDouble());
1411 
1412   // We can't access the calendar used by UDateIntervalFormat to change it to a
1413   // proleptic Gregorian calendar. Instead we need to call a different formatter
1414   // function which accepts UCalendar instead of UDate.
1415   // But creating new UCalendar objects for each call is slow, so when we can
1416   // ensure that the input dates are later than the Gregorian change date,
1417   // directly call the formatter functions taking UDate.
1418 
1419   // The Gregorian change date "1582-10-15T00:00:00.000Z".
1420   constexpr double GregorianChangeDate = -12219292800000.0;
1421 
1422   // Add a full day to account for time zone offsets.
1423   constexpr double GregorianChangeDatePlusOneDay =
1424       GregorianChangeDate + msPerDay;
1425 
1426   mozilla::intl::ICUResult result = Ok();
1427   if (x.toDouble() < GregorianChangeDatePlusOneDay) {
1428     // Create calendar objects for the start and end date by cloning the date
1429     // formatter calendar. The date formatter calendar already has the correct
1430     // time zone set and was changed to use a proleptic Gregorian calendar.
1431     auto startCal = df->CloneCalendar(x.toDouble());
1432     if (startCal.isErr()) {
1433       intl::ReportInternalError(cx, startCal.unwrapErr());
1434       return false;
1435     }
1436 
1437     auto endCal = df->CloneCalendar(y.toDouble());
1438     if (endCal.isErr()) {
1439       intl::ReportInternalError(cx, endCal.unwrapErr());
1440       return false;
1441     }
1442 
1443     result = dif->TryFormatCalendar(*startCal.unwrap(), *endCal.unwrap(),
1444                                     formatted, equal);
1445   } else {
1446     // The common fast path which doesn't require creating calendar objects.
1447     result =
1448         dif->TryFormatDateTime(x.toDouble(), y.toDouble(), formatted, equal);
1449   }
1450 
1451   if (result.isErr()) {
1452     intl::ReportInternalError(cx, result.unwrapErr());
1453     return false;
1454   }
1455 
1456   return true;
1457 }
1458 
1459 /**
1460  * FormatDateTimeRange( dateTimeFormat, x, y )
1461  */
FormatDateTimeRange(JSContext * cx,const mozilla::intl::DateTimeFormat * df,const mozilla::intl::DateIntervalFormat * dif,ClippedTime x,ClippedTime y,MutableHandleValue result)1462 static bool FormatDateTimeRange(JSContext* cx,
1463                                 const mozilla::intl::DateTimeFormat* df,
1464                                 const mozilla::intl::DateIntervalFormat* dif,
1465                                 ClippedTime x, ClippedTime y,
1466                                 MutableHandleValue result) {
1467   mozilla::intl::AutoFormattedDateInterval formatted;
1468   if (!formatted.IsValid()) {
1469     intl::ReportInternalError(cx, formatted.GetError());
1470     return false;
1471   }
1472 
1473   bool equal;
1474   if (!PartitionDateTimeRangePattern(cx, df, dif, formatted, x, y, &equal)) {
1475     return false;
1476   }
1477 
1478   // PartitionDateTimeRangePattern, step 12.
1479   if (equal) {
1480     return intl_FormatDateTime(cx, df, x, result);
1481   }
1482 
1483   auto spanResult = formatted.ToSpan();
1484   if (spanResult.isErr()) {
1485     intl::ReportInternalError(cx, spanResult.unwrapErr());
1486     return false;
1487   }
1488   JSString* resultStr = NewStringCopy<CanGC>(cx, spanResult.unwrap());
1489   if (!resultStr) {
1490     return false;
1491   }
1492 
1493   result.setString(resultStr);
1494   return true;
1495 }
1496 
1497 /**
1498  * FormatDateTimeRangeToParts ( dateTimeFormat, x, y )
1499  */
FormatDateTimeRangeToParts(JSContext * cx,const mozilla::intl::DateTimeFormat * df,const mozilla::intl::DateIntervalFormat * dif,ClippedTime x,ClippedTime y,MutableHandleValue result)1500 static bool FormatDateTimeRangeToParts(
1501     JSContext* cx, const mozilla::intl::DateTimeFormat* df,
1502     const mozilla::intl::DateIntervalFormat* dif, ClippedTime x, ClippedTime y,
1503     MutableHandleValue result) {
1504   mozilla::intl::AutoFormattedDateInterval formatted;
1505   if (!formatted.IsValid()) {
1506     intl::ReportInternalError(cx, formatted.GetError());
1507     return false;
1508   }
1509 
1510   bool equal;
1511   if (!PartitionDateTimeRangePattern(cx, df, dif, formatted, x, y, &equal)) {
1512     return false;
1513   }
1514 
1515   // PartitionDateTimeRangePattern, step 12.
1516   if (equal) {
1517     return intl_FormatToPartsDateTime(cx, df, x, /* hasNoSource */ false,
1518                                       result);
1519   }
1520 
1521   mozilla::intl::DateTimePartVector parts;
1522   auto r = dif->TryFormattedToParts(formatted, parts);
1523   if (r.isErr()) {
1524     intl::ReportInternalError(cx, r.unwrapErr());
1525     return false;
1526   }
1527 
1528   auto spanResult = formatted.ToSpan();
1529   if (spanResult.isErr()) {
1530     intl::ReportInternalError(cx, spanResult.unwrapErr());
1531     return false;
1532   }
1533   return CreateDateTimePartArray(cx, spanResult.unwrap(),
1534                                  /* hasNoSource */ false, parts, result);
1535 }
1536 
intl_FormatDateTimeRange(JSContext * cx,unsigned argc,Value * vp)1537 bool js::intl_FormatDateTimeRange(JSContext* cx, unsigned argc, Value* vp) {
1538   CallArgs args = CallArgsFromVp(argc, vp);
1539   MOZ_ASSERT(args.length() == 4);
1540   MOZ_ASSERT(args[0].isObject());
1541   MOZ_ASSERT(args[1].isNumber());
1542   MOZ_ASSERT(args[2].isNumber());
1543   MOZ_ASSERT(args[3].isBoolean());
1544 
1545   Rooted<DateTimeFormatObject*> dateTimeFormat(cx);
1546   dateTimeFormat = &args[0].toObject().as<DateTimeFormatObject>();
1547 
1548   bool formatToParts = args[3].toBoolean();
1549 
1550   // PartitionDateTimeRangePattern, steps 1-2.
1551   ClippedTime x = TimeClip(args[1].toNumber());
1552   if (!x.isValid()) {
1553     JS_ReportErrorNumberASCII(
1554         cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE, "DateTimeFormat",
1555         formatToParts ? "formatRangeToParts" : "formatRange");
1556     return false;
1557   }
1558 
1559   // PartitionDateTimeRangePattern, steps 3-4.
1560   ClippedTime y = TimeClip(args[2].toNumber());
1561   if (!y.isValid()) {
1562     JS_ReportErrorNumberASCII(
1563         cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE, "DateTimeFormat",
1564         formatToParts ? "formatRangeToParts" : "formatRange");
1565     return false;
1566   }
1567 
1568   // Self-hosted code should have checked this condition.
1569   MOZ_ASSERT(x.toDouble() <= y.toDouble(),
1570              "start date mustn't be after the end date");
1571 
1572   mozilla::intl::DateTimeFormat* df =
1573       GetOrCreateDateTimeFormat(cx, dateTimeFormat);
1574   if (!df) {
1575     return false;
1576   }
1577 
1578   mozilla::intl::DateIntervalFormat* dif =
1579       GetOrCreateDateIntervalFormat(cx, dateTimeFormat, *df);
1580   if (!dif) {
1581     return false;
1582   }
1583 
1584   // Use the DateIntervalFormat to actually format the time range.
1585   return formatToParts
1586              ? FormatDateTimeRangeToParts(cx, df, dif, x, y, args.rval())
1587              : FormatDateTimeRange(cx, df, dif, x, y, args.rval());
1588 }
1589