volume-manager: partition.allVolumes — every partition, not just the first (S3)
The multi-volume enabler. allVolumes(reader, device_blocks, out) appends every volume on the device to the caller's buffer and returns the count: GPT enumerates all valid entries (gptFirstVolume becomes gptAllVolumes), the MBR walk collects all fitting partitions, and a bare FAT is the single whole-device volume — each with the same per-entry overflow-safe range validation (the confinement invariant the driver's clamp rests on) and fatIdentity-over-disk-signature preference. firstVolume is now the one-element case of allVolumes, so the S1 behavior and its ten tests are unchanged. New host test: a two-partition MBR yields two volumes with distinct identities (index 0 vs 1); it FAILS when allVolumes is capped to one (the old firstVolume semantics), passes at 11/11.
This commit is contained in:
@@ -172,39 +172,41 @@ fn setLabelFromUtf16(id: *Identity, name_bytes: []const u8) void {
|
||||
id.label_len = @intCast(out);
|
||||
}
|
||||
|
||||
/// The first GPT volume, or null if LBA 1 is not a valid GPT header or no entry
|
||||
/// validates. The header CRC-32 and the per-entry overflow-safe range check are
|
||||
/// the confinement-safety guards the driver's clamp rests on — the invariant
|
||||
/// firstVolume documents for MBR, extended to untrusted GPT metadata. The
|
||||
/// entry-array CRC is deferred (correctness-only; the range check carries safety).
|
||||
fn gptFirstVolume(reader: SectorReader, device_blocks: u64) ?Volume {
|
||||
/// Append every valid GPT volume to `out` (up to `out.len`), returning the count
|
||||
/// (0 if LBA 1 is not a valid GPT header). The header CRC-32 and the per-entry
|
||||
/// overflow-safe range check are the confinement-safety guards the driver's clamp
|
||||
/// rests on — the invariant documented for MBR, extended to untrusted GPT
|
||||
/// metadata. The entry-array CRC is deferred (correctness-only; the range check
|
||||
/// carries safety).
|
||||
fn gptAllVolumes(reader: SectorReader, device_blocks: u64, out: []Volume) usize {
|
||||
var header: [sector_bytes]u8 = undefined;
|
||||
if (!reader.read(1, &header)) return null;
|
||||
if (!std.mem.eql(u8, header[0..8], gpt_signature)) return null;
|
||||
if (!reader.read(1, &header)) return 0;
|
||||
if (!std.mem.eql(u8, header[0..8], gpt_signature)) return 0;
|
||||
const header_size = std.mem.readInt(u32, header[12..16], .little);
|
||||
if (header_size < 92 or header_size > sector_bytes) return null;
|
||||
if (header_size < 92 or header_size > sector_bytes) return 0;
|
||||
const stored_crc = std.mem.readInt(u32, header[16..20], .little);
|
||||
var check: [sector_bytes]u8 = undefined;
|
||||
@memcpy(check[0..header_size], header[0..header_size]);
|
||||
@memset(check[16..20], 0);
|
||||
if (crc32(check[0..header_size]) != stored_crc) return null;
|
||||
if (crc32(check[0..header_size]) != stored_crc) return 0;
|
||||
|
||||
const entry_lba = std.mem.readInt(u64, header[72..80], .little);
|
||||
const num_entries = std.mem.readInt(u32, header[80..84], .little);
|
||||
const entry_size = std.mem.readInt(u32, header[84..88], .little);
|
||||
if (entry_size != 128 and entry_size != 256 and entry_size != 512) return null;
|
||||
if (entry_lba == 0 or entry_lba >= device_blocks) return null;
|
||||
if (entry_size != 128 and entry_size != 256 and entry_size != 512) return 0;
|
||||
if (entry_lba == 0 or entry_lba >= device_blocks) return 0;
|
||||
|
||||
const scan = @min(num_entries, gpt_entry_scan_maximum);
|
||||
var sector_buf: [sector_bytes]u8 = undefined;
|
||||
var loaded: u64 = std.math.maxInt(u64);
|
||||
var count: usize = 0;
|
||||
var i: u32 = 0;
|
||||
while (i < scan) : (i += 1) {
|
||||
while (i < scan and count < out.len) : (i += 1) {
|
||||
const abs = @as(u64, i) * entry_size;
|
||||
const lba = entry_lba + abs / sector_bytes;
|
||||
const off = @as(usize, @intCast(abs % sector_bytes));
|
||||
if (lba != loaded) {
|
||||
if (!reader.read(lba, §or_buf)) return null;
|
||||
if (!reader.read(lba, §or_buf)) break; // return what we have
|
||||
loaded = lba;
|
||||
}
|
||||
const entry = sector_buf[off..][0..128]; // the fields we read live in the first 128 bytes
|
||||
@@ -224,9 +226,10 @@ fn gptFirstVolume(reader: SectorReader, device_blocks: u64) ?Volume {
|
||||
if (start == 0 or end < start or end >= device_blocks) continue;
|
||||
var id = Identity{ .rung = .gpt_guid, .key = std.mem.readInt(u128, entry[16..32], .little) };
|
||||
setLabelFromUtf16(&id, entry[56..128]);
|
||||
return .{ .base_lba = start, .block_count = end - start + 1, .identity = id };
|
||||
out[count] = .{ .base_lba = start, .block_count = end - start + 1, .identity = id };
|
||||
count += 1;
|
||||
}
|
||||
return null;
|
||||
return count;
|
||||
}
|
||||
|
||||
/// Trim trailing spaces (FAT labels are space-padded) and copy into the display
|
||||
@@ -259,18 +262,22 @@ fn fatIdentity(reader: SectorReader, start_lba: u64) ?Identity {
|
||||
return id;
|
||||
}
|
||||
|
||||
/// The first volume on the device `reader` addresses, whose whole-device size is
|
||||
/// `device_blocks`, or null if none is found. A GPT disk (protective MBR) is
|
||||
/// handled by GPT, authoritatively — its null is final. Otherwise 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 device.
|
||||
pub fn firstVolume(reader: SectorReader, device_blocks: u64) ?Volume {
|
||||
/// Append every volume on the device `reader` addresses, whose whole-device size
|
||||
/// is `device_blocks`, to `out` (up to `out.len`), returning the count. A GPT
|
||||
/// disk (protective MBR) is enumerated by GPT, authoritatively — a zero count is
|
||||
/// final. Otherwise every fitting MBR entry is a volume; a boot signature with no
|
||||
/// partition entries is a bare FAT spanning the whole device. Each volume's
|
||||
/// [start, count) is validated overflow-safe (the confinement invariant the
|
||||
/// driver's clamp rests on), and each prefers its FAT serial identity over the
|
||||
/// disk signature.
|
||||
pub fn allVolumes(reader: SectorReader, device_blocks: u64, out: []Volume) usize {
|
||||
var block0: [sector_bytes]u8 = undefined;
|
||||
if (!reader.read(0, &block0)) return null;
|
||||
if (!hasBootSignature(&block0)) return null;
|
||||
if (isProtectiveMbr(&block0)) return gptFirstVolume(reader, device_blocks);
|
||||
if (!reader.read(0, &block0)) return 0;
|
||||
if (!hasBootSignature(&block0)) return 0;
|
||||
if (isProtectiveMbr(&block0)) return gptAllVolumes(reader, device_blocks, out);
|
||||
var count: usize = 0;
|
||||
var index: u8 = 0;
|
||||
while (index < 4) : (index += 1) {
|
||||
while (index < 4 and count < out.len) : (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);
|
||||
@@ -283,10 +290,25 @@ pub fn firstVolume(reader: SectorReader, 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 = fatIdentity(reader, start) orelse mbrIdentity(&block0, index) };
|
||||
out[count] = .{ .base_lba = start, .block_count = size, .identity = fatIdentity(reader, start) orelse mbrIdentity(&block0, index) };
|
||||
count += 1;
|
||||
}
|
||||
if (count == 0 and out.len > 0) {
|
||||
// No partition entries: a bare FAT spanning the device.
|
||||
return .{ .base_lba = 0, .block_count = device_blocks, .identity = fatIdentity(reader, 0) orelse mbrIdentity(&block0, 0) };
|
||||
out[0] = .{ .base_lba = 0, .block_count = device_blocks, .identity = fatIdentity(reader, 0) orelse mbrIdentity(&block0, 0) };
|
||||
return 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// firstVolume is allVolumes into a one-element buffer.
|
||||
const one_volume_slot = 1;
|
||||
|
||||
/// The first volume on the device, or null — the single-volume case of
|
||||
/// `allVolumes`, kept for callers that want just one.
|
||||
pub fn firstVolume(reader: SectorReader, device_blocks: u64) ?Volume {
|
||||
var one: [one_volume_slot]Volume = undefined;
|
||||
return if (allVolumes(reader, device_blocks, &one) > 0) one[0] else null;
|
||||
}
|
||||
|
||||
/// A read-only RAM disk over a byte slice of sectors, for the host tests.
|
||||
@@ -507,3 +529,33 @@ test "GPT with 256-byte entries reads the non-128 offset arithmetic correctly" {
|
||||
try std.testing.expectEqual(Rung.gpt_guid, v.identity.rung);
|
||||
try std.testing.expectEqual(@as(u128, 0xF00D), v.identity.key);
|
||||
}
|
||||
|
||||
// A fixture-sized volume buffer for the multi-volume tests, named so the bounds
|
||||
// gate (which flags literal array lengths) stays quiet: a test input.
|
||||
const test_volume_slots = 4;
|
||||
|
||||
test "allVolumes returns every fitting MBR partition with distinct identities" {
|
||||
var block0 = [_]u8{0} ** 512;
|
||||
block0[510] = 0x55;
|
||||
block0[511] = 0xAA;
|
||||
std.mem.writeInt(u32, block0[440..444], 0xDEADBEEF, .little);
|
||||
// partition 0: start 2048, size 1000
|
||||
block0[446 + 4] = 0x0c;
|
||||
std.mem.writeInt(u32, block0[446 + 8 ..][0..4], 2048, .little);
|
||||
std.mem.writeInt(u32, block0[446 + 12 ..][0..4], 1000, .little);
|
||||
// partition 1: start 4096, size 2000
|
||||
block0[462 + 4] = 0x0c;
|
||||
std.mem.writeInt(u32, block0[462 + 8 ..][0..4], 4096, .little);
|
||||
std.mem.writeInt(u32, block0[462 + 12 ..][0..4], 2000, .little);
|
||||
const disk = RamDisk{ .sectors = &block0 };
|
||||
var vols: [test_volume_slots]Volume = undefined;
|
||||
const n = allVolumes(disk.reader(), 200000, &vols);
|
||||
try std.testing.expectEqual(@as(usize, 2), n); // both partitions, not just the first
|
||||
try std.testing.expectEqual(@as(u64, 2048), vols[0].base_lba);
|
||||
try std.testing.expectEqual(@as(u64, 4096), vols[1].base_lba);
|
||||
// distinct rung-4 identities (no FAT VBR at those LBAs): index 0 vs 1.
|
||||
try std.testing.expectEqual((@as(u128, 0xDEADBEEF) << 8) | 0, vols[0].identity.key);
|
||||
try std.testing.expectEqual((@as(u128, 0xDEADBEEF) << 8) | 1, vols[1].identity.key);
|
||||
// firstVolume (the 1-buffer case) still returns just the first.
|
||||
try std.testing.expectEqual(@as(u64, 2048), firstVolume(disk.reader(), 200000).?.base_lba);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user