From cf7dbc98ded4e0d627366d319643f61ae29f5e56 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 4 May 2026 12:53:55 +0200 Subject: [PATCH] use v8 RegExp for Input.suffersPatternMismatch --- src/browser/js/RegExp.zig | 71 +++++++++++++++++++++++ src/browser/js/js.zig | 1 + src/browser/webapi/element/html/Input.zig | 29 ++++----- 3 files changed, 83 insertions(+), 18 deletions(-) create mode 100644 src/browser/js/RegExp.zig diff --git a/src/browser/js/RegExp.zig b/src/browser/js/RegExp.zig new file mode 100644 index 00000000..b341a944 --- /dev/null +++ b/src/browser/js/RegExp.zig @@ -0,0 +1,71 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// 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 . + +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), + }; +} diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig index 28a1fb51..adc36255 100644 --- a/src/browser/js/js.zig +++ b/src/browser/js/js.zig @@ -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"); diff --git a/src/browser/webapi/element/html/Input.zig b/src/browser/webapi/element/html/Input.zig index 46795637..ebcd4073 100644 --- a/src/browser/webapi/element/html/Input.zig +++ b/src/browser/webapi/element/html/Input.zig @@ -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 {