//! Partition-table parsing, the policy the storage architecture places above the //! block driver and below the filesystem (docs/file-system-development/ //! storage-architecture.md): read the medium, decide what block sub-ranges are //! volumes, and read each volume's content identity. The block DRIVER never does //! this — it clamps ranges it is told about; this is what tells it the numbers. //! //! Reads happen through a `SectorReader` (not one preloaded block-0 slice) so the //! parser can reach GPT metadata at LBA 1, the entry array beyond it, and each //! partition's VBR on demand. The identity it returns is a tagged `Identity`: the //! `key` is the id (the mount path is derived from it — a stable, unique, //! content-derived handle), and `label` is display metadata (the FAT volume label //! or the GPT partition name), never part of the id. Today's rung is MBR/bare-FAT; //! GPT (rung 1) and the FAT serial (rung 3) slot in without changing the shape. const std = @import("std"); /// A single 512-byte sector's worth of bytes. The parser assumes 512-byte /// logical sectors (4Kn media is a separate concern, noted in the plan). pub const sector_bytes = 512; /// The longest display label the parser records: a GPT partition name is 36 /// UTF-16 units; a FAT volume label is 11 bytes; 36 ASCII bytes covers both. pub const label_maximum = 36; /// Which rung of the identity ladder produced this identity. The rung tags the /// `key` namespace so a FAT serial and an MBR signature that happen to share bits /// stay distinct, and it drives how the mount path is rendered from the id. pub const Rung = enum(u8) { gpt_guid = 1, filesystem_uuid = 2, // reserved: no non-FAT engine reads a superblock UUID yet fat_serial = 3, mbr_index = 4, anonymous = 5, }; /// A volume's content identity. `key` is the ID — the stable, unique handle the /// mount path is derived from and the mount map keys on. `label` is DISPLAY /// metadata (FAT volume label / GPT partition name), exposed to a UI but never /// part of the path; two volumes with the same label but different keys are /// different volumes. Derived from the medium, never from a port. pub const Identity = struct { rung: Rung, key: u128 = 0, label: [label_maximum]u8 = [_]u8{0} ** label_maximum, label_len: u8 = 0, pub fn labelSlice(self: *const Identity) []const u8 { return self.label[0..self.label_len]; } /// Identity equality is the ID (rung + key) only — the label is display /// metadata and does not enter it. Same rung + same key means the same /// volume (the dd-cloned-media case the duplicate policy is for). pub fn eql(a: Identity, b: Identity) bool { return a.rung == b.rung and a.key == b.key; } }; /// One volume the parser found on the device: the block sub-range it occupies /// and its content identity. pub const Volume = struct { base_lba: u64, block_count: u64, identity: Identity, }; /// Read sectors on demand. `context` + `readFn` mirror the FAT engine's /// `BlockDevice` vtable; `readFn` returns false past the end of the device or on /// an I/O error, which the parser treats as "no volume". pub const SectorReader = struct { context: *anyopaque, readFn: *const fn (context: *anyopaque, lba: u64, buffer: *[sector_bytes]u8) bool, pub fn read(self: SectorReader, lba: u64, buffer: *[sector_bytes]u8) bool { return self.readFn(self.context, lba, buffer); } }; /// The MBR disk signature (offset 440, 4 bytes LE) — a 32-bit id written at /// partition time. Weak (dd-cloned disks share it) but on the medium, and the /// last rung of the identity ladder; the fuller rungs (GPT GUID, FAT serial) /// take precedence when present. fn diskSignature(block0: []const u8) u32 { if (block0.len < 444) return 0; return std.mem.readInt(u32, block0[440..444], .little); } /// The rung-4 identity of the volume at partition `index`: the disk signature /// paired with the index, so two partitions of one disk stay distinct. For a /// bare FAT (no table) the index is 0. Carries no label. fn mbrIdentity(block0: []const u8, index: u8) Identity { return .{ .rung = .mbr_index, .key = (@as(u128, diskSignature(block0)) << 8) | index }; } /// Whether a block looks like a boot sector / partition table (the 0x55AA boot /// signature). A bare FAT also carries it, so the caller distinguishes by whether /// any partition entry is non-empty. fn hasBootSignature(block0: []const u8) bool { return block0.len >= 512 and block0[510] == 0x55 and block0[511] == 0xAA; } /// The first volume on the device `reader` addresses, whose whole-device size is /// `device_blocks`, or null if none is found. An MBR with a non-empty entry /// yields that partition's [start, size); otherwise a boot signature with no /// partitions is treated as a bare FAT spanning the whole device. pub fn firstVolume(reader: SectorReader, device_blocks: u64) ?Volume { var block0: [sector_bytes]u8 = undefined; if (!reader.read(0, &block0)) return null; if (!hasBootSignature(&block0)) return null; var index: u8 = 0; while (index < 4) : (index += 1) { const entry = block0[446 + @as(usize, index) * 16 ..][0..16]; const kind = entry[4]; const start = std.mem.readInt(u32, entry[8..12], .little); const size = std.mem.readInt(u32, entry[12..16], .little); if (kind == 0 or start == 0 or size == 0) continue; // These bytes come off an untrusted removable medium. A partition that // does not fit inside the device is not a partition — skip it. This is // where the driver's confinement-safety invariant is established: the // clamp's overflow-safety rests on base + count staying inside the // device (usb-storage.zig resolveTransfer), which only holds because the // range handed down is validated here. The subtraction cannot overflow. if (start > device_blocks or device_blocks - start < size) continue; return .{ .base_lba = start, .block_count = size, .identity = mbrIdentity(&block0, index) }; } // No partition entries: a bare FAT spanning the device. return .{ .base_lba = 0, .block_count = device_blocks, .identity = mbrIdentity(&block0, 0) }; } /// A read-only RAM disk over a byte slice of sectors, for the host tests. const RamDisk = struct { sectors: []const u8, fn readFn(context: *anyopaque, lba: u64, buffer: *[sector_bytes]u8) bool { const self: *const RamDisk = @ptrCast(@alignCast(context)); const off = lba * sector_bytes; if (off + sector_bytes > self.sectors.len) return false; @memcpy(buffer, self.sectors[off..][0..sector_bytes]); return true; } fn reader(self: *const RamDisk) SectorReader { return .{ .context = @constCast(self), .readFn = readFn }; } }; test "an MBR with one partition yields its range and a distinct identity" { var block0 = [_]u8{0} ** 512; block0[510] = 0x55; block0[511] = 0xAA; std.mem.writeInt(u32, block0[440..444], 0xDEADBEEF, .little); // partition 0: type 0x0c (FAT32 LBA), start 2048, size 100000 block0[446 + 4] = 0x0c; std.mem.writeInt(u32, block0[446 + 8 ..][0..4], 2048, .little); std.mem.writeInt(u32, block0[446 + 12 ..][0..4], 100000, .little); const disk = RamDisk{ .sectors = &block0 }; const v = firstVolume(disk.reader(), 200000).?; try std.testing.expectEqual(@as(u64, 2048), v.base_lba); try std.testing.expectEqual(@as(u64, 100000), v.block_count); try std.testing.expectEqual(Rung.mbr_index, v.identity.rung); try std.testing.expectEqual((@as(u128, 0xDEADBEEF) << 8) | 0, v.identity.key); } test "a boot signature with no partitions is a bare FAT over the whole device" { var block0 = [_]u8{0} ** 512; block0[510] = 0x55; block0[511] = 0xAA; const disk = RamDisk{ .sectors = &block0 }; const v = firstVolume(disk.reader(), 65536).?; try std.testing.expectEqual(@as(u64, 0), v.base_lba); try std.testing.expectEqual(@as(u64, 65536), v.block_count); } test "no boot signature is no volume" { const block0 = [_]u8{0} ** 512; const disk = RamDisk{ .sectors = &block0 }; try std.testing.expect(firstVolume(disk.reader(), 65536) == null); } test "a partition that runs past the device is skipped, not trusted" { var block0 = [_]u8{0} ** 512; block0[510] = 0x55; block0[511] = 0xAA; // partition 0: start 0xFFFFFF00, size 0x400 — far past a 200000-block device. block0[446 + 4] = 0x0c; std.mem.writeInt(u32, block0[446 + 8 ..][0..4], 0xFFFFFF00, .little); std.mem.writeInt(u32, block0[446 + 12 ..][0..4], 0x400, .little); // partition 1: start 2048, size 1000 — fits. block0[462 + 4] = 0x0c; std.mem.writeInt(u32, block0[462 + 8 ..][0..4], 2048, .little); std.mem.writeInt(u32, block0[462 + 12 ..][0..4], 1000, .little); const disk = RamDisk{ .sectors = &block0 }; const v = firstVolume(disk.reader(), 200000).?; try std.testing.expectEqual(@as(u64, 2048), v.base_lba); // the fitting one, not the overflowing one try std.testing.expectEqual(@as(u64, 1000), v.block_count); }