//! 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)); }