Skip to content

Commit e8e05db

Browse files
Replace hand-rolled regex implementation with zig-regex library
- Add zig-regex dependency to build.zig.zon and build.zig - Replace custom matchesRegex() and matchesRegexAt() functions with library call - Simplify implementation from 94 lines to 10 lines - Improve security by removing potential ReDoS vulnerabilities - Use proper memory management with ArenaAllocator 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Brad <bradcypert@users.noreply.github.com>
1 parent dff249a commit e8e05db

File tree

3 files changed

+12
-91
lines changed

3 files changed

+12
-91
lines changed

build.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub fn build(b: *std.Build) void {
44
const exe_name = b.option([]const u8, "exe_name", "Name of the executable") orelse "httpspec";
55
const dependencies = [_][]const u8{
66
"clap",
7+
"regex",
78
};
89

910
const target = b.standardTargetOptions(.{});

build.zig.zon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@
4040
.url = "git+https://github.com/Hejsil/zig-clap#cc5c6a5d71a317ed4b0ad776842d1d0655f72d0a",
4141
.hash = "clap-0.10.0-oBajB7jkAQAZ4cKLlzkeV9mDu2yGZvtN2QuOyfAfjBij",
4242
},
43+
.regex = .{
44+
.url = "git+https://github.com/tiehuis/zig-regex#8e38e11d45d3c45e06ed3e994e1eb2e62ed60637",
45+
.hash = "1220c65e96eb14c7de3e3a82bfc45a66e7ca72b80e0ae82d1b6b6e58b7d8c9e7b8",
46+
},
4347
},
4448
.paths = .{
4549
"build.zig",

src/httpfile/assertion_checker.zig

Lines changed: 7 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ test "HttpParser supports contains and not_contains for headers" {
4343
}
4444
const std = @import("std");
4545
const http = std.http;
46+
const regex = @import("regex");
4647
const HttpParser = @import("./parser.zig");
4748
const Client = @import("./http_client.zig");
4849

@@ -55,99 +56,14 @@ fn extractHeaderName(key: []const u8) ![]const u8 {
5556
}
5657

5758
fn matchesRegex(text: []const u8, pattern: []const u8) bool {
58-
if (pattern.len == 0) return text.len == 0;
59+
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
60+
defer arena.deinit();
61+
const allocator = arena.allocator();
5962

60-
// Handle anchors
61-
const starts_with_anchor = pattern[0] == '^';
62-
const ends_with_anchor = pattern.len > 0 and pattern[pattern.len - 1] == '$';
63+
const compiled_regex = regex.compile(allocator, pattern) catch return false;
64+
defer compiled_regex.deinit();
6365

64-
var actual_pattern = pattern;
65-
if (starts_with_anchor) actual_pattern = pattern[1..];
66-
if (ends_with_anchor and actual_pattern.len > 0) actual_pattern = actual_pattern[0..actual_pattern.len - 1];
67-
68-
if (starts_with_anchor and ends_with_anchor) {
69-
return matchesRegexAt(text, actual_pattern, 0) == text.len;
70-
} else if (starts_with_anchor) {
71-
return matchesRegexAt(text, actual_pattern, 0) != null;
72-
} else if (ends_with_anchor) {
73-
var i: usize = 0;
74-
while (i <= text.len) : (i += 1) {
75-
if (matchesRegexAt(text[i..], actual_pattern, 0)) |end_pos| {
76-
if (i + end_pos == text.len) return true;
77-
}
78-
}
79-
return false;
80-
} else {
81-
var i: usize = 0;
82-
while (i <= text.len) : (i += 1) {
83-
if (matchesRegexAt(text[i..], actual_pattern, 0) != null) return true;
84-
}
85-
return false;
86-
}
87-
}
88-
89-
fn matchesRegexAt(text: []const u8, pattern: []const u8, text_pos: usize) ?usize {
90-
var p_pos: usize = 0;
91-
var t_pos = text_pos;
92-
93-
while (p_pos < pattern.len and t_pos < text.len) {
94-
if (p_pos + 1 < pattern.len and pattern[p_pos + 1] == '*') {
95-
// Handle .* or character*
96-
const match_char = pattern[p_pos];
97-
p_pos += 2; // Skip char and *
98-
99-
// Try matching zero occurrences first
100-
if (matchesRegexAt(text, pattern[p_pos..], t_pos)) |end_pos| {
101-
return t_pos + end_pos;
102-
}
103-
104-
// Try matching one or more occurrences
105-
while (t_pos < text.len) {
106-
if (match_char == '.' or text[t_pos] == match_char) {
107-
t_pos += 1;
108-
if (matchesRegexAt(text, pattern[p_pos..], t_pos)) |end_pos| {
109-
return t_pos + end_pos;
110-
}
111-
} else {
112-
break;
113-
}
114-
}
115-
return null;
116-
} else if (pattern[p_pos] == '.') {
117-
// Match any single character
118-
t_pos += 1;
119-
p_pos += 1;
120-
} else if (pattern[p_pos] == '[') {
121-
// Character class
122-
const close_bracket = std.mem.indexOfScalarPos(u8, pattern, p_pos + 1, ']') orelse return null;
123-
const char_class = pattern[p_pos + 1..close_bracket];
124-
var matched = false;
125-
for (char_class) |c| {
126-
if (text[t_pos] == c) {
127-
matched = true;
128-
break;
129-
}
130-
}
131-
if (!matched) return null;
132-
t_pos += 1;
133-
p_pos = close_bracket + 1;
134-
} else {
135-
// Literal character match
136-
if (text[t_pos] != pattern[p_pos]) return null;
137-
t_pos += 1;
138-
p_pos += 1;
139-
}
140-
}
141-
142-
// Handle remaining .* patterns at end
143-
while (p_pos + 1 < pattern.len and pattern[p_pos + 1] == '*') {
144-
p_pos += 2;
145-
}
146-
147-
if (p_pos == pattern.len) {
148-
return t_pos - text_pos;
149-
}
150-
return null;
66+
return compiled_regex.match(text);
15167
}
15268

15369
pub fn check(request: *HttpParser.HttpRequest, response: Client.HttpResponse) !void {

0 commit comments

Comments
 (0)