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.
This commit is contained in:
Andreas Kling 2026-06-22 13:07:20 +02:00 committed by Andreas Kling
parent bcf7f27a4f
commit 2d20322fce
15 changed files with 192 additions and 104 deletions

View file

@ -6,6 +6,7 @@
#include <AK/CharacterTypes.h> #include <AK/CharacterTypes.h>
#include <AK/Find.h> #include <AK/Find.h>
#include <AK/Utf16StringBuilder.h>
#include <LibJS/Runtime/Error.h> #include <LibJS/Runtime/Error.h>
#include <LibJS/Runtime/GlobalObject.h> #include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/RegExpConstructor.h> #include <LibJS/Runtime/RegExpConstructor.h>
@ -179,7 +180,7 @@ ThrowCompletionOr<GC::Ref<Object>> RegExpConstructor::construct_impl(FunctionObj
} }
// 22.2.5.1.1 EncodeForRegExpEscape ( cp ), https://tc39.es/ecma262/#sec-encodeforregexpescape // 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 // https://tc39.es/ecma262/#table-controlescape-code-point-values
// Table 63: 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 // 1. If c is matched by SyntaxCharacter or c is U+002F (SOLIDUS), then
if (is_syntax_character(code_point) || code_point == '/') { if (is_syntax_character(code_point) || code_point == '/') {
// a. Return the string-concatenation of 0x005C (REVERSE SOLIDUS) and UTF16EncodeCodePoint(c). // 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 // 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()) { if (it != control_escapes.end()) {
// a. Return the string-concatenation of 0x005C (REVERSE SOLIDUS) and the string in the “ControlEscape” column // 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. // 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). // 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). // i. Let hex be Number::toString(𝔽(cNum), 16).
// ii. Return the string-concatenation of the code unit 0x005C (REVERSE SOLIDUS), "x", and // ii. Return the string-concatenation of the code unit 0x005C (REVERSE SOLIDUS), "x", and
// StringPad(hex, 2, "0", START). // 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. // 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 // e. For each code unit cu of codeUnits, do
// i. Set escaped to the string-concatenation of escaped and UnicodeEscape(cu). // i. Set escaped to the string-concatenation of escaped and UnicodeEscape(cu).
// f. Return escaped. // f. Return escaped.
return MUST(String::formatted("\\u{:04x}", code_point)); return Utf16String::formatted("\\u{:04x}", code_point);
} }
// 6. Return UTF16EncodeCodePoint(c). // 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 // 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<TypeError>(ErrorType::NotAString, string); return vm.throw_completion<TypeError>(ErrorType::NotAString, string);
// 2. Let escaped be the empty 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). // 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 // 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 // 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)) { 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 // 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. // 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 // 22.2.5.3 get RegExp [ %Symbol.species% ], https://tc39.es/ecma262/#sec-get-regexp-@@species

View file

@ -261,14 +261,14 @@ static Result<RegExpObject::Flags, String> validate_flags(Utf16View const& flags
} }
// 22.2.3.4 Static Semantics: ParsePattern ( patternText, u, v ), https://tc39.es/ecma262/#sec-parsepattern // 22.2.3.4 Static Semantics: ParsePattern ( patternText, u, v ), https://tc39.es/ecma262/#sec-parsepattern
ErrorOr<String, ParseRegexPatternError> parse_regex_pattern(Utf16View const& pattern, bool unicode, bool unicode_sets) ErrorOr<Utf16String, ParseRegexPatternError> parse_regex_pattern(Utf16View const& pattern, bool unicode, bool unicode_sets)
{ {
if (unicode && unicode_sets) if (unicode && unicode_sets)
return ParseRegexPatternError { MUST(String::formatted(ErrorType::RegExpObjectIncompatibleFlags.format(), 'u', 'v')) }; return ParseRegexPatternError { MUST(String::formatted(ErrorType::RegExpObjectIncompatibleFlags.format(), 'u', 'v')) };
TRY(validate_named_group_name_surrogates(pattern, unicode || unicode_sets)); TRY(validate_named_group_name_surrogates(pattern, unicode || unicode_sets));
StringBuilder builder; Utf16StringBuilder builder;
auto previous_code_unit_was_backslash = false; auto previous_code_unit_was_backslash = false;
for (size_t i = 0; i < pattern.length_in_code_units(); ++i) { for (size_t i = 0; i < pattern.length_in_code_units(); ++i) {
@ -280,7 +280,7 @@ ErrorOr<String, ParseRegexPatternError> parse_regex_pattern(Utf16View const& pat
// leading to a matcher for the literal string "\uhhhh" instead of the intended code unit <c>. // leading to a matcher for the literal string "\uhhhh" instead of the intended code unit <c>.
// As such, we're going to remove the (invalid) backslash and pretend it never existed. // As such, we're going to remove the (invalid) backslash and pretend it never existed.
if (!previous_code_unit_was_backslash) 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()) { 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); u16 next_code_unit = pattern.code_unit_at(i + 1);
@ -298,7 +298,7 @@ ErrorOr<String, ParseRegexPatternError> parse_regex_pattern(Utf16View const& pat
else else
builder.appendff("u{:04x}", code_unit); builder.appendff("u{:04x}", code_unit);
} else { } else {
builder.append_code_point(code_unit); builder.append_code_unit(code_unit);
} }
if (code_unit == '\\') if (code_unit == '\\')
@ -307,11 +307,11 @@ ErrorOr<String, ParseRegexPatternError> parse_regex_pattern(Utf16View const& pat
previous_code_unit_was_backslash = false; 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 // 22.2.3.4 Static Semantics: ParsePattern ( patternText, u, v ), https://tc39.es/ecma262/#sec-parsepattern
ThrowCompletionOr<String> parse_regex_pattern(VM& vm, Utf16View const& pattern, bool unicode, bool unicode_sets) ThrowCompletionOr<Utf16String> parse_regex_pattern(VM& vm, Utf16View const& pattern, bool unicode, bool unicode_sets)
{ {
auto result = parse_regex_pattern(pattern, unicode, unicode_sets); auto result = parse_regex_pattern(pattern, unicode, unicode_sets);
if (result.is_error()) if (result.is_error())
@ -402,9 +402,9 @@ ThrowCompletionOr<GC::Ref<RegExpObject>> RegExpObject::regexp_initialize(VM& vm,
bool unicode = has_flag(flag_bits, Flags::Unicode); bool unicode = has_flag(flag_bits, Flags::Unicode);
bool unicode_sets = has_flag(flag_bits, Flags::UnicodeSets); 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()) { if (!pattern.is_empty()) {
auto result = parse_regex_pattern(pattern, unicode, unicode_sets); auto result = parse_regex_pattern(pattern, unicode, unicode_sets);
if (result.is_error()) if (result.is_error())
@ -425,7 +425,7 @@ ThrowCompletionOr<GC::Ref<RegExpObject>> RegExpObject::regexp_initialize(VM& vm,
compile_flags.unicode_sets = unicode_sets; compile_flags.unicode_sets = unicode_sets;
compile_flags.sticky = has_flag(flag_bits, Flags::Sticky); 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()) if (compiled.is_error())
return vm.throw_completion<SyntaxError>(ErrorType::RegExpCompileError, compiled.release_error()); return vm.throw_completion<SyntaxError>(ErrorType::RegExpCompileError, compiled.release_error());
@ -449,7 +449,7 @@ ThrowCompletionOr<GC::Ref<RegExpObject>> RegExpObject::regexp_initialize(VM& vm,
} }
// 22.2.6.13.1 EscapeRegExpPattern ( P, F ), https://tc39.es/ecma262/#sec-escaperegexppattern // 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 // 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 // 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 "(?:)". // specification can be met by letting S be "(?:)".
// 3. Return S. // 3. Return S.
if (m_pattern.is_empty()) if (m_pattern.is_empty())
return "(?:)"_string; return "(?:)"_utf16;
// FIXME: Check the 'u' and 'v' flags and escape accordingly // FIXME: Check the 'u' and 'v' flags and escape accordingly
StringBuilder builder; Utf16StringBuilder builder;
auto escaped = false; auto escaped = false;
auto in_character_class = false; auto in_character_class = false;
for (auto code_point : m_pattern) { for (auto code_point : m_pattern) {
if (escaped) { if (escaped) {
escaped = false; escaped = false;
builder.append('\\'); builder.append_ascii('\\');
switch (code_point) { switch (code_point) {
case '\n': case '\n':
builder.append('n'); builder.append_ascii('n');
break; break;
case '\r': case '\r':
builder.append('r'); builder.append_ascii('r');
break; break;
case LINE_SEPARATOR: case LINE_SEPARATOR:
builder.append("u2028"sv); builder.append_ascii("u2028"sv);
break; break;
case PARAGRAPH_SEPARATOR: case PARAGRAPH_SEPARATOR:
builder.append("u2029"sv); builder.append_ascii("u2029"sv);
break; break;
default: default:
builder.append_code_point(code_point); builder.append_code_point(code_point);
@ -511,21 +511,21 @@ String RegExpObject::escape_regexp_pattern() const
switch (code_point) { switch (code_point) {
case '/': case '/':
if (in_character_class) if (in_character_class)
builder.append('/'); builder.append_ascii('/');
else else
builder.append("\\/"sv); builder.append_ascii("\\/"sv);
break; break;
case '\n': case '\n':
builder.append("\\n"sv); builder.append_ascii("\\n"sv);
break; break;
case '\r': case '\r':
builder.append("\\r"sv); builder.append_ascii("\\r"sv);
break; break;
case LINE_SEPARATOR: case LINE_SEPARATOR:
builder.append("\\u2028"sv); builder.append_ascii("\\u2028"sv);
break; break;
case PARAGRAPH_SEPARATOR: case PARAGRAPH_SEPARATOR:
builder.append("\\u2029"sv); builder.append_ascii("\\u2029"sv);
break; break;
default: default:
builder.append_code_point(code_point); 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) void RegExpObject::visit_edges(JS::Cell::Visitor& visitor)

View file

@ -22,8 +22,8 @@ ThrowCompletionOr<GC::Ref<RegExpObject>> regexp_alloc(VM&, FunctionObject& new_t
struct ParseRegexPatternError { struct ParseRegexPatternError {
String error; String error;
}; };
ErrorOr<String, ParseRegexPatternError> parse_regex_pattern(Utf16View const& pattern, bool unicode, bool unicode_sets); ErrorOr<Utf16String, ParseRegexPatternError> parse_regex_pattern(Utf16View const& pattern, bool unicode, bool unicode_sets);
ThrowCompletionOr<String> parse_regex_pattern(VM& vm, Utf16View const& pattern, bool unicode, bool unicode_sets); ThrowCompletionOr<Utf16String> parse_regex_pattern(VM& vm, Utf16View const& pattern, bool unicode, bool unicode_sets);
class JS_API RegExpObject : public Object { class JS_API RegExpObject : public Object {
JS_OBJECT(RegExpObject, Object); JS_OBJECT(RegExpObject, Object);
@ -45,7 +45,7 @@ public:
static GC::Ref<RegExpObject> create(Realm&, Utf16String pattern, Utf16String flags); static GC::Ref<RegExpObject> create(Realm&, Utf16String pattern, Utf16String flags);
ThrowCompletionOr<GC::Ref<RegExpObject>> regexp_initialize(VM&, Value pattern, Value flags); ThrowCompletionOr<GC::Ref<RegExpObject>> regexp_initialize(VM&, Value pattern, Value flags);
String escape_regexp_pattern() const; Utf16String escape_regexp_pattern() const;
virtual void initialize(Realm&) override; virtual void initialize(Realm&) override;
virtual ~RegExpObject() override = default; virtual ~RegExpObject() override = default;

View file

@ -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.sticky = has_flag(flag_bits, RegExpObject::Flags::Sticky);
flags.has_indices = has_flag(flag_bits, RegExpObject::Flags::HasIndices); 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()) if (compiled.is_error())
return nullptr; return nullptr;

View file

@ -1160,7 +1160,7 @@ void free_function_ast(void* ast)
namespace JS::FFI { namespace JS::FFI {
struct RustCompiledRegex { struct RustCompiledRegex {
String parsed_pattern; Utf16String parsed_pattern;
}; };
static Utf16View view_from_ffi(FFIUtf16Slice slice) 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()) { if (compiled.is_error()) {
auto msg = MUST(String::formatted("RegExp compile error: {}", compiled.release_error())); auto msg = MUST(String::formatted("RegExp compile error: {}", compiled.release_error()));
auto* buf = static_cast<char*>(kmalloc(msg.byte_count() + 1)); auto* buf = static_cast<char*>(kmalloc(msg.byte_count() + 1));

View file

@ -15,7 +15,7 @@ struct ECMAScriptRegex::Impl {
Vector<ECMAScriptNamedCaptureGroup> named_groups; Vector<ECMAScriptNamedCaptureGroup> named_groups;
}; };
ErrorOr<ECMAScriptRegex, String> ECMAScriptRegex::compile(StringView utf8_pattern, ECMAScriptCompileFlags flags) ErrorOr<ECMAScriptRegex, String> ECMAScriptRegex::compile(Utf16View pattern, ECMAScriptCompileFlags flags)
{ {
RustRegexFlags rust_flags {}; RustRegexFlags rust_flags {};
rust_flags.global = flags.global; rust_flags.global = flags.global;
@ -27,7 +27,7 @@ ErrorOr<ECMAScriptRegex, String> ECMAScriptRegex::compile(StringView utf8_patter
rust_flags.sticky = flags.sticky; rust_flags.sticky = flags.sticky;
rust_flags.has_indices = flags.has_indices; 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()) if (compiled.is_error())
return compiled.release_error(); return compiled.release_error();

View file

@ -42,7 +42,7 @@ class REGEX_API ECMAScriptRegex {
AK_MAKE_NONCOPYABLE(ECMAScriptRegex); AK_MAKE_NONCOPYABLE(ECMAScriptRegex);
public: public:
static ErrorOr<ECMAScriptRegex, String> compile(StringView utf8_pattern, ECMAScriptCompileFlags); static ErrorOr<ECMAScriptRegex, String> compile(Utf16View pattern, ECMAScriptCompileFlags);
~ECMAScriptRegex(); ~ECMAScriptRegex();
ECMAScriptRegex(ECMAScriptRegex&&); ECMAScriptRegex(ECMAScriptRegex&&);

View file

@ -28,29 +28,12 @@ pub struct RustRegexFlags {
pub has_indices: bool, pub has_indices: bool,
} }
/// Compile a regex pattern. Returns an opaque handle, or null on error. fn compile_pattern(
/// On error, writes the error message to `error_out` and `error_len_out`. pattern: Vec<char>,
/// 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,
flags: RustRegexFlags, flags: RustRegexFlags,
error_out: *mut *const u8, error_out: *mut *const u8,
error_len_out: *mut usize, error_len_out: *mut usize,
) -> *mut RustRegex { ) -> *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 { let flags = Flags {
global: flags.global, global: flags.global,
ignore_case: flags.ignore_case, ignore_case: flags.ignore_case,
@ -62,7 +45,7 @@ pub unsafe extern "C" fn rust_regex_compile(
has_indices: flags.has_indices, 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))), Ok(regex) => Box::into_raw(Box::new(RustRegex(regex))),
Err(e) => { Err(e) => {
if !error_out.is_null() && !error_len_out.is_null() { 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`. /// Free an error string returned by `rust_regex_compile`.
/// ///
/// # Safety /// # Safety

View file

@ -23,6 +23,10 @@ pub fn parse(source: &str, flags: Flags) -> Result<Pattern, Error> {
Parser::new(source, flags).parse() Parser::new(source, flags).parse()
} }
pub fn parse_chars(source: Vec<char>, flags: Flags) -> Result<Pattern, Error> {
Parser::new_from_chars(source, flags).parse()
}
/// Parse a flags string like `"gimsu"` into [`Flags`]. /// Parse a flags string like `"gimsu"` into [`Flags`].
pub fn parse_flags(s: &str) -> Result<Flags, Error> { pub fn parse_flags(s: &str) -> Result<Flags, Error> {
let mut flags = Flags::default(); let mut flags = Flags::default();
@ -127,7 +131,10 @@ struct Parser {
impl Parser { impl Parser {
fn new(source: &str, flags: Flags) -> Self { fn new(source: &str, flags: Flags) -> Self {
let chars: Vec<char> = source.chars().collect(); Self::new_from_chars(source.chars().collect(), flags)
}
fn new_from_chars(chars: Vec<char>, flags: Flags) -> Self {
let (total, has_named) = Self::prescan_captures(&chars); let (total, has_named) = Self::prescan_captures(&chars);
Self { Self {
source: chars, source: chars,

View file

@ -43,6 +43,15 @@ impl Regex {
/// Compile a regex pattern with the given flags. /// Compile a regex pattern with the given flags.
pub fn compile(pattern: &str, flags: Flags) -> Result<Self, parser::Error> { pub fn compile(pattern: &str, flags: Flags) -> Result<Self, parser::Error> {
let parsed = parser::parse(pattern, flags)?; let parsed = parser::parse(pattern, flags)?;
Self::compile_parsed(parsed, flags)
}
pub fn compile_chars(pattern: Vec<char>, flags: Flags) -> Result<Self, parser::Error> {
let parsed = parser::parse_chars(pattern, flags)?;
Self::compile_parsed(parsed, flags)
}
fn compile_parsed(parsed: Pattern, flags: Flags) -> Result<Self, parser::Error> {
let mut program = compiler::compile(&parsed); let mut program = compiler::compile(&parsed);
Self::resolve_properties(&mut program); Self::resolve_properties(&mut program);
let required_literal_hint = extract_required_literal_hint(&parsed, flags); let required_literal_hint = extract_required_literal_hint(&parsed, flags);

View file

@ -8,17 +8,29 @@
namespace regex { namespace regex {
ErrorOr<CompiledRustRegex, String> CompiledRustRegex::compile(StringView pattern, RustRegexFlags flags) ErrorOr<CompiledRustRegex, String> CompiledRustRegex::compile(Utf16View pattern, RustRegexFlags flags)
{ {
unsigned char const* error_ptr = nullptr; unsigned char const* error_ptr = nullptr;
size_t error_len = 0; size_t error_len = 0;
auto* regex = rust_regex_compile( RustRegex* regex = nullptr;
reinterpret_cast<unsigned char const*>(pattern.characters_without_null_termination()), if (pattern.has_ascii_storage()) {
pattern.length(), auto ascii = pattern.ascii_span();
flags, regex = rust_regex_compile_ascii(
&error_ptr, reinterpret_cast<unsigned char const*>(ascii.data()),
&error_len); ascii.size(),
flags,
&error_ptr,
&error_len);
} else {
auto utf16 = pattern.utf16_span();
regex = rust_regex_compile(
reinterpret_cast<unsigned short const*>(utf16.data()),
utf16.size(),
flags,
&error_ptr,
&error_len);
}
if (!regex) { if (!regex) {
String error_message = "Invalid pattern"_string; String error_message = "Invalid pattern"_string;
if (error_ptr) { if (error_ptr) {

View file

@ -25,7 +25,7 @@ class REGEX_API CompiledRustRegex {
AK_MAKE_NONCOPYABLE(CompiledRustRegex); AK_MAKE_NONCOPYABLE(CompiledRustRegex);
public: public:
static ErrorOr<CompiledRustRegex, String> compile(StringView pattern, RustRegexFlags flags); static ErrorOr<CompiledRustRegex, String> compile(Utf16View pattern, RustRegexFlags flags);
~CompiledRustRegex(); ~CompiledRustRegex();
CompiledRustRegex(CompiledRustRegex&& other); CompiledRustRegex(CompiledRustRegex&& other);

View file

@ -14,6 +14,7 @@
*/ */
#include <AK/NeverDestroyed.h> #include <AK/NeverDestroyed.h>
#include <AK/Utf16StringBuilder.h>
#include <LibGfx/DecodedImageFrame.h> #include <LibGfx/DecodedImageFrame.h>
#include <LibJS/Runtime/Date.h> #include <LibJS/Runtime/Date.h>
#include <LibJS/Runtime/NativeFunction.h> #include <LibJS/Runtime/NativeFunction.h>
@ -279,22 +280,26 @@ Optional<regex::ECMAScriptRegex> HTMLInputElement::compiled_pattern_regular_expr
return {}; return {};
// 2. Let pattern be the value of the pattern attribute of the element. // 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"). // 3. Let regexpCompletion be RegExpCreate(pattern, "v").
regex::ECMAScriptCompileFlags compile_flags {}; regex::ECMAScriptCompileFlags compile_flags {};
compile_flags.unicode_sets = true; 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. // 4. If regexpCompletion is an abrupt completion, then return nothing. The element has no compiled pattern regular expression.
if (regexp_completion.is_error()) if (regexp_completion.is_error())
return {}; return {};
// 5. Let anchoredPattern be the string "^(?:", followed by pattern, followed by ")$". // 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"). // 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()) if (anchored.is_error())
return {}; return {};
return anchored.release_value(); return anchored.release_value();
@ -3605,7 +3610,7 @@ bool HTMLInputElement::suffering_from_being_missing() const
static regex::ECMAScriptRegex& valid_email_address_regex() static regex::ECMAScriptRegex& valid_email_address_regex()
{ {
static NeverDestroyed<regex::ECMAScriptRegex> regex { MUST(regex::ECMAScriptRegex::compile( static NeverDestroyed<regex::ECMAScriptRegex> 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 {})) }; regex::ECMAScriptCompileFlags {})) };
return *regex; return *regex;
} }

View file

@ -5,6 +5,7 @@
*/ */
#include <AK/StringView.h> #include <AK/StringView.h>
#include <AK/Utf16String.h>
#include <LibRegex/ECMAScriptRegex.h> #include <LibRegex/ECMAScriptRegex.h>
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
@ -12,7 +13,7 @@
extern "C" int LLVMFuzzerTestOneInput(uint8_t const* data, size_t size) extern "C" int LLVMFuzzerTestOneInput(uint8_t const* data, size_t size)
{ {
AK::set_debug_enabled(false); AK::set_debug_enabled(false);
auto pattern = StringView(static_cast<unsigned char const*>(data), size); auto pattern = Utf16String::from_utf8_with_replacement_character(StringView(static_cast<unsigned char const*>(data), size));
[[maybe_unused]] auto re = regex::ECMAScriptRegex::compile(pattern, {}); [[maybe_unused]] auto re = regex::ECMAScriptRegex::compile(pattern.utf16_view(), {});
return 0; return 0;
} }

View file

@ -11,14 +11,25 @@
#include <LibRegex/ECMAScriptRegex.h> #include <LibRegex/ECMAScriptRegex.h>
static ErrorOr<regex::ECMAScriptRegex, String> compile_regex_result(Utf16View pattern, regex::ECMAScriptCompileFlags flags = {})
{
return regex::ECMAScriptRegex::compile(pattern, flags);
}
static ErrorOr<regex::ECMAScriptRegex, String> 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 = {}) 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 = {}) 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 = {}) 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) TEST_CASE(compile_rejects_invalid_pattern)
{ {
auto regex = regex::ECMAScriptRegex::compile("("sv, {}); auto regex = compile_regex_result("("sv, {});
EXPECT(regex.is_error()); EXPECT(regex.is_error());
} }
TEST_CASE(exec_tracks_named_capture_slots) TEST_CASE(exec_tracks_named_capture_slots)
{ {
auto regex = MUST(regex::ECMAScriptRegex::compile("(?<word>foo)(bar)"sv, {})); auto regex = MUST(compile_regex_result("(?<word>foo)(bar)"sv, {}));
EXPECT_EQ(regex.capture_count(), 2u); EXPECT_EQ(regex.capture_count(), 2u);
EXPECT_EQ(regex.total_groups(), 3u); EXPECT_EQ(regex.total_groups(), 3u);
@ -80,7 +91,7 @@ TEST_CASE(exec_tracks_named_capture_slots)
TEST_CASE(exec_reports_unmatched_optional_groups) 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.exec(u"bar"sv, 0), regex::MatchResult::Match);
EXPECT_EQ(regex.capture_slot(0), 0); 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) TEST_CASE(ascii_backed_inputs_preserve_match_results)
{ {
auto regex = MUST(regex::ECMAScriptRegex::compile("(?<word>foo)(bar)"sv, {})); auto regex = MUST(compile_regex_result("(?<word>foo)(bar)"sv, {}));
EXPECT_EQ(regex.exec("foobar"sv, 0), regex::MatchResult::Match); EXPECT_EQ(regex.exec("foobar"sv, 0), regex::MatchResult::Match);
EXPECT_EQ(regex.capture_slot(0), 0); 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) 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"CaseSensitive"sv, 0), regex::MatchResult::Match);
EXPECT_EQ(regex.test(u"something else"sv, 0), regex::MatchResult::NoMatch); 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) 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.exec("...ZFVR..."sv, 0), regex::MatchResult::Match);
EXPECT_EQ(regex.capture_slot(0), 3); 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) 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("##YV22## and ##yv22##"sv, 0), 2);
EXPECT_EQ(regex.find_all_match(0).start, 0); 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) 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("##YV22## zFVr PUEBZR"sv, 0), 3);
EXPECT_EQ(regex.find_all_match(0).start, 0); 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) 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.exec("FoO"sv, 0), regex::MatchResult::Match);
EXPECT_EQ(regex.capture_slot(0), 0); 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) 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"\u017F"sv, 0), regex::MatchResult::Match);
EXPECT_EQ(regex.test(u"\u212A"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) 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("foo foo-bar barfoo foo2 _foo foo_"sv, 0), 2);
EXPECT_EQ(regex.find_all_match(0).start, 0); 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) 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("ZFVR zfvr1 _ZFVR zFVr"sv, 0), 2);
EXPECT_EQ(regex.find_all_match(0).start, 0); 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) 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); EXPECT_EQ(regex.test(u"\u212A"sv, 0), regex::MatchResult::Match);
} }
TEST_CASE(mixed_positive_class_with_word_builtin_preserves_legacy_ignore_case_behavior) 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("AZ_09$"sv, 0), regex::MatchResult::Match);
EXPECT_EQ(regex.test(u"\u017F"sv, 0), regex::MatchResult::NoMatch); 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) 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-123"sv, 0), regex::MatchResult::Match);
EXPECT_EQ(regex.test("abc"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) 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(u"aba aba"sv, 0), 2);
EXPECT_EQ(regex.find_all_match(0).start, 0); 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) 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"ASCII"sv, 0), regex::MatchResult::Match);
EXPECT_EQ(regex.test(u"😀"sv, 0), regex::MatchResult::NoMatch); 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) 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"https://cdn.example.com/assets/client-main.js"sv, 0), regex::MatchResult::Match);
EXPECT_EQ(regex.test(u"<script src=\"/assets/client-main.js\"></script>"sv, 0), regex::MatchResult::NoMatch); EXPECT_EQ(regex.test(u"<script src=\"/assets/client-main.js\"></script>"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) 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); 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) 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); 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) 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); 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) 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("${name} $name"sv, 0), 2);
EXPECT_EQ(regex.find_all_match(0).start, 0); 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); pattern_builder.append("c"sv);
auto pattern = MUST(pattern_builder.to_string()); auto pattern = MUST(pattern_builder.to_string());
auto regex = MUST(regex::ECMAScriptRegex::compile(pattern, {})); auto regex = MUST(compile_regex_result(pattern, {}));
StringBuilder subject_builder; StringBuilder subject_builder;
subject_builder.append(shared_prefix); subject_builder.append(shared_prefix);