//! The initial_ramdisk (initial ramdisk) container format — built in RAM by the
//! bootloader (boot/efi.zig walks the boot volume's /system tree) and unpacked by
//! the kernel. Deliberately trivial: a header, a table of fixed-size entries, then
//! the concatenated file blobs. We own both producer and consumer, so it need be
//! no fancier.
//!
//! v2: entry names are full FHS paths ("/system/services/init"), 64 bytes — the
//! same limit as a task name (abi.maximum_process_name), so a path-named task is
//! never truncated. The boot volume's file tree is the single source of truth;
//! this image is only the loader→kernel handoff snapshot of it.
//!
//! Layout:
//!   Header                       (magic, count)
//!   Entry * count                (name, offset, len) — offset/len into the image
//!   blob bytes...                (each entry's file, at its offset)

const std = @import("std");

/// "DNR2" — identifies a danos initial_ramdisk image, format v2 (path names).
/// The v1 magic ("DNRD", basename entries) is rejected: a stale image should
/// fail loudly at Reader.init, not misparse names.
pub const magic: u32 = 0x32524E44;

/// Entry name capacity. Matches abi.maximum_process_name so a spawned task can
/// always carry its full binary path as its name.
pub const maximum_name = 64;

pub const Header = extern struct {
    magic: u32,
    count: u32,
};

pub const Entry = extern struct {
    name: [maximum_name]u8, // NUL-padded FHS path, e.g. "/system/services/init"
    offset: u64, // byte offset of the blob within the image
    len: u64, // blob length in bytes
};

/// The basename of a path: the final component after the last '/'.
pub fn basename(path: []const u8) []const u8 {
    const i = std.mem.lastIndexOfScalar(u8, path, '/') orelse return path;
    return path[i + 1 ..];
}

/// A validated view over an initial_ramdisk image. `init` checks the magic and that the
/// entry table fits; `entry` bounds-checks each blob against the image.
pub const Reader = struct {
    image: []const u8,
    count: u32,

    pub fn init(image: []const u8) ?Reader {
        if (image.len < @sizeOf(Header)) return null;
        const h = std.mem.bytesToValue(Header, image[0..@sizeOf(Header)]);
        if (h.magic != magic) return null;
        const table_end = @sizeOf(Header) + @as(usize, h.count) * @sizeOf(Entry);
        if (table_end > image.len) return null;
        return .{ .image = image, .count = h.count };
    }

    pub const Item = struct { name: []const u8, blob: []const u8 };

    pub fn entry(self: Reader, i: u32) ?Item {
        if (i >= self.count) return null;
        const off = @sizeOf(Header) + @as(usize, i) * @sizeOf(Entry);
        const e = std.mem.bytesToValue(Entry, self.image[off..][0..@sizeOf(Entry)]);
        if (e.offset > self.image.len or e.len > self.image.len - e.offset) return null;
        // The name is stored in the entry's fixed field; return a stable slice
        // into the image (not the value copy) up to the NUL terminator.
        const name_field = self.image[off .. off + maximum_name];
        const nlen = std.mem.indexOfScalar(u8, name_field, 0) orelse name_field.len;
        return .{
            .name = name_field[0..nlen],
            .blob = self.image[@intCast(e.offset)..][0..@intCast(e.len)],
        };
    }

    /// Look a binary up by name: an exact path match wins; otherwise a unique
    /// basename match ("fat" finds "/system/services/fat") keeps pre-path callers
    /// working. Comparisons are ASCII case-insensitive — the entries come from a
    /// FAT volume, whose name lookups are case-insensitive by definition (and
    /// whose short entries store uppercase). The returned Item's name is always
    /// the stored full path.
    pub fn find(self: Reader, name: []const u8) ?Item {
        var i: u32 = 0;
        while (i < self.count) : (i += 1) {
            const item = self.entry(i) orelse continue;
            if (std.ascii.eqlIgnoreCase(item.name, name)) return item;
        }
        i = 0;
        while (i < self.count) : (i += 1) {
            const item = self.entry(i) orelse continue;
            if (std.ascii.eqlIgnoreCase(basename(item.name), name)) return item;
        }
        return null;
    }
};

// --- tests (host) -----------------------------------------------------------

fn testImage(buffer: []u8, entries: []const struct { name: []const u8, blob: []const u8 }) []const u8 {
    const table_end = @sizeOf(Header) + entries.len * @sizeOf(Entry);
    var offset: usize = table_end;
    std.mem.bytesAsValue(Header, buffer[0..@sizeOf(Header)]).* = .{ .magic = magic, .count = @intCast(entries.len) };
    for (entries, 0..) |e, i| {
        var record = Entry{ .name = @splat(0), .offset = offset, .len = e.blob.len };
        @memcpy(record.name[0..e.name.len], e.name);
        std.mem.bytesAsValue(Entry, buffer[@sizeOf(Header) + i * @sizeOf(Entry) ..][0..@sizeOf(Entry)]).* = record;
        @memcpy(buffer[offset..][0..e.blob.len], e.blob);
        offset += e.blob.len;
    }
    return buffer[0..offset];
}

test "find matches exact path, then unique basename; name is the stored path" {
    var buffer: [1024]u8 = undefined;
    const image = testImage(&buffer, &.{
        .{ .name = "/system/services/init", .blob = "INIT" },
        .{ .name = "/system/drivers/ps2-bus", .blob = "PS2" },
    });
    const rd = Reader.init(image).?;

    const by_path = rd.find("/system/services/init").?;
    try std.testing.expectEqualStrings("/system/services/init", by_path.name);
    try std.testing.expectEqualStrings("INIT", by_path.blob);

    const by_base = rd.find("ps2-bus").?;
    try std.testing.expectEqualStrings("/system/drivers/ps2-bus", by_base.name);
    try std.testing.expectEqualStrings("PS2", by_base.blob);

    try std.testing.expect(rd.find("no-such-binary") == null);
}

test "v1 magic is rejected" {
    var buffer: [64]u8 = @splat(0);
    std.mem.bytesAsValue(Header, buffer[0..@sizeOf(Header)]).* = .{ .magic = 0x444E5244, .count = 0 };
    try std.testing.expect(Reader.init(&buffer) == null);
}

test "basename" {
    try std.testing.expectEqualStrings("fat", basename("/system/services/fat"));
    try std.testing.expectEqualStrings("fat", basename("fat"));
}
