diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 11:48:17 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | 78c60342022bc44307ad789aaef163ab96ebd08b (patch) | |
| tree | 92c35aa8a5a39df2384d2dd1e297e4716798d23a /src/ninep/addr.zig | |
| parent | 1c3842f136b9f98fae784559ec12d66d629c5909 (diff) | |
| download | pardes-78c60342022bc44307ad789aaef163ab96ebd08b.tar.gz pardes-78c60342022bc44307ad789aaef163ab96ebd08b.zip | |
A regular expression search comes back: costly patterns refused, long lines windowed, 300 ms deadline
mvzr backtracks with no bound on its work (a*a*a*a*x over a hundred a's takes a
second, each repeat multiplying by the haystack length), and a search runs holding
the editor's turn, so one pasted pattern froze the editor. pardes does not write or
vendor a regex engine, so regexp.zig bounds what it hands mvzr: more than four
repeats is refused, a line longer than a window sized from measured worst cases is
searched in half-overlapping windows, and a deadline stops the search. addr names
each failure; normal s/S keeps what it found. The prescan also stops reading an
escaped backslash before n as a newline, and ends a class where mvzr does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ninep/addr.zig')
| -rw-r--r-- | src/ninep/addr.zig | 22 |
1 files changed, 17 insertions, 5 deletions
diff --git a/src/ninep/addr.zig b/src/ninep/addr.zig index 347a2d11..69e6dc77 100644 --- a/src/ninep/addr.zig +++ b/src/ninep/addr.zig @@ -14,6 +14,8 @@ fn clip(n: usize) u32 { pub const e_no_match = "no match for regexp"; pub const e_range = "address out of range"; pub const e_regexp = "bad regular expression"; +pub const e_costly = "regular expression has more than four repeats"; +pub const e_slow = "regular expression search took too long"; pub const e_syntax = "bad address syntax"; pub const Addr = struct { @@ -196,8 +198,8 @@ pub const Addr = struct { a.err = "no previous regular expression"; return null; } - const rx = regexp_.Regex.compile(pat) orelse { - a.err = e_regexp; + const rx = regexp_.Regex.compile(pat) catch |err| { + a.err = if (err == error.TooCostly) e_costly else e_regexp; return null; }; const found = if (back) found: { @@ -205,7 +207,10 @@ pub const Addr = struct { var before: ?Range = null; var at: usize = 0; while (at <= a.text.len) { - const m = rx.find(a.text, at, a.text.len, a.text.len) orelse break; + const m = (rx.find(a.text, at, a.text.len, a.text.len) catch { + a.err = e_slow; + return null; + }) orelse break; const found_range: Range = .{ .q0 = clip(m.start), .q1 = clip(m.end) }; if (found_range.q1 <= r.q0) before = found_range; last = found_range; @@ -215,7 +220,14 @@ pub const Addr = struct { } else found: { const hi = if (a.lim) |l| @min(@as(usize, l.q1), a.text.len) else a.text.len; const from = @min(@as(usize, r.q1), hi); - const m = rx.find(a.text, from, hi, hi) orelse (if (a.lim != null or from == 0) null else rx.find(a.text, 0, from - 1, hi)) orelse break :found null; + const ahead = rx.find(a.text, from, hi, hi) catch { + a.err = e_slow; + return null; + }; + const m = ahead orelse (if (a.lim != null or from == 0) null else rx.find(a.text, 0, from - 1, hi) catch { + a.err = e_slow; + return null; + }) orelse break :found null; break :found Range{ .q0 = clip(m.start), .q1 = clip(m.end) }; }; return found orelse { @@ -366,7 +378,7 @@ test "the address language, form by form" { for ([_][2][]const u8{ .{ "zzz", e_syntax }, .{ "/nomatch/", e_no_match }, .{ "99", e_range }, .{ "#999", e_range }, .{ "/a[/", e_regexp }, .{ "/(a/", e_regexp }, - .{ "/*a/", e_regexp }, + .{ "/*a/", e_regexp }, .{ "/a*b*c*d*e*/", e_costly }, }) |c| { _ = th.wr(p, addr, "#0"); try testing.expectEqualStrings(c[1], th.wr(p, addr, c[0]).reply.ename); |
