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