volume-manager: partition parser takes a SectorReader; identity is a tagged Identity (S1)
The identity ladder's flag-day — no behavior change. partition.firstVolume stops
taking one preloaded block-0 slice and takes a SectorReader (a read-one-sector
fn), so it can reach GPT metadata at LBA 1 and each partition's VBR on demand
(the next commits). The u64 identity becomes Identity{rung,key,label}: key is the
id (the mount path derives from it), label is display metadata (empty at rung 4).
Identity equality is id-only (rung+key) — the label never enters it. Only rung-4
(MBR sig+index / bare-FAT index 0) is produced, byte-identical to before; the
four host tests port to a RAM-disk reader, and fat-mount/volume-probe/
volume-removal stay green.
This commit is contained in:
@@ -1,54 +1,112 @@
|
||||
//! 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 block 0, decide what block sub-ranges are
|
||||
//! 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.
|
||||
//!
|
||||
//! Today: MBR (the four-entry table at offset 446) plus the bare-FAT case (a boot
|
||||
//! sector right at LBA 0). GPT is the next entry in the identity ladder and slots
|
||||
//! in here without touching anything above or below.
|
||||
//! 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 a content identity stable for the volume's life (the mount map keys on
|
||||
/// it; the boot volume is recorded by it). `identity` is derived from the medium,
|
||||
/// never from a port — a moved drive keeps it.
|
||||
/// and its content identity.
|
||||
pub const Volume = struct {
|
||||
base_lba: u64,
|
||||
block_count: u64,
|
||||
identity: 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
|
||||
/// simplest rung of the identity ladder; the fuller rungs (GPT partition GUID,
|
||||
/// FAT volume serial) refine `identityOf` without changing the shape.
|
||||
/// 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 identity of the volume at partition index `index`: the disk signature
|
||||
/// 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.
|
||||
fn identityOf(block0: []const u8, index: u8) u64 {
|
||||
return (@as(u64, diskSignature(block0)) << 8) | index;
|
||||
/// 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 block 0 looks like a partition table (the 0x55AA boot signature). A
|
||||
/// bare FAT also carries it, so the caller distinguishes by whether any partition
|
||||
/// entry is non-empty.
|
||||
/// 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 a device whose block 0 is `block0` and 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(block0: []const u8, device_blocks: u64) ?Volume {
|
||||
if (!hasBootSignature(block0)) return null;
|
||||
/// 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];
|
||||
@@ -63,12 +121,29 @@ pub fn firstVolume(block0: []const u8, device_blocks: u64) ?Volume {
|
||||
// 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 = identityOf(block0, index) };
|
||||
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 = identityOf(block0, 0) };
|
||||
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;
|
||||
@@ -78,24 +153,28 @@ test "an MBR with one partition yields its range and a distinct identity" {
|
||||
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 v = firstVolume(&block0, 200000).?;
|
||||
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((@as(u64, 0xDEADBEEF) << 8) | 0, v.identity);
|
||||
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 v = firstVolume(&block0, 65536).?;
|
||||
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;
|
||||
try std.testing.expect(firstVolume(&block0, 65536) == null);
|
||||
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" {
|
||||
@@ -110,7 +189,8 @@ test "a partition that runs past the device is skipped, not trusted" {
|
||||
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 v = firstVolume(&block0, 200000).?;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ const Volume = struct {
|
||||
storage_device_id: u64, // the device-manager id this volume's provider serves
|
||||
base_lba: u64,
|
||||
block_count: u64,
|
||||
identity: u64,
|
||||
identity: partition.Identity,
|
||||
id: u64,
|
||||
filesystem_pid: u32 = 0,
|
||||
};
|
||||
@@ -169,7 +169,7 @@ fn spawnFilesystem(v: *Volume) void {
|
||||
}
|
||||
v.filesystem_pid = pid;
|
||||
fs_spawn_ns = time.clock();
|
||||
std.log.info("volume 0x{x} -> {s} (pid {d}), lba {d}, {d} blocks", .{ v.identity, filesystem_binary, pid, v.base_lba, v.block_count });
|
||||
std.log.info("volume 0x{x} -> {s} (pid {d}), lba {d}, {d} blocks", .{ v.identity.key, filesystem_binary, pid, v.base_lba, v.block_count });
|
||||
}
|
||||
|
||||
/// Schedule a fat restart after backoff; the poll loop performs it once due.
|
||||
@@ -204,12 +204,19 @@ fn bringUpVolume() void {
|
||||
_ = ipc.close(device.endpoint);
|
||||
return;
|
||||
};
|
||||
if (!device.read(0, 1, bounce.physical)) {
|
||||
_ = ipc.close(device.endpoint);
|
||||
return;
|
||||
const ProbeReader = struct {
|
||||
device: block.Device,
|
||||
fn readSector(context: *anyopaque, lba: u64, buffer: *[partition.sector_bytes]u8) bool {
|
||||
const self: *@This() = @ptrCast(@alignCast(context));
|
||||
if (!self.device.read(lba, 1, bounce.physical)) return false;
|
||||
const src: [*]const u8 = @ptrFromInt(bounce.virtual);
|
||||
@memcpy(buffer, src[0..partition.sector_bytes]);
|
||||
return true;
|
||||
}
|
||||
const sector: [*]const u8 = @ptrFromInt(bounce.virtual);
|
||||
const found = partition.firstVolume(sector[0..512], geometry.block_count) orelse {
|
||||
};
|
||||
var probe = ProbeReader{ .device = device };
|
||||
const reader = partition.SectorReader{ .context = &probe, .readFn = ProbeReader.readSector };
|
||||
const found = partition.firstVolume(reader, geometry.block_count) orelse {
|
||||
if (!logged_no_volume) {
|
||||
_ = logging.write("volume-manager: storage present but no recognizable volume\n");
|
||||
logged_no_volume = true;
|
||||
|
||||
Reference in New Issue
Block a user