From 2d20322fcecfb6434c2be3954fc135a7fafb60ab Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Mon, 22 Jun 2026 13:07:20 +0200 Subject: [PATCH] LibRegex: Compile ECMAScript patterns from UTF-16 Accept Utf16View patterns at the LibRegex compile boundary and pass UTF-16 or ASCII storage directly into the Rust regex parser. This keeps JavaScript regular expression construction from converting patterns through UTF-8 when LibRegex can consume the same UTF-16 representation used by LibJS. Update RegExp construction, HTML pattern validation, the regex fuzzer, and LibRegex tests to use the UTF-16 compile API. --- Libraries/LibJS/Runtime/RegExpConstructor.cpp | 27 ++++--- Libraries/LibJS/Runtime/RegExpObject.cpp | 48 ++++++------ Libraries/LibJS/Runtime/RegExpObject.h | 6 +- Libraries/LibJS/Runtime/RegExpPrototype.cpp | 3 +- Libraries/LibJS/RustIntegration.cpp | 4 +- Libraries/LibRegex/ECMAScriptRegex.cpp | 4 +- Libraries/LibRegex/ECMAScriptRegex.h | 2 +- Libraries/LibRegex/Rust/src/ffi.rs | 75 ++++++++++++++----- Libraries/LibRegex/Rust/src/parser.rs | 9 ++- Libraries/LibRegex/Rust/src/regex.rs | 9 +++ Libraries/LibRegex/RustRegex.cpp | 26 +++++-- Libraries/LibRegex/RustRegex.h | 2 +- Libraries/LibWeb/HTML/HTMLInputElement.cpp | 15 ++-- Meta/Fuzzers/FuzzRegexECMA262.cpp | 5 +- Tests/LibRegex/TestRegex.cpp | 61 ++++++++------- 15 files changed, 192 insertions(+), 104 deletions(-) diff --git a/Libraries/LibJS/Runtime/RegExpConstructor.cpp b/Libraries/LibJS/Runtime/RegExpConstructor.cpp index 3124b48105..00337f8f55 100644 --- a/Libraries/LibJS/Runtime/RegExpConstructor.cpp +++ b/Libraries/LibJS/Runtime/RegExpConstructor.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -179,7 +180,7 @@ ThrowCompletionOr> RegExpConstructor::construct_impl(FunctionObj } // 22.2.5.1.1 EncodeForRegExpEscape ( cp ), https://tc39.es/ecma262/#sec-encodeforregexpescape -static String encode_for_regexp_escape(u32 code_point) +static Utf16String encode_for_regexp_escape(u32 code_point) { // https://tc39.es/ecma262/#table-controlescape-code-point-values // Table 63: ControlEscape Code Point Values @@ -198,7 +199,10 @@ static String encode_for_regexp_escape(u32 code_point) // 1. If c is matched by SyntaxCharacter or c is U+002F (SOLIDUS), then if (is_syntax_character(code_point) || code_point == '/') { // a. Return the string-concatenation of 0x005C (REVERSE SOLIDUS) and UTF16EncodeCodePoint(c). - return MUST(String::formatted("\\{}", String::from_code_point(code_point))); + Utf16StringBuilder builder; + builder.append_ascii('\\'); + builder.append_code_point(code_point); + return builder.to_string(); } // 2. Else if c is the code point listed in some cell of the “Code Point” column of Table 63, then @@ -209,7 +213,10 @@ static String encode_for_regexp_escape(u32 code_point) if (it != control_escapes.end()) { // a. Return the string-concatenation of 0x005C (REVERSE SOLIDUS) and the string in the “ControlEscape” column // of the row whose “Code Point” column contains c. - return MUST(String::formatted("\\{}", it->control_escape)); + Utf16StringBuilder builder; + builder.append_ascii('\\'); + builder.append_ascii(it->control_escape); + return builder.to_string(); } // 3. Let otherPunctuators be the string-concatenation of ",-=<>#&!%:;@~'`" and the code unit 0x0022 (QUOTATION MARK). @@ -225,7 +232,7 @@ static String encode_for_regexp_escape(u32 code_point) // i. Let hex be Number::toString(𝔽(cNum), 16). // ii. Return the string-concatenation of the code unit 0x005C (REVERSE SOLIDUS), "x", and // StringPad(hex, 2, "0", START). - return MUST(String::formatted("\\x{:02x}", code_point)); + return Utf16String::formatted("\\x{:02x}", code_point); } // c. Let escaped be the empty String. @@ -233,11 +240,11 @@ static String encode_for_regexp_escape(u32 code_point) // e. For each code unit cu of codeUnits, do // i. Set escaped to the string-concatenation of escaped and UnicodeEscape(cu). // f. Return escaped. - return MUST(String::formatted("\\u{:04x}", code_point)); + return Utf16String::formatted("\\u{:04x}", code_point); } // 6. Return UTF16EncodeCodePoint(c). - return String::from_code_point(code_point); + return Utf16String::from_code_point(code_point); } // 22.2.5.1 RegExp.escape ( S ), https://tc39.es/ecma262/#sec-regexp.escape @@ -250,13 +257,13 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpConstructor::escape) return vm.throw_completion(ErrorType::NotAString, string); // 2. Let escaped be the empty String. - StringBuilder escaped(string.as_string().utf8_string().byte_count()); + Utf16StringBuilder escaped(string.as_string().utf16_string_view().length_in_code_units()); // 3. Let cpList be StringToCodePoints(S). - auto code_point_list = string.as_string().utf8_string(); + auto code_point_list = string.as_string().utf16_string_view(); // 4. For each code point c of cpList, do - for (auto code_point : code_point_list.code_points()) { + for (auto code_point : code_point_list) { // a. If escaped is the empty String and c is matched by either DecimalDigit or AsciiLetter, then if (escaped.is_empty() && is_ascii_alphanumeric(code_point)) { // i. NOTE: Escaping a leading digit ensures that output corresponds with pattern text which may be used @@ -278,7 +285,7 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpConstructor::escape) } // 5. Return escaped. - return JS::PrimitiveString::create(vm, MUST(escaped.to_string())); + return JS::PrimitiveString::create(vm, escaped.to_string()); } // 22.2.5.3 get RegExp [ %Symbol.species% ], https://tc39.es/ecma262/#sec-get-regexp-@@species diff --git a/Libraries/LibJS/Runtime/RegExpObject.cpp b/Libraries/LibJS/Runtime/RegExpObject.cpp index 7590fd14e8..c2f67e1237 100644 --- a/Libraries/LibJS/Runtime/RegExpObject.cpp +++ b/Libraries/LibJS/Runtime/RegExpObject.cpp @@ -261,14 +261,14 @@ static Result validate_flags(Utf16View const& flags } // 22.2.3.4 Static Semantics: ParsePattern ( patternText, u, v ), https://tc39.es/ecma262/#sec-parsepattern -ErrorOr parse_regex_pattern(Utf16View const& pattern, bool unicode, bool unicode_sets) +ErrorOr parse_regex_pattern(Utf16View const& pattern, bool unicode, bool unicode_sets) { if (unicode && unicode_sets) return ParseRegexPatternError { MUST(String::formatted(ErrorType::RegExpObjectIncompatibleFlags.format(), 'u', 'v')) }; TRY(validate_named_group_name_surrogates(pattern, unicode || unicode_sets)); - StringBuilder builder; + Utf16StringBuilder builder; auto previous_code_unit_was_backslash = false; for (size_t i = 0; i < pattern.length_in_code_units(); ++i) { @@ -280,7 +280,7 @@ ErrorOr parse_regex_pattern(Utf16View const& pat // leading to a matcher for the literal string "\uhhhh" instead of the intended code unit . // As such, we're going to remove the (invalid) backslash and pretend it never existed. if (!previous_code_unit_was_backslash) - builder.append('\\'); + builder.append_ascii('\\'); 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); @@ -298,7 +298,7 @@ ErrorOr parse_regex_pattern(Utf16View const& pat else builder.appendff("u{:04x}", code_unit); } else { - builder.append_code_point(code_unit); + builder.append_code_unit(code_unit); } if (code_unit == '\\') @@ -307,11 +307,11 @@ ErrorOr parse_regex_pattern(Utf16View const& pat previous_code_unit_was_backslash = false; } - return builder.to_string_without_validation(); + return builder.to_string(); } // 22.2.3.4 Static Semantics: ParsePattern ( patternText, u, v ), https://tc39.es/ecma262/#sec-parsepattern -ThrowCompletionOr parse_regex_pattern(VM& vm, Utf16View const& pattern, bool unicode, bool unicode_sets) +ThrowCompletionOr parse_regex_pattern(VM& vm, Utf16View const& pattern, bool unicode, bool unicode_sets) { auto result = parse_regex_pattern(pattern, unicode, unicode_sets); if (result.is_error()) @@ -402,9 +402,9 @@ ThrowCompletionOr> RegExpObject::regexp_initialize(VM& vm, bool unicode = has_flag(flag_bits, Flags::Unicode); bool unicode_sets = has_flag(flag_bits, Flags::UnicodeSets); - auto parsed_pattern = String {}; + auto parsed_pattern = Utf16String {}; - // Convert UTF-16 pattern to UTF-8 (with escape normalization for non-ASCII). + // Normalize non-ASCII code units to ASCII escapes before compiling the pattern. if (!pattern.is_empty()) { auto result = parse_regex_pattern(pattern, unicode, unicode_sets); if (result.is_error()) @@ -425,7 +425,7 @@ ThrowCompletionOr> RegExpObject::regexp_initialize(VM& vm, compile_flags.unicode_sets = unicode_sets; compile_flags.sticky = has_flag(flag_bits, Flags::Sticky); - auto compiled = regex::ECMAScriptRegex::compile(parsed_pattern.bytes_as_string_view(), compile_flags); + auto compiled = regex::ECMAScriptRegex::compile(parsed_pattern.utf16_view(), compile_flags); if (compiled.is_error()) return vm.throw_completion(ErrorType::RegExpCompileError, compiled.release_error()); @@ -449,7 +449,7 @@ ThrowCompletionOr> RegExpObject::regexp_initialize(VM& vm, } // 22.2.6.13.1 EscapeRegExpPattern ( P, F ), https://tc39.es/ecma262/#sec-escaperegexppattern -String RegExpObject::escape_regexp_pattern() const +Utf16String RegExpObject::escape_regexp_pattern() const { // 1. Let S be a String in the form of a Pattern[~UnicodeMode] (Pattern[+UnicodeMode] if F contains "u") equivalent // to P interpreted as UTF-16 encoded Unicode code points (6.1.4), in which certain code points are escaped as @@ -465,30 +465,30 @@ String RegExpObject::escape_regexp_pattern() const // specification can be met by letting S be "(?:)". // 3. Return S. if (m_pattern.is_empty()) - return "(?:)"_string; + return "(?:)"_utf16; // FIXME: Check the 'u' and 'v' flags and escape accordingly - StringBuilder builder; + Utf16StringBuilder builder; auto escaped = false; auto in_character_class = false; for (auto code_point : m_pattern) { if (escaped) { escaped = false; - builder.append('\\'); + builder.append_ascii('\\'); switch (code_point) { case '\n': - builder.append('n'); + builder.append_ascii('n'); break; case '\r': - builder.append('r'); + builder.append_ascii('r'); break; case LINE_SEPARATOR: - builder.append("u2028"sv); + builder.append_ascii("u2028"sv); break; case PARAGRAPH_SEPARATOR: - builder.append("u2029"sv); + builder.append_ascii("u2029"sv); break; default: builder.append_code_point(code_point); @@ -511,21 +511,21 @@ String RegExpObject::escape_regexp_pattern() const switch (code_point) { case '/': if (in_character_class) - builder.append('/'); + builder.append_ascii('/'); else - builder.append("\\/"sv); + builder.append_ascii("\\/"sv); break; case '\n': - builder.append("\\n"sv); + builder.append_ascii("\\n"sv); break; case '\r': - builder.append("\\r"sv); + builder.append_ascii("\\r"sv); break; case LINE_SEPARATOR: - builder.append("\\u2028"sv); + builder.append_ascii("\\u2028"sv); break; case PARAGRAPH_SEPARATOR: - builder.append("\\u2029"sv); + builder.append_ascii("\\u2029"sv); break; default: builder.append_code_point(code_point); @@ -533,7 +533,7 @@ String RegExpObject::escape_regexp_pattern() const } } - return builder.to_string_without_validation(); + return builder.to_string(); } void RegExpObject::visit_edges(JS::Cell::Visitor& visitor) diff --git a/Libraries/LibJS/Runtime/RegExpObject.h b/Libraries/LibJS/Runtime/RegExpObject.h index a7b447afee..6760b70795 100644 --- a/Libraries/LibJS/Runtime/RegExpObject.h +++ b/Libraries/LibJS/Runtime/RegExpObject.h @@ -22,8 +22,8 @@ ThrowCompletionOr> regexp_alloc(VM&, FunctionObject& new_t struct ParseRegexPatternError { String error; }; -ErrorOr parse_regex_pattern(Utf16View const& pattern, bool unicode, bool unicode_sets); -ThrowCompletionOr parse_regex_pattern(VM& vm, Utf16View const& pattern, bool unicode, bool unicode_sets); +ErrorOr parse_regex_pattern(Utf16View const& pattern, bool unicode, bool unicode_sets); +ThrowCompletionOr parse_regex_pattern(VM& vm, Utf16View const& pattern, bool unicode, bool unicode_sets); class JS_API RegExpObject : public Object { JS_OBJECT(RegExpObject, Object); @@ -45,7 +45,7 @@ public: static GC::Ref create(Realm&, Utf16String pattern, Utf16String flags); ThrowCompletionOr> regexp_initialize(VM&, Value pattern, Value flags); - String escape_regexp_pattern() const; + Utf16String escape_regexp_pattern() const; virtual void initialize(Realm&) override; virtual ~RegExpObject() override = default; diff --git a/Libraries/LibJS/Runtime/RegExpPrototype.cpp b/Libraries/LibJS/Runtime/RegExpPrototype.cpp index 476781d65f..274fc2cc76 100644 --- a/Libraries/LibJS/Runtime/RegExpPrototype.cpp +++ b/Libraries/LibJS/Runtime/RegExpPrototype.cpp @@ -125,7 +125,8 @@ static regex::ECMAScriptRegex const* get_or_compile_regex(RegExpObject& regexp_o flags.sticky = has_flag(flag_bits, RegExpObject::Flags::Sticky); flags.has_indices = has_flag(flag_bits, RegExpObject::Flags::HasIndices); - auto compiled = regex::ECMAScriptRegex::compile(parsed_pattern.release_value(), flags); + auto normalized_pattern = parsed_pattern.release_value(); + auto compiled = regex::ECMAScriptRegex::compile(normalized_pattern.utf16_view(), flags); if (compiled.is_error()) return nullptr; diff --git a/Libraries/LibJS/RustIntegration.cpp b/Libraries/LibJS/RustIntegration.cpp index 506ee95a0c..b8a23ce4b2 100644 --- a/Libraries/LibJS/RustIntegration.cpp +++ b/Libraries/LibJS/RustIntegration.cpp @@ -1160,7 +1160,7 @@ void free_function_ast(void* ast) namespace JS::FFI { struct RustCompiledRegex { - String parsed_pattern; + Utf16String parsed_pattern; }; static Utf16View view_from_ffi(FFIUtf16Slice slice) @@ -1889,7 +1889,7 @@ extern "C" void* rust_compile_regex( } } - auto compiled = regex::ECMAScriptRegex::compile(pattern_str.bytes_as_string_view(), compile_flags); + auto compiled = regex::ECMAScriptRegex::compile(pattern_str.utf16_view(), compile_flags); if (compiled.is_error()) { auto msg = MUST(String::formatted("RegExp compile error: {}", compiled.release_error())); auto* buf = static_cast(kmalloc(msg.byte_count() + 1)); diff --git a/Libraries/LibRegex/ECMAScriptRegex.cpp b/Libraries/LibRegex/ECMAScriptRegex.cpp index 0cd2ed1a20..4e0dde3fd3 100644 --- a/Libraries/LibRegex/ECMAScriptRegex.cpp +++ b/Libraries/LibRegex/ECMAScriptRegex.cpp @@ -15,7 +15,7 @@ struct ECMAScriptRegex::Impl { Vector named_groups; }; -ErrorOr ECMAScriptRegex::compile(StringView utf8_pattern, ECMAScriptCompileFlags flags) +ErrorOr ECMAScriptRegex::compile(Utf16View pattern, ECMAScriptCompileFlags flags) { RustRegexFlags rust_flags {}; rust_flags.global = flags.global; @@ -27,7 +27,7 @@ ErrorOr ECMAScriptRegex::compile(StringView utf8_patter rust_flags.sticky = flags.sticky; rust_flags.has_indices = flags.has_indices; - auto compiled = CompiledRustRegex::compile(utf8_pattern, rust_flags); + auto compiled = CompiledRustRegex::compile(pattern, rust_flags); if (compiled.is_error()) return compiled.release_error(); diff --git a/Libraries/LibRegex/ECMAScriptRegex.h b/Libraries/LibRegex/ECMAScriptRegex.h index 9157ca4217..f5c09e6712 100644 --- a/Libraries/LibRegex/ECMAScriptRegex.h +++ b/Libraries/LibRegex/ECMAScriptRegex.h @@ -42,7 +42,7 @@ class REGEX_API ECMAScriptRegex { AK_MAKE_NONCOPYABLE(ECMAScriptRegex); public: - static ErrorOr compile(StringView utf8_pattern, ECMAScriptCompileFlags); + static ErrorOr compile(Utf16View pattern, ECMAScriptCompileFlags); ~ECMAScriptRegex(); ECMAScriptRegex(ECMAScriptRegex&&); diff --git a/Libraries/LibRegex/Rust/src/ffi.rs b/Libraries/LibRegex/Rust/src/ffi.rs index 077bef7796..44102b69d6 100644 --- a/Libraries/LibRegex/Rust/src/ffi.rs +++ b/Libraries/LibRegex/Rust/src/ffi.rs @@ -28,29 +28,12 @@ pub struct RustRegexFlags { pub has_indices: bool, } -/// Compile a regex pattern. Returns an opaque handle, or null on error. -/// On error, writes the error message to `error_out` and `error_len_out`. -/// The caller must free the error string with `rust_regex_free_error`. -/// -/// # Safety -/// `pattern` must be a valid pointer to `pattern_len` bytes of UTF-8. -/// `error_out` and `error_len_out` must be valid pointers. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn rust_regex_compile( - pattern: *const u8, - pattern_len: usize, +fn compile_pattern( + pattern: Vec, flags: RustRegexFlags, error_out: *mut *const u8, error_len_out: *mut usize, ) -> *mut RustRegex { - if pattern.is_null() { - return std::ptr::null_mut(); - } - let pattern_bytes = unsafe { slice::from_raw_parts(pattern, pattern_len) }; - let Ok(pattern_str) = std::str::from_utf8(pattern_bytes) else { - return std::ptr::null_mut(); - }; - let flags = Flags { global: flags.global, ignore_case: flags.ignore_case, @@ -62,7 +45,7 @@ pub unsafe extern "C" fn rust_regex_compile( has_indices: flags.has_indices, }; - match Regex::compile(pattern_str, flags) { + match Regex::compile_chars(pattern, flags) { Ok(regex) => Box::into_raw(Box::new(RustRegex(regex))), Err(e) => { if !error_out.is_null() && !error_len_out.is_null() { @@ -78,6 +61,58 @@ pub unsafe extern "C" fn rust_regex_compile( } } +/// Compile a regex pattern. Returns an opaque handle, or null on error. +/// On error, writes the error message to `error_out` and `error_len_out`. +/// The caller must free the error string with `rust_regex_free_error`. +/// +/// # Safety +/// `pattern` must be a valid pointer to `pattern_len` UTF-16 code units. +/// `error_out` and `error_len_out` must be valid pointers. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rust_regex_compile( + pattern: *const u16, + pattern_len: usize, + flags: RustRegexFlags, + error_out: *mut *const u8, + error_len_out: *mut usize, +) -> *mut RustRegex { + if pattern.is_null() { + return std::ptr::null_mut(); + } + + let code_units = unsafe { slice::from_raw_parts(pattern, pattern_len) }; + let pattern = char::decode_utf16(code_units.iter().copied()) + .map(|result| result.unwrap_or(char::REPLACEMENT_CHARACTER)) + .collect(); + + compile_pattern(pattern, flags, error_out, error_len_out) +} + +/// Compile an ASCII-only regex pattern. Returns an opaque handle, or null on error. +/// On error, writes the error message to `error_out` and `error_len_out`. +/// The caller must free the error string with `rust_regex_free_error`. +/// +/// # Safety +/// `pattern` must be a valid pointer to `pattern_len` ASCII bytes. +/// `error_out` and `error_len_out` must be valid pointers. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rust_regex_compile_ascii( + pattern: *const u8, + pattern_len: usize, + flags: RustRegexFlags, + error_out: *mut *const u8, + error_len_out: *mut usize, +) -> *mut RustRegex { + if pattern.is_null() { + return std::ptr::null_mut(); + } + + let bytes = unsafe { slice::from_raw_parts(pattern, pattern_len) }; + let pattern = bytes.iter().map(|byte| *byte as char).collect(); + + compile_pattern(pattern, flags, error_out, error_len_out) +} + /// Free an error string returned by `rust_regex_compile`. /// /// # Safety diff --git a/Libraries/LibRegex/Rust/src/parser.rs b/Libraries/LibRegex/Rust/src/parser.rs index 6b4f5a2c01..764f294414 100644 --- a/Libraries/LibRegex/Rust/src/parser.rs +++ b/Libraries/LibRegex/Rust/src/parser.rs @@ -23,6 +23,10 @@ pub fn parse(source: &str, flags: Flags) -> Result { Parser::new(source, flags).parse() } +pub fn parse_chars(source: Vec, flags: Flags) -> Result { + Parser::new_from_chars(source, flags).parse() +} + /// Parse a flags string like `"gimsu"` into [`Flags`]. pub fn parse_flags(s: &str) -> Result { let mut flags = Flags::default(); @@ -127,7 +131,10 @@ struct Parser { impl Parser { fn new(source: &str, flags: Flags) -> Self { - let chars: Vec = source.chars().collect(); + Self::new_from_chars(source.chars().collect(), flags) + } + + fn new_from_chars(chars: Vec, flags: Flags) -> Self { let (total, has_named) = Self::prescan_captures(&chars); Self { source: chars, diff --git a/Libraries/LibRegex/Rust/src/regex.rs b/Libraries/LibRegex/Rust/src/regex.rs index 6fb5112ba9..c4f9b4e4a7 100644 --- a/Libraries/LibRegex/Rust/src/regex.rs +++ b/Libraries/LibRegex/Rust/src/regex.rs @@ -43,6 +43,15 @@ impl Regex { /// Compile a regex pattern with the given flags. pub fn compile(pattern: &str, flags: Flags) -> Result { let parsed = parser::parse(pattern, flags)?; + Self::compile_parsed(parsed, flags) + } + + pub fn compile_chars(pattern: Vec, flags: Flags) -> Result { + let parsed = parser::parse_chars(pattern, flags)?; + Self::compile_parsed(parsed, flags) + } + + fn compile_parsed(parsed: Pattern, flags: Flags) -> Result { let mut program = compiler::compile(&parsed); Self::resolve_properties(&mut program); let required_literal_hint = extract_required_literal_hint(&parsed, flags); diff --git a/Libraries/LibRegex/RustRegex.cpp b/Libraries/LibRegex/RustRegex.cpp index 8f725798fc..9e34eb147f 100644 --- a/Libraries/LibRegex/RustRegex.cpp +++ b/Libraries/LibRegex/RustRegex.cpp @@ -8,17 +8,29 @@ namespace regex { -ErrorOr CompiledRustRegex::compile(StringView pattern, RustRegexFlags flags) +ErrorOr CompiledRustRegex::compile(Utf16View pattern, RustRegexFlags flags) { unsigned char const* error_ptr = nullptr; size_t error_len = 0; - auto* regex = rust_regex_compile( - reinterpret_cast(pattern.characters_without_null_termination()), - pattern.length(), - flags, - &error_ptr, - &error_len); + RustRegex* regex = nullptr; + if (pattern.has_ascii_storage()) { + auto ascii = pattern.ascii_span(); + regex = rust_regex_compile_ascii( + reinterpret_cast(ascii.data()), + ascii.size(), + flags, + &error_ptr, + &error_len); + } else { + auto utf16 = pattern.utf16_span(); + regex = rust_regex_compile( + reinterpret_cast(utf16.data()), + utf16.size(), + flags, + &error_ptr, + &error_len); + } if (!regex) { String error_message = "Invalid pattern"_string; if (error_ptr) { diff --git a/Libraries/LibRegex/RustRegex.h b/Libraries/LibRegex/RustRegex.h index ebde00279d..ad063f6cc1 100644 --- a/Libraries/LibRegex/RustRegex.h +++ b/Libraries/LibRegex/RustRegex.h @@ -25,7 +25,7 @@ class REGEX_API CompiledRustRegex { AK_MAKE_NONCOPYABLE(CompiledRustRegex); public: - static ErrorOr compile(StringView pattern, RustRegexFlags flags); + static ErrorOr compile(Utf16View pattern, RustRegexFlags flags); ~CompiledRustRegex(); CompiledRustRegex(CompiledRustRegex&& other); diff --git a/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Libraries/LibWeb/HTML/HTMLInputElement.cpp index 8cf6f7e161..fb7c550613 100644 --- a/Libraries/LibWeb/HTML/HTMLInputElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLInputElement.cpp @@ -14,6 +14,7 @@ */ #include +#include #include #include #include @@ -279,22 +280,26 @@ Optional HTMLInputElement::compiled_pattern_regular_expr return {}; // 2. Let pattern be the value of the pattern attribute of the element. - auto pattern = maybe_pattern.release_value(); + auto pattern = Utf16String::from_utf8(maybe_pattern.release_value()); // 3. Let regexpCompletion be RegExpCreate(pattern, "v"). regex::ECMAScriptCompileFlags compile_flags {}; compile_flags.unicode_sets = true; - auto regexp_completion = regex::ECMAScriptRegex::compile(pattern.bytes_as_string_view(), compile_flags); + auto regexp_completion = regex::ECMAScriptRegex::compile(pattern.utf16_view(), compile_flags); // 4. If regexpCompletion is an abrupt completion, then return nothing. The element has no compiled pattern regular expression. if (regexp_completion.is_error()) return {}; // 5. Let anchoredPattern be the string "^(?:", followed by pattern, followed by ")$". - auto anchored_pattern = MUST(String::formatted("^(?:{})$", pattern)); + Utf16StringBuilder anchored_pattern_builder; + anchored_pattern_builder.append_ascii("^(?:"sv); + anchored_pattern_builder.append(pattern.utf16_view()); + anchored_pattern_builder.append_ascii(")$"sv); + auto anchored_pattern = anchored_pattern_builder.to_string(); // 6. Return ! RegExpCreate(anchoredPattern, "v"). - auto anchored = regex::ECMAScriptRegex::compile(anchored_pattern.bytes_as_string_view(), compile_flags); + auto anchored = regex::ECMAScriptRegex::compile(anchored_pattern.utf16_view(), compile_flags); if (anchored.is_error()) return {}; return anchored.release_value(); @@ -3605,7 +3610,7 @@ bool HTMLInputElement::suffering_from_being_missing() const static regex::ECMAScriptRegex& valid_email_address_regex() { static NeverDestroyed regex { MUST(regex::ECMAScriptRegex::compile( - "^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"sv, + "^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"_utf16, regex::ECMAScriptCompileFlags {})) }; return *regex; } diff --git a/Meta/Fuzzers/FuzzRegexECMA262.cpp b/Meta/Fuzzers/FuzzRegexECMA262.cpp index 88b08c1e46..8c6234f563 100644 --- a/Meta/Fuzzers/FuzzRegexECMA262.cpp +++ b/Meta/Fuzzers/FuzzRegexECMA262.cpp @@ -5,6 +5,7 @@ */ #include +#include #include #include #include @@ -12,7 +13,7 @@ extern "C" int LLVMFuzzerTestOneInput(uint8_t const* data, size_t size) { AK::set_debug_enabled(false); - auto pattern = StringView(static_cast(data), size); - [[maybe_unused]] auto re = regex::ECMAScriptRegex::compile(pattern, {}); + auto pattern = Utf16String::from_utf8_with_replacement_character(StringView(static_cast(data), size)); + [[maybe_unused]] auto re = regex::ECMAScriptRegex::compile(pattern.utf16_view(), {}); return 0; } diff --git a/Tests/LibRegex/TestRegex.cpp b/Tests/LibRegex/TestRegex.cpp index 383af0700b..dac34bd865 100644 --- a/Tests/LibRegex/TestRegex.cpp +++ b/Tests/LibRegex/TestRegex.cpp @@ -11,14 +11,25 @@ #include +static ErrorOr compile_regex_result(Utf16View pattern, regex::ECMAScriptCompileFlags flags = {}) +{ + return regex::ECMAScriptRegex::compile(pattern, flags); +} + +static ErrorOr compile_regex_result(StringView pattern, regex::ECMAScriptCompileFlags flags = {}) +{ + auto utf16_pattern = Utf16String::from_utf8(pattern); + return compile_regex_result(utf16_pattern.utf16_view(), flags); +} + static regex::ECMAScriptRegex compile_regex(StringView pattern, regex::ECMAScriptCompileFlags flags = {}) { - return MUST(regex::ECMAScriptRegex::compile(pattern, flags)); + return MUST(compile_regex_result(pattern, flags)); } static bool compile_succeeds(StringView pattern, regex::ECMAScriptCompileFlags flags = {}) { - return !regex::ECMAScriptRegex::compile(pattern, flags).is_error(); + return !compile_regex_result(pattern, flags).is_error(); } static bool matches(StringView pattern, StringView subject, regex::ECMAScriptCompileFlags flags = {}) @@ -55,13 +66,13 @@ static void expect_capture_unmatched(regex::ECMAScriptRegex const& regex, unsign TEST_CASE(compile_rejects_invalid_pattern) { - auto regex = regex::ECMAScriptRegex::compile("("sv, {}); + auto regex = compile_regex_result("("sv, {}); EXPECT(regex.is_error()); } TEST_CASE(exec_tracks_named_capture_slots) { - auto regex = MUST(regex::ECMAScriptRegex::compile("(?foo)(bar)"sv, {})); + auto regex = MUST(compile_regex_result("(?foo)(bar)"sv, {})); EXPECT_EQ(regex.capture_count(), 2u); EXPECT_EQ(regex.total_groups(), 3u); @@ -80,7 +91,7 @@ TEST_CASE(exec_tracks_named_capture_slots) TEST_CASE(exec_reports_unmatched_optional_groups) { - auto regex = MUST(regex::ECMAScriptRegex::compile("(foo)?bar"sv, {})); + auto regex = MUST(compile_regex_result("(foo)?bar"sv, {})); EXPECT_EQ(regex.exec(u"bar"sv, 0), regex::MatchResult::Match); EXPECT_EQ(regex.capture_slot(0), 0); @@ -91,7 +102,7 @@ TEST_CASE(exec_reports_unmatched_optional_groups) TEST_CASE(ascii_backed_inputs_preserve_match_results) { - auto regex = MUST(regex::ECMAScriptRegex::compile("(?foo)(bar)"sv, {})); + auto regex = MUST(compile_regex_result("(?foo)(bar)"sv, {})); EXPECT_EQ(regex.exec("foobar"sv, 0), regex::MatchResult::Match); EXPECT_EQ(regex.capture_slot(0), 0); @@ -111,7 +122,7 @@ TEST_CASE(ascii_backed_inputs_preserve_match_results) TEST_CASE(test_honors_ignore_case) { - auto regex = MUST(regex::ECMAScriptRegex::compile("casesensitive"sv, { .ignore_case = true })); + auto regex = MUST(compile_regex_result("casesensitive"sv, { .ignore_case = true })); EXPECT_EQ(regex.test(u"CaseSensitive"sv, 0), regex::MatchResult::Match); EXPECT_EQ(regex.test(u"something else"sv, 0), regex::MatchResult::NoMatch); @@ -119,7 +130,7 @@ TEST_CASE(test_honors_ignore_case) TEST_CASE(ascii_ignore_case_literal_search_preserves_behavior) { - auto regex = MUST(regex::ECMAScriptRegex::compile("zfvr"sv, { .ignore_case = true })); + auto regex = MUST(compile_regex_result("zfvr"sv, { .ignore_case = true })); EXPECT_EQ(regex.exec("...ZFVR..."sv, 0), regex::MatchResult::Match); EXPECT_EQ(regex.capture_slot(0), 3); @@ -133,7 +144,7 @@ TEST_CASE(ascii_ignore_case_literal_search_preserves_behavior) TEST_CASE(ascii_ignore_case_literal_search_handles_punctuation_prefixes) { - auto regex = MUST(regex::ECMAScriptRegex::compile("##yv22##"sv, { .ignore_case = true })); + auto regex = MUST(compile_regex_result("##yv22##"sv, { .ignore_case = true })); EXPECT_EQ(regex.find_all("##YV22## and ##yv22##"sv, 0), 2); EXPECT_EQ(regex.find_all_match(0).start, 0); @@ -144,7 +155,7 @@ TEST_CASE(ascii_ignore_case_literal_search_handles_punctuation_prefixes) TEST_CASE(ascii_ignore_case_literal_alternation_preserves_behavior) { - auto regex = MUST(regex::ECMAScriptRegex::compile("##yv22##|zfvr|puebzr"sv, { .ignore_case = true })); + auto regex = MUST(compile_regex_result("##yv22##|zfvr|puebzr"sv, { .ignore_case = true })); EXPECT_EQ(regex.find_all("##YV22## zFVr PUEBZR"sv, 0), 3); EXPECT_EQ(regex.find_all_match(0).start, 0); @@ -157,7 +168,7 @@ TEST_CASE(ascii_ignore_case_literal_alternation_preserves_behavior) TEST_CASE(ascii_ignore_case_literal_alternation_respects_source_order) { - auto regex = MUST(regex::ECMAScriptRegex::compile("foo|f"sv, { .ignore_case = true })); + auto regex = MUST(compile_regex_result("foo|f"sv, { .ignore_case = true })); EXPECT_EQ(regex.exec("FoO"sv, 0), regex::MatchResult::Match); EXPECT_EQ(regex.capture_slot(0), 0); @@ -166,7 +177,7 @@ TEST_CASE(ascii_ignore_case_literal_alternation_respects_source_order) TEST_CASE(unicode_ignore_case_literal_alternation_preserves_behavior) { - auto regex = MUST(regex::ECMAScriptRegex::compile("s|k"sv, { .ignore_case = true, .unicode = true })); + auto regex = MUST(compile_regex_result("s|k"sv, { .ignore_case = true, .unicode = true })); EXPECT_EQ(regex.test(u"\u017F"sv, 0), regex::MatchResult::Match); EXPECT_EQ(regex.test(u"\u212A"sv, 0), regex::MatchResult::Match); @@ -174,7 +185,7 @@ TEST_CASE(unicode_ignore_case_literal_alternation_preserves_behavior) TEST_CASE(word_boundary_literal_preserves_behavior) { - auto regex = MUST(regex::ECMAScriptRegex::compile("\\bfoo\\b"sv, {})); + auto regex = MUST(compile_regex_result("\\bfoo\\b"sv, {})); EXPECT_EQ(regex.find_all("foo foo-bar barfoo foo2 _foo foo_"sv, 0), 2); EXPECT_EQ(regex.find_all_match(0).start, 0); @@ -185,7 +196,7 @@ TEST_CASE(word_boundary_literal_preserves_behavior) TEST_CASE(ascii_ignore_case_word_boundary_literal_preserves_behavior) { - auto regex = MUST(regex::ECMAScriptRegex::compile("\\bzfvr\\b"sv, { .ignore_case = true })); + auto regex = MUST(compile_regex_result("\\bzfvr\\b"sv, { .ignore_case = true })); EXPECT_EQ(regex.find_all("ZFVR zfvr1 _ZFVR zFVr"sv, 0), 2); EXPECT_EQ(regex.find_all_match(0).start, 0); @@ -196,14 +207,14 @@ TEST_CASE(ascii_ignore_case_word_boundary_literal_preserves_behavior) TEST_CASE(unicode_ignore_case_word_boundary_literal_preserves_behavior) { - auto regex = MUST(regex::ECMAScriptRegex::compile("\\bk\\b"sv, { .ignore_case = true, .unicode = true })); + auto regex = MUST(compile_regex_result("\\bk\\b"sv, { .ignore_case = true, .unicode = true })); EXPECT_EQ(regex.test(u"\u212A"sv, 0), regex::MatchResult::Match); } TEST_CASE(mixed_positive_class_with_word_builtin_preserves_legacy_ignore_case_behavior) { - auto regex = MUST(regex::ECMAScriptRegex::compile("[\\w\\$]+"sv, { .ignore_case = true })); + auto regex = MUST(compile_regex_result("[\\w\\$]+"sv, { .ignore_case = true })); EXPECT_EQ(regex.test("AZ_09$"sv, 0), regex::MatchResult::Match); EXPECT_EQ(regex.test(u"\u017F"sv, 0), regex::MatchResult::NoMatch); @@ -212,7 +223,7 @@ TEST_CASE(mixed_positive_class_with_word_builtin_preserves_legacy_ignore_case_be TEST_CASE(mixed_positive_class_with_digit_builtin_preserves_behavior) { - auto regex = MUST(regex::ECMAScriptRegex::compile("[A-Z\\d-]+"sv, { .ignore_case = true })); + auto regex = MUST(compile_regex_result("[A-Z\\d-]+"sv, { .ignore_case = true })); EXPECT_EQ(regex.test("ABC-123"sv, 0), regex::MatchResult::Match); EXPECT_EQ(regex.test("abc"sv, 0), regex::MatchResult::Match); @@ -221,7 +232,7 @@ TEST_CASE(mixed_positive_class_with_digit_builtin_preserves_behavior) TEST_CASE(find_all_returns_non_overlapping_matches) { - auto regex = MUST(regex::ECMAScriptRegex::compile("aba"sv, {})); + auto regex = MUST(compile_regex_result("aba"sv, {})); EXPECT_EQ(regex.find_all(u"aba aba"sv, 0), 2); EXPECT_EQ(regex.find_all_match(0).start, 0); @@ -232,7 +243,7 @@ TEST_CASE(find_all_returns_non_overlapping_matches) TEST_CASE(unicode_property_matching_works) { - auto regex = MUST(regex::ECMAScriptRegex::compile("\\p{ASCII}+"sv, { .unicode = true })); + auto regex = MUST(compile_regex_result("\\p{ASCII}+"sv, { .unicode = true })); EXPECT_EQ(regex.test(u"ASCII"sv, 0), regex::MatchResult::Match); EXPECT_EQ(regex.test(u"😀"sv, 0), regex::MatchResult::NoMatch); @@ -240,7 +251,7 @@ TEST_CASE(unicode_property_matching_works) TEST_CASE(end_anchored_suffix_patterns_preserve_behavior) { - auto regex = MUST(regex::ECMAScriptRegex::compile("(.*)\\/client-(.*)\\.js$"sv, {})); + auto regex = MUST(compile_regex_result("(.*)\\/client-(.*)\\.js$"sv, {})); EXPECT_EQ(regex.test(u"https://cdn.example.com/assets/client-main.js"sv, 0), regex::MatchResult::Match); EXPECT_EQ(regex.test(u""sv, 0), regex::MatchResult::NoMatch); @@ -248,7 +259,7 @@ TEST_CASE(end_anchored_suffix_patterns_preserve_behavior) TEST_CASE(leading_start_or_separator_prefix_preserves_behavior) { - auto regex = MUST(regex::ECMAScriptRegex::compile("(?:^|;)\\s*foo=([^;]*)"sv, {})); + auto regex = MUST(compile_regex_result("(?:^|;)\\s*foo=([^;]*)"sv, {})); { auto subject = Utf16String::from_utf8("foo=bar"sv); @@ -267,7 +278,7 @@ TEST_CASE(leading_start_or_separator_prefix_preserves_behavior) TEST_CASE(required_literal_prefilter_preserves_assignment_extractors) { - auto regex = MUST(regex::ECMAScriptRegex::compile("(?:^|;)\\s*foo=([^;]*)"sv, {})); + auto regex = MUST(compile_regex_result("(?:^|;)\\s*foo=([^;]*)"sv, {})); { auto subject = Utf16String::from_utf8("a=1; bar=baz; foo=qux"sv); @@ -280,7 +291,7 @@ TEST_CASE(required_literal_prefilter_preserves_assignment_extractors) TEST_CASE(ascii_ignore_case_required_literal_prefilter_preserves_behavior) { - auto regex = MUST(regex::ECMAScriptRegex::compile("\\bfoo\\s*=\\s*([^;]*)"sv, { .ignore_case = true })); + auto regex = MUST(compile_regex_result("\\bfoo\\s*=\\s*([^;]*)"sv, { .ignore_case = true })); { auto subject = Utf16String::from_utf8("FOO = Bar"sv); @@ -293,7 +304,7 @@ TEST_CASE(ascii_ignore_case_required_literal_prefilter_preserves_behavior) TEST_CASE(required_literal_prefilter_handles_common_substrings_across_alternatives) { - auto regex = MUST(regex::ECMAScriptRegex::compile("(\\$\\{name\\})|(\\$name\\b)"sv, {})); + auto regex = MUST(compile_regex_result("(\\$\\{name\\})|(\\$name\\b)"sv, {})); EXPECT_EQ(regex.find_all("${name} $name"sv, 0), 2); EXPECT_EQ(regex.find_all_match(0).start, 0); @@ -327,7 +338,7 @@ TEST_CASE(required_literal_prefilter_compiles_long_literal_alternations) pattern_builder.append("c"sv); auto pattern = MUST(pattern_builder.to_string()); - auto regex = MUST(regex::ECMAScriptRegex::compile(pattern, {})); + auto regex = MUST(compile_regex_result(pattern, {})); StringBuilder subject_builder; subject_builder.append(shared_prefix);