dns core: wire-format parse/encode, iterators, response builder

This commit is contained in:
2026-08-01 01:06:11 +02:00
parent bf02f83adc
commit 7429b96cde
9 changed files with 2525 additions and 0 deletions
+138
View File
@@ -0,0 +1,138 @@
//! Question section entries (RFC 1035 §4.1.2). Pure: no allocation, no
//! `std.Io` beyond writing encoded bytes to a caller's writer.
const std = @import("std");
const types = @import("types.zig");
const name = @import("name.zig");
const Writer = std.Io.Writer;
pub const Question = struct {
name: name.Name,
qtype: types.Type,
qclass: types.Class,
};
pub const ParseError = name.ParseError;
pub const Parsed = struct {
question: Question,
/// Offset just past the question as it appears at `offset`; follows the
/// same convention as `name.Parsed.end` for a compressed name.
end: usize,
};
pub fn parse(packet: []const u8, offset: usize) ParseError!Parsed {
const parsed_name = try name.parse(packet, offset);
const fixed = parsed_name.end;
if (fixed + 4 > packet.len) return error.Truncated;
return .{
.question = .{
.name = parsed_name.name,
.qtype = @enumFromInt(std.mem.readInt(u16, packet[fixed..][0..2], .big)),
.qclass = @enumFromInt(std.mem.readInt(u16, packet[fixed + 2 ..][0..2], .big)),
},
.end = fixed + 4,
};
}
pub fn encode(q: Question, w: *Writer) Writer.Error!void {
try name.encode(q.name, w);
try w.writeInt(u16, @intFromEnum(q.qtype), .big);
try w.writeInt(u16, @intFromEnum(q.qclass), .big);
}
const testing = std.testing;
test "parse an uncompressed question" {
const packet = "\x07example\x03com\x00\x00\x01\x00\x01";
const r = try parse(packet, 0);
try testing.expectEqualSlices(
u8,
(try name.fromText("example.com")).wire(),
r.question.name.wire(),
);
try testing.expectEqual(types.Type.a, r.question.qtype);
try testing.expectEqual(types.Class.in, r.question.qclass);
try testing.expectEqual(@as(usize, 17), r.end);
}
test "parse a question whose name is compressed" {
// "com" at offset 0, then a question at offset 5 naming "example.com"
// through a pointer.
const packet = "\x03com\x00" ++ "\x07example\xc0\x00\x00\x1c\x00\x01";
const r = try parse(packet, 5);
try testing.expectEqualSlices(
u8,
(try name.fromText("example.com")).wire(),
r.question.name.wire(),
);
try testing.expectEqual(types.Type.aaaa, r.question.qtype);
try testing.expectEqual(types.Class.in, r.question.qclass);
try testing.expectEqual(@as(usize, 19), r.end);
}
test "parse keeps unknown type and class values" {
const packet = "\x00\x12\x34\x56\x78";
const r = try parse(packet, 0);
try testing.expect(r.question.name.isRoot());
try testing.expectEqual(@as(u16, 0x1234), @intFromEnum(r.question.qtype));
try testing.expectEqual(@as(u16, 0x5678), @intFromEnum(r.question.qclass));
try testing.expectEqual(@as(usize, 5), r.end);
}
test "parse rejects a truncated fixed field" {
const full = "\x07example\x03com\x00\x00\x01\x00\x01";
var i: usize = 13;
while (i < full.len) : (i += 1) {
try testing.expectError(error.Truncated, parse(full[0..i], 0));
}
}
test "parse propagates a name error" {
try testing.expectError(error.BadPointer, parse("\xc0\x00\x00\x01\x00\x01", 0));
try testing.expectError(error.Truncated, parse("\x07exa", 0));
try testing.expectError(error.LabelTooLong, parse("\x40abc\x00\x00\x01\x00\x01", 0));
}
test "encode round-trips" {
const original: Question = .{
.name = try name.fromText("www.example.com"),
.qtype = .aaaa,
.qclass = .in,
};
var buf: [512]u8 = undefined;
var w = Writer.fixed(&buf);
try encode(original, &w);
const bytes = w.buffered();
try testing.expectEqual(@as(usize, 21), bytes.len);
const r = try parse(bytes, 0);
try testing.expectEqualSlices(u8, original.name.wire(), r.question.name.wire());
try testing.expectEqual(original.qtype, r.question.qtype);
try testing.expectEqual(original.qclass, r.question.qclass);
try testing.expectEqual(bytes.len, r.end);
}
test "encode writes big-endian fixed fields" {
const q: Question = .{
.name = try name.fromText("."),
.qtype = @enumFromInt(0x1234),
.qclass = @enumFromInt(0x5678),
};
var buf: [16]u8 = undefined;
var w = Writer.fixed(&buf);
try encode(q, &w);
try testing.expectEqualSlices(u8, "\x00\x12\x34\x56\x78", w.buffered());
}
test "encode reports a short buffer" {
const q: Question = .{
.name = try name.fromText("example.com"),
.qtype = .a,
.qclass = .in,
};
var buf: [16]u8 = undefined;
var w = Writer.fixed(&buf);
try testing.expectError(error.WriteFailed, encode(q, &w));
}