//! The VFS wire protocol — what a client (through the file API, //! library/kernel/file-system.zig) says to a filesystem backend over IPC. Defined //! through the envelope (docs/os-development/protocol-namespace.md), so every //! packet begins with the folded `Header`: the verb in `Header.operation`, and //! **the open node id in `Header.target`** — the field that used to be //! `Request.node`. A path appears in the conversation once, at `open`; every //! packet after it addresses that integer. //! //! This is a danos-native contract, so it uses danos names throughout. It is //! user-space only — the kernel knows nothing of files or paths; it only routes //! (`fs_resolve`) and moves the bytes. The backends that serve it today are the //! FAT server (system/services/fat/) and the protocol registry inside PID 1 //! (system/services/init/), which is a *synthetic* backend: `/protocol` holds //! contracts rather than files. //! //! **An `open` reply may carry a capability.** The `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 the registry does: opening a //! `NodeKind.protocol` node under `/protocol` returns the provider's endpoint, //! which is the channel. The convention is per-backend, not per-operation: a //! client that did not ask a synthetic backend simply gets no capability back. const envelope = @import("envelope"); /// 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: the fixed part of a `readdir` reply, followed inline by /// `name_len` bytes of name. **A zero `name_len` is end of directory** — the /// reply's own length cannot say so any more, because the envelope always sends /// the fixed part. pub const DirectoryEntry = extern struct { kind: u32 = 0, // a NodeKind name_len: u32 = 0, size: u64 = 0, }; pub const directory_entry_size: usize = @sizeOf(DirectoryEntry); /// 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, }; // --- the per-operation request and reply parts ------------------------------ // // Each names the bytes AFTER the prefix. Nothing here carries an operation or a // node id: those are the packet header's, folded in once. /// `open(flags)` with the path as the packet's tail. The one verb that spends a /// path; everything after it addresses the node id this returns. pub const Open = extern struct { flags: u32 = 0 }; /// The node id an `open` established — the integer every later packet puts in /// `Header.target`. Meaningful only between this client and this backend. pub const Opened = extern struct { node: u64 }; /// `read(offset, len)` on `Header.target`; the bytes come back as the reply tail. pub const Read = extern struct { offset: u64, len: u32, _padding: u32 = 0, }; /// `write(offset, len)` on `Header.target`, with the data as the packet's tail. pub const Write = extern struct { offset: u64, len: u32, _padding: u32 = 0, }; /// How many bytes a `write` actually took — it may be short. pub const Written = extern struct { count: u32 }; /// `readdir(cursor)` on `Header.target`: one entry per call, cursor-advanced. pub const Readdir = extern struct { cursor: u64 }; /// The contract, whole. Verbs number from `envelope.first_protocol_operation` /// (16) in this order; the reserved verbs below it mean what they mean /// everywhere. `readdir` stays a protocol verb rather than folding into the /// reserved `enumerate`: it enumerates the children of one *node*, where /// `enumerate` names a provider's targets. pub const Protocol = envelope.Define(.{ .name = "vfs", .version = 1, .operations = &.{ .{ .name = "open", .request = Open, .reply = Opened }, .{ .name = "close" }, .{ .name = "read", .request = Read }, .{ .name = "write", .request = Write, .reply = Written }, .{ .name = "status", .reply = FileStatus }, .{ .name = "readdir", .request = Readdir, .reply = DirectoryEntry }, // The mount router's two verbs. Path routing lives in the kernel now // (system/kernel/vfs.zig), so no backend implements either; they keep // their numbers so the vocabulary stays the one docs/vfs-protocol.md // describes. .{ .name = "mount" }, // tail = the prefix, capability = the backend's endpoint .{ .name = "unmount" }, // tail = the prefix // Filesystem mutation, path-based: the path is the packet's tail. .{ .name = "mkdir" }, .{ .name = "unlink" }, // rename: the tail is the old path, a single 0x00 separator, then the // new path. Same-directory rename only. .{ .name = "rename" }, // The registry's claim verb (P2): the name is the tail and the // provider's endpoint rides the call as its capability. A file backend // refuses it; only init implements it. .{ .name = "bind" }, }, }); pub const Operation = Protocol.Operation; /// What a backend sizes its buffers to — the call floor, as every protocol does. pub const message_maximum: usize = Protocol.message_maximum; /// The most inline payload any request may carry: the floor less the header and /// the widest fixed request part, so one bound serves every verb (a path, write /// data, a read's answer). pub const maximum_payload: usize = envelope.packet_maximum - Protocol.request_maximum; /// Open flags (danos-native; `file_system.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 "the stable wire values: node kinds, entry layout, and the verb numbering" { 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 numbering the envelope gives this protocol. These are NEW values: the // rebase moved every verb above the reserved range, so the old 0..11 are // gone and 16..27 are what the wire carries. Pinned because both sides of a // flag-day have to agree on them, not because they may never change again. try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Operation.open)); try std.testing.expectEqual(@as(u32, 17), @intFromEnum(Operation.close)); try std.testing.expectEqual(@as(u32, 18), @intFromEnum(Operation.read)); try std.testing.expectEqual(@as(u32, 19), @intFromEnum(Operation.write)); try std.testing.expectEqual(@as(u32, 20), @intFromEnum(Operation.status)); try std.testing.expectEqual(@as(u32, 21), @intFromEnum(Operation.readdir)); try std.testing.expectEqual(@as(u32, 26), @intFromEnum(Operation.rename)); try std.testing.expectEqual(@as(u32, 27), @intFromEnum(Operation.bind)); // The payload bound is what it always was, arrived at the other way round: // the header plus the widest fixed request part is 32 bytes of the floor. try std.testing.expectEqual(@as(usize, 224), maximum_payload); } test "the node id rides the header, and a path rides the tail" { const std = @import("std"); var buffer: [message_maximum]u8 = undefined; const opening = Protocol.encodeRequest(.open, 0, .{ .flags = create }, "/a/b", &buffer).?; try std.testing.expectEqual(@as(u32, create), Protocol.decodeRequest(.open, opening).?.flags); try std.testing.expectEqualStrings("/a/b", Protocol.requestTail(.open, opening)); try std.testing.expectEqual(@as(u64, 0), envelope.headerOf(opening).?.target); const reading = Protocol.encodeRequest(.read, 7, .{ .offset = 512, .len = 64 }, &.{}, &buffer).?; try std.testing.expectEqual(@as(u64, 7), envelope.headerOf(reading).?.target); try std.testing.expectEqual(@as(u64, 512), Protocol.decodeRequest(.read, reading).?.offset); }