ajhahn.de
← FlashOS
Flash 271 lines
// initramfs_backend: src/initramfs.zig newc cpio parser as a
// VfsOps vtable.
//
// Lives separately from src/initramfs.zig on purpose: the parser stays
// VFS-agnostic and host-testable in isolation (it imports neither
// `vfs` nor `file`). The split mirrors a fsh -> flibc -> syscalls
// layering — the bottom layer never imports the top layer's types.
//
// The read / seek bodies live here next to their private state; the
// sys.zig handlers dispatch through the vtable rather than inlining
// the per-backend arithmetic.

const initramfs = #import("initramfs")
const vfs = #import("vfs")
const file_mod = #import("file")

const File = file_mod.File

// Single static superblock — initramfs is a singleton mount (slot 0).
// fs_type is re-stamped by vfs.register_initramfs; the initialiser
// here just keeps the field non-garbage before init() runs.
pub var sb vfs.SuperBlock = .{ .fs_type = #intFromEnum(vfs.FsType.INITRAMFS) }

// `var`, not `const`: init() relocates these entries to their
// high-mem aliases in place via vfs.relocateOps (see there).
var ops_vtable vfs.VfsOps = .{
    .open = open,
    .read = read,
    .seek = seek,
    .close = close,
    .write = writeEROFS,
    .readdir = readdir,
    .create = createEROFS,
    .unlink = unlinkEROFS,
    .rename = renameEROFS,
}

// Maximum directory-query path the synthesised-prefix walk handles. The
// query is the resolved absolute path sys_readdir copies in, bounded by
// the task cwd buffer; a longer path can prefix no stored name, so it
// lists as an empty directory.
const READDIR_PREFIX_MAX = 256

// Byte-wise compare for archive-borrowed child slices (unaligned
// `.incbin` bytes — same rationale as initramfs.zig's bytesEql).
extern fn mem_eql_bytes(a [*]u8, b [*]u8, n u64) bool

fn bytesEql(a []u8, b []u8) bool {
    if a.len != b.len { return false }
    return mem_eql_bytes(a.ptr, b.ptr, a.len)
}

// Kernel bring-up hook — relocates the vtable to its high-mem alias,
// wires it onto the superblock, and registers the mount. Called from
// kernel_main_impl before the free-page baseline emit; allocates
// nothing (just sets pointers), so the baseline holds.
pub fn init() void {
    vfs.relocateOps(&ops_vtable)
    sb.ops = &ops_vtable
    vfs.register_initramfs(&sb)
}

fn open(_ *mut vfs.SuperBlock, path_ptr [*]u8, path_len usize, out *mut vfs.OpenResult) callconv(.c) c_int {
    const path = path_ptr[0..path_len]
    const entry = (initramfs.locate(path) catch return -1) orelse return -1
    out.private = #intFromPtr(entry.data.ptr)
    out.size = entry.data.len
    // Permission metadata straight from the newc header — the
    // build encoder stamps per-file mode + root ownership, so the
    // syscall layer can enforce them (e.g. /etc/shadow 0600).
    out.mode = entry.mode
    out.uid = entry.uid
    out.gid = entry.gid
    return 0
}

fn read(_ *mut vfs.SuperBlock, f *mut File, buf [*]mut u8, len u64) callconv(.c) i64 {
    if f.offset >= f.size { return 0 }
    const remaining = f.size - f.offset
    const n u64 = if (len > remaining) remaining else len
    const src [*]u8 = #ptrFromInt(f.private)
    var i u64 = 0
    while i < n {
        buf[i] = src[f.offset + i]
        i += 1
    }
    f.offset += n
    return #bitCast(n)
}

fn seek(_ *mut vfs.SuperBlock, f *mut File, off i64, whence i32) callconv(.c) i64 {
    const cur_signed i64 = #bitCast(f.offset)
    const sz_signed i64 = #bitCast(f.size)
    const target i64 = switch whence {
        0 => off, // SEEK_SET
        1 => cur_signed + off, // SEEK_CUR
        2 => sz_signed + off, // SEEK_END
        else => return -1,
    }
    if (target < 0 || target > sz_signed) { return -1 }
    f.offset = #bitCast(target)
    return target
}

fn close(_ *mut vfs.SuperBlock, _ *mut File) callconv(.c) void {
    // Initramfs has no per-handle state beyond what file.zig owns —
    // the File page lifetime is the refcount's job.
}

// Initramfs is read-only by design (it's the CPIO image baked into
// the kernel). Every write returns -1 — caller treats as EROFS.
fn writeEROFS(_ *mut vfs.SuperBlock, _ *mut File, _ [*]u8, _ u64) callconv(.c) i64 {
    return -1
}

// create / unlink / rename are likewise EROFS on the read-only root. Wired
// explicitly (not left to the VfsOps defaults) so the read-only contract is
// visible at this backend, not inferred from an absent field.
fn createEROFS(_ *mut vfs.SuperBlock, _ [*]u8, _ usize, _ *mut vfs.OpenResult) callconv(.c) c_int {
    return -1
}
fn unlinkEROFS(_ *mut vfs.SuperBlock, _ [*]u8, _ usize) callconv(.c) c_int {
    return -1
}
fn renameEROFS(_ *mut vfs.SuperBlock, _ [*]u8, _ usize, _ [*]u8, _ usize) callconv(.c) c_int {
    return -1
}

// Synthesise a directory listing from the flat cpio. `path` is the
// directory to enumerate (absolute, mount-prefix-stripped). Stored names
// are walked in archive (sorted) order; each one's direct-child
// contribution to `path` (initramfs.directEntry) is collapsed against
// the previous distinct child, so duplicate synthetic subdirs fold into
// one entry. The `index`-th distinct child fills `out` and returns 0;
// past the last child (or on a parse error / over-long path) returns -1.
// Allocates nothing — fixed prefix buffer + the caller's Dirent.
fn readdir(_ *mut vfs.SuperBlock, path_ptr [*]u8, path_len usize, index u64, out *mut vfs.Dirent) callconv(.c) c_int {
    const path = path_ptr[0..path_len]

    // Lookup prefix = the directory path with a guaranteed trailing
    // slash. Root "/" already ends in one; everything else gets one
    // appended so "/bin" matches "/bin/cat" but not "/binutils/x".
    var prefix_buf [READDIR_PREFIX_MAX]u8 = undefined
    const prefix []u8 = blk: {
        if (path.len == 1 && path[0] == '/') { break :blk "/" }
        if (path.len + 1 > prefix_buf.len) { return -1 }
        #memcpy(prefix_buf[0..path.len], path)
        prefix_buf[path.len] = '/'
        break :blk prefix_buf[0 .. path.len + 1]
    }

    var it = initramfs.iterator()
    var emitted u64 = 0
    var last_child []u8 = &.{}
    var have_last = false
    while (it.next() catch return -1) |e| {
        const de = initramfs.directEntry(e.name, prefix) orelse continue
        if (have_last && bytesEql(de.child, last_child)) { continue } // adjacent dup
        last_child = de.child
        have_last = true
        if (emitted == index) {
            out.* = .{}
            const n = #min(de.child.len, out.name.len - 1)
            #memcpy(out.name[0..n], de.child[0..n])
            out.d_type = if (de.is_dir) vfs.DT_DIR else vfs.DT_REG
            return 0
        }
        emitted += 1
    }
    return -1
}

// ---- Host Tests ----
const std = #import("std")
const testing = std.testing

test "initramfs_backend: open/read/seek" {
    // Manually build a minimal cpio archive with one file "hello" containing "world"
    // Header (110 bytes) + name "hello\0" (6 bytes) + pad (0) + data "world" (5 bytes) + pad (3)
    // OFF_NAMESIZE = 6 + 8 * 11 = 94
    // OFF_FILESIZE = 6 + 8 * 6 = 54
    // OFF_MODE = 6 + 8 * 1 = 14
    var archive [256]u8 = [_]u8{'0'} ** 256
    #memcpy(archive[0..6], "070701")
    #memcpy(archive[14..22], "000081A4") // mode=0o100644
    #memcpy(archive[22..30], "000003E8") // uid=1000
    #memcpy(archive[30..38], "000003E8") // gid=1000
    #memcpy(archive[54..62], "00000005") // filesize=5
    #memcpy(archive[94..102], "00000006") // namesize=6
    #memcpy(archive[110..116], "hello\x00")
    #memcpy(archive[116..121], "world")

    // Add trailer to be a valid archive
    const trailer_start = 124 // 116 + 5 (data) + 3 (pad) = 124
    #memcpy(archive[trailer_start..][0..6], "070701")
    #memcpy(archive[trailer_start + 94 ..][0..8], "0000000B") // namesize=11
    #memcpy(archive[trailer_start + 110 ..][0..11], "TRAILER!!!\x00")

    initramfs.host_fixture_base = &archive
    initramfs.host_fixture_size = archive.len

    var out vfs.OpenResult = .{}
    const res = open(&sb, "hello", 5, &out)
    try testing.expectEqual(#as(c_int, 0), res)
    try testing.expectEqual(#as(u64, 5), out.size)
    try testing.expect(out.private != 0)
    // The newc header's mode/uid/gid thread through into OpenResult.
    try testing.expectEqual(#as(u32, 0o100644), out.mode)
    try testing.expectEqual(#as(u32, 1000), out.uid)
    try testing.expectEqual(#as(u32, 1000), out.gid)

    var f File = .{
        .refs = 1,
        .offset = 0,
        .size = out.size,
        .private = out.private,
        .sb = &sb,
    }

    var buf [5]u8 = undefined
    const n = read(&sb, &f, &buf, 5)
    try testing.expectEqual(#as(i64, 5), n)
    try testing.expectEqualStrings("world", &buf)
    try testing.expectEqual(#as(u64, 5), f.offset)

    // Seek back to start
    const s = seek(&sb, &f, 0, 0)
    try testing.expectEqual(#as(i64, 0), s)
    try testing.expectEqual(#as(u64, 0), f.offset)

    // Read again
    const n2 = read(&sb, &f, &buf, 3)
    try testing.expectEqual(#as(i64, 3), n2)
    try testing.expectEqualStrings("wor", buf[0..3])
}

test "initramfs_backend: readdir synthesises dirs with adjacent-dup collapse" {
    // Sorted arcs (as the real cpio is staged): the two /bin/* entries
    // are adjacent, so their synthetic "bin" subdir must collapse to one.
    comptime const fixture = initramfs.buildFixture(&.{
        .{ .name = "/bin/cat", .data = "C", .mode = 0o100755 },
        .{ .name = "/bin/echo", .data = "E", .mode = 0o100755 },
        .{ .name = "/etc/fshrc", .data = "F", .mode = 0o100644 },
        .{ .name = "/sbin/init", .data = "I", .mode = 0o100755 },
    })
    initramfs.host_fixture_base = fixture.ptr
    initramfs.host_fixture_size = fixture.len

    var d vfs.Dirent = .{}

    // Root: three top-level dirs, "bin" synthesised once despite two
    // /bin/* files.
    try testing.expectEqual(#as(c_int, 0), readdir(&sb, "/".ptr, 1, 0, &d))
    try testing.expectEqualStrings("bin", std.mem.sliceTo(&d.name, 0))
    try testing.expectEqual(vfs.DT_DIR, d.d_type)
    try testing.expectEqual(#as(c_int, 0), readdir(&sb, "/".ptr, 1, 1, &d))
    try testing.expectEqualStrings("etc", std.mem.sliceTo(&d.name, 0))
    try testing.expectEqual(#as(c_int, 0), readdir(&sb, "/".ptr, 1, 2, &d))
    try testing.expectEqualStrings("sbin", std.mem.sliceTo(&d.name, 0))
    // Past the last distinct child: end sentinel.
    try testing.expectEqual(#as(c_int, -1), readdir(&sb, "/".ptr, 1, 3, &d))

    // /bin lists its two files as DT_REG, then ends.
    try testing.expectEqual(#as(c_int, 0), readdir(&sb, "/bin".ptr, 4, 0, &d))
    try testing.expectEqualStrings("cat", std.mem.sliceTo(&d.name, 0))
    try testing.expectEqual(vfs.DT_REG, d.d_type)
    try testing.expectEqual(#as(c_int, 0), readdir(&sb, "/bin".ptr, 4, 1, &d))
    try testing.expectEqualStrings("echo", std.mem.sliceTo(&d.name, 0))
    try testing.expectEqual(#as(c_int, -1), readdir(&sb, "/bin".ptr, 4, 2, &d))
}