//! Fuzz targets for the DNS wire-format core (`src/dns/`).
//!
//! Every target holds the same contract: rejecting attacker bytes with an
//! error is correct, panicking, overflowing or reading out of bounds is not.
//! Where a parse succeeds the target then checks the invariant the rest of the
//! server is entitled to rely on:
//!
//!   - a `Packet` that `parse` accepted iterates cleanly in every section, and
//!     its OPT record, option list and typed RDATA accessors cannot panic;
//!   - a `Name` that `parse` accepted ends inside the packet and survives a
//!     round trip through presentation form;
//!   - a buffer that `decrementTtls` aged still parses, and no record it aged
//!     holds a TTL below the minimum it reported;
//!   - a query that `stripEcs` rewrote still parses, still carries a valid OPT
//!     record, no longer carries an ECS option, and kept all four of its
//!     section counts.
//!
//! The targets stay inside the documented safe entry points. `setId` is called
//! only on a buffer long enough to hold a header, because it asserts that
//! length rather than returning an error. `ResponseBuilder` is absent: its
//! assertions guard programmer error, not attacker input, so tripping them
//! would report a fuzz finding that no packet can cause.
//!
//! Runner semantics: under a plain `zig build test` each target runs once per
//! corpus entry plus once on empty input, which makes the corpus a regression
//! suite. `zig build test --fuzz=<n>` gives each target `n` generated inputs;
//! the limit counts iterations, not seconds.

const std = @import("std");
const dns = @import("dns");
const corpus = @import("corpus.zig");

const packet = dns.packet;
const name = dns.name;
const record = dns.record;
const edns = dns.edns;
const types = dns.types;
const Smith = std.testing.Smith;

/// Inputs are capped well under the 65535-byte protocol maximum. The parsers
/// are length-driven — every bound comes from a count, an RDLENGTH or the
/// slice end — so a longer buffer adds iterations, not code paths, and a
/// smaller one buys more executions per second.
const max_input = 4096;

const fuzz_options: std.testing.FuzzInputOptions = .{ .corpus = &corpus.inputs };

test "fuzz packet.parse" {
    try std.testing.fuzz({}, parseTarget, fuzz_options);
}

test "fuzz name.parse" {
    try std.testing.fuzz({}, nameTarget, fuzz_options);
}

test "fuzz packet.decrementTtls" {
    try std.testing.fuzz({}, ttlTarget, fuzz_options);
}

test "fuzz edns.stripEcs" {
    try std.testing.fuzz({}, stripEcsTarget, fuzz_options);
}

fn parseTarget(_: void, smith: *Smith) anyerror!void {
    var buf: [max_input]u8 = undefined;
    const bytes = buf[0..smith.slice(&buf)];

    const p = packet.parse(bytes) catch return;

    var questions = packet.questions(p);
    while (try questions.next()) |_| {}

    for ([_]packet.RecordIterator{
        packet.answers(p),
        packet.authorities(p),
        packet.additionals(p),
    }) |section| {
        var records = section;
        while (try records.next()) |rec| sweepRdata(bytes, rec);
    }

    _ = packet.firstQuestion(p);

    const opt_record = packet.findOptRecord(p) orelse return;
    // A non-OPT record cannot reach here, so `NotOpt` is impossible; a
    // malformed option list is not, and is a legitimate rejection.
    const opt = edns.parseOpt(bytes, opt_record) catch return;

    // `parseOpt` validates the whole option list, so an `OptRecord` it
    // returned must iterate to the end without error.
    var options = edns.options(bytes, opt);
    while (try options.next()) |option| std.mem.doNotOptimizeAway(option.data);
    ignore(edns.findOption(bytes, opt, edns.ecs_option_code));
}

fn nameTarget(_: void, smith: *Smith) anyerror!void {
    var buf: [max_input]u8 = undefined;
    const bytes = buf[0..smith.slice(&buf)];
    const offset = smith.valueRangeAtMost(u32, 0, @intCast(bytes.len));

    const parsed = name.parse(bytes, offset) catch return;

    try std.testing.expect(parsed.end <= bytes.len);
    try std.testing.expect(parsed.name.len >= 1);
    try std.testing.expect(parsed.name.len <= types.max_name_len);
    try std.testing.expectEqual(@as(u8, 0), parsed.name.bytes[parsed.name.len - 1]);

    try expectTextRoundTrip(parsed.name);
}

fn ttlTarget(_: void, smith: *Smith) anyerror!void {
    var buf: [max_input]u8 = undefined;
    const bytes = buf[0..smith.slice(&buf)];
    const elapsed = smith.value(u32);

    if (bytes.len >= types.header_len) {
        packet.setId(bytes, @truncate(elapsed));
        try std.testing.expectEqual(
            @as(u16, @truncate(elapsed)),
            std.mem.readInt(u16, bytes[0..2], .big),
        );
    }

    const minimum = packet.decrementTtls(bytes, elapsed) catch return;

    // Aging rewrites TTL words in place and nothing else, so a buffer that
    // aged cleanly is still structurally valid.
    const p = try packet.parse(bytes);
    const reported = minimum orelse return;

    for ([_]packet.RecordIterator{
        packet.answers(p),
        packet.authorities(p),
        packet.additionals(p),
    }) |section| {
        var records = section;
        while (try records.next()) |rec| {
            if (rec.rtype == .opt) continue;
            try std.testing.expect(rec.ttl >= reported);
        }
    }
}

/// `stripEcs` is the only attacker-facing entry point that rewrites a packet, so
/// it is the only one where a finding can be a wrong output rather than a crash.
///
/// The input is derived exactly as `parseTarget` derives it, because `stripEcs`
/// asserts its preconditions rather than returning an error: `query` must be the
/// same bytes `pkt` was parsed from, and `out` must not overlap them. `out` is a
/// separate stack buffer for that reason, and tripping either assertion from a
/// hand-built argument would report a fault no packet can cause.
fn stripEcsTarget(_: void, smith: *Smith) anyerror!void {
    var buf: [max_input]u8 = undefined;
    const bytes = buf[0..smith.slice(&buf)];

    const p = packet.parse(bytes) catch return;
    const opt_record = packet.findOptRecord(p) orelse return;
    const opt = edns.parseOpt(bytes, opt_record) catch return;

    // Removing an option only ever shortens the query, so a buffer the size of
    // the input always holds the rewrite.
    var out: [max_input]u8 = undefined;
    const result = edns.stripEcs(bytes, p, opt, &out) catch return;
    const rewritten = switch (result) {
        .unchanged => return,
        .rewritten => |message| message,
    };

    try std.testing.expect(rewritten.len <= bytes.len);

    const stripped = try packet.parse(rewritten);
    try std.testing.expectEqual(p.header.id, stripped.header.id);
    try std.testing.expectEqual(p.header.qdcount, stripped.header.qdcount);
    try std.testing.expectEqual(p.header.ancount, stripped.header.ancount);
    try std.testing.expectEqual(p.header.nscount, stripped.header.nscount);
    try std.testing.expectEqual(p.header.arcount, stripped.header.arcount);

    const stripped_record = packet.findOptRecord(stripped) orelse
        return error.TestOptRecordLost;
    const stripped_opt = try edns.parseOpt(rewritten, stripped_record);
    try std.testing.expectEqual(opt.udp_payload_size, stripped_opt.udp_payload_size);
    try std.testing.expectEqual(opt.do_bit, stripped_opt.do_bit);

    var options = edns.options(rewritten, stripped_opt);
    while (try options.next()) |option| {
        try std.testing.expect(option.code != edns.ecs_option_code);
    }
    try std.testing.expect(
        (try edns.findOption(rewritten, stripped_opt, edns.ecs_option_code)) == null,
    );
}

/// Runs every typed RDATA accessor over a record. Each one rejects a record of
/// the wrong type or a truncated RDATA, so only a panic is a finding here.
fn sweepRdata(bytes: []const u8, rec: record.Record) void {
    ignore(record.rdataA(bytes, rec));
    ignore(record.rdataAaaa(bytes, rec));
    ignore(record.rdataCname(bytes, rec));
    ignore(record.rdataSoaMinimumTtl(bytes, rec));
    std.mem.doNotOptimizeAway(rec.rdata.slice(bytes));
}

fn ignore(result: anytype) void {
    if (result) |value| std.mem.doNotOptimizeAway(value) else |_| {}
}

/// Presentation form is lossy for labels holding a dot: `formatText` joins
/// labels with dots and `fromText` splits on them, so "a.b" as one label and
/// as two labels write the same text. Wire parsing accepts either, so the
/// round trip is only a property of names that carry no dot inside a label.
fn expectTextRoundTrip(n: name.Name) !void {
    var i: usize = 0;
    while (i < n.len and n.bytes[i] != 0) {
        const label_len = n.bytes[i];
        if (std.mem.findScalar(u8, n.bytes[i + 1 ..][0..label_len], '.') != null) return;
        i += 1 + @as(usize, label_len);
    }

    // Presentation form replaces each length byte with a separating dot and
    // drops the terminating zero, so it never exceeds the wire length.
    var text_buf: [types.max_name_len]u8 = undefined;
    var w: std.Io.Writer = .fixed(&text_buf);
    try name.formatText(n, &w);

    const round_tripped = try name.fromText(w.buffered());
    try std.testing.expectEqualSlices(u8, n.wire(), round_tripped.wire());
}
