LibJS: Prevent escaped surrogates from combining in Unicode regexes

Escaped surrogate sequences should not combine with adjacent literal
surrogates in Unicode mode.

We now use `\u{XXXX}` braces instead of `\uXXXX` when escaping code
units in Unicode mode, so LibRegex treats each as a standalone code
point. Also prevent GenericLexer from combining `\uXXXX` and `\u{XXXX}`.
This commit is contained in:
aplefull 2026-02-10 14:26:53 +01:00 committed by Ali Mohammad Pur
parent 6c411563d3
commit d95baf67f6
3 changed files with 33 additions and 2 deletions

View file

@ -452,6 +452,11 @@ protected:
if (!combine_surrogate_pairs || !consume_specific("\\u"sv))
return *high_surrogate;
if (next_is('{')) {
retreat(2);
return *high_surrogate;
}
auto low_surrogate = decode_one_surrogate();
if (!low_surrogate.has_value())
return UnicodeEscapeError::MalformedUnicodeEscape;

View file

@ -6,6 +6,7 @@
*/
#include <AK/Function.h>
#include <AK/UnicodeUtils.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/PrimitiveString.h>
@ -97,7 +98,6 @@ ErrorOr<String, ParseRegexPatternError> parse_regex_pattern(Utf16View const& pat
StringBuilder builder;
// FIXME: We need to escape multi-byte code units for LibRegex to parse since the lexer there doesn't handle unicode.
auto previous_code_unit_was_backslash = false;
for (size_t i = 0; i < pattern.length_in_code_units(); ++i) {
u16 code_unit = pattern.code_unit_at(i);
@ -109,7 +109,22 @@ ErrorOr<String, ParseRegexPatternError> parse_regex_pattern(Utf16View const& pat
// As such, we're going to remove the (invalid) backslash and pretend it never existed.
if (!previous_code_unit_was_backslash)
builder.append('\\');
builder.appendff("u{:04x}", code_unit);
if ((unicode || unicode_sets) && AK::UnicodeUtils::is_utf16_high_surrogate(code_unit) && i + 1 < pattern.length_in_code_units()) {
u16 next_code_unit = pattern.code_unit_at(i + 1);
if (AK::UnicodeUtils::is_utf16_low_surrogate(next_code_unit)) {
u32 combined = AK::UnicodeUtils::decode_utf16_surrogate_pair(code_unit, next_code_unit);
builder.appendff("u{{{:x}}}", combined);
++i;
previous_code_unit_was_backslash = false;
continue;
}
}
if (unicode || unicode_sets)
builder.appendff("u{{{:04x}}}", code_unit);
else
builder.appendff("u{:04x}", code_unit);
} else {
builder.append_code_point(code_unit);
}

View file

@ -555,3 +555,14 @@ test("Unicode case-insensitive matching", () => {
testMatch(/[\u03A9]/i, "\u2126", null);
testMatch(/[\u03A9]/iv, "\u2126", ["\u2126"]);
});
test("surrogate pairs", () => {
expect(eval(`/[\uD83D\uDC38]/u`).exec("\u{1F438}")?.[0]).toBe("\u{1F438}");
expect(eval(`/[\uD83D\uDC38]/`).exec("\u{1F438}")?.[0]).toBe("\uD83D");
expect(eval(`/[\\uD83D\uDC38]/u`).exec("\u{1F438}")).toBeNull();
expect(eval(`/[\\u{D83D}\uDC38]/u`).exec("\u{1F438}")).toBeNull();
expect(eval(`/[\uD83D\\uDC38]/u`).exec("\u{1F438}")).toBeNull();
expect(eval(`/[\uD83D\\u{DC38}]/u`).exec("\u{1F438}")).toBeNull();
expect(eval(`/[\\uD83D\uDC38]/`).exec("\u{1F438}")?.[0]).toBe("\uD83D");
expect(eval(`/[\uD83D\\uDC38]/`).exec("\u{1F438}")?.[0]).toBe("\uD83D");
});