M5: VFS mount support — mount table + forwarding router

Turn the flat-ramfs VFS into a router: a mount table maps an absolute path prefix
(e.g. /mnt/usb) to a backend server's endpoint, and open/read/write/status/
readdir/close on a path under a mount are forwarded to that backend, which speaks
the same vfs-protocol. This is what a FAT filesystem mounts into.

- protocol: append readdir / mount / unmount operations, a NodeKind enum (the FSH
  file types) that now fills FileStatus.kind, a DirectoryEntry record, and a
  directory open flag. Appended values keep existing clients and tests unchanged.
- vfs.zig: a mount table, longest-prefix routing, forwarding of every op on a
  backend handle, mount/unmount handlers (the backend arrives as the call's
  capability), and release-on-death that also closes the backend's handles.
- path.zig: pure, host-tested mount-prefix matching that never captures a
  non-boundary like /mnt/usbextra.
- unistd: mount(), opendir / readdir / closedir clients.

Bare names still resolve in the flat ramfs — the backward-compat contract; the
vfs and vfs-client-death tests pass unchanged. End-to-end mount+read is exercised
by the FAT server (M6). Host units cover path matching and protocol sizes.
This commit is contained in:
Daniel Samson
2026-07-13 14:21:30 +01:00
parent 3fb9d5936a
commit 35e8921de8
5 changed files with 322 additions and 16 deletions
+58
View File
@@ -146,3 +146,61 @@ pub fn close(fd: i32) void {
_ = transact(request, &.{}, &.{});
f.used = false;
}
/// Mount a filesystem backend (its server endpoint) at absolute path `target`;
/// the VFS then routes every path under `target` to that backend. Returns 0 or
/// -1. This is the one call that hands the VFS a capability (the backend).
pub fn mount(target: []const u8, backend: usize) i32 {
const h = vfs() orelse return -1;
const request = protocol.Request{ .operation = .mount, .node = 0, .offset = 0, .len = @intCast(target.len), .flags = 0 };
var message: [protocol.message_maximum]u8 = undefined;
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
const tlen = @min(target.len, protocol.maximum_payload);
@memcpy(message[protocol.request_size..][0..tlen], target[0..tlen]);
var rbuf: [protocol.message_maximum]u8 = undefined;
const result = ipc.callCap(h, message[0 .. protocol.request_size + tlen], &rbuf, backend) catch return -1;
if (result.len < protocol.reply_size) return -1;
return if (std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]).status == 0) 0 else -1;
}
/// A directory entry filled by `readdir`.
pub const DirEntry = struct {
kind: u32 = 0, // a protocol.NodeKind
size: u64 = 0,
name_buffer: [64]u8 = undefined,
name_len: usize = 0,
pub fn name(self: *const DirEntry) []const u8 {
return self.name_buffer[0..self.name_len];
}
};
/// Open a directory for reading with `readdir`. Returns an fd or -1.
pub fn opendir(path: []const u8) i32 {
return open(path, protocol.directory);
}
/// Read the next entry of a directory fd into `entry`; returns false at EOF or on
/// error. Advances the fd's cursor by one entry.
pub fn readdir(fd: i32, entry: *DirEntry) bool {
const f = fdPtr(fd) orelse return false;
const request = protocol.Request{ .operation = .readdir, .node = f.node, .offset = f.offset, .len = 0, .flags = 0 };
var buffer: [protocol.message_maximum]u8 = undefined;
const r = transact(request, &.{}, &buffer) orelse return false;
if (r.reply.status != 0 or r.reply.len == 0) return false; // error or EOF
if (r.payload.len < protocol.directory_entry_size) return false;
const header = std.mem.bytesToValue(protocol.DirectoryEntry, r.payload[0..protocol.directory_entry_size]);
entry.kind = header.kind;
entry.size = header.size;
const source = r.payload[protocol.directory_entry_size..];
const nlen = @min(@min(@as(usize, header.name_len), source.len), entry.name_buffer.len);
@memcpy(entry.name_buffer[0..nlen], source[0..nlen]);
entry.name_len = nlen;
f.offset += 1;
return true;
}
/// Close a directory fd (same as `close`).
pub fn closedir(fd: i32) void {
close(fd);
}