dns core: wire-format parse/encode, iterators, response builder
This commit is contained in:
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user