275 lines
12 KiB
Zig
275 lines
12 KiB
Zig
//! runtime.fs — the danos-native file API. A program opens, reads, writes, and
|
|
//! lists files served by the user-space VFS (system/services/vfs), 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 ipc = @import("ipc.zig");
|
|
const protocol = @import("vfs-protocol");
|
|
|
|
/// The kind of a filesystem node — re-exported so a caller need not import the
|
|
/// wire protocol.
|
|
pub const Kind = 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 |= protocol.create;
|
|
if (self.directory) f |= protocol.directory;
|
|
if (self.truncate) f |= protocol.truncate;
|
|
return f;
|
|
}
|
|
};
|
|
|
|
// The VFS server endpoint, looked up once by well-known id and cached.
|
|
var vfs_handle: ipc.Handle = 0;
|
|
var vfs_resolved = false;
|
|
fn vfs() ?ipc.Handle {
|
|
if (!vfs_resolved) {
|
|
vfs_handle = ipc.lookup(.vfs) orelse return null;
|
|
vfs_resolved = true;
|
|
}
|
|
return vfs_handle;
|
|
}
|
|
|
|
const Result = struct { reply: protocol.Reply, payload: []u8 };
|
|
|
|
// One request/reply round trip: [Request header][send payload] -> VFS ->
|
|
// [Reply header][receive payload]. The receive payload lands in `out`.
|
|
fn transact(request: protocol.Request, send: []const u8, out: []u8) ?Result {
|
|
const h = vfs() orelse return null;
|
|
var message: [protocol.message_maximum]u8 = undefined;
|
|
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
|
|
const slen = @min(send.len, protocol.maximum_payload);
|
|
@memcpy(message[protocol.request_size..][0..slen], send[0..slen]);
|
|
|
|
var rbuf: [protocol.message_maximum]u8 = undefined;
|
|
const n = ipc.call(h, message[0 .. protocol.request_size + slen], &rbuf) catch return null;
|
|
if (n < protocol.reply_size) return null;
|
|
const reply = std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]);
|
|
const rpl = @min(n - protocol.reply_size, out.len);
|
|
@memcpy(out[0..rpl], rbuf[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,
|
|
|
|
/// 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 want: u32 = @intCast(@min(buffer.len, protocol.maximum_payload));
|
|
const request = protocol.Request{ .operation = .read, .node = self.node, .offset = self.offset, .len = want, .flags = 0 };
|
|
const r = transact(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.
|
|
pub fn write(self: *File, data: []const u8) ?usize {
|
|
const want: u32 = @intCast(@min(data.len, protocol.maximum_payload));
|
|
const request = protocol.Request{ .operation = .write, .node = self.node, .offset = self.offset, .len = want, .flags = 0 };
|
|
const r = transact(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 request = protocol.Request{ .operation = .status, .node = self.node, .offset = 0, .len = 0, .flags = 0 };
|
|
var buffer: [@sizeOf(protocol.FileStatus)]u8 = undefined;
|
|
const r = transact(request, &.{}, &buffer) orelse return null;
|
|
if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.FileStatus)) return null;
|
|
const status = std.mem.bytesToValue(protocol.FileStatus, buffer[0..@sizeOf(protocol.FileStatus)]);
|
|
return .{ .size = status.size, .kind = kindFromWire(status.kind), .mtime = status.mtime };
|
|
}
|
|
|
|
/// Release the VFS's open handle for this file.
|
|
pub fn close(self: *File) void {
|
|
const request = protocol.Request{ .operation = .close, .node = self.node, .offset = 0, .len = 0, .flags = 0 };
|
|
_ = transact(request, &.{}, &.{});
|
|
}
|
|
};
|
|
|
|
/// Open (or create, with `.create`) `path`. Returns the open file, or null.
|
|
pub fn open(path: []const u8, options: OpenOptions) ?File {
|
|
const request = protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(path.len), .flags = options.wireFlags() };
|
|
const r = transact(request, path, &.{}) orelse return null;
|
|
if (r.reply.status != 0) return null;
|
|
return .{ .node = r.reply.node };
|
|
}
|
|
|
|
/// 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,
|
|
|
|
/// Fill `entry` with the next directory entry; false at end of directory or
|
|
/// on error.
|
|
pub fn next(self: *Directory, entry: *Entry) bool {
|
|
const request = protocol.Request{ .operation = .readdir, .node = self.node, .offset = self.cursor, .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 = kindFromWire(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;
|
|
self.cursor += 1;
|
|
return true;
|
|
}
|
|
|
|
/// Release the VFS's open handle for this directory.
|
|
pub fn close(self: *Directory) void {
|
|
var f = File{ .node = self.node };
|
|
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 };
|
|
}
|
|
|
|
// A path-based request that returns only a status (mkdir, unlink).
|
|
fn pathOperation(operation: protocol.Operation, path: []const u8) bool {
|
|
const request = protocol.Request{ .operation = operation, .node = 0, .offset = 0, .len = @intCast(path.len), .flags = 0 };
|
|
const r = transact(request, path, &.{}) 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);
|
|
}
|
|
|
|
/// 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 be in the same directory (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 total = old_path.len + 1 + new_path.len;
|
|
if (total > protocol.maximum_payload) return false;
|
|
var payload: [protocol.maximum_payload]u8 = undefined;
|
|
@memcpy(payload[0..old_path.len], old_path);
|
|
payload[old_path.len] = 0;
|
|
@memcpy(payload[old_path.len + 1 ..][0..new_path.len], new_path);
|
|
const request = protocol.Request{ .operation = .rename, .node = 0, .offset = 0, .len = @intCast(total), .flags = 0 };
|
|
const r = transact(request, payload[0..total], &.{}) orelse return false;
|
|
return r.reply.status == 0;
|
|
}
|
|
|
|
/// Mount a filesystem backend (its server endpoint) at absolute path `target`;
|
|
/// the VFS then routes everything under `target` to that backend. This is the one
|
|
/// call that hands the VFS a capability (the backend endpoint). Returns true on
|
|
/// success.
|
|
pub fn mount(target: []const u8, backend: ipc.Handle) bool {
|
|
const h = vfs() orelse return false;
|
|
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 false;
|
|
if (result.len < protocol.reply_size) return false;
|
|
return std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]).status == 0;
|
|
}
|