milestone 18: collapse duplicated infrastructure into shared listener core, crud list helper, resource shells, transport race, name and line helpers, ui modules
CI / test (push) Successful in 1m22s
CI / test-aarch64 (push) Successful in 5m6s
CI / frontend (push) Successful in 45s
CI / cross (push) Successful in 7m53s
CI / docker (push) Failing after 1h10m57s

This commit is contained in:
2026-08-07 18:20:30 +02:00
parent c50c6d285a
commit 6f67940995
82 changed files with 3167 additions and 3114 deletions
+9 -18
View File
@@ -61,30 +61,17 @@ pub fn compile(
var wild: Entries = .{};
defer wild.deinit(gpa);
while (true) {
const raw = r.takeDelimiter('\n') catch |err| switch (err) {
error.ReadFailed => return error.ReadFailed,
// `takeDelimiter` leaves the stream unmodified on `StreamTooLong`
// (Reader.zig:885). Without this discard the loop re-reads the same
// bytes forever.
error.StreamTooLong => {
while (try parsers.nextBoundedLine(r, max_line_len)) |event| {
const raw = switch (event) {
.long_line => {
counts.long_lines += 1;
_ = r.discardDelimiterInclusive('\n') catch |discard_err| switch (discard_err) {
error.EndOfStream => break,
error.ReadFailed => return error.ReadFailed,
};
continue;
},
} orelse break;
.line => |line| line,
};
var line = raw;
if (line.len != 0 and line[line.len - 1] == '\r') line = line[0 .. line.len - 1];
// A reader whose buffer is larger than `max_line_len` reports the
// over-long line here instead of through `error.StreamTooLong`.
if (line.len > max_line_len) {
counts.long_lines += 1;
continue;
}
const parsed = parsers.parseLine(format, line);
switch (parsed.kind) {
@@ -120,6 +107,10 @@ pub fn compile(
/// Normalizes one whitespace-separated candidate and files it under `.list`,
/// `.wild`, or neither.
///
/// The normalization below is deliberately not `dns.name.normalizeText`: this
/// one adds the two-label minimum, rejects control bytes, and reports every
/// rejection through `counts.invalid` rather than an error.
fn addCandidate(
gpa: std.mem.Allocator,
field: []const u8,
+5 -13
View File
@@ -1530,20 +1530,12 @@ fn destroySnapshot(gpa: Allocator, snapshot: *matcher.Snapshot) void {
fn collectSample(r: *std.Io.Reader, w: *std.Io.Writer) error{ ReadFailed, WriteFailed }!void {
var considered: usize = 0;
while (considered < parsers.sample_lines) {
const raw = r.takeDelimiter('\n') catch |err| switch (err) {
// The stream is left unmodified here, so the line has to be stepped
// over or this loop never advances.
error.StreamTooLong => {
_ = r.discardDelimiterInclusive('\n') catch |discard_err| switch (discard_err) {
error.EndOfStream => return,
error.ReadFailed => return error.ReadFailed,
};
continue;
},
error.ReadFailed => return error.ReadFailed,
} orelse return;
const event = (try parsers.nextBoundedLine(r, compiler.max_line_len)) orelse return;
const raw = switch (event) {
.long_line => continue,
.line => |line| line,
};
if (raw.len > compiler.max_line_len) continue;
const line = std.mem.trim(u8, raw, &std.ascii.whitespace);
if (line.len == 0) continue;
if (parsers.isComment(line)) continue;
+96
View File
@@ -48,6 +48,50 @@ pub fn parseLine(format: Format, line: []const u8) Line {
};
}
pub const LineEvent = union(enum) {
/// One line without its delimiter, borrowed from the reader's buffer and
/// valid only until the next call. A trailing '\r' is left on: whether it
/// belongs to the line is the caller's decision.
line: []const u8,
/// A line longer than `max_len`. It has already been stepped over.
long_line,
};
/// One line, or `null` at end of stream. `max_len` bounds a line; anything
/// longer comes back as `.long_line` with the stream positioned on the line
/// after it, so a caller that keeps calling always advances.
///
/// The bound is a parameter because this file may not import `compiler.zig`:
/// the compiler imports this one, and this file is the root of a separate fuzz
/// module. Both callers pass `compiler.max_line_len`.
///
/// The two over-long paths exist because a `Reader` reports an over-long line
/// two different ways. A reader whose buffer is smaller than `max_len` reports
/// `error.StreamTooLong` and — this is the hazard — leaves the stream
/// unmodified (Reader.zig:895-919), so without the discard a caller re-reads
/// the same bytes forever. A reader whose buffer is larger hands the whole line
/// over and the length check catches it.
///
/// An over-long final line with no delimiter ends the stream inside the
/// discard. That still counts as a line, so it comes back as `.long_line`; the
/// discard drained the stream (Reader.zig:1042), so the next call returns
/// `null`.
pub fn nextBoundedLine(r: *std.Io.Reader, max_len: usize) error{ReadFailed}!?LineEvent {
const raw = r.takeDelimiter('\n') catch |err| switch (err) {
error.ReadFailed => return error.ReadFailed,
error.StreamTooLong => {
_ = r.discardDelimiterInclusive('\n') catch |discard_err| switch (discard_err) {
error.EndOfStream => return .long_line,
error.ReadFailed => return error.ReadFailed,
};
return .long_line;
},
} orelse return null;
if (raw.len > max_len) return .long_line;
return .{ .line = raw };
}
pub const sample_lines = 64;
/// Picks a format from the first `sample_lines` lines that are not blank and
@@ -309,6 +353,58 @@ test "parseLine dispatches to the abp parser" {
try testing.expect(line.covers_apex);
}
fn expectLine(expected: []const u8, event: ?LineEvent) !void {
const got = event orelse return error.TestExpectedLine;
switch (got) {
.line => |line| try testing.expectEqualStrings(expected, line),
.long_line => return error.TestExpectedLine,
}
}
test "nextBoundedLine walks lines and ends at the stream" {
var r: std.Io.Reader = .fixed("a\nbb\n\nccc");
try expectLine("a", try nextBoundedLine(&r, 16));
try expectLine("bb", try nextBoundedLine(&r, 16));
try expectLine("", try nextBoundedLine(&r, 16));
// A final line with no delimiter is still a line.
try expectLine("ccc", try nextBoundedLine(&r, 16));
try testing.expectEqual(@as(?LineEvent, null), try nextBoundedLine(&r, 16));
}
test "nextBoundedLine reports an over-long line when the reader buffer is large" {
var r: std.Io.Reader = .fixed("a\nxxxxxxxx\nb\n");
try expectLine("a", try nextBoundedLine(&r, 4));
try testing.expectEqual(LineEvent.long_line, (try nextBoundedLine(&r, 4)).?);
try expectLine("b", try nextBoundedLine(&r, 4));
try testing.expectEqual(@as(?LineEvent, null), try nextBoundedLine(&r, 4));
}
test "nextBoundedLine steps over a line that does not fit the reader buffer" {
// A buffer smaller than the long line makes `takeDelimiter` report
// `error.StreamTooLong` and leave the stream where it was, which is the
// path that loops forever without the discard.
var backing: std.Io.Reader = .fixed("a\n" ++ "x" ** 64 ++ "\nb\n");
var buf: [16]u8 = undefined;
var limited = backing.limited(.unlimited, &buf);
const r = &limited.interface;
try expectLine("a", try nextBoundedLine(r, 16));
try testing.expectEqual(LineEvent.long_line, (try nextBoundedLine(r, 16)).?);
try expectLine("b", try nextBoundedLine(r, 16));
try testing.expectEqual(@as(?LineEvent, null), try nextBoundedLine(r, 16));
}
test "nextBoundedLine reports an over-long final line that ends inside the discard" {
var backing: std.Io.Reader = .fixed("a\n" ++ "x" ** 64);
var buf: [16]u8 = undefined;
var limited = backing.limited(.unlimited, &buf);
const r = &limited.interface;
try expectLine("a", try nextBoundedLine(r, 16));
try testing.expectEqual(LineEvent.long_line, (try nextBoundedLine(r, 16)).?);
try testing.expectEqual(@as(?LineEvent, null), try nextBoundedLine(r, 16));
}
test "looksLikeIpLiteral separates addresses from names" {
try testing.expect(looksLikeIpLiteral("0.0.0.0"));
try testing.expect(looksLikeIpLiteral("127.0.0.1"));
+4
View File
@@ -192,6 +192,10 @@ const NameError = error{BadName};
/// Lowercases over ASCII and strips one trailing dot. A byte ≥ 0x80 is
/// rejected: query names reach the matcher ASCII-lowercased, so a pattern
/// carrying a high byte could never match anything.
///
/// Deliberately not `dns.name.normalizeText`: a pattern may hold `*`, which
/// `name.fromText` would reject, so this variant skips that check and rejects
/// control bytes and space instead.
fn normalize(text: []const u8, buf: *[types.max_name_len]u8) NameError![]const u8 {
var rest = text;
if (rest.len > 0 and rest[rest.len - 1] == '.') rest = rest[0 .. rest.len - 1];