//------------------------------------------------------------------------------------------------------- // Copyright (C) Microsoft. All rights reserved. // Copyright (c) ChakraCore Project Contributors. All rights reserved. // Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. //------------------------------------------------------------------------------------------------------- #include "RuntimeLibraryPch.h" #include "EngineInterfaceObject.h" #include "IntlEngineInterfaceExtensionObject.h" #include "Types/DeferredTypeHandler.h" #include "Base/WindowsGlobalizationAdapter.h" #ifdef ENABLE_INTL_OBJECT #include "ByteCode/ByteCodeSerializer.h" #include "errstr.h" #include "ByteCode/ByteCodeDumper.h" #include "Codex/Utf8Helper.h" #ifdef INTL_WINGLOB using namespace Windows::Globalization; #endif #ifdef INTL_ICU #include #include "PlatformAgnostic/ChakraICU.h" using namespace PlatformAgnostic::ICUHelpers; #if defined(DBG) || defined(ENABLE_DEBUG_CONFIG_OPTIONS) #define INTL_TRACE(fmt, ...) Output::Trace(Js::IntlPhase, _u("%S(): " fmt "\n"), __func__, __VA_ARGS__) #else #define INTL_TRACE(fmt, ...) #endif #define ICU_ASSERT(e, expr) \ do \ { \ if (e == U_MEMORY_ALLOCATION_ERROR) \ { \ Js::Throw::OutOfMemory(); \ } \ else if (ICU_FAILURE(e)) \ { \ AssertOrFailFastMsg(false, ICU_ERRORMESSAGE(e)); \ } \ else if (!(expr)) \ { \ AssertOrFailFast(expr); \ } \ } while (false) #endif // INTL_ICU // The following macros allow the key-value pairs to be C++ enums as well as JS objects // in Intl.js. When adding a new macro, follow the same format as the _VALUES macros below, // and add your new _VALUES macro to PROJECTED_ENUMS along with the name of the enum. // NOTE: make sure the last VALUE macro has the highest integer value, since the C++ enum's ::Max // value is added to the end of the C++ enum definition as an increment of the previous value. // The ::Max value is used in a defensive assert, and we want to make sure its always 1 greater // than the highest valid value. #define NUMBERFORMATSTYLE_VALUES(VALUE) \ VALUE(Default, default_, 0) \ VALUE(Decimal, decimal, 0) \ VALUE(Percent, percent, 1) \ VALUE(Currency, currency, 2) #define NUMBERFORMATCURRENCYDISPLAY_VALUES(VALUE) \ VALUE(Default, default_, 0) \ VALUE(Symbol, symbol, 0) \ VALUE(Code, code, 1) \ VALUE(Name, name, 2) #define COLLATORSENSITIVITY_VALUES(VALUE) \ VALUE(Default, default_, 3) \ VALUE(Base, base, 0) \ VALUE(Accent, accent, 1) \ VALUE(Case, case_, 2) \ VALUE(Variant, variant, 3) #define COLLATORCASEFIRST_VALUES(VALUE) \ VALUE(Default, default_, 2) \ VALUE(Upper, upper, 0) \ VALUE(Lower, lower, 1) \ VALUE(False, false_, 2) // LocaleDataKind intentionally has no Default value #define LOCALEDATAKIND_VALUES(VALUE) \ VALUE(Collation, co, 0) \ VALUE(CaseFirst, kf, 1) \ VALUE(Numeric, kn, 2) \ VALUE(Calendar, ca, 3) \ VALUE(NumberingSystem, nu, 4) \ VALUE(HourCycle, hc, 5) //BuiltInFunctionID intentionally has no Default value #define BUILTINFUNCTIONID_VALUES(VALUE) \ VALUE(DateToLocaleString, DateToLocaleString, 0) \ VALUE(DateToLocaleDateString, DateToLocaleDateString, 1) \ VALUE(DateToLocaleTimeString, DateToLocaleTimeString, 2) \ VALUE(NumberToLocaleString, NumberToLocaleString, 3) \ VALUE(StringLocaleCompare, StringLocaleCompare, 4) #define ENUM_VALUE(enumName, propId, value) enumName = value, #define PROJECTED_ENUM(ClassName, VALUES) \ enum class ClassName \ { \ VALUES(ENUM_VALUE) \ Max \ }; #define PROJECTED_ENUMS(PROJECT) \ PROJECT(LocaleDataKind, LOCALEDATAKIND_VALUES) \ PROJECT(CollatorCaseFirst, COLLATORCASEFIRST_VALUES) \ PROJECT(CollatorSensitivity, COLLATORSENSITIVITY_VALUES) \ PROJECT(NumberFormatCurrencyDisplay, NUMBERFORMATCURRENCYDISPLAY_VALUES) \ PROJECT(NumberFormatStyle, NUMBERFORMATSTYLE_VALUES) \ PROJECT(BuiltInFunctionID, BUILTINFUNCTIONID_VALUES) PROJECTED_ENUMS(PROJECTED_ENUM) #undef PROJECTED_ENUM #undef ENUM_VALUE #define IfFailAssertAndThrowHr(op) \ if (FAILED(hr=(op))) \ { \ AssertMsg(false, "HRESULT was a failure."); \ JavascriptError::MapAndThrowError(scriptContext, hr); \ } \ #define IfFailAssertMsgAndThrowHr(op, msg) \ if (FAILED(hr=(op))) \ { \ AssertMsg(false, msg); \ JavascriptError::MapAndThrowError(scriptContext, hr); \ } \ #ifdef INTL_WINGLOB #define TO_JSBOOL(sc, b) ((b) ? (sc)->GetLibrary()->GetTrue() : (sc)->GetLibrary()->GetFalse()) #define IfCOMFailIgnoreSilentlyAndReturn(op) \ if(FAILED(hr=(op))) \ { \ return; \ } \ #define HandleOOMSOEHR(hr) \ if (hr == E_OUTOFMEMORY) \ { \ JavascriptError::ThrowOutOfMemoryError(scriptContext); \ } \ else if(hr == VBSERR_OutOfStack) \ { \ JavascriptError::ThrowStackOverflowError(scriptContext); \ } \ #define IfFailThrowHr(op) \ if (FAILED(hr=(op))) \ { \ JavascriptError::MapAndThrowError(scriptContext, hr); \ } \ #define SetPropertyOn(obj, propID, value) \ obj->SetProperty(propID, value, Js::PropertyOperationFlags::PropertyOperation_None, nullptr) \ #define SetStringPropertyOn(obj, propID, propValue) \ SetPropertyOn(obj, propID, Js::JavascriptString::NewCopySz(propValue, scriptContext)) \ #define SetPropertyLOn(obj, literalProperty, value) \ obj->SetProperty(Js::JavascriptString::NewCopySz(literalProperty, scriptContext), value, Js::PropertyOperationFlags::PropertyOperation_None, nullptr) \ #define SetStringPropertyLOn(obj, literalProperty, propValue) \ SetPropertyLOn(obj, literalProperty, Js::JavascriptString::NewCopySz(propValue, scriptContext)) \ #define SetPropertyBuiltInOn(obj, builtInPropID, value) \ SetPropertyOn(obj, Js::PropertyIds::builtInPropID, value) \ #define SetStringPropertyBuiltInOn(obj, builtInPropID, propValue) \ SetPropertyBuiltInOn(obj, builtInPropID, Js::JavascriptString::NewCopySz(propValue, scriptContext)) #define GetPropertyFrom(obj, propertyID) \ Js::JavascriptOperators::GetProperty(obj, propertyID, &propertyValue, scriptContext) \ #define GetPropertyLFrom(obj, propertyName) \ GetPropertyFrom(obj, scriptContext->GetOrAddPropertyIdTracked(propertyName, wcslen(propertyName))) #define GetPropertyBuiltInFrom(obj, builtInPropID) \ GetPropertyFrom(obj, Js::PropertyIds::builtInPropID) \ #define GetTypedPropertyBuiltInFrom(obj, builtInPropID, Type) \ (GetPropertyFrom(obj, Js::PropertyIds::builtInPropID) && VarIs(propertyValue)) \ #define HasPropertyOn(obj, propID) \ Js::JavascriptOperators::HasProperty(obj, propID) \ #define HasPropertyBuiltInOn(obj, builtInPropID) \ HasPropertyOn(obj, Js::PropertyIds::builtInPropID) \ #define HasPropertyLOn(obj, propertyName) \ HasPropertyOn(obj, scriptContext->GetOrAddPropertyIdTracked(propertyName, wcslen(propertyName))) #define SetHSTRINGPropertyOn(obj, propID, hstringValue) \ SetStringPropertyOn(obj, propID, wgl->WindowsGetStringRawBuffer(hstringValue, &length)) \ #define SetHSTRINGPropertyLOn(obj, literalProperty, hstringValue) \ SetStringPropertyLOn(obj, literalProperty, wgl->WindowsGetStringRawBuffer(hstringValue, &length)) \ #define SetHSTRINGPropertyBuiltInOn(obj, builtInPropID, hstringValue) \ SetStringPropertyBuiltInOn(obj, builtInPropID, wgl->WindowsGetStringRawBuffer(hstringValue, &length)) \ #endif #define INTL_CHECK_ARGS(argcheck) AssertOrFailFastMsg((argcheck), "Intl platform function given bad arguments") namespace Js { #ifdef ENABLE_INTL_OBJECT #ifdef INTL_WINGLOB class AutoHSTRING { PREVENT_COPY(AutoHSTRING) private: HSTRING value; public: HSTRING *operator&() { Assert(value == nullptr); return &value; } HSTRING operator*() const { Assert(value != nullptr); return value; } AutoHSTRING() : value(nullptr) { } ~AutoHSTRING() { Clear(); } void Clear() { if (value != nullptr) { WindowsDeleteString(value); value = nullptr; } } }; #endif // Defining Finalizable wrappers for Intl data #if defined(INTL_WINGLOB) class AutoCOMJSObject : public FinalizableObject { IInspectable *instance; public: DEFINE_VTABLE_CTOR_NOBASE(AutoCOMJSObject); AutoCOMJSObject(IInspectable *object) : instance(object) { } static AutoCOMJSObject * New(Recycler * recycler, IInspectable *object) { return RecyclerNewFinalized(recycler, AutoCOMJSObject, object); } void Finalize(bool isShutdown) override { } void Dispose(bool isShutdown) override { if (!isShutdown) { instance->Release(); } } void Mark(Recycler * recycler) override { } IInspectable *GetInstance() { return instance; } }; #elif defined(INTL_ICU) template class FinalizableICUObject : public FinalizableObject { private: FieldNoBarrier(TResource) resource; public: FinalizableICUObject(TResource resource) : resource(resource) { } static FinalizableICUObject *New(Recycler *recycler, TResource resource) { return RecyclerNewFinalized(recycler, FinalizableICUObject, resource); } TResource GetInstance() { return resource; } operator TResource() { return resource; } void Finalize(bool isShutdown) override { } void Dispose(bool isShutdown) override { if (!isShutdown) { CloseFunction(resource); } } void Mark(Recycler *recycler) override { } }; typedef FinalizableICUObject FinalizableUNumberFormat; typedef FinalizableICUObject FinalizableUDateFormat; typedef FinalizableICUObject FinalizableUFieldPositionIterator; typedef FinalizableICUObject FinalizableUCollator; typedef FinalizableICUObject FinalizableUPluralRules; template static void EnsureBuffer(_In_ TExecutor executor, _In_ Recycler *recycler, _Outptr_result_buffer_(*returnLength) char16 **ret, _Out_ int *returnLength, _In_ bool allowZeroLengthStrings = false, _In_ int firstTryLength = 8) { UErrorCode status = U_ZERO_ERROR; *ret = RecyclerNewArrayLeaf(recycler, char16, firstTryLength); *returnLength = executor(reinterpret_cast(*ret), firstTryLength, &status); AssertOrFailFast(allowZeroLengthStrings ? *returnLength >= 0 : *returnLength > 0); if (ICU_BUFFER_FAILURE(status)) { AssertOrFailFastMsg(*returnLength >= firstTryLength, "Executor reported buffer failure but did not require additional space"); int secondTryLength = *returnLength + 1; INTL_TRACE("Buffer of length %d was too short, retrying with buffer of length %d", firstTryLength, secondTryLength); status = U_ZERO_ERROR; *ret = RecyclerNewArrayLeaf(recycler, char16, secondTryLength); *returnLength = executor(reinterpret_cast(*ret), secondTryLength, &status); AssertOrFailFastMsg(*returnLength == secondTryLength - 1, "Second try of executor returned unexpected length"); } else { AssertOrFailFastMsg(*returnLength < firstTryLength, "Executor required additional length but reported successful status"); } AssertOrFailFastMsg(!ICU_FAILURE(status), ICU_ERRORMESSAGE(status)); } template static T *AssertProperty(_In_ DynamicObject *state, _In_ PropertyIds propertyId) { Var propertyValue = nullptr; JavascriptOperators::GetProperty(state, propertyId, &propertyValue, state->GetScriptContext()); AssertOrFailFast(propertyValue && VarIs(propertyValue)); return UnsafeVarTo(propertyValue); } static JavascriptString *AssertStringProperty(_In_ DynamicObject *state, _In_ PropertyIds propertyId) { return AssertProperty(state, propertyId); } static int AssertIntegerProperty(_In_ DynamicObject *state, _In_ PropertyIds propertyId) { Var propertyValue = nullptr; JavascriptOperators::GetProperty(state, propertyId, &propertyValue, state->GetScriptContext()); AssertOrFailFast(propertyValue); if (TaggedInt::Is(propertyValue)) { return TaggedInt::ToInt32(propertyValue); } else { AssertOrFailFast(JavascriptNumber::Is(propertyValue)); int ret; AssertOrFailFast(JavascriptNumber::TryGetInt32Value(JavascriptNumber::GetValue(propertyValue), &ret)); return ret; } } static bool AssertBooleanProperty(_In_ DynamicObject *state, _In_ PropertyIds propertyId) { return AssertProperty(state, propertyId)->GetValue(); } template static T AssertEnumProperty(_In_ DynamicObject *state, _In_ PropertyIds propertyId) { int p = AssertIntegerProperty(state, propertyId); T ret = static_cast(p); AssertMsg(p >= 0 && ret < T::Max, "Invalid value for enum property"); return ret; } template static _Ret_notnull_ T ThrowOOMIfNull(_In_ T value) { if (value == nullptr) { Throw::OutOfMemory(); } return value; } template static void LangtagToLocaleID(_In_count_(langtagLength) const char16 *langtag, _In_ charcount_t langtagLength, _Out_ char(&localeID)[N]) { static_assert(N >= ULOC_FULLNAME_CAPACITY, "LocaleID must be large enough to fit the largest possible ICU localeID"); UErrorCode status = U_ZERO_ERROR; utf8::WideToNarrow langtag8(langtag, langtagLength); int32_t localeIDLength = 0; uloc_forLanguageTag(langtag8, localeID, N, &localeIDLength, &status); ICU_ASSERT(status, localeIDLength > 0 && static_cast(localeIDLength) < N); } template static void LangtagToLocaleID(_In_ JavascriptString *langtag, _Out_ char(&localeID)[N]) { LangtagToLocaleID(langtag->GetString(), langtag->GetLength(), localeID); } template static void ForEachUEnumeration(UEnumeration *enumeration, Callback callback) { int valueLength = 0; UErrorCode status = U_ZERO_ERROR; for (int index = 0, const char *value = uenum_next(enumeration, &valueLength, &status); value != nullptr; index++, value = uenum_next(enumeration, &valueLength, &status)) { ICU_ASSERT(status, valueLength > 0); // cast valueLength now since we have verified its greater than 0 callback(index, value, static_cast(valueLength)); } } template static void ForEachUEnumeration16(UEnumeration *enumeration, Callback callback) { int valueLength = 0; UErrorCode status = U_ZERO_ERROR; int index = 0; for (const UChar *value = uenum_unext(enumeration, &valueLength, &status); value != nullptr; index++, value = uenum_unext(enumeration, &valueLength, &status)) { ICU_ASSERT(status, valueLength > 0); // cast valueLength now since we have verified its greater than 0 callback(index, reinterpret_cast(value), static_cast(valueLength)); } } #endif IntlEngineInterfaceExtensionObject::IntlEngineInterfaceExtensionObject(Js::ScriptContext* scriptContext) : EngineExtensionObjectBase(EngineInterfaceExtensionKind_Intl, scriptContext), dateToLocaleString(nullptr), dateToLocaleTimeString(nullptr), dateToLocaleDateString(nullptr), numberToLocaleString(nullptr), stringLocaleCompare(nullptr), intlNativeInterfaces(nullptr), intlByteCode(nullptr), wasInitialized(false) { } // Initializes the IntlEngineInterfaceExtensionObject::EntryInfo struct #ifdef INTL_ENTRY #undef INTL_ENTRY #endif #define INTL_ENTRY(id, func) \ NoProfileFunctionInfo IntlEngineInterfaceExtensionObject::EntryInfo::Intl_##func##(FORCE_NO_WRITE_BARRIER_TAG(IntlEngineInterfaceExtensionObject::EntryIntl_##func##)); #include "IntlExtensionObjectBuiltIns.h" #undef INTL_ENTRY #ifdef INTL_WINGLOB WindowsGlobalizationAdapter* IntlEngineInterfaceExtensionObject::GetWindowsGlobalizationAdapter(_In_ ScriptContext * scriptContext) { return scriptContext->GetThreadContext()->GetWindowsGlobalizationAdapter(); } #endif void IntlEngineInterfaceExtensionObject::Initialize() { if (wasInitialized) { return; } JavascriptLibrary* library = scriptContext->GetLibrary(); DynamicObject* commonObject = library->GetEngineInterfaceObject()->GetCommonNativeInterfaces(); if (scriptContext->IsIntlEnabled()) { Assert(library->GetEngineInterfaceObject() != nullptr); this->intlNativeInterfaces = DynamicObject::New(library->GetRecycler(), DynamicType::New(scriptContext, TypeIds_Object, commonObject, nullptr, DeferredTypeHandler::GetDefaultInstance())); library->AddMember(library->GetEngineInterfaceObject(), Js::PropertyIds::Intl, this->intlNativeInterfaces); // Only show the platform object publicly if -IntlPlatform is passed if (CONFIG_FLAG(IntlPlatform)) { library->AddMember(library->GetIntlObject(), PropertyIds::platform, this->intlNativeInterfaces); } } wasInitialized = true; } #if DBG void IntlEngineInterfaceExtensionObject::DumpByteCode() { Output::Print(_u("Dumping Intl Byte Code:")); Assert(this->intlByteCode); Js::ByteCodeDumper::DumpRecursively(intlByteCode); } #endif bool IntlEngineInterfaceExtensionObject::InitializeIntlNativeInterfaces(DynamicObject* intlNativeInterfaces, DeferredTypeHandlerBase * typeHandler, DeferredInitializeMode mode) { int initSlotCapacity = 0; // automatically get the initSlotCapacity from everything we are about to add to intlNativeInterfaces #define INTL_ENTRY(id, func) initSlotCapacity++; #include "IntlExtensionObjectBuiltIns.h" #undef INTL_ENTRY #define PROJECTED_ENUM(ClassName, VALUES) initSlotCapacity++; PROJECTED_ENUMS(PROJECTED_ENUM) #undef PROJECTED_ENUM // add capacity for platform.winglob and platform.FallbackSymbol initSlotCapacity += 2; typeHandler->Convert(intlNativeInterfaces, mode, initSlotCapacity); ScriptContext* scriptContext = intlNativeInterfaces->GetScriptContext(); JavascriptLibrary* library = scriptContext->GetLibrary(); // gives each entrypoint a property ID on the intlNativeInterfaces library object #define INTL_ENTRY(id, func) library->AddFunctionToLibraryObject(intlNativeInterfaces, Js::PropertyIds::##id, &IntlEngineInterfaceExtensionObject::EntryInfo::Intl_##func, 1); #include "IntlExtensionObjectBuiltIns.h" #undef INTL_ENTRY library->AddMember(intlNativeInterfaces, PropertyIds::FallbackSymbol, library->CreateSymbol(BuiltInPropertyRecords::_intlFallbackSymbol)); DynamicObject * enumObj = nullptr; // Projects the exact layout of our C++ enums into Intl.js so that we dont have to remember to keep them in sync #define ENUM_VALUE(enumName, propId, value) library->AddMember(enumObj, PropertyIds::##propId, JavascriptNumber::ToVar(value, scriptContext)); #define PROJECTED_ENUM(ClassName, VALUES) \ enumObj = library->CreateObject(); \ VALUES(ENUM_VALUE) \ library->AddMember(intlNativeInterfaces, PropertyIds::##ClassName, enumObj); \ PROJECTED_ENUMS(PROJECTED_ENUM) #undef PROJECTED_ENUM #undef ENUM_VALUE #if INTL_WINGLOB library->AddMember(intlNativeInterfaces, Js::PropertyIds::winglob, library->GetTrue()); #else library->AddMember(intlNativeInterfaces, Js::PropertyIds::winglob, library->GetFalse()); #if defined(NTBUILD) // when using ICU, we can call ulocdata_getCLDRVersion to ensure that ICU is functioning properly before allowing Intl to continue. // ulocdata_getCLDRVersion will cause the data file to be loaded, and if we don't have enough memory to do so, we can throw OutOfMemory here. // This is to protect against spurious U_MISSING_RESOURCE_ERRORs and U_FILE_ACCESS_ERRORs coming from early-lifecycle // functions that require ICU data. // See OS#16897150, OS#16896933, and others relating to bad statuses returned by GetLocaleData and IsLocaleAvailable // This was initially attempted using u_init, however u_init does not work with Node's default small-icu data file // because it contains no converters. UErrorCode status = U_ZERO_ERROR; UVersionInfo cldrVersion; ulocdata_getCLDRVersion(cldrVersion, &status); if (status == U_MEMORY_ALLOCATION_ERROR || status == U_FILE_ACCESS_ERROR || status == U_MISSING_RESOURCE_ERROR) { // Trace that this happens in case there are build system changes that actually cause the data file to be not found INTL_TRACE("Could not initialize ICU - ulocdata_getCLDRVersion returned status %S", u_errorName(status)); Throw::OutOfMemory(); } else { INTL_TRACE("Using CLDR version %d.%d.%d.%d", cldrVersion[0], cldrVersion[1], cldrVersion[2], cldrVersion[3]); } AssertOrFailFastMsg(U_SUCCESS(status), "ulocdata_getCLDRVersion returned non-OOM failure"); #endif // defined(NTBUILD) #endif // else !INTL_WINGLOB // Add a reference to the built-in Intl object that can be referenced from the init code in Intl.js. library->AddMember(intlNativeInterfaces, Js::PropertyIds::Intl, library->GetIntlObject()); intlNativeInterfaces->SetHasNoEnumerableProperties(true); return true; } void IntlEngineInterfaceExtensionObject::deletePrototypePropertyHelper(ScriptContext* scriptContext, DynamicObject* intlObject, Js::PropertyId objectPropertyId, Js::PropertyId getterFunctionId) { DynamicObject *prototypeObject = nullptr; DynamicObject *functionObj = nullptr; Var propertyValue = nullptr; Var prototypeValue = nullptr; Var resolvedOptionsValue = nullptr; Var getter = nullptr; Var setter = nullptr; if (!JavascriptOperators::GetProperty(intlObject, objectPropertyId, &propertyValue, scriptContext) || !JavascriptOperators::IsObject(propertyValue)) { return; } if (!JavascriptOperators::GetProperty(VarTo(propertyValue), Js::PropertyIds::prototype, &prototypeValue, scriptContext) || !JavascriptOperators::IsObject(prototypeValue)) { return; } prototypeObject = VarTo(prototypeValue); if (!JavascriptOperators::GetProperty(prototypeObject, Js::PropertyIds::resolvedOptions, &resolvedOptionsValue, scriptContext) || !JavascriptOperators::IsObject(resolvedOptionsValue)) { return; } functionObj = VarTo(resolvedOptionsValue); functionObj->SetConfigurable(Js::PropertyIds::prototype, true); functionObj->DeleteProperty(Js::PropertyIds::prototype, Js::PropertyOperationFlags::PropertyOperation_None); if (!JavascriptOperators::GetOwnAccessors(prototypeObject, getterFunctionId, &getter, &setter, scriptContext) || !JavascriptOperators::IsObject(getter)) { return; } functionObj = VarTo(getter); functionObj->SetConfigurable(Js::PropertyIds::prototype, true); functionObj->DeleteProperty(Js::PropertyIds::prototype, Js::PropertyOperationFlags::PropertyOperation_None); } void IntlEngineInterfaceExtensionObject::cleanUpIntl(ScriptContext *scriptContext, DynamicObject* intlObject) { this->dateToLocaleString = nullptr; this->dateToLocaleTimeString = nullptr; this->dateToLocaleDateString = nullptr; this->numberToLocaleString = nullptr; this->stringLocaleCompare = nullptr; //Failed to setup Intl; Windows.Globalization.dll is most likely missing. if (Js::JavascriptOperators::HasProperty(intlObject, Js::PropertyIds::Collator)) { intlObject->DeleteProperty(Js::PropertyIds::Collator, Js::PropertyOperationFlags::PropertyOperation_None); } if (Js::JavascriptOperators::HasProperty(intlObject, Js::PropertyIds::NumberFormat)) { intlObject->DeleteProperty(Js::PropertyIds::NumberFormat, Js::PropertyOperationFlags::PropertyOperation_None); } if (Js::JavascriptOperators::HasProperty(intlObject, Js::PropertyIds::DateTimeFormat)) { intlObject->DeleteProperty(Js::PropertyIds::DateTimeFormat, Js::PropertyOperationFlags::PropertyOperation_None); } } void IntlEngineInterfaceExtensionObject::EnsureIntlByteCode(_In_ ScriptContext * scriptContext) { if (this->intlByteCode == nullptr) { SourceContextInfo * sourceContextInfo = scriptContext->GetSourceContextInfo(Js::Constants::NoHostSourceContext, NULL); Assert(sourceContextInfo != nullptr); SRCINFO si; memset((void*)&si, 0, sizeof(si)); si.sourceContextInfo = sourceContextInfo; SRCINFO *hsi = scriptContext->AddHostSrcInfo(&si); uint32 flags = fscrIsLibraryCode | (CONFIG_FLAG(CreateFunctionProxy) && !scriptContext->IsProfiling() ? fscrAllowFunctionProxy : 0); HRESULT hr = Js::ByteCodeSerializer::DeserializeFromBuffer(scriptContext, flags, (LPCUTF8)nullptr, hsi, (byte*)js::Library_Bytecode_Intl, nullptr, &this->intlByteCode); IfFailAssertMsgAndThrowHr(hr, "Failed to deserialize Intl.js bytecode - very probably the bytecode needs to be rebuilt."); this->SetHasBytecode(); } } void IntlEngineInterfaceExtensionObject::InjectIntlLibraryCode(_In_ ScriptContext * scriptContext, DynamicObject* intlObject, IntlInitializationType intlInitializationType) { // Ensure JsBuiltIns are initialized before initializing Intl which uses some of them. scriptContext->GetLibrary()->EnsureArrayBuiltInsAreReady(); scriptContext->GetLibrary()->EnsureMathBuiltInsAreReady(); JavascriptExceptionObject *pExceptionObject = nullptr; #ifdef INTL_WINGLOB WindowsGlobalizationAdapter* globAdapter = GetWindowsGlobalizationAdapter(scriptContext); #endif try { this->EnsureIntlByteCode(scriptContext); Assert(intlByteCode != nullptr); #ifdef INTL_WINGLOB DelayLoadWindowsGlobalization *library = scriptContext->GetThreadContext()->GetWindowsGlobalizationLibrary(); #endif JavascriptString* initType = nullptr; #ifdef INTL_WINGLOB HRESULT hr; //Ensure we have initialized all appropriate COM objects for the adapter (we will be using them now) IfCOMFailIgnoreSilentlyAndReturn(globAdapter->EnsureCommonObjectsInitialized(library)); #endif switch (intlInitializationType) { default: AssertMsg(false, "Not a valid intlInitializationType."); // fall thru case IntlInitializationType::Intl: #ifdef INTL_WINGLOB IfCOMFailIgnoreSilentlyAndReturn(globAdapter->EnsureNumberFormatObjectsInitialized(library)); IfCOMFailIgnoreSilentlyAndReturn(globAdapter->EnsureDateTimeFormatObjectsInitialized(library)); #endif initType = scriptContext->GetPropertyString(PropertyIds::Intl); break; case IntlInitializationType::StringPrototype: // No other windows globalization adapter needed. Common adapter should suffice initType = scriptContext->GetPropertyString(PropertyIds::String); break; case IntlInitializationType::DatePrototype: #ifdef INTL_WINGLOB IfCOMFailIgnoreSilentlyAndReturn(globAdapter->EnsureDateTimeFormatObjectsInitialized(library)); #endif initType = scriptContext->GetPropertyString(PropertyIds::Date); break; case IntlInitializationType::NumberPrototype: #ifdef INTL_WINGLOB IfCOMFailIgnoreSilentlyAndReturn(globAdapter->EnsureNumberFormatObjectsInitialized(library)); #endif initType = scriptContext->GetPropertyString(PropertyIds::Number); break; } Js::ScriptFunction *function = scriptContext->GetLibrary()->CreateScriptFunction(intlByteCode->GetNestedFunctionForExecution(0)); #ifdef ENABLE_SCRIPT_PROFILING // If we are profiling, we need to register the script to the profiler callback, so the script compiled event will be sent. if (scriptContext->IsProfiling()) { scriptContext->RegisterScript(function->GetFunctionProxy()); } #endif #ifdef ENABLE_SCRIPT_DEBUGGING // Mark we are profiling library code already, so that any initialization library code called here won't be reported to profiler. // Also tell the debugger not to record events during intialization so that we don't leak information about initialization. AutoInitLibraryCodeScope autoInitLibraryCodeScope(scriptContext); #endif Js::Var args[] = { scriptContext->GetLibrary()->GetUndefined(), scriptContext->GetLibrary()->GetEngineInterfaceObject(), initType }; Js::CallInfo callInfo(Js::CallFlags_Value, _countof(args)); Js::Arguments arguments(callInfo, args); scriptContext->GetThreadContext()->ExecuteImplicitCall(function, Js::ImplicitCall_Accessor, [=]()->Js::Var { return JavascriptFunction::CallRootFunctionInScript(function, arguments); }); // Delete prototypes on functions if initialized Intl object if (intlInitializationType == IntlInitializationType::Intl) { deletePrototypePropertyHelper(scriptContext, intlObject, Js::PropertyIds::Collator, Js::PropertyIds::compare); deletePrototypePropertyHelper(scriptContext, intlObject, Js::PropertyIds::NumberFormat, Js::PropertyIds::format); deletePrototypePropertyHelper(scriptContext, intlObject, Js::PropertyIds::DateTimeFormat, Js::PropertyIds::format); } #if DBG_DUMP if (PHASE_DUMP(Js::ByteCodePhase, function->GetFunctionProxy()) && Js::Configuration::Global.flags.Verbose) { DumpByteCode(); } #endif } catch (const JavascriptException& err) { pExceptionObject = err.GetAndClear(); } if (pExceptionObject) { if (intlInitializationType == IntlInitializationType::Intl) { cleanUpIntl(scriptContext, intlObject); } if (pExceptionObject == ThreadContext::GetContextForCurrentThread()->GetPendingOOMErrorObject() || pExceptionObject == ThreadContext::GetContextForCurrentThread()->GetPendingSOErrorObject()) { // Reset factory objects that are might not have fully initialized #ifdef INTL_WINGLOB globAdapter->ResetCommonFactoryObjects(); #endif switch (intlInitializationType) { default: AssertMsg(false, "Not a valid intlInitializationType."); // fall thru case IntlInitializationType::Intl: #ifdef INTL_WINGLOB globAdapter->ResetNumberFormatFactoryObjects(); globAdapter->ResetDateTimeFormatFactoryObjects(); #endif scriptContext->GetLibrary()->ResetIntlObject(); break; case IntlInitializationType::StringPrototype: // No other windows globalization adapter is created. Resetting common adapter should suffice break; case IntlInitializationType::DatePrototype: #ifdef INTL_WINGLOB globAdapter->ResetDateTimeFormatFactoryObjects(); #endif break; case IntlInitializationType::NumberPrototype: #ifdef INTL_WINGLOB globAdapter->ResetNumberFormatFactoryObjects(); #endif break; } JavascriptExceptionOperators::DoThrowCheckClone(pExceptionObject, scriptContext); } #if DEBUG JavascriptExceptionOperators::DoThrowCheckClone(pExceptionObject, scriptContext); #else JavascriptError::ThrowTypeError(scriptContext, JSERR_IntlNotAvailable); #endif } } // First parameter is boolean. Var IntlEngineInterfaceExtensionObject::EntryIntl_RaiseAssert(RecyclableObject* function, CallInfo callInfo, ...) { EngineInterfaceObject_CommonFunctionProlog(function, callInfo); if (args.Info.Count < 2 || !VarIs(args.Values[1])) { AssertMsg(false, "Intl's Assert platform API was called incorrectly."); return scriptContext->GetLibrary()->GetUndefined(); } #if DEBUG #ifdef INTL_ICU_DEBUG Output::Print(_u("EntryIntl_RaiseAssert\n")); #endif JavascriptExceptionOperators::Throw(VarTo(args.Values[1]), scriptContext); #else return scriptContext->GetLibrary()->GetUndefined(); #endif } Var IntlEngineInterfaceExtensionObject::EntryIntl_IsWellFormedLanguageTag(RecyclableObject* function, CallInfo callInfo, ...) { #if defined(INTL_ICU) AssertOrFailFastMsg(false, "IsWellFormedLanguageTag is not implemented using ICU"); return nullptr; #else EngineInterfaceObject_CommonFunctionProlog(function, callInfo); if (args.Info.Count < 2 || !VarIs(args.Values[1])) { // IsWellFormedLanguageTag of undefined or non-string is false return scriptContext->GetLibrary()->GetFalse(); } JavascriptString *argString = VarTo(args.Values[1]); return TO_JSBOOL(scriptContext, GetWindowsGlobalizationAdapter(scriptContext)->IsWellFormedLanguageTag(scriptContext, argString->GetSz())); #endif } Var IntlEngineInterfaceExtensionObject::EntryIntl_NormalizeLanguageTag(RecyclableObject* function, CallInfo callInfo, ...) { EngineInterfaceObject_CommonFunctionProlog(function, callInfo); #if defined(INTL_ICU) INTL_CHECK_ARGS(args.Info.Count == 2 && VarIs(args[1])); UErrorCode status = U_ZERO_ERROR; JavascriptString *langtag = UnsafeVarTo(args[1]); utf8::WideToNarrow langtag8(langtag->GetSz(), langtag->GetLength()); // ICU doesn't have a full-fledged canonicalization implementation that correctly replaces all preferred values // and grandfathered tags, as required by #sec-canonicalizelanguagetag. // However, passing the locale through uloc_forLanguageTag -> uloc_toLanguageTag gets us most of the way there // by replacing some(?) values, correctly capitalizing the tag, and re-ordering extensions int parsedLength = 0; char localeID[ULOC_FULLNAME_CAPACITY] = { 0 }; int forLangTagResultLength = uloc_forLanguageTag(langtag8, localeID, ULOC_FULLNAME_CAPACITY, &parsedLength, &status); AssertOrFailFast(parsedLength >= 0); if (status == U_ILLEGAL_ARGUMENT_ERROR || ((charcount_t) parsedLength) < langtag->GetLength()) { // The string passed in to NormalizeLanguageTag has already passed IsStructurallyValidLanguageTag. // However, duplicate unicode extension keys, such as "de-u-co-phonebk-co-phonebk", are structurally // valid according to RFC5646 yet still trigger U_ILLEGAL_ARGUMENT_ERROR // V8 ~6.2 says that the above language tag is invalid, while SpiderMonkey ~58 handles it. // Until we have a more spec-compliant implementation of CanonicalizeLanguageTag, err on the side // of caution and say it is invalid. // We also check for parsedLength < langtag->GetLength() because there are cases when status == U_ZERO_ERROR // but the langtag was not valid, such as "en-tesTER-TESter" (OSS-Fuzz #6657). // NOTE: make sure we check for `undefined` at the platform.normalizeLanguageTag callsite. return scriptContext->GetLibrary()->GetUndefined(); } // forLangTagResultLength can be 0 if langtag is "und". // uloc_toLanguageTag("") returns "und", so this works out (forLanguageTag can return >= 0 but toLanguageTag must return > 0) ICU_ASSERT(status, forLangTagResultLength >= 0 && ((charcount_t) parsedLength) == langtag->GetLength()); char canonicalized[ULOC_FULLNAME_CAPACITY] = { 0 }; int toLangTagResultLength = uloc_toLanguageTag(localeID, canonicalized, ULOC_FULLNAME_CAPACITY, true, &status); ICU_ASSERT(status, toLangTagResultLength > 0); // allocate toLangTagResultLength + 1 to leave room for null terminator char16 *canonicalized16 = RecyclerNewArrayLeaf(scriptContext->GetRecycler(), char16, toLangTagResultLength + 1); charcount_t canonicalized16Len = 0; HRESULT hr = utf8::NarrowStringToWideNoAlloc( canonicalized, toLangTagResultLength, canonicalized16, toLangTagResultLength + 1, &canonicalized16Len ); AssertOrFailFast(hr == S_OK && ((int) canonicalized16Len) == toLangTagResultLength); return JavascriptString::NewWithBuffer(canonicalized16, toLangTagResultLength, scriptContext); #else if (args.Info.Count < 2 || !VarIs(args.Values[1])) { // NormalizeLanguageTag of undefined or non-string is undefined return scriptContext->GetLibrary()->GetUndefined(); } JavascriptString *argString = VarTo(args.Values[1]); JavascriptString *retVal; HRESULT hr; AutoHSTRING str; hr = GetWindowsGlobalizationAdapter(scriptContext)->NormalizeLanguageTag(scriptContext, argString->GetSz(), &str); DelayLoadWindowsGlobalization *wsl = scriptContext->GetThreadContext()->GetWindowsGlobalizationLibrary(); PCWSTR strBuf = wsl->WindowsGetStringRawBuffer(*str, NULL); retVal = Js::JavascriptString::NewCopySz(strBuf, scriptContext); if (FAILED(hr)) { HandleOOMSOEHR(hr); //If we can't normalize the tag; return undefined. return scriptContext->GetLibrary()->GetUndefined(); } return retVal; #endif } #ifdef INTL_ICU template static bool BinarySearchForLocale(const char *localeID) { const int count = CountAvailableLocalesFunc(); int left = 0; int right = count - 1; int iterations = 0; while (true) { iterations += 1; if (left > right) { INTL_TRACE("Could not find localeID %S in %d iterations", localeID, iterations); return false; } int i = (left + right) / 2; Assert(i >= 0 && i < count); const char *cur = GetAvailableLocalesFunc(i); // Ensure that this list is actually binary searchable Assert(i > 0 ? strcmp(GetAvailableLocalesFunc(i - 1), cur) < 0 : true); Assert(i < count - 1 ? strcmp(GetAvailableLocalesFunc(i + 1), cur) > 0 : true); int res = strcmp(localeID, cur); if (res == 0) { INTL_TRACE("Found localeID %S in %d iterations", localeID, iterations); return true; } else if (res < 0) { right = i - 1; } else { left = i + 1; } } } template static bool IsLocaleAvailable(JavascriptString *langtag) { char localeID[ULOC_FULLNAME_CAPACITY] = { 0 }; LangtagToLocaleID(langtag, localeID); if (!BinarySearchForLocale(localeID)) { // ICU's "available locales" do not include (all? most?) aliases. // For example, searching for "zh_TW" will return false, even though // zh_TW is equivalent to zh_Hant_TW, which would return true. // We can work around this by searching for both the locale as requested and // the locale in addition to all of its "likely subtags." // This works in practice because, for instance, unum_open("zh_TW") will actually // use "zh_Hant_TW" (confirmed with unum_getLocaleByType(..., ULOC_VALID_LOCALE)) // The code below performs, for example, the following mappings: // pa_PK -> pa_Arab_PK // sr_RS -> sr_Cyrl_RS // zh_CN -> zh_Hans_CN // zh_TW -> zh_Hant_TW // TODO(jahorto): Determine if there is any scenario where a language tag + likely subtags is // not exactly functionally equivalent to the language tag on its own -- basically, where // constructor_open(language_tag) behaves differently to constructor_open(language_tag_and_likely_subtags) // for all supported constructors. UErrorCode status = U_ZERO_ERROR; char localeIDWithLikelySubtags[ULOC_FULLNAME_CAPACITY] = { 0 }; int likelySubtagLen = uloc_addLikelySubtags(localeID, localeIDWithLikelySubtags, ULOC_FULLNAME_CAPACITY, &status); ICU_ASSERT(status, likelySubtagLen > 0 && likelySubtagLen < ULOC_FULLNAME_CAPACITY); return BinarySearchForLocale(localeIDWithLikelySubtags); } return true; } #endif #ifdef INTL_ICU #define DEFINE_ISXLOCALEAVAILABLE(ctorShortName, icuNamespace) \ Var IntlEngineInterfaceExtensionObject::EntryIntl_Is##ctorShortName##LocaleAvailable(RecyclableObject* function, CallInfo callInfo, ...) \ { \ EngineInterfaceObject_CommonFunctionProlog(function, callInfo); \ INTL_CHECK_ARGS(args.Info.Count == 2 && VarIs(args.Values[1])); \ return scriptContext->GetLibrary()->GetTrueOrFalse( \ IsLocaleAvailable<##icuNamespace##_getAvailable, ##icuNamespace##_countAvailable>(UnsafeVarTo(args.Values[1])) \ ); \ } #else #define DEFINE_ISXLOCALEAVAILABLE(ctorShortName, icuNamespace) \ Var IntlEngineInterfaceExtensionObject::EntryIntl_Is##ctorShortName##LocaleAvailable(RecyclableObject* function, CallInfo callInfo, ...) \ { \ AssertOrFailFastMsg(false, "Intl with Windows Globalization should never call Is" #ctorShortName "LocaleAvailable"); \ return nullptr; \ } #endif DEFINE_ISXLOCALEAVAILABLE(Collator, ucol) DEFINE_ISXLOCALEAVAILABLE(NF, unum) DEFINE_ISXLOCALEAVAILABLE(DTF, udat) // uplrules namespace doesn't have its own getAvailable/countAvailable // assume it supports whatever is supported in the base data DEFINE_ISXLOCALEAVAILABLE(PR, uloc) #ifdef INTL_ICU #endif Var IntlEngineInterfaceExtensionObject::EntryIntl_GetLocaleData(RecyclableObject* function, CallInfo callInfo, ...) { #ifdef INTL_ICU EngineInterfaceObject_CommonFunctionProlog(function, callInfo); INTL_CHECK_ARGS( args.Info.Count == 3 && (JavascriptNumber::Is(args.Values[1]) || TaggedInt::Is(args.Values[1])) && VarIs(args.Values[2]) ); LocaleDataKind kind = (LocaleDataKind) (TaggedInt::Is(args.Values[1]) ? TaggedInt::ToInt32(args.Values[1]) : (int) JavascriptNumber::GetValue(args.Values[1])); JavascriptArray *ret = nullptr; UErrorCode status = U_ZERO_ERROR; char localeID[ULOC_FULLNAME_CAPACITY] = { 0 }; JavascriptString *langtag = UnsafeVarTo(args.Values[2]); LangtagToLocaleID(langtag, localeID); JavascriptLibrary *library = scriptContext->GetLibrary(); PropertyOperationFlags flag = PropertyOperationFlags::PropertyOperation_None; if (kind == LocaleDataKind::Collation) { ScopedUEnumeration collations(ucol_getKeywordValuesForLocale("collation", localeID, false, &status)); int collationsCount = uenum_count(collations, &status); // we expect collationsCount to have at least "standard" and "search" in it ICU_ASSERT(status, collationsCount > 2); // the return array can't include "standard" and "search", but must have its first element be null (count - 2 + 1) [#sec-intl-collator-internal-slots] ret = library->CreateArray(collationsCount - 1); ret->SetItem(0, library->GetNull(), flag); int collationLen = 0; const char *collation = nullptr; int i = 0; for (collation = uenum_next(collations, &collationLen, &status); collation != nullptr; collation = uenum_next(collations, &collationLen, &status)) { ICU_ASSERT(status, collation != nullptr && collationLen > 0); if (strcmp(collation, "standard") == 0 || strcmp(collation, "search") == 0) { // continue does not create holes in ret because i is set outside the loop continue; } // OS#17172584: OOM during uloc_toUnicodeLocaleType can make this return nullptr even for known collations const char *unicodeCollation = ThrowOOMIfNull(uloc_toUnicodeLocaleType("collation", collation)); const size_t unicodeCollationLen = strlen(unicodeCollation); // we only need strlen(unicodeCollation) + 1 char16s because unicodeCollation will always be ASCII (funnily enough) char16 *unicodeCollation16 = RecyclerNewArrayLeaf(scriptContext->GetRecycler(), char16, unicodeCollationLen + 1); charcount_t unicodeCollation16Len = 0; HRESULT hr = utf8::NarrowStringToWideNoAlloc( unicodeCollation, unicodeCollationLen, unicodeCollation16, unicodeCollationLen + 1, &unicodeCollation16Len ); AssertOrFailFastMsg( hr == S_OK && unicodeCollation16Len == unicodeCollationLen && unicodeCollation16Len < MaxCharCount, "Unicode collation char16 conversion was unsuccessful" ); // i + 1 to not overwrite leading null element ret->SetItem(i + 1, JavascriptString::NewWithBuffer( unicodeCollation16, unicodeCollation16Len, scriptContext ), PropertyOperationFlags::PropertyOperation_None); i++; } } else if (kind == LocaleDataKind::CaseFirst) { ScopedUCollator collator(ucol_open(localeID, &status)); UColAttributeValue kf = ucol_getAttribute(collator, UCOL_CASE_FIRST, &status); ICU_ASSERT(status, true); ret = library->CreateArray(3); JavascriptString *falseStr = library->GetFalseDisplayString(); JavascriptString *upperStr = library->GetIntlCaseFirstUpperString(); JavascriptString *lowerStr = library->GetIntlCaseFirstLowerString(); if (kf == UCOL_OFF) { ret->SetItem(0, falseStr, flag); ret->SetItem(1, upperStr, flag); ret->SetItem(2, lowerStr, flag); } else if (kf == UCOL_UPPER_FIRST) { ret->SetItem(0, upperStr, flag); ret->SetItem(1, lowerStr, flag); ret->SetItem(2, falseStr, flag); } else if (kf == UCOL_LOWER_FIRST) { ret->SetItem(0, lowerStr, flag); ret->SetItem(1, upperStr, flag); ret->SetItem(2, falseStr, flag); } } else if (kind == LocaleDataKind::Numeric) { ScopedUCollator collator(ucol_open(localeID, &status)); UColAttributeValue kn = ucol_getAttribute(collator, UCOL_NUMERIC_COLLATION, &status); ICU_ASSERT(status, true); ret = library->CreateArray(2); JavascriptString *falseStr = library->GetFalseDisplayString(); JavascriptString *trueStr = library->GetTrueDisplayString(); if (kn == UCOL_OFF) { ret->SetItem(0, falseStr, flag); ret->SetItem(1, trueStr, flag); } else if (kn == UCOL_ON) { ret->SetItem(0, trueStr, flag); ret->SetItem(1, falseStr, flag); } } else if (kind == LocaleDataKind::Calendar) { ScopedUEnumeration calendars(ucal_getKeywordValuesForLocale("calendar", localeID, false, &status)); ret = library->CreateArray(uenum_count(calendars, &status)); ICU_ASSERT(status, true); int calendarLen = 0; const char *calendar = nullptr; int i = 0; for (calendar = uenum_next(calendars, &calendarLen, &status); calendar != nullptr; calendar = uenum_next(calendars, &calendarLen, &status)) { ICU_ASSERT(status, calendar != nullptr && calendarLen > 0); // OS#17172584: OOM during uloc_toUnicodeLocaleType can make this return nullptr even for known calendars const char *unicodeCalendar = ThrowOOMIfNull(uloc_toUnicodeLocaleType("calendar", calendar)); const size_t unicodeCalendarLen = strlen(unicodeCalendar); // we only need strlen(unicodeCalendar) + 1 char16s because unicodeCalendar will always be ASCII (funnily enough) char16 *unicodeCalendar16 = RecyclerNewArrayLeaf(scriptContext->GetRecycler(), char16, strlen(unicodeCalendar) + 1); charcount_t unicodeCalendar16Len = 0; HRESULT hr = utf8::NarrowStringToWideNoAlloc( unicodeCalendar, unicodeCalendarLen, unicodeCalendar16, unicodeCalendarLen + 1, &unicodeCalendar16Len ); AssertOrFailFastMsg( hr == S_OK && unicodeCalendar16Len == unicodeCalendarLen && unicodeCalendar16Len < MaxCharCount, "Unicode calendar char16 conversion was unsuccessful" ); ret->SetItem(i, JavascriptString::NewWithBuffer( unicodeCalendar16, static_cast(unicodeCalendar16Len), scriptContext ), flag); i++; } } else if (kind == LocaleDataKind::NumberingSystem) { // unumsys_openAvailableNames has multiple bugs (http://bugs.icu-project.org/trac/ticket/11908) and also // does not provide a locale-specific set of numbering systems // the Intl spec provides a list of required numbering systems to support in #table-numbering-system-digits // For now, assume that all of those numbering systems are supported, and just get the default using unumsys_open // unumsys_open will also ensure that "native", "traditio", and "finance" are not returned, as per #sec-intl.datetimeformat-internal-slots ScopedUNumberingSystem numsys(unumsys_open(localeID, &status)); ICU_ASSERT(status, true); utf8::NarrowToWide numsysName(unumsys_getName(numsys)); // NOTE: update the initial array length if the list of available numbering systems changes in the future! ret = library->CreateArray(22); int i = 0; ret->SetItem(i++, JavascriptString::NewCopySz(numsysName, scriptContext), flag); // It doesn't matter that item 0 will be in the array twice (aside for size), because item 0 is the // preferred numbering system for the given locale, so it has precedence over everything else ret->SetItem(i++, library->GetIntlNumsysArabString(), flag); ret->SetItem(i++, library->GetIntlNumsysArabextString(), flag); ret->SetItem(i++, library->GetIntlNumsysBaliString(), flag); ret->SetItem(i++, library->GetIntlNumsysBengString(), flag); ret->SetItem(i++, library->GetIntlNumsysDevaString(), flag); ret->SetItem(i++, library->GetIntlNumsysFullwideString(), flag); ret->SetItem(i++, library->GetIntlNumsysGujrString(), flag); ret->SetItem(i++, library->GetIntlNumsysGuruString(), flag); ret->SetItem(i++, library->GetIntlNumsysHanidecString(), flag); ret->SetItem(i++, library->GetIntlNumsysKhmrString(), flag); ret->SetItem(i++, library->GetIntlNumsysKndaString(), flag); ret->SetItem(i++, library->GetIntlNumsysLaooString(), flag); ret->SetItem(i++, library->GetIntlNumsysLatnString(), flag); ret->SetItem(i++, library->GetIntlNumsysLimbString(), flag); ret->SetItem(i++, library->GetIntlNumsysMlymString(), flag); ret->SetItem(i++, library->GetIntlNumsysMongString(), flag); ret->SetItem(i++, library->GetIntlNumsysMymrString(), flag); ret->SetItem(i++, library->GetIntlNumsysOryaString(), flag); ret->SetItem(i++, library->GetIntlNumsysTamldecString(), flag); ret->SetItem(i++, library->GetIntlNumsysTeluString(), flag); ret->SetItem(i++, library->GetIntlNumsysThaiString(), flag); ret->SetItem(i++, library->GetIntlNumsysTibtString(), flag); } else if (kind == LocaleDataKind::HourCycle) { // #sec-intl.datetimeformat-internal-slots: "[[LocaleData]][locale].hc must be < null, h11, h12, h23, h24 > for all locale values" ret = library->CreateArray(5); int i = 0; ret->SetItem(i++, library->GetNull(), flag); ret->SetItem(i++, library->GetIntlHourCycle11String(), flag); ret->SetItem(i++, library->GetIntlHourCycle12String(), flag); ret->SetItem(i++, library->GetIntlHourCycle23String(), flag); ret->SetItem(i++, library->GetIntlHourCycle24String(), flag); } else { AssertOrFailFastMsg(false, "GetLocaleData called with unknown kind parameter"); } return ret; #else AssertOrFailFastMsg(false, "Intl with Windows Globalization should never call GetLocaleData"); return nullptr; #endif } Var IntlEngineInterfaceExtensionObject::EntryIntl_ResolveLocaleLookup(RecyclableObject* function, CallInfo callInfo, ...) { EngineInterfaceObject_CommonFunctionProlog(function, callInfo); if (args.Info.Count < 2 || !VarIs(args.Values[1])) { // ResolveLocaleLookup of undefined or non-string is undefined return scriptContext->GetLibrary()->GetUndefined(); } #if defined(INTL_ICU) #if defined(INTL_ICU_DEBUG) Output::Print(_u("Intl::ResolveLocaleLookup returned false: EntryIntl_ResolveLocaleLookup returning null to fallback to JS\n")); #endif return scriptContext->GetLibrary()->GetNull(); #else JavascriptString *argString = VarTo(args.Values[1]); PCWSTR passedLocale = argString->GetSz(); // REVIEW should we zero the whole array for safety? WCHAR resolvedLocaleName[LOCALE_NAME_MAX_LENGTH]; resolvedLocaleName[0] = '\0'; ResolveLocaleName(passedLocale, resolvedLocaleName, _countof(resolvedLocaleName)); if (resolvedLocaleName[0] == '\0') { return scriptContext->GetLibrary()->GetUndefined(); } return JavascriptString::NewCopySz(resolvedLocaleName, scriptContext); #endif } Var IntlEngineInterfaceExtensionObject::EntryIntl_ResolveLocaleBestFit(RecyclableObject* function, CallInfo callInfo, ...) { EngineInterfaceObject_CommonFunctionProlog(function, callInfo); if (args.Info.Count < 2 || !VarIs(args.Values[1])) { // NormalizeLanguageTag of undefined or non-string is undefined return scriptContext->GetLibrary()->GetUndefined(); } #if defined(INTL_ICU) AssertOrFailFastMsg(false, "Intl-ICU does not implement ResolveLocaleBestFit"); return nullptr; #else // !INTL_ICU JavascriptString *localeStrings = VarTo(args.Values[1]); PCWSTR passedLocale = localeStrings->GetSz(); DelayLoadWindowsGlobalization* wgl = scriptContext->GetThreadContext()->GetWindowsGlobalizationLibrary(); WindowsGlobalizationAdapter* wga = GetWindowsGlobalizationAdapter(scriptContext); AutoCOMPtr formatter; HRESULT hr; if (FAILED(hr = wga->CreateDateTimeFormatter(scriptContext, _u("longdate"), &passedLocale, 1, nullptr, nullptr, &formatter))) { HandleOOMSOEHR(hr); return scriptContext->GetLibrary()->GetUndefined(); } AutoHSTRING locale; if (FAILED(hr = wga->GetResolvedLanguage(formatter, &locale))) { HandleOOMSOEHR(hr); return scriptContext->GetLibrary()->GetUndefined(); } return JavascriptString::NewCopySz(wgl->WindowsGetStringRawBuffer(*locale, NULL), scriptContext); #endif } Var IntlEngineInterfaceExtensionObject::EntryIntl_GetDefaultLocale(RecyclableObject* function, CallInfo callInfo, ...) { EngineInterfaceObject_CommonFunctionProlog(function, callInfo); #ifdef INTL_WINGLOB char16 defaultLocale[LOCALE_NAME_MAX_LENGTH]; defaultLocale[0] = '\0'; if (GetUserDefaultLocaleName(defaultLocale, _countof(defaultLocale)) == 0) { JavascriptError::MapAndThrowError(scriptContext, HRESULT_FROM_WIN32(GetLastError())); } return JavascriptString::NewCopySz(defaultLocale, scriptContext); #else UErrorCode status = U_ZERO_ERROR; char defaultLangtag[ULOC_FULLNAME_CAPACITY] = { 0 }; char defaultLocaleID[ULOC_FULLNAME_CAPACITY] = { 0 }; int localeIDActual = uloc_getName(nullptr, defaultLocaleID, _countof(defaultLocaleID), &status); ICU_ASSERT(status, localeIDActual > 0 && localeIDActual < _countof(defaultLocaleID)); int langtagActual = uloc_toLanguageTag(defaultLocaleID, defaultLangtag, _countof(defaultLangtag), true, &status); ICU_ASSERT(status, langtagActual > 0 && langtagActual < _countof(defaultLangtag)); char16 *defaultLangtag16 = RecyclerNewArrayLeaf(scriptContext->GetRecycler(), char16, langtagActual + 1); charcount_t defaultLangtag16Actual = 0; utf8::NarrowStringToWideNoAlloc(defaultLangtag, static_cast(langtagActual), defaultLangtag16, langtagActual + 1, &defaultLangtag16Actual); AssertOrFailFastMsg(defaultLangtag16Actual == static_cast(langtagActual), "Language tags should always be ASCII"); return JavascriptString::NewWithBuffer(defaultLangtag16, defaultLangtag16Actual, scriptContext); #endif } Var IntlEngineInterfaceExtensionObject::EntryIntl_GetExtensions(RecyclableObject* function, CallInfo callInfo, ...) { EngineInterfaceObject_CommonFunctionProlog(function, callInfo); if (args.Info.Count < 2 || !VarIs(args.Values[1])) { // NormalizeLanguageTag of undefined or non-string is undefined return scriptContext->GetLibrary()->GetUndefined(); } #ifdef INTL_WINGLOB DelayLoadWindowsGlobalization* wgl = scriptContext->GetThreadContext()->GetWindowsGlobalizationLibrary(); WindowsGlobalizationAdapter* wga = GetWindowsGlobalizationAdapter(scriptContext); AutoCOMPtr language; AutoCOMPtr extensionSubtags; HRESULT hr; if (FAILED(hr = wga->CreateLanguage(scriptContext, VarTo(args.Values[1])->GetSz(), &language))) { HandleOOMSOEHR(hr); return scriptContext->GetLibrary()->GetUndefined(); } if (FAILED(hr = language->QueryInterface(__uuidof(ILanguageExtensionSubtags), reinterpret_cast(&extensionSubtags)))) { HandleOOMSOEHR(hr); return scriptContext->GetLibrary()->GetUndefined(); } Assert(extensionSubtags); AutoHSTRING singletonString; AutoCOMPtr> subtags; uint32 length; if (FAILED(hr = wgl->WindowsCreateString(_u("u"), 1, &singletonString)) || FAILED(hr = extensionSubtags->GetExtensionSubtags(*singletonString, &subtags)) || FAILED(subtags->get_Size(&length))) { HandleOOMSOEHR(hr); return scriptContext->GetLibrary()->GetUndefined(); } JavascriptArray *toReturn = scriptContext->GetLibrary()->CreateArray(length); for (uint32 i = 0; i < length; i++) { AutoHSTRING str; if (!FAILED(hr = wga->GetItemAt(subtags, i, &str))) { toReturn->SetItem(i, JavascriptString::NewCopySz(wgl->WindowsGetStringRawBuffer(*str, NULL), scriptContext), Js::PropertyOperationFlags::PropertyOperation_None); } else { HandleOOMSOEHR(hr); } } return toReturn; #else AssertOrFailFastMsg(false, "ICU should not be calling platform.getExtensions"); return nullptr; #endif } #ifdef INTL_ICU // This is used by both NumberFormat and PluralRules static void SetUNumberFormatDigitOptions(UNumberFormat *fmt, DynamicObject *state) { if (JavascriptOperators::HasProperty(state, PropertyIds::minimumSignificantDigits)) { unum_setAttribute(fmt, UNUM_SIGNIFICANT_DIGITS_USED, true); unum_setAttribute(fmt, UNUM_MIN_SIGNIFICANT_DIGITS, AssertIntegerProperty(state, PropertyIds::minimumSignificantDigits)); unum_setAttribute(fmt, UNUM_MAX_SIGNIFICANT_DIGITS, AssertIntegerProperty(state, PropertyIds::maximumSignificantDigits)); } else { unum_setAttribute(fmt, UNUM_MIN_INTEGER_DIGITS, AssertIntegerProperty(state, PropertyIds::minimumIntegerDigits)); unum_setAttribute(fmt, UNUM_MIN_FRACTION_DIGITS, AssertIntegerProperty(state, PropertyIds::minimumFractionDigits)); unum_setAttribute(fmt, UNUM_MAX_FRACTION_DIGITS, AssertIntegerProperty(state, PropertyIds::maximumFractionDigits)); } } #endif Var IntlEngineInterfaceExtensionObject::EntryIntl_CacheNumberFormat(RecyclableObject * function, CallInfo callInfo, ...) { EngineInterfaceObject_CommonFunctionProlog(function, callInfo); INTL_CHECK_ARGS(args.Info.Count == 2 && DynamicObject::IsBaseDynamicObject(args.Values[1])); #if defined(INTL_ICU) DynamicObject *state = UnsafeVarTo(args.Values[1]); // always AssertOrFailFast that the properties we need are there, because if they aren't, Intl.js isn't functioning correctly NumberFormatStyle style = AssertEnumProperty(state, PropertyIds::formatterToUse); UNumberFormatStyle unumStyle = UNUM_IGNORE; UErrorCode status = U_ZERO_ERROR; JavascriptString *currency = nullptr; JavascriptString *langtag = AssertStringProperty(state, PropertyIds::locale); char localeID[ULOC_FULLNAME_CAPACITY] = { 0 }; LangtagToLocaleID(langtag, localeID); if (style == NumberFormatStyle::Decimal) { unumStyle = UNUM_DECIMAL; } else if (style == NumberFormatStyle::Percent) { unumStyle = UNUM_PERCENT; } else if (style == NumberFormatStyle::Currency) { NumberFormatCurrencyDisplay nfcd = AssertEnumProperty(state, PropertyIds::currencyDisplayToUse); // TODO(jahorto): Investigate making our enum values equal to the corresponding UNumberFormatStyle values if (nfcd == NumberFormatCurrencyDisplay::Symbol) { unumStyle = UNUM_CURRENCY; } else if (nfcd == NumberFormatCurrencyDisplay::Code) { unumStyle = UNUM_CURRENCY_ISO; } else if (nfcd == NumberFormatCurrencyDisplay::Name) { unumStyle = UNUM_CURRENCY_PLURAL; } currency = AssertStringProperty(state, PropertyIds::currency); } AssertOrFailFast(unumStyle != UNUM_IGNORE); auto fmt = FinalizableUNumberFormat::New(scriptContext->GetRecycler(), unum_open(unumStyle, nullptr, 0, localeID, nullptr, &status)); ICU_ASSERT(status, true); bool groupingUsed = AssertBooleanProperty(state, PropertyIds::useGrouping); unum_setAttribute(*fmt, UNUM_GROUPING_USED, groupingUsed); unum_setAttribute(*fmt, UNUM_ROUNDING_MODE, UNUM_ROUND_HALFUP); SetUNumberFormatDigitOptions(*fmt, state); if (currency != nullptr) { unum_setTextAttribute(*fmt, UNUM_CURRENCY_CODE, reinterpret_cast(currency->GetSz()), currency->GetLength(), &status); ICU_ASSERT(status, true); } state->SetInternalProperty( InternalPropertyIds::CachedUNumberFormat, fmt, PropertyOperationFlags::PropertyOperation_None, nullptr ); return scriptContext->GetLibrary()->GetUndefined(); #else HRESULT hr = S_OK; JavascriptString *localeJSstr = nullptr; DynamicObject *options = VarTo(args.Values[1]); DelayLoadWindowsGlobalization* wgl = scriptContext->GetThreadContext()->GetWindowsGlobalizationLibrary(); WindowsGlobalizationAdapter* wga = GetWindowsGlobalizationAdapter(scriptContext); Var propertyValue; // Verify locale is present // REVIEW (doilij): Fix comparison of the unsigned value <= 0 if (!GetTypedPropertyBuiltInFrom(options, __locale, JavascriptString) || (localeJSstr = VarTo(propertyValue))->GetLength() <= 0) { // REVIEW (doilij): Should we throw? Or otherwise, from Intl.js, should detect something didn't work right here... return scriptContext->GetLibrary()->GetUndefined(); } //First we have to determine which formatter(number, percent, or currency) we will be using. //Note some options might not be present. AutoCOMPtr numberFormatter(nullptr); PCWSTR locale = localeJSstr->GetSz(); uint16 formatterToUseVal = 0; // 0 (default) is number, 1 is percent, 2 is currency if (GetTypedPropertyBuiltInFrom(options, __formatterToUse, TaggedInt) && (formatterToUseVal = TaggedInt::ToUInt16(propertyValue)) == 1) { //Use the percent formatter IfFailThrowHr(wga->CreatePercentFormatter(scriptContext, &locale, 1, &numberFormatter)); } else if (formatterToUseVal == 2) { //Use the currency formatter AutoCOMPtr currencyFormatter(nullptr); if (!GetTypedPropertyBuiltInFrom(options, __currency, JavascriptString)) { return scriptContext->GetLibrary()->GetUndefined(); } //API call retrieves a currency formatter, have to query its interface for numberFormatter IfFailThrowHr(GetWindowsGlobalizationAdapter(scriptContext)->CreateCurrencyFormatter(scriptContext, &locale, 1, VarTo(propertyValue)->GetSz(), ¤cyFormatter)); if (GetTypedPropertyBuiltInFrom(options, __currencyDisplayToUse, TaggedInt)) // 0 is for symbol, 1 is for code, 2 is for name. //Currently name isn't supported; so it will default to code in that case. { AutoCOMPtr currencyFormatter2(nullptr); IfFailThrowHr(currencyFormatter->QueryInterface(__uuidof(NumberFormatting::ICurrencyFormatter2), reinterpret_cast(¤cyFormatter2))); if (TaggedInt::ToUInt16(propertyValue) == 0) { IfFailThrowHr(currencyFormatter2->put_Mode(NumberFormatting::CurrencyFormatterMode::CurrencyFormatterMode_UseSymbol)); } else { IfFailThrowHr(currencyFormatter2->put_Mode(NumberFormatting::CurrencyFormatterMode::CurrencyFormatterMode_UseCurrencyCode)); } } IfFailThrowHr(currencyFormatter->QueryInterface(__uuidof(NumberFormatting::INumberFormatter), reinterpret_cast(&numberFormatter))); } else { //Use the number formatter (default) IfFailThrowHr(wga->CreateNumberFormatter(scriptContext, &locale, 1, &numberFormatter)); } Assert(numberFormatter); AutoCOMPtr signedZeroOption(nullptr); IfFailThrowHr(numberFormatter->QueryInterface(__uuidof(NumberFormatting::ISignedZeroOption), reinterpret_cast(&signedZeroOption))); IfFailThrowHr(signedZeroOption->put_IsZeroSigned(true)); //Configure non-digit related options AutoCOMPtr numberFormatterOptions(nullptr); IfFailThrowHr(numberFormatter->QueryInterface(__uuidof(NumberFormatting::INumberFormatterOptions), reinterpret_cast(&numberFormatterOptions))); Assert(numberFormatterOptions); if (GetTypedPropertyBuiltInFrom(options, __useGrouping, JavascriptBoolean)) { IfFailThrowHr(numberFormatterOptions->put_IsGrouped((boolean)(VarTo(propertyValue)->GetValue()))); } //Get the numeral system and add it to the object since it will be located in the locale AutoHSTRING hNumeralSystem; AutoHSTRING hResolvedLanguage; uint32 length; IfFailThrowHr(wga->GetNumeralSystem(numberFormatterOptions, &hNumeralSystem)); SetHSTRINGPropertyBuiltInOn(options, __numberingSystem, *hNumeralSystem); IfFailThrowHr(wga->GetResolvedLanguage(numberFormatterOptions, &hResolvedLanguage)); SetHSTRINGPropertyBuiltInOn(options, __locale, *hResolvedLanguage); AutoCOMPtr rounderOptions(nullptr); IfFailThrowHr(numberFormatter->QueryInterface(__uuidof(NumberFormatting::INumberRounderOption), reinterpret_cast(&rounderOptions))); Assert(rounderOptions); if (HasPropertyBuiltInOn(options, __minimumSignificantDigits) || HasPropertyBuiltInOn(options, __maximumSignificantDigits)) { uint16 minSignificantDigits = 1, maxSignificantDigits = 21; //Do significant digit rounding if (GetTypedPropertyBuiltInFrom(options, __minimumSignificantDigits, TaggedInt)) { minSignificantDigits = max(min(TaggedInt::ToUInt16(propertyValue), 21), 1); } if (GetTypedPropertyBuiltInFrom(options, __maximumSignificantDigits, TaggedInt)) { maxSignificantDigits = max(min(TaggedInt::ToUInt16(propertyValue), 21), minSignificantDigits); } prepareWithSignificantDigits(scriptContext, rounderOptions, numberFormatter, numberFormatterOptions, minSignificantDigits, maxSignificantDigits); } else { uint16 minFractionDigits = 0, maxFractionDigits = 3, minIntegerDigits = 1; //Do fraction/integer digit rounding if (GetTypedPropertyBuiltInFrom(options, __minimumIntegerDigits, TaggedInt)) { minIntegerDigits = max(min(TaggedInt::ToUInt16(propertyValue), 21), 1); } if (GetTypedPropertyBuiltInFrom(options, __minimumFractionDigits, TaggedInt)) { minFractionDigits = min(TaggedInt::ToUInt16(propertyValue), 20);//ToUInt16 will get rid of negatives by making them high } if (GetTypedPropertyBuiltInFrom(options, __maximumFractionDigits, TaggedInt)) { maxFractionDigits = max(min(TaggedInt::ToUInt16(propertyValue), 20), minFractionDigits);//ToUInt16 will get rid of negatives by making them high } prepareWithFractionIntegerDigits(scriptContext, rounderOptions, numberFormatterOptions, minFractionDigits, maxFractionDigits + (formatterToUseVal == 1 ? 2 : 0), minIntegerDigits);//extend max fractions for percent } //Set the object as a cache numberFormatter->AddRef(); options->SetInternalProperty(Js::InternalPropertyIds::HiddenObject, AutoCOMJSObject::New(scriptContext->GetRecycler(), numberFormatter), Js::PropertyOperationFlags::PropertyOperation_None, NULL); return scriptContext->GetLibrary()->GetUndefined(); #endif } // Unlike CacheNumberFormat; this call takes an additional parameter to specify whether we are going to cache it. // We have to create this formatter twice; first time get the date/time patterns; and second time cache with correct format string. Var IntlEngineInterfaceExtensionObject::EntryIntl_CreateDateTimeFormat(RecyclableObject * function, CallInfo callInfo, ...) { EngineInterfaceObject_CommonFunctionProlog(function, callInfo); if (args.Info.Count < 3 || !DynamicObject::IsBaseDynamicObject(args.Values[1]) || !VarIs(args.Values[2])) { return scriptContext->GetLibrary()->GetUndefined(); } #ifdef INTL_WINGLOB DynamicObject* obj = VarTo(args.Values[1]); DelayLoadWindowsGlobalization* wgl = scriptContext->GetThreadContext()->GetWindowsGlobalizationLibrary(); WindowsGlobalizationAdapter* wga = GetWindowsGlobalizationAdapter(scriptContext); HRESULT hr; Var propertyValue = nullptr; uint32 length; PCWSTR locale = GetTypedPropertyBuiltInFrom(obj, __locale, JavascriptString) ? VarTo(propertyValue)->GetSz() : nullptr; PCWSTR templateString = GetTypedPropertyBuiltInFrom(obj, __templateString, JavascriptString) ? VarTo(propertyValue)->GetSz() : nullptr; if (locale == nullptr || templateString == nullptr) { AssertMsg(false, "For some reason, locale and templateString aren't defined or aren't a JavascriptString."); return scriptContext->GetLibrary()->GetUndefined(); } PCWSTR clock = GetTypedPropertyBuiltInFrom(obj, __windowsClock, JavascriptString) ? VarTo(propertyValue)->GetSz() : nullptr; AutoHSTRING hDummyCalendar; if (clock != nullptr) { //Because both calendar and clock are needed to pass into the datetimeformatter constructor (or neither); create a dummy one to get the value of calendar out so clock can be passed in with it. AutoCOMPtr dummyFormatter; IfFailThrowHr(wga->CreateDateTimeFormatter(scriptContext, templateString, &locale, 1, nullptr, nullptr, &dummyFormatter)); IfFailThrowHr(wga->GetCalendar(dummyFormatter, &hDummyCalendar)); } //Now create the real formatter. AutoCOMPtr cachedFormatter; IfFailThrowHr(wga->CreateDateTimeFormatter(scriptContext, templateString, &locale, 1, clock == nullptr ? nullptr : wgl->WindowsGetStringRawBuffer(*hDummyCalendar, &length), clock, &cachedFormatter)); AutoHSTRING hCalendar; AutoHSTRING hClock; AutoHSTRING hLocale; AutoHSTRING hNumberingSystem; //In case the upper code path wasn't hit; extract the calendar string again so it can be set. IfFailThrowHr(wga->GetCalendar(cachedFormatter, &hCalendar)); SetHSTRINGPropertyBuiltInOn(obj, __windowsCalendar, *hCalendar); IfFailThrowHr(wga->GetClock(cachedFormatter, &hClock)); SetHSTRINGPropertyBuiltInOn(obj, __windowsClock, *hClock); IfFailThrowHr(wga->GetResolvedLanguage(cachedFormatter, &hLocale)); SetHSTRINGPropertyBuiltInOn(obj, __locale, *hLocale); //Get the numbering system IfFailThrowHr(wga->GetNumeralSystem(cachedFormatter, &hNumberingSystem)); SetHSTRINGPropertyBuiltInOn(obj, __numberingSystem, *hNumberingSystem); //Extract the pattern strings AutoCOMPtr> dateResult; IfFailThrowHr(cachedFormatter->get_Patterns(&dateResult)); IfFailThrowHr(dateResult->get_Size(&length)); JavascriptArray *patternStrings = scriptContext->GetLibrary()->CreateArray(length); for (uint32 i = 0; i < length; i++) { AutoHSTRING item; IfFailThrowHr(wga->GetItemAt(dateResult, i, &item)); patternStrings->SetItem(i, Js::JavascriptString::NewCopySz(wgl->WindowsGetStringRawBuffer(*item, NULL), scriptContext), PropertyOperation_None); } SetPropertyBuiltInOn(obj, __patternStrings, patternStrings); //This parameter tells us whether we are caching it this time around; or just validating pattern strings if ((boolean)(VarTo(args.Values[2])->GetValue())) { //If timeZone is undefined; then use the standard dateTimeFormatter to format in local time; otherwise use the IDateTimeFormatter2 to format using specified timezone (UTC) if (!GetPropertyBuiltInFrom(obj, __timeZone) || JavascriptOperators::IsUndefinedObject(propertyValue)) { cachedFormatter->AddRef(); obj->SetInternalProperty(Js::InternalPropertyIds::HiddenObject, AutoCOMJSObject::New(scriptContext->GetRecycler(), cachedFormatter), Js::PropertyOperationFlags::PropertyOperation_None, NULL); } else { AutoCOMPtr tzCachedFormatter; IfFailThrowHr(cachedFormatter->QueryInterface(__uuidof(DateTimeFormatting::IDateTimeFormatter2), reinterpret_cast(&tzCachedFormatter))); tzCachedFormatter->AddRef(); //Set the object as a cache obj->SetInternalProperty(Js::InternalPropertyIds::HiddenObject, AutoCOMJSObject::New(scriptContext->GetRecycler(), tzCachedFormatter), Js::PropertyOperationFlags::PropertyOperation_None, NULL); } } return scriptContext->GetLibrary()->GetUndefined(); #else // TODO (doilij): implement INTL_ICU version #ifdef INTL_ICU_DEBUG Output::Print(_u("EntryIntl_CreateDateTimeFormat > returning null, fallback to JS\n")); #endif return scriptContext->GetLibrary()->GetNull(); #endif } Var IntlEngineInterfaceExtensionObject::EntryIntl_LocaleCompare(RecyclableObject* function, CallInfo callInfo, ...) { #ifdef INTL_WINGLOB AssertOrFailFastMsg(false, "platform.localeCompare should not be called in Intl-WinGlob"); return nullptr; #else EngineInterfaceObject_CommonFunctionProlog(function, callInfo); INTL_CHECK_ARGS( args.Info.Count == 5 && VarIs(args[1]) && VarIs(args[2]) && DynamicObject::IsBaseDynamicObject(args[3]) && VarIs(args[4]) ); JavascriptString *left = UnsafeVarTo(args[1]); JavascriptString *right = UnsafeVarTo(args[2]); DynamicObject *state = UnsafeVarTo(args[3]); bool forStringPrototypeLocaleCompare = UnsafeVarTo(args[4])->GetValue(); if (forStringPrototypeLocaleCompare) { CHAKRATEL_LANGSTATS_INC_BUILTINCOUNT(String_Prototype_localeCompare); INTL_TRACE("Calling '%s'.localeCompare('%s', ...)", left->GetSz(), right->GetSz()); } else { CHAKRATEL_LANGSTATS_INC_BUILTINCOUNT(Collator_Prototype_compare); INTL_TRACE("Calling Collator.prototype.compare('%s', '%s')", left->GetSz(), right->GetSz()); } // Below, we lazy-initialize the backing UCollator on the first call to localeCompare // On subsequent calls, the UCollator will be cached in state.CachedUCollator Var cachedUCollator = nullptr; FinalizableUCollator *coll = nullptr; UErrorCode status = U_ZERO_ERROR; if (state->GetInternalProperty(state, InternalPropertyIds::CachedUCollator, &cachedUCollator, nullptr, scriptContext)) { coll = reinterpret_cast(cachedUCollator); INTL_TRACE("Using previously cached UCollator (0x%x)", coll); } else { // the object key is locale according to Intl spec, but its more accurately a BCP47 Language Tag, not an ICU LocaleID JavascriptString *langtag = AssertStringProperty(state, PropertyIds::locale); CollatorSensitivity sensitivity = AssertEnumProperty(state, PropertyIds::sensitivityEnum); bool ignorePunctuation = AssertBooleanProperty(state, PropertyIds::ignorePunctuation); bool numeric = AssertBooleanProperty(state, PropertyIds::numeric); CollatorCaseFirst caseFirst = AssertEnumProperty(state, PropertyIds::caseFirstEnum); JavascriptString *usage = AssertStringProperty(state, PropertyIds::usage); char localeID[ULOC_FULLNAME_CAPACITY] = { 0 }; LangtagToLocaleID(langtag, localeID); const char16 searchString[] = _u("search"); if (usage->BufferEquals(searchString, _countof(searchString) - 1)) // minus the null terminator { uloc_setKeywordValue("collation", "search", localeID, _countof(localeID), &status); ICU_ASSERT(status, true); } coll = FinalizableUCollator::New(scriptContext->GetRecycler(), ucol_open(localeID, &status)); ICU_ASSERT(status, true); // REVIEW(jahorto): Anything that requires a status will no-op if its in a failure state // Thus, we can ICU_ASSERT the status once after all of the properties are set for simplicity if (sensitivity == CollatorSensitivity::Base) { ucol_setStrength(*coll, UCOL_PRIMARY); } else if (sensitivity == CollatorSensitivity::Accent) { ucol_setStrength(*coll, UCOL_SECONDARY); } else if (sensitivity == CollatorSensitivity::Case) { // see "description" for the caseLevel default option: http://userguide.icu-project.org/collation/customization ucol_setStrength(*coll, UCOL_PRIMARY); ucol_setAttribute(*coll, UCOL_CASE_LEVEL, UCOL_ON, &status); } else if (sensitivity == CollatorSensitivity::Variant) { ucol_setStrength(*coll, UCOL_TERTIARY); } if (ignorePunctuation) { // see http://userguide.icu-project.org/collation/customization/ignorepunct ucol_setAttribute(*coll, UCOL_ALTERNATE_HANDLING, UCOL_SHIFTED, &status); } if (numeric) { ucol_setAttribute(*coll, UCOL_NUMERIC_COLLATION, UCOL_ON, &status); } if (caseFirst == CollatorCaseFirst::Upper) { ucol_setAttribute(*coll, UCOL_CASE_FIRST, UCOL_UPPER_FIRST, &status); } else if (caseFirst == CollatorCaseFirst::Lower) { ucol_setAttribute(*coll, UCOL_CASE_FIRST, UCOL_LOWER_FIRST, &status); } // Ensure that collator configuration was successfull ICU_ASSERT(status, true); INTL_TRACE( "Caching UCollator (0x%x) with langtag = %s, sensitivity = %d, caseFirst = %d, ignorePunctuation = %d, and numeric = %d", coll, langtag->GetSz(), sensitivity, caseFirst, ignorePunctuation, numeric ); // cache coll for later use (so that the condition that brought us here returns true for future calls) state->SetInternalProperty( InternalPropertyIds::CachedUCollator, coll, PropertyOperationFlags::PropertyOperation_None, nullptr ); } // As of ES2015, String.prototype.localeCompare must compare canonically equivalent strings as equal BEGIN_TEMP_ALLOCATOR(tempAllocator, scriptContext, _u("EntryIntl_LocaleCompare")); const char16 *leftNormalized = nullptr; charcount_t leftNormalizedLength = 0; if (UnicodeText::IsNormalizedString(UnicodeText::NormalizationForm::C, left->GetSz(), left->GetLength())) { leftNormalized = left->GetSz(); leftNormalizedLength = left ->GetLength(); } else { leftNormalized = left->GetNormalizedString(UnicodeText::NormalizationForm::C, tempAllocator, leftNormalizedLength); } const char16 *rightNormalized = nullptr; charcount_t rightNormalizedLength = 0; if (UnicodeText::IsNormalizedString(UnicodeText::NormalizationForm::C, right->GetSz(), right->GetLength())) { rightNormalized = right->GetSz(); rightNormalizedLength = right->GetLength(); } else { rightNormalized = right->GetNormalizedString(UnicodeText::NormalizationForm::C, tempAllocator, rightNormalizedLength); } static_assert(UCOL_LESS == -1 && UCOL_EQUAL == 0 && UCOL_GREATER == 1, "ucol_strcoll should return values compatible with localeCompare"); Var ret = JavascriptNumber::ToVar(ucol_strcoll( *coll, reinterpret_cast(leftNormalized), leftNormalizedLength, reinterpret_cast(rightNormalized), rightNormalizedLength ), scriptContext); END_TEMP_ALLOCATOR(tempAllocator, scriptContext); return ret; #endif } #ifdef INTL_WINGLOB static DWORD GetCompareStringComparisonFlags(CollatorSensitivity sensitivity, bool ignorePunctuation, bool numeric) { DWORD flags = 0; if (sensitivity == CollatorSensitivity::Base) { flags |= LINGUISTIC_IGNOREDIACRITIC | LINGUISTIC_IGNORECASE | NORM_IGNOREKANATYPE | NORM_IGNOREWIDTH; } else if (sensitivity == CollatorSensitivity::Accent) { flags |= LINGUISTIC_IGNORECASE | NORM_IGNOREKANATYPE | NORM_IGNOREWIDTH; } else if (sensitivity == CollatorSensitivity::Case) { flags |= NORM_IGNOREKANATYPE | NORM_IGNOREWIDTH | LINGUISTIC_IGNOREDIACRITIC; } else if (sensitivity == CollatorSensitivity::Variant) { flags |= NORM_LINGUISTIC_CASING; } if (ignorePunctuation) { flags |= NORM_IGNORESYMBOLS; } if (numeric) { flags |= SORT_DIGITSASNUMBERS; } return flags; } #endif Var IntlEngineInterfaceExtensionObject::EntryIntl_CompareString(RecyclableObject* function, CallInfo callInfo, ...) { #ifdef INTL_ICU AssertOrFailFastMsg(false, "EntryIntl_CompareString should not be called in Intl-ICU"); return nullptr; #else EngineInterfaceObject_CommonFunctionProlog(function, callInfo); INTL_CHECK_ARGS(args.Info.Count >= 3 && VarIs(args[1]) && VarIs(args[2])); const char16 *locale = nullptr; // args[3] char16 defaultLocale[LOCALE_NAME_MAX_LENGTH] = { 0 }; CollatorSensitivity sensitivity = CollatorSensitivity::Default; // args[4] bool ignorePunctuation = false; // args[5] bool numeric = false; // args[6] JavascriptString *str1 = VarTo(args.Values[1]); JavascriptString *str2 = VarTo(args.Values[2]); CollatorCaseFirst caseFirst = CollatorCaseFirst::Default; // args[7] // we only need to parse arguments 3 through 7 if locale and options are provided // see fast path in JavascriptString::EntryLocaleCompare if (args.Info.Count > 3) { if (args.Info.Count < 8) { JavascriptError::MapAndThrowError(scriptContext, E_INVALIDARG); } if (!JavascriptOperators::IsUndefinedObject(args.Values[3]) && VarIs(args.Values[3])) { locale = VarTo(args.Values[3])->GetSz(); } else { JavascriptError::MapAndThrowError(scriptContext, E_INVALIDARG); } if (!JavascriptOperators::IsUndefinedObject(args.Values[4]) && TaggedInt::Is(args.Values[4])) { sensitivity = static_cast(TaggedInt::ToUInt16(args.Values[4])); } if (!JavascriptOperators::IsUndefinedObject(args.Values[5]) && VarIs(args.Values[5])) { ignorePunctuation = (VarTo(args.Values[5])->GetValue() != 0); } if (!JavascriptOperators::IsUndefinedObject(args.Values[6]) && VarIs(args.Values[6])) { numeric = (VarTo(args.Values[6])->GetValue() != 0); } if (!JavascriptOperators::IsUndefinedObject(args.Values[7]) && TaggedInt::Is(args.Values[7])) { caseFirst = static_cast(TaggedInt::ToUInt16(args.Values[7])); } } else { if (GetUserDefaultLocaleName(defaultLocale, _countof(defaultLocale)) != 0) { locale = defaultLocale; } else { JavascriptError::MapAndThrowError(scriptContext, HRESULT_FROM_WIN32(GetLastError())); } } Assert(locale != nullptr); Assert((int)sensitivity >= 0 && sensitivity < CollatorSensitivity::Max); Assert((int)caseFirst >= 0 && caseFirst < CollatorCaseFirst::Max); BEGIN_TEMP_ALLOCATOR(tempAllocator, scriptContext, _u("EntryIntl_CompareString")); const char16 *left = nullptr; charcount_t leftLen = 0; if (UnicodeText::IsNormalizedString(UnicodeText::NormalizationForm::C, str1->GetSz(), str1->GetLength())) { left = str1->GetSz(); leftLen = str1->GetLength(); } else { left = str1->GetNormalizedString(UnicodeText::NormalizationForm::C, tempAllocator, leftLen); } const char16 *right = nullptr; charcount_t rightLen = 0; if (UnicodeText::IsNormalizedString(UnicodeText::NormalizationForm::C, str2->GetSz(), str2->GetLength())) { right = str2->GetSz(); rightLen = str2->GetLength(); } else { right = str2->GetNormalizedString(UnicodeText::NormalizationForm::C, tempAllocator, rightLen); } // CompareStringEx on Windows returns 0 for error, 1 if less, 2 if equal, 3 if greater // Default to the strings being equal, because sorting with == causes no change in the order but converges, whereas < would cause an infinite loop. int compareResult = 2; HRESULT error = S_OK; DWORD comparisonFlags = GetCompareStringComparisonFlags(sensitivity, ignorePunctuation, numeric); compareResult = CompareStringEx(locale, comparisonFlags, left, leftLen, right, rightLen, NULL, NULL, 0); error = HRESULT_FROM_WIN32(GetLastError()); END_TEMP_ALLOCATOR(tempAllocator, scriptContext); if (compareResult == 0) { JavascriptError::MapAndThrowError(scriptContext, error); } return JavascriptNumber::ToVar(compareResult - 2, scriptContext); #endif } Var IntlEngineInterfaceExtensionObject::EntryIntl_CurrencyDigits(RecyclableObject* function, CallInfo callInfo, ...) { EngineInterfaceObject_CommonFunctionProlog(function, callInfo); INTL_CHECK_ARGS( args.Info.Count == 2 && VarIs(args.Values[1]) ); const char16 *currencyCode = UnsafeVarTo(args.Values[1])->GetSz(); #if defined(INTL_ICU) UErrorCode status = U_ZERO_ERROR; ScopedUNumberFormat fmt(unum_open(UNUM_CURRENCY, nullptr, 0, nullptr, nullptr, &status)); unum_setTextAttribute(fmt, UNUM_CURRENCY_CODE, reinterpret_cast(currencyCode), -1, &status); ICU_ASSERT(status, true); int currencyDigits = unum_getAttribute(fmt, UNUM_FRACTION_DIGITS); return JavascriptNumber::ToVar(currencyDigits, scriptContext); #else HRESULT hr; AutoCOMPtr currencyFormatter(nullptr); IfFailThrowHr(GetWindowsGlobalizationAdapter(scriptContext)->CreateCurrencyFormatterCode(scriptContext, currencyCode, ¤cyFormatter)); AutoCOMPtr numberFormatterOptions; IfFailThrowHr(currencyFormatter->QueryInterface(__uuidof(NumberFormatting::INumberFormatterOptions), reinterpret_cast(&numberFormatterOptions))); Assert(numberFormatterOptions); INT32 fractionDigits; IfFailThrowHr(numberFormatterOptions->get_FractionDigits(&fractionDigits)); return JavascriptNumber::ToVar(fractionDigits, scriptContext); #endif } #ifdef INTL_WINGLOB //Helper, this just prepares based on fraction and integer format options void IntlEngineInterfaceExtensionObject::prepareWithFractionIntegerDigits(ScriptContext* scriptContext, NumberFormatting::INumberRounderOption* rounderOptions, NumberFormatting::INumberFormatterOptions* formatterOptions, uint16 minFractionDigits, uint16 maxFractionDigits, uint16 minIntegerDigits) { HRESULT hr; WindowsGlobalizationAdapter* wga = GetWindowsGlobalizationAdapter(scriptContext); AutoCOMPtr numberRounder(nullptr); AutoCOMPtr incrementNumberRounder(nullptr); IfFailThrowHr(wga->CreateIncrementNumberRounder(scriptContext, &numberRounder)); IfFailThrowHr(numberRounder->QueryInterface(__uuidof(NumberFormatting::IIncrementNumberRounder), reinterpret_cast(&incrementNumberRounder))); Assert(incrementNumberRounder); IfFailThrowHr(incrementNumberRounder->put_RoundingAlgorithm(Windows::Globalization::NumberFormatting::RoundingAlgorithm::RoundingAlgorithm_RoundHalfAwayFromZero)); IfFailThrowHr(incrementNumberRounder->put_Increment(pow(10.0, -maxFractionDigits))); IfFailThrowHr(rounderOptions->put_NumberRounder(numberRounder)); IfFailThrowHr(formatterOptions->put_FractionDigits(minFractionDigits)); IfFailThrowHr(formatterOptions->put_IntegerDigits(minIntegerDigits)); } //Helper, this just prepares based on significant digits format options void IntlEngineInterfaceExtensionObject::prepareWithSignificantDigits(ScriptContext* scriptContext, NumberFormatting::INumberRounderOption* rounderOptions, NumberFormatting::INumberFormatter *numberFormatter, NumberFormatting::INumberFormatterOptions* formatterOptions, uint16 minSignificantDigits, uint16 maxSignificantDigits) { HRESULT hr; WindowsGlobalizationAdapter* wga = GetWindowsGlobalizationAdapter(scriptContext); AutoCOMPtr numberRounder(nullptr); AutoCOMPtr incrementNumberRounder(nullptr); AutoCOMPtr significantDigitsOptions(nullptr); IfFailThrowHr(wga->CreateSignificantDigitsRounder(scriptContext, &numberRounder)); IfFailThrowHr(numberRounder->QueryInterface(__uuidof(NumberFormatting::ISignificantDigitsNumberRounder), reinterpret_cast(&incrementNumberRounder))); Assert(incrementNumberRounder); IfFailThrowHr(incrementNumberRounder->put_RoundingAlgorithm(Windows::Globalization::NumberFormatting::RoundingAlgorithm::RoundingAlgorithm_RoundHalfAwayFromZero)); IfFailThrowHr(incrementNumberRounder->put_SignificantDigits(maxSignificantDigits)); IfFailThrowHr(rounderOptions->put_NumberRounder(numberRounder)); IfFailThrowHr(numberFormatter->QueryInterface(__uuidof(NumberFormatting::ISignificantDigitsOption), reinterpret_cast(&significantDigitsOptions))); IfFailThrowHr(significantDigitsOptions->put_SignificantDigits(minSignificantDigits)); Assert(significantDigitsOptions); //Clear minimum fraction digits as in the case of percent 2 is supplied IfFailThrowHr(formatterOptions->put_FractionDigits(0)); } #endif #ifdef INTL_ICU // Rationale for this data structure: ICU reports back a tree of parts where each node in the tree // has a type and a width, and there must be at least one node corresponding to each character in the tree // (nodes can be wider than one character). Nodes can have children, and the parent-child relationship is // that child node represents a more specific type for a given character range than the parent. Since ICU // doesn't ever report "literal" parts of strings (like spaces or other extra characters), the root node in // the tree will always be the entire width of the string with the type UnsetField, and we later map UnsetField // to the "literal" part. Then, for a string like "US$ 1,000", there will be two child nodes, one of type // currency with width [0, 3) and one of type integer with width [4, 9). The integer node will have a child // of type group and width [6, 7). So the most specific type for characters 0 to 3 is currency, 3 to 4 is unset // (literal), 4 to 5 is integer, 5 to 6 is group, and 6 to 9 is integer. This linear scan across the string, // determining the part of consecutive spans of characters, is what the NumberFormat.prototype.formatToParts // API needs to return to the user. // // I thought about this a bunch and I didn't like the idea of traversing an actual tree structure to get that // information because it felt awkward to encode the "width" of nodes with specific meaning during traveral. // So, I came up with an array structure where basically when we are told a part exists from position x to y, // we can figure out what type used to apply to that span and update that section of the array with the new type. // We skip over sections of the span [x, y) that have a type that doesn't match the start and end because that // means we have already gotten a more specific part for that sub-span (for instance if we got a grouping // separator before it's parent integer) class NumberFormatPartsBuilder { private: double num; Field(const char16 *) formatted; const charcount_t formattedLength; Field(ScriptContext *) sc; Field(UNumberFormatFields *) fields; static const uint8_t UnsetField = 0xFF; JavascriptString *GetPartTypeString(UNumberFormatFields field) { JavascriptLibrary *library = sc->GetLibrary(); // this is outside the switch because MSVC doesn't like that UnsetField is not a valid enum value if ((static_cast(field) & 0xFF) == UnsetField) { return library->GetIntlLiteralPartString(); } switch (field) { case UNUM_INTEGER_FIELD: { if (JavascriptNumber::IsNan(num)) { return library->GetIntlNanPartString(); } else if (!NumberUtilities::IsFinite(num)) { return library->GetIntlInfinityPartString(); } else { return library->GetIntlIntegerPartString(); } } case UNUM_FRACTION_FIELD: return library->GetIntlFractionPartString(); case UNUM_DECIMAL_SEPARATOR_FIELD: return library->GetIntlDecimalPartString(); // The following three should only show up if UNUM_SCIENTIFIC is used, which Intl.NumberFormat doesn't currently use case UNUM_EXPONENT_SYMBOL_FIELD: AssertOrFailFastMsg(false, "Unexpected exponent symbol field"); case UNUM_EXPONENT_SIGN_FIELD: AssertOrFailFastMsg(false, "Unexpected exponent sign field"); case UNUM_EXPONENT_FIELD: AssertOrFailFastMsg(false, "Unexpected exponent field"); case UNUM_GROUPING_SEPARATOR_FIELD: return library->GetIntlGroupPartString(); case UNUM_CURRENCY_FIELD: return library->GetIntlCurrencyPartString(); case UNUM_PERCENT_FIELD: return library->GetIntlPercentPartString(); // TODO(jahorto): Determine if this would ever be returned and what it would map to case UNUM_PERMILL_FIELD: AssertOrFailFastMsg(false, "Unexpected permill field"); case UNUM_SIGN_FIELD: return num < 0 ? library->GetIntlMinusSignPartString() : library->GetIntlPlusSignPartString(); // At the ECMA-402 TC39 call for May 2017, it was decided that we should treat unmapped parts as type: "unknown" default: return library->GetIntlUnknownPartString(); } } public: NumberFormatPartsBuilder(double num, const char16 *formatted, charcount_t formattedLength, ScriptContext *scriptContext) : num(num) , formatted(formatted) , formattedLength(formattedLength) , sc(scriptContext) , fields(RecyclerNewArrayLeaf(sc->GetRecycler(), UNumberFormatFields, formattedLength)) { // this will allow InsertPart to tell what fields have been initialized or not, and will // be the value of resulting { type: "literal" } fields in ToPartsArray memset(fields, UnsetField, sizeof(UNumberFormatFields) * formattedLength); } // Note -- ICU reports fields as inclusive start, exclusive end, so thats how we handle the parameters here void InsertPart(UNumberFormatFields field, int start, int end) { AssertOrFailFast(start >= 0); AssertOrFailFast(start < end); // the asserts above mean the cast to charcount_t is safe charcount_t ccStart = static_cast(start); charcount_t ccEnd = static_cast(end); AssertOrFailFast(ccEnd <= formattedLength); // Make sure that the part does not overlap/is a strict subset or superset of other fields // The part builder could probably be built and tested to handle this structure not being so rigid, // but its safer for now to make sure this is true. UNumberFormatFields parentStartField = fields[ccStart]; UNumberFormatFields parentEndField = fields[ccEnd - 1]; AssertOrFailFast(parentStartField == parentEndField); // Actually insert the part now // Only overwrite fields in the span that match start and end, since fields that don't match are sub-parts that were already inserted for (charcount_t i = ccStart; i < ccEnd; ++i) { if (fields[i] == parentStartField) { fields[i] = field; } } } JavascriptArray *ToPartsArray() { JavascriptArray *ret = sc->GetLibrary()->CreateArray(0); int retIndex = 0; // With the structure of the fields array, creating the resulting parts list to return to JS is simple // Map set parts to the corresponding `type` value (see ECMA-402 #sec-partitionnumberpattern), and unset parts to the `literal` type for (charcount_t start = 0; start < formattedLength; ++retIndex) { // At the top of the loop, fields[start] should always be different from fields[start - 1] (we should be at the start of a new part), // since start is adjusted later in the loop to the end of the part AssertOrFailFast(start == 0 || fields[start - 1] != fields[start]); charcount_t end = start + 1; while (end < formattedLength && fields[end] == fields[start]) { ++end; } JavascriptString *partType = GetPartTypeString(fields[start]); JavascriptString *partValue = JavascriptString::NewCopyBuffer(formatted + start, end - start, sc); DynamicObject* part = sc->GetLibrary()->CreateObject(); JavascriptOperators::InitProperty(part, PropertyIds::type, partType); JavascriptOperators::InitProperty(part, PropertyIds::value, partValue); ret->SetItem(retIndex, part, PropertyOperationFlags::PropertyOperation_None); start = end; } return ret; } }; #endif Var IntlEngineInterfaceExtensionObject::EntryIntl_FormatNumber(RecyclableObject* function, CallInfo callInfo, ...) { EngineInterfaceObject_CommonFunctionProlog(function, callInfo); #if defined(INTL_ICU) INTL_CHECK_ARGS( args.Info.Count == 5 && (TaggedInt::Is(args[1]) || JavascriptNumber::Is(args[1])) && DynamicObject::IsBaseDynamicObject(args[2]) && VarIs(args[3]) && VarIs(args[4]) ); double num = JavascriptConversion::ToNumber(args[1], scriptContext); DynamicObject *state = UnsafeVarTo(args[2]); bool toParts = UnsafeVarTo(args[3])->GetValue(); bool forNumberPrototypeToLocaleString = UnsafeVarTo(args[4])->GetValue(); Var cachedUNumberFormat = nullptr; // cached by EntryIntl_CacheNumberFormat AssertOrFailFast(state->GetInternalProperty(state, InternalPropertyIds::CachedUNumberFormat, &cachedUNumberFormat, NULL, scriptContext)); if (forNumberPrototypeToLocaleString) { CHAKRATEL_LANGSTATS_INC_BUILTINCOUNT(Number_Prototype_toLocaleString); INTL_TRACE("Calling %f.toLocaleString(...)", num); } else if (toParts) { CHAKRATEL_LANGSTATS_INC_BUILTINCOUNT(NumberFormat_Prototype_formatToParts); INTL_TRACE("Calling NumberFormat.prototype.formatToParts(%f)", num); } else { CHAKRATEL_LANGSTATS_INC_BUILTINCOUNT(NumberFormat_Prototype_format); INTL_TRACE("Calling NumberFormat.prototype.format(%f)", num); } auto fmt = static_cast(cachedUNumberFormat); char16 *formatted = nullptr; int formattedLen = 0; if (!toParts) { EnsureBuffer([&](UChar *buf, int bufLen, UErrorCode *status) { return unum_formatDouble(*fmt, num, buf, bufLen, nullptr, status); }, scriptContext->GetRecycler(), &formatted, &formattedLen); return JavascriptString::NewWithBuffer(formatted, formattedLen, scriptContext); } // windows-icu does not define `ICU_VERSION` but does support `unum_formatDoubleForFields` (ICU 61+) since Windows 10 1809 "Redstone 5" (`NTDDI_WIN10_RS5`). #if !defined(ICU_VERSION) || ICU_VERSION >= 61 UErrorCode status = U_ZERO_ERROR; ScopedUFieldPositionIterator fpi(ufieldpositer_open(&status)); EnsureBuffer([&](UChar *buf, int bufLen, UErrorCode *status) { return unum_formatDoubleForFields(*fmt, num, buf, bufLen, fpi, status); }, scriptContext->GetRecycler(), &formatted, &formattedLen); NumberFormatPartsBuilder nfpb(num, formatted, formattedLen, scriptContext); int partStart = 0; int partEnd = 0; int i = 0; for (int kind = ufieldpositer_next(fpi, &partStart, &partEnd); kind >= 0; kind = ufieldpositer_next(fpi, &partStart, &partEnd), ++i) { nfpb.InsertPart(static_cast(kind), partStart, partEnd); } return nfpb.ToPartsArray(); #else JavascriptLibrary *library = scriptContext->GetLibrary(); JavascriptArray *ret = library->CreateArray(1); DynamicObject* part = library->CreateObject(); EnsureBuffer([&](UChar *buf, int bufLen, UErrorCode *status) { return unum_formatDouble(*fmt, num, buf, bufLen, nullptr, status); }, scriptContext->GetRecycler(), &formatted, &formattedLen); JavascriptOperators::InitProperty(part, PropertyIds::type, library->GetIntlUnknownPartString()); JavascriptOperators::InitProperty(part, PropertyIds::value, JavascriptString::NewWithBuffer(formatted, formattedLen, scriptContext)); ret->SetItem(0, part, PropertyOperationFlags::PropertyOperation_None); return ret; #endif // #if ICU_VERSION >= 61 ... #else #else INTL_CHECK_ARGS( args.Info.Count == 3 && (TaggedInt::Is(args.Values[1]) || JavascriptNumber::Is(args.Values[1])) && DynamicObject::IsBaseDynamicObject(args.Values[2]) ); DynamicObject *options = VarTo(args.Values[2]); Var hiddenObject = nullptr; AssertOrFailFastMsg(options->GetInternalProperty(options, Js::InternalPropertyIds::HiddenObject, &hiddenObject, NULL, scriptContext), "EntryIntl_FormatNumber: Could not retrieve hiddenObject."); DelayLoadWindowsGlobalization* wsl = scriptContext->GetThreadContext()->GetWindowsGlobalizationLibrary(); NumberFormatting::INumberFormatter *numberFormatter; numberFormatter = static_cast(((AutoCOMJSObject *)hiddenObject)->GetInstance()); AutoHSTRING result; HRESULT hr; if (TaggedInt::Is(args.Values[1])) { IfFailThrowHr(numberFormatter->FormatInt(TaggedInt::ToInt32(args.Values[1]), &result)); } else { IfFailThrowHr(numberFormatter->FormatDouble(JavascriptNumber::GetValue(args.Values[1]), &result)); } PCWSTR strBuf = wsl->WindowsGetStringRawBuffer(*result, NULL); if (strBuf == nullptr) { return scriptContext->GetLibrary()->GetUndefined(); } else { JavascriptStringObject *retVal = scriptContext->GetLibrary()->CreateStringObject(Js::JavascriptString::NewCopySz(strBuf, scriptContext)); return retVal; } #endif } #ifdef INTL_ICU static void AddPartToPartsArray(ScriptContext *scriptContext, JavascriptArray *arr, int arrIndex, const char16 *src, int start, int end, JavascriptString *partType) { JavascriptString *partValue = JavascriptString::NewCopyBuffer( src + start, end - start, scriptContext ); DynamicObject* part = scriptContext->GetLibrary()->CreateObject(); JavascriptOperators::InitProperty(part, PropertyIds::type, partType); JavascriptOperators::InitProperty(part, PropertyIds::value, partValue); arr->SetItem(arrIndex, part, PropertyOperationFlags::PropertyOperation_None); } #endif // For Windows Globalization, this function takes a number (date) and a state object and returns a string // For ICU, this function takes a state object, number, and boolean for whether or not to format to parts. // if args.Values[3] ~= true, an array of objects is returned; else, a string is returned Var IntlEngineInterfaceExtensionObject::EntryIntl_FormatDateTime(RecyclableObject* function, CallInfo callInfo, ...) { EngineInterfaceObject_CommonFunctionProlog(function, callInfo); #ifdef INTL_WINGLOB if (args.Info.Count < 3 || !(TaggedInt::Is(args.Values[1]) || JavascriptNumber::Is(args.Values[1])) || !DynamicObject::IsBaseDynamicObject(args.Values[2])) { return scriptContext->GetLibrary()->GetUndefined(); } Windows::Foundation::DateTime winDate; HRESULT hr; if (TaggedInt::Is(args.Values[1])) { hr = Js::DateUtilities::ES5DateToWinRTDate(TaggedInt::ToInt32(args.Values[1]), &(winDate.UniversalTime)); } else { hr = Js::DateUtilities::ES5DateToWinRTDate(JavascriptNumber::GetValue(args.Values[1]), &(winDate.UniversalTime)); } if (FAILED(hr)) { HandleOOMSOEHR(hr); // If conversion failed, double value is outside the range of WinRT DateTime Js::JavascriptError::ThrowRangeError(scriptContext, JSERR_OutOfDateTimeRange); } DynamicObject* obj = VarTo(args.Values[2]); Var hiddenObject = nullptr; AssertOrFailFastMsg(obj->GetInternalProperty(obj, Js::InternalPropertyIds::HiddenObject, &hiddenObject, NULL, scriptContext), "EntryIntl_FormatDateTime: Could not retrieve hiddenObject."); //We are going to perform the same check for timeZone as when caching the formatter. Var propertyValue = nullptr; AutoHSTRING result; //If timeZone is undefined; then use the standard dateTimeFormatter to format in local time; otherwise use the IDateTimeFormatter2 to format using specified timezone (UTC) if (!GetPropertyBuiltInFrom(obj, __timeZone) || JavascriptOperators::IsUndefinedObject(propertyValue)) { DateTimeFormatting::IDateTimeFormatter *formatter = static_cast(((AutoCOMJSObject *)hiddenObject)->GetInstance()); Assert(formatter); IfFailThrowHr(formatter->Format(winDate, &result)); } else { DateTimeFormatting::IDateTimeFormatter2 *formatter = static_cast(((AutoCOMJSObject *)hiddenObject)->GetInstance()); Assert(formatter); HSTRING timeZone; HSTRING_HEADER timeZoneHeader; // IsValidTimeZone() has already verified that this is JavascriptString. JavascriptString* userDefinedTimeZoneId = VarTo(propertyValue); IfFailThrowHr(WindowsCreateStringReference(userDefinedTimeZoneId->GetSz(), userDefinedTimeZoneId->GetLength(), &timeZoneHeader, &timeZone)); Assert(timeZone); IfFailThrowHr(formatter->FormatUsingTimeZone(winDate, timeZone, &result)); } PCWSTR strBuf = scriptContext->GetThreadContext()->GetWindowsGlobalizationLibrary()->WindowsGetStringRawBuffer(*result, NULL); return Js::JavascriptString::NewCopySz(strBuf, scriptContext); #else // This function vaguely implements ECMA 402 #sec-partitiondatetimepattern INTL_CHECK_ARGS( args.Info.Count == 5 && DynamicObject::IsBaseDynamicObject(args[1]) && (TaggedInt::Is(args[2]) || JavascriptNumber::Is(args[2])) && VarIs(args[3]) && VarIs(args[4]) ); DynamicObject *state = UnsafeVarTo(args[1]); bool toParts = Js::UnsafeVarTo(args[3])->GetValue(); // 1. Let x be TimeClip(x) // 2. If x is NaN, throw a RangeError exception double date = JavascriptDate::TimeClip(args[2]); if (JavascriptNumber::IsNan(date)) { JavascriptError::ThrowRangeError(scriptContext, JSERR_InvalidDate); } bool forDatePrototypeToLocaleString = UnsafeVarTo(args[4])->GetValue(); if (forDatePrototypeToLocaleString) { CHAKRATEL_LANGSTATS_INC_BUILTINCOUNT(Date_Prototype_toLocaleString); INTL_TRACE("Calling new Date(%f).toLocaleString(...)", date); } else if (toParts) { CHAKRATEL_LANGSTATS_INC_BUILTINCOUNT(DateTimeFormat_Prototype_formatToParts); INTL_TRACE("Calling DateTimeFormat.prototype.formatToParts(new Date(%f))", date); } else { CHAKRATEL_LANGSTATS_INC_BUILTINCOUNT(DateTimeFormat_Prototype_format); INTL_TRACE("Calling DateTimeFormat.prototype.format(new Date(%f))", date); } // Below, we lazy-initialize the backing UDateFormat on the first call to format{ToParts} // On subsequent calls, the UDateFormat will be cached in state.CachedUDateFormat Var cachedUDateFormat = nullptr; FinalizableUDateFormat *dtf = nullptr; UErrorCode status = U_ZERO_ERROR; if (state->GetInternalProperty(state, InternalPropertyIds::CachedUDateFormat, &cachedUDateFormat, nullptr, scriptContext)) { dtf = reinterpret_cast(cachedUDateFormat); INTL_TRACE("Using previously cached UDateFormat (0x%x)", dtf); } else { JavascriptString *langtag = AssertStringProperty(state, PropertyIds::locale); JavascriptString *timeZone = AssertStringProperty(state, PropertyIds::timeZone); JavascriptString *pattern = AssertStringProperty(state, PropertyIds::pattern); char localeID[ULOC_FULLNAME_CAPACITY] = { 0 }; LangtagToLocaleID(langtag, localeID); dtf = FinalizableUDateFormat::New(scriptContext->GetRecycler(), udat_open( UDAT_PATTERN, UDAT_PATTERN, localeID, reinterpret_cast(timeZone->GetSz()), timeZone->GetLength(), reinterpret_cast(pattern->GetSz()), pattern->GetLength(), &status )); ICU_ASSERT(status, true); // DateTimeFormat is expected to use the "proleptic Gregorian calendar", which means that the Julian calendar should never be used. // To accomplish this, we can set the switchover date between julian/gregorian // to the ECMAScript beginning of time, which is -8.64e15 according to ecma262 #sec-time-values-and-time-range UCalendar *cal = const_cast(udat_getCalendar(*dtf)); const char *calType = ucal_getType(cal, &status); ICU_ASSERT(status, calType != nullptr); if (strcmp(calType, "gregorian") == 0) { double beginningOfTime = -8.64e15; ucal_setGregorianChange(cal, beginningOfTime, &status); double actualGregorianChange = ucal_getGregorianChange(cal, &status); ICU_ASSERT(status, beginningOfTime == actualGregorianChange); } INTL_TRACE("Caching new UDateFormat (0x%x) with langtag=%s, pattern=%s, timezone=%s", dtf, langtag->GetSz(), pattern->GetSz(), timeZone->GetSz()); // cache dtf for later use (so that the condition that brought us here returns true for future calls) state->SetInternalProperty( InternalPropertyIds::CachedUDateFormat, dtf, PropertyOperationFlags::PropertyOperation_None, nullptr ); } // We intentionally special-case the following two calls to EnsureBuffer to allow zero-length strings. // See comment in GetPatternForSkeleton. char16 *formatted = nullptr; int formattedLen = 0; if (!toParts) { // if we aren't formatting to parts, we simply want to call udat_format with retry EnsureBuffer([&](UChar *buf, int bufLen, UErrorCode *status) { return udat_format(*dtf, date, buf, bufLen, nullptr, status); }, scriptContext->GetRecycler(), &formatted, &formattedLen, /* allowZeroLengthStrings */ true); return JavascriptString::NewWithBuffer(formatted, formattedLen, scriptContext); } // The rest of this function most closely corresponds to ECMA 402 #sec-partitiondatetimepattern ScopedUFieldPositionIterator fpi(ufieldpositer_open(&status)); ICU_ASSERT(status, true); EnsureBuffer([&](UChar *buf, int bufLen, UErrorCode *status) { return udat_formatForFields(*dtf, date, buf, bufLen, fpi, status); }, scriptContext->GetRecycler(), &formatted, &formattedLen, /* allowZeroLengthStrings */ true); JavascriptLibrary *library = scriptContext->GetLibrary(); JavascriptArray* ret = library->CreateArray(0); int partStart = 0; int partEnd = 0; int lastPartEnd = 0; int i = 0; for ( int kind = ufieldpositer_next(fpi, &partStart, &partEnd); kind >= 0; kind = ufieldpositer_next(fpi, &partStart, &partEnd), ++i ) { Assert(partStart < partEnd && partEnd <= formattedLen); JavascriptString *typeString = nullptr; UDateFormatField fieldKind = (UDateFormatField)kind; switch (fieldKind) { case UDAT_ERA_FIELD: typeString = library->GetIntlEraPartString(); break; case UDAT_YEAR_FIELD: case UDAT_EXTENDED_YEAR_FIELD: case UDAT_YEAR_NAME_FIELD: typeString = library->GetIntlYearPartString(); break; case UDAT_MONTH_FIELD: case UDAT_STANDALONE_MONTH_FIELD: typeString = library->GetIntlMonthPartString(); break; case UDAT_DATE_FIELD: typeString = library->GetIntlDayPartString(); break; case UDAT_HOUR_OF_DAY1_FIELD: case UDAT_HOUR_OF_DAY0_FIELD: case UDAT_HOUR1_FIELD: case UDAT_HOUR0_FIELD: typeString = library->GetIntlHourPartString(); break; case UDAT_MINUTE_FIELD: typeString = library->GetIntlMinutePartString(); break; case UDAT_SECOND_FIELD: typeString = library->GetIntlSecondPartString(); break; case UDAT_DAY_OF_WEEK_FIELD: case UDAT_STANDALONE_DAY_FIELD: case UDAT_DOW_LOCAL_FIELD: typeString = library->GetIntlWeekdayPartString(); break; case UDAT_AM_PM_FIELD: typeString = library->GetIntlDayPeriodPartString(); break; case UDAT_TIMEZONE_FIELD: case UDAT_TIMEZONE_RFC_FIELD: case UDAT_TIMEZONE_GENERIC_FIELD: case UDAT_TIMEZONE_SPECIAL_FIELD: case UDAT_TIMEZONE_LOCALIZED_GMT_OFFSET_FIELD: case UDAT_TIMEZONE_ISO_FIELD: case UDAT_TIMEZONE_ISO_LOCAL_FIELD: typeString = library->GetIntlTimeZoneNamePartString(); break; #if defined(ICU_VERSION) && ICU_VERSION == 55 case UDAT_TIME_SEPARATOR_FIELD: // ICU 55 (Ubuntu 16.04 system default) has the ":" in "5:23 PM" as a special field // Intl should just treat this as a literal typeString = library->GetIntlLiteralPartString(); break; #endif default: typeString = library->GetIntlUnknownPartString(); break; } if (partStart > lastPartEnd) { // formatForFields does not report literal fields directly, so we have to detect them // by seeing if the current part starts after the previous one ended AddPartToPartsArray(scriptContext, ret, i, formatted, lastPartEnd, partStart, library->GetIntlLiteralPartString()); i += 1; } AddPartToPartsArray(scriptContext, ret, i, formatted, partStart, partEnd, typeString); lastPartEnd = partEnd; } // Sometimes, there can be a literal at the end of the string, such as when formatting just the year in // the chinese calendar, where the pattern string will be `r(U)`. The trailing `)` will be a literal if (lastPartEnd != formattedLen) { AssertOrFailFast(lastPartEnd < formattedLen); // `i` was incremented by the consequence of the last iteration of the for loop AddPartToPartsArray(scriptContext, ret, i, formatted, lastPartEnd, formattedLen, library->GetIntlLiteralPartString()); } return ret; #endif } Var IntlEngineInterfaceExtensionObject::EntryIntl_GetPatternForSkeleton(RecyclableObject *function, CallInfo callInfo, ...) { #ifdef INTL_ICU EngineInterfaceObject_CommonFunctionProlog(function, callInfo); INTL_CHECK_ARGS(args.Info.Count == 3 && VarIs(args.Values[1]) && VarIs(args.Values[2])); JavascriptString *langtag = UnsafeVarTo(args.Values[1]); JavascriptString *skeleton = UnsafeVarTo(args.Values[2]); UErrorCode status = U_ZERO_ERROR; char localeID[ULOC_FULLNAME_CAPACITY] = { 0 }; LangtagToLocaleID(langtag, localeID); // See https://github.com/tc39/ecma402/issues/225 // When picking a format, we should be using the locale data of the basename of the resolved locale, // compared to when we actually format the date using the format string, where we use the full locale including extensions // // ECMA 402 #sec-initializedatetimeformat // 10: Let localeData be %DateTimeFormat%.[[LocaleData]]. // 11: Let r be ResolveLocale( %DateTimeFormat%.[[AvailableLocales]], requestedLocales, opt, %DateTimeFormat%.[[RelevantExtensionKeys]], localeData). // 16: Let dataLocale be r.[[dataLocale]]. // 23: Let dataLocaleData be localeData.[[]]. // 24: Let formats be dataLocaleData.[[formats]]. char baseLocaleID[ULOC_FULLNAME_CAPACITY] = { 0 }; int baseLocaleIDLength = uloc_getBaseName(localeID, baseLocaleID, _countof(baseLocaleID), &status); ICU_ASSERT(status, baseLocaleIDLength > 0 && baseLocaleIDLength < ULOC_FULLNAME_CAPACITY); INTL_TRACE("Converted input langtag '%s' to base locale ID '%S' for pattern generation", langtag->GetSz(), baseLocaleID); ScopedUDateTimePatternGenerator dtpg(udatpg_open(baseLocaleID, &status)); ICU_ASSERT(status, true); char16 *formatted = nullptr; int formattedLen = 0; // OS#17513493 (OSS-Fuzz 7950): It is possible for the skeleton to be a zero-length string // because [[Get]] operations are performed on the options object twice, according to spec. // Follow-up spec discussion here: https://github.com/tc39/ecma402/issues/237. // We need to special-case this because calling udatpg_getBestPatternWithOptions on an empty skeleton // will produce an empty pattern, which causes an assert in EnsureBuffer by default. // As a result, we pass a final optional parameter to EnsureBuffer to say that zero-length results are OK. // TODO(jahorto): re-visit this workaround and the one in FormatDateTime upon resolution of the spec issue. EnsureBuffer([&](UChar *buf, int bufLen, UErrorCode *status) { return udatpg_getBestPatternWithOptions( dtpg, reinterpret_cast(skeleton->GetSz()), skeleton->GetLength(), UDATPG_MATCH_ALL_FIELDS_LENGTH, buf, bufLen, status ); }, scriptContext->GetRecycler(), &formatted, &formattedLen, /* allowZeroLengthStrings */ true); INTL_TRACE("Best pattern '%s' will be used for skeleton '%s' and langtag '%s'", formatted, skeleton->GetSz(), langtag->GetSz()); return JavascriptString::NewWithBuffer(formatted, formattedLen, scriptContext); #else AssertOrFailFastMsg(false, "GetPatternForSkeleton is not implemented outside of ICU"); return nullptr; #endif } // given a timezone name as an argument, this will return a canonicalized version of that name, or undefined if the timezone is invalid Var IntlEngineInterfaceExtensionObject::EntryIntl_ValidateAndCanonicalizeTimeZone(RecyclableObject* function, CallInfo callInfo, ...) { EngineInterfaceObject_CommonFunctionProlog(function, callInfo); INTL_CHECK_ARGS(args.Info.Count == 2 && VarIs(args.Values[1])); JavascriptString *tz = VarTo(args.Values[1]); #ifdef INTL_WINGLOB AutoHSTRING canonicalizedTimeZone; boolean isValidTimeZone = GetWindowsGlobalizationAdapter(scriptContext)->ValidateAndCanonicalizeTimeZone(scriptContext, tz->GetSz(), &canonicalizedTimeZone); if (isValidTimeZone) { DelayLoadWindowsGlobalization* wsl = scriptContext->GetThreadContext()->GetWindowsGlobalizationLibrary(); PCWSTR strBuf = wsl->WindowsGetStringRawBuffer(*canonicalizedTimeZone, NULL); return Js::JavascriptString::NewCopySz(strBuf, scriptContext); } else { return scriptContext->GetLibrary()->GetUndefined(); } #else UErrorCode status = U_ZERO_ERROR; // TODO(jahorto): Is this the list of timeZones that we want? How is this different from // the other two enum values or ucal_openTimeZones? ScopedUEnumeration available(ucal_openTimeZoneIDEnumeration(UCAL_ZONE_TYPE_ANY, nullptr, nullptr, &status)); int availableLength = uenum_count(available, &status); ICU_ASSERT(status, availableLength > 0); charcount_t matchLen = 0; UChar match[100] = { 0 }; for (int a = 0; a < availableLength; ++a) { int curLen = -1; const UChar *cur = uenum_unext(available, &curLen, &status); ICU_ASSERT(status, true); if (curLen == 0) { // OS#17175014: in rare cases, ICU will return U_ZERO_ERROR and a valid `cur` string but a length of 0 // This only happens in an OOM during uenum_(u)next // Tracked by ICU: http://bugs.icu-project.org/trac/ticket/13739 Throw::OutOfMemory(); } AssertOrFailFast(curLen > 0); if (_wcsicmp(reinterpret_cast(cur), tz->GetSz()) == 0) { ucal_getCanonicalTimeZoneID(cur, curLen, match, _countof(match), nullptr, &status); ICU_ASSERT(status, true); size_t len = wcslen(reinterpret_cast(match)); AssertMsg(len < MaxCharCount, "Returned canonicalized timezone is far too long"); matchLen = static_cast(len); break; } } if (matchLen == 0) { return scriptContext->GetLibrary()->GetUndefined(); } return JavascriptString::NewCopyBuffer(reinterpret_cast(match), matchLen, scriptContext); #endif } // returns the current system time zone Var IntlEngineInterfaceExtensionObject::EntryIntl_GetDefaultTimeZone(RecyclableObject* function, CallInfo callInfo, ...) { EngineInterfaceObject_CommonFunctionProlog(function, callInfo); #ifdef INTL_WINGLOB WindowsGlobalizationAdapter* wga = GetWindowsGlobalizationAdapter(scriptContext); DelayLoadWindowsGlobalization* wsl = scriptContext->GetThreadContext()->GetWindowsGlobalizationLibrary(); AutoHSTRING str; HRESULT hr; if (FAILED(hr = wga->GetDefaultTimeZoneId(scriptContext, &str))) { HandleOOMSOEHR(hr); //If we can't get default timeZone, return undefined. return scriptContext->GetLibrary()->GetUndefined(); } PCWSTR strBuf = wsl->WindowsGetStringRawBuffer(*str, NULL); return Js::JavascriptString::NewCopySz(strBuf, scriptContext); #else int timeZoneLen = 0; char16 *timeZone = nullptr; EnsureBuffer([&](UChar *buf, int bufLen, UErrorCode *status) { return ucal_getDefaultTimeZone(buf, bufLen, status); }, scriptContext->GetRecycler(), &timeZone, &timeZoneLen); return JavascriptString::NewWithBuffer(timeZone, timeZoneLen, scriptContext); #endif } #ifdef INTL_ICU static FinalizableUPluralRules *GetOrCreateCachedUPluralRules(DynamicObject *stateObject, ScriptContext *scriptContext) { Var cachedUPluralRules = nullptr; FinalizableUPluralRules *pr = nullptr; if (stateObject->GetInternalProperty(stateObject, InternalPropertyIds::CachedUPluralRules, &cachedUPluralRules, nullptr, scriptContext)) { pr = reinterpret_cast(cachedUPluralRules); INTL_TRACE("Using previously cached UPluralRules (0x%x)", pr); } else { UErrorCode status = U_ZERO_ERROR; JavascriptString *langtag = AssertStringProperty(stateObject, PropertyIds::locale); JavascriptString *type = AssertStringProperty(stateObject, PropertyIds::type); UPluralType prType = UPLURAL_TYPE_CARDINAL; if (wcscmp(type->GetSz(), _u("ordinal")) == 0) { prType = UPLURAL_TYPE_ORDINAL; } else { AssertOrFailFast(wcscmp(type->GetSz(), _u("cardinal")) == 0); } char localeID[ULOC_FULLNAME_CAPACITY] = { 0 }; LangtagToLocaleID(langtag, localeID); pr = FinalizableUPluralRules::New(scriptContext->GetRecycler(), uplrules_openForType(localeID, prType, &status)); ICU_ASSERT(status, true); INTL_TRACE("Caching UPluralRules object (0x%x) with langtag %s and type %s", pr, langtag->GetSz(), type->GetSz()); stateObject->SetInternalProperty(InternalPropertyIds::CachedUPluralRules, pr, PropertyOperationFlags::PropertyOperation_None, nullptr); } return pr; } #endif // This method implements Step 13 and 14 of ECMA 402 #sec-initializepluralrules Var IntlEngineInterfaceExtensionObject::EntryIntl_PluralRulesKeywords(RecyclableObject *function, CallInfo callInfo, ...) { #ifdef INTL_ICU EngineInterfaceObject_CommonFunctionProlog(function, callInfo); INTL_CHECK_ARGS(args.Info.Count == 2 && DynamicObject::IsBaseDynamicObject(args[1])); JavascriptArray *ret = scriptContext->GetLibrary()->CreateArray(0); // uplrules_getKeywords is only stable since ICU 61. // For ICU < 61, we can fake it by creating an array of ["other"], which // uplrules_getKeywords is guaranteed to return at minimum. // This array is only used in resolved options, so the majority of the functionality can remain (namely, select() still works) // // windows-icu does not define `ICU_VERSION` but does support `uplrules_getKeywords` (ICU 61+) since Windows 10 1809 "Redstone 5" (`NTDDI_WIN10_RS5`). #if !defined(ICU_VERSION) || ICU_VERSION >= 61 DynamicObject *state = UnsafeVarTo(args[1]); FinalizableUPluralRules *pr = GetOrCreateCachedUPluralRules(state, scriptContext); UErrorCode status = U_ZERO_ERROR; ScopedUEnumeration keywords(uplrules_getKeywords(*pr, &status)); ICU_ASSERT(status, true); ForEachUEnumeration16(keywords, [&](int index, const char16 *kw, charcount_t kwLength) { ret->SetItem(index, JavascriptString::NewCopyBuffer(kw, kwLength, scriptContext), PropertyOperation_None); }); #else ret->SetItem(0, scriptContext->GetLibrary()->GetIntlPluralRulesOtherString(), PropertyOperation_None); #endif return ret; #else AssertOrFailFastMsg(false, "Intl-WinGlob should not be using PluralRulesKeywords"); return nullptr; #endif } // This method roughly implements ECMA 402 #sec-pluralruleselect, except ICU takes care of handling the operand logic for us Var IntlEngineInterfaceExtensionObject::EntryIntl_PluralRulesSelect(RecyclableObject *function, CallInfo callInfo, ...) { #ifdef INTL_ICU EngineInterfaceObject_CommonFunctionProlog(function, callInfo); INTL_CHECK_ARGS(args.Info.Count == 3 && DynamicObject::IsBaseDynamicObject(args[1])); DynamicObject *state = UnsafeVarTo(args[1]); double n = 0.0; if (TaggedInt::Is(args[2])) { n = TaggedInt::ToDouble(args[2]); } else { AssertOrFailFast(JavascriptNumber::Is(args[2])); n = JavascriptNumber::GetValue(args[2]); } CHAKRATEL_LANGSTATS_INC_BUILTINCOUNT(PluralRules_Prototype_select); INTL_TRACE("Calling PluralRules.prototype.select(%f)", n); UErrorCode status = U_ZERO_ERROR; FinalizableUPluralRules *pr = GetOrCreateCachedUPluralRules(state, scriptContext); // ICU has an internal API, uplrules_selectWithFormat, that is equivalent to uplrules_select but will respect digit options of the passed UNumberFormat. // Since its an internal API, we can't use it -- however, we can work around it by creating a UNumberFormat with provided digit options, // formatting the requested number to a string, and then converting the string back to a double which we can pass to uplrules_select. // This workaround was suggested during the May 2018 ECMA-402 discussion. // The below is similar to GetOrCreateCachedUPluralRules, but since creating a UNumberFormat for Intl.NumberFormat is much more involved and no one else // uses this functionality, it makes more sense to me to just put the logic inline. Var cachedUNumberFormat = nullptr; FinalizableUNumberFormat *nf = nullptr; if (state->GetInternalProperty(state, InternalPropertyIds::CachedUNumberFormat, &cachedUNumberFormat, nullptr, scriptContext)) { nf = reinterpret_cast(cachedUNumberFormat); INTL_TRACE("Using previously cached UNumberFormat (0x%x)", nf); } else { char localeID[ULOC_FULLNAME_CAPACITY] = { 0 }; LangtagToLocaleID(AssertStringProperty(state, PropertyIds::locale), localeID); nf = FinalizableUNumberFormat::New(scriptContext->GetRecycler(), unum_open(UNUM_DECIMAL, nullptr, 0, localeID, nullptr, &status)); SetUNumberFormatDigitOptions(*nf, state); INTL_TRACE("Caching UNumberFormat object (0x%x) with localeID %S", nf, localeID); state->SetInternalProperty(InternalPropertyIds::CachedUNumberFormat, nf, PropertyOperationFlags::PropertyOperation_None, nullptr); } char16 *formattedN = nullptr; int formattedNLength = 0; EnsureBuffer([&](UChar *buf, int bufLen, UErrorCode *status) { return unum_formatDouble(*nf, n, buf, bufLen, nullptr, status); }, scriptContext->GetRecycler(), &formattedN, &formattedNLength); double nWithOptions = unum_parseDouble(*nf, reinterpret_cast(formattedN), formattedNLength, nullptr, &status); ICU_ASSERT(status, (n > 0.0 && nWithOptions <= n && nWithOptions >= 0.0) || (n < 0.0 && nWithOptions >= n && nWithOptions <= 0) || (n == 0.0 && nWithOptions == 0)); char16 *selected = nullptr; int selectedLength = 0; EnsureBuffer([&](UChar *buf, int bufLen, UErrorCode *status) { return uplrules_select(*pr, nWithOptions, buf, bufLen, status); }, scriptContext->GetRecycler(), &selected, &selectedLength); return JavascriptString::NewWithBuffer(selected, static_cast(selectedLength), scriptContext); #else AssertOrFailFastMsg(false, "Intl-WinGlob should not be using PluralRulesSelect"); return nullptr; #endif } /* * This function registers built in functions when Intl initializes. * Call with (Function : toRegister, integer : id) * ID Mappings: * - 0 for Date.prototype.toLocaleString * - 1 for Date.prototype.toLocaleDateString * - 2 for Date.prototype.toLocaleTimeString * - 3 for Number.prototype.toLocaleString * - 4 for String.prototype.localeCompare */ Var IntlEngineInterfaceExtensionObject::EntryIntl_RegisterBuiltInFunction(RecyclableObject* function, CallInfo callInfo, ...) { EngineInterfaceObject_CommonFunctionProlog(function, callInfo); // This function will only be used during the construction of the Intl object, hence Asserts are in place. Assert(args.Info.Count >= 3 && VarIs(args.Values[1]) && TaggedInt::Is(args.Values[2])); JavascriptFunction *func = VarTo(args.Values[1]); int32 id = TaggedInt::ToInt32(args.Values[2]); Assert(id >= 0 && id < (int32)BuiltInFunctionID::Max); EngineInterfaceObject* nativeEngineInterfaceObj = scriptContext->GetLibrary()->GetEngineInterfaceObject(); IntlEngineInterfaceExtensionObject* extensionObject = static_cast(nativeEngineInterfaceObj->GetEngineExtension(EngineInterfaceExtensionKind_Intl)); BuiltInFunctionID functionID = static_cast(id); switch (functionID) { case BuiltInFunctionID::DateToLocaleString: extensionObject->dateToLocaleString = func; break; case BuiltInFunctionID::DateToLocaleDateString: extensionObject->dateToLocaleDateString = func; break; case BuiltInFunctionID::DateToLocaleTimeString: extensionObject->dateToLocaleTimeString = func; break; case BuiltInFunctionID::NumberToLocaleString: extensionObject->numberToLocaleString = func; break; case BuiltInFunctionID::StringLocaleCompare: extensionObject->stringLocaleCompare = func; break; default: AssertMsg(false, "functionID was not one of the allowed values. The previous assert should catch this."); break; } // Don't need to return anything return scriptContext->GetLibrary()->GetUndefined(); } Var IntlEngineInterfaceExtensionObject::EntryIntl_GetHiddenObject(RecyclableObject* function, CallInfo callInfo, ...) { EngineInterfaceObject_CommonFunctionProlog(function, callInfo); if (callInfo.Count < 2 || !DynamicObject::IsBaseDynamicObject(args.Values[1])) { return scriptContext->GetLibrary()->GetUndefined(); } DynamicObject* obj = VarTo(args.Values[1]); Var hiddenObject = nullptr; if (!obj->GetInternalProperty(obj, Js::InternalPropertyIds::HiddenObject, &hiddenObject, NULL, scriptContext)) { return scriptContext->GetLibrary()->GetUndefined(); } return hiddenObject; } Var IntlEngineInterfaceExtensionObject::EntryIntl_SetHiddenObject(RecyclableObject* function, CallInfo callInfo, ...) { EngineInterfaceObject_CommonFunctionProlog(function, callInfo); if (callInfo.Count < 3 || !DynamicObject::IsBaseDynamicObject(args.Values[1]) || !DynamicObject::IsBaseDynamicObject(args.Values[2])) { return scriptContext->GetLibrary()->GetUndefined(); } DynamicObject* obj = VarTo(args.Values[1]); DynamicObject* value = VarTo(args.Values[2]); if (obj->SetInternalProperty(Js::InternalPropertyIds::HiddenObject, value, Js::PropertyOperationFlags::PropertyOperation_None, NULL)) { return scriptContext->GetLibrary()->GetTrue(); } else { return scriptContext->GetLibrary()->GetFalse(); } } #endif } #endif