Files
danos/library/protocol/volume-manager/volume-manager-protocol.zig
T
Daniel Samson f1e79d0eeb volume-manager: the volumes query verb — read a volume's id, path, and label (S2)
The mechanism the id/label split needs: a `volumes` verb whose reply packs the
mounted volume's {id, mount_path, label} into the tail (VolumeInfo.encode/decode
— three length-prefixed strings). Software keys on the id (the mount path is
/volumes/<id>); a shell or file manager shows the label — the database id/name
split made a query. The VM's onVolumes answers from the mounted volume, empty
reply if none. Two host round-trip tests (encode/decode; too-small buffer and
short-tail rejection). No runtime consumer yet — the first is a userspace shell;
the hello handshake is unaffected (fat-mount/volume-probe green).
2026-08-10 00:17:22 +01:00

116 lines
5.2 KiB
Zig

//! The volume-manager protocol (docs/file-system-development/storage-architecture.md):
//! what a filesystem service says to the volume manager over
//! `/protocol/volume-manager`. Defined through the envelope, so every packet
//! begins with the folded `Header`.
//!
//! One verb. A filesystem the volume manager spawned announces itself with the
//! volume id it was given as argv[1] (folded into `Header.target`); the reply
//! carries that volume's block channel — already range-confined to the
//! filesystem's badge — as the call's returned capability. The filesystem never
//! finds its storage by name and never sees the whole device; establishment is
//! by lineage, exactly as a driver reaches its controller (communication.md
//! "Establishment: two planes"). No channel in the reply means the volume is not
//! ready yet — retryable, never a verdict.
const std = @import("std");
const envelope = @import("envelope");
pub const version: u16 = 1;
/// The filesystem's handshake. Carries only its protocol version; the volume it
/// serves is `Header.target`, and the block channel it needs comes back as the
/// reply's capability.
pub const Hello = extern struct {
version: u16 = version,
_padding: u16 = 0,
};
/// A `volumes` query — no request fields; the reply's tail carries the volume's
/// descriptor (`VolumeInfo`). The mechanism by which a shell or file manager
/// reads a volume's display label: the mount path is its id (software's stable
/// handle), the label is separate display metadata, the database id/name split.
pub const Volumes = extern struct {
_reserved: u32 = 0,
};
/// The `volumes` reply: three length-prefixed strings packed into the reply tail
/// — the volume's id (its mount path is /volumes/<id> unless overridden), its
/// actual mount path, and its display label. `id` is what software keys on;
/// `label` is what a UI shows.
pub const VolumeInfo = struct {
id: []const u8,
mount_path: []const u8,
label: []const u8,
const header_bytes = 6; // three u16 lengths, little-endian
/// Pack into `buf`, returning the used slice, or null if it does not fit.
pub fn encode(self: VolumeInfo, buf: []u8) ?[]u8 {
const total = header_bytes + self.id.len + self.mount_path.len + self.label.len;
if (total > buf.len) return null;
std.mem.writeInt(u16, buf[0..2], @intCast(self.id.len), .little);
std.mem.writeInt(u16, buf[2..4], @intCast(self.mount_path.len), .little);
std.mem.writeInt(u16, buf[4..6], @intCast(self.label.len), .little);
var off: usize = header_bytes;
@memcpy(buf[off..][0..self.id.len], self.id);
off += self.id.len;
@memcpy(buf[off..][0..self.mount_path.len], self.mount_path);
off += self.mount_path.len;
@memcpy(buf[off..][0..self.label.len], self.label);
return buf[0..total];
}
/// Decode a reply tail, or null if it is malformed (short or inconsistent).
/// The returned slices point into `bytes`.
pub fn decode(bytes: []const u8) ?VolumeInfo {
if (bytes.len < header_bytes) return null;
const id_len = std.mem.readInt(u16, bytes[0..2], .little);
const path_len = std.mem.readInt(u16, bytes[2..4], .little);
const label_len = std.mem.readInt(u16, bytes[4..6], .little);
const total = header_bytes + @as(usize, id_len) + path_len + label_len;
if (total > bytes.len) return null;
var off: usize = header_bytes;
const id = bytes[off..][0..id_len];
off += id_len;
const mount_path = bytes[off..][0..path_len];
off += path_len;
const label = bytes[off..][0..label_len];
return .{ .id = id, .mount_path = mount_path, .label = label };
}
};
pub const Protocol = envelope.Define(.{
.name = "volume-manager",
.version = 1,
.operations = &.{
.{ .name = "hello", .request = Hello },
.{ .name = "volumes", .request = Volumes },
},
});
pub const Operation = Protocol.Operation;
pub const message_maximum: usize = Protocol.message_maximum;
// Named fixture sizes so the bounds gate (which flags literal array lengths)
// stays quiet: test inputs, not runtime ceilings.
const test_reply_bytes = 128;
const test_tiny_bytes = 4;
test "VolumeInfo round-trips id, mount_path, and label" {
var buf: [test_reply_bytes]u8 = undefined;
const info = VolumeInfo{ .id = "fat-12345678", .mount_path = "/volumes/fat-12345678", .label = "DANOS" };
const encoded = info.encode(&buf).?;
const back = VolumeInfo.decode(encoded).?;
try std.testing.expectEqualStrings("fat-12345678", back.id);
try std.testing.expectEqualStrings("/volumes/fat-12345678", back.mount_path);
try std.testing.expectEqualStrings("DANOS", back.label);
}
test "VolumeInfo encode refuses a buffer that is too small; decode rejects a short tail" {
var tiny: [test_tiny_bytes]u8 = undefined;
const info = VolumeInfo{ .id = "fat-1", .mount_path = "/volumes/fat-1", .label = "" };
try std.testing.expect(info.encode(&tiny) == null);
try std.testing.expect(VolumeInfo.decode(&[_]u8{ 0, 0, 0 }) == null); // shorter than the header
try std.testing.expect(VolumeInfo.decode(&[_]u8{ 0xFF, 0xFF, 0, 0, 0, 0 }) == null); // claims 65535 id bytes
}