LibJS: Keep more string construction in UTF-16
Use a typed RegExp cache key instead of serializing the UTF-16 pattern and flags into a String. This avoids another StringBuilder path for data that is already naturally represented as UTF-16 plus flag bits. Also make legacy unescape operate on UTF-16 code units and build its result with Utf16StringBuilder. This preserves direct non-ASCII input and %uXXXX escapes, including lone surrogate code units, without routing the result through UTF-8 storage.
This commit is contained in:
parent
2d20322fce
commit
950c460efd
3 changed files with 39 additions and 21 deletions
|
|
@ -588,36 +588,43 @@ JS_DEFINE_NATIVE_FUNCTION(GlobalObject::escape)
|
|||
JS_DEFINE_NATIVE_FUNCTION(GlobalObject::unescape)
|
||||
{
|
||||
// 1. Set string to ? ToString(string).
|
||||
auto string = TRY(vm.argument(0).to_byte_string(vm));
|
||||
auto string = TRY(vm.argument(0).to_utf16_string(vm));
|
||||
|
||||
// 2. Let length be the length of string.
|
||||
ssize_t length = string.length();
|
||||
ssize_t length = string.length_in_code_units();
|
||||
|
||||
// 3. Let R be the empty String.
|
||||
StringBuilder unescaped(length);
|
||||
Utf16StringBuilder unescaped(length);
|
||||
|
||||
// 4. Let k be 0.
|
||||
// 5. Repeat, while k ≠ length,
|
||||
for (auto k = 0; k < length; ++k) {
|
||||
// a. Let c be the code unit at index k within string.
|
||||
u32 code_point = string[k];
|
||||
u16 code_unit = string.code_unit_at(k);
|
||||
|
||||
// b. If c is the code unit 0x0025 (PERCENT SIGN), then
|
||||
if (code_point == '%') {
|
||||
if (code_unit == '%') {
|
||||
// i. Let hexEscape be the empty String.
|
||||
// ii. Let skip be 0.
|
||||
// iii. If k ≤ length - 6 and the code unit at index k + 1 within string is the code unit 0x0075 (LATIN SMALL LETTER U), then
|
||||
if (k <= length - 6 && string[k + 1] == 'u' && is_ascii_hex_digit(string[k + 2]) && is_ascii_hex_digit(string[k + 3]) && is_ascii_hex_digit(string[k + 4]) && is_ascii_hex_digit(string[k + 5])) {
|
||||
if (k <= length - 6
|
||||
&& string.code_unit_at(k + 1) == 'u'
|
||||
&& is_ascii_hex_digit(string.code_unit_at(k + 2))
|
||||
&& is_ascii_hex_digit(string.code_unit_at(k + 3))
|
||||
&& is_ascii_hex_digit(string.code_unit_at(k + 4))
|
||||
&& is_ascii_hex_digit(string.code_unit_at(k + 5))) {
|
||||
// 1. Set hexEscape to the substring of string from k + 2 to k + 6.
|
||||
code_point = (parse_ascii_hex_digit(string[k + 2]) << 12) | (parse_ascii_hex_digit(string[k + 3]) << 8) | (parse_ascii_hex_digit(string[k + 4]) << 4) | parse_ascii_hex_digit(string[k + 5]);
|
||||
code_unit = (parse_ascii_hex_digit(string.code_unit_at(k + 2)) << 12) | (parse_ascii_hex_digit(string.code_unit_at(k + 3)) << 8) | (parse_ascii_hex_digit(string.code_unit_at(k + 4)) << 4) | parse_ascii_hex_digit(string.code_unit_at(k + 5));
|
||||
|
||||
// 2. Set skip to 5.
|
||||
k += 5;
|
||||
}
|
||||
// iv. Else if k ≤ length - 3, then
|
||||
else if (k <= length - 3 && is_ascii_hex_digit(string[k + 1]) && is_ascii_hex_digit(string[k + 2])) {
|
||||
else if (k <= length - 3
|
||||
&& is_ascii_hex_digit(string.code_unit_at(k + 1))
|
||||
&& is_ascii_hex_digit(string.code_unit_at(k + 2))) {
|
||||
// 1. Set hexEscape to the substring of string from k + 1 to k + 3.
|
||||
code_point = (parse_ascii_hex_digit(string[k + 1]) << 4) | parse_ascii_hex_digit(string[k + 2]);
|
||||
code_unit = (parse_ascii_hex_digit(string.code_unit_at(k + 1)) << 4) | parse_ascii_hex_digit(string.code_unit_at(k + 2));
|
||||
|
||||
// 2. Set skip to 2.
|
||||
k += 2;
|
||||
|
|
@ -632,13 +639,13 @@ JS_DEFINE_NATIVE_FUNCTION(GlobalObject::unescape)
|
|||
}
|
||||
|
||||
// c. Set R to the string-concatenation of R and c.
|
||||
unescaped.append_code_point(code_point);
|
||||
unescaped.append_code_unit(code_unit);
|
||||
|
||||
// d. Set k to k + 1.
|
||||
}
|
||||
|
||||
// 6. Return R.
|
||||
return PrimitiveString::create(vm, unescaped.to_byte_string());
|
||||
return PrimitiveString::create(vm, unescaped.to_string());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,9 +78,23 @@ static ThrowCompletionOr<void> increment_last_index(VM& vm, Object& regexp_objec
|
|||
}
|
||||
|
||||
// FIXME: Add an eviction policy to bound the size of this cache.
|
||||
struct RegexCacheKey {
|
||||
Utf16String pattern;
|
||||
RegExpObject::Flags flags;
|
||||
|
||||
bool operator==(RegexCacheKey const&) const = default;
|
||||
};
|
||||
|
||||
struct RegexCacheKeyTraits : public Traits<RegexCacheKey> {
|
||||
static unsigned hash(RegexCacheKey const& key)
|
||||
{
|
||||
return pair_int_hash(key.pattern.hash(), to_underlying(key.flags));
|
||||
}
|
||||
};
|
||||
|
||||
static auto& regex_cache()
|
||||
{
|
||||
static NeverDestroyed<HashMap<String, NonnullOwnPtr<regex::ECMAScriptRegex>>> cache;
|
||||
static NeverDestroyed<HashMap<RegexCacheKey, NonnullOwnPtr<regex::ECMAScriptRegex>, RegexCacheKeyTraits>> cache;
|
||||
return *cache;
|
||||
}
|
||||
|
||||
|
|
@ -93,13 +107,7 @@ static regex::ECMAScriptRegex const* get_or_compile_regex(RegExpObject& regexp_o
|
|||
auto const& pattern = regexp_object.pattern();
|
||||
auto flag_bits = regexp_object.flag_bits();
|
||||
|
||||
// Build a cache key from pattern + flag bits.
|
||||
StringBuilder key_builder;
|
||||
key_builder.append('/');
|
||||
key_builder.append(pattern.utf16_view());
|
||||
key_builder.append('/');
|
||||
key_builder.append_code_point(static_cast<u8>(flag_bits));
|
||||
auto cache_key = key_builder.to_string_without_validation();
|
||||
RegexCacheKey cache_key { pattern, flag_bits };
|
||||
|
||||
if (auto it = regex_cache().find(cache_key); it != regex_cache().end()) {
|
||||
auto* ptr = it->value.ptr();
|
||||
|
|
@ -110,7 +118,7 @@ static regex::ECMAScriptRegex const* get_or_compile_regex(RegExpObject& regexp_o
|
|||
bool unicode = has_flag(flag_bits, RegExpObject::Flags::Unicode);
|
||||
bool unicode_sets = has_flag(flag_bits, RegExpObject::Flags::UnicodeSets);
|
||||
|
||||
// Parse the pattern from UTF-16 source to UTF-8 with escape normalization.
|
||||
// Normalize non-ASCII code units to ASCII escapes before compiling the pattern.
|
||||
auto parsed_pattern = parse_regex_pattern(pattern.utf16_view(), unicode, unicode_sets);
|
||||
if (parsed_pattern.is_error())
|
||||
return nullptr;
|
||||
|
|
@ -132,7 +140,7 @@ static regex::ECMAScriptRegex const* get_or_compile_regex(RegExpObject& regexp_o
|
|||
|
||||
auto owned = make<regex::ECMAScriptRegex>(compiled.release_value());
|
||||
auto* ptr = owned.ptr();
|
||||
regex_cache().set(cache_key, move(owned));
|
||||
regex_cache().set(move(cache_key), move(owned));
|
||||
regexp_object.set_cached_regex(ptr);
|
||||
return ptr;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ test("unescape", () => {
|
|||
["%E4%F6%FC", "äöü"],
|
||||
["%u0107", "ć"],
|
||||
["@*_+-./", "@*_+-./"],
|
||||
["äöü", "äöü"],
|
||||
["%uD834%uDF06", "\ud834\udf06"],
|
||||
["%uD800", "\ud800"],
|
||||
].forEach(test => {
|
||||
expect(unescape(test[0])).toBe(test[1]);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue