/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * 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/. */ #include "builtin/intl/ListFormat.h" #include "mozilla/Assertions.h" #include "mozilla/CheckedInt.h" #include "mozilla/PodOperations.h" #include "mozilla/Unused.h" #include #include #include "builtin/Array.h" #include "builtin/intl/CommonFunctions.h" #include "builtin/intl/ScopedICUObject.h" #include "gc/FreeOp.h" #include "js/Utility.h" #include "js/Vector.h" #include "unicode/uformattedvalue.h" #include "unicode/ulistformatter.h" #include "unicode/utypes.h" #include "vm/JSContext.h" #include "vm/PlainObject.h" // js::PlainObject #include "vm/Runtime.h" // js::ReportAllocationOverflow #include "vm/SelfHosting.h" #include "vm/Stack.h" #include "vm/StringType.h" #include "vm/JSObject-inl.h" #include "vm/NativeObject-inl.h" #include "vm/ObjectOperations-inl.h" using namespace js; using mozilla::CheckedInt; using js::intl::CallICU; using js::intl::IcuLocale; const JSClassOps ListFormatObject::classOps_ = { nullptr, // addProperty nullptr, // delProperty nullptr, // enumerate nullptr, // newEnumerate nullptr, // resolve nullptr, // mayResolve ListFormatObject::finalize, // finalize nullptr, // call nullptr, // hasInstance nullptr, // construct nullptr, // trace }; const JSClass ListFormatObject::class_ = { "Intl.ListFormat", JSCLASS_HAS_RESERVED_SLOTS(ListFormatObject::SLOT_COUNT) | JSCLASS_HAS_CACHED_PROTO(JSProto_ListFormat) | JSCLASS_FOREGROUND_FINALIZE, &ListFormatObject::classOps_, &ListFormatObject::classSpec_}; const JSClass& ListFormatObject::protoClass_ = PlainObject::class_; static bool listFormat_toSource(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); args.rval().setString(cx->names().ListFormat); return true; } static const JSFunctionSpec listFormat_static_methods[] = { JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_ListFormat_supportedLocalesOf", 1, 0), JS_FS_END}; static const JSFunctionSpec listFormat_methods[] = { JS_SELF_HOSTED_FN("resolvedOptions", "Intl_ListFormat_resolvedOptions", 0, 0), JS_SELF_HOSTED_FN("format", "Intl_ListFormat_format", 1, 0), JS_SELF_HOSTED_FN("formatToParts", "Intl_ListFormat_formatToParts", 1, 0), JS_FN(js_toSource_str, listFormat_toSource, 0, 0), JS_FS_END}; static const JSPropertySpec listFormat_properties[] = { JS_STRING_SYM_PS(toStringTag, "Intl.ListFormat", JSPROP_READONLY), JS_PS_END}; static bool ListFormat(JSContext* cx, unsigned argc, Value* vp); const ClassSpec ListFormatObject::classSpec_ = { GenericCreateConstructor, GenericCreatePrototype, listFormat_static_methods, nullptr, listFormat_methods, listFormat_properties, nullptr, ClassSpec::DontDefineConstructor}; enum class ListFormatOptions { SupportsTypeAndStyle, NoTypeAndStyle, }; /** * Initialize a new Intl.ListFormat object using the named self-hosted function. */ static bool InitializeListFormatObject(JSContext* cx, HandleObject obj, HandlePropertyName initializer, HandleValue locales, HandleValue options, ListFormatOptions lfoptions) { FixedInvokeArgs<4> args(cx); args[0].setObject(*obj); args[1].set(locales); args[2].set(options); args[3].setBoolean(lfoptions == ListFormatOptions::SupportsTypeAndStyle); RootedValue ignored(cx); if (!CallSelfHostedFunction(cx, initializer, JS::NullHandleValue, args, &ignored)) { return false; } MOZ_ASSERT(ignored.isUndefined(), "Unexpected return value from non-legacy Intl object initializer"); return true; } /** * Intl.ListFormat([ locales [, options]]) */ static bool ListFormat(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); // Step 1. if (!ThrowIfNotConstructing(cx, args, "Intl.ListFormat")) { return false; } // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). RootedObject proto(cx); if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_ListFormat, &proto)) { return false; } Rooted listFormat( cx, NewObjectWithClassProto(cx, proto)); if (!listFormat) { return false; } HandleValue locales = args.get(0); HandleValue options = args.get(1); constexpr ListFormatOptions lfoptions = #ifndef U_HIDE_DRAFT_API ListFormatOptions::SupportsTypeAndStyle #else ListFormatOptions::NoTypeAndStyle #endif ; // Step 3. if (!InitializeListFormatObject(cx, listFormat, cx->names().InitializeListFormat, locales, options, lfoptions)) { return false; } args.rval().setObject(*listFormat); return true; } void js::ListFormatObject::finalize(JSFreeOp* fop, JSObject* obj) { MOZ_ASSERT(fop->onMainThread()); if (UListFormatter* lf = obj->as().getListFormatter()) { intl::RemoveICUCellMemory(fop, obj, ListFormatObject::EstimatedMemoryUse); ulistfmt_close(lf); } } /** * Returns a new UListFormatter with the locale and list formatting options * of the given ListFormat. */ static UListFormatter* NewUListFormatter(JSContext* cx, Handle listFormat) { RootedObject internals(cx, intl::GetInternalsObject(cx, listFormat)); if (!internals) { return nullptr; } RootedValue value(cx); if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) { return nullptr; } UniqueChars locale = intl::EncodeLocale(cx, value.toString()); if (!locale) { return nullptr; } enum class ListFormatType { Conjunction, Disjunction, Unit }; ListFormatType type; if (!GetProperty(cx, internals, internals, cx->names().type, &value)) { return nullptr; } { JSLinearString* strType = value.toString()->ensureLinear(cx); if (!strType) { return nullptr; } if (StringEqualsLiteral(strType, "conjunction")) { type = ListFormatType::Conjunction; } else if (StringEqualsLiteral(strType, "disjunction")) { type = ListFormatType::Disjunction; } else { MOZ_ASSERT(StringEqualsLiteral(strType, "unit")); type = ListFormatType::Unit; } } enum class ListFormatStyle { Long, Short, Narrow }; ListFormatStyle style; if (!GetProperty(cx, internals, internals, cx->names().style, &value)) { return nullptr; } { JSLinearString* strStyle = value.toString()->ensureLinear(cx); if (!strStyle) { return nullptr; } if (StringEqualsLiteral(strStyle, "long")) { style = ListFormatStyle::Long; } else if (StringEqualsLiteral(strStyle, "short")) { style = ListFormatStyle::Short; } else { MOZ_ASSERT(StringEqualsLiteral(strStyle, "narrow")); style = ListFormatStyle::Narrow; } } UErrorCode status = U_ZERO_ERROR; UListFormatter* lf; #ifndef U_HIDE_DRAFT_API UListFormatterType utype; switch (type) { case ListFormatType::Conjunction: utype = ULISTFMT_TYPE_AND; break; case ListFormatType::Disjunction: utype = ULISTFMT_TYPE_OR; break; case ListFormatType::Unit: utype = ULISTFMT_TYPE_UNITS; break; } UListFormatterWidth uwidth; switch (style) { case ListFormatStyle::Long: uwidth = ULISTFMT_WIDTH_WIDE; break; case ListFormatStyle::Short: uwidth = ULISTFMT_WIDTH_SHORT; break; case ListFormatStyle::Narrow: uwidth = ULISTFMT_WIDTH_NARROW; break; } lf = ulistfmt_openForType(IcuLocale(locale.get()), utype, uwidth, &status); #else MOZ_ASSERT(type == ListFormatType::Conjunction); MOZ_ASSERT(style == ListFormatStyle::Long); mozilla::Unused << type; mozilla::Unused << style; lf = ulistfmt_open(IcuLocale(locale.get()), &status); #endif if (U_FAILURE(status)) { intl::ReportInternalError(cx); return nullptr; } return lf; } static constexpr size_t DEFAULT_LIST_LENGTH = 8; using ListFormatStringVector = Vector; using ListFormatStringLengthVector = Vector; static_assert(sizeof(UniqueTwoByteChars) == sizeof(char16_t*), "UniqueTwoByteChars are stored efficiently and are held in " "continuous memory"); /** * FormatList ( listFormat, list ) */ static bool FormatList(JSContext* cx, UListFormatter* lf, const ListFormatStringVector& strings, const ListFormatStringLengthVector& stringLengths, MutableHandleValue result) { MOZ_ASSERT(strings.length() == stringLengths.length()); MOZ_ASSERT(strings.length() <= INT32_MAX); JSString* str = intl::CallICU(cx, [lf, &strings, &stringLengths]( UChar* chars, int32_t size, UErrorCode* status) { return ulistfmt_format( lf, reinterpret_cast(strings.begin()), stringLengths.begin(), int32_t(strings.length()), chars, size, status); }); if (!str) { return false; } result.setString(str); return true; } /** * FormatListToParts ( listFormat, list ) */ static bool FormatListToParts(JSContext* cx, UListFormatter* lf, const ListFormatStringVector& strings, const ListFormatStringLengthVector& stringLengths, MutableHandleValue result) { MOZ_ASSERT(strings.length() == stringLengths.length()); MOZ_ASSERT(strings.length() <= INT32_MAX); UErrorCode status = U_ZERO_ERROR; UFormattedList* formatted = ulistfmt_openResult(&status); if (U_FAILURE(status)) { intl::ReportInternalError(cx); return false; } ScopedICUObject toClose(formatted); ulistfmt_formatStringsToResult( lf, reinterpret_cast(strings.begin()), stringLengths.begin(), int32_t(strings.length()), formatted, &status); if (U_FAILURE(status)) { intl::ReportInternalError(cx); return false; } const UFormattedValue* formattedValue = ulistfmt_resultAsValue(formatted, &status); if (U_FAILURE(status)) { intl::ReportInternalError(cx); return false; } RootedString overallResult(cx, intl::FormattedValueToString(cx, formattedValue)); if (!overallResult) { return false; } RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx)); if (!partsArray) { return false; } using FieldType = js::ImmutablePropertyNamePtr JSAtomState::*; size_t lastEndIndex = 0; RootedObject singlePart(cx); RootedValue val(cx); auto AppendPart = [&](FieldType type, size_t beginIndex, size_t endIndex) { singlePart = NewBuiltinClassInstance(cx); if (!singlePart) { return false; } val = StringValue(cx->names().*type); if (!DefineDataProperty(cx, singlePart, cx->names().type, val)) { return false; } JSLinearString* partSubstr = NewDependentString( cx, overallResult, beginIndex, endIndex - beginIndex); if (!partSubstr) { return false; } val = StringValue(partSubstr); if (!DefineDataProperty(cx, singlePart, cx->names().value, val)) { return false; } if (!NewbornArrayPush(cx, partsArray, ObjectValue(*singlePart))) { return false; } lastEndIndex = endIndex; return true; }; UConstrainedFieldPosition* fpos = ucfpos_open(&status); if (U_FAILURE(status)) { intl::ReportInternalError(cx); return false; } ScopedICUObject toCloseFpos(fpos); // We're only interested in ULISTFMT_ELEMENT_FIELD fields. ucfpos_constrainField(fpos, UFIELD_CATEGORY_LIST, ULISTFMT_ELEMENT_FIELD, &status); if (U_FAILURE(status)) { intl::ReportInternalError(cx); return false; } while (true) { bool hasMore = ufmtval_nextPosition(formattedValue, fpos, &status); if (U_FAILURE(status)) { intl::ReportInternalError(cx); return false; } if (!hasMore) { break; } int32_t beginIndexInt, endIndexInt; ucfpos_getIndexes(fpos, &beginIndexInt, &endIndexInt, &status); if (U_FAILURE(status)) { intl::ReportInternalError(cx); return false; } MOZ_ASSERT(beginIndexInt >= 0); MOZ_ASSERT(endIndexInt >= 0); MOZ_ASSERT(beginIndexInt <= endIndexInt, "field iterator returning invalid range"); size_t beginIndex = size_t(beginIndexInt); size_t endIndex = size_t(endIndexInt); // Indices are guaranteed to be returned in order (from left to right). MOZ_ASSERT(lastEndIndex <= beginIndex, "field iteration didn't return fields in order start to " "finish as expected"); if (lastEndIndex < beginIndex) { if (!AppendPart(&JSAtomState::literal, lastEndIndex, beginIndex)) { return false; } } if (!AppendPart(&JSAtomState::element, 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_FormatList(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); MOZ_ASSERT(args.length() == 3); Rooted listFormat( cx, &args[0].toObject().as()); bool formatToParts = args[2].toBoolean(); // Obtain a cached UListFormatter object. UListFormatter* lf = listFormat->getListFormatter(); if (!lf) { lf = NewUListFormatter(cx, listFormat); if (!lf) { return false; } listFormat->setListFormatter(lf); intl::AddICUCellMemory(listFormat, ListFormatObject::EstimatedMemoryUse); } // Collect all strings and their lengths. ListFormatStringVector strings(cx); ListFormatStringLengthVector stringLengths(cx); // Keep a conservative running count of overall length. CheckedInt stringLengthTotal(0); RootedArrayObject list(cx, &args[1].toObject().as()); RootedValue value(cx); uint32_t listLen = list->length(); for (uint32_t i = 0; i < listLen; i++) { if (!GetElement(cx, list, list, i, &value)) { return false; } JSLinearString* linear = value.toString()->ensureLinear(cx); if (!linear) { return false; } size_t linearLength = linear->length(); if (!stringLengths.append(linearLength)) { return false; } stringLengthTotal += linearLength; UniqueTwoByteChars chars = cx->make_pod_array(linearLength); if (!chars) { return false; } CopyChars(chars.get(), *linear); if (!strings.append(std::move(chars))) { return false; } } // Add space for N unrealistically large conjunctions. constexpr int32_t MaxConjunctionLen = 100; stringLengthTotal += CheckedInt(listLen) * MaxConjunctionLen; // If the overestimate exceeds ICU length limits, don't try to format. if (!stringLengthTotal.isValid()) { ReportAllocationOverflow(cx); return false; } // Use the UListFormatter to actually format the strings. if (formatToParts) { return FormatListToParts(cx, lf, strings, stringLengths, args.rval()); } return FormatList(cx, lf, strings, stringLengths, args.rval()); }