use v8 RegExp for Input.suffersPatternMismatch

This commit is contained in:
Pierre Tachoire
2026-05-04 12:53:55 +02:00
parent f8f14efe40
commit cf7dbc98de
3 changed files with 83 additions and 18 deletions

71
src/browser/js/RegExp.zig Normal file
View File

@@ -0,0 +1,71 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const RegExp = @This();
local: *const js.Local,
handle: *const v8.RegExp,
// Mirrors v8::RegExp::Flags. Combine with bitwise OR.
pub const Flag = struct {
pub const none: c_int = v8.kRegExpNone;
pub const global: c_int = v8.kRegExpGlobal;
pub const ignore_case: c_int = v8.kRegExpIgnoreCase;
pub const multiline: c_int = v8.kRegExpMultiline;
pub const sticky: c_int = v8.kRegExpSticky;
pub const unicode: c_int = v8.kRegExpUnicode;
pub const dot_all: c_int = v8.kRegExpDotAll;
pub const linear: c_int = v8.kRegExpLinear;
pub const has_inSelfdices: c_int = v8.kRegExpHasIndices;
pub const unicode_sets: c_int = v8.kRegExpUnicodeSets;
};
pub fn init(local: *const js.Local, pattern: []const u8, flags: c_int) !RegExp {
const pattern_handle = local.isolate.initStringHandle(pattern);
const handle = v8.v8__RegExp__New(local.handle, pattern_handle, flags) orelse return error.JsException;
return .{ .local = local, .handle = handle };
}
// Runs the pattern against `subject`. Returns the result Array (as a generic
// Object) on match, or null on no match. Returns error.JsException if V8
// throws — typically when the pattern is malformed for the current flags.
pub fn exec(self: RegExp, subject: []const u8) !?js.Object {
const local = self.local;
const subject_handle = local.isolate.initStringHandle(subject);
const handle = v8.v8__RegExp__Exec(self.handle, local.handle, subject_handle) orelse return error.JsException;
if (v8.v8__Value__IsNullOrUndefined(@ptrCast(handle))) return null;
return .{ .local = local, .handle = handle };
}
// Equivalent to `RegExp.prototype.test()` — true iff the pattern matches.
pub fn match(self: RegExp, subject: []const u8) !bool {
return (try self.exec(subject)) != null;
}
pub fn toValue(self: RegExp) js.Value {
return .{
.local = self.local,
.handle = @ptrCast(self.handle),
};
}

View File

@@ -42,6 +42,7 @@ pub const Object = @import("Object.zig");
pub const TryCatch = @import("TryCatch.zig");
pub const Function = @import("Function.zig");
pub const Promise = @import("Promise.zig");
pub const RegExp = @import("RegExp.zig");
pub const Module = @import("Module.zig");
pub const BigInt = @import("BigInt.zig");
pub const Number = @import("Number.zig");

View File

@@ -307,30 +307,23 @@ pub fn suffersPatternMismatch(self: *const Input, frame: *Frame) bool {
const pattern = self.asConstElement().getAttributeSafe(comptime .wrap("pattern")) orelse return false;
if (pattern.len == 0) return false;
// Evaluate `new RegExp("^(?:" + pattern + ")$", "v").test(value)` via V8.
// Per spec, an invalid pattern is ignored — the catch arm returns null and
// we treat that as "no mismatch".
// Per HTML spec, anchor the pattern with ^(?:...)$ and compile under the
// "v" (Unicode sets) flag. An invalid pattern is ignored — V8 throws and
// we treat that as "no mismatch". TryCatch absorbs the exception so it
// doesn't linger in the isolate.
var ls: js.Local.Scope = undefined;
frame.js.localScope(&ls);
defer ls.deinit();
const arena = frame.call_arena;
const pattern_json = std.json.Stringify.valueAlloc(arena, pattern, .{}) catch return false;
const value_json = std.json.Stringify.valueAlloc(arena, value, .{}) catch return false;
var try_catch: js.TryCatch = undefined;
try_catch.init(&ls.local);
defer try_catch.deinit();
const expr = std.fmt.allocPrint(arena,
\\(function() {{
\\ try {{
\\ return new RegExp("^(?:" + {s} + ")$", "v").test({s});
\\ }} catch (_) {{
\\ return null;
\\ }}
\\}})()
, .{ pattern_json, value_json }) catch return false;
const wrapped = std.fmt.allocPrint(frame.call_arena, "^(?:{s})$", .{pattern}) catch return false;
const re = js.RegExp.init(&ls.local, wrapped, js.RegExp.Flag.unicode_sets) catch return false;
const matched = re.match(value) catch return false;
const result = ls.local.exec(expr, "Input.suffersPatternMismatch") catch return false;
if (result.isNullOrUndefined()) return false;
return !result.toBool();
return !matched;
}
pub fn suffersTooLong(self: *const Input) bool {