From f1e79d0eeb052c9076f6775bc2eb8074cd01ad31 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Mon, 10 Aug 2026 00:17:22 +0100 Subject: [PATCH] =?UTF-8?q?volume-manager:=20the=20`volumes`=20query=20ver?= =?UTF-8?q?b=20=E2=80=94=20read=20a=20volume's=20id,=20path,=20and=20label?= =?UTF-8?q?=20(S2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/); 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). --- .../volume-manager-protocol.zig | 79 +++++++++++++++++++ .../volume-manager/volume-manager.zig | 19 ++++- 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/library/protocol/volume-manager/volume-manager-protocol.zig b/library/protocol/volume-manager/volume-manager-protocol.zig index 9fa9f21..75ecce2 100644 --- a/library/protocol/volume-manager/volume-manager-protocol.zig +++ b/library/protocol/volume-manager/volume-manager-protocol.zig @@ -12,6 +12,7 @@ //! "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; @@ -24,13 +25,91 @@ pub const Hello = extern struct { _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/ 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 +} diff --git a/system/services/volume-manager/volume-manager.zig b/system/services/volume-manager/volume-manager.zig index f80d6d9..9423e58 100644 --- a/system/services/volume-manager/volume-manager.zig +++ b/system/services/volume-manager/volume-manager.zig @@ -348,7 +348,24 @@ fn onHello(_: void, invocation: Invocation(volume_manager_protocol.Hello), _: An return 0; } -const handlers = Serve.Handlers{ .hello = onHello }; +/// Answer a `volumes` query with the mounted volume's descriptor — its id (its +/// mount path is /volumes/ unless overridden), its actual mount path, and +/// its display label. This is how a shell or file manager reads a volume's +/// friendly name: software keys on the id, a UI shows the label. An empty reply +/// means no volume is mounted. +fn onVolumes(_: void, _: Invocation(volume_manager_protocol.Volumes), answer: Answer(void)) isize { + const v = volume orelse return 0; + var id_buf: [volume_map.id_maximum]u8 = undefined; + const info = volume_manager_protocol.VolumeInfo{ + .id = volume_map.idString(v.identity, &id_buf), + .mount_path = v.mount_prefix, + .label = v.identity.labelSlice(), + }; + const encoded = info.encode(answer.tail()) orelse return 0; + return @intCast(encoded.len); +} + +const handlers = Serve.Handlers{ .hello = onHello, .volumes = onVolumes }; fn onMessage(message: []const u8, out: []u8, sender: u32, arrived: *ipc.Arrival) usize { // No verb takes a capability up, so the turn closes whatever arrives.