Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions JSTests/stress/string-case-conversion-longer-than-max-length.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//@ memoryHog!
//@ slow!
//@ skip if $addressBits <= 32
//@ runDefault

// Full case mapping can make a string longer: "ß" uppercases to "SS", "ffi" to "FFI", and "İ"
// lowercases to "i̇" (two code units). When the converted string would be longer than the
// maximum string length, the conversion has to throw. It used to hand back the input unchanged.
// Results between 2^30 and the maximum length are fine and must neither throw nor crash.
// Every block below works on strings of a gigabyte or more, so each takes seconds.

function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error(`bad value: expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`);
}

function shouldThrowOutOfMemory(func) {
let error = null;
try {
func();
} catch (e) {
error = e;
}
if (String(error) !== "RangeError: Out of memory")
throw new Error(`bad error: ${String(error)}`);
}

const maxLength = 2 ** 31 - 1;

function toUpperCase(string) { return string.toUpperCase(); }
noInline(toUpperCase);
function toLowerCase(string) { return string.toLowerCase(); }
noInline(toLowerCase);

// Warm these up so that the conversions below also run through the DFG's ToUpperCase and
// ToLowerCase nodes, whose slow path is a different function from the baseline one.
for (let i = 0; i < testLoopCount; ++i) {
shouldBe(toUpperCase("Stra\u00DFe" + i), "STRASSE" + i);
shouldBe(toLowerCase("\u0130I" + i), "i\u0307i" + i);
}

{
// 8-bit input. Each "ß" adds one character, so the result would be 2^31 long.
const input = "\u00DF".repeat(2 ** 30);
shouldThrowOutOfMemory(() => input.toUpperCase());
shouldThrowOutOfMemory(() => input.toLocaleUpperCase("en"));
shouldThrowOutOfMemory(() => toUpperCase(input));
// The language-sensitive locales convert through ICU into a separate buffer, with the same limit.
shouldThrowOutOfMemory(() => input.toLocaleUpperCase("lt"));

// One character less and the result is exactly the maximum length.
const fits = "\u00DF".repeat(2 ** 30 - 1) + "a";
const upper = fits.toUpperCase();
shouldBe(upper.length, maxLength);
shouldBe(upper[0], "S");
shouldBe(upper[maxLength - 1], "A");
}
gc();

{
// 16-bit input through ICU. Each "ffi" becomes three characters, 3 * 715827883 = 2^31 + 1, which
// does not even fit in ICU's int32_t length.
const input = "\uFB03".repeat(715827883);
shouldThrowOutOfMemory(() => input.toUpperCase());
}
gc();

{
// 16-bit input through ICU where the result, 3 * 715827878 + 2 * 4 = 2^31 - 6 characters, fits
// in ICU's int32_t length but is too long for a 16-bit String.
const input = "\uFB03".repeat(715827878) + "\u00DF".repeat(4);
shouldThrowOutOfMemory(() => input.toUpperCase());
}
gc();

{
// Lowercasing grows too: "İ" (U+0130) becomes "i" followed by U+0307 COMBINING DOT ABOVE.
const input = "\u0130".repeat(2 ** 30);
shouldThrowOutOfMemory(() => input.toLowerCase());
shouldThrowOutOfMemory(() => toLowerCase(input));
}
gc();

{
// A result of 2^30 characters from a language-sensitive locale, which is fine. "ß" is "SS" under
// Azerbaijani rules too, and the "i", which becomes "İ", is what sends the string to them.
const upper = ("\u00DF".repeat(2 ** 29) + "i").toLocaleUpperCase("az");
shouldBe(upper.length, 2 ** 30 + 1);
shouldBe(upper[0], "S");
shouldBe(upper[2 ** 30 - 1], "S");
shouldBe(upper[2 ** 30], "\u0130");
}
gc();

{
// 8-bit strings of 2^30 or more characters that have to be widened for ICU: "i" is "İ" in
// Turkish, and "ÿ" uppercases to "Ÿ" (U+0178) everywhere.
const dotted = "i".repeat(2 ** 30).toLocaleUpperCase("tr");
shouldBe(dotted.length, 2 ** 30);
shouldBe(dotted[2 ** 30 - 1], "\u0130");

const diaeresis = toUpperCase("a".repeat(2 ** 30) + "\u00FF");
shouldBe(diaeresis.length, 2 ** 30 + 1);
shouldBe(diaeresis[0], "A");
shouldBe(diaeresis[2 ** 30], "\u0178");
}
12 changes: 10 additions & 2 deletions Source/JavaScriptCore/dfg/DFGOperations.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3883,7 +3883,11 @@ JSC_DEFINE_JIT_OPERATION(operationToUpperCase, JSString*, (JSGlobalObject* globa
if (!inputString->length())
OPERATION_RETURN(scope, vm.smallStrings.emptyString());

String uppercasedString = inputString->is8Bit() ? inputString->convertToUppercaseWithoutLocaleStartingAtFailingIndex8Bit(failingIndex) : inputString->convertToUppercaseWithoutLocaleStartingAtFailingIndex16Bit(failingIndex);
String uppercasedString = inputString->is8Bit() ? inputString->tryConvertToUppercaseWithoutLocaleStartingAtFailingIndex8Bit(failingIndex) : inputString->tryConvertToUppercaseWithoutLocaleStartingAtFailingIndex16Bit(failingIndex);
if (uppercasedString.isNull()) [[unlikely]] {
throwOutOfMemoryError(globalObject, scope);
OPERATION_RETURN(scope, nullptr);
}
if (uppercasedString.impl() == inputString->impl())
OPERATION_RETURN(scope, string);
OPERATION_RETURN(scope, jsString(vm, WTF::move(uppercasedString)));
Expand All @@ -3902,7 +3906,11 @@ JSC_DEFINE_JIT_OPERATION(operationToLowerCase, JSString*, (JSGlobalObject* globa
if (!inputString->length())
OPERATION_RETURN(scope, vm.smallStrings.emptyString());

String lowercasedString = inputString->is8Bit() ? inputString->convertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(failingIndex) : inputString->convertToLowercaseWithoutLocaleStartingAtFailingIndex16Bit(failingIndex);
String lowercasedString = inputString->is8Bit() ? inputString->tryConvertToLowercaseWithoutLocaleStartingAtFailingIndex8Bit(failingIndex) : inputString->tryConvertToLowercaseWithoutLocaleStartingAtFailingIndex16Bit(failingIndex);
if (lowercasedString.isNull()) [[unlikely]] {
throwOutOfMemoryError(globalObject, scope);
OPERATION_RETURN(scope, nullptr);
}
if (lowercasedString.impl() == inputString->impl())
OPERATION_RETURN(scope, string);
OPERATION_RETURN(scope, jsString(vm, WTF::move(lowercasedString)));
Expand Down
36 changes: 16 additions & 20 deletions Source/JavaScriptCore/runtime/StringPrototype.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1709,7 +1709,9 @@ JSC_DEFINE_HOST_FUNCTION(stringProtoFuncToLowerCase, (JSGlobalObject* globalObje
auto s = sVal->value(globalObject);
RETURN_IF_EXCEPTION(scope, { });

String lowercasedString = s->convertToLowercaseWithoutLocale();
String lowercasedString = s->tryConvertToLowercaseWithoutLocale();
if (lowercasedString.isNull()) [[unlikely]]
return JSValue::encode(throwOutOfMemoryError(globalObject, scope));
if (lowercasedString.impl() == s->impl())
return JSValue::encode(sVal);

Expand Down Expand Up @@ -1745,7 +1747,9 @@ JSC_DEFINE_HOST_FUNCTION(stringProtoFuncToUpperCase, (JSGlobalObject* globalObje
auto s = sVal->value(globalObject);
RETURN_IF_EXCEPTION(scope, { });

String uppercasedString = s->convertToUppercaseWithoutLocale();
String uppercasedString = s->tryConvertToUppercaseWithoutLocale();
if (uppercasedString.isNull()) [[unlikely]]
return JSValue::encode(throwOutOfMemoryError(globalObject, scope));
if (uppercasedString.impl() == s->impl())
return JSValue::encode(sVal);

Expand Down Expand Up @@ -1862,32 +1866,24 @@ static EncodedJSValue toLocaleCase(JSGlobalObject* globalObject, CallFrame* call
});

// 12. If locale is undefined, let locale be "und".
if (locale.isNull()) {
// Case mapping is locale-independent here, so skip ICU and use the root conversion.
String converted = mode == CaseConversionMode::Lower ? s->convertToLowercaseWithoutLocale() : s->convertToUppercaseWithoutLocale();
if (converted.impl() == s->impl())
return JSValue::encode(sVal);
RELEASE_AND_RETURN(scope, JSValue::encode(jsString(vm, WTF::move(converted))));
}

// Delegate the following steps to icu u_strToLower or u_strToUpper.
// 13. Let cpList be a List containing in order the code points of S as defined in ES2015, 6.1.4, starting at the first element of S.
// 14. For each code point c in cpList, if the Unicode Character Database provides a lower(/upper) case equivalent of c that is either language insensitive or for the language locale, then replace c in cpList with that/those equivalent code point(s).
// 15. Let cuList be a new List.
// 16. For each code point c in cpList, in order, append to cuList the elements of the UTF-16 Encoding (defined in ES2015, 6.1.4) of c.
// 17. Let L be a String whose elements are, in order, the elements of cuList.

// Most strings lower/upper case will be the same size as original, so try that first.
Vector<char16_t> buffer;
if (!StringImpl::isValidLength<char16_t>(s->length()) || !buffer.tryReserveInitialCapacity(s->length())) [[unlikely]]
// WTF applies the language-sensitive mappings of "az", "el", "lt" and "tr" through ICU, and the
// root mappings for every other locale identifier, including the null one.
if (s->isEmpty())
return JSValue::encode(sVal);
AtomString localeIdentifier { locale };
String converted = mode == CaseConversionMode::Lower ? s->tryConvertToLowercaseWithLocale(localeIdentifier) : s->tryConvertToUppercaseWithLocale(localeIdentifier);
if (converted.isNull()) [[unlikely]]
return JSValue::encode(throwOutOfMemoryError(globalObject, scope));
auto convertCase = mode == CaseConversionMode::Lower ? u_strToLower : u_strToUpper;
auto status = callBufferProducingFunction(convertCase, buffer, StringView { s }.upconvertedCharacters().get(), s->length(), locale.utf8().legacyCStringPointer());
if (U_FAILURE(status))
return throwVMTypeError(globalObject, scope, String::fromLatin1(u_errorName(status)));

// 18. Return L.
RELEASE_AND_RETURN(scope, JSValue::encode(jsString(vm, String { WTF::move(buffer) })));
if (converted.impl() == s->impl())
return JSValue::encode(sVal);
RELEASE_AND_RETURN(scope, JSValue::encode(jsString(vm, WTF::move(converted))));
}

JSC_DEFINE_HOST_FUNCTION(stringProtoFuncToLocaleLowerCase, (JSGlobalObject* globalObject, CallFrame* callFrame))
Expand Down
Loading
Loading