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))
}