The storage layer gains its policy home (storage-architecture.md): a new system/services/volume-manager, spawned by init, that acquires the mass- storage block channel through the device manager (the same lineage a filesystem uses), reads block 0, and parses the first volume out of it. The partition-table walk that lived in the FAT engine moves here, above the driver where it belongs (partition.zig, host-tested: MBR entry, bare-FAT, no-signature). Identity is the MBR disk signature + partition index — the weak rung of the ladder; GPT GUID and FAT serial refine identityOf without changing shape. This increment is discovery + probe + log only, additive: the FAT service still acquires its own volume, so nothing changes for it. Confining each filesystem to its partition and spawning one per volume (the flip) lands next, keeping fat working throughout. Grants + wiring: init.csv spawns it after the device manager; protocol.csv grants bind volume-manager + open device-manager. Verified: volume-probe asserts the parse (bare-FAT volume at lba 0), neutral 10/10 across storage, restart, display, logging, confinement — the volume manager now runs in every boot and disturbs nothing.
93 lines
4.2 KiB
Zig
93 lines
4.2 KiB
Zig
//! 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
|
|
//! 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.
|
|
|
|
const std = @import("std");
|
|
|
|
/// 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.
|
|
pub const Volume = struct {
|
|
base_lba: u64,
|
|
block_count: u64,
|
|
identity: u64,
|
|
};
|
|
|
|
/// 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.
|
|
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
|
|
/// 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;
|
|
}
|
|
|
|
/// 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.
|
|
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;
|
|
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;
|
|
return .{ .base_lba = start, .block_count = size, .identity = identityOf(block0, index) };
|
|
}
|
|
// No partition entries: a bare FAT spanning the device.
|
|
return .{ .base_lba = 0, .block_count = device_blocks, .identity = identityOf(block0, 0) };
|
|
}
|
|
|
|
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 v = firstVolume(&block0, 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);
|
|
}
|
|
|
|
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).?;
|
|
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);
|
|
}
|