danos/library/kernel/file-system.zig

431 lines
19 KiB
Zig

//! 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 envelope = @import("envelope");
const vfs_protocol = @import("vfs-protocol");
/// The generated vfs contract: encode/decode for every verb, with the node id
/// carried in the packet header's `target`.
const Protocol = vfs_protocol.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,
@intFromEnum(Kind.protocol) => .protocol,
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;
},
}
}
// One request/reply round trip: frame `[Header][request][tail]`, send it, and
// hand back the whole reply packet for the caller to decode with the generated
// helpers. A backend that refused (a negative status) reads as null, which is
// what every caller here did with it anyway.
fn transact(
comptime operation: Protocol.Operation,
handle: ipc.Handle,
target: u64,
request: Protocol.RequestOf(operation),
tail: []const u8,
reply: []u8,
) ?[]u8 {
var packet: [vfs_protocol.message_maximum]u8 = undefined;
const framed = Protocol.encodeRequest(operation, target, request, tail, &packet) orelse return null;
const n = ipc.call(handle, framed, reply) catch return null;
const status = envelope.statusOf(reply[0..n]) orelse return null;
if (status.status != 0) return null;
return reply[0..n];
}
/// 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));
var reply: [vfs_protocol.message_maximum]u8 = undefined;
const answered = transact(.read, h, self.node, .{ .offset = self.offset, .len = want }, &.{}, &reply) orelse return null;
const bytes = Protocol.replyTail(.read, answered);
const n = @min(bytes.len, buffer.len);
@memcpy(buffer[0..n], bytes[0..n]);
self.offset += n;
return n;
}
/// 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));
var reply: [vfs_protocol.message_maximum]u8 = undefined;
const answered = transact(.write, h, self.node, .{ .offset = self.offset, .len = want }, data[0..want], &reply) orelse return null;
const written = Protocol.decodeReply(.write, answered) orelse return null;
self.offset += written.count;
return written.count;
}
/// 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 };
};
var reply: [vfs_protocol.message_maximum]u8 = undefined;
const answered = transact(.status, h, self.node, {}, &.{}, &reply) orelse return null;
const status = Protocol.decodeReply(.status, answered) orelse return null;
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;
var reply: [vfs_protocol.message_maximum]u8 = undefined;
_ = transact(.close, h, self.node, {}, &.{}, &reply);
}
};
/// 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();
var reply: [vfs_protocol.message_maximum]u8 = undefined;
const answered = transact(.open, b.handle, 0, .{ .flags = options.wireFlags() }, relative, &reply) orelse return null;
const opened = Protocol.decodeReply(.open, answered) orelse return null;
return .{ .node = opened.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;
};
var reply: [vfs_protocol.message_maximum]u8 = undefined;
const answered = transact(.readdir, h, self.node, .{ .cursor = self.cursor }, &.{}, &reply) orelse return false;
const header = Protocol.decodeReply(.readdir, answered) orelse return false;
if (header.name_len == 0) return false; // end of directory
entry.kind = kindFromWire(header.kind);
entry.size = header.size;
const source = Protocol.replyTail(.readdir, answered);
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(comptime operation: Protocol.Operation, path: []const u8) bool {
const route = resolve(path, 0) orelse return false;
if (route != .backend) return false;
var reply: [vfs_protocol.message_maximum]u8 = undefined;
return transact(operation, route.backend.handle, 0, {}, route.backendPath(), &reply) != null;
}
/// 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);
var reply: [vfs_protocol.message_maximum]u8 = undefined;
return transact(.rename, old_route.backend.handle, 0, {}, payload[0..total], &reply) != null;
}
/// 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;
}