220 lines
9.1 KiB
Zig
220 lines
9.1 KiB
Zig
//! POSIX-style file API for user programs — the low level under C stdio. Files
|
|
//! are named objects served by the user-space VFS server (system/services/vfs/vfs.zig); each
|
|
//! call marshals a request, IPC_Calls the VFS, and unmarshals the reply. The
|
|
//! kernel knows nothing of files or fds — the fd table lives here, per process.
|
|
|
|
const std = @import("std");
|
|
const protocol = @import("vfs-protocol");
|
|
const ipc = @import("runtime").ipc;
|
|
|
|
pub const O_CREAT = protocol.create;
|
|
pub const SEEK_SET: u32 = 0;
|
|
pub const SEEK_CURRENT: u32 = 1;
|
|
pub const SEEK_END: u32 = 2;
|
|
|
|
// Resolve (and cache) the VFS server endpoint, looked up by well-known id.
|
|
var vfs_handle: usize = 0;
|
|
var vfs_resolved = false;
|
|
fn vfs() ?usize {
|
|
if (!vfs_resolved) {
|
|
vfs_handle = ipc.lookup(.vfs) orelse return null;
|
|
vfs_resolved = true;
|
|
}
|
|
return vfs_handle;
|
|
}
|
|
|
|
const maximum_fds = 32;
|
|
const Fd = struct { used: bool = false, node: u64 = 0, offset: u64 = 0 };
|
|
var fds = [_]Fd{.{}} ** maximum_fds;
|
|
|
|
fn allocFd() ?usize {
|
|
for (&fds, 0..) |*f, i| {
|
|
if (!f.used) {
|
|
f.* = .{ .used = true };
|
|
return i;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
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 is written into `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] };
|
|
}
|
|
|
|
/// Open (or create, with O_CREAT) `path`; returns an fd or -1.
|
|
pub fn open(path: []const u8, flags: u32) i32 {
|
|
const fd = allocFd() orelse return -1;
|
|
const request = protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(path.len), .flags = flags };
|
|
const r = transact(request, path, &.{}) orelse {
|
|
fds[fd].used = false;
|
|
return -1;
|
|
};
|
|
if (r.reply.status != 0) {
|
|
fds[fd].used = false;
|
|
return -1;
|
|
}
|
|
fds[fd] = .{ .used = true, .node = r.reply.node, .offset = 0 };
|
|
return @intCast(fd);
|
|
}
|
|
|
|
fn fdPtr(fd: i32) ?*Fd {
|
|
if (fd < 0 or fd >= maximum_fds) return null;
|
|
const f = &fds[@intCast(fd)];
|
|
return if (f.used) f else null;
|
|
}
|
|
|
|
/// Read up to `buffer.len` bytes at the current offset; returns the count or -1.
|
|
pub fn read(fd: i32, buffer: []u8) isize {
|
|
const f = fdPtr(fd) orelse return -1;
|
|
const want: u32 = @intCast(@min(buffer.len, protocol.maximum_payload));
|
|
const request = protocol.Request{ .operation = .read, .node = f.node, .offset = f.offset, .len = want, .flags = 0 };
|
|
const r = transact(request, &.{}, buffer) orelse return -1;
|
|
if (r.reply.status != 0) return -1;
|
|
f.offset += r.reply.len;
|
|
return @intCast(r.reply.len);
|
|
}
|
|
|
|
/// Write `data` at the current offset; returns the count or -1.
|
|
pub fn write(fd: i32, data: []const u8) isize {
|
|
const f = fdPtr(fd) orelse return -1;
|
|
const want: u32 = @intCast(@min(data.len, protocol.maximum_payload));
|
|
const request = protocol.Request{ .operation = .write, .node = f.node, .offset = f.offset, .len = want, .flags = 0 };
|
|
const r = transact(request, data[0..want], &.{}) orelse return -1;
|
|
if (r.reply.status != 0) return -1;
|
|
f.offset += r.reply.len;
|
|
return @intCast(r.reply.len);
|
|
}
|
|
|
|
/// Write all of `data`, looping until it is fully written (a single `write` caps
|
|
/// each call at the VFS payload size). Returns the total written, or -1 if a
|
|
/// underlying write failed before any progress on that chunk.
|
|
pub fn writeAll(fd: i32, data: []const u8) isize {
|
|
var written: usize = 0;
|
|
while (written < data.len) {
|
|
const n = write(fd, data[written..]);
|
|
if (n <= 0) return if (written == 0) -1 else @intCast(written);
|
|
written += @intCast(n);
|
|
}
|
|
return @intCast(written);
|
|
}
|
|
|
|
/// Reposition the fd's offset. Returns the new offset or -1. (SEEK_END needs the
|
|
/// file size, which `stat` provides; handled by fetching it here.)
|
|
pub fn lseek(fd: i32, off: i64, whence: u32) i64 {
|
|
const f = fdPtr(fd) orelse return -1;
|
|
const base: i64 = switch (whence) {
|
|
SEEK_SET => 0,
|
|
SEEK_CURRENT => @intCast(f.offset),
|
|
SEEK_END => blk: {
|
|
const request = protocol.Request{ .operation = .status, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
|
|
var sbuf: [@sizeOf(protocol.FileStatus)]u8 = undefined;
|
|
const r = transact(request, &.{}, &sbuf) orelse return -1;
|
|
if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.FileStatus)) return -1;
|
|
const st = std.mem.bytesToValue(protocol.FileStatus, sbuf[0..@sizeOf(protocol.FileStatus)]);
|
|
break :blk @intCast(st.size);
|
|
},
|
|
else => return -1,
|
|
};
|
|
const pos = base + off;
|
|
if (pos < 0) return -1;
|
|
f.offset = @intCast(pos);
|
|
return pos;
|
|
}
|
|
|
|
/// Stat `path`. Returns 0 or -1.
|
|
pub fn stat(path: []const u8, out: *protocol.FileStatus) i32 {
|
|
// Open, stat by node, close — simple and enough for now.
|
|
const fd = open(path, 0);
|
|
if (fd < 0) return -1;
|
|
defer close(fd);
|
|
const f = fdPtr(fd).?;
|
|
const request = protocol.Request{ .operation = .status, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
|
|
var sbuf: [@sizeOf(protocol.FileStatus)]u8 = undefined;
|
|
const r = transact(request, &.{}, &sbuf) orelse return -1;
|
|
if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.FileStatus)) return -1;
|
|
out.* = std.mem.bytesToValue(protocol.FileStatus, sbuf[0..@sizeOf(protocol.FileStatus)]);
|
|
return 0;
|
|
}
|
|
|
|
/// Close an fd (best effort — tells the VFS to release the open file).
|
|
pub fn close(fd: i32) void {
|
|
const f = fdPtr(fd) orelse return;
|
|
const request = protocol.Request{ .operation = .close, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
|
|
_ = 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);
|
|
}
|