145 lines
7.1 KiB
Zig
145 lines
7.1 KiB
Zig
//! The VFS wire protocol — the message format spoken between a client (via the file
|
|
//! API) and the user-space VFS server over IPC. A request is a fixed `Request` header
|
|
//! followed by an inline payload (a path, or write bytes); a reply is a fixed `Reply`
|
|
//! header followed by an inline payload (read bytes, or a FileStatus). Everything fits
|
|
//! in one IPC message (<= ipc MESSAGE_MAXIMUM = 256 bytes).
|
|
//!
|
|
//! This is a danos-native contract, so it uses danos names throughout. The client
|
|
//! side is `runtime.fs` (library/runtime/fs.zig), which programs use directly.
|
|
//!
|
|
//! This is user-space only — the kernel knows nothing of files or paths; it only moves the bytes.
|
|
//! Shared by library/runtime/fs.zig (the client) and the mount backends that serve it (today
|
|
//! the fat server, system/services/fat/). The standalone user-space VFS server it was first
|
|
//! written against has retired — path routing moved into the kernel (system/kernel/vfs.zig,
|
|
//! fs_resolve) — but the protocol module outlived it.
|
|
|
|
//! **An `open` reply may carry a capability.** The vfs `open` request rides
|
|
//! `ipc_call`, and the reply direction of a call can hand back an endpoint
|
|
//! (`ipc.callCap`'s `Reply.cap`). A file backend never uses it — FAT answers
|
|
//! with a node id and nothing else — but a *synthetic* backend does: opening a
|
|
//! `NodeKind.protocol` node under `/protocol` returns the provider's endpoint,
|
|
//! which is the channel (docs/os-development/protocol-namespace.md). The
|
|
//! convention is per-backend, not per-operation: a client that did not ask a
|
|
//! synthetic backend simply gets no capability back, exactly as today.
|
|
|
|
pub const Operation = enum(u32) {
|
|
open, // open(path) -> node id (a synthetic backend may reply with a capability instead)
|
|
close, // close(node)
|
|
read, // read(node, offset, len) -> bytes
|
|
write, // write(node, offset, bytes) -> count
|
|
status, // status(node) -> FileStatus
|
|
// Appended for the mount router (M5). Values stay stable, so existing clients
|
|
// and the flat-ramfs tests are unaffected.
|
|
readdir, // readdir(dir_node, cursor=offset) -> one DirectoryEntry (len==0 => EOF)
|
|
mount, // mount(prefix payload, capability = backend endpoint)
|
|
unmount, // unmount(prefix payload)
|
|
// Appended for filesystem mutation (Phase 2). Path-based (the path is the
|
|
// payload); a mounted backend handles them, the flat ramfs refuses them.
|
|
mkdir, // mkdir(path payload) -> status
|
|
unlink, // unlink(path payload) -> status
|
|
// rename: the payload is the old path, a single 0x00 separator, then the new
|
|
// path. Same-directory rename only (the router requires both under one mount).
|
|
rename, // rename(old\0new payload) -> status
|
|
// Appended for the protocol namespace (P2). The registry is a synthetic
|
|
// backend mounted at /protocol: `open` establishes a channel and `readdir`
|
|
// lists the bound names like any directory, so those two verbs need nothing
|
|
// new — but *claiming* a name does. A file backend refuses it, alongside the
|
|
// router verbs it does not implement either; only the registry implements it.
|
|
bind, // bind(name payload, capability = the provider's endpoint) -> status
|
|
};
|
|
|
|
/// The type of a filesystem node, aligned to the node-kind table
|
|
/// (docs/file-system-development/file-system-hierarchy.md). Fills `FileStatus.kind` and
|
|
/// `DirectoryEntry.kind`; `regular = 0` keeps the historical hardcoded value.
|
|
pub const NodeKind = enum(u32) {
|
|
regular = 0,
|
|
directory = 1,
|
|
character_device = 2,
|
|
block_device = 3,
|
|
symbolic_link = 4,
|
|
fifo = 5,
|
|
socket = 6,
|
|
/// A node that names a *contract*, not a file: opening it establishes a
|
|
/// channel to whatever process currently provides that protocol, delivered
|
|
/// as an endpoint capability in the reply rather than a node id. This is
|
|
/// what lives under `/protocol`; `readdir` lists these like any other node,
|
|
/// so the tree stays browsable for diagnosis.
|
|
protocol = 7,
|
|
};
|
|
|
|
/// One directory entry, returned by `readdir`: a fixed header followed inline in
|
|
/// the reply payload by `name_len` bytes of name. A zero-length reply is EOF.
|
|
pub const DirectoryEntry = extern struct {
|
|
kind: u32, // a NodeKind
|
|
name_len: u32,
|
|
size: u64,
|
|
};
|
|
|
|
pub const directory_entry_size: usize = @sizeOf(DirectoryEntry);
|
|
|
|
/// Request header. `node` is the server-side open-file id (from a prior open);
|
|
/// for `open` the path is the payload and `len` is its length. `offset`/`len`
|
|
/// carry the read/write position and count.
|
|
pub const Request = extern struct {
|
|
operation: Operation,
|
|
node: u64,
|
|
offset: u64,
|
|
len: u32,
|
|
flags: u32,
|
|
};
|
|
|
|
/// Reply header. `status` is 0 on success or a negative errno; `node` is the new
|
|
/// open-file id (for `open`); `len` is the payload length (bytes read, or the
|
|
/// FileStatus size).
|
|
pub const Reply = extern struct {
|
|
status: i32,
|
|
_padding: u32 = 0,
|
|
node: u64 = 0,
|
|
len: u32 = 0,
|
|
_padding2: u32 = 0,
|
|
};
|
|
|
|
/// A file's metadata (the danos-native answer to a `status` request). The POSIX
|
|
/// layer maps this onto `struct stat`.
|
|
pub const FileStatus = extern struct {
|
|
size: u64,
|
|
kind: u32,
|
|
_padding: u32 = 0,
|
|
/// Modification time — Unix epoch seconds, UTC. 0 if the backend has none (the
|
|
/// flat ramfs). Filled from the FAT directory entry's write date/time.
|
|
mtime: u64 = 0,
|
|
};
|
|
|
|
pub const message_maximum: usize = 256;
|
|
pub const request_size: usize = @sizeOf(Request);
|
|
pub const reply_size: usize = @sizeOf(Reply);
|
|
/// Largest inline payload that still fits one IPC message alongside a header.
|
|
pub const maximum_payload: usize = message_maximum - request_size;
|
|
|
|
/// Open flags (danos-native; `runtime.fs.OpenOptions` maps its booleans onto these).
|
|
pub const create: u32 = 1;
|
|
/// Open a directory (for readdir) rather than a file. A mounted backend uses
|
|
/// this to open a directory node; the flat ramfs ignores it.
|
|
pub const directory: u32 = 2;
|
|
/// Truncate the file to zero length on open (O_TRUNC): replace its contents rather
|
|
/// than overwriting in place, so a shorter new file leaves no stale tail. A mounted
|
|
/// backend frees the old cluster chain; the flat ramfs ignores it.
|
|
pub const truncate: u32 = 4;
|
|
|
|
test "protocol struct sizes and node kinds" {
|
|
const std = @import("std");
|
|
try std.testing.expectEqual(@as(u32, 0), @intFromEnum(NodeKind.regular));
|
|
try std.testing.expectEqual(@as(u32, 1), @intFromEnum(NodeKind.directory));
|
|
// Appended with the protocol namespace; every earlier value keeps its own.
|
|
try std.testing.expectEqual(@as(u32, 6), @intFromEnum(NodeKind.socket));
|
|
try std.testing.expectEqual(@as(u32, 7), @intFromEnum(NodeKind.protocol));
|
|
try std.testing.expectEqual(@as(usize, 16), @sizeOf(DirectoryEntry));
|
|
// The appended operations keep the original values.
|
|
try std.testing.expectEqual(@as(u32, 0), @intFromEnum(Operation.open));
|
|
try std.testing.expectEqual(@as(u32, 4), @intFromEnum(Operation.status));
|
|
try std.testing.expectEqual(@as(u32, 5), @intFromEnum(Operation.readdir));
|
|
try std.testing.expectEqual(@as(u32, 10), @intFromEnum(Operation.rename));
|
|
// The registry's claim verb, appended last with the protocol namespace.
|
|
try std.testing.expectEqual(@as(u32, 11), @intFromEnum(Operation.bind));
|
|
}
|