//! Resource records (RFC 1035 §4.1.3). 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;

/// Where the RDATA sits inside the packet that produced the record. The bytes
/// stay in the packet: RDATA of NS, CNAME, PTR, MX and SOA can hold names with
/// compression pointers that target any earlier offset in the message, so the
/// span alone does not decode. Every typed accessor therefore takes the whole
/// packet as well as the record.
pub const RdataSpan = struct {
    offset: usize,
    len: usize,

    /// The raw bytes. `packet` must be the packet the record came from.
    pub fn slice(self: RdataSpan, packet: []const u8) []const u8 {
        return packet[self.offset..][0..self.len];
    }
};

pub const Record = struct {
    name: name.Name,
    rtype: types.Type,
    /// Left as a plain `u16`: OPT reuses this field as the requestor's UDP
    /// payload size (RFC 6891 §6.1.2), so it is not always a class.
    class: u16,
    /// Left as a plain `u32` for the same reason: OPT reuses it as flags.
    ttl: u32,
    rdata: RdataSpan,
};

pub const ParseError = name.ParseError;

pub const Parsed = struct {
    record: Record,
    /// Offset just past the record. The name follows the `name.Parsed.end`
    /// convention, so a compressed owner name costs two bytes here.
    end: usize,
};

/// Fixed fields between the owner name and the RDATA: TYPE, CLASS, TTL,
/// RDLENGTH.
const fixed_len = 2 + 2 + 4 + 2;

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 + fixed_len > packet.len) return error.Truncated;

    const rdlength: usize = std.mem.readInt(u16, packet[fixed + 8 ..][0..2], .big);
    const rdata_offset = fixed + fixed_len;
    if (rdata_offset + rdlength > packet.len) return error.Truncated;

    return .{
        .record = .{
            .name = parsed_name.name,
            .rtype = @enumFromInt(std.mem.readInt(u16, packet[fixed..][0..2], .big)),
            .class = std.mem.readInt(u16, packet[fixed + 2 ..][0..2], .big),
            .ttl = std.mem.readInt(u32, packet[fixed + 4 ..][0..4], .big),
            .rdata = .{ .offset = rdata_offset, .len = rdlength },
        },
        .end = rdata_offset + rdlength,
    };
}

/// Writes the record with its owner name uncompressed. `rdata_bytes` is copied
/// verbatim, so it must not contain compression pointers: this path serves
/// synthesized records, whose RDATA nxdns builds itself.
pub fn encode(rec: Record, rdata_bytes: []const u8, w: *Writer) (Writer.Error || error{RdataTooLong})!void {
    if (rdata_bytes.len > std.math.maxInt(u16)) return error.RdataTooLong;
    try name.encode(rec.name, w);
    try w.writeInt(u16, @intFromEnum(rec.rtype), .big);
    try w.writeInt(u16, rec.class, .big);
    try w.writeInt(u32, rec.ttl, .big);
    try w.writeInt(u16, @intCast(rdata_bytes.len), .big);
    try w.writeAll(rdata_bytes);
}

pub const RdataError = name.ParseError || error{
    WrongType,
    BadRdata,
};

pub fn rdataA(packet: []const u8, rec: Record) RdataError![4]u8 {
    if (rec.rtype != .a) return error.WrongType;
    if (rec.rdata.len != 4) return error.BadRdata;
    return rec.rdata.slice(packet)[0..4].*;
}

pub fn rdataAaaa(packet: []const u8, rec: Record) RdataError![16]u8 {
    if (rec.rtype != .aaaa) return error.WrongType;
    if (rec.rdata.len != 16) return error.BadRdata;
    return rec.rdata.slice(packet)[0..16].*;
}

/// Decodes the single name in CNAME, NS or PTR RDATA, following compression
/// pointers into the rest of the packet.
pub fn rdataCname(packet: []const u8, rec: Record) RdataError!name.Name {
    switch (rec.rtype) {
        .cname, .ns, .ptr => {},
        else => return error.WrongType,
    }
    const parsed = try name.parse(packet, rec.rdata.offset);
    if (parsed.end != rec.rdata.offset + rec.rdata.len) return error.BadRdata;
    return parsed.name;
}

/// The MINIMUM field of SOA RDATA (RFC 1035 §3.3.13), which RFC 2308 §4 makes
/// the ceiling for negative caching. MNAME and RNAME precede the five fixed
/// 32-bit fields and may both be compressed, so they have to be walked.
pub fn rdataSoaMinimumTtl(packet: []const u8, rec: Record) RdataError!u32 {
    if (rec.rtype != .soa) return error.WrongType;
    const rdata_end = rec.rdata.offset + rec.rdata.len;

    const mname = try name.parse(packet, rec.rdata.offset);
    if (mname.end > rdata_end) return error.BadRdata;
    const rname = try name.parse(packet, mname.end);
    if (rname.end > rdata_end) return error.BadRdata;

    // SERIAL, REFRESH, RETRY, EXPIRE, MINIMUM.
    if (rname.end + 20 != rdata_end) return error.BadRdata;
    return std.mem.readInt(u32, packet[rname.end + 16 ..][0..4], .big);
}

const testing = std.testing;

/// An answer for "example.com A 1.2.3.4" preceded by a 12-byte header stand-in
/// and the question name it compresses against.
const a_packet =
    "\x00" ** 12 ++ // header stand-in
    "\x07example\x03com\x00\x00\x01\x00\x01" ++ // question, offset 12
    "\xc0\x0c\x00\x01\x00\x01\x00\x00\x0e\x10\x00\x04\x01\x02\x03\x04";

test "parse an A record with a compressed owner name" {
    const r = try parse(a_packet, 29);
    try testing.expectEqualSlices(
        u8,
        (try name.fromText("example.com")).wire(),
        r.record.name.wire(),
    );
    try testing.expectEqual(types.Type.a, r.record.rtype);
    try testing.expectEqual(@as(u16, 1), r.record.class);
    try testing.expectEqual(@as(u32, 3600), r.record.ttl);
    try testing.expectEqual(@as(usize, 4), r.record.rdata.len);
    try testing.expectEqual(@as(usize, a_packet.len), r.end);
    try testing.expectEqualSlices(u8, "\x01\x02\x03\x04", r.record.rdata.slice(a_packet));
    try testing.expectEqual([4]u8{ 1, 2, 3, 4 }, try rdataA(a_packet, r.record));
}

test "parse an AAAA record" {
    const packet = "\x00\x00\x1c\x00\x01\x00\x00\x00\x3c\x00\x10" ++
        "\x20\x01\x0d\xb8" ++ "\x00" ** 11 ++ "\x01";
    const r = try parse(packet, 0);
    try testing.expectEqual(types.Type.aaaa, r.record.rtype);
    try testing.expectEqual(@as(u32, 60), r.record.ttl);
    const addr = try rdataAaaa(packet, r.record);
    try testing.expectEqual(@as(u8, 0x20), addr[0]);
    try testing.expectEqual(@as(u8, 0x01), addr[15]);
}

test "parse a CNAME whose target is compressed" {
    // "example.com" at offset 0; the CNAME RDATA is "www" plus a pointer to it.
    const packet = "\x07example\x03com\x00" ++
        "\x03www\xc0\x00\x00\x05\x00\x01\x00\x00\x01\x2c\x00\x02\xc0\x00";
    const r = try parse(packet, 13);
    try testing.expectEqual(types.Type.cname, r.record.rtype);
    try testing.expectEqual(@as(usize, 2), r.record.rdata.len);
    const target = try rdataCname(packet, r.record);
    try testing.expectEqualSlices(u8, (try name.fromText("example.com")).wire(), target.wire());
    try testing.expectEqual(@as(usize, packet.len), r.end);
}

test "rdataCname accepts NS and PTR, rejects other types" {
    const packet = "\x03com\x00" ++ "\x00\x00\x02\x00\x01\x00\x00\x00\x0a\x00\x02\xc0\x00";
    const r = try parse(packet, 5);
    try testing.expectEqual(types.Type.ns, r.record.rtype);
    try testing.expectEqualSlices(
        u8,
        (try name.fromText("com")).wire(),
        (try rdataCname(packet, r.record)).wire(),
    );

    var wrong = r.record;
    wrong.rtype = .mx;
    try testing.expectError(error.WrongType, rdataCname(packet, wrong));
}

test "rdataCname rejects a name that does not fill the rdata" {
    // RDLENGTH claims 4 bytes but the name uses 2.
    const packet = "\x03com\x00" ++ "\x00\x00\x05\x00\x01\x00\x00\x00\x0a\x00\x04\xc0\x00\x00\x00";
    const r = try parse(packet, 5);
    try testing.expectError(error.BadRdata, rdataCname(packet, r.record));
}

test "rdataA and rdataAaaa reject the wrong type and the wrong length" {
    const short = "\x00\x00\x01\x00\x01\x00\x00\x00\x0a\x00\x03\x01\x02\x03";
    const r = try parse(short, 0);
    try testing.expectError(error.BadRdata, rdataA(short, r.record));
    try testing.expectError(error.WrongType, rdataAaaa(short, r.record));
}

test "SOA minimum ttl extraction with compressed MNAME and RNAME" {
    // Offset 0 holds "example.com"; the owner name and both RDATA names are
    // pointers to it.
    const rdata = "\x03ns1\xc0\x00" ++ // MNAME: ns1.example.com, 6 bytes
        "\x0ahostmaster\xc0\x00" ++ // RNAME: hostmaster.example.com, 13 bytes
        "\x00\x00\x00\x01" ++ // serial
        "\x00\x00\x1c\x20" ++ // refresh
        "\x00\x00\x0e\x10" ++ // retry
        "\x00\x36\xee\x80" ++ // expire
        "\x00\x00\x02\x58"; // minimum = 600
    const soa = "\x07example\x03com\x00" ++
        "\xc0\x00\x00\x06\x00\x01\x00\x00\x0e\x10" ++
        "\x00\x27" ++ // rdlength = 6 + 13 + 20 = 39
        rdata;
    try testing.expectEqual(@as(usize, 39), rdata.len);

    const r = try parse(soa, 13);
    try testing.expectEqual(types.Type.soa, r.record.rtype);
    try testing.expectEqual(@as(u32, 600), try rdataSoaMinimumTtl(soa, r.record));
    try testing.expectEqual(@as(usize, soa.len), r.end);
}

test "SOA minimum ttl rejects a short or long rdata" {
    const rdata = "\x00\x00" ++ // root MNAME and RNAME
        "\x00\x00\x00\x01\x00\x00\x1c\x20\x00\x00\x0e\x10\x00\x36\xee\x80\x00\x00\x02\x58";
    const head = "\x00\x00\x06\x00\x01\x00\x00\x0e\x10";

    const good = head ++ "\x00\x16" ++ rdata;
    const r = try parse(good, 0);
    try testing.expectEqual(@as(u32, 600), try rdataSoaMinimumTtl(good, r.record));

    // One fixed field short.
    const short = head ++ "\x00\x12" ++ rdata[0..18];
    const rs = try parse(short, 0);
    try testing.expectError(error.BadRdata, rdataSoaMinimumTtl(short, rs.record));

    // One byte of slack past MINIMUM.
    const long = head ++ "\x00\x17" ++ rdata ++ "\x00";
    const rl = try parse(long, 0);
    try testing.expectError(error.BadRdata, rdataSoaMinimumTtl(long, rl.record));

    var wrong = r.record;
    wrong.rtype = .a;
    try testing.expectError(error.WrongType, rdataSoaMinimumTtl(good, wrong));
}

test "parse rejects an rdlength that overruns the packet" {
    const packet = "\x00\x00\x01\x00\x01\x00\x00\x0e\x10\x00\x08\x01\x02\x03\x04";
    try testing.expectError(error.Truncated, parse(packet, 0));
}

test "parse rejects truncated fixed fields" {
    const full = "\x00\x00\x01\x00\x01\x00\x00\x0e\x10\x00\x04\x01\x02\x03\x04";
    var i: usize = 1;
    while (i < full.len) : (i += 1) {
        try testing.expectError(error.Truncated, parse(full[0..i], 0));
    }
    _ = try parse(full, 0);
}

test "parse propagates a name error" {
    try testing.expectError(error.BadPointer, parse("\xc0\x00" ++ "\x00" ** 11, 0));
    try testing.expectError(error.LabelTooLong, parse("\x40ab" ++ "\x00" ** 12, 0));
}

test "parse accepts an empty rdata" {
    const packet = "\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x00";
    const r = try parse(packet, 0);
    try testing.expectEqual(types.Type.opt, r.record.rtype);
    try testing.expectEqual(@as(usize, 0), r.record.rdata.len);
    try testing.expectEqual(@as(usize, packet.len), r.end);
    try testing.expectEqualSlices(u8, "", r.record.rdata.slice(packet));
}

test "encode round-trips through parse" {
    const original: Record = .{
        .name = try name.fromText("www.example.com"),
        .rtype = .a,
        .class = 1,
        .ttl = 0x0000_0e10,
        .rdata = .{ .offset = 0, .len = 0 },
    };

    var buf: [512]u8 = undefined;
    var w = Writer.fixed(&buf);
    try encode(original, "\x0a\x00\x00\x01", &w);
    const bytes = w.buffered();

    const r = try parse(bytes, 0);
    try testing.expectEqualSlices(u8, original.name.wire(), r.record.name.wire());
    try testing.expectEqual(original.rtype, r.record.rtype);
    try testing.expectEqual(original.class, r.record.class);
    try testing.expectEqual(original.ttl, r.record.ttl);
    try testing.expectEqual(@as(usize, bytes.len), r.end);
    try testing.expectEqual([4]u8{ 10, 0, 0, 1 }, try rdataA(bytes, r.record));
}

test "encode reports a short buffer" {
    const rec: Record = .{
        .name = try name.fromText("example.com"),
        .rtype = .a,
        .class = 1,
        .ttl = 60,
        .rdata = .{ .offset = 0, .len = 0 },
    };
    var buf: [16]u8 = undefined;
    var w = Writer.fixed(&buf);
    try testing.expectError(error.WriteFailed, encode(rec, "\x01\x02\x03\x04", &w));
}
