danos/library/protocol/vfs/vfs-protocol.zig

194 lines
9.4 KiB
Zig

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