From 35ca1152a7dad4225d41e4bfa37cd45aa2a55ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bargull?= Date: Mon, 10 Jul 2017 04:55:54 -0700 Subject: [PATCH] Bug 1379222 - Avoid [[Get]] for "prototype" property when calling builtin constructors. r=jandem --- js/src/builtin/MapObject.cpp | 6 +- js/src/builtin/Promise.cpp | 14 +- js/src/builtin/RegExp.cpp | 6 +- js/src/builtin/WeakMapObject.cpp | 7 +- js/src/builtin/WeakSetObject.cpp | 3 +- js/src/builtin/intl/Collator.cpp | 944 ++++---- js/src/builtin/intl/DateTimeFormat.cpp | 2393 ++++++++++---------- js/src/builtin/intl/Locale.cpp | 2 +- js/src/builtin/intl/NumberFormat.cpp | 1772 +++++++-------- js/src/builtin/intl/RelativeTimeFormat.cpp | 574 ++--- js/src/jsarray.cpp | 2 +- js/src/jsbool.cpp | 4 +- js/src/jsdate.cpp | 3 +- js/src/jsfun.cpp | 2 +- js/src/jsnum.cpp | 9 +- js/src/jsobj.cpp | 11 - js/src/jsobj.h | 17 +- js/src/jsstr.cpp | 3 +- js/src/vm/ErrorObject.cpp | 4 +- js/src/vm/SharedArrayObject.cpp | 3 +- js/src/vm/TypedArrayObject.cpp | 86 +- 21 files changed, 2922 insertions(+), 2943 deletions(-) diff --git a/js/src/builtin/MapObject.cpp b/js/src/builtin/MapObject.cpp index fe748a6bde..4e02790cca 100644 --- a/js/src/builtin/MapObject.cpp +++ b/js/src/builtin/MapObject.cpp @@ -590,8 +590,7 @@ MapObject::construct(JSContext* cx, unsigned argc, Value* vp) return false; RootedObject proto(cx); - RootedObject newTarget(cx, &args.newTarget().toObject()); - if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) + if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) return false; Rooted obj(cx, MapObject::create(cx, proto)); @@ -1196,8 +1195,7 @@ SetObject::construct(JSContext* cx, unsigned argc, Value* vp) return false; RootedObject proto(cx); - RootedObject newTarget(cx, &args.newTarget().toObject()); - if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) + if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) return false; Rooted obj(cx, SetObject::create(cx, proto)); diff --git a/js/src/builtin/Promise.cpp b/js/src/builtin/Promise.cpp index 9660a758a1..524b7c5b38 100644 --- a/js/src/builtin/Promise.cpp +++ b/js/src/builtin/Promise.cpp @@ -2009,7 +2009,6 @@ PromiseConstructor(JSContext* cx, unsigned argc, Value* vp) // Steps 3-10. RootedObject newTarget(cx, &args.newTarget().toObject()); - RootedObject originalNewTarget(cx, newTarget); bool needsWrapping = false; // If the constructor is called via an Xray wrapper, then the newTarget @@ -2061,10 +2060,15 @@ PromiseConstructor(JSContext* cx, unsigned argc, Value* vp) } RootedObject proto(cx); - if (!GetPrototypeFromConstructor(cx, needsWrapping ? newTarget : originalNewTarget, &proto)) - return false; - if (needsWrapping && !cx->compartment()->wrap(cx, &proto)) - return false; + if (needsWrapping) { + if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) + return false; + if (!cx->compartment()->wrap(cx, &proto)) + return false; + } else { + if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) + return false; + } Rooted promise(cx, PromiseObject::create(cx, executor, proto, needsWrapping)); if (!promise) return false; diff --git a/js/src/builtin/RegExp.cpp b/js/src/builtin/RegExp.cpp index ef5f629249..9004101e95 100644 --- a/js/src/builtin/RegExp.cpp +++ b/js/src/builtin/RegExp.cpp @@ -530,7 +530,7 @@ js::regexp_construct(JSContext* cx, unsigned argc, Value* vp) return false; // We can delay step 3 and step 4a until later, during - // GetPrototypeFromCallableConstructor calls. Accessing the new.target + // GetPrototypeFromBuiltinConstructor calls. Accessing the new.target // and the callee from the stack is unobservable. if (!args.isConstructing()) { // Step 3.b. @@ -578,7 +578,7 @@ js::regexp_construct(JSContext* cx, unsigned argc, Value* vp) // Step 7. RootedObject proto(cx); - if (!GetPrototypeFromCallableConstructor(cx, args, &proto)) + if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) return false; Rooted regexp(cx, RegExpAlloc(cx, proto)); @@ -637,7 +637,7 @@ js::regexp_construct(JSContext* cx, unsigned argc, Value* vp) // Step 7. RootedObject proto(cx); - if (!GetPrototypeFromCallableConstructor(cx, args, &proto)) + if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) return false; Rooted regexp(cx, RegExpAlloc(cx, proto)); diff --git a/js/src/builtin/WeakMapObject.cpp b/js/src/builtin/WeakMapObject.cpp index 82b6ef1a7c..55b5887534 100644 --- a/js/src/builtin/WeakMapObject.cpp +++ b/js/src/builtin/WeakMapObject.cpp @@ -294,8 +294,11 @@ WeakMap_construct(JSContext* cx, unsigned argc, Value* vp) if (!ThrowIfNotConstructing(cx, args, "WeakMap")) return false; - RootedObject newTarget(cx, &args.newTarget().toObject()); - RootedObject obj(cx, CreateThis(cx, &WeakMapObject::class_, newTarget)); + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) + return false; + + RootedObject obj(cx, NewObjectWithClassProto(cx, proto)); if (!obj) return false; diff --git a/js/src/builtin/WeakSetObject.cpp b/js/src/builtin/WeakSetObject.cpp index bb9708f6f9..15b543e1e4 100644 --- a/js/src/builtin/WeakSetObject.cpp +++ b/js/src/builtin/WeakSetObject.cpp @@ -92,8 +92,7 @@ WeakSetObject::construct(JSContext* cx, unsigned argc, Value* vp) return false; RootedObject proto(cx); - RootedObject newTarget(cx, &args.newTarget().toObject()); - if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) + if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) return false; Rooted obj(cx, WeakSetObject::create(cx, proto)); diff --git a/js/src/builtin/intl/Collator.cpp b/js/src/builtin/intl/Collator.cpp index aafb7535c7..72811d259f 100644 --- a/js/src/builtin/intl/Collator.cpp +++ b/js/src/builtin/intl/Collator.cpp @@ -1,472 +1,472 @@ -/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/* Intl.Collator implementation. */ - -#include "builtin/intl/Collator.h" - -#include "mozilla/Assertions.h" -#include "mozilla/Span.h" - -#include "jsapi.h" -#include "jscntxt.h" - -#include "builtin/intl/CommonFunctions.h" -#include "builtin/intl/ICUHeader.h" -#include "builtin/intl/LanguageTag.h" -#include "builtin/intl/ScopedICUObject.h" -#include "builtin/intl/SharedIntlData.h" -#include "js/TypeDecls.h" -#include "vm/GlobalObject.h" -#include "vm/Runtime.h" -#include "vm/String.h" - -#include "jsobjinlines.h" - -using namespace js; -using js::intl::IcuLocale; -using js::intl::ReportInternalError; -using js::intl::SharedIntlData; -using js::intl::StringsAreEqual; - -/******************** Collator ********************/ - -const ClassOps CollatorObject::classOps_ = { - nullptr, /* addProperty */ - nullptr, /* delProperty */ - nullptr, /* getProperty */ - nullptr, /* setProperty */ - nullptr, /* enumerate */ - nullptr, /* resolve */ - nullptr, /* mayResolve */ - CollatorObject::finalize -}; - -const Class CollatorObject::class_ = { - js_Object_str, - JSCLASS_HAS_RESERVED_SLOTS(CollatorObject::SLOT_COUNT) | - JSCLASS_FOREGROUND_FINALIZE, - &CollatorObject::classOps_ -}; - -#if JS_HAS_TOSOURCE -static bool -collator_toSource(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - args.rval().setString(cx->names().Collator); - return true; -} -#endif - -static const JSFunctionSpec collator_static_methods[] = { - JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_Collator_supportedLocalesOf", 1, 0), - JS_FS_END -}; - -static const JSFunctionSpec collator_methods[] = { - JS_SELF_HOSTED_FN("resolvedOptions", "Intl_Collator_resolvedOptions", 0, 0), -#if JS_HAS_TOSOURCE - JS_FN(js_toSource_str, collator_toSource, 0, 0), -#endif - JS_FS_END -}; - -/** - * 10.1.2 Intl.Collator([ locales [, options]]) - * - * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b - */ -static bool -Collator(JSContext* cx, const CallArgs& args) -{ - // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code). - - // Steps 2-5 (Inlined 9.1.14, OrdinaryCreateFromConstructor). - RootedObject proto(cx); - if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto)) - return false; - - if (!proto) { - proto = GlobalObject::getOrCreateCollatorPrototype(cx, cx->global()); - if (!proto) - return false; - } - - Rooted collator(cx, NewObjectWithGivenProto(cx, proto)); - if (!collator) - return false; - - collator->setReservedSlot(CollatorObject::INTERNALS_SLOT, NullValue()); - collator->setReservedSlot(CollatorObject::UCOLLATOR_SLOT, PrivateValue(nullptr)); - - RootedValue locales(cx, args.get(0)); - RootedValue options(cx, args.get(1)); - - // Step 6. - if (!intl::InitializeObject(cx, collator, cx->names().InitializeCollator, locales, options)) - return false; - - args.rval().setObject(*collator); - return true; -} - -static bool -Collator(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - return Collator(cx, args); -} - -bool -js::intl_Collator(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 2); - MOZ_ASSERT(!args.isConstructing()); - - return Collator(cx, args); -} - -void -js::CollatorObject::finalize(FreeOp* fop, JSObject* obj) -{ - MOZ_ASSERT(fop->onMainThread()); - - const Value& slot = obj->as().getReservedSlot(CollatorObject::UCOLLATOR_SLOT); - if (UCollator* coll = static_cast(slot.toPrivate())) - ucol_close(coll); -} - -JSObject* -js::CreateCollatorPrototype(JSContext* cx, HandleObject Intl, Handle global) -{ - RootedFunction ctor(cx, GlobalObject::createConstructor(cx, &Collator, cx->names().Collator, - 0)); - if (!ctor) - return nullptr; - - RootedObject proto(cx, GlobalObject::createBlankPrototype(cx, global)); - if (!proto) - return nullptr; - - if (!LinkConstructorAndPrototype(cx, ctor, proto)) - return nullptr; - - // 10.2.2 - if (!JS_DefineFunctions(cx, ctor, collator_static_methods)) - return nullptr; - - // 10.3.2 and 10.3.3 - if (!JS_DefineFunctions(cx, proto, collator_methods)) - return nullptr; - - /* - * Install the getter for Collator.prototype.compare, which returns a bound - * comparison function for the specified Collator object (suitable for - * passing to methods like Array.prototype.sort). - */ - RootedValue getter(cx); - if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().CollatorCompareGet, &getter)) - return nullptr; - if (!DefineProperty(cx, proto, cx->names().compare, UndefinedHandleValue, - JS_DATA_TO_FUNC_PTR(JSGetterOp, &getter.toObject()), - nullptr, JSPROP_GETTER | JSPROP_SHARED)) - { - return nullptr; - } - - // 8.1 - RootedValue ctorValue(cx, ObjectValue(*ctor)); - if (!DefineProperty(cx, Intl, cx->names().Collator, ctorValue, nullptr, nullptr, 0)) - return nullptr; - - return proto; -} - -bool -js::intl_availableCollations(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 1); - MOZ_ASSERT(args[0].isString()); - - JSAutoByteString locale(cx, args[0].toString()); - if (!locale) - return false; - UErrorCode status = U_ZERO_ERROR; - UEnumeration* values = ucol_getKeywordValuesForLocale("co", locale.ptr(), false, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - ScopedICUObject toClose(values); - - uint32_t count = uenum_count(values, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - RootedObject collations(cx, NewDenseEmptyArray(cx)); - if (!collations) - return false; - - uint32_t index = 0; - - // The first element of the collations array must be |null| per - // ES2017 Intl, 10.2.3 Internal Slots. - if (!DefineElement(cx, collations, index++, NullHandleValue)) - return false; - - RootedValue element(cx); - - for (uint32_t i = 0; i < count; i++) { - const char* collation = uenum_next(values, nullptr, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - // Per ECMA-402, 10.2.3, we don't include standard and search: - // "The values 'standard' and 'search' must not be used as elements in - // any [[sortLocaleData]][locale].co and [[searchLocaleData]][locale].co - // array." - if (StringsAreEqual(collation, "standard") || StringsAreEqual(collation, "search")) - continue; - - // ICU returns old-style keyword values; map them to BCP 47 equivalents. - JSString* jscollation = JS_NewStringCopyZ(cx, uloc_toUnicodeLocaleType("co", collation)); - if (!jscollation) - return false; - element = StringValue(jscollation); - if (!DefineElement(cx, collations, index++, element)) - return false; - } - - args.rval().setObject(*collations); - return true; -} - -/** - * Returns a new UCollator with the locale and collation options - * of the given Collator. - */ -static UCollator* -NewUCollator(JSContext* cx, Handle collator) -{ - RootedValue value(cx); - - RootedObject internals(cx, intl::GetInternalsObject(cx, collator)); - if (!internals) - return nullptr; - - if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) - return nullptr; - JSAutoByteString locale(cx, value.toString()); - if (!locale) - return nullptr; - - // UCollator options with default values. - UColAttributeValue uStrength = UCOL_DEFAULT; - UColAttributeValue uCaseLevel = UCOL_OFF; - UColAttributeValue uAlternate = UCOL_DEFAULT; - UColAttributeValue uNumeric = UCOL_OFF; - // Normalization is always on to meet the canonical equivalence requirement. - UColAttributeValue uNormalization = UCOL_ON; - UColAttributeValue uCaseFirst = UCOL_DEFAULT; - - if (!GetProperty(cx, internals, internals, cx->names().usage, &value)) - return nullptr; - JSAutoByteString usage(cx, value.toString()); - if (!usage) - return nullptr; - if (StringsAreEqual(usage, "search")) { - // ICU expects search as a Unicode locale extension on locale. - intl::LanguageTag tag(cx); - if (!intl::LanguageTagParser::parse( - cx, mozilla::MakeCStringSpan(locale.ptr()), tag)) { - return nullptr; - } - - JS::RootedVector keywords(cx); - - if (!keywords.emplaceBack("co", cx->names().search)) { - return nullptr; - } - - // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of - // the Unicode extension subtag. We're then relying on ICU to follow RFC - // 6067, which states that any trailing keywords using the same key - // should be ignored. - if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) { - return nullptr; - } - - locale.clear(); - locale.encodeLatin1(cx, tag.toString(cx)); - if (!locale) { - return nullptr; - } - } else { - MOZ_ASSERT(StringsAreEqual(usage, "sort")); - } - - // We don't need to look at the collation property - it can only be set - // via the Unicode locale extension and is therefore already set on - // locale. - - if (!GetProperty(cx, internals, internals, cx->names().sensitivity, &value)) - return nullptr; - JSAutoByteString sensitivity(cx, value.toString()); - if (!sensitivity) - return nullptr; - if (StringsAreEqual(sensitivity, "base")) { - uStrength = UCOL_PRIMARY; - } else if (StringsAreEqual(sensitivity, "accent")) { - uStrength = UCOL_SECONDARY; - } else if (StringsAreEqual(sensitivity, "case")) { - uStrength = UCOL_PRIMARY; - uCaseLevel = UCOL_ON; - } else { - MOZ_ASSERT(StringsAreEqual(sensitivity, "variant")); - uStrength = UCOL_TERTIARY; - } - - if (!GetProperty(cx, internals, internals, cx->names().ignorePunctuation, &value)) - return nullptr; - // According to the ICU team, UCOL_SHIFTED causes punctuation to be - // ignored. Looking at Unicode Technical Report 35, Unicode Locale Data - // Markup Language, "shifted" causes whitespace and punctuation to be - // ignored - that's a bit more than asked for, but there's no way to get - // less. - if (value.toBoolean()) - uAlternate = UCOL_SHIFTED; - - if (!GetProperty(cx, internals, internals, cx->names().numeric, &value)) - return nullptr; - if (!value.isUndefined() && value.toBoolean()) - uNumeric = UCOL_ON; - - if (!GetProperty(cx, internals, internals, cx->names().caseFirst, &value)) - return nullptr; - if (!value.isUndefined()) { - JSAutoByteString caseFirst(cx, value.toString()); - if (!caseFirst) - return nullptr; - if (StringsAreEqual(caseFirst, "upper")) - uCaseFirst = UCOL_UPPER_FIRST; - else if (StringsAreEqual(caseFirst, "lower")) - uCaseFirst = UCOL_LOWER_FIRST; - else { - MOZ_ASSERT(StringsAreEqual(caseFirst, "false")); - uCaseFirst = UCOL_OFF; - } - } - - UErrorCode status = U_ZERO_ERROR; - UCollator* coll = ucol_open(IcuLocale(locale.ptr()), &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return nullptr; - } - - ucol_setAttribute(coll, UCOL_STRENGTH, uStrength, &status); - ucol_setAttribute(coll, UCOL_CASE_LEVEL, uCaseLevel, &status); - ucol_setAttribute(coll, UCOL_ALTERNATE_HANDLING, uAlternate, &status); - ucol_setAttribute(coll, UCOL_NUMERIC_COLLATION, uNumeric, &status); - ucol_setAttribute(coll, UCOL_NORMALIZATION_MODE, uNormalization, &status); - ucol_setAttribute(coll, UCOL_CASE_FIRST, uCaseFirst, &status); - if (U_FAILURE(status)) { - ucol_close(coll); - intl::ReportInternalError(cx); - return nullptr; - } - - return coll; -} - -static bool -intl_CompareStrings(JSContext* cx, UCollator* coll, HandleString str1, HandleString str2, - MutableHandleValue result) -{ - MOZ_ASSERT(str1); - MOZ_ASSERT(str2); - - if (str1 == str2) { - result.setInt32(0); - return true; - } - - AutoStableStringChars stableChars1(cx); - if (!stableChars1.initTwoByte(cx, str1)) - return false; - - AutoStableStringChars stableChars2(cx); - if (!stableChars2.initTwoByte(cx, str2)) - return false; - - mozilla::Range chars1 = stableChars1.twoByteRange(); - mozilla::Range chars2 = stableChars2.twoByteRange(); - - UCollationResult uresult = ucol_strcoll(coll, - Char16ToUChar(chars1.begin().get()), chars1.length(), - Char16ToUChar(chars2.begin().get()), chars2.length()); - int32_t res; - switch (uresult) { - case UCOL_LESS: res = -1; break; - case UCOL_EQUAL: res = 0; break; - case UCOL_GREATER: res = 1; break; - default: MOZ_CRASH("ucol_strcoll returned bad UCollationResult"); - } - result.setInt32(res); - return true; -} - -bool -js::intl_CompareStrings(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 3); - MOZ_ASSERT(args[0].isObject()); - MOZ_ASSERT(args[1].isString()); - MOZ_ASSERT(args[2].isString()); - - Rooted collator(cx, &args[0].toObject().as()); - - // Obtain a cached UCollator object. - // XXX Does this handle Collator instances from other globals correctly? - void* priv = collator->getReservedSlot(CollatorObject::UCOLLATOR_SLOT).toPrivate(); - UCollator* coll = static_cast(priv); - if (!coll) { - coll = NewUCollator(cx, collator); - if (!coll) - return false; - collator->setReservedSlot(CollatorObject::UCOLLATOR_SLOT, PrivateValue(coll)); - } - - // Use the UCollator to actually compare the strings. - RootedString str1(cx, args[1].toString()); - RootedString str2(cx, args[2].toString()); - return intl_CompareStrings(cx, coll, str1, str2, args.rval()); -} - -bool -js::intl_isUpperCaseFirst(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 1); - MOZ_ASSERT(args[0].isString()); - - SharedIntlData& sharedIntlData = cx->sharedIntlData; - - RootedString locale(cx, args[0].toString()); - bool isUpperFirst; - if (!sharedIntlData.isUpperCaseFirst(cx, locale, &isUpperFirst)) - return false; - - args.rval().setBoolean(isUpperFirst); - return true; -} +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Intl.Collator implementation. */ + +#include "builtin/intl/Collator.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Span.h" + +#include "jsapi.h" +#include "jscntxt.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/ICUHeader.h" +#include "builtin/intl/LanguageTag.h" +#include "builtin/intl/ScopedICUObject.h" +#include "builtin/intl/SharedIntlData.h" +#include "js/TypeDecls.h" +#include "vm/GlobalObject.h" +#include "vm/Runtime.h" +#include "vm/String.h" + +#include "jsobjinlines.h" + +using namespace js; +using js::intl::IcuLocale; +using js::intl::ReportInternalError; +using js::intl::SharedIntlData; +using js::intl::StringsAreEqual; + +/******************** Collator ********************/ + +const ClassOps CollatorObject::classOps_ = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* setProperty */ + nullptr, /* enumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + CollatorObject::finalize +}; + +const Class CollatorObject::class_ = { + js_Object_str, + JSCLASS_HAS_RESERVED_SLOTS(CollatorObject::SLOT_COUNT) | + JSCLASS_FOREGROUND_FINALIZE, + &CollatorObject::classOps_ +}; + +#if JS_HAS_TOSOURCE +static bool +collator_toSource(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().Collator); + return true; +} +#endif + +static const JSFunctionSpec collator_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_Collator_supportedLocalesOf", 1, 0), + JS_FS_END +}; + +static const JSFunctionSpec collator_methods[] = { + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_Collator_resolvedOptions", 0, 0), +#if JS_HAS_TOSOURCE + JS_FN(js_toSource_str, collator_toSource, 0, 0), +#endif + JS_FS_END +}; + +/** + * 10.1.2 Intl.Collator([ locales [, options]]) + * + * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b + */ +static bool +Collator(JSContext* cx, const CallArgs& args) +{ + // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code). + + // Steps 2-5 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) + return false; + + if (!proto) { + proto = GlobalObject::getOrCreateCollatorPrototype(cx, cx->global()); + if (!proto) + return false; + } + + Rooted collator(cx, NewObjectWithGivenProto(cx, proto)); + if (!collator) + return false; + + collator->setReservedSlot(CollatorObject::INTERNALS_SLOT, NullValue()); + collator->setReservedSlot(CollatorObject::UCOLLATOR_SLOT, PrivateValue(nullptr)); + + RootedValue locales(cx, args.get(0)); + RootedValue options(cx, args.get(1)); + + // Step 6. + if (!intl::InitializeObject(cx, collator, cx->names().InitializeCollator, locales, options)) + return false; + + args.rval().setObject(*collator); + return true; +} + +static bool +Collator(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return Collator(cx, args); +} + +bool +js::intl_Collator(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(!args.isConstructing()); + + return Collator(cx, args); +} + +void +js::CollatorObject::finalize(FreeOp* fop, JSObject* obj) +{ + MOZ_ASSERT(fop->onActiveCooperatingThread()); + + const Value& slot = obj->as().getReservedSlot(CollatorObject::UCOLLATOR_SLOT); + if (UCollator* coll = static_cast(slot.toPrivate())) + ucol_close(coll); +} + +JSObject* +js::CreateCollatorPrototype(JSContext* cx, HandleObject Intl, Handle global) +{ + RootedFunction ctor(cx, GlobalObject::createConstructor(cx, &Collator, cx->names().Collator, + 0)); + if (!ctor) + return nullptr; + + RootedObject proto(cx, GlobalObject::createBlankPrototype(cx, global)); + if (!proto) + return nullptr; + + if (!LinkConstructorAndPrototype(cx, ctor, proto)) + return nullptr; + + // 10.2.2 + if (!JS_DefineFunctions(cx, ctor, collator_static_methods)) + return nullptr; + + // 10.3.2 and 10.3.3 + if (!JS_DefineFunctions(cx, proto, collator_methods)) + return nullptr; + + /* + * Install the getter for Collator.prototype.compare, which returns a bound + * comparison function for the specified Collator object (suitable for + * passing to methods like Array.prototype.sort). + */ + RootedValue getter(cx); + if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().CollatorCompareGet, &getter)) + return nullptr; + if (!DefineProperty(cx, proto, cx->names().compare, UndefinedHandleValue, + JS_DATA_TO_FUNC_PTR(JSGetterOp, &getter.toObject()), + nullptr, JSPROP_GETTER | JSPROP_SHARED)) + { + return nullptr; + } + + // 8.1 + RootedValue ctorValue(cx, ObjectValue(*ctor)); + if (!DefineProperty(cx, Intl, cx->names().Collator, ctorValue, nullptr, nullptr, 0)) + return nullptr; + + return proto; +} + +bool +js::intl_availableCollations(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + JSAutoByteString locale(cx, args[0].toString()); + if (!locale) + return false; + UErrorCode status = U_ZERO_ERROR; + UEnumeration* values = ucol_getKeywordValuesForLocale("co", locale.ptr(), false, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(values); + + uint32_t count = uenum_count(values, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + RootedObject collations(cx, NewDenseEmptyArray(cx)); + if (!collations) + return false; + + uint32_t index = 0; + + // The first element of the collations array must be |null| per + // ES2017 Intl, 10.2.3 Internal Slots. + if (!DefineElement(cx, collations, index++, NullHandleValue)) + return false; + + RootedValue element(cx); + + for (uint32_t i = 0; i < count; i++) { + const char* collation = uenum_next(values, nullptr, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + // Per ECMA-402, 10.2.3, we don't include standard and search: + // "The values 'standard' and 'search' must not be used as elements in + // any [[sortLocaleData]][locale].co and [[searchLocaleData]][locale].co + // array." + if (StringsAreEqual(collation, "standard") || StringsAreEqual(collation, "search")) + continue; + + // ICU returns old-style keyword values; map them to BCP 47 equivalents. + JSString* jscollation = JS_NewStringCopyZ(cx, uloc_toUnicodeLocaleType("co", collation)); + if (!jscollation) + return false; + element = StringValue(jscollation); + if (!DefineElement(cx, collations, index++, element)) + return false; + } + + args.rval().setObject(*collations); + return true; +} + +/** + * Returns a new UCollator with the locale and collation options + * of the given Collator. + */ +static UCollator* +NewUCollator(JSContext* cx, Handle collator) +{ + RootedValue value(cx); + + RootedObject internals(cx, intl::GetInternalsObject(cx, collator)); + if (!internals) + return nullptr; + + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) + return nullptr; + JSAutoByteString locale(cx, value.toString()); + if (!locale) + return nullptr; + + // UCollator options with default values. + UColAttributeValue uStrength = UCOL_DEFAULT; + UColAttributeValue uCaseLevel = UCOL_OFF; + UColAttributeValue uAlternate = UCOL_DEFAULT; + UColAttributeValue uNumeric = UCOL_OFF; + // Normalization is always on to meet the canonical equivalence requirement. + UColAttributeValue uNormalization = UCOL_ON; + UColAttributeValue uCaseFirst = UCOL_DEFAULT; + + if (!GetProperty(cx, internals, internals, cx->names().usage, &value)) + return nullptr; + JSAutoByteString usage(cx, value.toString()); + if (!usage) + return nullptr; + if (StringsAreEqual(usage, "search")) { + // ICU expects search as a Unicode locale extension on locale. + intl::LanguageTag tag(cx); + if (!intl::LanguageTagParser::parse( + cx, mozilla::MakeCStringSpan(locale.ptr()), tag)) { + return nullptr; + } + + JS::RootedVector keywords(cx); + + if (!keywords.emplaceBack("co", cx->names().search)) { + return nullptr; + } + + // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of + // the Unicode extension subtag. We're then relying on ICU to follow RFC + // 6067, which states that any trailing keywords using the same key + // should be ignored. + if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) { + return nullptr; + } + + locale.clear(); + locale.encodeLatin1(cx, tag.toString(cx)); + if (!locale) { + return nullptr; + } + } else { + MOZ_ASSERT(StringsAreEqual(usage, "sort")); + } + + // We don't need to look at the collation property - it can only be set + // via the Unicode locale extension and is therefore already set on + // locale. + + if (!GetProperty(cx, internals, internals, cx->names().sensitivity, &value)) + return nullptr; + JSAutoByteString sensitivity(cx, value.toString()); + if (!sensitivity) + return nullptr; + if (StringsAreEqual(sensitivity, "base")) { + uStrength = UCOL_PRIMARY; + } else if (StringsAreEqual(sensitivity, "accent")) { + uStrength = UCOL_SECONDARY; + } else if (StringsAreEqual(sensitivity, "case")) { + uStrength = UCOL_PRIMARY; + uCaseLevel = UCOL_ON; + } else { + MOZ_ASSERT(StringsAreEqual(sensitivity, "variant")); + uStrength = UCOL_TERTIARY; + } + + if (!GetProperty(cx, internals, internals, cx->names().ignorePunctuation, &value)) + return nullptr; + // According to the ICU team, UCOL_SHIFTED causes punctuation to be + // ignored. Looking at Unicode Technical Report 35, Unicode Locale Data + // Markup Language, "shifted" causes whitespace and punctuation to be + // ignored - that's a bit more than asked for, but there's no way to get + // less. + if (value.toBoolean()) + uAlternate = UCOL_SHIFTED; + + if (!GetProperty(cx, internals, internals, cx->names().numeric, &value)) + return nullptr; + if (!value.isUndefined() && value.toBoolean()) + uNumeric = UCOL_ON; + + if (!GetProperty(cx, internals, internals, cx->names().caseFirst, &value)) + return nullptr; + if (!value.isUndefined()) { + JSAutoByteString caseFirst(cx, value.toString()); + if (!caseFirst) + return nullptr; + if (StringsAreEqual(caseFirst, "upper")) + uCaseFirst = UCOL_UPPER_FIRST; + else if (StringsAreEqual(caseFirst, "lower")) + uCaseFirst = UCOL_LOWER_FIRST; + else { + MOZ_ASSERT(StringsAreEqual(caseFirst, "false")); + uCaseFirst = UCOL_OFF; + } + } + + UErrorCode status = U_ZERO_ERROR; + UCollator* coll = ucol_open(IcuLocale(locale.ptr()), &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return nullptr; + } + + ucol_setAttribute(coll, UCOL_STRENGTH, uStrength, &status); + ucol_setAttribute(coll, UCOL_CASE_LEVEL, uCaseLevel, &status); + ucol_setAttribute(coll, UCOL_ALTERNATE_HANDLING, uAlternate, &status); + ucol_setAttribute(coll, UCOL_NUMERIC_COLLATION, uNumeric, &status); + ucol_setAttribute(coll, UCOL_NORMALIZATION_MODE, uNormalization, &status); + ucol_setAttribute(coll, UCOL_CASE_FIRST, uCaseFirst, &status); + if (U_FAILURE(status)) { + ucol_close(coll); + intl::ReportInternalError(cx); + return nullptr; + } + + return coll; +} + +static bool +intl_CompareStrings(JSContext* cx, UCollator* coll, HandleString str1, HandleString str2, + MutableHandleValue result) +{ + MOZ_ASSERT(str1); + MOZ_ASSERT(str2); + + if (str1 == str2) { + result.setInt32(0); + return true; + } + + AutoStableStringChars stableChars1(cx); + if (!stableChars1.initTwoByte(cx, str1)) + return false; + + AutoStableStringChars stableChars2(cx); + if (!stableChars2.initTwoByte(cx, str2)) + return false; + + mozilla::Range chars1 = stableChars1.twoByteRange(); + mozilla::Range chars2 = stableChars2.twoByteRange(); + + UCollationResult uresult = ucol_strcoll(coll, + Char16ToUChar(chars1.begin().get()), chars1.length(), + Char16ToUChar(chars2.begin().get()), chars2.length()); + int32_t res; + switch (uresult) { + case UCOL_LESS: res = -1; break; + case UCOL_EQUAL: res = 0; break; + case UCOL_GREATER: res = 1; break; + default: MOZ_CRASH("ucol_strcoll returned bad UCollationResult"); + } + result.setInt32(res); + return true; +} + +bool +js::intl_CompareStrings(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + MOZ_ASSERT(args[0].isObject()); + MOZ_ASSERT(args[1].isString()); + MOZ_ASSERT(args[2].isString()); + + Rooted collator(cx, &args[0].toObject().as()); + + // Obtain a cached UCollator object. + // XXX Does this handle Collator instances from other globals correctly? + void* priv = collator->getReservedSlot(CollatorObject::UCOLLATOR_SLOT).toPrivate(); + UCollator* coll = static_cast(priv); + if (!coll) { + coll = NewUCollator(cx, collator); + if (!coll) + return false; + collator->setReservedSlot(CollatorObject::UCOLLATOR_SLOT, PrivateValue(coll)); + } + + // Use the UCollator to actually compare the strings. + RootedString str1(cx, args[1].toString()); + RootedString str2(cx, args[2].toString()); + return intl_CompareStrings(cx, coll, str1, str2, args.rval()); +} + +bool +js::intl_isUpperCaseFirst(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); + + RootedString locale(cx, args[0].toString()); + bool isUpperFirst; + if (!sharedIntlData.isUpperCaseFirst(cx, locale, &isUpperFirst)) + return false; + + args.rval().setBoolean(isUpperFirst); + return true; +} diff --git a/js/src/builtin/intl/DateTimeFormat.cpp b/js/src/builtin/intl/DateTimeFormat.cpp index 1cd18c87ec..4be5654e5c 100644 --- a/js/src/builtin/intl/DateTimeFormat.cpp +++ b/js/src/builtin/intl/DateTimeFormat.cpp @@ -1,1195 +1,1198 @@ -/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/* Intl.DateTimeFormat implementation. */ - -#include "builtin/intl/DateTimeFormat.h" - -#include "mozilla/Assertions.h" -#include "mozilla/Range.h" -#include "mozilla/Span.h" - -#include "jscntxt.h" -#include "jsfriendapi.h" - -#include "builtin/intl/CommonFunctions.h" -#include "builtin/intl/ICUHeader.h" -#include "builtin/intl/LanguageTag.h" -#include "builtin/intl/ScopedICUObject.h" -#include "builtin/intl/SharedIntlData.h" -#include "builtin/intl/TimeZoneDataGenerated.h" -#include "vm/GlobalObject.h" -#include "vm/Runtime.h" - -#include "jsobjinlines.h" - -#include "vm/NativeObject-inl.h" - -using namespace js; - -using mozilla::IsFinite; - -using JS::ClippedTime; -using JS::TimeClip; - -using js::intl::CallICU; -using js::intl::DateTimeFormatOptions; -using js::intl::IcuLocale; -using js::intl::INITIAL_CHAR_BUFFER_SIZE; -using js::intl::SharedIntlData; -using js::intl::StringsAreEqual; - -/******************** DateTimeFormat ********************/ - -const ClassOps DateTimeFormatObject::classOps_ = { - nullptr, /* addProperty */ - nullptr, /* delProperty */ - nullptr, /* getProperty */ - nullptr, /* setProperty */ - nullptr, /* enumerate */ - nullptr, /* resolve */ - nullptr, /* mayResolve */ - DateTimeFormatObject::finalize -}; - -const Class DateTimeFormatObject::class_ = { - js_Object_str, - JSCLASS_HAS_RESERVED_SLOTS(DateTimeFormatObject::SLOT_COUNT) | - JSCLASS_FOREGROUND_FINALIZE, - &DateTimeFormatObject::classOps_ -}; - -#if JS_HAS_TOSOURCE -static bool -dateTimeFormat_toSource(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - args.rval().setString(cx->names().DateTimeFormat); - return true; -} -#endif - -static const JSFunctionSpec dateTimeFormat_static_methods[] = { - JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_DateTimeFormat_supportedLocalesOf", 1, 0), - JS_FS_END -}; - -static const JSFunctionSpec dateTimeFormat_methods[] = { - JS_SELF_HOSTED_FN("resolvedOptions", "Intl_DateTimeFormat_resolvedOptions", 0, 0), - JS_SELF_HOSTED_FN("formatToParts", "Intl_DateTimeFormat_formatToParts", 0, 0), -#if JS_HAS_TOSOURCE - JS_FN(js_toSource_str, dateTimeFormat_toSource, 0, 0), -#endif - JS_FS_END -}; - -/** - * 12.2.1 Intl.DateTimeFormat([ locales [, options]]) - * - * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b - */ -static bool -DateTimeFormat(JSContext* cx, const CallArgs& args, bool construct, DateTimeFormatOptions dtfOptions) -{ - // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code). - - // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). - RootedObject proto(cx); - if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto)) - return false; - - if (!proto) { - proto = GlobalObject::getOrCreateDateTimeFormatPrototype(cx, cx->global()); - if (!proto) - return false; - } - - Rooted dateTimeFormat(cx); - dateTimeFormat = NewObjectWithGivenProto(cx, proto); - if (!dateTimeFormat) - return false; - - dateTimeFormat->setReservedSlot(DateTimeFormatObject::INTERNALS_SLOT, NullValue()); - dateTimeFormat->setReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT, - PrivateValue(nullptr)); - - RootedValue thisValue(cx, construct ? ObjectValue(*dateTimeFormat) : args.thisv()); - RootedValue locales(cx, args.get(0)); - RootedValue options(cx, args.get(1)); - - // Step 3. - return intl::LegacyIntlInitialize(cx, dateTimeFormat, cx->names().InitializeDateTimeFormat, - thisValue, locales, options, dtfOptions, args.rval()); -} - -static bool -DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - return DateTimeFormat(cx, args, args.isConstructing(), DateTimeFormatOptions::Standard); -} - -static bool -MozDateTimeFormat(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - - // Don't allow to call mozIntl.DateTimeFormat as a function. That way we - // don't need to worry how to handle the legacy initialization semantics - // when applied on mozIntl.DateTimeFormat. - if (!ThrowIfNotConstructing(cx, args, "mozIntl.DateTimeFormat")) - return false; - - return DateTimeFormat(cx, args, true, DateTimeFormatOptions::EnableMozExtensions); -} - -bool -js::intl_DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 2); - MOZ_ASSERT(!args.isConstructing()); - // intl_DateTimeFormat is an intrinsic for self-hosted JavaScript, so it - // cannot be used with "new", but it still has to be treated as a - // constructor. - return DateTimeFormat(cx, args, true, DateTimeFormatOptions::Standard); -} - -void -js::DateTimeFormatObject::finalize(FreeOp* fop, JSObject* obj) -{ - MOZ_ASSERT(fop->onMainThread()); - - const Value& slot = obj->as().getReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT); - if (UDateFormat* df = static_cast(slot.toPrivate())) - udat_close(df); -} - -JSObject* -js::CreateDateTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handle global, - MutableHandleObject constructor, DateTimeFormatOptions dtfOptions) -{ - RootedFunction ctor(cx); - ctor = dtfOptions == DateTimeFormatOptions::EnableMozExtensions - ? GlobalObject::createConstructor(cx, MozDateTimeFormat, cx->names().DateTimeFormat, 0) - : GlobalObject::createConstructor(cx, DateTimeFormat, cx->names().DateTimeFormat, 0); - if (!ctor) - return nullptr; - - RootedObject proto(cx, GlobalObject::createBlankPrototype(cx, global)); - if (!proto) - return nullptr; - - if (!LinkConstructorAndPrototype(cx, ctor, proto)) - return nullptr; - - // 12.2.2 - if (!JS_DefineFunctions(cx, ctor, dateTimeFormat_static_methods)) - return nullptr; - - // 12.3.2 and 12.3.3 - if (!JS_DefineFunctions(cx, proto, dateTimeFormat_methods)) - return nullptr; - - // Install a getter for DateTimeFormat.prototype.format that returns a - // formatting function bound to a specified DateTimeFormat object (suitable - // for passing to methods like Array.prototype.map). - RootedValue getter(cx); - if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().DateTimeFormatFormatGet, - &getter)) - { - return nullptr; - } - if (!DefineProperty(cx, proto, cx->names().format, UndefinedHandleValue, - JS_DATA_TO_FUNC_PTR(JSGetterOp, &getter.toObject()), - nullptr, JSPROP_GETTER | JSPROP_SHARED)) - { - return nullptr; - } - - // 8.1 - RootedValue ctorValue(cx, ObjectValue(*ctor)); - if (!DefineProperty(cx, Intl, cx->names().DateTimeFormat, ctorValue, nullptr, nullptr, 0)) - return nullptr; - - constructor.set(ctor); - return proto; -} - -bool -js::AddMozDateTimeFormatConstructor(JSContext* cx, JS::Handle intl) -{ - Handle global = cx->global(); - - RootedObject mozDateTimeFormat(cx); - JSObject* mozDateTimeFormatProto = - CreateDateTimeFormatPrototype(cx, intl, global, &mozDateTimeFormat, DateTimeFormatOptions::EnableMozExtensions); - return mozDateTimeFormatProto != nullptr; -} - -static bool -DefaultCalendar(JSContext* cx, const JSAutoByteString& locale, MutableHandleValue rval) -{ - UErrorCode status = U_ZERO_ERROR; - UCalendar* cal = ucal_open(nullptr, 0, locale.ptr(), UCAL_DEFAULT, &status); - - // This correctly handles nullptr |cal| when opening failed. - ScopedICUObject closeCalendar(cal); - - const char* calendar = ucal_getType(cal, &status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - - // ICU returns old-style keyword values; map them to BCP 47 equivalents - JSString* str = JS_NewStringCopyZ(cx, uloc_toUnicodeLocaleType("ca", calendar)); - if (!str) - return false; - - rval.setString(str); - return true; -} - -struct CalendarAlias -{ - const char* const calendar; - const char* const alias; -}; - -const CalendarAlias calendarAliases[] = { - { "islamic-civil", "islamicc" }, - { "ethioaa", "ethiopic-amete-alem" } -}; - -bool -js::intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 1); - MOZ_ASSERT(args[0].isString()); - - JSAutoByteString locale(cx, args[0].toString()); - if (!locale) - return false; - - RootedObject calendars(cx, NewDenseEmptyArray(cx)); - if (!calendars) - return false; - uint32_t index = 0; - - // We need the default calendar for the locale as the first result. - RootedValue element(cx); - if (!DefaultCalendar(cx, locale, &element)) - return false; - - if (!DefineElement(cx, calendars, index++, element)) - return false; - - // Now get the calendars that "would make a difference", i.e., not the default. - UErrorCode status = U_ZERO_ERROR; - UEnumeration* values = ucal_getKeywordValuesForLocale("ca", locale.ptr(), false, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - ScopedICUObject toClose(values); - - uint32_t count = uenum_count(values, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - for (; count > 0; count--) { - const char* calendar = uenum_next(values, nullptr, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - // ICU returns old-style keyword values; map them to BCP 47 equivalents - calendar = uloc_toUnicodeLocaleType("ca", calendar); - - JSString* jscalendar = JS_NewStringCopyZ(cx, calendar); - if (!jscalendar) - return false; - element = StringValue(jscalendar); - if (!DefineElement(cx, calendars, index++, element)) - return false; - - // ICU doesn't return calendar aliases, append them here. - for (const auto& calendarAlias : calendarAliases) { - if (StringsAreEqual(calendar, calendarAlias.calendar)) { - JSString* jscalendar = JS_NewStringCopyZ(cx, calendarAlias.alias); - if (!jscalendar) - return false; - element = StringValue(jscalendar); - if (!DefineElement(cx, calendars, index++, element)) - return false; - } - } - } - - args.rval().setObject(*calendars); - return true; -} - -bool -js::intl_defaultCalendar(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 1); - MOZ_ASSERT(args[0].isString()); - - JSAutoByteString locale(cx, args[0].toString()); - if (!locale) - return false; - - return DefaultCalendar(cx, locale, args.rval()); -} - -bool -js::intl_IsValidTimeZoneName(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 1); - MOZ_ASSERT(args[0].isString()); - - SharedIntlData& sharedIntlData = cx->sharedIntlData; - - RootedString timeZone(cx, args[0].toString()); - RootedString validatedTimeZone(cx); - if (!sharedIntlData.validateTimeZoneName(cx, timeZone, &validatedTimeZone)) - return false; - - if (validatedTimeZone) - args.rval().setString(validatedTimeZone); - else - args.rval().setNull(); - - return true; -} - -bool -js::intl_canonicalizeTimeZone(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 1); - MOZ_ASSERT(args[0].isString()); - - SharedIntlData& sharedIntlData = cx->sharedIntlData; - - // Some time zone names are canonicalized differently by ICU -- handle - // those first: - RootedString timeZone(cx, args[0].toString()); - RootedString ianaTimeZone(cx); - if (!sharedIntlData.tryCanonicalizeTimeZoneConsistentWithIANA(cx, timeZone, &ianaTimeZone)) - return false; - - if (ianaTimeZone) { - args.rval().setString(ianaTimeZone); - return true; - } - - AutoStableStringChars stableChars(cx); - if (!stableChars.initTwoByte(cx, timeZone)) - return false; - - mozilla::Range tzchars = stableChars.twoByteRange(); - - JSString* str = CallICU(cx, [&tzchars](UChar* chars, uint32_t size, UErrorCode* status) { - return ucal_getCanonicalTimeZoneID(tzchars.begin().get(), tzchars.length(), - chars, size, nullptr, status); - }); - if (!str) - return false; - args.rval().setString(str); - return true; -} - -bool -js::intl_defaultTimeZone(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 0); - - // The current default might be stale, because JS::ResetTimeZone() doesn't - // immediately update ICU's default time zone. So perform an update if - // needed. - js::ResyncICUDefaultTimeZone(); - - JSString* str = CallICU(cx, ucal_getDefaultTimeZone); - if (!str) - return false; - args.rval().setString(str); - return true; -} - -bool -js::intl_defaultTimeZoneOffset(JSContext* cx, unsigned argc, Value* vp) { - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 0); - - UErrorCode status = U_ZERO_ERROR; - const UChar* uTimeZone = nullptr; - int32_t uTimeZoneLength = 0; - const char* rootLocale = ""; - UCalendar* cal = ucal_open(uTimeZone, uTimeZoneLength, rootLocale, UCAL_DEFAULT, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - ScopedICUObject toClose(cal); - - int32_t offset = ucal_get(cal, UCAL_ZONE_OFFSET, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - args.rval().setInt32(offset); - return true; -} - -enum class HourCycle { - // 12 hour cycle, from 0 to 11. - H11, - - // 12 hour cycle, from 1 to 12. - H12, - - // 24 hour cycle, from 0 to 23. - H23, - - // 24 hour cycle, from 1 to 24. - H24 -}; - -static bool -IsHour12(HourCycle hc) -{ - return hc == HourCycle::H11 || hc == HourCycle::H12; -} - -static char16_t -HourSymbol(HourCycle hc) -{ - switch (hc) { - case HourCycle::H11: - return 'K'; - case HourCycle::H12: - return 'h'; - case HourCycle::H23: - return 'H'; - case HourCycle::H24: - return 'k'; - } - MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("unexpected hour cycle"); -} - -/** -* Parse a pattern according to the format specified in -* . -*/ -template -class PatternIterator { - CharT* iter_; - const CharT* const end_; - - public: - explicit PatternIterator(mozilla::Span pattern) - : iter_(pattern.data()), end_(pattern.data() + pattern.size()) {} - - CharT* next() { - MOZ_ASSERT(iter_ != nullptr); - - bool inQuote = false; - while (iter_ < end_) { - CharT* cur = iter_++; - if (*cur == '\'') { - inQuote = !inQuote; - } else if (!inQuote) { - return cur; - } - } - - iter_ = nullptr; - return nullptr; - } -}; - -/** -* Return the hour cycle for the given option string. -*/ -static HourCycle -HourCycleFromOption(JSLinearString* str) -{ - if (StringEqualsAscii(str, "h11")) { - return HourCycle::H11; - } - if (StringEqualsAscii(str, "h12")) { - return HourCycle::H12; - } - if (StringEqualsAscii(str, "h23")) { - return HourCycle::H23; - } - MOZ_ASSERT(StringEqualsAscii(str, "h24")); - return HourCycle::H24; -} - -/** -* Return the hour cycle used in the input pattern or Nothing if none was found. -*/ -static mozilla::Maybe -HourCycleFromPattern(mozilla::Span pattern) -{ - PatternIterator iter(pattern); - while (const auto* ptr = iter.next()) { - switch (*ptr) { - case 'K': - return mozilla::Some(HourCycle::H11); - case 'h': - return mozilla::Some(HourCycle::H12); - case 'H': - return mozilla::Some(HourCycle::H23); - case 'k': - return mozilla::Some(HourCycle::H24); - } - } - return mozilla::Nothing(); -} - -/** -* Replaces all hour pattern characters in |pattern| to use the matching hour -* representation for |hourCycle|. -*/ -static void -ReplaceHourSymbol(mozilla::Span pattern, HourCycle hc) -{ - char16_t replacement = HourSymbol(hc); - PatternIterator iter(pattern); - while (auto* ptr = iter.next()) { - char16_t ch = *ptr; - if (ch == 'K' || ch == 'h' || ch == 'H' || ch == 'k') { - *ptr = replacement; - } - } -} - -bool -js::intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 3); - MOZ_ASSERT(args[0].isString()); - MOZ_ASSERT(args[1].isString()); - MOZ_ASSERT(args[2].isString() || args[2].isUndefined()); - - JSAutoByteString locale(cx, args[0].toString()); - if (!locale) - return false; - - JSFlatString* skeletonFlat = args[1].toString()->ensureFlat(cx); - if (!skeletonFlat) - return false; - - AutoStableStringChars stableChars(cx); - if (!stableChars.initTwoByte(cx, skeletonFlat)) - return false; - - mozilla::Maybe hourCycle; - if (args[2].isString()) { - JSLinearString* hourCycleStr = args[2].toString()->ensureLinear(cx); - if (!hourCycleStr) { - return false; - } - - hourCycle.emplace(HourCycleFromOption(hourCycleStr)); - } - - mozilla::Range skeletonChars = stableChars.twoByteRange(); - uint32_t skeletonLen = u_strlen(Char16ToUChar(skeletonChars.begin().get())); - - UErrorCode status = U_ZERO_ERROR; - UDateTimePatternGenerator* gen = udatpg_open(IcuLocale(locale.ptr()), &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - ScopedICUObject toClose(gen); - - Vector pattern(cx); - - int32_t patternSize = CallICU( - cx, - pattern, - [gen, &skeletonChars](UChar* chars, uint32_t size, UErrorCode* status) { - return udatpg_getBestPattern(gen, skeletonChars.begin().get(), - skeletonChars.length(), chars, size, status); - }); - if (patternSize < 0) { - return false; - } - - // If the hourCycle option was set, adjust the resolved pattern to use the - // requested hour cycle representation. - if (hourCycle) { - ReplaceHourSymbol(pattern, hourCycle.value()); - } - - JSString* str = NewStringCopyN(cx, pattern.begin(), pattern.length()); - if (!str) { - return false; - } - args.rval().setString(str); - return true; -} - -/** - * Find a matching pattern using the requested hour-12 options. - * - * This function is needed to work around the following two issues. - * - https://unicode-org.atlassian.net/browse/ICU-21023 - * - https://unicode-org.atlassian.net/browse/CLDR-13425 - * - * We're currently using a relatively simple workaround, which doesn't give the - * most accurate results. For example: - * - * ``` - * var dtf = new Intl.DateTimeFormat("en", { - * timeZone: "UTC", - * dateStyle: "long", - * timeStyle: "long", - * hourCycle: "h12", - * }); - * print(dtf.format(new Date("2020-01-01T00:00Z"))); - * ``` - * - * Returns the pattern "MMMM d, y 'at' h:mm:ss a z", but when going through - * |udatpg_getSkeleton| and then |udatpg_getBestPattern| to find an equivalent - * pattern for "h23", we'll end up with the pattern "MMMM d, y, HH:mm:ss z", so - * the combinator element " 'at' " was lost in the process. - */ -template -static bool -FindPatternWithHourCycle(JSContext* cx, const char* locale, - Vector& pattern, bool hour12) -{ - UErrorCode status = U_ZERO_ERROR; - UDateTimePatternGenerator* gen = udatpg_open(IcuLocale(locale), &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - ScopedICUObject toClose(gen); - - if (!gen) { - return false; - } - - Vector skeleton(cx); - - int32_t skeletonSize = CallICU( - cx, - skeleton, - [&pattern](UChar* chars, uint32_t size, UErrorCode* status) { - return udatpg_getSkeleton(nullptr, pattern.begin(), pattern.length(), - chars, size, status); - }); - if (skeletonSize < 0) { - return false; - } - - // Input skeletons don't differentiate between "K" and "h" resp. "k" and "H". - ReplaceHourSymbol(skeleton, hour12 ? HourCycle::H12 : HourCycle::H23); - - MOZ_ALWAYS_TRUE(pattern.resize(0)); - - int32_t patternSize = CallICU( - cx, - pattern, - [gen, &skeleton](UChar* chars, uint32_t size, UErrorCode* status) { - return udatpg_getBestPattern(gen, skeleton.begin(), skeleton.length(), - chars, size, status); - }); - if (patternSize < 0) { - return false; - } - - return true; -} - -bool -js::intl_patternForStyle(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 6); - MOZ_ASSERT(args[0].isString()); - MOZ_ASSERT(args[1].isString() || args[1].isUndefined()); - MOZ_ASSERT(args[2].isString() || args[2].isUndefined()); - MOZ_ASSERT(args[3].isString()); - MOZ_ASSERT(args[4].isBoolean() || args[4].isUndefined()); - MOZ_ASSERT(args[5].isString() || args[5].isUndefined()); - - JSAutoByteString locale(cx, args[0].toString()); - if (!locale) - return false; - - auto toDateFormatStyle = [](JSLinearString* str) { - if (StringEqualsAscii(str, "full")) { - return UDAT_FULL; - } - if (StringEqualsAscii(str, "long")) { - return UDAT_LONG; - } - if (StringEqualsAscii(str, "medium")) { - return UDAT_MEDIUM; - } - MOZ_ASSERT(StringEqualsAscii(str, "short")); - return UDAT_SHORT; - }; - - UDateFormatStyle dateStyle = UDAT_NONE; - - if (args[1].isString()) { - JSLinearString* dateStyleStr = args[1].toString()->ensureLinear(cx); - if (!dateStyleStr) - return false; - - dateStyle = toDateFormatStyle(dateStyleStr); - } - - UDateFormatStyle timeStyle = UDAT_NONE; - if (args[2].isString()) { - JSLinearString* timeStyleStr = args[2].toString()->ensureLinear(cx); - if (!timeStyleStr) - return false; - - timeStyle = toDateFormatStyle(timeStyleStr); - } - - AutoStableStringChars timeZone(cx); - if (!timeZone.initTwoByte(cx, args[3].toString())) - return false; - - mozilla::Maybe hour12; - if (args[4].isBoolean()) { - hour12.emplace(args[4].toBoolean()); - } - - mozilla::Maybe hourCycle; - if (args[5].isString()) { - JSLinearString* hourCycleStr = args[5].toString()->ensureLinear(cx); - if (!hourCycleStr) { - return false; - } - - hourCycle.emplace(HourCycleFromOption(hourCycleStr)); - } - - mozilla::Range timeZoneChars = timeZone.twoByteRange(); - - UErrorCode status = U_ZERO_ERROR; - UDateFormat* df = udat_open(timeStyle, dateStyle, IcuLocale(locale.ptr()), - Char16ToUChar(timeZoneChars.begin().get()), - timeZoneChars.length(), nullptr, -1, &status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - ScopedICUObject toClose(df); - - Vector pattern(cx); - - int32_t patternSize = CallICU( - cx, - pattern, - [df](UChar* chars, uint32_t size, UErrorCode* status) { - return udat_toPattern(df, false, chars, size, status); - }); - if (patternSize < 0) { - return false; - } - - // If a specific hour cycle was requested and this hour cycle doesn't match - // the hour cycle used in the resolved pattern, find an equivalent pattern - // with the correct hour cycle. - if (timeStyle != UDAT_NONE && (hour12 || hourCycle)) { - if (auto hcPattern = HourCycleFromPattern(pattern)) { - bool wantHour12 = hour12 ? hour12.value() : IsHour12(hourCycle.value()); - if (wantHour12 != IsHour12(hcPattern.value())) { - if (!FindPatternWithHourCycle(cx, locale.ptr(), pattern, wantHour12)) { - return false; - } - } - } - } - - // If the hourCycle option was set, adjust the resolved pattern to use the - // requested hour cycle representation. - if (hourCycle) { - ReplaceHourSymbol(pattern, hourCycle.value()); - } - - JSString* str = NewStringCopyN(cx, pattern.begin(), pattern.length()); - if (!str) - return false; - args.rval().setString(str); - return true; -} - -/** - * Returns a new UDateFormat with the locale and date-time formatting options - * of the given DateTimeFormat. - */ -static UDateFormat* -NewUDateFormat(JSContext* cx, Handle dateTimeFormat) -{ - RootedValue value(cx); - - RootedObject internals(cx, intl::GetInternalsObject(cx, dateTimeFormat)); - if (!internals) - return nullptr; - - if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) - return nullptr; - - // ICU expects calendar and numberingSystem as Unicode locale extensions on - // locale. - - intl::LanguageTag tag(cx); - { - JSLinearString* locale = value.toString()->ensureLinear(cx); - if (!locale) - return nullptr; - - if (!intl::LanguageTagParser::parse(cx, locale, tag)) - return nullptr; - } - - JS::RootedVector keywords(cx); - - if (!GetProperty(cx, internals, internals, cx->names().calendar, &value)) - return nullptr; - - { - JSLinearString* calendar = value.toString()->ensureLinear(cx); - if (!calendar) - return nullptr; - - if (!keywords.emplaceBack("ca", calendar)) - return nullptr; - } - - if (!GetProperty(cx, internals, internals, cx->names().numberingSystem, &value)) - return nullptr; - - { - JSLinearString* numberingSystem = value.toString()->ensureLinear(cx); - if (!numberingSystem) - return nullptr; - - if (!keywords.emplaceBack("nu", numberingSystem)) - return nullptr; - } - - // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of - // the Unicode extension subtag. We're then relying on ICU to follow RFC - // 6067, which states that any trailing keywords using the same key - // should be ignored. - if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) - return nullptr; - - UniqueChars locale = tag.toStringZ(cx); - if (!locale) - return nullptr; - - if (!GetProperty(cx, internals, internals, cx->names().timeZone, &value)) - return nullptr; - - AutoStableStringChars timeZoneChars(cx); - Rooted timeZoneFlat(cx, value.toString()->ensureFlat(cx)); - if (!timeZoneFlat || !timeZoneChars.initTwoByte(cx, timeZoneFlat)) - return nullptr; - - const UChar* uTimeZone = Char16ToUChar(timeZoneChars.twoByteRange().begin().get()); - uint32_t uTimeZoneLength = u_strlen(uTimeZone); - - if (!GetProperty(cx, internals, internals, cx->names().pattern, &value)) - return nullptr; - - AutoStableStringChars patternChars(cx); - Rooted patternFlat(cx, value.toString()->ensureFlat(cx)); - if (!patternFlat || !patternChars.initTwoByte(cx, patternFlat)) - return nullptr; - - const UChar* uPattern = Char16ToUChar(patternChars.twoByteRange().begin().get()); - uint32_t uPatternLength = u_strlen(uPattern); - - UErrorCode status = U_ZERO_ERROR; - UDateFormat* df = - udat_open(UDAT_PATTERN, UDAT_PATTERN, IcuLocale(locale.get()), uTimeZone, uTimeZoneLength, - uPattern, uPatternLength, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return nullptr; - } - - // ECMAScript requires the Gregorian calendar to be used from the beginning - // of ECMAScript time. - UCalendar* cal = const_cast(udat_getCalendar(df)); - ucal_setGregorianChange(cal, StartOfTime, &status); - - // An error here means the calendar is not Gregorian, so we don't care. - - return df; -} - -static bool -intl_FormatDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue result) -{ - if (!IsFinite(x)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE, - "DateTimeFormat", "format"); - return false; - } - - JSString* str = CallICU(cx, [df, x](UChar* chars, int32_t size, UErrorCode* status) { - return udat_format(df, x, chars, size, nullptr, status); - }); - if (!str) - return false; - - result.setString(str); - return true; -} - -using FieldType = ImmutablePropertyNamePtr JSAtomState::*; - -static FieldType -GetFieldTypeForFormatField(UDateFormatField fieldName) -{ - // See intl/icu/source/i18n/unicode/udat.h for a detailed field list. This - // switch is deliberately exhaustive: cases might have to be added/removed - // if this code is compiled with a different ICU with more - // UDateFormatField enum initializers. Please guard such cases with - // appropriate ICU version-testing #ifdefs, should cross-version divergence - // occur. - switch (fieldName) { - case UDAT_ERA_FIELD: - return &JSAtomState::era; - case UDAT_YEAR_FIELD: - case UDAT_YEAR_WOY_FIELD: - case UDAT_EXTENDED_YEAR_FIELD: - case UDAT_YEAR_NAME_FIELD: - return &JSAtomState::year; - - case UDAT_MONTH_FIELD: - case UDAT_STANDALONE_MONTH_FIELD: - return &JSAtomState::month; - - case UDAT_DATE_FIELD: - case UDAT_JULIAN_DAY_FIELD: - return &JSAtomState::day; - - case UDAT_HOUR_OF_DAY1_FIELD: - case UDAT_HOUR_OF_DAY0_FIELD: - case UDAT_HOUR1_FIELD: - case UDAT_HOUR0_FIELD: - return &JSAtomState::hour; - - case UDAT_MINUTE_FIELD: - return &JSAtomState::minute; - - case UDAT_SECOND_FIELD: - return &JSAtomState::second; - - case UDAT_DAY_OF_WEEK_FIELD: - case UDAT_STANDALONE_DAY_FIELD: - case UDAT_DOW_LOCAL_FIELD: - case UDAT_DAY_OF_WEEK_IN_MONTH_FIELD: - return &JSAtomState::weekday; - - case UDAT_AM_PM_FIELD: - return &JSAtomState::dayPeriod; - - case UDAT_TIMEZONE_FIELD: - return &JSAtomState::timeZoneName; - - case UDAT_FRACTIONAL_SECOND_FIELD: - case UDAT_DAY_OF_YEAR_FIELD: - case UDAT_WEEK_OF_YEAR_FIELD: - case UDAT_WEEK_OF_MONTH_FIELD: - case UDAT_MILLISECONDS_IN_DAY_FIELD: - case UDAT_TIMEZONE_RFC_FIELD: - case UDAT_TIMEZONE_GENERIC_FIELD: - case UDAT_QUARTER_FIELD: - case UDAT_STANDALONE_QUARTER_FIELD: - case UDAT_TIMEZONE_SPECIAL_FIELD: - case UDAT_TIMEZONE_LOCALIZED_GMT_OFFSET_FIELD: - case UDAT_TIMEZONE_ISO_FIELD: - case UDAT_TIMEZONE_ISO_LOCAL_FIELD: -#ifndef U_HIDE_INTERNAL_API - case UDAT_RELATED_YEAR_FIELD: -#endif -#ifndef U_HIDE_DRAFT_API - case UDAT_AM_PM_MIDNIGHT_NOON_FIELD: - case UDAT_FLEXIBLE_DAY_PERIOD_FIELD: -#endif -#ifndef U_HIDE_INTERNAL_API - case UDAT_TIME_SEPARATOR_FIELD: -#endif - // These fields are all unsupported. - return nullptr; - - case UDAT_FIELD_COUNT: - MOZ_ASSERT_UNREACHABLE("format field sentinel value returned by " - "iterator!"); - } - - MOZ_ASSERT_UNREACHABLE("unenumerated, undocumented format field returned " - "by iterator"); - return nullptr; -} - -static bool -intl_FormatToPartsDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue result) -{ - if (!IsFinite(x)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE, - "DateTimeFormat", "formatToParts"); - return false; - } - - Vector chars(cx); - if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) - return false; - - UErrorCode status = U_ZERO_ERROR; - UFieldPositionIterator* fpositer = ufieldpositer_open(&status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - ScopedICUObject toClose(fpositer); - - RootedString overallResult(cx); - overallResult = CallICU(cx, [df, x, fpositer](UChar* chars, int32_t size, UErrorCode* status) { - return udat_formatForFields(df, x, chars, size, fpositer, status); - }); - if (!overallResult) - return false; - - RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx)); - if (!partsArray) - return false; - if (overallResult->length() == 0) { - // An empty string contains no parts, so avoid extra work below. - result.setObject(*partsArray); - return true; - } - - size_t lastEndIndex = 0; - - uint32_t partIndex = 0; - RootedObject singlePart(cx); - RootedValue partType(cx); - RootedValue val(cx); - - auto AppendPart = [&](FieldType type, size_t beginIndex, size_t endIndex) { - singlePart = NewBuiltinClassInstance(cx); - if (!singlePart) - return false; - - partType = StringValue(cx->names().*type); - if (!DefineProperty(cx, singlePart, cx->names().type, partType)) - return false; - - JSLinearString* partSubstr = - NewDependentString(cx, overallResult, beginIndex, endIndex - beginIndex); - if (!partSubstr) - return false; - - val = StringValue(partSubstr); - if (!DefineProperty(cx, singlePart, cx->names().value, val)) - return false; - - val = ObjectValue(*singlePart); - if (!DefineElement(cx, partsArray, partIndex, val)) - return false; - - lastEndIndex = endIndex; - partIndex++; - return true; - }; - - int32_t fieldInt, beginIndexInt, endIndexInt; - while ((fieldInt = ufieldpositer_next(fpositer, &beginIndexInt, &endIndexInt)) >= 0) { - MOZ_ASSERT(beginIndexInt >= 0); - MOZ_ASSERT(endIndexInt >= 0); - MOZ_ASSERT(beginIndexInt <= endIndexInt, - "field iterator returning invalid range"); - - size_t beginIndex(beginIndexInt); - size_t endIndex(endIndexInt); - - // Technically this isn't guaranteed. But it appears true in pratice, - // and http://bugs.icu-project.org/trac/ticket/12024 is expected to - // correct the documentation lapse. - MOZ_ASSERT(lastEndIndex <= beginIndex, - "field iteration didn't return fields in order start to " - "finish as expected"); - - if (FieldType type = GetFieldTypeForFormatField(static_cast(fieldInt))) { - if (lastEndIndex < beginIndex) { - if (!AppendPart(&JSAtomState::literal, lastEndIndex, beginIndex)) - return false; - } - - if (!AppendPart(type, beginIndex, endIndex)) - return false; - } - } - - // Append any final literal. - if (lastEndIndex < overallResult->length()) { - if (!AppendPart(&JSAtomState::literal, lastEndIndex, overallResult->length())) - return false; - } - - result.setObject(*partsArray); - return true; -} - -bool -js::intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 3); - MOZ_ASSERT(args[0].isObject()); - MOZ_ASSERT(args[1].isNumber()); - MOZ_ASSERT(args[2].isBoolean()); - - Rooted dateTimeFormat(cx); - dateTimeFormat = &args[0].toObject().as(); - - // Obtain a cached UDateFormat object. - void* priv = - dateTimeFormat->getReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT).toPrivate(); - UDateFormat* df = static_cast(priv); - if (!df) { - df = NewUDateFormat(cx, dateTimeFormat); - if (!df) - return false; - dateTimeFormat->setReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT, PrivateValue(df)); - } - - // Use the UDateFormat to actually format the time stamp. - return args[2].toBoolean() - ? intl_FormatToPartsDateTime(cx, df, args[1].toNumber(), args.rval()) - : intl_FormatDateTime(cx, df, args[1].toNumber(), args.rval()); -} - +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Intl.DateTimeFormat implementation. */ + +#include "builtin/intl/DateTimeFormat.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Range.h" +#include "mozilla/Span.h" + +#include "jscntxt.h" +#include "jsfriendapi.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/ICUHeader.h" +#include "builtin/intl/LanguageTag.h" +#include "builtin/intl/ScopedICUObject.h" +#include "builtin/intl/SharedIntlData.h" +#include "builtin/intl/TimeZoneDataGenerated.h" +#include "vm/GlobalObject.h" +#include "vm/Runtime.h" + +#include "jsobjinlines.h" + +#include "vm/NativeObject-inl.h" + +using namespace js; + +using mozilla::IsFinite; + +using JS::ClippedTime; +using JS::TimeClip; + +using js::intl::CallICU; +using js::intl::DateTimeFormatOptions; +using js::intl::IcuLocale; +using js::intl::INITIAL_CHAR_BUFFER_SIZE; +using js::intl::SharedIntlData; +using js::intl::StringsAreEqual; + +/******************** DateTimeFormat ********************/ + +const ClassOps DateTimeFormatObject::classOps_ = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* setProperty */ + nullptr, /* enumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + DateTimeFormatObject::finalize +}; + +const Class DateTimeFormatObject::class_ = { + js_Object_str, + JSCLASS_HAS_RESERVED_SLOTS(DateTimeFormatObject::SLOT_COUNT) | + JSCLASS_FOREGROUND_FINALIZE, + &DateTimeFormatObject::classOps_ +}; + +#if JS_HAS_TOSOURCE +static bool +dateTimeFormat_toSource(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().DateTimeFormat); + return true; +} +#endif + +static const JSFunctionSpec dateTimeFormat_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_DateTimeFormat_supportedLocalesOf", 1, 0), + JS_FS_END +}; + +static const JSFunctionSpec dateTimeFormat_methods[] = { + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_DateTimeFormat_resolvedOptions", 0, 0), + JS_SELF_HOSTED_FN("formatToParts", "Intl_DateTimeFormat_formatToParts", 0, 0), +#if JS_HAS_TOSOURCE + JS_FN(js_toSource_str, dateTimeFormat_toSource, 0, 0), +#endif + JS_FS_END +}; + +/** + * 12.2.1 Intl.DateTimeFormat([ locales [, options]]) + * + * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b + */ +static bool +DateTimeFormat(JSContext* cx, const CallArgs& args, bool construct, DateTimeFormatOptions dtfOptions) +{ + // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code). + + // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) + return false; + + if (!proto) { + proto = GlobalObject::getOrCreateDateTimeFormatPrototype(cx, cx->global()); + if (!proto) + return false; + } + + Rooted dateTimeFormat(cx); + dateTimeFormat = NewObjectWithGivenProto(cx, proto); + if (!dateTimeFormat) + return false; + + dateTimeFormat->setReservedSlot(DateTimeFormatObject::INTERNALS_SLOT, NullValue()); + dateTimeFormat->setReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT, + PrivateValue(nullptr)); + + RootedValue thisValue(cx, construct ? ObjectValue(*dateTimeFormat) : args.thisv()); + RootedValue locales(cx, args.get(0)); + RootedValue options(cx, args.get(1)); + + // Step 3. + return intl::LegacyIntlInitialize(cx, dateTimeFormat, cx->names().InitializeDateTimeFormat, + thisValue, locales, options, dtfOptions, args.rval()); +} + +static bool +DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return DateTimeFormat(cx, args, args.isConstructing(), DateTimeFormatOptions::Standard); +} + +static bool +MozDateTimeFormat(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + // Don't allow to call mozIntl.DateTimeFormat as a function. That way we + // don't need to worry how to handle the legacy initialization semantics + // when applied on mozIntl.DateTimeFormat. + if (!ThrowIfNotConstructing(cx, args, "mozIntl.DateTimeFormat")) + return false; + + return DateTimeFormat(cx, args, true, DateTimeFormatOptions::EnableMozExtensions); +} + +bool +js::intl_DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(!args.isConstructing()); + // intl_DateTimeFormat is an intrinsic for self-hosted JavaScript, so it + // cannot be used with "new", but it still has to be treated as a + // constructor. + return DateTimeFormat(cx, args, true, DateTimeFormatOptions::Standard); +} + +void +js::DateTimeFormatObject::finalize(FreeOp* fop, JSObject* obj) +{ + MOZ_ASSERT(fop->onActiveCooperatingThread()); + + const Value& slot = obj->as().getReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT); + if (UDateFormat* df = static_cast(slot.toPrivate())) + udat_close(df); +} + +JSObject* +js::CreateDateTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handle global, + MutableHandleObject constructor, DateTimeFormatOptions dtfOptions) +{ + RootedFunction ctor(cx); + ctor = dtfOptions == DateTimeFormatOptions::EnableMozExtensions + ? GlobalObject::createConstructor(cx, MozDateTimeFormat, cx->names().DateTimeFormat, 0) + : GlobalObject::createConstructor(cx, DateTimeFormat, cx->names().DateTimeFormat, 0); + if (!ctor) + return nullptr; + + RootedObject proto(cx, GlobalObject::createBlankPrototype(cx, global)); + if (!proto) + return nullptr; + + if (!LinkConstructorAndPrototype(cx, ctor, proto)) + return nullptr; + + // 12.2.2 + if (!JS_DefineFunctions(cx, ctor, dateTimeFormat_static_methods)) + return nullptr; + + // 12.3.2 and 12.3.3 + if (!JS_DefineFunctions(cx, proto, dateTimeFormat_methods)) + return nullptr; + + // Install a getter for DateTimeFormat.prototype.format that returns a + // formatting function bound to a specified DateTimeFormat object (suitable + // for passing to methods like Array.prototype.map). + RootedValue getter(cx); + if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().DateTimeFormatFormatGet, + &getter)) + { + return nullptr; + } + if (!DefineProperty(cx, proto, cx->names().format, UndefinedHandleValue, + JS_DATA_TO_FUNC_PTR(JSGetterOp, &getter.toObject()), + nullptr, JSPROP_GETTER | JSPROP_SHARED)) + { + return nullptr; + } + + // 8.1 + RootedValue ctorValue(cx, ObjectValue(*ctor)); + if (!DefineProperty(cx, Intl, cx->names().DateTimeFormat, ctorValue, nullptr, nullptr, 0)) + return nullptr; + + constructor.set(ctor); + return proto; +} + +bool +js::AddMozDateTimeFormatConstructor(JSContext* cx, JS::Handle intl) +{ + Handle global = cx->global(); + + RootedObject mozDateTimeFormat(cx); + JSObject* mozDateTimeFormatProto = + CreateDateTimeFormatPrototype(cx, intl, global, &mozDateTimeFormat, DateTimeFormatOptions::EnableMozExtensions); + return mozDateTimeFormatProto != nullptr; +} + +static bool +DefaultCalendar(JSContext* cx, const JSAutoByteString& locale, MutableHandleValue rval) +{ + UErrorCode status = U_ZERO_ERROR; + UCalendar* cal = ucal_open(nullptr, 0, locale.ptr(), UCAL_DEFAULT, &status); + + // This correctly handles nullptr |cal| when opening failed. + ScopedICUObject closeCalendar(cal); + + const char* calendar = ucal_getType(cal, &status); + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + + // ICU returns old-style keyword values; map them to BCP 47 equivalents + JSString* str = JS_NewStringCopyZ(cx, uloc_toUnicodeLocaleType("ca", calendar)); + if (!str) + return false; + + rval.setString(str); + return true; +} + +struct CalendarAlias +{ + const char* const calendar; + const char* const alias; +}; + +const CalendarAlias calendarAliases[] = { + { "islamic-civil", "islamicc" }, + { "ethioaa", "ethiopic-amete-alem" } +}; + +bool +js::intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + JSAutoByteString locale(cx, args[0].toString()); + if (!locale) + return false; + + RootedObject calendars(cx, NewDenseEmptyArray(cx)); + if (!calendars) + return false; + uint32_t index = 0; + + // We need the default calendar for the locale as the first result. + RootedValue element(cx); + if (!DefaultCalendar(cx, locale, &element)) + return false; + + if (!DefineElement(cx, calendars, index++, element)) + return false; + + // Now get the calendars that "would make a difference", i.e., not the default. + UErrorCode status = U_ZERO_ERROR; + UEnumeration* values = ucal_getKeywordValuesForLocale("ca", locale.ptr(), false, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(values); + + uint32_t count = uenum_count(values, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + for (; count > 0; count--) { + const char* calendar = uenum_next(values, nullptr, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + // ICU returns old-style keyword values; map them to BCP 47 equivalents + calendar = uloc_toUnicodeLocaleType("ca", calendar); + + JSString* jscalendar = JS_NewStringCopyZ(cx, calendar); + if (!jscalendar) + return false; + element = StringValue(jscalendar); + if (!DefineElement(cx, calendars, index++, element)) + return false; + + // ICU doesn't return calendar aliases, append them here. + for (const auto& calendarAlias : calendarAliases) { + if (StringsAreEqual(calendar, calendarAlias.calendar)) { + JSString* jscalendar = JS_NewStringCopyZ(cx, calendarAlias.alias); + if (!jscalendar) + return false; + element = StringValue(jscalendar); + if (!DefineElement(cx, calendars, index++, element)) + return false; + } + } + } + + args.rval().setObject(*calendars); + return true; +} + +bool +js::intl_defaultCalendar(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + JSAutoByteString locale(cx, args[0].toString()); + if (!locale) + return false; + + return DefaultCalendar(cx, locale, args.rval()); +} + +bool +js::intl_IsValidTimeZoneName(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); + + RootedString timeZone(cx, args[0].toString()); + RootedAtom validatedTimeZone(cx); + if (!sharedIntlData.validateTimeZoneName(cx, timeZone, &validatedTimeZone)) + return false; + + if (validatedTimeZone) { + cx->markAtom(validatedTimeZone); + args.rval().setString(validatedTimeZone); + } else { + args.rval().setNull(); + } + + return true; +} + +bool +js::intl_canonicalizeTimeZone(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); + + // Some time zone names are canonicalized differently by ICU -- handle + // those first: + RootedString timeZone(cx, args[0].toString()); + RootedAtom ianaTimeZone(cx); + if (!sharedIntlData.tryCanonicalizeTimeZoneConsistentWithIANA(cx, timeZone, &ianaTimeZone)) + return false; + + if (ianaTimeZone) { + cx->markAtom(ianaTimeZone); + args.rval().setString(ianaTimeZone); + return true; + } + + AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, timeZone)) + return false; + + mozilla::Range tzchars = stableChars.twoByteRange(); + + JSString* str = CallICU(cx, [&tzchars](UChar* chars, uint32_t size, UErrorCode* status) { + return ucal_getCanonicalTimeZoneID(tzchars.begin().get(), tzchars.length(), + chars, size, nullptr, status); + }); + if (!str) + return false; + args.rval().setString(str); + return true; +} + +bool +js::intl_defaultTimeZone(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + // The current default might be stale, because JS::ResetTimeZone() doesn't + // immediately update ICU's default time zone. So perform an update if + // needed. + js::ResyncICUDefaultTimeZone(); + + JSString* str = CallICU(cx, ucal_getDefaultTimeZone); + if (!str) + return false; + args.rval().setString(str); + return true; +} + +bool +js::intl_defaultTimeZoneOffset(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + UErrorCode status = U_ZERO_ERROR; + const UChar* uTimeZone = nullptr; + int32_t uTimeZoneLength = 0; + const char* rootLocale = ""; + UCalendar* cal = ucal_open(uTimeZone, uTimeZoneLength, rootLocale, UCAL_DEFAULT, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(cal); + + int32_t offset = ucal_get(cal, UCAL_ZONE_OFFSET, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + args.rval().setInt32(offset); + return true; +} + +enum class HourCycle { + // 12 hour cycle, from 0 to 11. + H11, + + // 12 hour cycle, from 1 to 12. + H12, + + // 24 hour cycle, from 0 to 23. + H23, + + // 24 hour cycle, from 1 to 24. + H24 +}; + +static bool +IsHour12(HourCycle hc) +{ + return hc == HourCycle::H11 || hc == HourCycle::H12; +} + +static char16_t +HourSymbol(HourCycle hc) +{ + switch (hc) { + case HourCycle::H11: + return 'K'; + case HourCycle::H12: + return 'h'; + case HourCycle::H23: + return 'H'; + case HourCycle::H24: + return 'k'; + } + MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("unexpected hour cycle"); +} + +/** +* Parse a pattern according to the format specified in +* . +*/ +template +class PatternIterator { + CharT* iter_; + const CharT* const end_; + + public: + explicit PatternIterator(mozilla::Span pattern) + : iter_(pattern.data()), end_(pattern.data() + pattern.size()) {} + + CharT* next() { + MOZ_ASSERT(iter_ != nullptr); + + bool inQuote = false; + while (iter_ < end_) { + CharT* cur = iter_++; + if (*cur == '\'') { + inQuote = !inQuote; + } else if (!inQuote) { + return cur; + } + } + + iter_ = nullptr; + return nullptr; + } +}; + +/** +* Return the hour cycle for the given option string. +*/ +static HourCycle +HourCycleFromOption(JSLinearString* str) +{ + if (StringEqualsAscii(str, "h11")) { + return HourCycle::H11; + } + if (StringEqualsAscii(str, "h12")) { + return HourCycle::H12; + } + if (StringEqualsAscii(str, "h23")) { + return HourCycle::H23; + } + MOZ_ASSERT(StringEqualsAscii(str, "h24")); + return HourCycle::H24; +} + +/** +* Return the hour cycle used in the input pattern or Nothing if none was found. +*/ +static mozilla::Maybe +HourCycleFromPattern(mozilla::Span pattern) +{ + PatternIterator iter(pattern); + while (const auto* ptr = iter.next()) { + switch (*ptr) { + case 'K': + return mozilla::Some(HourCycle::H11); + case 'h': + return mozilla::Some(HourCycle::H12); + case 'H': + return mozilla::Some(HourCycle::H23); + case 'k': + return mozilla::Some(HourCycle::H24); + } + } + return mozilla::Nothing(); +} + +/** +* Replaces all hour pattern characters in |pattern| to use the matching hour +* representation for |hourCycle|. +*/ +static void +ReplaceHourSymbol(mozilla::Span pattern, HourCycle hc) +{ + char16_t replacement = HourSymbol(hc); + PatternIterator iter(pattern); + while (auto* ptr = iter.next()) { + char16_t ch = *ptr; + if (ch == 'K' || ch == 'h' || ch == 'H' || ch == 'k') { + *ptr = replacement; + } + } +} + +bool +js::intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + MOZ_ASSERT(args[0].isString()); + MOZ_ASSERT(args[1].isString()); + MOZ_ASSERT(args[2].isString() || args[2].isUndefined()); + + JSAutoByteString locale(cx, args[0].toString()); + if (!locale) + return false; + + JSFlatString* skeletonFlat = args[1].toString()->ensureFlat(cx); + if (!skeletonFlat) + return false; + + AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, skeletonFlat)) + return false; + + mozilla::Maybe hourCycle; + if (args[2].isString()) { + JSLinearString* hourCycleStr = args[2].toString()->ensureLinear(cx); + if (!hourCycleStr) { + return false; + } + + hourCycle.emplace(HourCycleFromOption(hourCycleStr)); + } + + mozilla::Range skeletonChars = stableChars.twoByteRange(); + uint32_t skeletonLen = u_strlen(Char16ToUChar(skeletonChars.begin().get())); + + UErrorCode status = U_ZERO_ERROR; + UDateTimePatternGenerator* gen = udatpg_open(IcuLocale(locale.ptr()), &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(gen); + + Vector pattern(cx); + + int32_t patternSize = CallICU( + cx, + pattern, + [gen, &skeletonChars](UChar* chars, uint32_t size, UErrorCode* status) { + return udatpg_getBestPattern(gen, skeletonChars.begin().get(), + skeletonChars.length(), chars, size, status); + }); + if (patternSize < 0) { + return false; + } + + // If the hourCycle option was set, adjust the resolved pattern to use the + // requested hour cycle representation. + if (hourCycle) { + ReplaceHourSymbol(pattern, hourCycle.value()); + } + + JSString* str = NewStringCopyN(cx, pattern.begin(), pattern.length()); + if (!str) { + return false; + } + args.rval().setString(str); + return true; +} + +/** + * Find a matching pattern using the requested hour-12 options. + * + * This function is needed to work around the following two issues. + * - https://unicode-org.atlassian.net/browse/ICU-21023 + * - https://unicode-org.atlassian.net/browse/CLDR-13425 + * + * We're currently using a relatively simple workaround, which doesn't give the + * most accurate results. For example: + * + * ``` + * var dtf = new Intl.DateTimeFormat("en", { + * timeZone: "UTC", + * dateStyle: "long", + * timeStyle: "long", + * hourCycle: "h12", + * }); + * print(dtf.format(new Date("2020-01-01T00:00Z"))); + * ``` + * + * Returns the pattern "MMMM d, y 'at' h:mm:ss a z", but when going through + * |udatpg_getSkeleton| and then |udatpg_getBestPattern| to find an equivalent + * pattern for "h23", we'll end up with the pattern "MMMM d, y, HH:mm:ss z", so + * the combinator element " 'at' " was lost in the process. + */ +template +static bool +FindPatternWithHourCycle(JSContext* cx, const char* locale, + Vector& pattern, bool hour12) +{ + UErrorCode status = U_ZERO_ERROR; + UDateTimePatternGenerator* gen = udatpg_open(IcuLocale(locale), &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(gen); + + if (!gen) { + return false; + } + + Vector skeleton(cx); + + int32_t skeletonSize = CallICU( + cx, + skeleton, + [&pattern](UChar* chars, uint32_t size, UErrorCode* status) { + return udatpg_getSkeleton(nullptr, pattern.begin(), pattern.length(), + chars, size, status); + }); + if (skeletonSize < 0) { + return false; + } + + // Input skeletons don't differentiate between "K" and "h" resp. "k" and "H". + ReplaceHourSymbol(skeleton, hour12 ? HourCycle::H12 : HourCycle::H23); + + MOZ_ALWAYS_TRUE(pattern.resize(0)); + + int32_t patternSize = CallICU( + cx, + pattern, + [gen, &skeleton](UChar* chars, uint32_t size, UErrorCode* status) { + return udatpg_getBestPattern(gen, skeleton.begin(), skeleton.length(), + chars, size, status); + }); + if (patternSize < 0) { + return false; + } + + return true; +} + +bool +js::intl_patternForStyle(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 6); + MOZ_ASSERT(args[0].isString()); + MOZ_ASSERT(args[1].isString() || args[1].isUndefined()); + MOZ_ASSERT(args[2].isString() || args[2].isUndefined()); + MOZ_ASSERT(args[3].isString()); + MOZ_ASSERT(args[4].isBoolean() || args[4].isUndefined()); + MOZ_ASSERT(args[5].isString() || args[5].isUndefined()); + + JSAutoByteString locale(cx, args[0].toString()); + if (!locale) + return false; + + auto toDateFormatStyle = [](JSLinearString* str) { + if (StringEqualsAscii(str, "full")) { + return UDAT_FULL; + } + if (StringEqualsAscii(str, "long")) { + return UDAT_LONG; + } + if (StringEqualsAscii(str, "medium")) { + return UDAT_MEDIUM; + } + MOZ_ASSERT(StringEqualsAscii(str, "short")); + return UDAT_SHORT; + }; + + UDateFormatStyle dateStyle = UDAT_NONE; + + if (args[1].isString()) { + JSLinearString* dateStyleStr = args[1].toString()->ensureLinear(cx); + if (!dateStyleStr) + return false; + + dateStyle = toDateFormatStyle(dateStyleStr); + } + + UDateFormatStyle timeStyle = UDAT_NONE; + if (args[2].isString()) { + JSLinearString* timeStyleStr = args[2].toString()->ensureLinear(cx); + if (!timeStyleStr) + return false; + + timeStyle = toDateFormatStyle(timeStyleStr); + } + + AutoStableStringChars timeZone(cx); + if (!timeZone.initTwoByte(cx, args[3].toString())) + return false; + + mozilla::Maybe hour12; + if (args[4].isBoolean()) { + hour12.emplace(args[4].toBoolean()); + } + + mozilla::Maybe hourCycle; + if (args[5].isString()) { + JSLinearString* hourCycleStr = args[5].toString()->ensureLinear(cx); + if (!hourCycleStr) { + return false; + } + + hourCycle.emplace(HourCycleFromOption(hourCycleStr)); + } + + mozilla::Range timeZoneChars = timeZone.twoByteRange(); + + UErrorCode status = U_ZERO_ERROR; + UDateFormat* df = udat_open(timeStyle, dateStyle, IcuLocale(locale.ptr()), + Char16ToUChar(timeZoneChars.begin().get()), + timeZoneChars.length(), nullptr, -1, &status); + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + ScopedICUObject toClose(df); + + Vector pattern(cx); + + int32_t patternSize = CallICU( + cx, + pattern, + [df](UChar* chars, uint32_t size, UErrorCode* status) { + return udat_toPattern(df, false, chars, size, status); + }); + if (patternSize < 0) { + return false; + } + + // If a specific hour cycle was requested and this hour cycle doesn't match + // the hour cycle used in the resolved pattern, find an equivalent pattern + // with the correct hour cycle. + if (timeStyle != UDAT_NONE && (hour12 || hourCycle)) { + if (auto hcPattern = HourCycleFromPattern(pattern)) { + bool wantHour12 = hour12 ? hour12.value() : IsHour12(hourCycle.value()); + if (wantHour12 != IsHour12(hcPattern.value())) { + if (!FindPatternWithHourCycle(cx, locale.ptr(), pattern, wantHour12)) { + return false; + } + } + } + } + + // If the hourCycle option was set, adjust the resolved pattern to use the + // requested hour cycle representation. + if (hourCycle) { + ReplaceHourSymbol(pattern, hourCycle.value()); + } + + JSString* str = NewStringCopyN(cx, pattern.begin(), pattern.length()); + if (!str) + return false; + args.rval().setString(str); + return true; +} + +/** + * Returns a new UDateFormat with the locale and date-time formatting options + * of the given DateTimeFormat. + */ +static UDateFormat* +NewUDateFormat(JSContext* cx, Handle dateTimeFormat) +{ + RootedValue value(cx); + + RootedObject internals(cx, intl::GetInternalsObject(cx, dateTimeFormat)); + if (!internals) + return nullptr; + + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) + return nullptr; + + // ICU expects calendar and numberingSystem as Unicode locale extensions on + // locale. + + intl::LanguageTag tag(cx); + { + JSLinearString* locale = value.toString()->ensureLinear(cx); + if (!locale) + return nullptr; + + if (!intl::LanguageTagParser::parse(cx, locale, tag)) + return nullptr; + } + + JS::RootedVector keywords(cx); + + if (!GetProperty(cx, internals, internals, cx->names().calendar, &value)) + return nullptr; + + { + JSLinearString* calendar = value.toString()->ensureLinear(cx); + if (!calendar) + return nullptr; + + if (!keywords.emplaceBack("ca", calendar)) + return nullptr; + } + + if (!GetProperty(cx, internals, internals, cx->names().numberingSystem, &value)) + return nullptr; + + { + JSLinearString* numberingSystem = value.toString()->ensureLinear(cx); + if (!numberingSystem) + return nullptr; + + if (!keywords.emplaceBack("nu", numberingSystem)) + return nullptr; + } + + // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of + // the Unicode extension subtag. We're then relying on ICU to follow RFC + // 6067, which states that any trailing keywords using the same key + // should be ignored. + if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) + return nullptr; + + UniqueChars locale = tag.toStringZ(cx); + if (!locale) + return nullptr; + + if (!GetProperty(cx, internals, internals, cx->names().timeZone, &value)) + return nullptr; + + AutoStableStringChars timeZoneChars(cx); + Rooted timeZoneFlat(cx, value.toString()->ensureFlat(cx)); + if (!timeZoneFlat || !timeZoneChars.initTwoByte(cx, timeZoneFlat)) + return nullptr; + + const UChar* uTimeZone = Char16ToUChar(timeZoneChars.twoByteRange().begin().get()); + uint32_t uTimeZoneLength = u_strlen(uTimeZone); + + if (!GetProperty(cx, internals, internals, cx->names().pattern, &value)) + return nullptr; + + AutoStableStringChars patternChars(cx); + Rooted patternFlat(cx, value.toString()->ensureFlat(cx)); + if (!patternFlat || !patternChars.initTwoByte(cx, patternFlat)) + return nullptr; + + const UChar* uPattern = Char16ToUChar(patternChars.twoByteRange().begin().get()); + uint32_t uPatternLength = u_strlen(uPattern); + + UErrorCode status = U_ZERO_ERROR; + UDateFormat* df = + udat_open(UDAT_PATTERN, UDAT_PATTERN, IcuLocale(locale.get()), uTimeZone, uTimeZoneLength, + uPattern, uPatternLength, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return nullptr; + } + + // ECMAScript requires the Gregorian calendar to be used from the beginning + // of ECMAScript time. + UCalendar* cal = const_cast(udat_getCalendar(df)); + ucal_setGregorianChange(cal, StartOfTime, &status); + + // An error here means the calendar is not Gregorian, so we don't care. + + return df; +} + +static bool +intl_FormatDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue result) +{ + if (!IsFinite(x)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE, + "DateTimeFormat", "format"); + return false; + } + + JSString* str = CallICU(cx, [df, x](UChar* chars, int32_t size, UErrorCode* status) { + return udat_format(df, x, chars, size, nullptr, status); + }); + if (!str) + return false; + + result.setString(str); + return true; +} + +using FieldType = ImmutablePropertyNamePtr JSAtomState::*; + +static FieldType +GetFieldTypeForFormatField(UDateFormatField fieldName) +{ + // See intl/icu/source/i18n/unicode/udat.h for a detailed field list. This + // switch is deliberately exhaustive: cases might have to be added/removed + // if this code is compiled with a different ICU with more + // UDateFormatField enum initializers. Please guard such cases with + // appropriate ICU version-testing #ifdefs, should cross-version divergence + // occur. + switch (fieldName) { + case UDAT_ERA_FIELD: + return &JSAtomState::era; + case UDAT_YEAR_FIELD: + case UDAT_YEAR_WOY_FIELD: + case UDAT_EXTENDED_YEAR_FIELD: + case UDAT_YEAR_NAME_FIELD: + return &JSAtomState::year; + + case UDAT_MONTH_FIELD: + case UDAT_STANDALONE_MONTH_FIELD: + return &JSAtomState::month; + + case UDAT_DATE_FIELD: + case UDAT_JULIAN_DAY_FIELD: + return &JSAtomState::day; + + case UDAT_HOUR_OF_DAY1_FIELD: + case UDAT_HOUR_OF_DAY0_FIELD: + case UDAT_HOUR1_FIELD: + case UDAT_HOUR0_FIELD: + return &JSAtomState::hour; + + case UDAT_MINUTE_FIELD: + return &JSAtomState::minute; + + case UDAT_SECOND_FIELD: + return &JSAtomState::second; + + case UDAT_DAY_OF_WEEK_FIELD: + case UDAT_STANDALONE_DAY_FIELD: + case UDAT_DOW_LOCAL_FIELD: + case UDAT_DAY_OF_WEEK_IN_MONTH_FIELD: + return &JSAtomState::weekday; + + case UDAT_AM_PM_FIELD: + return &JSAtomState::dayPeriod; + + case UDAT_TIMEZONE_FIELD: + return &JSAtomState::timeZoneName; + + case UDAT_FRACTIONAL_SECOND_FIELD: + case UDAT_DAY_OF_YEAR_FIELD: + case UDAT_WEEK_OF_YEAR_FIELD: + case UDAT_WEEK_OF_MONTH_FIELD: + case UDAT_MILLISECONDS_IN_DAY_FIELD: + case UDAT_TIMEZONE_RFC_FIELD: + case UDAT_TIMEZONE_GENERIC_FIELD: + case UDAT_QUARTER_FIELD: + case UDAT_STANDALONE_QUARTER_FIELD: + case UDAT_TIMEZONE_SPECIAL_FIELD: + case UDAT_TIMEZONE_LOCALIZED_GMT_OFFSET_FIELD: + case UDAT_TIMEZONE_ISO_FIELD: + case UDAT_TIMEZONE_ISO_LOCAL_FIELD: +#ifndef U_HIDE_INTERNAL_API + case UDAT_RELATED_YEAR_FIELD: +#endif +#ifndef U_HIDE_DRAFT_API + case UDAT_AM_PM_MIDNIGHT_NOON_FIELD: + case UDAT_FLEXIBLE_DAY_PERIOD_FIELD: +#endif +#ifndef U_HIDE_INTERNAL_API + case UDAT_TIME_SEPARATOR_FIELD: +#endif + // These fields are all unsupported. + return nullptr; + + case UDAT_FIELD_COUNT: + MOZ_ASSERT_UNREACHABLE("format field sentinel value returned by " + "iterator!"); + } + + MOZ_ASSERT_UNREACHABLE("unenumerated, undocumented format field returned " + "by iterator"); + return nullptr; +} + +static bool +intl_FormatToPartsDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue result) +{ + if (!IsFinite(x)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE, + "DateTimeFormat", "formatToParts"); + return false; + } + + Vector chars(cx); + if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) + return false; + + UErrorCode status = U_ZERO_ERROR; + UFieldPositionIterator* fpositer = ufieldpositer_open(&status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(fpositer); + + RootedString overallResult(cx); + overallResult = CallICU(cx, [df, x, fpositer](UChar* chars, int32_t size, UErrorCode* status) { + return udat_formatForFields(df, x, chars, size, fpositer, status); + }); + if (!overallResult) + return false; + + RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx)); + if (!partsArray) + return false; + if (overallResult->length() == 0) { + // An empty string contains no parts, so avoid extra work below. + result.setObject(*partsArray); + return true; + } + + size_t lastEndIndex = 0; + + uint32_t partIndex = 0; + RootedObject singlePart(cx); + RootedValue partType(cx); + RootedValue val(cx); + + auto AppendPart = [&](FieldType type, size_t beginIndex, size_t endIndex) { + singlePart = NewBuiltinClassInstance(cx); + if (!singlePart) + return false; + + partType = StringValue(cx->names().*type); + if (!DefineProperty(cx, singlePart, cx->names().type, partType)) + return false; + + JSLinearString* partSubstr = + NewDependentString(cx, overallResult, beginIndex, endIndex - beginIndex); + if (!partSubstr) + return false; + + val = StringValue(partSubstr); + if (!DefineProperty(cx, singlePart, cx->names().value, val)) + return false; + + val = ObjectValue(*singlePart); + if (!DefineElement(cx, partsArray, partIndex, val)) + return false; + + lastEndIndex = endIndex; + partIndex++; + return true; + }; + + int32_t fieldInt, beginIndexInt, endIndexInt; + while ((fieldInt = ufieldpositer_next(fpositer, &beginIndexInt, &endIndexInt)) >= 0) { + MOZ_ASSERT(beginIndexInt >= 0); + MOZ_ASSERT(endIndexInt >= 0); + MOZ_ASSERT(beginIndexInt <= endIndexInt, + "field iterator returning invalid range"); + + size_t beginIndex(beginIndexInt); + size_t endIndex(endIndexInt); + + // Technically this isn't guaranteed. But it appears true in pratice, + // and http://bugs.icu-project.org/trac/ticket/12024 is expected to + // correct the documentation lapse. + MOZ_ASSERT(lastEndIndex <= beginIndex, + "field iteration didn't return fields in order start to " + "finish as expected"); + + if (FieldType type = GetFieldTypeForFormatField(static_cast(fieldInt))) { + if (lastEndIndex < beginIndex) { + if (!AppendPart(&JSAtomState::literal, lastEndIndex, beginIndex)) + return false; + } + + if (!AppendPart(type, beginIndex, endIndex)) + return false; + } + } + + // Append any final literal. + if (lastEndIndex < overallResult->length()) { + if (!AppendPart(&JSAtomState::literal, lastEndIndex, overallResult->length())) + return false; + } + + result.setObject(*partsArray); + return true; +} + +bool +js::intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + MOZ_ASSERT(args[0].isObject()); + MOZ_ASSERT(args[1].isNumber()); + MOZ_ASSERT(args[2].isBoolean()); + + Rooted dateTimeFormat(cx); + dateTimeFormat = &args[0].toObject().as(); + + // Obtain a cached UDateFormat object. + void* priv = + dateTimeFormat->getReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT).toPrivate(); + UDateFormat* df = static_cast(priv); + if (!df) { + df = NewUDateFormat(cx, dateTimeFormat); + if (!df) + return false; + dateTimeFormat->setReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT, PrivateValue(df)); + } + + // Use the UDateFormat to actually format the time stamp. + return args[2].toBoolean() + ? intl_FormatToPartsDateTime(cx, df, args[1].toNumber(), args.rval()) + : intl_FormatDateTime(cx, df, args[1].toNumber(), args.rval()); +} + diff --git a/js/src/builtin/intl/Locale.cpp b/js/src/builtin/intl/Locale.cpp index b34ee953ca..3e2aa6ff46 100644 --- a/js/src/builtin/intl/Locale.cpp +++ b/js/src/builtin/intl/Locale.cpp @@ -481,7 +481,7 @@ static bool Locale(JSContext* cx, unsigned argc, Value* vp) { // Steps 2-6 (Inlined 9.1.14, OrdinaryCreateFromConstructor). RootedObject proto(cx); - if (!GetPrototypeFromCallableConstructor(cx, args, &proto)) { + if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) { return false; } diff --git a/js/src/builtin/intl/NumberFormat.cpp b/js/src/builtin/intl/NumberFormat.cpp index 76d5753efd..a5b53b6300 100644 --- a/js/src/builtin/intl/NumberFormat.cpp +++ b/js/src/builtin/intl/NumberFormat.cpp @@ -1,886 +1,886 @@ -/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/* Intl.NumberFormat implementation. */ - -#include "builtin/intl/NumberFormat.h" - -#include "mozilla/Assertions.h" -#include "mozilla/FloatingPoint.h" - -#include -#include -#include - -#include "jscntxt.h" - -#include "builtin/intl/CommonFunctions.h" -#include "builtin/intl/ICUHeader.h" -#include "builtin/intl/LanguageTag.h" -#include "builtin/intl/ScopedICUObject.h" -#include "ds/Sort.h" -#include "js/RootingAPI.h" - -#include "js/TypeDecls.h" -#include "vm/SelfHosting.h" -#include "vm/Stack.h" - -#include "jsobjinlines.h" - -using namespace js; - -using mozilla::AssertedCast; -using mozilla::IsFinite; -using mozilla::IsNegative; -using mozilla::IsNaN; -using mozilla::IsNegativeZero; -using js::intl::CallICU; -using js::intl::DateTimeFormatOptions; -using js::intl::IcuLocale; -using js::intl::INITIAL_CHAR_BUFFER_SIZE; -using js::intl::StringsAreEqual; - -/******************** NumberFormat ********************/ - -const ClassOps NumberFormatObject::classOps_ = { - nullptr, /* addProperty */ - nullptr, /* delProperty */ - nullptr, /* getProperty */ - nullptr, /* setProperty */ - nullptr, /* enumerate */ - nullptr, /* resolve */ - nullptr, /* mayResolve */ - NumberFormatObject::finalize -}; - -const Class NumberFormatObject::class_ = { - js_Object_str, - JSCLASS_HAS_RESERVED_SLOTS(NumberFormatObject::SLOT_COUNT) | - JSCLASS_FOREGROUND_FINALIZE, - &NumberFormatObject::classOps_ -}; - -#if JS_HAS_TOSOURCE -static bool -numberFormat_toSource(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - args.rval().setString(cx->names().NumberFormat); - return true; -} -#endif - -static const JSFunctionSpec numberFormat_static_methods[] = { - JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_NumberFormat_supportedLocalesOf", 1, 0), - JS_FS_END -}; - -static const JSFunctionSpec numberFormat_methods[] = { - JS_SELF_HOSTED_FN("resolvedOptions", "Intl_NumberFormat_resolvedOptions", 0, 0), - JS_SELF_HOSTED_FN("formatToParts", "Intl_NumberFormat_formatToParts", 1, 0), -#if JS_HAS_TOSOURCE - JS_FN(js_toSource_str, numberFormat_toSource, 0, 0), -#endif - JS_FS_END -}; - -/** - * 11.2.1 Intl.NumberFormat([ locales [, options]]) - * - * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b - */ -static bool -NumberFormat(JSContext* cx, const CallArgs& args, bool construct) -{ - // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code). - - // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). - RootedObject proto(cx); - if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto)) - return false; - - if (!proto) { - proto = GlobalObject::getOrCreateNumberFormatPrototype(cx, cx->global()); - if (!proto) - return false; - } - - Rooted numberFormat(cx); - numberFormat = NewObjectWithGivenProto(cx, proto); - if (!numberFormat) - return false; - - numberFormat->setReservedSlot(NumberFormatObject::INTERNALS_SLOT, NullValue()); - numberFormat->setReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT, PrivateValue(nullptr)); - - RootedValue thisValue(cx, construct ? ObjectValue(*numberFormat) : args.thisv()); - RootedValue locales(cx, args.get(0)); - RootedValue options(cx, args.get(1)); - - // Step 3. - return intl::LegacyIntlInitialize(cx, numberFormat, cx->names().InitializeNumberFormat, thisValue, - locales, options, DateTimeFormatOptions::Standard, args.rval()); -} - -static bool -NumberFormat(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - return NumberFormat(cx, args, args.isConstructing()); -} - -bool -js::intl_NumberFormat(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 2); - MOZ_ASSERT(!args.isConstructing()); - // intl_NumberFormat is an intrinsic for self-hosted JavaScript, so it - // cannot be used with "new", but it still has to be treated as a - // constructor. - return NumberFormat(cx, args, true); -} - -void -js::NumberFormatObject::finalize(FreeOp* fop, JSObject* obj) -{ - MOZ_ASSERT(fop->onMainThread()); - - const Value& slot = obj->as().getReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT); - if (UNumberFormat* nf = static_cast(slot.toPrivate())) - unum_close(nf); -} - -JSObject* -js::CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handle global, - MutableHandleObject constructor) -{ - RootedFunction ctor(cx); - ctor = GlobalObject::createConstructor(cx, &NumberFormat, cx->names().NumberFormat, 0); - if (!ctor) - return nullptr; - - RootedObject proto(cx, GlobalObject::createBlankPrototype(cx, global)); - if (!proto) - return nullptr; - - if (!LinkConstructorAndPrototype(cx, ctor, proto)) - return nullptr; - - // 11.2.2 - if (!JS_DefineFunctions(cx, ctor, numberFormat_static_methods)) - return nullptr; - - // 11.3.2 and 11.3.3 - if (!JS_DefineFunctions(cx, proto, numberFormat_methods)) - return nullptr; - - /* - * Install the getter for NumberFormat.prototype.format, which returns a - * bound formatting function for the specified NumberFormat object (suitable - * for passing to methods like Array.prototype.map). - */ - RootedValue getter(cx); - if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().NumberFormatFormatGet, - &getter)) - { - return nullptr; - } - if (!DefineProperty(cx, proto, cx->names().format, UndefinedHandleValue, - JS_DATA_TO_FUNC_PTR(JSGetterOp, &getter.toObject()), - nullptr, JSPROP_GETTER | JSPROP_SHARED)) - { - return nullptr; - } - - // 8.1 - RootedValue ctorValue(cx, ObjectValue(*ctor)); - if (!DefineProperty(cx, Intl, cx->names().NumberFormat, ctorValue, nullptr, nullptr, 0)) - return nullptr; - - constructor.set(ctor); - return proto; -} - -bool -js::intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 1); - MOZ_ASSERT(args[0].isString()); - - JSAutoByteString locale(cx, args[0].toString()); - if (!locale) - return false; - - UErrorCode status = U_ZERO_ERROR; - UNumberingSystem* numbers = unumsys_open(IcuLocale(locale.ptr()), &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - ScopedICUObject toClose(numbers); - - const char* name = unumsys_getName(numbers); - RootedString jsname(cx, JS_NewStringCopyZ(cx, name)); - if (!jsname) - return false; - - args.rval().setString(jsname); - return true; -} - -/** - * Returns a new UNumberFormat with the locale and number formatting options - * of the given NumberFormat. - */ -static UNumberFormat* -NewUNumberFormat(JSContext* cx, Handle numberFormat) -{ - RootedValue value(cx); - - RootedObject internals(cx, intl::GetInternalsObject(cx, numberFormat)); - if (!internals) - return nullptr; - - if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) - return nullptr; - - // ICU expects numberingSystem as a Unicode locale extensions on locale. - - intl::LanguageTag tag(cx); - { - JSLinearString* locale = value.toString()->ensureLinear(cx); - if (!locale) - return nullptr; - - if (!intl::LanguageTagParser::parse(cx, locale, tag)) - return nullptr; - } - - JS::RootedVector keywords(cx); - - if (!GetProperty(cx, internals, internals, cx->names().numberingSystem, &value)) - return nullptr; - - { - JSLinearString* numberingSystem = value.toString()->ensureLinear(cx); - if (!numberingSystem) - return nullptr; - - if (!keywords.emplaceBack("nu", numberingSystem)) - return nullptr; - } - - // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of - // the Unicode extension subtag. We're then relying on ICU to follow RFC - // 6067, which states that any trailing keywords using the same key - // should be ignored. - if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) - return nullptr; - - UniqueChars locale = tag.toStringZ(cx); - if (!locale) - return nullptr; - - // UNumberFormat options with default values - UNumberFormatStyle uStyle = UNUM_DECIMAL; - const UChar* uCurrency = nullptr; - uint32_t uMinimumIntegerDigits = 1; - uint32_t uMinimumFractionDigits = 0; - uint32_t uMaximumFractionDigits = 3; - int32_t uMinimumSignificantDigits = -1; - int32_t uMaximumSignificantDigits = -1; - bool uUseGrouping = true; - - // Sprinkle appropriate rooting flavor over things the GC might care about. - RootedString currency(cx); - AutoStableStringChars stableChars(cx); - - if (!GetProperty(cx, internals, internals, cx->names().style, &value)) - return nullptr; - JSAutoByteString style(cx, value.toString()); - if (!style) - return nullptr; - - if (StringsAreEqual(style, "currency")) { - if (!GetProperty(cx, internals, internals, cx->names().currency, &value)) - return nullptr; - currency = value.toString(); - MOZ_ASSERT(currency->length() == 3, - "IsWellFormedCurrencyCode permits only length-3 strings"); - if (!currency->ensureFlat(cx) || !stableChars.initTwoByte(cx, currency)) - return nullptr; - // uCurrency remains owned by stableChars. - uCurrency = Char16ToUChar(stableChars.twoByteRange().begin().get()); - if (!uCurrency) - return nullptr; - - if (!GetProperty(cx, internals, internals, cx->names().currencyDisplay, &value)) - return nullptr; - JSAutoByteString currencyDisplay(cx, value.toString()); - if (!currencyDisplay) - return nullptr; - if (StringsAreEqual(currencyDisplay, "code")) { - uStyle = UNUM_CURRENCY_ISO; - } else if (StringsAreEqual(currencyDisplay, "symbol")) { - uStyle = UNUM_CURRENCY; - } else { - MOZ_ASSERT(StringsAreEqual(currencyDisplay, "name")); - uStyle = UNUM_CURRENCY_PLURAL; - } - } else if (StringsAreEqual(style, "percent")) { - uStyle = UNUM_PERCENT; - } else { - MOZ_ASSERT(StringsAreEqual(style, "decimal")); - uStyle = UNUM_DECIMAL; - } - - RootedId id(cx, NameToId(cx->names().minimumSignificantDigits)); - bool hasP; - if (!HasProperty(cx, internals, id, &hasP)) - return nullptr; - if (hasP) { - if (!GetProperty(cx, internals, internals, cx->names().minimumSignificantDigits, - &value)) - return nullptr; - uMinimumSignificantDigits = value.toInt32(); - if (!GetProperty(cx, internals, internals, cx->names().maximumSignificantDigits, - &value)) - return nullptr; - uMaximumSignificantDigits = value.toInt32(); - } else { - if (!GetProperty(cx, internals, internals, cx->names().minimumFractionDigits, - &value)) - return nullptr; - uMinimumFractionDigits = AssertedCast(value.toInt32()); - if (!GetProperty(cx, internals, internals, cx->names().maximumFractionDigits, - &value)) - return nullptr; - uMaximumFractionDigits = AssertedCast(value.toInt32()); - } - - if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits, - &value)) - return nullptr; - uMinimumIntegerDigits = AssertedCast(value.toInt32()); - - if (!GetProperty(cx, internals, internals, cx->names().useGrouping, &value)) - return nullptr; - uUseGrouping = value.toBoolean(); - - UErrorCode status = U_ZERO_ERROR; - UNumberFormat* nf = unum_open(uStyle, nullptr, 0, IcuLocale(locale.get()), nullptr, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return nullptr; - } - ScopedICUObject toClose(nf); - - if (uCurrency) { - unum_setTextAttribute(nf, UNUM_CURRENCY_CODE, uCurrency, 3, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return nullptr; - } - } - if (uMinimumSignificantDigits != -1) { - unum_setAttribute(nf, UNUM_SIGNIFICANT_DIGITS_USED, true); - unum_setAttribute(nf, UNUM_MIN_SIGNIFICANT_DIGITS, uMinimumSignificantDigits); - unum_setAttribute(nf, UNUM_MAX_SIGNIFICANT_DIGITS, uMaximumSignificantDigits); - } else { - unum_setAttribute(nf, UNUM_MIN_INTEGER_DIGITS, uMinimumIntegerDigits); - unum_setAttribute(nf, UNUM_MIN_FRACTION_DIGITS, uMinimumFractionDigits); - unum_setAttribute(nf, UNUM_MAX_FRACTION_DIGITS, uMaximumFractionDigits); - } - unum_setAttribute(nf, UNUM_GROUPING_USED, uUseGrouping); - unum_setAttribute(nf, UNUM_ROUNDING_MODE, UNUM_ROUND_HALFUP); - - return toClose.forget(); -} - -static JSString* -PartitionNumberPattern(JSContext* cx, UNumberFormat* nf, HandleValue x, - UFieldPositionIterator* fpositer) -{ - if (x.isNumber()) { - double num = x.toNumber(); - - // PartitionNumberPattern doesn't consider -0.0 to be negative. - if (IsNegativeZero(num)) - num = 0.0; - - return CallICU(cx, [nf, num, fpositer](UChar* chars, int32_t size, UErrorCode* status) { - return unum_formatDoubleForFields(nf, num, chars, size, fpositer, status); - }); - } else if(x.isBigInt()) { - RootedBigInt bi(cx, x.toBigInt()); - int64_t num; - - if (BigInt::isInt64(bi, &num)) { - return CallICU(cx, [nf, num](UChar* chars, int32_t size, UErrorCode* status) { - return unum_formatInt64(nf, num, chars, size, nullptr, status); - }); - } else { - JSLinearString* str = BigInt::toString(cx, bi, 10); - if (!str) { - return nullptr; - } - MOZ_ASSERT(str->hasLatin1Chars()); - - JS::AutoCheckCannotGC noGC(cx); - const char* latinchars = reinterpret_cast(str->latin1Chars(noGC)); - size_t length = str->length(); - return CallICU(cx, [nf, latinchars, length](UChar* chars, int32_t size, UErrorCode* status) { - return unum_formatDecimal(nf, latinchars, length, chars, size, nullptr, status); - }); - } - } - return nullptr; -} - -bool -js::FormatNumeric(JSContext* cx, UNumberFormat* nf, HandleValue x, MutableHandleValue result) -{ - // Passing null for |fpositer| will just not compute partition information, - // letting us common up all ICU number-formatting code. - JSString* str = PartitionNumberPattern(cx, nf, x, nullptr); - if (!str) - return false; - - result.setString(str); - return true; -} - -using FieldType = ImmutablePropertyNamePtr JSAtomState::*; - -static FieldType -GetFieldTypeForNumberField(UNumberFormatFields fieldName, HandleValue x) -{ - // See intl/icu/source/i18n/unicode/unum.h for a detailed field list. This - // list is deliberately exhaustive: cases might have to be added/removed if - // this code is compiled with a different ICU with more UNumberFormatFields - // enum initializers. Please guard such cases with appropriate ICU - // version-testing #ifdefs, should cross-version divergence occur. - switch (fieldName) { - case UNUM_INTEGER_FIELD: - if (x.isNumber()) { - double d = x.toNumber(); - if (IsNaN(d)) { - return &JSAtomState::nan; - } - if (!IsFinite(d)) { - return &JSAtomState::infinity; - } - } - return &JSAtomState::integer; - - case UNUM_GROUPING_SEPARATOR_FIELD: - return &JSAtomState::group; - - case UNUM_DECIMAL_SEPARATOR_FIELD: - return &JSAtomState::decimal; - - case UNUM_FRACTION_FIELD: - return &JSAtomState::fraction; - - case UNUM_SIGN_FIELD: { - // Manual trawling through the ICU call graph appears to indicate that - // the basic formatting we request will never include a positive sign. - // But this analysis may be mistaken, so don't absolutely trust it. - MOZ_ASSERT(!x.isNumber() || !IsNaN(x.toNumber()), - "ICU appearing not to produce positive-sign among fields, " - "plus our coercing all NaNs to one with sign bit unset " - "(i.e. \"positive\"), means we shouldn't reach here with a " - "NaN value"); - bool isNegative = - x.isNumber() ? IsNegative(x.toNumber()) : x.toBigInt()->isNegative(); - return isNegative ? &JSAtomState::minusSign : &JSAtomState::plusSign; - } - - case UNUM_PERCENT_FIELD: - return &JSAtomState::percentSign; - - case UNUM_CURRENCY_FIELD: - return &JSAtomState::currency; - - case UNUM_PERMILL_FIELD: - MOZ_ASSERT_UNREACHABLE("unexpected permill field found, even though " - "we don't use any user-defined patterns that " - "would require a permill field"); - break; - - case UNUM_EXPONENT_SYMBOL_FIELD: - case UNUM_EXPONENT_SIGN_FIELD: - case UNUM_EXPONENT_FIELD: - MOZ_ASSERT_UNREACHABLE("exponent field unexpectedly found in " - "formatted number, even though UNUM_SCIENTIFIC " - "and scientific notation were never requested"); - break; - - case UNUM_FIELD_COUNT: - MOZ_ASSERT_UNREACHABLE("format field sentinel value returned by " - "iterator!"); - break; - } - - MOZ_ASSERT_UNREACHABLE("unenumerated, undocumented format field returned " - "by iterator"); - return nullptr; -} - -static bool -FormatNumericToParts(JSContext* cx, UNumberFormat* nf, HandleValue x, MutableHandleValue result) -{ - UErrorCode status = U_ZERO_ERROR; - - UFieldPositionIterator* fpositer = ufieldpositer_open(&status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - MOZ_ASSERT(fpositer); - ScopedICUObject toClose(fpositer); - - RootedString overallResult(cx, PartitionNumberPattern(cx, nf, x, fpositer)); - if (!overallResult) - return false; - - RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx)); - if (!partsArray) - return false; - - // First, vacuum up fields in the overall formatted string. - - struct Field - { - uint32_t begin; - uint32_t end; - FieldType type; - - // Needed for vector-resizing scratch space. - Field() = default; - - Field(uint32_t begin, uint32_t end, FieldType type) - : begin(begin), end(end), type(type) - {} - }; - - using FieldsVector = Vector; - FieldsVector fields(cx); - - int32_t fieldInt, beginIndexInt, endIndexInt; - while ((fieldInt = ufieldpositer_next(fpositer, &beginIndexInt, &endIndexInt)) >= 0) { - MOZ_ASSERT(beginIndexInt >= 0); - MOZ_ASSERT(endIndexInt >= 0); - MOZ_ASSERT(beginIndexInt < endIndexInt, - "erm, aren't fields always non-empty?"); - - FieldType type = GetFieldTypeForNumberField(UNumberFormatFields(fieldInt), x); - if (!fields.emplaceBack(uint32_t(beginIndexInt), uint32_t(endIndexInt), type)) - return false; - } - - // Second, merge sort the fields vector. Expand the vector to have scratch - // space for performing the sort. - size_t fieldsLen = fields.length(); - if (!fields.resizeUninitialized(fieldsLen * 2)) - return false; - - MOZ_ALWAYS_TRUE(MergeSort(fields.begin(), fieldsLen, fields.begin() + fieldsLen, - [](const Field& left, const Field& right, - bool* lessOrEqual) - { - // Sort first by begin index, then to place - // enclosing fields before nested fields. - *lessOrEqual = left.begin < right.begin || - (left.begin == right.begin && - left.end > right.end); - return true; - })); - - // Deallocate the scratch space. - if (!fields.resize(fieldsLen)) - return false; - - // Third, iterate over the sorted field list to generate a sequence of - // parts (what ECMA-402 actually exposes). A part is a maximal character - // sequence entirely within no field or a single most-nested field. - // - // Diagrams may be helpful to illustrate how fields map to parts. Consider - // formatting -28,114,774,228,750.32, the US national surplus (negative - // because it's actually a debt) on March 31, 2021. - // - // var options = - // { style: "currency", currency: "USD", currencyDisplay: "name" }; - // var usdFormatter = new Intl.NumberFormat("en-US", options); - // usdFormatter.format(-28114774228750.32); - // - // The formatted result is "-28,114,774,228,750.32 US dollars". ICU - // identifies these fields in the string: - // - // UNUM_GROUPING_SEPARATOR_FIELD - // | - // UNUM_SIGN_FIELD | UNUM_DECIMAL_SEPARATOR_FIELD - // | __________/| | - // | / | | | | - // "-28,114,774,228,750.32 US dollars" - // \________________/ |/ \_______/ - // | | | - // UNUM_INTEGER_FIELD | UNUM_CURRENCY_FIELD - // | - // UNUM_FRACTION_FIELD - // - // These fields map to parts as follows: - // - // integer decimal - // _____|________ | - // / /| |\ |\ |\ | literal - // /| / | | \ | \ | \| | - // "-28,114,774,228,750.32 US dollars" - // | \___|___|___/ |/ \________/ - // | | | | - // | group | currency - // | | - // minusSign fraction - // - // The sign is a part. Each comma is a part, splitting the integer field - // into parts for trillions/billions/&c. digits. The decimal point is a - // part. Cents are a part. The space between cents and currency is a part - // (outside any field). Last, the currency field is a part. - // - // Because parts fully partition the formatted string, we only track the - // end of each part -- the beginning is implicitly the last part's end. - struct Part - { - uint32_t end; - FieldType type; - }; - - class PartGenerator - { - // The fields in order from start to end, then least to most nested. - const FieldsVector& fields; - - // Index of the current field, in |fields|, being considered to - // determine part boundaries. |lastEnd <= fields[index].begin| is an - // invariant. - size_t index; - - // The end index of the last part produced, always less than or equal - // to |limit|, strictly increasing. - uint32_t lastEnd; - - // The length of the overall formatted string. - const uint32_t limit; - - Vector enclosingFields; - - void popEnclosingFieldsEndingAt(uint32_t end) { - MOZ_ASSERT_IF(enclosingFields.length() > 0, - fields[enclosingFields.back()].end >= end); - - while (enclosingFields.length() > 0 && fields[enclosingFields.back()].end == end) - enclosingFields.popBack(); - } - - bool nextPartInternal(Part* part) { - size_t len = fields.length(); - MOZ_ASSERT(index <= len); - - // If we're out of fields, all that remains are part(s) consisting - // of trailing portions of enclosing fields, and maybe a final - // literal part. - if (index == len) { - if (enclosingFields.length() > 0) { - const auto& enclosing = fields[enclosingFields.popCopy()]; - part->end = enclosing.end; - part->type = enclosing.type; - - // If additional enclosing fields end where this part ends, - // pop them as well. - popEnclosingFieldsEndingAt(part->end); - } else { - part->end = limit; - part->type = &JSAtomState::literal; - } - - return true; - } - - // Otherwise we still have a field to process. - const Field* current = &fields[index]; - MOZ_ASSERT(lastEnd <= current->begin); - MOZ_ASSERT(current->begin < current->end); - - // But first, deal with inter-field space. - if (lastEnd < current->begin) { - if (enclosingFields.length() > 0) { - // Space between fields, within an enclosing field, is part - // of that enclosing field, until the start of the current - // field or the end of the enclosing field, whichever is - // earlier. - const auto& enclosing = fields[enclosingFields.back()]; - part->end = std::min(enclosing.end, current->begin); - part->type = enclosing.type; - popEnclosingFieldsEndingAt(part->end); - } else { - // If there's no enclosing field, the space is a literal. - part->end = current->begin; - part->type = &JSAtomState::literal; - } - - return true; - } - - // Otherwise, the part spans a prefix of the current field. Find - // the most-nested field containing that prefix. - const Field* next; - do { - current = &fields[index]; - - // If the current field is last, the part extends to its end. - if (++index == len) { - part->end = current->end; - part->type = current->type; - return true; - } - - next = &fields[index]; - MOZ_ASSERT(current->begin <= next->begin); - MOZ_ASSERT(current->begin < next->end); - - // If the next field nests within the current field, push an - // enclosing field. (If there are no nested fields, don't - // bother pushing a field that'd be immediately popped.) - if (current->end > next->begin) { - if (!enclosingFields.append(index - 1)) - return false; - } - - // Do so until the next field begins after this one. - } while (current->begin == next->begin); - - part->type = current->type; - - if (current->end <= next->begin) { - // The next field begins after the current field ends. Therefore - // the current part ends at the end of the current field. - part->end = current->end; - popEnclosingFieldsEndingAt(part->end); - } else { - // The current field encloses the next one. The current part - // ends where the next field/part will start. - part->end = next->begin; - } - - return true; - } - - public: - PartGenerator(JSContext* cx, const FieldsVector& vec, uint32_t limit) - : fields(vec), index(0), lastEnd(0), limit(limit), enclosingFields(cx) - {} - - bool nextPart(bool* hasPart, Part* part) { - // There are no parts left if we've partitioned the entire string. - if (lastEnd == limit) { - MOZ_ASSERT(enclosingFields.length() == 0); - *hasPart = false; - return true; - } - - if (!nextPartInternal(part)) - return false; - - *hasPart = true; - lastEnd = part->end; - return true; - } - }; - - // Finally, generate the result array. - size_t lastEndIndex = 0; - uint32_t partIndex = 0; - RootedObject singlePart(cx); - RootedValue propVal(cx); - - PartGenerator gen(cx, fields, overallResult->length()); - do { - bool hasPart; - Part part; - if (!gen.nextPart(&hasPart, &part)) - return false; - - if (!hasPart) - break; - - FieldType type = part.type; - size_t endIndex = part.end; - - MOZ_ASSERT(lastEndIndex < endIndex); - - singlePart = NewBuiltinClassInstance(cx); - if (!singlePart) - return false; - - propVal.setString(cx->names().*type); - if (!DefineProperty(cx, singlePart, cx->names().type, propVal)) - return false; - - JSLinearString* partSubstr = - NewDependentString(cx, overallResult, lastEndIndex, endIndex - lastEndIndex); - if (!partSubstr) - return false; - - propVal.setString(partSubstr); - if (!DefineProperty(cx, singlePart, cx->names().value, propVal)) - return false; - - propVal.setObject(*singlePart); - if (!DefineElement(cx, partsArray, partIndex, propVal)) - return false; - - lastEndIndex = endIndex; - partIndex++; - } while (true); - - MOZ_ASSERT(lastEndIndex == overallResult->length(), - "result array must partition the entire string"); - - result.setObject(*partsArray); - return true; -} - -bool -js::intl_FormatNumber(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 3); - MOZ_ASSERT(args[0].isObject()); - MOZ_ASSERT(args[1].isNumeric()); - MOZ_ASSERT(args[2].isBoolean()); - - Rooted numberFormat(cx, &args[0].toObject().as()); - - // Obtain a cached UNumberFormat object. - void* priv = - numberFormat->getReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT).toPrivate(); - UNumberFormat* nf = static_cast(priv); - if (!nf) { - nf = NewUNumberFormat(cx, numberFormat); - if (!nf) - return false; - numberFormat->setReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT, PrivateValue(nf)); - } - - // Use the UNumberFormat to actually format the number. - if (args[2].toBoolean()) { - return FormatNumericToParts(cx, nf, args.get(1), args.rval()); - } - return FormatNumeric(cx, nf, args.get(1), args.rval()); -} +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Intl.NumberFormat implementation. */ + +#include "builtin/intl/NumberFormat.h" + +#include "mozilla/Assertions.h" +#include "mozilla/FloatingPoint.h" + +#include +#include +#include + +#include "jscntxt.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/ICUHeader.h" +#include "builtin/intl/LanguageTag.h" +#include "builtin/intl/ScopedICUObject.h" +#include "ds/Sort.h" +#include "js/RootingAPI.h" + +#include "js/TypeDecls.h" +#include "vm/SelfHosting.h" +#include "vm/Stack.h" + +#include "jsobjinlines.h" + +using namespace js; + +using mozilla::AssertedCast; +using mozilla::IsFinite; +using mozilla::IsNegative; +using mozilla::IsNaN; +using mozilla::IsNegativeZero; +using js::intl::CallICU; +using js::intl::DateTimeFormatOptions; +using js::intl::IcuLocale; +using js::intl::INITIAL_CHAR_BUFFER_SIZE; +using js::intl::StringsAreEqual; + +/******************** NumberFormat ********************/ + +const ClassOps NumberFormatObject::classOps_ = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* setProperty */ + nullptr, /* enumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + NumberFormatObject::finalize +}; + +const Class NumberFormatObject::class_ = { + js_Object_str, + JSCLASS_HAS_RESERVED_SLOTS(NumberFormatObject::SLOT_COUNT) | + JSCLASS_FOREGROUND_FINALIZE, + &NumberFormatObject::classOps_ +}; + +#if JS_HAS_TOSOURCE +static bool +numberFormat_toSource(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().NumberFormat); + return true; +} +#endif + +static const JSFunctionSpec numberFormat_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_NumberFormat_supportedLocalesOf", 1, 0), + JS_FS_END +}; + +static const JSFunctionSpec numberFormat_methods[] = { + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_NumberFormat_resolvedOptions", 0, 0), + JS_SELF_HOSTED_FN("formatToParts", "Intl_NumberFormat_formatToParts", 1, 0), +#if JS_HAS_TOSOURCE + JS_FN(js_toSource_str, numberFormat_toSource, 0, 0), +#endif + JS_FS_END +}; + +/** + * 11.2.1 Intl.NumberFormat([ locales [, options]]) + * + * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b + */ +static bool +NumberFormat(JSContext* cx, const CallArgs& args, bool construct) +{ + // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code). + + // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) + return false; + + if (!proto) { + proto = GlobalObject::getOrCreateNumberFormatPrototype(cx, cx->global()); + if (!proto) + return false; + } + + Rooted numberFormat(cx); + numberFormat = NewObjectWithGivenProto(cx, proto); + if (!numberFormat) + return false; + + numberFormat->setReservedSlot(NumberFormatObject::INTERNALS_SLOT, NullValue()); + numberFormat->setReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT, PrivateValue(nullptr)); + + RootedValue thisValue(cx, construct ? ObjectValue(*numberFormat) : args.thisv()); + RootedValue locales(cx, args.get(0)); + RootedValue options(cx, args.get(1)); + + // Step 3. + return intl::LegacyIntlInitialize(cx, numberFormat, cx->names().InitializeNumberFormat, thisValue, + locales, options, DateTimeFormatOptions::Standard, args.rval()); +} + +static bool +NumberFormat(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return NumberFormat(cx, args, args.isConstructing()); +} + +bool +js::intl_NumberFormat(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(!args.isConstructing()); + // intl_NumberFormat is an intrinsic for self-hosted JavaScript, so it + // cannot be used with "new", but it still has to be treated as a + // constructor. + return NumberFormat(cx, args, true); +} + +void +js::NumberFormatObject::finalize(FreeOp* fop, JSObject* obj) +{ + MOZ_ASSERT(fop->onActiveCooperatingThread()); + + const Value& slot = obj->as().getReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT); + if (UNumberFormat* nf = static_cast(slot.toPrivate())) + unum_close(nf); +} + +JSObject* +js::CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handle global, + MutableHandleObject constructor) +{ + RootedFunction ctor(cx); + ctor = GlobalObject::createConstructor(cx, &NumberFormat, cx->names().NumberFormat, 0); + if (!ctor) + return nullptr; + + RootedObject proto(cx, GlobalObject::createBlankPrototype(cx, global)); + if (!proto) + return nullptr; + + if (!LinkConstructorAndPrototype(cx, ctor, proto)) + return nullptr; + + // 11.2.2 + if (!JS_DefineFunctions(cx, ctor, numberFormat_static_methods)) + return nullptr; + + // 11.3.2 and 11.3.3 + if (!JS_DefineFunctions(cx, proto, numberFormat_methods)) + return nullptr; + + /* + * Install the getter for NumberFormat.prototype.format, which returns a + * bound formatting function for the specified NumberFormat object (suitable + * for passing to methods like Array.prototype.map). + */ + RootedValue getter(cx); + if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().NumberFormatFormatGet, + &getter)) + { + return nullptr; + } + if (!DefineProperty(cx, proto, cx->names().format, UndefinedHandleValue, + JS_DATA_TO_FUNC_PTR(JSGetterOp, &getter.toObject()), + nullptr, JSPROP_GETTER | JSPROP_SHARED)) + { + return nullptr; + } + + // 8.1 + RootedValue ctorValue(cx, ObjectValue(*ctor)); + if (!DefineProperty(cx, Intl, cx->names().NumberFormat, ctorValue, nullptr, nullptr, 0)) + return nullptr; + + constructor.set(ctor); + return proto; +} + +bool +js::intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + JSAutoByteString locale(cx, args[0].toString()); + if (!locale) + return false; + + UErrorCode status = U_ZERO_ERROR; + UNumberingSystem* numbers = unumsys_open(IcuLocale(locale.ptr()), &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + ScopedICUObject toClose(numbers); + + const char* name = unumsys_getName(numbers); + RootedString jsname(cx, JS_NewStringCopyZ(cx, name)); + if (!jsname) + return false; + + args.rval().setString(jsname); + return true; +} + +/** + * Returns a new UNumberFormat with the locale and number formatting options + * of the given NumberFormat. + */ +static UNumberFormat* +NewUNumberFormat(JSContext* cx, Handle numberFormat) +{ + RootedValue value(cx); + + RootedObject internals(cx, intl::GetInternalsObject(cx, numberFormat)); + if (!internals) + return nullptr; + + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) + return nullptr; + + // ICU expects numberingSystem as a Unicode locale extensions on locale. + + intl::LanguageTag tag(cx); + { + JSLinearString* locale = value.toString()->ensureLinear(cx); + if (!locale) + return nullptr; + + if (!intl::LanguageTagParser::parse(cx, locale, tag)) + return nullptr; + } + + JS::RootedVector keywords(cx); + + if (!GetProperty(cx, internals, internals, cx->names().numberingSystem, &value)) + return nullptr; + + { + JSLinearString* numberingSystem = value.toString()->ensureLinear(cx); + if (!numberingSystem) + return nullptr; + + if (!keywords.emplaceBack("nu", numberingSystem)) + return nullptr; + } + + // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of + // the Unicode extension subtag. We're then relying on ICU to follow RFC + // 6067, which states that any trailing keywords using the same key + // should be ignored. + if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) + return nullptr; + + UniqueChars locale = tag.toStringZ(cx); + if (!locale) + return nullptr; + + // UNumberFormat options with default values + UNumberFormatStyle uStyle = UNUM_DECIMAL; + const UChar* uCurrency = nullptr; + uint32_t uMinimumIntegerDigits = 1; + uint32_t uMinimumFractionDigits = 0; + uint32_t uMaximumFractionDigits = 3; + int32_t uMinimumSignificantDigits = -1; + int32_t uMaximumSignificantDigits = -1; + bool uUseGrouping = true; + + // Sprinkle appropriate rooting flavor over things the GC might care about. + RootedString currency(cx); + AutoStableStringChars stableChars(cx); + + if (!GetProperty(cx, internals, internals, cx->names().style, &value)) + return nullptr; + JSAutoByteString style(cx, value.toString()); + if (!style) + return nullptr; + + if (StringsAreEqual(style, "currency")) { + if (!GetProperty(cx, internals, internals, cx->names().currency, &value)) + return nullptr; + currency = value.toString(); + MOZ_ASSERT(currency->length() == 3, + "IsWellFormedCurrencyCode permits only length-3 strings"); + if (!currency->ensureFlat(cx) || !stableChars.initTwoByte(cx, currency)) + return nullptr; + // uCurrency remains owned by stableChars. + uCurrency = Char16ToUChar(stableChars.twoByteRange().begin().get()); + if (!uCurrency) + return nullptr; + + if (!GetProperty(cx, internals, internals, cx->names().currencyDisplay, &value)) + return nullptr; + JSAutoByteString currencyDisplay(cx, value.toString()); + if (!currencyDisplay) + return nullptr; + if (StringsAreEqual(currencyDisplay, "code")) { + uStyle = UNUM_CURRENCY_ISO; + } else if (StringsAreEqual(currencyDisplay, "symbol")) { + uStyle = UNUM_CURRENCY; + } else { + MOZ_ASSERT(StringsAreEqual(currencyDisplay, "name")); + uStyle = UNUM_CURRENCY_PLURAL; + } + } else if (StringsAreEqual(style, "percent")) { + uStyle = UNUM_PERCENT; + } else { + MOZ_ASSERT(StringsAreEqual(style, "decimal")); + uStyle = UNUM_DECIMAL; + } + + RootedId id(cx, NameToId(cx->names().minimumSignificantDigits)); + bool hasP; + if (!HasProperty(cx, internals, id, &hasP)) + return nullptr; + if (hasP) { + if (!GetProperty(cx, internals, internals, cx->names().minimumSignificantDigits, + &value)) + return nullptr; + uMinimumSignificantDigits = value.toInt32(); + if (!GetProperty(cx, internals, internals, cx->names().maximumSignificantDigits, + &value)) + return nullptr; + uMaximumSignificantDigits = value.toInt32(); + } else { + if (!GetProperty(cx, internals, internals, cx->names().minimumFractionDigits, + &value)) + return nullptr; + uMinimumFractionDigits = AssertedCast(value.toInt32()); + if (!GetProperty(cx, internals, internals, cx->names().maximumFractionDigits, + &value)) + return nullptr; + uMaximumFractionDigits = AssertedCast(value.toInt32()); + } + + if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits, + &value)) + return nullptr; + uMinimumIntegerDigits = AssertedCast(value.toInt32()); + + if (!GetProperty(cx, internals, internals, cx->names().useGrouping, &value)) + return nullptr; + uUseGrouping = value.toBoolean(); + + UErrorCode status = U_ZERO_ERROR; + UNumberFormat* nf = unum_open(uStyle, nullptr, 0, IcuLocale(locale.get()), nullptr, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return nullptr; + } + ScopedICUObject toClose(nf); + + if (uCurrency) { + unum_setTextAttribute(nf, UNUM_CURRENCY_CODE, uCurrency, 3, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return nullptr; + } + } + if (uMinimumSignificantDigits != -1) { + unum_setAttribute(nf, UNUM_SIGNIFICANT_DIGITS_USED, true); + unum_setAttribute(nf, UNUM_MIN_SIGNIFICANT_DIGITS, uMinimumSignificantDigits); + unum_setAttribute(nf, UNUM_MAX_SIGNIFICANT_DIGITS, uMaximumSignificantDigits); + } else { + unum_setAttribute(nf, UNUM_MIN_INTEGER_DIGITS, uMinimumIntegerDigits); + unum_setAttribute(nf, UNUM_MIN_FRACTION_DIGITS, uMinimumFractionDigits); + unum_setAttribute(nf, UNUM_MAX_FRACTION_DIGITS, uMaximumFractionDigits); + } + unum_setAttribute(nf, UNUM_GROUPING_USED, uUseGrouping); + unum_setAttribute(nf, UNUM_ROUNDING_MODE, UNUM_ROUND_HALFUP); + + return toClose.forget(); +} + +static JSString* +PartitionNumberPattern(JSContext* cx, UNumberFormat* nf, HandleValue x, + UFieldPositionIterator* fpositer) +{ + if (x.isNumber()) { + double num = x.toNumber(); + + // PartitionNumberPattern doesn't consider -0.0 to be negative. + if (IsNegativeZero(num)) + num = 0.0; + + return CallICU(cx, [nf, num, fpositer](UChar* chars, int32_t size, UErrorCode* status) { + return unum_formatDoubleForFields(nf, num, chars, size, fpositer, status); + }); + } else if(x.isBigInt()) { + RootedBigInt bi(cx, x.toBigInt()); + int64_t num; + + if (BigInt::isInt64(bi, &num)) { + return CallICU(cx, [nf, num](UChar* chars, int32_t size, UErrorCode* status) { + return unum_formatInt64(nf, num, chars, size, nullptr, status); + }); + } else { + JSLinearString* str = BigInt::toString(cx, bi, 10); + if (!str) { + return nullptr; + } + MOZ_ASSERT(str->hasLatin1Chars()); + + JS::AutoCheckCannotGC noGC(cx); + const char* latinchars = reinterpret_cast(str->latin1Chars(noGC)); + size_t length = str->length(); + return CallICU(cx, [nf, latinchars, length](UChar* chars, int32_t size, UErrorCode* status) { + return unum_formatDecimal(nf, latinchars, length, chars, size, nullptr, status); + }); + } + } + return nullptr; +} + +bool +js::FormatNumeric(JSContext* cx, UNumberFormat* nf, HandleValue x, MutableHandleValue result) +{ + // Passing null for |fpositer| will just not compute partition information, + // letting us common up all ICU number-formatting code. + JSString* str = PartitionNumberPattern(cx, nf, x, nullptr); + if (!str) + return false; + + result.setString(str); + return true; +} + +using FieldType = ImmutablePropertyNamePtr JSAtomState::*; + +static FieldType +GetFieldTypeForNumberField(UNumberFormatFields fieldName, HandleValue x) +{ + // See intl/icu/source/i18n/unicode/unum.h for a detailed field list. This + // list is deliberately exhaustive: cases might have to be added/removed if + // this code is compiled with a different ICU with more UNumberFormatFields + // enum initializers. Please guard such cases with appropriate ICU + // version-testing #ifdefs, should cross-version divergence occur. + switch (fieldName) { + case UNUM_INTEGER_FIELD: + if (x.isNumber()) { + double d = x.toNumber(); + if (IsNaN(d)) { + return &JSAtomState::nan; + } + if (!IsFinite(d)) { + return &JSAtomState::infinity; + } + } + return &JSAtomState::integer; + + case UNUM_GROUPING_SEPARATOR_FIELD: + return &JSAtomState::group; + + case UNUM_DECIMAL_SEPARATOR_FIELD: + return &JSAtomState::decimal; + + case UNUM_FRACTION_FIELD: + return &JSAtomState::fraction; + + case UNUM_SIGN_FIELD: { + // Manual trawling through the ICU call graph appears to indicate that + // the basic formatting we request will never include a positive sign. + // But this analysis may be mistaken, so don't absolutely trust it. + MOZ_ASSERT(!x.isNumber() || !IsNaN(x.toNumber()), + "ICU appearing not to produce positive-sign among fields, " + "plus our coercing all NaNs to one with sign bit unset " + "(i.e. \"positive\"), means we shouldn't reach here with a " + "NaN value"); + bool isNegative = + x.isNumber() ? IsNegative(x.toNumber()) : x.toBigInt()->isNegative(); + return isNegative ? &JSAtomState::minusSign : &JSAtomState::plusSign; + } + + case UNUM_PERCENT_FIELD: + return &JSAtomState::percentSign; + + case UNUM_CURRENCY_FIELD: + return &JSAtomState::currency; + + case UNUM_PERMILL_FIELD: + MOZ_ASSERT_UNREACHABLE("unexpected permill field found, even though " + "we don't use any user-defined patterns that " + "would require a permill field"); + break; + + case UNUM_EXPONENT_SYMBOL_FIELD: + case UNUM_EXPONENT_SIGN_FIELD: + case UNUM_EXPONENT_FIELD: + MOZ_ASSERT_UNREACHABLE("exponent field unexpectedly found in " + "formatted number, even though UNUM_SCIENTIFIC " + "and scientific notation were never requested"); + break; + + case UNUM_FIELD_COUNT: + MOZ_ASSERT_UNREACHABLE("format field sentinel value returned by " + "iterator!"); + break; + } + + MOZ_ASSERT_UNREACHABLE("unenumerated, undocumented format field returned " + "by iterator"); + return nullptr; +} + +static bool +FormatNumericToParts(JSContext* cx, UNumberFormat* nf, HandleValue x, MutableHandleValue result) +{ + UErrorCode status = U_ZERO_ERROR; + + UFieldPositionIterator* fpositer = ufieldpositer_open(&status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + MOZ_ASSERT(fpositer); + ScopedICUObject toClose(fpositer); + + RootedString overallResult(cx, PartitionNumberPattern(cx, nf, x, fpositer)); + if (!overallResult) + return false; + + RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx)); + if (!partsArray) + return false; + + // First, vacuum up fields in the overall formatted string. + + struct Field + { + uint32_t begin; + uint32_t end; + FieldType type; + + // Needed for vector-resizing scratch space. + Field() = default; + + Field(uint32_t begin, uint32_t end, FieldType type) + : begin(begin), end(end), type(type) + {} + }; + + using FieldsVector = Vector; + FieldsVector fields(cx); + + int32_t fieldInt, beginIndexInt, endIndexInt; + while ((fieldInt = ufieldpositer_next(fpositer, &beginIndexInt, &endIndexInt)) >= 0) { + MOZ_ASSERT(beginIndexInt >= 0); + MOZ_ASSERT(endIndexInt >= 0); + MOZ_ASSERT(beginIndexInt < endIndexInt, + "erm, aren't fields always non-empty?"); + + FieldType type = GetFieldTypeForNumberField(UNumberFormatFields(fieldInt), x); + if (!fields.emplaceBack(uint32_t(beginIndexInt), uint32_t(endIndexInt), type)) + return false; + } + + // Second, merge sort the fields vector. Expand the vector to have scratch + // space for performing the sort. + size_t fieldsLen = fields.length(); + if (!fields.resizeUninitialized(fieldsLen * 2)) + return false; + + MOZ_ALWAYS_TRUE(MergeSort(fields.begin(), fieldsLen, fields.begin() + fieldsLen, + [](const Field& left, const Field& right, + bool* lessOrEqual) + { + // Sort first by begin index, then to place + // enclosing fields before nested fields. + *lessOrEqual = left.begin < right.begin || + (left.begin == right.begin && + left.end > right.end); + return true; + })); + + // Deallocate the scratch space. + if (!fields.resize(fieldsLen)) + return false; + + // Third, iterate over the sorted field list to generate a sequence of + // parts (what ECMA-402 actually exposes). A part is a maximal character + // sequence entirely within no field or a single most-nested field. + // + // Diagrams may be helpful to illustrate how fields map to parts. Consider + // formatting -28,114,774,228,750.32, the US national surplus (negative + // because it's actually a debt) on March 31, 2021. + // + // var options = + // { style: "currency", currency: "USD", currencyDisplay: "name" }; + // var usdFormatter = new Intl.NumberFormat("en-US", options); + // usdFormatter.format(-28114774228750.32); + // + // The formatted result is "-28,114,774,228,750.32 US dollars". ICU + // identifies these fields in the string: + // + // UNUM_GROUPING_SEPARATOR_FIELD + // | + // UNUM_SIGN_FIELD | UNUM_DECIMAL_SEPARATOR_FIELD + // | __________/| | + // | / | | | | + // "-28,114,774,228,750.32 US dollars" + // \________________/ |/ \_______/ + // | | | + // UNUM_INTEGER_FIELD | UNUM_CURRENCY_FIELD + // | + // UNUM_FRACTION_FIELD + // + // These fields map to parts as follows: + // + // integer decimal + // _____|________ | + // / /| |\ |\ |\ | literal + // /| / | | \ | \ | \| | + // "-28,114,774,228,750.32 US dollars" + // | \___|___|___/ |/ \________/ + // | | | | + // | group | currency + // | | + // minusSign fraction + // + // The sign is a part. Each comma is a part, splitting the integer field + // into parts for trillions/billions/&c. digits. The decimal point is a + // part. Cents are a part. The space between cents and currency is a part + // (outside any field). Last, the currency field is a part. + // + // Because parts fully partition the formatted string, we only track the + // end of each part -- the beginning is implicitly the last part's end. + struct Part + { + uint32_t end; + FieldType type; + }; + + class PartGenerator + { + // The fields in order from start to end, then least to most nested. + const FieldsVector& fields; + + // Index of the current field, in |fields|, being considered to + // determine part boundaries. |lastEnd <= fields[index].begin| is an + // invariant. + size_t index; + + // The end index of the last part produced, always less than or equal + // to |limit|, strictly increasing. + uint32_t lastEnd; + + // The length of the overall formatted string. + const uint32_t limit; + + Vector enclosingFields; + + void popEnclosingFieldsEndingAt(uint32_t end) { + MOZ_ASSERT_IF(enclosingFields.length() > 0, + fields[enclosingFields.back()].end >= end); + + while (enclosingFields.length() > 0 && fields[enclosingFields.back()].end == end) + enclosingFields.popBack(); + } + + bool nextPartInternal(Part* part) { + size_t len = fields.length(); + MOZ_ASSERT(index <= len); + + // If we're out of fields, all that remains are part(s) consisting + // of trailing portions of enclosing fields, and maybe a final + // literal part. + if (index == len) { + if (enclosingFields.length() > 0) { + const auto& enclosing = fields[enclosingFields.popCopy()]; + part->end = enclosing.end; + part->type = enclosing.type; + + // If additional enclosing fields end where this part ends, + // pop them as well. + popEnclosingFieldsEndingAt(part->end); + } else { + part->end = limit; + part->type = &JSAtomState::literal; + } + + return true; + } + + // Otherwise we still have a field to process. + const Field* current = &fields[index]; + MOZ_ASSERT(lastEnd <= current->begin); + MOZ_ASSERT(current->begin < current->end); + + // But first, deal with inter-field space. + if (lastEnd < current->begin) { + if (enclosingFields.length() > 0) { + // Space between fields, within an enclosing field, is part + // of that enclosing field, until the start of the current + // field or the end of the enclosing field, whichever is + // earlier. + const auto& enclosing = fields[enclosingFields.back()]; + part->end = std::min(enclosing.end, current->begin); + part->type = enclosing.type; + popEnclosingFieldsEndingAt(part->end); + } else { + // If there's no enclosing field, the space is a literal. + part->end = current->begin; + part->type = &JSAtomState::literal; + } + + return true; + } + + // Otherwise, the part spans a prefix of the current field. Find + // the most-nested field containing that prefix. + const Field* next; + do { + current = &fields[index]; + + // If the current field is last, the part extends to its end. + if (++index == len) { + part->end = current->end; + part->type = current->type; + return true; + } + + next = &fields[index]; + MOZ_ASSERT(current->begin <= next->begin); + MOZ_ASSERT(current->begin < next->end); + + // If the next field nests within the current field, push an + // enclosing field. (If there are no nested fields, don't + // bother pushing a field that'd be immediately popped.) + if (current->end > next->begin) { + if (!enclosingFields.append(index - 1)) + return false; + } + + // Do so until the next field begins after this one. + } while (current->begin == next->begin); + + part->type = current->type; + + if (current->end <= next->begin) { + // The next field begins after the current field ends. Therefore + // the current part ends at the end of the current field. + part->end = current->end; + popEnclosingFieldsEndingAt(part->end); + } else { + // The current field encloses the next one. The current part + // ends where the next field/part will start. + part->end = next->begin; + } + + return true; + } + + public: + PartGenerator(JSContext* cx, const FieldsVector& vec, uint32_t limit) + : fields(vec), index(0), lastEnd(0), limit(limit), enclosingFields(cx) + {} + + bool nextPart(bool* hasPart, Part* part) { + // There are no parts left if we've partitioned the entire string. + if (lastEnd == limit) { + MOZ_ASSERT(enclosingFields.length() == 0); + *hasPart = false; + return true; + } + + if (!nextPartInternal(part)) + return false; + + *hasPart = true; + lastEnd = part->end; + return true; + } + }; + + // Finally, generate the result array. + size_t lastEndIndex = 0; + uint32_t partIndex = 0; + RootedObject singlePart(cx); + RootedValue propVal(cx); + + PartGenerator gen(cx, fields, overallResult->length()); + do { + bool hasPart; + Part part; + if (!gen.nextPart(&hasPart, &part)) + return false; + + if (!hasPart) + break; + + FieldType type = part.type; + size_t endIndex = part.end; + + MOZ_ASSERT(lastEndIndex < endIndex); + + singlePart = NewBuiltinClassInstance(cx); + if (!singlePart) + return false; + + propVal.setString(cx->names().*type); + if (!DefineProperty(cx, singlePart, cx->names().type, propVal)) + return false; + + JSLinearString* partSubstr = + NewDependentString(cx, overallResult, lastEndIndex, endIndex - lastEndIndex); + if (!partSubstr) + return false; + + propVal.setString(partSubstr); + if (!DefineProperty(cx, singlePart, cx->names().value, propVal)) + return false; + + propVal.setObject(*singlePart); + if (!DefineElement(cx, partsArray, partIndex, propVal)) + return false; + + lastEndIndex = endIndex; + partIndex++; + } while (true); + + MOZ_ASSERT(lastEndIndex == overallResult->length(), + "result array must partition the entire string"); + + result.setObject(*partsArray); + return true; +} + +bool +js::intl_FormatNumber(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + MOZ_ASSERT(args[0].isObject()); + MOZ_ASSERT(args[1].isNumeric()); + MOZ_ASSERT(args[2].isBoolean()); + + Rooted numberFormat(cx, &args[0].toObject().as()); + + // Obtain a cached UNumberFormat object. + void* priv = + numberFormat->getReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT).toPrivate(); + UNumberFormat* nf = static_cast(priv); + if (!nf) { + nf = NewUNumberFormat(cx, numberFormat); + if (!nf) + return false; + numberFormat->setReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT, PrivateValue(nf)); + } + + // Use the UNumberFormat to actually format the number. + if (args[2].toBoolean()) { + return FormatNumericToParts(cx, nf, args.get(1), args.rval()); + } + return FormatNumeric(cx, nf, args.get(1), args.rval()); +} diff --git a/js/src/builtin/intl/RelativeTimeFormat.cpp b/js/src/builtin/intl/RelativeTimeFormat.cpp index 277baae4c4..14929de63d 100644 --- a/js/src/builtin/intl/RelativeTimeFormat.cpp +++ b/js/src/builtin/intl/RelativeTimeFormat.cpp @@ -1,287 +1,287 @@ -/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/* Implementation of the Intl.RelativeTimeFormat proposal. */ - -#include "builtin/intl/RelativeTimeFormat.h" - -#include "mozilla/Assertions.h" -#include "mozilla/Casting.h" - -#include "jscntxt.h" - -#include "builtin/intl/CommonFunctions.h" -#include "builtin/intl/ICUHeader.h" -#include "builtin/intl/ScopedICUObject.h" -#include "vm/GlobalObject.h" - -#include "vm/NativeObject-inl.h" - -using namespace js; - -using mozilla::IsNegativeZero; -using mozilla::Range; -using mozilla::RangedPtr; - -using js::intl::CallICU; -using js::intl::IcuLocale; -using js::intl::INITIAL_CHAR_BUFFER_SIZE; -using js::intl::StringsAreEqual; - -/**************** RelativeTimeFormat *****************/ - -const ClassOps RelativeTimeFormatObject::classOps_ = { - nullptr, /* addProperty */ - nullptr, /* delProperty */ - nullptr, /* getProperty */ - nullptr, /* enumerate */ - nullptr, /* newEnumerate */ - nullptr, /* resolve */ - nullptr, /* mayResolve */ - RelativeTimeFormatObject::finalize -}; - -const Class RelativeTimeFormatObject::class_ = { - js_Object_str, - JSCLASS_HAS_RESERVED_SLOTS(RelativeTimeFormatObject::SLOT_COUNT) | - JSCLASS_FOREGROUND_FINALIZE, - &RelativeTimeFormatObject::classOps_ -}; - -#if JS_HAS_TOSOURCE -static bool -relativeTimeFormat_toSource(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - args.rval().setString(cx->names().RelativeTimeFormat); - return true; -} -#endif - -static const JSFunctionSpec relativeTimeFormat_static_methods[] = { - JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_RelativeTimeFormat_supportedLocalesOf", 1, 0), - JS_FS_END -}; - -static const JSFunctionSpec relativeTimeFormat_methods[] = { - JS_SELF_HOSTED_FN("resolvedOptions", "Intl_RelativeTimeFormat_resolvedOptions", 0, 0), - JS_SELF_HOSTED_FN("format", "Intl_RelativeTimeFormat_format", 2, 0), -#if JS_HAS_TOSOURCE - JS_FN(js_toSource_str, relativeTimeFormat_toSource, 0, 0), -#endif - JS_FS_END -}; - -static const JSPropertySpec relativeTimeFormat_properties[] = { - JS_STRING_SYM_PS(toStringTag, "Intl.RelativeTimeFormat", JSPROP_READONLY), - JS_PS_END}; - -/** - * RelativeTimeFormat constructor. - * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.1 - */ -static bool -RelativeTimeFormat(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - - // Step 1. - if (!ThrowIfNotConstructing(cx, args, "Intl.RelativeTimeFormat")) - return false; - - // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). - RootedObject proto(cx); - if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto)) - return false; - - if (!proto) { - proto = GlobalObject::getOrCreateRelativeTimeFormatPrototype(cx, cx->global()); - if (!proto) - return false; - } - - RootedObject relativeTimeFormat(cx); - relativeTimeFormat = NewObjectWithGivenProto(cx, proto); - if (!relativeTimeFormat) - return false; - - relativeTimeFormat->as().setReservedSlot(RelativeTimeFormatObject::INTERNALS_SLOT, NullValue()); - relativeTimeFormat->as().setReservedSlot(RelativeTimeFormatObject::URELATIVE_TIME_FORMAT_SLOT, PrivateValue(nullptr)); - - RootedValue locales(cx, args.get(0)); - RootedValue options(cx, args.get(1)); - - // Step 3. - if (!intl::InitializeObject(cx, relativeTimeFormat, cx->names().InitializeRelativeTimeFormat, locales, options)) - return false; - - args.rval().setObject(*relativeTimeFormat); - return true; -} - -void -js::RelativeTimeFormatObject::finalize(FreeOp* fop, JSObject* obj) -{ - MOZ_ASSERT(fop->onMainThread()); - - const Value& slot = obj->as().getReservedSlot(RelativeTimeFormatObject::URELATIVE_TIME_FORMAT_SLOT); - if (URelativeDateTimeFormatter* rtf = static_cast(slot.toPrivate())) - ureldatefmt_close(rtf); -} - -JSObject* -js::CreateRelativeTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handle global) -{ - RootedFunction ctor(cx); - ctor = global->createConstructor(cx, &RelativeTimeFormat, cx->names().RelativeTimeFormat, 0); - if (!ctor) - return nullptr; - - RootedObject proto(cx, GlobalObject::createBlankPrototype(cx, global)); - if (!proto) - return nullptr; - - if (!LinkConstructorAndPrototype(cx, ctor, proto)) - return nullptr; - - if (!JS_DefineFunctions(cx, ctor, relativeTimeFormat_static_methods)) - return nullptr; - - if (!JS_DefineFunctions(cx, proto, relativeTimeFormat_methods)) - return nullptr; - - if (!JS_DefineProperties(cx, proto, relativeTimeFormat_properties)) - return nullptr; - - RootedValue ctorValue(cx, ObjectValue(*ctor)); - if (!DefineProperty(cx, Intl, cx->names().RelativeTimeFormat, ctorValue, nullptr, nullptr, 0)) { - return nullptr; - } - - return proto; -} - -enum class RelativeTimeNumeric -{ - /** - * Only strings with numeric components like `1 day ago`. - */ - Always, - /** - * Natural-language strings like `yesterday` when possible, - * otherwise strings with numeric components as in `7 months ago`. - */ - Auto, -}; - -bool -js::intl_FormatRelativeTime(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 4); - - RootedObject relativeTimeFormat(cx, &args[0].toObject()); - - RootedObject internals(cx, intl::GetInternalsObject(cx, relativeTimeFormat)); - if (!internals) - return false; - - RootedValue value(cx); - - if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) - return false; - JSAutoByteString locale(cx, value.toString()); - if (!locale) - return false; - - if (!GetProperty(cx, internals, internals, cx->names().style, &value)) - return false; - RootedLinearString style(cx, value.toString()->ensureLinear(cx)); - if (!style) - return false; - - double t = args[1].toNumber(); - - UDateRelativeDateTimeFormatterStyle relDateTimeStyle; - - if (StringEqualsAscii(style, "short")) { - relDateTimeStyle = UDAT_STYLE_SHORT; - } else if (StringEqualsAscii(style, "narrow")) { - relDateTimeStyle = UDAT_STYLE_NARROW; - } else { - MOZ_ASSERT(StringEqualsAscii(style, "long")); - relDateTimeStyle = UDAT_STYLE_LONG; - } - - URelativeDateTimeUnit relDateTimeUnit; - { - JSLinearString* unit = args[2].toString()->ensureLinear(cx); - if (!unit) { - return false; - } - - if (StringEqualsAscii(unit, "second") || StringEqualsAscii(unit, "seconds")) { - relDateTimeUnit = UDAT_REL_UNIT_SECOND; - } else if (StringEqualsAscii(unit, "minute") || StringEqualsAscii(unit, "minutes")) { - relDateTimeUnit = UDAT_REL_UNIT_MINUTE; - } else if (StringEqualsAscii(unit, "hour") || StringEqualsAscii(unit, "hours")) { - relDateTimeUnit = UDAT_REL_UNIT_HOUR; - } else if (StringEqualsAscii(unit, "day") || StringEqualsAscii(unit, "days")) { - relDateTimeUnit = UDAT_REL_UNIT_DAY; - } else if (StringEqualsAscii(unit, "week") || StringEqualsAscii(unit, "weeks")) { - relDateTimeUnit = UDAT_REL_UNIT_WEEK; - } else if (StringEqualsAscii(unit, "month") || StringEqualsAscii(unit, "months")) { - relDateTimeUnit = UDAT_REL_UNIT_MONTH; - } else if (StringEqualsAscii(unit, "quarter") || StringEqualsAscii(unit, "quarters")) { - relDateTimeUnit = UDAT_REL_UNIT_QUARTER; - } else { - MOZ_ASSERT(StringEqualsAscii(unit, "year") || StringEqualsAscii(unit, "years")); - relDateTimeUnit = UDAT_REL_UNIT_YEAR; - } - } - - if (!GetProperty(cx, internals, internals, cx->names().numeric, &value)) - return false; - RootedLinearString numeric(cx, value.toString()->ensureLinear(cx)); - if (!numeric) - return false; - - RelativeTimeNumeric relDateTimeNumeric; - - if (StringEqualsAscii(numeric, "auto")) { - relDateTimeNumeric = RelativeTimeNumeric::Auto; - } else { - MOZ_ASSERT(StringEqualsAscii(numeric, "always")); - relDateTimeNumeric = RelativeTimeNumeric::Always; - } - - Vector chars(cx); - if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) - return false; - UErrorCode status = U_ZERO_ERROR; - URelativeDateTimeFormatter* rtf = - ureldatefmt_open(IcuLocale(locale.ptr()), nullptr, relDateTimeStyle, - UDISPCTX_CAPITALIZATION_FOR_STANDALONE, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - ScopedICUObject closeRelativeTimeFormat(rtf); - - JSString* str = - CallICU(cx, [rtf, t, relDateTimeUnit, relDateTimeNumeric](UChar* chars, int32_t size, - UErrorCode* status) - { - auto fmt = relDateTimeNumeric == RelativeTimeNumeric::Auto - ? ureldatefmt_format - : ureldatefmt_formatNumeric; - return fmt(rtf, t, relDateTimeUnit, chars, size, status); - }); - if (!str) - return false; - - args.rval().setString(str); - return true; -} +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Implementation of the Intl.RelativeTimeFormat proposal. */ + +#include "builtin/intl/RelativeTimeFormat.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Casting.h" + +#include "jscntxt.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/ICUHeader.h" +#include "builtin/intl/ScopedICUObject.h" +#include "vm/GlobalObject.h" + +#include "vm/NativeObject-inl.h" + +using namespace js; + +using mozilla::IsNegativeZero; +using mozilla::Range; +using mozilla::RangedPtr; + +using js::intl::CallICU; +using js::intl::IcuLocale; +using js::intl::INITIAL_CHAR_BUFFER_SIZE; +using js::intl::StringsAreEqual; + +/**************** RelativeTimeFormat *****************/ + +const ClassOps RelativeTimeFormatObject::classOps_ = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* enumerate */ + nullptr, /* newEnumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + RelativeTimeFormatObject::finalize +}; + +const Class RelativeTimeFormatObject::class_ = { + js_Object_str, + JSCLASS_HAS_RESERVED_SLOTS(RelativeTimeFormatObject::SLOT_COUNT) | + JSCLASS_FOREGROUND_FINALIZE, + &RelativeTimeFormatObject::classOps_ +}; + +#if JS_HAS_TOSOURCE +static bool +relativeTimeFormat_toSource(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().RelativeTimeFormat); + return true; +} +#endif + +static const JSFunctionSpec relativeTimeFormat_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_RelativeTimeFormat_supportedLocalesOf", 1, 0), + JS_FS_END +}; + +static const JSFunctionSpec relativeTimeFormat_methods[] = { + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_RelativeTimeFormat_resolvedOptions", 0, 0), + JS_SELF_HOSTED_FN("format", "Intl_RelativeTimeFormat_format", 2, 0), +#if JS_HAS_TOSOURCE + JS_FN(js_toSource_str, relativeTimeFormat_toSource, 0, 0), +#endif + JS_FS_END +}; + +static const JSPropertySpec relativeTimeFormat_properties[] = { + JS_STRING_SYM_PS(toStringTag, "Intl.RelativeTimeFormat", JSPROP_READONLY), + JS_PS_END}; + +/** + * RelativeTimeFormat constructor. + * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.1 + */ +static bool +RelativeTimeFormat(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!ThrowIfNotConstructing(cx, args, "Intl.RelativeTimeFormat")) + return false; + + // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) + return false; + + if (!proto) { + proto = GlobalObject::getOrCreateRelativeTimeFormatPrototype(cx, cx->global()); + if (!proto) + return false; + } + + RootedObject relativeTimeFormat(cx); + relativeTimeFormat = NewObjectWithGivenProto(cx, proto); + if (!relativeTimeFormat) + return false; + + relativeTimeFormat->as().setReservedSlot(RelativeTimeFormatObject::INTERNALS_SLOT, NullValue()); + relativeTimeFormat->as().setReservedSlot(RelativeTimeFormatObject::URELATIVE_TIME_FORMAT_SLOT, PrivateValue(nullptr)); + + RootedValue locales(cx, args.get(0)); + RootedValue options(cx, args.get(1)); + + // Step 3. + if (!intl::InitializeObject(cx, relativeTimeFormat, cx->names().InitializeRelativeTimeFormat, locales, options)) + return false; + + args.rval().setObject(*relativeTimeFormat); + return true; +} + +void +js::RelativeTimeFormatObject::finalize(FreeOp* fop, JSObject* obj) +{ + MOZ_ASSERT(fop->onActiveCooperatingThread()); + + const Value& slot = obj->as().getReservedSlot(RelativeTimeFormatObject::URELATIVE_TIME_FORMAT_SLOT); + if (URelativeDateTimeFormatter* rtf = static_cast(slot.toPrivate())) + ureldatefmt_close(rtf); +} + +JSObject* +js::CreateRelativeTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handle global) +{ + RootedFunction ctor(cx); + ctor = global->createConstructor(cx, &RelativeTimeFormat, cx->names().RelativeTimeFormat, 0); + if (!ctor) + return nullptr; + + RootedObject proto(cx, GlobalObject::createBlankPrototype(cx, global)); + if (!proto) + return nullptr; + + if (!LinkConstructorAndPrototype(cx, ctor, proto)) + return nullptr; + + if (!JS_DefineFunctions(cx, ctor, relativeTimeFormat_static_methods)) + return nullptr; + + if (!JS_DefineFunctions(cx, proto, relativeTimeFormat_methods)) + return nullptr; + + if (!JS_DefineProperties(cx, proto, relativeTimeFormat_properties)) + return nullptr; + + RootedValue ctorValue(cx, ObjectValue(*ctor)); + if (!DefineProperty(cx, Intl, cx->names().RelativeTimeFormat, ctorValue, nullptr, nullptr, 0)) { + return nullptr; + } + + return proto; +} + +enum class RelativeTimeNumeric +{ + /** + * Only strings with numeric components like `1 day ago`. + */ + Always, + /** + * Natural-language strings like `yesterday` when possible, + * otherwise strings with numeric components as in `7 months ago`. + */ + Auto, +}; + +bool +js::intl_FormatRelativeTime(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 4); + + RootedObject relativeTimeFormat(cx, &args[0].toObject()); + + RootedObject internals(cx, intl::GetInternalsObject(cx, relativeTimeFormat)); + if (!internals) + return false; + + RootedValue value(cx); + + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) + return false; + JSAutoByteString locale(cx, value.toString()); + if (!locale) + return false; + + if (!GetProperty(cx, internals, internals, cx->names().style, &value)) + return false; + RootedLinearString style(cx, value.toString()->ensureLinear(cx)); + if (!style) + return false; + + double t = args[1].toNumber(); + + UDateRelativeDateTimeFormatterStyle relDateTimeStyle; + + if (StringEqualsAscii(style, "short")) { + relDateTimeStyle = UDAT_STYLE_SHORT; + } else if (StringEqualsAscii(style, "narrow")) { + relDateTimeStyle = UDAT_STYLE_NARROW; + } else { + MOZ_ASSERT(StringEqualsAscii(style, "long")); + relDateTimeStyle = UDAT_STYLE_LONG; + } + + URelativeDateTimeUnit relDateTimeUnit; + { + JSLinearString* unit = args[2].toString()->ensureLinear(cx); + if (!unit) { + return false; + } + + if (StringEqualsAscii(unit, "second") || StringEqualsAscii(unit, "seconds")) { + relDateTimeUnit = UDAT_REL_UNIT_SECOND; + } else if (StringEqualsAscii(unit, "minute") || StringEqualsAscii(unit, "minutes")) { + relDateTimeUnit = UDAT_REL_UNIT_MINUTE; + } else if (StringEqualsAscii(unit, "hour") || StringEqualsAscii(unit, "hours")) { + relDateTimeUnit = UDAT_REL_UNIT_HOUR; + } else if (StringEqualsAscii(unit, "day") || StringEqualsAscii(unit, "days")) { + relDateTimeUnit = UDAT_REL_UNIT_DAY; + } else if (StringEqualsAscii(unit, "week") || StringEqualsAscii(unit, "weeks")) { + relDateTimeUnit = UDAT_REL_UNIT_WEEK; + } else if (StringEqualsAscii(unit, "month") || StringEqualsAscii(unit, "months")) { + relDateTimeUnit = UDAT_REL_UNIT_MONTH; + } else if (StringEqualsAscii(unit, "quarter") || StringEqualsAscii(unit, "quarters")) { + relDateTimeUnit = UDAT_REL_UNIT_QUARTER; + } else { + MOZ_ASSERT(StringEqualsAscii(unit, "year") || StringEqualsAscii(unit, "years")); + relDateTimeUnit = UDAT_REL_UNIT_YEAR; + } + } + + if (!GetProperty(cx, internals, internals, cx->names().numeric, &value)) + return false; + RootedLinearString numeric(cx, value.toString()->ensureLinear(cx)); + if (!numeric) + return false; + + RelativeTimeNumeric relDateTimeNumeric; + + if (StringEqualsAscii(numeric, "auto")) { + relDateTimeNumeric = RelativeTimeNumeric::Auto; + } else { + MOZ_ASSERT(StringEqualsAscii(numeric, "always")); + relDateTimeNumeric = RelativeTimeNumeric::Always; + } + + Vector chars(cx); + if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) + return false; + UErrorCode status = U_ZERO_ERROR; + URelativeDateTimeFormatter* rtf = + ureldatefmt_open(IcuLocale(locale.ptr()), nullptr, relDateTimeStyle, + UDISPCTX_CAPITALIZATION_FOR_STANDALONE, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject closeRelativeTimeFormat(rtf); + + JSString* str = + CallICU(cx, [rtf, t, relDateTimeUnit, relDateTimeNumeric](UChar* chars, int32_t size, + UErrorCode* status) + { + auto fmt = relDateTimeNumeric == RelativeTimeNumeric::Auto + ? ureldatefmt_format + : ureldatefmt_formatNumeric; + return fmt(rtf, t, relDateTimeUnit, chars, size, status); + }); + if (!str) + return false; + + args.rval().setString(str); + return true; +} diff --git a/js/src/jsarray.cpp b/js/src/jsarray.cpp index 15cb23a213..1b3f5385c3 100644 --- a/js/src/jsarray.cpp +++ b/js/src/jsarray.cpp @@ -3271,7 +3271,7 @@ ArrayConstructorImpl(JSContext* cx, CallArgs& args, bool isConstructor) RootedObject proto(cx); if (isConstructor) { - if (!GetPrototypeFromCallableConstructor(cx, args, &proto)) + if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) return false; } else { // We're emulating |new Array(n)| with |std_Array(n)| in self-hosted JS, diff --git a/js/src/jsbool.cpp b/js/src/jsbool.cpp index 0a70fe49f2..9c923c32d7 100644 --- a/js/src/jsbool.cpp +++ b/js/src/jsbool.cpp @@ -116,10 +116,8 @@ Boolean(JSContext* cx, unsigned argc, Value* vp) bool b = args.length() != 0 ? JS::ToBoolean(args[0]) : false; if (args.isConstructing()) { - RootedObject newTarget (cx, &args.newTarget().toObject()); RootedObject proto(cx); - - if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) + if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) return false; JSObject* obj = BooleanObject::create(cx, b, proto); diff --git a/js/src/jsdate.cpp b/js/src/jsdate.cpp index e29d6c6b24..30f04a57ce 100755 --- a/js/src/jsdate.cpp +++ b/js/src/jsdate.cpp @@ -3014,8 +3014,7 @@ NewDateObject(JSContext* cx, const CallArgs& args, ClippedTime t) MOZ_ASSERT(args.isConstructing()); RootedObject proto(cx); - RootedObject newTarget(cx, &args.newTarget().toObject()); - if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) + if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) return false; JSObject* obj = NewDateObjectMsec(cx, t, proto); diff --git a/js/src/jsfun.cpp b/js/src/jsfun.cpp index 4d85ce846a..b05cc619f3 100644 --- a/js/src/jsfun.cpp +++ b/js/src/jsfun.cpp @@ -1750,7 +1750,7 @@ FunctionConstructor(JSContext* cx, const CallArgs& args, GeneratorKind generator // Step 24. RootedObject proto(cx); if (!isAsync) { - if (!GetPrototypeFromCallableConstructor(cx, args, &proto)) + if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) return false; } diff --git a/js/src/jsnum.cpp b/js/src/jsnum.cpp index ee8a7137b7..79162a1d9f 100644 --- a/js/src/jsnum.cpp +++ b/js/src/jsnum.cpp @@ -520,19 +520,17 @@ Number(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); - /* Sample JS_CALLEE before clobbering. */ - bool isConstructing = args.isConstructing(); - if (args.length() > 0) { // BigInt proposal section 6.2, steps 2a-c. if (!ToNumeric(cx, args[0])) return false; + if (args[0].isBigInt()) args[0].setNumber(BigInt::numberValue(args[0].toBigInt())); MOZ_ASSERT(args[0].isNumber()); } - if (!isConstructing) { + if (!args.isConstructing()) { if (args.length() > 0) { args.rval().set(args[0]); } else { @@ -541,9 +539,8 @@ Number(JSContext* cx, unsigned argc, Value* vp) return true; } - RootedObject newTarget(cx, &args.newTarget().toObject()); RootedObject proto(cx); - if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) + if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) return false; double d = args.length() > 0 ? args[0].toNumber() : 0; diff --git a/js/src/jsobj.cpp b/js/src/jsobj.cpp index 2045364297..be0633ac9f 100644 --- a/js/src/jsobj.cpp +++ b/js/src/jsobj.cpp @@ -970,17 +970,6 @@ js::GetPrototypeFromConstructor(JSContext* cx, HandleObject newTarget, MutableHa return true; } -bool -js::GetPrototypeFromCallableConstructor(JSContext* cx, const CallArgs& args, MutableHandleObject proto) -{ - RootedObject newTarget(cx); - if (args.isConstructing()) - newTarget = &args.newTarget().toObject(); - else - newTarget = &args.callee(); - return GetPrototypeFromConstructor(cx, newTarget, proto); -} - JSObject* js::CreateThisForFunction(JSContext* cx, HandleObject callee, HandleObject newTarget, NewObjectKind newKind) diff --git a/js/src/jsobj.h b/js/src/jsobj.h index a53667d251..d41598635b 100644 --- a/js/src/jsobj.h +++ b/js/src/jsobj.h @@ -1125,8 +1125,21 @@ NewObjectWithTaggedProtoIsCachable(ExclusiveContext* cxArg, Handle extern bool GetPrototypeFromConstructor(JSContext* cx, js::HandleObject newTarget, js::MutableHandleObject proto); -extern bool -GetPrototypeFromCallableConstructor(JSContext* cx, const CallArgs& args, js::MutableHandleObject proto); +MOZ_ALWAYS_INLINE bool +GetPrototypeFromBuiltinConstructor(JSContext* cx, const CallArgs& args, js::MutableHandleObject proto) +{ + // When proto is set to nullptr, the caller is expected to select the + // correct default built-in prototype for this constructor. + if (!args.isConstructing() || &args.newTarget().toObject() == &args.callee()) { + proto.set(nullptr); + return true; + } + + // We're calling this constructor from a derived class, retrieve the + // actual prototype from newTarget. + RootedObject newTarget(cx, &args.newTarget().toObject()); + return GetPrototypeFromConstructor(cx, newTarget, proto); +} // Specialized call for constructing |this| with a known function callee, // and a known prototype. diff --git a/js/src/jsstr.cpp b/js/src/jsstr.cpp index 5209e979de..a605934a06 100644 --- a/js/src/jsstr.cpp +++ b/js/src/jsstr.cpp @@ -3402,8 +3402,7 @@ js::StringConstructor(JSContext* cx, unsigned argc, Value* vp) if (args.isConstructing()) { RootedObject proto(cx); - RootedObject newTarget(cx, &args.newTarget().toObject()); - if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) + if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) return false; StringObject* strobj = StringObject::create(cx, str, proto); diff --git a/js/src/vm/ErrorObject.cpp b/js/src/vm/ErrorObject.cpp index 46e3777dfb..9e8cc2f36f 100644 --- a/js/src/vm/ErrorObject.cpp +++ b/js/src/vm/ErrorObject.cpp @@ -290,7 +290,7 @@ static bool Error(JSContext* cx, unsigned argc, Value* vp) // ES6 19.5.1.1 mandates the .prototype lookup happens before the toString RootedObject proto(cx); - if (!GetPrototypeFromCallableConstructor(cx, args, &proto)) + if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) return false; auto* obj = CreateErrorObject(cx, args, 0, exnType, proto); @@ -341,7 +341,7 @@ static bool AggregateError(JSContext* cx, unsigned argc, Value* vp) // Steps 1-2. (9.1.13 OrdinaryCreateFromConstructor, steps 1-2). RootedObject proto(cx); - if (!GetPrototypeFromCallableConstructor(cx, args, &proto)) { + if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) { return false; } diff --git a/js/src/vm/SharedArrayObject.cpp b/js/src/vm/SharedArrayObject.cpp index 6a3c6a91c3..7061e1acee 100644 --- a/js/src/vm/SharedArrayObject.cpp +++ b/js/src/vm/SharedArrayObject.cpp @@ -254,8 +254,7 @@ SharedArrayBufferObject::class_constructor(JSContext* cx, unsigned argc, Value* } RootedObject proto(cx); - RootedObject newTarget(cx, &args.newTarget().toObject()); - if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) + if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) return false; JSObject* bufobj = New(cx, length, proto); diff --git a/js/src/vm/TypedArrayObject.cpp b/js/src/vm/TypedArrayObject.cpp index faf3dc40df..94a248a5ed 100644 --- a/js/src/vm/TypedArrayObject.cpp +++ b/js/src/vm/TypedArrayObject.cpp @@ -347,20 +347,6 @@ NewArray(JSContext* cx, uint32_t nelements); namespace { -// We allow nullptr for newTarget for all the creation methods, to allow for -// JSFriendAPI functions that don't care about subclassing -static bool -GetPrototypeForInstance(JSContext* cx, HandleObject newTarget, MutableHandleObject proto) -{ - if (newTarget) { - if (!GetPrototypeFromConstructor(cx, newTarget, proto)) - return false; - } else { - proto.set(nullptr); - } - return true; -} - enum class SpeciesConstructorOverride { None, ArrayBuffer @@ -497,7 +483,7 @@ class TypedArrayObjectTemplate : public TypedArrayObject // the time, though, that [[Prototype]] will not be interesting. If // it isn't, we can do some more TI optimizations. RootedObject checkProto(cx); - if (!GetBuiltinPrototype(cx, JSCLASS_CACHED_PROTO_KEY(instanceClass()), &checkProto)) + if (proto && !GetBuiltinPrototype(cx, JSCLASS_CACHED_PROTO_KEY(instanceClass()), &checkProto)) return nullptr; AutoSetNewObjectMetadata metadata(cx); @@ -753,28 +739,28 @@ class TypedArrayObjectTemplate : public TypedArrayObject if (!ToIndex(cx, args.get(0), JSMSG_BAD_ARRAY_LENGTH, &len)) return nullptr; - return fromLength(cx, len, newTarget); + // 22.2.4.1, step 3 and 22.2.4.2, step 5. + // 22.2.4.2.1 AllocateTypedArray, step 1. + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) + return nullptr; + + return fromLength(cx, len, proto); } RootedObject dataObj(cx, &args[0].toObject()); - /* - * (typedArray) - * (sharedTypedArray) - * (type[] array) - * - * Otherwise create a new typed array and copy elements 0..len-1 - * properties from the object, treating it as some sort of array. - * Note that offset and length will be ignored. Note that a - * shared array's values are copied here. - */ - if (!UncheckedUnwrap(dataObj)->is()) - return fromArray(cx, dataObj, newTarget); + // 22.2.4.1, step 3 and 22.2.4.2, step 5. + // 22.2.4.2.1 AllocateTypedArray, step 1. + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) + return nullptr; - /* (ArrayBuffer, [byteOffset, [length]]) */ - RootedObject proto(cx); - if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) - return nullptr; + + if (!UncheckedUnwrap(dataObj)->is()) + return fromArray(cx, dataObj, proto); + + // 22.2.4.5 TypedArray ( buffer [ , byteOffset [ , length ] ] ) int32_t byteOffset = 0; if (args.hasDefined(1)) { @@ -956,11 +942,9 @@ class TypedArrayObjectTemplate : public TypedArrayObject } static JSObject* - fromLength(JSContext* cx, uint64_t nelements, HandleObject newTarget = nullptr) + fromLength(JSContext* cx, uint64_t nelements, HandleObject proto = nullptr) { - RootedObject proto(cx); - if (!GetPrototypeForInstance(cx, newTarget, &proto)) - return nullptr; + // 22.2.4.1, step 3 and 22.2.4.2, step 5 (call AllocateTypedArray). if (nelements > UINT32_MAX) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_ARRAY_LENGTH); @@ -986,13 +970,13 @@ class TypedArrayObjectTemplate : public TypedArrayObject MutableHandle buffer); static JSObject* - fromArray(JSContext* cx, HandleObject other, HandleObject newTarget = nullptr); + fromArray(JSContext* cx, HandleObject other, HandleObject proto = nullptr); static JSObject* - fromTypedArray(JSContext* cx, HandleObject other, bool isWrapped, HandleObject newTarget); + fromTypedArray(JSContext* cx, HandleObject other, bool isWrapped, HandleObject proto); static JSObject* - fromObject(JSContext* cx, HandleObject other, HandleObject newTarget); + fromObject(JSContext* cx, HandleObject other, HandleObject proto); static const NativeType getIndex(JSObject* obj, uint32_t index) @@ -1259,17 +1243,17 @@ TypedArrayObjectTemplate::CloneArrayBufferNoCopy(JSContext* cx, template /* static */ JSObject* TypedArrayObjectTemplate::fromArray(JSContext* cx, HandleObject other, - HandleObject newTarget /* = nullptr */) + HandleObject proto /* = nullptr */) { - // Allow nullptr newTarget for FriendAPI methods, which don't care about + // Allow nullptr proto for FriendAPI methods, which don't care about // subclassing. if (other->is()) - return fromTypedArray(cx, other, /* wrapped= */ false, newTarget); + return fromTypedArray(cx, other, /* wrapped= */ false, proto); if (other->is() && UncheckedUnwrap(other)->is()) - return fromTypedArray(cx, other, /* wrapped= */ true, newTarget); + return fromTypedArray(cx, other, /* wrapped= */ true, proto); - return fromObject(cx, other, newTarget); + return fromObject(cx, other, proto); } // ES2017 draft rev 6390c2f1b34b309895d31d8c0512eac8660a0210 @@ -1277,7 +1261,7 @@ TypedArrayObjectTemplate::fromArray(JSContext* cx, HandleObject other, template /* static */ JSObject* TypedArrayObjectTemplate::fromTypedArray(JSContext* cx, HandleObject other, bool isWrapped, - HandleObject newTarget) + HandleObject proto) { // Step 1. MOZ_ASSERT_IF(!isWrapped, other->is()); @@ -1285,12 +1269,9 @@ TypedArrayObjectTemplate::fromTypedArray(JSContext* cx, HandleObject other, b other->is() && UncheckedUnwrap(other)->is()); - // Step 2 (done in caller). + // Step 2 (Already performed in caller). - // Step 4 (partially). - RootedObject proto(cx); - if (!GetPrototypeForInstance(cx, newTarget, &proto)) - return nullptr; + // Step 4 (Allocation deferred until later). // Step 5. Rooted srcArray(cx); @@ -1406,14 +1387,11 @@ IsOptimizableInit(JSContext* cx, HandleObject iterable, bool* optimized) // 22.2.4.4 TypedArray ( object ) template /* static */ JSObject* -TypedArrayObjectTemplate::fromObject(JSContext* cx, HandleObject other, HandleObject newTarget) +TypedArrayObjectTemplate::fromObject(JSContext* cx, HandleObject other, HandleObject proto) { // Steps 1-2 (Already performed in caller). // Steps 3-4 (Allocation deferred until later). - RootedObject proto(cx); - if (!GetPrototypeForInstance(cx, newTarget, &proto)) - return nullptr; bool optimized = false; if (!IsOptimizableInit(cx, other, &optimized))