//! runtime.fs — the danos-native file API. A program opens, reads, writes, and //! lists files through the kernel VFS root (resolve + redirect), each call //! marshalling a vfs-protocol request over IPC. This is the danos-native layer //! danos programs use directly; it is also where the file operations that later //! become `std.os.danos` are staged (see docs/zig-self-hosting.md). It replaces //! the old POSIX `unistd` shim — a compatibility spelling danos does not need yet. //! //! Handles are *values*, not entries in a global descriptor table: a `File` / //! `Directory` owns its VFS node id and (for files) a byte offset. So there is no //! per-process fd limit and no shared table to synchronise — the danos-native //! shape, unlike the POSIX fd model the old shim emulated. const std = @import("std"); const abi = @import("abi"); const sc = @import("system-call"); const ipc = @import("ipc"); const vfs_protocol = @import("vfs-protocol"); /// The kind of a filesystem node — re-exported so a caller need not import the /// wire protocol. pub const Kind = vfs_protocol.NodeKind; /// A node's metadata (the answer to a status request). pub const Attributes = struct { size: u64, kind: Kind, /// Modification time — Unix epoch seconds, UTC. 0 if the filesystem has none. mtime: u64 = 0, }; // Map a wire `NodeKind` value to the enum, defaulting anything unrecognised to // `.regular` (the server is trusted, but a value outside the enum would be // illegal to `@enumFromInt` directly). fn kindFromWire(value: u32) Kind { return switch (value) { @intFromEnum(Kind.directory) => .directory, @intFromEnum(Kind.character_device) => .character_device, @intFromEnum(Kind.block_device) => .block_device, @intFromEnum(Kind.symbolic_link) => .symbolic_link, @intFromEnum(Kind.fifo) => .fifo, @intFromEnum(Kind.socket) => .socket, else => .regular, }; } /// How to open a path. pub const OpenOptions = struct { /// Create the file if it does not exist. create: bool = false, /// Open a directory node (for listing) rather than a file. directory: bool = false, /// Truncate an existing file to zero length on open (O_TRUNC) — replace its /// contents rather than overwriting in place. truncate: bool = false, fn wireFlags(self: OpenOptions) u32 { var f: u32 = 0; if (self.create) f |= vfs_protocol.create; if (self.directory) f |= vfs_protocol.directory; if (self.truncate) f |= vfs_protocol.truncate; return f; } }; // The route to a path: the kernel resolves (fs_resolve) and either serves the // node itself (the initrd at /system — a permanent token) or redirects us to // the owning filesystem backend's endpoint, to which we speak the vfs-protocol // rendezvous directly with the rewritten mount-relative path. const Route = union(enum) { kernel: u64, backend: struct { handle: ipc.Handle, path: [224]u8, path_len: usize }, fn backendPath(self: *const Route) []const u8 { return self.backend.path[0..self.backend.path_len]; } }; fn resolve(path: []const u8, flags: usize) ?Route { var out: [224]u8 = undefined; const route = fsResolve(path, flags, &out) orelse return null; switch (route) { .kernel => |token| return .{ .kernel = token }, .backend => |b| { var r: Route = .{ .backend = .{ .handle = b.handle, .path = undefined, .path_len = b.path_len } }; @memcpy(r.backend.path[0..b.path_len], out[0..b.path_len]); return r; }, } } const Result = struct { reply: vfs_protocol.Reply, payload: []u8 }; // One request/reply round trip: [Request header][send payload] -> backend -> // [Reply header][receive payload]. The receive payload lands in `out`. fn transact(h: ipc.Handle, request: vfs_protocol.Request, send: []const u8, out: []u8) ?Result { var message: [vfs_protocol.message_maximum]u8 = undefined; @memcpy(message[0..vfs_protocol.request_size], std.mem.asBytes(&request)); const slen = @min(send.len, vfs_protocol.maximum_payload); @memcpy(message[vfs_protocol.request_size..][0..slen], send[0..slen]); var rbuf: [vfs_protocol.message_maximum]u8 = undefined; const n = ipc.call(h, message[0 .. vfs_protocol.request_size + slen], &rbuf) catch return null; if (n < vfs_protocol.reply_size) return null; const reply = std.mem.bytesToValue(vfs_protocol.Reply, rbuf[0..vfs_protocol.reply_size]); const rpl = @min(n - vfs_protocol.reply_size, out.len); @memcpy(out[0..rpl], rbuf[vfs_protocol.reply_size..][0..rpl]); return .{ .reply = reply, .payload = out[0..rpl] }; } /// An open file: a VFS node plus a byte cursor. Read and write advance the cursor. pub const File = struct { node: u64, offset: u64 = 0, /// The owning backend's endpoint, or null for a kernel-served node (the /// read-only /system tree), whose `node` is a permanent fs_node token. backend: ?ipc.Handle = null, /// Read up to `buffer.len` bytes at the current offset; returns the count, or /// null on error. pub fn read(self: *File, buffer: []u8) ?usize { const h = self.backend orelse { const n = fsNodeRead(self.node, self.offset, buffer) orelse return null; self.offset += n; return n; }; const want: u32 = @intCast(@min(buffer.len, vfs_protocol.maximum_payload)); const request = vfs_protocol.Request{ .operation = .read, .node = self.node, .offset = self.offset, .len = want, .flags = 0 }; const r = transact(h, request, &.{}, buffer) orelse return null; if (r.reply.status != 0) return null; self.offset += r.reply.len; return r.reply.len; } /// Write `data` at the current offset; returns the count written. A single /// call is capped at the VFS payload size, so the return may be short — use /// `writeAll` to write the whole slice. Null on error (kernel-served nodes /// are read-only). pub fn write(self: *File, data: []const u8) ?usize { const h = self.backend orelse return null; const want: u32 = @intCast(@min(data.len, vfs_protocol.maximum_payload)); const request = vfs_protocol.Request{ .operation = .write, .node = self.node, .offset = self.offset, .len = want, .flags = 0 }; const r = transact(h, request, data[0..want], &.{}) orelse return null; if (r.reply.status != 0) return null; self.offset += r.reply.len; return r.reply.len; } /// Write all of `data`, looping past the per-call payload cap. Returns the /// total written, or null if a write failed before any progress. pub fn writeAll(self: *File, data: []const u8) ?usize { var written: usize = 0; while (written < data.len) { const n = self.write(data[written..]) orelse return if (written == 0) null else written; if (n == 0) return written; // no forward progress; stop rather than spin written += n; } return written; } /// Move the read/write cursor to an absolute byte position. pub fn seekTo(self: *File, position: u64) void { self.offset = position; } /// This file's metadata. pub fn attributes(self: *File) ?Attributes { const h = self.backend orelse { const a = fsNodeStatus(self.node) orelse return null; return .{ .size = a.size, .kind = if (a.kind == file_kind_directory) .directory else .regular, .mtime = a.mtime }; }; const request = vfs_protocol.Request{ .operation = .status, .node = self.node, .offset = 0, .len = 0, .flags = 0 }; var buffer: [@sizeOf(vfs_protocol.FileStatus)]u8 = undefined; const r = transact(h, request, &.{}, &buffer) orelse return null; if (r.reply.status != 0 or r.payload.len < @sizeOf(vfs_protocol.FileStatus)) return null; const status = std.mem.bytesToValue(vfs_protocol.FileStatus, buffer[0..@sizeOf(vfs_protocol.FileStatus)]); return .{ .size = status.size, .kind = kindFromWire(status.kind), .mtime = status.mtime }; } /// Release the backend's open handle for this file. Kernel-served node /// tokens are permanent — nothing to release. pub fn close(self: *File) void { const h = self.backend orelse return; const request = vfs_protocol.Request{ .operation = .close, .node = self.node, .offset = 0, .len = 0, .flags = 0 }; _ = transact(h, request, &.{}, &.{}); } }; /// Open (or create, with `.create`) `path`. Returns the open file, or null. pub fn open(path: []const u8, options: OpenOptions) ?File { const route = resolve(path, options.wireFlags()) orelse return null; switch (route) { .kernel => |token| return .{ .node = token, .backend = null }, .backend => |b| { const relative = route.backendPath(); const request = vfs_protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(relative.len), .flags = options.wireFlags() }; const r = transact(b.handle, request, relative, &.{}) orelse return null; if (r.reply.status != 0) return null; return .{ .node = r.reply.node, .backend = b.handle }; }, } } /// A path's metadata without keeping it open (open -> status -> close). pub fn attributes(path: []const u8) ?Attributes { var file = open(path, .{}) orelse return null; defer file.close(); return file.attributes(); } /// Whether `path` resolves — handy as a readiness check (e.g. waiting for a mount /// to come up before writing to it). pub fn exists(path: []const u8) bool { return attributes(path) != null; } /// One entry returned by `Directory.next`. pub const Entry = struct { kind: Kind = .regular, size: u64 = 0, name_buffer: [64]u8 = undefined, name_len: usize = 0, pub fn name(self: *const Entry) []const u8 { return self.name_buffer[0..self.name_len]; } }; /// An open directory being listed, cursor-advanced by `next`. pub const Directory = struct { node: u64, cursor: u64 = 0, backend: ?ipc.Handle = null, /// Fill `entry` with the next directory entry; false at end of directory or /// on error. pub fn next(self: *Directory, entry: *Entry) bool { const h = self.backend orelse { var buffer: [@sizeOf(DirectoryEntryHeader) + 64]u8 = undefined; const n = fsNodeReaddir(self.node, self.cursor, &buffer) orelse return false; if (n < @sizeOf(DirectoryEntryHeader)) return false; // end const header = std.mem.bytesToValue(DirectoryEntryHeader, buffer[0..@sizeOf(DirectoryEntryHeader)]); entry.kind = if (header.kind == file_kind_directory) .directory else .regular; entry.size = header.size; const nlen = @min(@as(usize, header.name_len), entry.name_buffer.len); @memcpy(entry.name_buffer[0..nlen], buffer[@sizeOf(DirectoryEntryHeader)..][0..nlen]); entry.name_len = nlen; self.cursor += 1; return true; }; const request = vfs_protocol.Request{ .operation = .readdir, .node = self.node, .offset = self.cursor, .len = 0, .flags = 0 }; var buffer: [vfs_protocol.message_maximum]u8 = undefined; const r = transact(h, request, &.{}, &buffer) orelse return false; if (r.reply.status != 0 or r.reply.len == 0) return false; // error or EOF if (r.payload.len < vfs_protocol.directory_entry_size) return false; const header = std.mem.bytesToValue(vfs_protocol.DirectoryEntry, r.payload[0..vfs_protocol.directory_entry_size]); entry.kind = kindFromWire(header.kind); entry.size = header.size; const source = r.payload[vfs_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; self.cursor += 1; return true; } /// Release the backend's open handle for this directory. pub fn close(self: *Directory) void { var f = File{ .node = self.node, .backend = self.backend }; f.close(); } }; /// Open `path` as a directory for listing. Returns null if it isn't one / on error. pub fn openDirectory(path: []const u8) ?Directory { const file = open(path, .{ .directory = true }) orelse return null; return .{ .node = file.node, .backend = file.backend }; } // A path-based request that returns only a status (mkdir, unlink). Kernel-served // paths (the read-only /system) refuse mutation by construction: the resolve // must land on a backend. fn pathOperation(operation: vfs_protocol.Operation, path: []const u8) bool { const route = resolve(path, 0) orelse return false; if (route != .backend) return false; const relative = route.backendPath(); const request = vfs_protocol.Request{ .operation = operation, .node = 0, .offset = 0, .len = @intCast(relative.len), .flags = 0 }; const r = transact(route.backend.handle, request, relative, &.{}) orelse return false; return r.reply.status == 0; } /// Create a directory at `path` (its parent must already exist). Returns true on /// success. Only works under a mounted filesystem that supports directories. pub fn makeDirectory(path: []const u8) bool { return pathOperation(.mkdir, path); } /// Create every missing directory along `path` (mkdir -p). Probes each prefix /// with `exists` first — a FAT mkdir of an existing name is refused, and the /// probe keeps the common "already there" case cheap. Returns true when the /// whole path exists afterwards. pub fn makePath(path: []const u8) bool { var end: usize = 0; while (end < path.len) { end += 1; while (end < path.len and path[end] != '/') end += 1; const prefix = path[0..end]; if (prefix.len == 0 or (prefix.len == 1 and prefix[0] == '/')) continue; // Best-effort per prefix: components at or above a mount point ("/volumes") // are router names, not filesystem nodes — they neither exist as nodes // nor accept mkdir, and that is fine. Only the final verdict counts. if (!exists(prefix)) _ = makeDirectory(prefix); } return exists(path); } /// Remove the file at `path`. Returns true on success. Directories are refused /// (a separate directory-removal would have to check emptiness). pub fn remove(path: []const u8) bool { return pathOperation(.unlink, path); } /// Rename `old_path` to `new_path`. Both must resolve to the SAME filesystem /// backend (same-directory, 8.3-name rename only for now). Returns true on /// success. pub fn rename(old_path: []const u8, new_path: []const u8) bool { const old_route = resolve(old_path, 0) orelse return false; const new_route = resolve(new_path, 0) orelse return false; if (old_route != .backend or new_route != .backend) return false; if (old_route.backend.handle != new_route.backend.handle) return false; // cross-filesystem const old_relative = old_route.backendPath(); const new_relative = new_route.backendPath(); const total = old_relative.len + 1 + new_relative.len; if (total > vfs_protocol.maximum_payload) return false; var payload: [vfs_protocol.maximum_payload]u8 = undefined; @memcpy(payload[0..old_relative.len], old_relative); payload[old_relative.len] = 0; @memcpy(payload[old_relative.len + 1 ..][0..new_relative.len], new_relative); const request = vfs_protocol.Request{ .operation = .rename, .node = 0, .offset = 0, .len = @intCast(total), .flags = 0 }; const r = transact(old_route.backend.handle, request, payload[0..total], &.{}) orelse return false; return r.reply.status == 0; } /// Mount a filesystem backend (its server endpoint) at absolute path `target`; /// the kernel VFS then routes everything under `target` to that backend. /// Possession of the endpoint handle is the capability. Returns true on success. pub fn mount(target: []const u8, backend: ipc.Handle) bool { return fsMount(target, backend, ""); } /// As `mount`, with a backend-side rewrite prefix: a path under `target` reaches /// the backend as `rewrite` + the mount-relative tail. How one volume serves /// several mounts ("/volumes/usb" from its root, "/system/logs" from its /// /system/logs subtree). pub fn mountRewritten(target: []const u8, backend: ipc.Handle, rewrite: []const u8) bool { return fsMount(target, backend, rewrite); } // --- raw filesystem syscalls, formerly in the system.zig dumping ground --- pub const FileAttributes = abi.FileAttributes; pub const DirectoryEntryHeader = abi.DirectoryEntryHeader; pub const file_kind_regular = abi.file_kind_regular; pub const file_kind_directory = abi.file_kind_directory; /// Where fs_resolve routed a path: served by the kernel (a permanent node token for /// `fs_node`) or by a user-space filesystem backend (an endpoint handle plus the rewritten /// mount-relative path returned in the caller's buffer). pub const FsRoute = union(enum) { kernel: u64, backend: struct { handle: usize, path_len: usize }, }; /// Route `path` through the kernel VFS. For a backend route the rewritten mount-relative /// path lands in `out` (behind a kernel-written length prefix, already stripped here: /// out[0..path_len] is the path). pub fn fsResolve(path: []const u8, flags: usize, out: []u8) ?FsRoute { var rax: usize = undefined; var rdx: usize = flags; // in: flags (arg #3); out: node token / backend handle asm volatile ("syscall" : [rax] "={rax}" (rax), [rdx] "+{rdx}" (rdx), : [n] "{rax}" (@intFromEnum(abi.SystemCall.fs_resolve)), [a0] "{rdi}" (@intFromPtr(path.ptr)), [a1] "{rsi}" (path.len), [a3] "{r10}" (@intFromPtr(out.ptr)), [a4] "{r8}" (out.len), : .{ .rcx = true, .r11 = true, .memory = true }); if (@as(isize, @bitCast(rax)) < 0) return null; if (rax == abi.fs_route_kernel) return .{ .kernel = rdx }; if (rax != abi.fs_route_backend) return null; const path_len = @as(usize, out[0]) | (@as(usize, out[1]) << 8); if (path_len + 2 > out.len) return null; std.mem.copyForwards(u8, out[0..path_len], out[2..][0..path_len]); return .{ .backend = .{ .handle = rdx, .path_len = path_len } }; } /// Read `out.len` bytes of a kernel-served node at `offset` (fs_node read). pub fn fsNodeRead(node_token: u64, offset: u64, out: []u8) ?usize { const r = sc.systemCall5(.fs_node, abi.fs_node_read, node_token, offset, @intFromPtr(out.ptr), out.len); if (@as(isize, @bitCast(r)) < 0) return null; return r; } /// A kernel-served node's metadata (fs_node status). pub fn fsNodeStatus(node_token: u64) ?abi.FileAttributes { var attrs: abi.FileAttributes = undefined; const r = sc.systemCall5(.fs_node, abi.fs_node_status, node_token, 0, @intFromPtr(&attrs), @sizeOf(abi.FileAttributes)); if (@as(isize, @bitCast(r)) < 0) return null; return attrs; } /// The `cursor`th child of a kernel-served directory (fs_node readdir): fills `out` with /// [DirectoryEntryHeader][name]; returns total bytes (0 = end). pub fn fsNodeReaddir(node_token: u64, cursor: u64, out: []u8) ?usize { const r = sc.systemCall5(.fs_node, abi.fs_node_readdir, node_token, cursor, @intFromPtr(out.ptr), out.len); if (@as(isize, @bitCast(r)) < 0) return null; return r; } /// Mount a userspace filesystem's endpoint at `prefix`, with an optional backend-side /// `rewrite` prefix ("" = none). Possession of the endpoint handle is the capability. pub fn fsMount(prefix: []const u8, backend: usize, rewrite: []const u8) bool { return sc.systemCall5(.fs_mount, @intFromPtr(prefix.ptr), prefix.len, backend, @intFromPtr(rewrite.ptr), rewrite.len) == 0; } pub fn fsUnmount(prefix: []const u8) bool { return sc.systemCall2(.fs_unmount, @intFromPtr(prefix.ptr), prefix.len) == 0; }