194 lines
9.4 KiB
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);
|
|
}
|