Skip to content

Commit f5df228

Browse files
committed
Assortment of Intl fixes and cleanup
* Properly name Intl.PluralRules functions * Address issues brought up in the May 2018 and June 2018 Intl call * Address previously uncaught issue where we failed fast unintentionally when handling the sign part in ICU > 61. * Always use null-prototyped property descriptors to avoid Object.prototype.get tainting.
1 parent d720588 commit f5df228

File tree

5 files changed

+123
-43
lines changed

5 files changed

+123
-43
lines changed

lib/Runtime/Library/InJavascript/Intl.js

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,11 @@
136136

137137
keys: platform.builtInJavascriptObjectEntryKeys,
138138
hasOwnProperty(o, prop) { return callInstanceFunc(platform.builtInJavascriptObjectEntryHasOwnProperty, o, prop); },
139-
defineProperty: platform.builtInJavascriptObjectEntryDefineProperty,
139+
// If we don't set the descriptor's prototype to null, defining properties with `value`s can fail of Object.prototype.get is defined
140+
defineProperty(o, prop, desc) {
141+
_.setPrototypeOf(desc, null);
142+
platform.builtInJavascriptObjectEntryDefineProperty(o, prop, desc);
143+
},
140144
isExtensible: platform.builtInJavascriptObjectEntryIsExtensible,
141145
create(proto = null) { return platform.builtInJavascriptObjectCreate(proto); },
142146
setPrototypeOf(target, proto = null) { return platform.builtInSetPrototype(target, proto); },
@@ -846,19 +850,17 @@
846850
? BestFitSupportedLocales(isAvailableLocale, requestedLocales)
847851
: LookupSupportedLocales(isAvailableLocale, requestedLocales);
848852

849-
// make sure property descriptor is null-prototyped to avoid tainting of Object.prototype.{get|set}
850-
const descriptor = _.setPrototypeOf({ configurable: false, writable: false });
851853
for (let i = 0; i < supportedLocales.length; i++) {
852-
_.defineProperty(supportedLocales, Internal.ToString(i), descriptor);
854+
_.defineProperty(supportedLocales, Internal.ToString(i), { configurable: false, writable: false });
853855
}
854856

855857
// test262 supportedLocalesOf-returned-array-elements-are-frozen.js:
856858
// Property length of object returned by SupportedLocales should not be writable
857-
_.defineProperty(supportedLocales, "length", _.setPrototypeOf({
859+
_.defineProperty(supportedLocales, "length", {
858860
writable: false,
859861
configurable: false,
860862
enumerable: false,
861-
}));
863+
});
862864

863865
return supportedLocales;
864866
};
@@ -2015,7 +2017,7 @@
20152017
};
20162018

20172019
// params are explicitly `= undefined` to make PluralRules.length === 0
2018-
const PluralRules = function (locales = undefined, options = undefined) {
2020+
const PluralRules = function PluralRules(locales = undefined, options = undefined) {
20192021
if (new.target === undefined) {
20202022
platform.raiseNeedObjectOfType("Intl.PluralRules", "PluralRules");
20212023
}
@@ -2058,7 +2060,7 @@
20582060
});
20592061

20602062
// ECMA 402: #sec-intl.pluralrules.prototype.select
2061-
const select = function (value) {
2063+
const select = function select(value) {
20622064
const pr = platform.getHiddenObject(this);
20632065
if (!pr || !pr.initializedPluralRules) {
20642066
platform.raiseNeedObjectOfType("Intl.PluralRules.prototype.select", "PluralRules");
@@ -2075,31 +2077,29 @@
20752077
writable: true,
20762078
});
20772079

2078-
const resolvedOptions = function () {
2080+
const resolvedOptions = function resolvedOptions() {
20792081
const pr = platform.getHiddenObject(this);
20802082
if (!pr || !pr.initializedPluralRules) {
20812083
platform.raiseNeedObjectOfType("Intl.PluralRules.prototype.select", "PluralRules");
20822084
}
20832085

2084-
const options = {};
2085-
_.forEach([
2086+
return createResolvedOptions([
20862087
"locale",
20872088
"type",
20882089
"minimumIntegerDigits",
20892090
"minimumFractionDigits",
20902091
"maximumFractionDigits",
20912092
"minimumSignificantDigits",
2092-
"maximumSignificantDigits"
2093-
], (prop) => {
2094-
if (pr[prop] !== undefined) {
2095-
options[prop] = pr[prop];
2093+
"maximumSignificantDigits",
2094+
"pluralCategories"
2095+
], pr, (prop, resolved) => {
2096+
if (prop === "pluralCategories") {
2097+
// https://github.com/tc39/ecma402/issues/224: create a copy of the pluralCategories array
2098+
resolved.pluralCategories = _.slice(pr.pluralCategories, 0);
2099+
} else {
2100+
resolved[prop] = pr[prop];
20962101
}
20972102
});
2098-
2099-
// https://github.com/tc39/ecma402/issues/224: create a copy of the pluralCategories array
2100-
options.pluralCategories = _.slice(pr.pluralCategories, 0);
2101-
2102-
return options;
21032103
};
21042104
tagPublicFunction("Intl.PluralRules.prototype.resolvedOptions", resolvedOptions);
21052105
_.defineProperty(PluralRules.prototype, "resolvedOptions", {
@@ -2114,10 +2114,10 @@
21142114

21152115
// Initialize Intl properties only if needed
21162116
if (InitType === "Intl") {
2117-
ObjectDefineProperty(Intl, "Collator", { value: Collator, writable: true, enumerable: false, configurable: true });
2118-
ObjectDefineProperty(Intl, "NumberFormat", { value: NumberFormat, writable: true, enumerable: false, configurable: true });
2119-
ObjectDefineProperty(Intl, "DateTimeFormat", { value: DateTimeFormat, writable: true, enumerable: false, configurable: true });
2120-
ObjectDefineProperty(Intl, "PluralRules", { value: PluralRules, writable: true, enumerable: false, configurable: true });
2117+
_.defineProperty(Intl, "Collator", { value: Collator, writable: true, enumerable: false, configurable: true });
2118+
_.defineProperty(Intl, "NumberFormat", { value: NumberFormat, writable: true, enumerable: false, configurable: true });
2119+
_.defineProperty(Intl, "DateTimeFormat", { value: DateTimeFormat, writable: true, enumerable: false, configurable: true });
2120+
_.defineProperty(Intl, "PluralRules", { value: PluralRules, writable: true, enumerable: false, configurable: true });
21212121
}
21222122

21232123
}

lib/Runtime/Library/IntlEngineInterfaceExtensionObject.cpp

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,6 +1457,25 @@ DEFINE_ISXLOCALEAVAILABLE(PR, uloc)
14571457
#endif
14581458
}
14591459

1460+
#ifdef INTL_ICU
1461+
// This is used by both NumberFormat and PluralRules
1462+
static void SetUNumberFormatDigitOptions(UNumberFormat *fmt, DynamicObject *state)
1463+
{
1464+
if (JavascriptOperators::HasProperty(state, PropertyIds::minimumSignificantDigits))
1465+
{
1466+
unum_setAttribute(fmt, UNUM_SIGNIFICANT_DIGITS_USED, true);
1467+
unum_setAttribute(fmt, UNUM_MIN_SIGNIFICANT_DIGITS, AssertIntegerProperty(state, PropertyIds::minimumSignificantDigits));
1468+
unum_setAttribute(fmt, UNUM_MAX_SIGNIFICANT_DIGITS, AssertIntegerProperty(state, PropertyIds::maximumSignificantDigits));
1469+
}
1470+
else
1471+
{
1472+
unum_setAttribute(fmt, UNUM_MIN_INTEGER_DIGITS, AssertIntegerProperty(state, PropertyIds::minimumIntegerDigits));
1473+
unum_setAttribute(fmt, UNUM_MIN_FRACTION_DIGITS, AssertIntegerProperty(state, PropertyIds::minimumFractionDigits));
1474+
unum_setAttribute(fmt, UNUM_MAX_FRACTION_DIGITS, AssertIntegerProperty(state, PropertyIds::maximumFractionDigits));
1475+
}
1476+
}
1477+
#endif
1478+
14601479
Var IntlEngineInterfaceExtensionObject::EntryIntl_CacheNumberFormat(RecyclableObject * function, CallInfo callInfo, ...)
14611480
{
14621481
EngineInterfaceObject_CommonFunctionProlog(function, callInfo);
@@ -1515,18 +1534,7 @@ DEFINE_ISXLOCALEAVAILABLE(PR, uloc)
15151534

15161535
unum_setAttribute(*fmt, UNUM_ROUNDING_MODE, UNUM_ROUND_HALFUP);
15171536

1518-
if (JavascriptOperators::HasProperty(state, PropertyIds::minimumSignificantDigits))
1519-
{
1520-
unum_setAttribute(*fmt, UNUM_SIGNIFICANT_DIGITS_USED, true);
1521-
unum_setAttribute(*fmt, UNUM_MIN_SIGNIFICANT_DIGITS, AssertIntegerProperty(state, PropertyIds::minimumSignificantDigits));
1522-
unum_setAttribute(*fmt, UNUM_MAX_SIGNIFICANT_DIGITS, AssertIntegerProperty(state, PropertyIds::maximumSignificantDigits));
1523-
}
1524-
else
1525-
{
1526-
unum_setAttribute(*fmt, UNUM_MIN_INTEGER_DIGITS, AssertIntegerProperty(state, PropertyIds::minimumIntegerDigits));
1527-
unum_setAttribute(*fmt, UNUM_MIN_FRACTION_DIGITS, AssertIntegerProperty(state, PropertyIds::minimumFractionDigits));
1528-
unum_setAttribute(*fmt, UNUM_MAX_FRACTION_DIGITS, AssertIntegerProperty(state, PropertyIds::maximumFractionDigits));
1529-
}
1537+
SetUNumberFormatDigitOptions(*fmt, state);
15301538

15311539
if (currency != nullptr)
15321540
{
@@ -2256,8 +2264,10 @@ DEFINE_ISXLOCALEAVAILABLE(PR, uloc)
22562264
// TODO(jahorto): Determine if this would ever be returned and what it would map to
22572265
case UNUM_PERMILL_FIELD: AssertOrFailFastMsg(false, "Unexpected permill field");
22582266

2259-
case UNUM_SIGN_FIELD: num < 0 ? library->GetIntlMinusSignPartString() : library->GetIntlPlusSignPartString();
2260-
default: AssertOrFailFastMsg(false, "Unexpected unknown part"); return nullptr;
2267+
case UNUM_SIGN_FIELD: return num < 0 ? library->GetIntlMinusSignPartString() : library->GetIntlPlusSignPartString();
2268+
2269+
// At the ECMA-402 TC39 call for May 2017, it was decided that we should treat unmapped parts as type: "unknown"
2270+
default: return library->GetIntlUnknownPartString();
22612271
}
22622272
}
22632273

@@ -2418,7 +2428,7 @@ DEFINE_ISXLOCALEAVAILABLE(PR, uloc)
24182428
{
24192429
return unum_formatDouble(*fmt, num, buf, bufLen, nullptr, status);
24202430
}, scriptContext->GetRecycler(), &formatted, &formattedLen);
2421-
JavascriptOperators::InitProperty(part, PropertyIds::type, library->GetIntlLiteralPartString());
2431+
JavascriptOperators::InitProperty(part, PropertyIds::type, library->GetIntlUnknownPartString());
24222432
JavascriptOperators::InitProperty(part, PropertyIds::value, JavascriptString::NewWithBuffer(formatted, formattedLen, scriptContext));
24232433

24242434
ret->SetItem(0, part, PropertyOperationFlags::PropertyOperation_None);
@@ -2754,7 +2764,6 @@ DEFINE_ISXLOCALEAVAILABLE(PR, uloc)
27542764
typeString = library->GetIntlLiteralPartString(); break;
27552765
#endif
27562766
default:
2757-
AssertMsg(false, "Unmapped UDateFormatField");
27582767
typeString = library->GetIntlUnknownPartString(); break;
27592768
}
27602769

@@ -3046,11 +3055,35 @@ DEFINE_ISXLOCALEAVAILABLE(PR, uloc)
30463055

30473056
FinalizableUPluralRules *pr = GetOrCreatePluralRulesCache(state, scriptContext);
30483057

3058+
// ICU has an internal API, uplrules_selectWithFormat, that is equivalent to uplrules_select but will respect digit options of the passed UNumberFormat.
3059+
// Since its an internal API, we can't use it -- however, we can work around it by creating a UNumberFormat with provided digit options,
3060+
// formatting the requested number to a string, and then converting the string back to a double which we can pass to uplrules_select.
3061+
// This workaround was suggested during the May 2018 ECMA-402 discussion.
3062+
// TODO(jahorto): investigate caching this UNumberFormat on the state as well. This is currently not possible because we are using InternalProperyIds::HiddenObject
3063+
// for all ICU object caching, but once we move to better names for the cache property IDs, we can cache both the UNumberFormat as well as the UPluralRules.
3064+
char localeID[ULOC_FULLNAME_CAPACITY] = { 0 };
3065+
LangtagToLocaleID(AssertStringProperty(state, PropertyIds::locale), localeID);
3066+
UErrorCode status = U_ZERO_ERROR;
3067+
FinalizableUNumberFormat *fmt = FinalizableUNumberFormat::New(scriptContext->GetRecycler(), unum_open(UNUM_DECIMAL, nullptr, 0, localeID, nullptr, &status));
3068+
3069+
SetUNumberFormatDigitOptions(*fmt, state);
3070+
3071+
char16 *formattedN = nullptr;
3072+
int formattedNLength = 0;
3073+
EnsureBuffer([&](UChar *buf, int bufLen, UErrorCode *status)
3074+
{
3075+
return unum_formatDouble(*fmt, n, buf, bufLen, nullptr, status);
3076+
}, scriptContext->GetRecycler(), &formattedN, &formattedNLength);
3077+
3078+
double nWithOptions = unum_parseDouble(*fmt, reinterpret_cast<UChar *>(formattedN), formattedNLength, nullptr, &status);
3079+
double roundtripDiff = n - nWithOptions;
3080+
ICU_ASSERT(status, roundtripDiff <= 1.0 && roundtripDiff >= -1.0);
3081+
30493082
char16 *selected = nullptr;
30503083
int selectedLength = 0;
30513084
EnsureBuffer([&](UChar *buf, int bufLen, UErrorCode *status)
30523085
{
3053-
return uplrules_select(*pr, n, buf, bufLen, status);
3086+
return uplrules_select(*pr, nWithOptions, buf, bufLen, status);
30543087
}, scriptContext->GetRecycler(), &selected, &selectedLength);
30553088

30563089
return JavascriptString::NewWithBuffer(selected, static_cast<charcount_t>(selectedLength), scriptContext);

test/Intl/NumberFormat.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ const tests = [
190190
// real formatToParts support was only added with ICU 61
191191
assert.areEqual(1, actualParts.length, `formatToParts(${n}) stub implementation should return only one part`);
192192
const literal = actualParts[0];
193-
assert.areEqual("literal", literal.type, `formatToParts(${n}) stub implementation should return a literal part`);
193+
assert.areEqual("unknown", literal.type, `formatToParts(${n}) stub implementation should return an unknown part`);
194194
assert.areEqual(nf.format(n), literal.value, `formatToParts(${n}) stub implementation should return one part whose value matches the fully formatted number`);
195195
return;
196196
}
@@ -207,6 +207,12 @@ const tests = [
207207
{ type: "group", value: "," },
208208
{ type: "integer", value: "000" }
209209
]);
210+
assertParts("en-US", undefined, -1000, [
211+
{ type: "minusSign", value: "-" },
212+
{ type: "integer" , value: "1" },
213+
{ type: "group", value: "," },
214+
{ type: "integer", value: "000" }
215+
]);
210216
assertParts("en-US", undefined, NaN, [{ type: "nan", value: "NaN" }]);
211217
assertParts("en-US", undefined, Infinity, [{ type: "infinity", value: "∞" }]);
212218
assertParts("en-US", undefined, 1000.3423, [

test/Intl/PluralRules.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,5 +109,42 @@ testRunner.runTests([
109109
opts1.pluralCategories[0] = "changed";
110110
assert.areNotEqual(opts1.pluralCategories[0], pr1.resolvedOptions().pluralCategories[0], "Changing the pluralCategories from one call to resolvedOptions should not impact future calls");
111111
}
112-
}
112+
},
113+
{
114+
name: "Number digit options",
115+
body() {
116+
function test(options, n, expected) {
117+
const pr = new Intl.PluralRules("en", options);
118+
assert.areEqual(expected, pr.select(n), `Incorrect result using n = ${n} and options = ${JSON.stringify(options)}`);
119+
}
120+
121+
test(undefined, 1.0, "one");
122+
test(undefined, 1.1, "other");
123+
test(undefined, 1.001, "other");
124+
125+
test({ minimumFractionDigits: 1 }, 1.0, "one");
126+
test({ minimumFractionDigits: 1 }, 1.1, "other");
127+
test({ minimumFractionDigits: 1 }, 1.001, "other");
128+
129+
test({ maximumFractionDigits: 0 }, 1.0, "one");
130+
test({ maximumFractionDigits: 0 }, 1.1, "one");
131+
test({ maximumFractionDigits: 0 }, 1.001, "one");
132+
133+
test({ maximumFractionDigits: 1 }, 1.0, "one");
134+
test({ maximumFractionDigits: 1 }, 1.1, "other");
135+
test({ maximumFractionDigits: 1 }, 1.001, "one");
136+
137+
test({ minimumSignificantDigits: 2 }, 1.0, "one");
138+
test({ minimumSignificantDigits: 2 }, 1.1, "other");
139+
test({ minimumSignificantDigits: 2 }, 1.001, "other");
140+
141+
test({ maximumSignificantDigits: 2 }, 1.0, "one");
142+
test({ maximumSignificantDigits: 2 }, 1.1, "other");
143+
test({ maximumSignificantDigits: 2 }, 1.001, "one");
144+
145+
test({ maximumSignificantDigits: 1 }, 1.0, "one");
146+
test({ maximumSignificantDigits: 1 }, 1.1, "one");
147+
test({ maximumSignificantDigits: 1 }, 1.001, "one");
148+
}
149+
},
113150
], { verbose: false });

test/Intl/common.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ WScript.LoadScriptFile("..\\UnitTestFramework\\UnitTestFramework.js");
77

88
const constructors = [Intl.Collator, Intl.NumberFormat, Intl.DateTimeFormat];
99

10+
if (WScript.Platform.INTL_LIBRARY === "icu") {
11+
constructors.push(Intl.PluralRules);
12+
}
13+
1014
testRunner.runTests([
1115
{
1216
name: "OSS-Fuzz #6657: stress uloc_forLanguageTag status code and parsed length on duplicate variant subtags",

0 commit comments

Comments
 (0)