diff --git a/Cargo.lock b/Cargo.lock index 802affd915..600ac99693 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1290,6 +1290,9 @@ version = "0.1.0" [[package]] name = "toyos-gpt" version = "0.1.0" +dependencies = [ + "gpt", +] [[package]] name = "toyos-hda" diff --git a/bootloader/src/rootimage.rs b/bootloader/src/rootimage.rs index d5d0be69ff..3609e088ba 100644 --- a/bootloader/src/rootimage.rs +++ b/bootloader/src/rootimage.rs @@ -21,7 +21,7 @@ use alloc::alloc::Layout; use alloc::string::String; use core::num::NonZeroU64; -use toyos_gpt::{Guid, Partition, Sectors}; +use toyos_gpt::{Guid, Located, Sectors}; use toyos_rootimage::chunk; use toyos_update::slots::{self, Table}; use uefi::prelude::*; @@ -178,29 +178,31 @@ impl<'a> Disk<'a> { /// The partition `guid` names on this disk, checked against the table as /// `toyos_gpt::locate` checks every one. - pub fn locate(&mut self, guid: [u8; 16]) -> Result { + pub fn locate(&mut self, guid: [u8; 16]) -> Result { toyos_gpt::locate(self, Guid(guid)) - .map(|located| located.partition) .map_err(|e| alloc::format!("partition {} on the boot disk: {e:?}", Guid(guid))) } /// The slot table on this disk's one TOYOS-SLOTS partition. pub fn slot_table(&mut self) -> Result { - let blank = Partition { index: 0, type_guid: Guid::ZERO, unique_guid: Guid::ZERO, first_lba: 0, last_lba: 0 }; - let mut found = [blank; 2]; + let mut found = [None; 2]; let scan = toyos_gpt::locate_type(self, Guid::TOYOS_SLOTS, &mut found) .map_err(|e| alloc::format!("the boot disk's partition table: {e:?}"))?; - if scan.matched != 1 { - return Err(alloc::format!("the boot disk carries {} slot tables, and a machine has one", scan.matched)); - } - let part = self.locate(found[0].unique_guid.0)?; + let listed = match (scan.matched, found[0]) { + (1, Some(Ok(listed))) => listed, + (1, Some(Err(unplaced))) => { + return Err(alloc::format!("the boot disk's slot table entry is no partition on it: {unplaced:?}")); + } + (n, _) => return Err(alloc::format!("the boot disk carries {n} slot tables, and a machine has one")), + }; + let part = self.locate(listed.unique_guid().0)?.partition(); let lbas = BLOCK as u64 / u64::from(self.lba_bytes); - if part.lba_count() < slots::COPIES * lbas { + if part.lba_count().get() < slots::COPIES * lbas { return Err(alloc::format!("the slot table's partition is {} blocks, short of its two copies", part.lba_count())); } let mut copies = [[0u8; BLOCK]; 2]; for (i, copy) in copies.iter_mut().enumerate() { - let at = part.first_lba + i as u64 * lbas; + let at = part.first_lba() + i as u64 * lbas; self.io .read_blocks(self.media_id, at, self.scratch) .map_err(|e| alloc::format!("the slot table's copy {i} would not read: {:?}", e.status()))?; @@ -220,8 +222,9 @@ impl<'a> Disk<'a> { /// resets the machine, if the firmware honours it. What shows where it /// stopped is the slot's line before the read and the attempt count the /// next pass reads. - pub fn read_root(&mut self, bs: &BootServices, part: &Partition, len: u64) -> Result { - let capacity = part.lba_count().saturating_mul(u64::from(self.lba_bytes)); + pub fn read_root(&mut self, bs: &BootServices, part: &Located, len: u64) -> Result { + let part = part.partition(); + let capacity = part.lba_count().get().saturating_mul(u64::from(self.lba_bytes)); if len > capacity || !len.is_multiple_of(BLOCK as u64) { return Err(alloc::format!("{len} bytes of ROOT do not fit whole in its {capacity}-byte partition")); } @@ -241,8 +244,8 @@ impl<'a> Disk<'a> { let chunk = chunk::chunk_bytes(CHUNK_BOUND, BLOCK, self.lba_bytes, granularity.unwrap_or(0)); let began = crate::tsc(); let mut device = Firmware { io: &self.io, media_id: self.media_id }; - let read = chunk::read(&mut device, part.first_lba, self.lba_bytes, chunk, into); - let image = RootImage { at, len, partition: part.unique_guid.0, cycles: crate::tsc().wrapping_sub(began) }; + let read = chunk::read(&mut device, part.first_lba(), self.lba_bytes, chunk, into); + let image = RootImage { at, len, partition: part.unique_guid().0, cycles: crate::tsc().wrapping_sub(began) }; if let Err(failed) = read { let why = alloc::format!( "the read of {} blocks at LBA {} failed: {:?}, after {} of {len} bytes read", @@ -256,7 +259,7 @@ impl<'a> Disk<'a> { } println!( "{READ_AT} {at:#x}+{len:#x} from LBA {}+{}, {chunk} bytes a request (optimal granularity: {}), in {} TSC cycles", - part.first_lba, + part.first_lba(), len / u64::from(self.lba_bytes), match granularity { Some(lbas) => alloc::format!("{lbas} block(s)"), diff --git a/kernel/src/gpt.rs b/kernel/src/gpt.rs index a83625d0b2..e54d473758 100644 --- a/kernel/src/gpt.rs +++ b/kernel/src/gpt.rs @@ -66,9 +66,8 @@ static DATA: Lock> = Lock::new(Vec::new()); /// partition claim is looked for on. Taken alone. static DISKS: Lock> = Lock::new(Vec::new()); -/// Every entry each disk's table stated when [`probe`] read it, for the -/// inventory: a table is outside every partition, so nothing a holder writes -/// changes it, and nothing here reads a disk again to answer. +/// A table is outside every partition, so nothing a holder writes changes it, +/// and nothing here reads a disk again to answer. static LISTED: Lock> = Lock::new(Vec::new()); /// One disk's entries, as [`probe`] listed them. @@ -83,9 +82,8 @@ struct Listed { /// when it is probed. const MAX_LISTED: usize = 128; -/// Every GPT entry on every disk [`probe`] read, and who holds exactly its -/// span now, from the block layer's holds. A partition that is not whole -/// blocks is held by nothing, since no view can be made of it. +/// A partition that is not whole blocks is held by nothing, since no view can +/// be made of it. pub fn inventory() -> Vec<(DeviceId, Partition, Option)> { let listed: Vec<(Handle, u32, Vec)> = LISTED .lock() @@ -95,7 +93,7 @@ pub fn inventory() -> Vec<(DeviceId, Partition, Option)> { let mut out = Vec::new(); for (handle, lba_bytes, parts) in listed { for part in parts { - let holder = match crate::block::span_blocks(part.first_lba, part.lba_count(), lba_bytes) { + let holder = match crate::block::span_blocks(part.first_lba(), part.lba_count().get(), lba_bytes) { Ok((first_block, blocks)) => handle.holder(first_block, first_block + blocks), Err(_) => None, }; @@ -108,19 +106,31 @@ pub fn inventory() -> Vec<(DeviceId, Partition, Option)> { /// List `handle`'s table into [`LISTED`], once per disk. fn list(sectors: &mut DeviceSectors<'_>, handle: &Handle, lba_bytes: u32) { let id = handle.device_id(); - let mut found = alloc::vec![BLANK; MAX_LISTED]; + let mut found = alloc::vec![None; MAX_LISTED]; // A disk with no table this kernel parses carries no partition, and // `collect` says so, naming the refusal. let Ok(scan) = toyos_gpt::list(sectors, &mut found) else { return }; - if scan.matched as usize > scan.listed { + if scan.matched as usize > MAX_LISTED { log!( - "gpt: device {id} carries {} partitions and the inventory lists {}", - scan.matched, - scan.listed + "gpt: device {id} carries {} partitions and the inventory lists {MAX_LISTED}", + scan.matched ); } - found.truncate(scan.listed); - LISTED.lock().push(Listed { handle: handle.clone(), lba_bytes, parts: found }); + let mut parts = Vec::new(); + for entry in found.into_iter().flatten() { + match entry { + Ok(part) => parts.push(part), + Err(unplaced) => log!( + "gpt: device {id} states entry {} ({}) at LBA {}..={}, whose blocks are no \ + partition on it, and the inventory does not list it", + unplaced.index, + unplaced.unique_guid, + unplaced.first, + unplaced.last + ), + } + } + LISTED.lock().push(Listed { handle: handle.clone(), lba_bytes, parts }); } /// How many partitions of one ToyOS type one device may offer this kernel. @@ -216,13 +226,13 @@ pub fn probe(handle: &Handle, lba_bytes: u32) { }; // Firmware's and the table's accounts must agree; a mismatch refuses, never repairs. - let part = found.partition; - if part.first_lba != firmware.start_lba || part.lba_count() != firmware.blocks { + let part = found.partition(); + if part.first_lba() != firmware.start_lba || part.lba_count().get() != firmware.blocks { log!( "gpt: device {id} puts {} at LBA {}+{} but firmware said {}+{} — not treating it as \ the boot volume", - part.unique_guid, - part.first_lba, + part.unique_guid(), + part.first_lba(), part.lba_count(), firmware.start_lba, firmware.blocks @@ -233,8 +243,8 @@ pub fn probe(handle: &Handle, lba_bytes: u32) { let volume = Volume { device: id, lba_bytes, - start_lba: part.first_lba, - blocks: part.lba_count(), + start_lba: part.first_lba(), + blocks: part.lba_count().get(), }; let mut resolved = RESOLVED.lock(); @@ -248,9 +258,9 @@ pub fn probe(handle: &Handle, lba_bytes: u32) { volume.start_lba, volume.blocks, lba_bytes, - part.index, - found.used_entries, - found.disk_guid, + part.index(), + found.used_entries(), + found.disk_guid(), if part.is_efi_system() { "" } else { " — and its type is not ESP" } ); *resolved = Resolution::Found { boot: volume, log }; @@ -283,7 +293,7 @@ fn collect( ty: Guid, into: &Lock>, ) { - let mut found = [BLANK; MAX_PER_DEVICE]; + let mut found = [None; MAX_PER_DEVICE]; let scan = match toyos_gpt::locate_type(sectors, ty, &mut found) { Ok(scan) => scan, Err(e) => { @@ -291,38 +301,38 @@ fn collect( return; } }; - if scan.matched as usize > scan.listed { + if scan.matched as usize > MAX_PER_DEVICE { log!( - "gpt: device {id} carries {} {what} partitions and this kernel looks at {}", - scan.matched, - scan.listed + "gpt: device {id} carries {} {what} partitions and this kernel looks at {MAX_PER_DEVICE}", + scan.matched ); } - for candidate in &found[..scan.listed] { - let checked = match toyos_gpt::locate(sectors, candidate.unique_guid) { - Ok(located) => located.partition, + // An entry whose blocks are no partition was logged once, by `list`. + for candidate in found.iter().flatten().flatten() { + let checked = match toyos_gpt::locate(sectors, candidate.unique_guid()) { + Ok(located) => located.partition(), Err(e) => { log!( "gpt: device {id} names a {what} {} its own table then refuses: {e:?}", - candidate.unique_guid + candidate.unique_guid() ); continue; } }; log!( "gpt: device {id} carries the {what} candidate {} at LBA {}+{}", - checked.unique_guid, - checked.first_lba, + checked.unique_guid(), + checked.first_lba(), checked.lba_count() ); into.lock().push(Candidate { volume: Volume { device: id, lba_bytes, - start_lba: checked.first_lba, - blocks: checked.lba_count(), + start_lba: checked.first_lba(), + blocks: checked.lba_count().get(), }, - guid: checked.unique_guid, + guid: checked.unique_guid(), }); } } @@ -410,7 +420,7 @@ pub fn seek(guid: PartGuid) -> Sought { for (handle, lba_bytes) in &disks { let id = handle.device_id(); let part = match toyos_gpt::locate(&mut DeviceSectors::new(handle, *lba_bytes), target) { - Ok(located) => located.partition, + Ok(located) => located.partition(), Err(e) => { match table_refused(id, target, e) { Ok(Unread::Lacks) => {} @@ -432,10 +442,10 @@ pub fn seek(guid: PartGuid) -> Sought { volume: Volume { device: id, lba_bytes: *lba_bytes, - start_lba: part.first_lba, - blocks: part.lba_count(), + start_lba: part.first_lba(), + blocks: part.lba_count().get(), }, - unique: part.unique_guid, + unique: part.unique_guid(), }); } Sought { found: Ok(found), silent } @@ -486,33 +496,24 @@ fn table_refused(id: DeviceId, target: Guid, e: GptError) -> Result, id: DeviceId, lba_bytes: u32) -> Option { let target = LOG_GUID.lock().expect("gpt::init runs before any device is probed"); match toyos_gpt::locate(sectors, target) { Ok(found) => { - let part = found.partition; + let part = found.partition(); log!( "gpt: device {id} carries the log partition {target} at LBA {}+{}, entry {} of {}", - part.first_lba, + part.first_lba(), part.lba_count(), - part.index, - found.used_entries + part.index(), + found.used_entries() ); Some(Volume { device: id, lba_bytes, - start_lba: part.first_lba, - blocks: part.lba_count(), + start_lba: part.first_lba(), + blocks: part.lba_count().get(), }) } Err(e) => { diff --git a/kernel/src/inventory.rs b/kernel/src/inventory.rs index 00d023f72f..c5796b6ed1 100644 --- a/kernel/src/inventory.rs +++ b/kernel/src/inventory.rs @@ -3,9 +3,8 @@ //! //! **Assembled from what each subsystem already keeps**: the PCI functions and //! their identities are `pcidev`'s from enumeration, the USB devices are the -//! ones the xHCI driver bound, the block devices are the ones registered, and -//! the partitions are what each disk's table stated when `gpt::probe` listed -//! it. A partition's state is the block layer's hold on exactly its span, the +//! ones the xHCI driver bound, and the block devices are the ones registered. +//! A partition's state is the block layer's hold on exactly its span, the //! record every view is refused against. //! //! **A holder is found where its handle is**: every process's table is walked @@ -33,11 +32,11 @@ pub fn collect() -> Vec { for (device, part, holder) in crate::gpt::inventory() { out.push(Record::Partition(Partition { device, - index: part.index, - type_guid: part.type_guid.0, - unique_guid: part.unique_guid.0, - first_lba: part.first_lba, - lbas: part.lba_count(), + index: part.index(), + type_guid: part.type_guid().0, + unique_guid: part.unique_guid().0, + first_lba: part.first_lba(), + lbas: part.lba_count().get(), state: match holder { None => PartState::Free, Some(crate::block::Holder::Kernel(_)) => PartState::Kernel, diff --git a/src/image.rs b/src/image.rs index e2471bc1e8..43d25814e9 100644 --- a/src/image.rs +++ b/src/image.rs @@ -340,19 +340,23 @@ pub fn slot_table_of(file: &mut std::fs::File) -> Result Result<(u64, u64), String> { - let mut out = [BLANK_PARTITION; 16]; + let mut out = [None; 16]; let scan = toyos_gpt::list(&mut FileSectors(file), &mut out) .map_err(|e| format!("no readable partition table: {e:?}"))?; - let found: Vec<&toyos_gpt::Partition> = - out[..scan.listed].iter().filter(|p| p.unique_guid == toyos_gpt::Guid(guid)).collect(); + let unique = |entry: &toyos_gpt::Entry| match entry { + Ok(p) => p.unique_guid(), + Err(unplaced) => unplaced.unique_guid, + }; + let found: Vec<&toyos_gpt::Entry> = + out.iter().flatten().filter(|entry| unique(entry) == toyos_gpt::Guid(guid)).collect(); match found[..] { - [p] if scan.listed == scan.matched as usize => { - Ok((p.first_lba * u64::from(LBA), p.lba_count() * u64::from(LBA))) + [Ok(p)] if scan.matched as usize <= out.len() => { + Ok((p.first_lba() * u64::from(LBA), p.lba_count().get() * u64::from(LBA))) + } + [Err(unplaced)] => { + Err(format!("partition {}: its blocks are no partition: {unplaced:?}", toyos_gpt::Guid(guid))) } _ => Err(format!( "partition {}: the table states it {} time(s) among {} entries", @@ -473,15 +477,45 @@ pub fn restage_table(path: &Path, edit: impl FnOnce(&mut toyos_update::slots::Ta /// The unique GUID of the one partition of type `kind` on the disk image /// `file`, as a GPT entry stores it. pub fn unique_guid_of(file: &mut std::fs::File, kind: toyos_gpt::Guid) -> Result<[u8; 16], String> { - let mut out = [BLANK_PARTITION; 2]; - let scan = toyos_gpt::locate_type(&mut FileSectors(file), kind, &mut out) - .map_err(|e| format!("no readable partition table: {e:?}"))?; - match scan.matched { - 1 => Ok(out[0].unique_guid.0), - n => Err(format!("{n} partitions of type {kind}, and this asks for one")), + only_partition(&mut FileSectors(file), kind).map(|part| part.unique_guid().0) +} + +/// Why a scan's `out[0]` and `matched` count did not pick out exactly one +/// partition, once the table itself was readable. +pub enum OnePartitionError { + /// The one entry of the wanted type is no partition on this disk. + Unplaced(toyos_gpt::Stated), + /// Not exactly one entry carried the wanted type. + Matched(u32), +} + +/// The one partition a [`toyos_gpt::locate_type`] scan found, out of its +/// [`toyos_gpt::TypeScan`] and the `out[0]` slot it filled — the match shared +/// by every caller that owes exactly one partition of a type and nothing else. +pub(crate) fn one_partition_of( + scan: toyos_gpt::TypeScan, + first: Option, +) -> Result { + match (scan.matched, first) { + (1, Some(Ok(part))) => Ok(part), + (1, Some(Err(unplaced))) => Err(OnePartitionError::Unplaced(unplaced)), + (matched, _) => Err(OnePartitionError::Matched(matched)), } } +/// The one partition of type `kind` on `disk`. +pub fn only_partition(disk: &mut dyn toyos_gpt::Sectors, kind: toyos_gpt::Guid) -> Result { + let mut out = [None; 2]; + let scan = toyos_gpt::locate_type(disk, kind, &mut out) + .map_err(|e| format!("no readable partition table: {e:?}"))?; + one_partition_of(scan, out[0]).map_err(|e| match e { + OnePartitionError::Unplaced(unplaced) => { + format!("the one entry of type {kind} is no partition: {unplaced:?}") + } + OnePartitionError::Matched(n) => format!("{n} partitions of type {kind}, where one is owed"), + }) +} + /// Overwrite the file `name` on the FAT partition `guid` of the disk image at /// `path` with `bytes`, exactly its length, **writing its data clusters and /// nothing else** — so a guest running on the image that does not write that @@ -520,14 +554,9 @@ pub fn overwrite_file_on(path: &Path, guid: [u8; 16], name: &str, bytes: &[u8]) /// The slot table on the disk image `file`, which copy is current, and where /// its partition starts. fn table_on(file: &mut std::fs::File) -> Result<(toyos_update::slots::Table, usize, u64), String> { - let mut out = [BLANK_PARTITION; 2]; - let scan = toyos_gpt::locate_type(&mut FileSectors(file), toyos_gpt::Guid::TOYOS_SLOTS, &mut out) - .map_err(|e| format!("no readable partition table: {e:?}"))?; - if scan.matched != 1 { - return Err(format!("{} slot tables, and a boot image has one", scan.matched)); - } + let part = only_partition(&mut FileSectors(file), toyos_gpt::Guid::TOYOS_SLOTS)?; let mut copies = [[0u8; toyos_update::slots::BLOCK]; 2]; - let at = out[0].first_lba * u64::from(LBA); + let at = part.first_lba() * u64::from(LBA); for (i, copy) in copies.iter_mut().enumerate() { file.seek(SeekFrom::Start(at + (i * toyos_update::slots::BLOCK) as u64)) .and_then(|_| file.read_exact(copy)) @@ -903,25 +932,11 @@ fn designation(blocks: u64) -> [u8; SECTOR] { pub fn data_partition_of(path: &Path) -> Result<(u64, u64), String> { let mut file = std::fs::File::open(path).map_err(|e| format!("open {}: {e}", path.display()))?; - let mut out = [BLANK_PARTITION; 2]; - let scan = - toyos_gpt::locate_type(&mut FileSectors(&mut file), toyos_gpt::Guid::TOYOS_DATA, &mut out) - .map_err(|e| format!("{} has no readable partition table: {e:?}", path.display()))?; - if scan.matched != 1 { - return Err(format!("{} carries {} TOYOS-DATA partitions", path.display(), scan.matched)); - } - Ok((out[0].first_lba * u64::from(LBA), out[0].lba_count() * u64::from(LBA))) + let part = only_partition(&mut FileSectors(&mut file), toyos_gpt::Guid::TOYOS_DATA) + .map_err(|why| format!("{}: {why}", path.display()))?; + Ok((part.first_lba() * u64::from(LBA), part.lba_count().get() * u64::from(LBA))) } -/// A slot [`toyos_gpt::locate_type`] has not filled in. -const BLANK_PARTITION: toyos_gpt::Partition = toyos_gpt::Partition { - index: 0, - type_guid: toyos_gpt::Guid::ZERO, - unique_guid: toyos_gpt::Guid::ZERO, - first_lba: 0, - last_lba: 0, -}; - /// A disk file as logical blocks, for a reader that may not hold the image. pub(crate) struct FileSectors<'a>(pub(crate) &'a mut std::fs::File); @@ -1122,8 +1137,8 @@ fn certify(disk: &[u8], parts: &[(&str, toyos_gpt::Guid, Volume)]) -> Result<(), for (what, guid, kind) in parts { let located = toyos_gpt::locate(&mut ImageSectors(disk), *guid) .map_err(|e| format!("toyos-gpt cannot find the {what} ({guid}) on this image: {e:?}"))?; - let at = located.partition.first_lba as usize * LBA as usize; - let bytes = located.partition.lba_count() as usize * LBA as usize; + let at = located.partition().first_lba() as usize * LBA as usize; + let bytes = located.partition().lba_count().get() as usize * LBA as usize; let volume = disk .get(at..at + bytes) .ok_or_else(|| format!("the {what} runs to byte {} of a {}-byte image", at + bytes, disk.len()))?; @@ -1232,11 +1247,7 @@ mod tests { /// The one partition of `kind` on `disk`. fn only(disk: &[u8], kind: toyos_gpt::Guid) -> toyos_gpt::Guid { - let mut out = [BLANK_PARTITION; 2]; - let scan = toyos_gpt::locate_type(&mut ImageSectors(disk), kind, &mut out) - .expect("the image has a partition table"); - assert_eq!(scan.matched, 1, "the image carries {} partitions of type {kind}", scan.matched); - out[0].unique_guid + only_partition(&mut ImageSectors(disk), kind).expect("the image carries one").unique_guid() } /// Publishing a flash target runs every reader over the assembled image, and @@ -1262,8 +1273,8 @@ mod tests { let start_of = |guid| { toyos_gpt::locate(&mut ImageSectors(&disk), guid) .expect("the partition is on the image") - .partition - .first_lba as usize + .partition() + .first_lba() as usize * LBA as usize }; @@ -1495,24 +1506,10 @@ mod tests { // Located by *type*, through the parser the kernel uses, at the offset // the table gives — never at the one the writer computed. - let mut out = [toyos_gpt::Partition { - index: 0, - type_guid: toyos_gpt::Guid::ZERO, - unique_guid: toyos_gpt::Guid::ZERO, - first_lba: 0, - last_lba: 0, - }; 4]; - let scan = - toyos_gpt::locate_type(&mut ImageSectors(&disk), toyos_gpt::Guid::TOYOS_ROOT, &mut out) - .expect("the image this build wrote has a partition table"); - assert_eq!( - (scan.matched, scan.listed), - (1, 1), - "a boot image carries exactly one TOYOS-ROOT partition; this one has {}", - scan.matched - ); - let at = out[0].first_lba as usize * LBA as usize; - let bytes = out[0].lba_count() as usize * LBA as usize; + let part = only_partition(&mut ImageSectors(&disk), toyos_gpt::Guid::TOYOS_ROOT) + .expect("a boot image carries exactly one TOYOS-ROOT partition"); + let at = part.first_lba() as usize * LBA as usize; + let bytes = part.lba_count().get() as usize * LBA as usize; let volume = disk[at..at + bytes].to_vec(); let fs = bcachefs::Mounted::<_, bcachefs::ReadOnly>::open(VecBlockIO::from_vec(volume)) @@ -1599,13 +1596,10 @@ mod tests { None, ); - let mut out = [BLANK_PARTITION; 2]; - let scan = - toyos_gpt::locate_type(&mut ImageSectors(&disk), toyos_gpt::Guid::TOYOS_ROOT, &mut out) - .expect("the image this build wrote has a partition table"); - assert_eq!(scan.matched, 1, "a boot image carries exactly one TOYOS-ROOT partition"); - let at = out[0].first_lba as usize * LBA as usize; - let bytes = out[0].lba_count() as usize * LBA as usize; + let part = only_partition(&mut ImageSectors(&disk), toyos_gpt::Guid::TOYOS_ROOT) + .expect("a boot image carries exactly one TOYOS-ROOT partition"); + let at = part.first_lba() as usize * LBA as usize; + let bytes = part.lba_count().get() as usize * LBA as usize; let fs = bcachefs::Mounted::<_, bcachefs::ReadOnly>::open(VecBlockIO::from_vec( disk[at..at + bytes].to_vec(), )) diff --git a/src/metal.rs b/src/metal.rs index b6c5e539df..dab449ddb9 100644 --- a/src/metal.rs +++ b/src/metal.rs @@ -954,24 +954,20 @@ fn one_partition( guid: toyos_gpt::Guid, what: &'static str, ) -> Result { - const BLANK: toyos_gpt::Partition = toyos_gpt::Partition { - index: 0, - type_guid: toyos_gpt::Guid::ZERO, - unique_guid: toyos_gpt::Guid::ZERO, - first_lba: 0, - last_lba: 0, - }; - let mut out = [BLANK; 2]; + let mut out = [None; 2]; let scan = toyos_gpt::locate_type(&mut crate::image::FileSectors(file), guid, &mut out) .map_err(|e| Refusal::Table(format!("{e:?}")))?; - if scan.matched != 1 { - return Err(Refusal::Partitions { what, matched: scan.matched }); - } + let part = crate::image::one_partition_of(scan, out[0]).map_err(|e| match e { + crate::image::OnePartitionError::Unplaced(unplaced) => { + Refusal::Table(format!("the {what} entry is no partition: {unplaced:?}")) + } + crate::image::OnePartitionError::Matched(matched) => Refusal::Partitions { what, matched }, + })?; Ok(Part { - index: out[0].index + 1, - start: out[0].first_lba, - sectors: out[0].lba_count(), - guid: out[0].unique_guid, + index: part.index() + 1, + start: part.first_lba(), + sectors: part.lba_count().get(), + guid: part.unique_guid(), }) } diff --git a/tests/common/inspect.rs b/tests/common/inspect.rs index 56ff8564ce..767c623acd 100644 --- a/tests/common/inspect.rs +++ b/tests/common/inspect.rs @@ -6,7 +6,7 @@ //! matches too much is a path in the answer this file did not name, and one //! that matches too little is a named path missing from it. Values are judged //! where the machine fixes them — QEMU's user network leases `10.0.2.15/24`, -//! nothing plays audio until `inspect_plays` does, and the NVMe disk this file +//! nothing plays audio until `inspect_plays` does, and the USB stick this file //! crafts has one partition free and one init grants — and read only for shape //! elsewhere. @@ -32,6 +32,8 @@ pub const BOUNDS: &str = "inventory_bounds"; const FREE: &str = "9D1E2F30-4A5B-4C6D-8E7F-0A1B2C3D4E5F"; /// The one init grants test-runner; mirrored in the config. const GRANTED: &str = "B4C5D6E7-F809-4A1B-8C2D-3E4F5A6B7C8D"; +/// An entry whose first block is after its last, which no inventory lists. +const BACKWARDS: &str = "0D5C4B3A-2918-4F7E-8D6C-5B4A39281706"; /// Every path the reader answers for netd on a virtio NIC with a lease. const NET: &[&str] = &[ @@ -57,16 +59,13 @@ pub fn boot(rust_bins: &[(String, Vec)]) -> Result { if bins.len() != 3 { return Err(format!("{DENIED}, {PLAYS} and {BOUNDS} were not all built")); } - let nvme = super::lane::dir().join("inspect-disk.img"); + let stick = super::lane::dir().join("inspect-stick.img"); let mib = 1024 * 1024; - super::partclaim::craft_plain_disk( - &nvme, - &[("free", mib, FREE), ("granted", mib, GRANTED)], - 96 * mib, - )?; + super::partclaim::craft_stick(&stick, 8 * mib, &[("free", mib, FREE), ("granted", mib, GRANTED)])?; + state_backwards(&stick)?; let config = Path::new(env!("CARGO_MANIFEST_DIR")).join(CONFIG); let options = - BootOptions { profile: qemu::Profile::Gop, nvme_image: Some(nvme), ..Default::default() }; + BootOptions { profile: qemu::Profile::GopUsbDisk, usb_images: vec![stick], ..Default::default() }; let argv = qemu::profile_argv(&options); if !argv.iter().any(|a| a.contains("virtio-net")) || !argv.iter().any(|a| a.contains("virtio-sound")) { return Err("this test needs a virtio NIC and a virtio sound card".to_string()); @@ -80,6 +79,24 @@ pub fn boot(rust_bins: &[(String, Vec)]) -> Result { Ok(qemu) } +/// Write [`BACKWARDS`] into the first free entry of the table of the disk at +/// `path`, both copies resealed. +fn state_backwards(path: &Path) -> Result<(), String> { + let guid = |text: &str| uuid::Uuid::parse_str(text).map(|u| u.to_bytes_le()).map_err(|e| format!("{text}: {e}")); + let (ty, unique) = (guid(super::partclaim::PLAIN_TYPE)?, guid(BACKWARDS)?); + let mut image = std::fs::read(path).map_err(|e| format!("read {}: {e}", path.display()))?; + let len = image.len(); + super::volumes::rewrite_gpt(&mut image, len, |entries, entry_bytes| { + let free = entries.chunks_mut(entry_bytes).find(|e| e[..16] == [0; 16]).ok_or("the table has no free entry")?; + free[..16].copy_from_slice(&ty); + free[16..32].copy_from_slice(&unique); + free[32..40].copy_from_slice(&500u64.to_le_bytes()); + free[40..48].copy_from_slice(&400u64.to_le_bytes()); + Ok(()) + })?; + std::fs::write(path, image).map_err(|e| format!("write {}: {e}", path.display())) +} + /// The `path = value` lines of a job's output, and nothing else the console /// carried while it ran. fn answer(result: &TestResult) -> BTreeMap { @@ -374,5 +391,12 @@ fn inventory(qemu: &mut QemuInstance) -> Result<(), String> { if holders(&got, granted) != ["test-runner"] { return Err(format!("`{line}`: {granted} is held by {:?}, not test-runner", holders(&got, granted))); } + if got.values().any(|v| *v == BACKWARDS.to_ascii_lowercase()) { + return Err(format!("`{line}` lists {BACKWARDS}, whose first block is after its last")); + } + let refused = format!("({BACKWARDS}) at LBA 500..=400, whose blocks are no partition on it"); + if !format!("{}{}", qemu.uart_log(), qemu.boot_log()).contains(&refused) { + return Err(format!("the kernel did not say it refused {BACKWARDS}")); + } Ok(()) } diff --git a/tests/common/partclaim.rs b/tests/common/partclaim.rs index e613a46a8f..71bbafcfeb 100644 --- a/tests/common/partclaim.rs +++ b/tests/common/partclaim.rs @@ -541,27 +541,15 @@ fn boot_stick_guids(image: &Path) -> Result<[String; 3], String> { // ROOT's type is read with the kernel's parser: the `gpt` crate answers the // all-zero GUID for a type its own table does not name. let bytes = std::fs::read(image).map_err(|e| format!("read the boot image: {e}"))?; - let blank = toyos_gpt::Partition { - index: 0, - type_guid: toyos_gpt::Guid::ZERO, - unique_guid: toyos_gpt::Guid::ZERO, - first_lba: 0, - last_lba: 0, - }; - let mut found = [blank; 2]; - let scan = toyos_gpt::locate_type( + let root = toyos_build::image::only_partition( &mut super::volumes::ImageSectors { bytes: &bytes }, toyos_gpt::Guid::TOYOS_ROOT, - &mut found, ) - .map_err(|e| format!("the boot image's table: {e:?}"))?; - if scan.matched != 1 { - return Err(format!("the boot image has {} of ROOT, expected one", scan.matched)); - } + .map_err(|why| format!("the boot image's ROOT: {why}"))?; Ok([ one(gpt::partition_types::EFI.guid, "ESP")?, one(gpt::partition_types::BASIC.guid, "log partition")?, - found[0].unique_guid.to_string(), + root.unique_guid().to_string(), ]) } @@ -772,7 +760,7 @@ pub(super) fn craft_plain_disk( /// A USB stick of `bytes` carrying `parts`, each a name, a length and its /// unique GUID; their spans. -fn craft_stick( +pub(super) fn craft_stick( path: &Path, bytes: u64, parts: &[(&'static str, u64, &'static str)], diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index 7bc5712fa1..c6f140a0a1 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -1111,6 +1111,9 @@ pub enum Profile { /// the 82574 nor any virtio function does. E1000eBesideIgb, Gop, + /// [`Profile::Gop`] with a second USB stick beside the boot stick, whose + /// table the test writes: the bus a stick of somebody else's arrives on. + GopUsbDisk, /// A virtio-gpu function and no VGA: the owner's own desktop, and the one /// machine where a mode change can succeed rather than answering /// `NotSupported` ahead of everything a resize does. @@ -1397,6 +1400,7 @@ impl Profile { | Self::E1000eNoServer | Self::E1000eBesideIgb | Self::Gop + | Self::GopUsbDisk | Self::VirtioGpu | Self::Metal | Self::MetalNoUsb @@ -1825,6 +1829,7 @@ impl Profile { hda: &[], iommu: Some(IOMMU_DEFAULT), }, + Self::GopUsbDisk => Shape { usb_disks: &[UsbDisk::DATA], ..Self::Gop.shape() }, Self::VirtioGpu => Shape { // No VGA at all: firmware then publishes no GOP, and the one // display the guest has is the one whose mode it can set. diff --git a/tests/common/volumes.rs b/tests/common/volumes.rs index c07eafeaa7..3273dc8560 100644 --- a/tests/common/volumes.rs +++ b/tests/common/volumes.rs @@ -2461,22 +2461,15 @@ pub fn log_partition_layout( (toyos_gpt::Guid::TOYOS_SLOTS, toyos_gpt::Guid::TOYOS_SLOTS_TEXT, "the slot table", slots), (toyos_gpt::Guid::TOYOS_BOOT, toyos_gpt::Guid::TOYOS_BOOT_TEXT, "slot A's volume", volume), ] { - let mut found = [toyos_gpt::Partition { - index: 0, - type_guid: toyos_gpt::Guid::ZERO, - unique_guid: toyos_gpt::Guid::ZERO, - first_lba: 0, - last_lba: 0, - }; 4]; - let scan = toyos_gpt::locate_type(&mut ImageSectors { bytes: &image }, kind, &mut found) - .map_err(|e| format!("the kernel's own GPT parser cannot read this table: {e:?}"))?; - if (scan.matched, scan.listed) != (1, 1) { - return Err(format!("the built image carries {} partitions typed {text}, wanted one", scan.matched)); - } - if found[0].first_lba != entry.first_lba || found[0].last_lba != entry.last_lba { + let found = toyos_build::image::only_partition(&mut ImageSectors { bytes: &image }, kind) + .map_err(|why| format!("the kernel's own GPT parser, on the partitions typed {text}: {why}"))?; + if found.first_lba() != entry.first_lba || found.last_lba() != entry.last_lba { return Err(format!( "the kernel's parser puts {what} at LBA {}..{} and the table says {}..{}", - found[0].first_lba, found[0].last_lba, entry.first_lba, entry.last_lba + found.first_lba(), + found.last_lba(), + entry.first_lba, + entry.last_lba )); } } @@ -2555,12 +2548,14 @@ pub fn log_partition_layout( // given the GUID exactly as the file carries it. let located = toyos_gpt::locate(&mut ImageSectors { bytes: &image }, toyos_gpt::Guid(named)) .map_err(|e| format!("the kernel's own GPT parser cannot find the log partition: {e:?}"))?; - if located.partition.first_lba != log.first_lba - || located.partition.last_lba != log.last_lba - { + let found = located.partition(); + if found.first_lba() != log.first_lba || found.last_lba() != log.last_lba { return Err(format!( "the kernel's parser puts the log partition at LBA {}..{} and the table says {}..{}", - located.partition.first_lba, located.partition.last_lba, log.first_lba, log.last_lba + found.first_lba(), + found.last_lba(), + log.first_lba, + log.last_lba )); } @@ -3092,21 +3087,9 @@ pub fn log_flush_retry( /// Where ROOT is on `image`, found by the parser the kernel uses. fn root_extent(image: &[u8]) -> Result<(usize, usize), String> { - let mut found = [toyos_gpt::Partition { - index: 0, - type_guid: toyos_gpt::Guid::ZERO, - unique_guid: toyos_gpt::Guid::ZERO, - first_lba: 0, - last_lba: 0, - }; 4]; - let scan = - toyos_gpt::locate_type(&mut ImageSectors { bytes: image }, toyos_gpt::Guid::TOYOS_ROOT, &mut found) - .map_err(|e| format!("this image has no readable partition table: {e:?}"))?; - if (scan.matched, scan.listed) != (1, 1) { - return Err(format!("this image carries {} ROOT partitions, wanted one", scan.matched)); - } - let at = found[0].first_lba as usize * 512; - Ok((at, found[0].lba_count() as usize * 512)) + let root = toyos_build::image::only_partition(&mut ImageSectors { bytes: image }, toyos_gpt::Guid::TOYOS_ROOT) + .map_err(|why| format!("this image's ROOT: {why}"))?; + Ok((root.first_lba() as usize * 512, root.lba_count().get() as usize * 512)) } /// A second USB disk carrying a copy of `image`, `mutate`d after the copy. @@ -3444,7 +3427,7 @@ fn entry_lba(entry: &[u8], at: usize) -> u64 { /// the backup array and header moved to the new end, the primary's pointers to /// them and its last usable LBA, both arrays' and both headers' CRCs, and the /// protective MBR's size. `edit` gets the array and one entry's length. -fn rewrite_gpt( +pub(super) fn rewrite_gpt( image: &mut Vec, len: usize, edit: impl FnOnce(&mut [u8], usize) -> Result<(), String>, diff --git a/tests/inspectcase/system.toml b/tests/inspectcase/system.toml index 1c12310260..075d5118e2 100644 --- a/tests/inspectcase/system.toml +++ b/tests/inspectcase/system.toml @@ -1,6 +1,6 @@ # The one boot that runs all four owners `inspect` reads: logd, the compositor, # soundd and netd, on a machine with a framebuffer, a virtio NIC and a virtio -# sound card (`Profile::Gop`), whose NVMe disk `tests/common/inspect.rs` crafts +# sound card (`Profile::GopUsbDisk`), whose USB stick `tests/common/inspect.rs` crafts # with one partition nobody holds and one init grants test-runner. # # test-runner holds no `launcher`, so every binary it runs is spawned directly diff --git a/toyos-gpt/Cargo.toml b/toyos-gpt/Cargo.toml index 342a3f8586..e2b9d2199d 100644 --- a/toyos-gpt/Cargo.toml +++ b/toyos-gpt/Cargo.toml @@ -9,3 +9,7 @@ name = "toyos-gpt" version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" + +[dev-dependencies] +# The differential oracle of `tests/oracle.rs`. +gpt = "3.1.0" diff --git a/toyos-gpt/src/crc32.rs b/toyos-gpt/src/crc32.rs index 1bbf20b21d..6cda25a924 100644 --- a/toyos-gpt/src/crc32.rs +++ b/toyos-gpt/src/crc32.rs @@ -6,22 +6,23 @@ //! every table on earth. The two implementations look identical apart from one //! constant, which is exactly why this file says so. +const POLY: u32 = 0xEDB8_8320; + +/// The CRC of each byte value, built by a `split_first_mut` walk so nothing is indexed. const TABLE: [u32; 256] = { let mut table = [0u32; 256]; - let mut i = 0u32; - while i < 256 { - let mut crc = i; - let mut j = 0; - while j < 8 { - if crc & 1 != 0 { - crc = (crc >> 1) ^ 0xEDB8_8320; - } else { - crc >>= 1; - } - j += 1; + let mut rest: &mut [u32] = &mut table; + let mut byte = 0u32; + while let Some((slot, tail)) = rest.split_first_mut() { + let mut crc = byte; + let mut bit = 0u32; + while bit < 8 { + crc = if crc & 1 == 0 { crc.wrapping_shr(1) } else { crc.wrapping_shr(1) ^ POLY }; + bit = bit.wrapping_add(1); } - table[i as usize] = crc; - i += 1; + *slot = crc; + rest = tail; + byte = byte.wrapping_add(1); } table }; @@ -39,8 +40,10 @@ impl Crc32 { pub fn update(&mut self, data: &[u8]) { for &byte in data { - let idx = ((self.0 ^ byte as u32) & 0xFF) as usize; - self.0 = (self.0 >> 8) ^ TABLE[idx]; + let [low, ..] = (self.0 ^ u32::from(byte)).to_le_bytes(); + // A `u8` is below 256: `get` is never `None`. + let entry = TABLE.get(usize::from(low)).copied().unwrap_or(0); + self.0 = self.0.wrapping_shr(8) ^ entry; } } diff --git a/toyos-gpt/src/guid.rs b/toyos-gpt/src/guid.rs index 7c1ec1bd36..0564ef80de 100644 --- a/toyos-gpt/src/guid.rs +++ b/toyos-gpt/src/guid.rs @@ -100,14 +100,7 @@ impl Guid { } pub const fn is_zero(&self) -> bool { - let mut i = 0; - while i < 16 { - if self.0[i] != 0 { - return false; - } - i += 1; - } - true + u128::from_ne_bytes(self.0) == 0 } } diff --git a/toyos-gpt/src/lib.rs b/toyos-gpt/src/lib.rs index 7e06472739..73e5862dd5 100644 --- a/toyos-gpt/src/lib.rs +++ b/toyos-gpt/src/lib.rs @@ -21,24 +21,26 @@ #![no_std] #![forbid(unsafe_code)] +#![cfg_attr( + not(test), + forbid( + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + clippy::as_conversions + ) +)] mod crc32; mod guid; +use core::num::NonZeroU64; + pub use crc32::{crc32, Crc32}; pub use guid::Guid; -/// The block sizes this crate will parse a GPT out of. -/// -/// The floor is the smallest logical block any device has ever reported and -/// the value every GPT in the wild is laid out in; the ceiling is 4Kn, and is -/// also what the rest of this kernel is written in. It matches the NVMe -/// driver's own accepted range, which is not a coincidence: above 4096 the -/// block no longer divides the kernel's 4 KiB block, and below 512 the GPT -/// header does not fit in one. -pub const MIN_LBA_BYTES: u32 = 512; -pub const MAX_LBA_BYTES: u32 = 4096; - /// The largest partition entry array this crate will walk, in bytes. /// /// Policy, not physics, and generous: UEFI requires the array to be at least @@ -69,7 +71,7 @@ pub trait Sectors { fn lba_count(&self) -> u64; /// Granularity of `lba_count()` in logical blocks. A count floored by a /// coarser reader can omit at most one less than this value. - fn lba_count_granularity(&self) -> core::num::NonZeroU64; + fn lba_count_granularity(&self) -> NonZeroU64; /// Fill `buf` — exactly `lba_bytes()` long — with logical block `lba`. /// `false` means the read did not happen and its contents are unknown. fn read_lba(&mut self, lba: u64, buf: &mut [u8]) -> bool; @@ -154,21 +156,63 @@ impl GptError { } } -/// One partition entry, after it has been checked against the disk it is on. +/// One partition entry. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Partition { - /// Position in the entry array, from 0. - pub index: u32, - pub type_guid: Guid, - pub unique_guid: Guid, - pub first_lba: u64, - /// Inclusive, as GPT stores it. - pub last_lba: u64, + index: u32, + type_guid: Guid, + unique_guid: Guid, + first_lba: u64, + last_lba: u64, + lba_count: NonZeroU64, } impl Partition { - pub const fn lba_count(&self) -> u64 { - self.last_lba - self.first_lba + 1 + /// `stated`, if its blocks are a partition inside `header`'s usable range. + fn place(stated: Stated, header: &Header) -> Result { + if stated.first < header.first_usable_lba || stated.last > header.last_usable_lba { + return Err(stated); + } + let lba_count = stated + .last + .checked_sub(stated.first) + .and_then(|span| span.checked_add(1)) + .and_then(NonZeroU64::new) + .ok_or(stated)?; + Ok(Self { + index: stated.index, + type_guid: stated.type_guid, + unique_guid: stated.unique_guid, + first_lba: stated.first, + last_lba: stated.last, + lba_count, + }) + } + + /// Position in the entry array, from 0. + pub const fn index(&self) -> u32 { + self.index + } + + pub const fn type_guid(&self) -> Guid { + self.type_guid + } + + pub const fn unique_guid(&self) -> Guid { + self.unique_guid + } + + pub const fn first_lba(&self) -> u64 { + self.first_lba + } + + /// Inclusive, as GPT stores it. + pub const fn last_lba(&self) -> u64 { + self.last_lba + } + + pub const fn lba_count(&self) -> NonZeroU64 { + self.lba_count } /// Whether this is an ESP *by type*. A sanity check for a log line and @@ -178,18 +222,49 @@ impl Partition { } } +/// One used entry as the table states it, before anything is proven of its +/// blocks. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Stated { + pub index: u32, + pub type_guid: Guid, + pub unique_guid: Guid, + pub first: u64, + pub last: u64, +} + +/// One used entry a scan hands back: `Err` where its blocks are no partition +/// on its disk. +pub type Entry = Result; + /// A located partition plus what the table around it looked like. /// +/// Made only by [`locate`]: no other entry of its table claims its blocks. +/// /// The extra fields are not decoration: a log line saying "this disk has three /// partitions and none of them is ours" is a different diagnostic from "this /// disk has no partition table", and on a machine with no serial port the /// difference is the whole debugging session. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Located { - pub partition: Partition, - pub disk_guid: Guid, + partition: Partition, + disk_guid: Guid, + used_entries: u32, +} + +impl Located { + pub const fn partition(&self) -> Partition { + self.partition + } + + pub const fn disk_guid(&self) -> Guid { + self.disk_guid + } + /// Entries with a non-zero type GUID, i.e. partitions that exist. - pub used_entries: u32, + pub const fn used_entries(&self) -> u32 { + self.used_entries + } } /// The primary GPT header, once every field in it has been checked. @@ -199,8 +274,14 @@ struct Header { first_usable_lba: u64, last_usable_lba: u64, entry_array_lba: u64, + /// One past the array's last block, which the header check proved + /// representable. + entry_array_end: u64, entry_count: u32, entry_bytes: u32, + /// `entry_bytes`, which the header check proved at least 128 and a + /// divisor of the block. + entry_len: usize, entry_array_crc: u32, } @@ -229,117 +310,163 @@ pub fn locate(dev: &mut dyn Sectors, target: Guid) -> Result match locate_at(dev, 1, target, &disk) { Ok(located) => Ok(located), Err(primary_err) if primary_err.primary_never_checked_out() => { - locate_at(dev, disk.lba_count - 1, target, &disk).or(Err(primary_err)) + locate_at(dev, disk.backup_header_lba, target, &disk).or(Err(primary_err)) } Err(primary_err) => Err(primary_err), } } /// Every partition on `dev` whose *type* GUID is `target`, in entry order. -/// -/// The same walk [`locate`] makes, up to and including the entry array's CRC -/// and no further: a match here is a **candidate**, not a partition anything -/// may read. `locate` on a candidate's own [`Partition::unique_guid`] is what -/// applies the range and overlap checks a set cannot carry. +/// `out` is cleared, then filled from the front. pub fn locate_type( dev: &mut dyn Sectors, target: Guid, - out: &mut [Partition], + out: &mut [Option], ) -> Result { - scan(dev, &|part| part.type_guid == target, out) + scan(dev, &|type_guid| type_guid == target, out) } /// Every partition on `dev`, in entry order: [`locate_type`] with no type -/// asked for, so a match here is what the table *states* — checked up to the -/// entry array's CRC and not for range or overlap. -pub fn list(dev: &mut dyn Sectors, out: &mut [Partition]) -> Result { +/// asked for. +pub fn list(dev: &mut dyn Sectors, out: &mut [Option]) -> Result { scan(dev, &|_| true, out) } fn scan( dev: &mut dyn Sectors, - keep: &dyn Fn(&Partition) -> bool, - out: &mut [Partition], + keep: &dyn Fn(Guid) -> bool, + out: &mut [Option], ) -> Result { let disk = open_disk(dev)?; match scan_type_at(dev, 1, keep, &disk, out) { Ok(scan) => Ok(scan), Err(primary_err) if primary_err.primary_never_checked_out() => { - scan_type_at(dev, disk.lba_count - 1, keep, &disk, out).or(Err(primary_err)) + scan_type_at(dev, disk.backup_header_lba, keep, &disk, out).or(Err(primary_err)) } Err(primary_err) => Err(primary_err), } } -/// What [`locate_type`] found: `matched` is how many entries carried the type, -/// `listed` how many of those fit the caller's slice. +/// What [`locate_type`] found: `matched` is how many entries carried the type. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TypeScan { pub matched: u32, - pub listed: usize, pub disk_guid: Guid, /// Entries with a non-zero type GUID, i.e. partitions that exist. pub used_entries: u32, } struct Disk { - lba_bytes: u32, + lba: LbaSize, lba_count: u64, + /// `lba_count - 1`, where the backup header is: at least 2. + backup_header_lba: u64, lba_count_slack: u64, } +/// A logical block size this crate parses, so one block is a fixed prefix of +/// a [`Block`]. +/// +/// The floor is the smallest logical block any device has ever reported and +/// the value every GPT in the wild is laid out in; the ceiling is 4Kn, and is +/// also what the rest of this kernel is written in. It matches the NVMe +/// driver's own accepted range, which is not a coincidence: above 4096 the +/// block no longer divides the kernel's 4 KiB block, and below 512 the GPT +/// header does not fit in one. +#[derive(Clone, Copy)] +enum LbaSize { + B512, + B1024, + B2048, + B4096, +} + +/// One logical block of the largest size this crate parses. +type Block = [u8; 4096]; + +impl LbaSize { + fn of(bytes: u32) -> Option { + match bytes { + 512 => Some(Self::B512), + 1024 => Some(Self::B1024), + 2048 => Some(Self::B2048), + 4096 => Some(Self::B4096), + _ => None, + } + } + + const fn bytes(self) -> u32 { + match self { + Self::B512 => 512, + Self::B1024 => 1024, + Self::B2048 => 2048, + Self::B4096 => 4096, + } + } + + fn of_block(self, block: &mut Block) -> &mut [u8] { + match self { + Self::B512 => &mut block[..512], + Self::B1024 => &mut block[..1024], + Self::B2048 => &mut block[..2048], + Self::B4096 => &mut block[..], + } + } +} + /// The preamble both walks share: a block size this crate parses, a device big /// enough to hold a table, and a protective MBR at LBA 0. fn open_disk(dev: &mut dyn Sectors) -> Result { let lba_bytes = dev.lba_bytes(); - if !(MIN_LBA_BYTES..=MAX_LBA_BYTES).contains(&lba_bytes) || !lba_bytes.is_power_of_two() { - return Err(GptError::UnsupportedLbaSize(lba_bytes)); - } + let lba = LbaSize::of(lba_bytes).ok_or(GptError::UnsupportedLbaSize(lba_bytes))?; let lba_count = dev.lba_count(); // LBA 0 protective MBR, LBA 1 header, at least one block of entries. - if lba_count < 3 { - return Err(GptError::DeviceTooSmall(lba_count)); - } - let lba_count_slack = dev.lba_count_granularity().get() - 1; + let backup_header_lba = match lba_count.checked_sub(1) { + Some(last) if last >= 2 => last, + _ => return Err(GptError::DeviceTooSmall(lba_count)), + }; + let lba_count_slack = dev.lba_count_granularity().get().saturating_sub(1); - let mut block = [0u8; MAX_LBA_BYTES as usize]; - let block = &mut block[..lba_bytes as usize]; + let mut block: Block = [0; 4096]; + let block = lba.of_block(&mut block); read(dev, 0, block)?; check_protective_mbr(block)?; - Ok(Disk { lba_bytes, lba_count, lba_count_slack }) + Ok(Disk { lba, lba_count, backup_header_lba, lba_count_slack }) } /// [`locate_type`]'s work against one header, primary or backup. fn scan_type_at( dev: &mut dyn Sectors, header_lba: u64, - keep: &dyn Fn(&Partition) -> bool, + keep: &dyn Fn(Guid) -> bool, disk: &Disk, - out: &mut [Partition], + out: &mut [Option], ) -> Result { - let mut block = [0u8; MAX_LBA_BYTES as usize]; - let block = &mut block[..disk.lba_bytes as usize]; + out.fill(None); + let mut block: Block = [0; 4096]; + let block = disk.lba.of_block(&mut block); read(dev, header_lba, block)?; let header = parse_header(block, disk, header_lba)?; + // At most `entry_count`, a `u32`: never saturates. let mut matched = 0u32; - let mut listed = 0usize; + let mut slots = out.iter_mut(); // Meaningless unless `walk_entries` returns `Ok`: the array's CRC is // checked at the end of the walk, and an `Err` hands the caller nothing. - let used_entries = walk_entries(dev, &header, disk.lba_bytes, &mut |part| { - if keep(&part) { - matched += 1; - if let Some(slot) = out.get_mut(listed) { - *slot = part; - listed += 1; - } + let used_entries = walk_entries(dev, &header, disk.lba, &mut |stated| { + if !keep(stated.type_guid) { + return; + } + matched = matched.saturating_add(1); + if let Some(slot) = slots.next() { + *slot = Some(Partition::place(stated, &header)); } })?; - Ok(TypeScan { matched, listed, disk_guid: header.disk_guid, used_entries }) + Ok(TypeScan { matched, disk_guid: header.disk_guid, used_entries }) } /// `locate`'s work against one header, primary or backup — read it, check it, @@ -350,27 +477,19 @@ fn locate_at( target: Guid, disk: &Disk, ) -> Result { - let mut block = [0u8; MAX_LBA_BYTES as usize]; - let block = &mut block[..disk.lba_bytes as usize]; + let mut block: Block = [0; 4096]; + let block = disk.lba.of_block(&mut block); read(dev, header_lba, block)?; let header = parse_header(block, disk, header_lba)?; - let (found, used_entries) = scan_entries(dev, &header, target, disk.lba_bytes)?; - let Some(partition) = found else { + let (found, used_entries) = scan_entries(dev, &header, target, disk.lba)?; + let Some(stated) = found else { return Err(GptError::NotFound { used_entries }); }; - - if partition.first_lba > partition.last_lba - || partition.first_lba < header.first_usable_lba - || partition.last_lba > header.last_usable_lba - { - return Err(GptError::PartitionRange { - first: partition.first_lba, - last: partition.last_lba, - }); - } - check_no_overlap(dev, &header, &partition, disk.lba_bytes)?; + let partition = Partition::place(stated, &header) + .map_err(|stated| GptError::PartitionRange { first: stated.first, last: stated.last })?; + check_no_overlap(dev, &header, &partition, disk.lba)?; Ok(Located { partition, disk_guid: header.disk_guid, used_entries }) } @@ -391,62 +510,97 @@ fn read(dev: &mut dyn Sectors, lba: u64, buf: &mut [u8]) -> Result<(), GptError> /// one of them is guessing. fn check_protective_mbr(lba0: &[u8]) -> Result<(), GptError> { // The MBR is 512 bytes at the front of LBA 0 whatever the block size is. - let Some(mbr) = lba0.get(..512) else { + let Some(mbr) = lba0.first_chunk::<512>() else { return Err(GptError::NoProtectiveMbr); }; if mbr[510] != 0x55 || mbr[511] != 0xAA { return Err(GptError::NoProtectiveMbr); } - let mut protective = 0; - for record in 0..4 { - let ty = mbr[446 + record * 16 + 4]; - match ty { - 0 => {} - MBR_TYPE_PROTECTIVE => protective += 1, - _ => return Err(GptError::NoProtectiveMbr), - } - } - if protective != 1 { + // The type byte of each of the four 16-byte records from byte 446. + let types = [mbr[450], mbr[466], mbr[482], mbr[498]]; + let protective = types.iter().filter(|&&ty| ty == MBR_TYPE_PROTECTIVE).count(); + if protective != 1 || types.iter().any(|&ty| ty != 0 && ty != MBR_TYPE_PROTECTIVE) { return Err(GptError::NoProtectiveMbr); } Ok(()) } +/// Every header field this crate reads, as the disk states it. +struct StatedHeader { + signature: [u8; 8], + revision: u32, + header_bytes: u32, + crc: u32, + reserved: u32, + my_lba: u64, + first_usable_lba: u64, + last_usable_lba: u64, + disk_guid: Guid, + entry_array_lba: u64, + entry_count: u32, + entry_bytes: u32, + entry_array_crc: u32, +} + +impl StatedHeader { + /// `None` only where `block` is shorter than a header. + fn decode(block: &[u8]) -> Option { + Some(Self { + signature: bytes(block, 0)?, + revision: le_u32(block, 8)?, + header_bytes: le_u32(block, 12)?, + crc: le_u32(block, 16)?, + reserved: le_u32(block, 20)?, + my_lba: le_u64(block, 24)?, + first_usable_lba: le_u64(block, 40)?, + last_usable_lba: le_u64(block, 48)?, + disk_guid: bytes(block, 56).map(Guid)?, + entry_array_lba: le_u64(block, 72)?, + entry_count: le_u32(block, 80)?, + entry_bytes: le_u32(block, 84)?, + entry_array_crc: le_u32(block, 88)?, + }) + } +} + fn parse_header(lba1: &[u8], disk: &Disk, header_lba: u64) -> Result { - let Disk { lba_bytes, lba_count, lba_count_slack } = *disk; - if lba1.get(..8) != Some(&HEADER_SIGNATURE[..]) { + let Disk { lba, lba_count, backup_header_lba, lba_count_slack } = *disk; + let lba_bytes = lba.bytes(); + let Some(stated) = StatedHeader::decode(lba1) else { + return Err(GptError::NoHeader); + }; + if &stated.signature != HEADER_SIGNATURE { return Err(GptError::NoHeader); } - let revision = le_u32(lba1, 8); - if revision != HEADER_REVISION_1_0 { - return Err(GptError::UnsupportedRevision(revision)); + if stated.revision != HEADER_REVISION_1_0 { + return Err(GptError::UnsupportedRevision(stated.revision)); } - let header_bytes = le_u32(lba1, 12); + let header_bytes = stated.header_bytes; if header_bytes < MIN_HEADER_BYTES || header_bytes > lba_bytes { return Err(GptError::HeaderSize(header_bytes)); } - let reserved = le_u32(lba1, 20); - if reserved != 0 { - return Err(GptError::HeaderReserved(reserved)); + if stated.reserved != 0 { + return Err(GptError::HeaderReserved(stated.reserved)); } - let my_lba = le_u64(lba1, 24); - if my_lba != header_lba { - return Err(GptError::HeaderMisplaced(my_lba)); + if stated.my_lba != header_lba { + return Err(GptError::HeaderMisplaced(stated.my_lba)); } - let stored_crc = le_u32(lba1, 16); - let computed = header_crc(lba1, header_bytes as usize); - if stored_crc != computed { - return Err(GptError::HeaderCrc { stored: stored_crc, computed }); + let covered = usize::try_from(header_bytes).ok().and_then(|len| lba1.get(..len)); + let Some(computed) = covered.and_then(header_crc) else { + return Err(GptError::HeaderSize(header_bytes)); + }; + if stated.crc != computed { + return Err(GptError::HeaderCrc { stored: stated.crc, computed }); } - let first_usable_lba = le_u64(lba1, 40); - let last_usable_lba = le_u64(lba1, 48); + let (first_usable_lba, last_usable_lba) = (stated.first_usable_lba, stated.last_usable_lba); if first_usable_lba < 2 || last_usable_lba < first_usable_lba || last_usable_lba >= lba_count { return Err(GptError::UsableRange { first: first_usable_lba, last: last_usable_lba }); } - let entry_bytes = le_u32(lba1, 84); + let entry_bytes = stated.entry_bytes; + let entry_len = usize::try_from(entry_bytes).map_err(|_| GptError::EntrySize(entry_bytes))?; if entry_bytes < MIN_ENTRY_BYTES || !entry_bytes.is_power_of_two() || entry_bytes > lba_bytes @@ -454,15 +608,16 @@ fn parse_header(lba1: &[u8], disk: &Disk, header_lba: u64) -> Result MAX_ENTRY_ARRAY_BYTES { - return Err(GptError::EntryArrayTooBig { entries: entry_count, entry_size: entry_bytes }); + return Err(too_big); } - let entry_array_lba = le_u64(lba1, 72); - let array_lbas = array_bytes.div_ceil(lba_bytes as u64); - let array_end = entry_array_lba + let entry_array_lba = stated.entry_array_lba; + let array_lbas = array_bytes.div_ceil(u64::from(lba_bytes)); + let entry_array_end = entry_array_lba .checked_add(array_lbas) .ok_or(GptError::EntryArrayMisplaced { lba: entry_array_lba, lbas: array_lbas })?; // The primary's array sits between the header block and the first usable @@ -472,9 +627,9 @@ fn parse_header(lba1: &[u8], disk: &Disk, header_lba: u64) -> Result first_usable_lba + entry_array_lba < 2 || entry_array_end > first_usable_lba } else { - entry_array_lba <= last_usable_lba || array_end > header_lba + entry_array_lba <= last_usable_lba || entry_array_end > header_lba }; if misplaced { return Err(GptError::EntryArrayMisplaced { lba: entry_array_lba, lbas: array_lbas }); @@ -485,31 +640,36 @@ fn parse_header(lba1: &[u8], disk: &Disk, header_lba: u64) -> Result= backup_array_lba { return Err(GptError::UsableRangeCoversBackup { last: last_usable_lba, backup_array_lba }); } Ok(Header { - disk_guid: read_guid(lba1, 56), + disk_guid: stated.disk_guid, first_usable_lba, last_usable_lba, entry_array_lba, + entry_array_end, entry_count, entry_bytes, - entry_array_crc: le_u32(lba1, 88), + entry_len, + entry_array_crc: stated.entry_array_crc, }) } /// The header's CRC is taken over itself with its own CRC field zeroed, so it /// is computed in three pieces rather than by copying the block to patch it. -fn header_crc(lba1: &[u8], header_bytes: usize) -> u32 { +/// `None` only where `covered` is shorter than the CRC field's end. +fn header_crc(covered: &[u8]) -> Option { + let (before, rest) = covered.split_first_chunk::<16>()?; + let (_stored, after) = rest.split_first_chunk::<4>()?; let mut crc = Crc32::new(); - crc.update(&lba1[..16]); + crc.update(before); crc.update(&[0; 4]); - crc.update(&lba1[20..header_bytes]); - crc.finish() + crc.update(after); + Some(crc.finish()) } /// Walk the entry array once, checking its CRC as we go, and show `visit` @@ -522,46 +682,32 @@ fn header_crc(lba1: &[u8], header_bytes: usize) -> u32 { fn walk_entries( dev: &mut dyn Sectors, header: &Header, - lba_bytes: u32, - visit: &mut dyn FnMut(Partition), + lba: LbaSize, + visit: &mut dyn FnMut(Stated), ) -> Result { - let mut block = [0u8; MAX_LBA_BYTES as usize]; - let block = &mut block[..lba_bytes as usize]; + let mut block: Block = [0; 4096]; + let block = lba.of_block(&mut block); - let entries_per_lba = lba_bytes / header.entry_bytes; let mut crc = Crc32::new(); - let mut remaining = header.entry_count as u64 * header.entry_bytes as u64; + let mut indices = 0..header.entry_count; + // At most `entry_count`, a `u32`: never saturates. let mut used = 0u32; - let mut index = 0u32; - let mut lba = header.entry_array_lba; - - while remaining > 0 { - read(dev, lba, block)?; - let take = remaining.min(lba_bytes as u64) as usize; - crc.update(&block[..take]); - for slot in 0..entries_per_lba { - if index >= header.entry_count { - break; - } - let at = slot as usize * header.entry_bytes as usize; - let entry = &block[at..at + header.entry_bytes as usize]; - let type_guid = read_guid(entry, 0); - if !type_guid.is_zero() { - used += 1; - visit(Partition { - index, - type_guid, - unique_guid: read_guid(entry, 16), - first_lba: le_u64(entry, 32), - last_lba: le_u64(entry, 40), - }); + for at in header.entry_array_lba..header.entry_array_end { + read(dev, at, block)?; + // The array is `entry_count` whole entries and nothing after them, so + // the entries walked are exactly the bytes its CRC covers. + for entry in block.chunks_exact(header.entry_len) { + let Some(index) = indices.next() else { break }; + crc.update(entry); + let Some(stated) = Stated::decode(index, entry) else { + return Err(GptError::EntrySize(header.entry_bytes)); + }; + if !stated.type_guid.is_zero() { + used = used.saturating_add(1); + visit(stated); } - index += 1; } - - remaining -= take as u64; - lba += 1; } let computed = crc.finish(); @@ -571,23 +717,36 @@ fn walk_entries( Ok(used) } +impl Stated { + /// `None` only where `entry` is shorter than the fields read. + fn decode(index: u32, entry: &[u8]) -> Option { + Some(Self { + index, + type_guid: bytes(entry, 0).map(Guid)?, + unique_guid: bytes(entry, 16).map(Guid)?, + first: le_u64(entry, 32)?, + last: le_u64(entry, 40)?, + }) + } +} + /// The entry carrying the unique GUID `target`, out of a walk whose CRC held. fn scan_entries( dev: &mut dyn Sectors, header: &Header, target: Guid, - lba_bytes: u32, -) -> Result<(Option, u32), GptError> { - let mut found: Option = None; + lba: LbaSize, +) -> Result<(Option, u32), GptError> { + let mut found: Option = None; let mut duplicate: Option<(u32, u32)> = None; - let used = walk_entries(dev, header, lba_bytes, &mut |part| { - if part.unique_guid != target { + let used = walk_entries(dev, header, lba, &mut |stated| { + if stated.unique_guid != target { return; } match &found { - None => found = Some(part), - Some(first) if duplicate.is_none() => duplicate = Some((first.index, part.index)), + None => found = Some(stated), + Some(first) if duplicate.is_none() => duplicate = Some((first.index, stated.index)), Some(_) => {} } })?; @@ -608,53 +767,54 @@ fn check_no_overlap( dev: &mut dyn Sectors, header: &Header, matched: &Partition, - lba_bytes: u32, + lba: LbaSize, ) -> Result<(), GptError> { - let mut block = [0u8; MAX_LBA_BYTES as usize]; - let block = &mut block[..lba_bytes as usize]; - - let entries_per_lba = lba_bytes / header.entry_bytes; - let mut remaining = header.entry_count as u64 * header.entry_bytes as u64; - let mut index = 0u32; - let mut lba = header.entry_array_lba; - - while remaining > 0 { - read(dev, lba, block)?; - for slot in 0..entries_per_lba { - if index >= header.entry_count { - break; - } - let at = slot as usize * header.entry_bytes as usize; - let entry = &block[at..at + header.entry_bytes as usize]; - if index != matched.index && !read_guid(entry, 0).is_zero() { - let first = le_u64(entry, 32); - let last = le_u64(entry, 40); - if first <= last && first <= matched.last_lba && matched.first_lba <= last { - return Err(GptError::PartitionOverlap { index }); - } - } - index += 1; + let mut overlap = None; + walk_entries(dev, header, lba, &mut |other| { + if overlap.is_none() + && other.index != matched.index + && other.first <= other.last + && other.first <= matched.last_lba + && matched.first_lba <= other.last + { + overlap = Some(other.index); } - remaining -= remaining.min(lba_bytes as u64); - lba += 1; + })?; + match overlap { + Some(index) => Err(GptError::PartitionOverlap { index }), + None => Ok(()), } - Ok(()) } -fn read_guid(buf: &[u8], at: usize) -> Guid { - let mut out = [0u8; 16]; - out.copy_from_slice(&buf[at..at + 16]); - Guid(out) +/// `N` bytes of `buf` from `at`, or `None` where `buf` ends first. +fn bytes(buf: &[u8], at: usize) -> Option<[u8; N]> { + buf.get(at..)?.first_chunk::().copied() +} + +fn le_u32(buf: &[u8], at: usize) -> Option { + bytes(buf, at).map(u32::from_le_bytes) } -fn le_u32(buf: &[u8], at: usize) -> u32 { - let mut b = [0u8; 4]; - b.copy_from_slice(&buf[at..at + 4]); - u32::from_le_bytes(b) +fn le_u64(buf: &[u8], at: usize) -> Option { + bytes(buf, at).map(u64::from_le_bytes) } -fn le_u64(buf: &[u8], at: usize) -> u64 { - let mut b = [0u8; 8]; - b.copy_from_slice(&buf[at..at + 8]); - u64::from_le_bytes(b) +#[cfg(test)] +mod tests { + use super::*; + + /// The sizes [`LbaSize`] admits are one range. + #[test] + fn the_block_sizes_parsed_are_the_declared_range() { + let parsed: [u32; 4] = [512, 1024, 2048, 4096]; + for bytes in 0..=8192 { + let admitted = LbaSize::of(bytes).map(LbaSize::bytes); + assert_eq!(admitted.is_some(), parsed.contains(&bytes), "{bytes}"); + assert_eq!(admitted.unwrap_or(bytes), bytes); + } + let mut block: Block = [0; 4096]; + for lba in [LbaSize::B512, LbaSize::B1024, LbaSize::B2048, LbaSize::B4096] { + assert_eq!(lba.of_block(&mut block).len() as u32, lba.bytes()); + } + } } diff --git a/toyos-gpt/tests/mutate.rs b/toyos-gpt/tests/mutate.rs new file mode 100644 index 0000000000..84d68f1816 --- /dev/null +++ b/toyos-gpt/tests/mutate.rs @@ -0,0 +1,232 @@ +//! Value-mutated tables with both CRCs resealed: no parse panics, and every answer is true of a copy of the table. + +mod table; + +use std::collections::BTreeMap; +use std::panic::{catch_unwind, AssertUnwindSafe}; + +use table::{OnDisk, Image, RawEntry, Rng, Shape}; +use toyos_gpt::{GptError, Guid}; + +/// Iterations of the normal host run; each draws its own seed from its number. +const ITERATIONS: u64 = 5_000; +const SEED: u64 = 0x6770_745F_6D75_7461; + +const SHAPE: Shape<'static> = Shape { + lba_sizes: &[512, 512, 512, 1024, 2048, 4096], + types: &[Guid::EFI_SYSTEM, Guid::MICROSOFT_BASIC, Guid::TOYOS_DATA, Guid::TOYOS_ROOT], + entry_sizes: &[128, 128, 128, 128, 256, 512], + backup: true, + floored: true, +}; + +/// A partition as the parser handed it back. +#[derive(Debug)] +struct Seen { + index: u32, + type_guid: Guid, + unique: Guid, + first: u64, + last: u64, + count: u64, +} + +/// An entry the parser handed back as no partition. +#[derive(Debug)] +struct Refused { + index: u32, + unique: Guid, + first: u64, + last: u64, +} + +/// What `list` answered: every entry in order, and its two counts. +struct Listed { + entries: Vec>, + matched: u32, + used_entries: u32, +} + +/// What `locate` answered for each GUID asked. +type Answers = Vec<(Guid, Result)>; + +/// What the parser answered for one image; the one place this file names its API. +fn observe(img: &mut Image, targets: &[Guid]) -> (Result, Answers) { + let seen = |p: &toyos_gpt::Partition| Seen { + index: p.index(), + type_guid: p.type_guid(), + unique: p.unique_guid(), + first: p.first_lba(), + last: p.last_lba(), + count: p.lba_count().get(), + }; + let mut out = [None; 64]; + let listed = toyos_gpt::list(img, &mut out).map(|scan| Listed { + entries: out + .iter() + .flatten() + .map(|entry| match entry { + Ok(p) => Ok(seen(p)), + Err(u) => Err(Refused { index: u.index, unique: u.unique_guid, first: u.first, last: u.last }), + }) + .collect(), + matched: scan.matched, + used_entries: scan.used_entries, + }); + let located = targets.iter().map(|&t| (t, toyos_gpt::locate(img, t).map(|l| seen(&l.partition())))).collect(); + (listed, located) +} + +fn is(e: &RawEntry, s: &Seen) -> bool { + (e.index, e.type_guid, e.unique, e.first, e.last) == (s.index, s.type_guid, s.unique, s.first, s.last) +} + +/// A partition's own invariant, against the copy it came from and the device. +fn sound(c: &OnDisk, s: &Seen, lba_count: u64) -> bool { + c.entry(s.index).is_some_and(|e| is(e, s)) + && c.first_usable <= s.first + && s.first <= s.last + && s.last <= c.last_usable + && s.last < lba_count + && u128::from(s.count) == u128::from(s.last) - u128::from(s.first) + 1 +} + +fn overlaps(a: &RawEntry, b: &RawEntry) -> bool { + a.first <= a.last && b.first <= b.last && a.first <= b.last && b.first <= a.last +} + +/// Every property this file states, for one image; `Err` names the one broken. +fn judge( + copies: &[OnDisk], + lba_count: u64, + listed: &Result, + located: &[(Guid, Result)], + tally: &mut BTreeMap, +) -> Result<(), String> { + let mut count = |what: String| *tally.entry(what).or_default() += 1; + match listed { + Ok(l) => { + let faithful = |c: &OnDisk| { + l.used_entries as usize == c.entries.len() + && l.matched as usize == c.entries.len() + && l.entries.len() == c.entries.len().min(64) + && l.entries.iter().all(|entry| match entry { + Ok(s) => sound(c, s, lba_count), + Err(r) => c.entry(r.index).is_some_and(|e| { + (e.index, e.unique, e.first, e.last) == (r.index, r.unique, r.first, r.last) && !c.places(e) + }), + }) + }; + if !copies.iter().any(faithful) { + return Err(format!("list answered what no copy of the table says: {:?}", l.entries)); + } + for entry in &l.entries { + count(if entry.is_ok() { "list: partition" } else { "list: no partition" }.into()); + } + } + Err(e) => count(format!("list: {}", variant(e))), + } + for (target, answer) in located { + let carrying = |c: &OnDisk| c.entries.iter().filter(|e| e.unique == *target).copied().collect::>(); + let true_of_a_copy = |c: &OnDisk| match answer { + Ok(s) => { + sound(c, s, lba_count) + && s.unique == *target + && carrying(c).len() == 1 + && c.entry(s.index).is_some_and(|me| !c.entries.iter().any(|o| o.index != me.index && overlaps(o, me))) + } + Err(GptError::PartitionRange { first, last }) => { + matches!(carrying(c)[..], [e] if (e.first, e.last) == (*first, *last) && !c.places(&e)) + } + Err(GptError::PartitionOverlap { index }) => match (&carrying(c)[..], c.entry(*index)) { + ([me], Some(other)) => other.index != me.index && overlaps(other, me) && c.places(me), + _ => false, + }, + Err(GptError::DuplicateUniqueGuid { first, second }) => { + carrying(c).iter().map(|e| e.index).take(2).eq([*first, *second]) + } + Err(GptError::NotFound { used_entries }) => { + carrying(c).is_empty() && *used_entries as usize == c.entries.len() + } + // Refusals of the table itself, before any entry means anything. + Err(_) => true, + }; + let of_the_table = matches!( + answer, + Err(GptError::PartitionRange { .. } + | GptError::PartitionOverlap { .. } + | GptError::DuplicateUniqueGuid { .. } + | GptError::NotFound { .. }) + | Ok(_) + ); + if of_the_table && !copies.iter().any(true_of_a_copy) { + return Err(format!("locate({target}) answered {answer:?}, which no copy of the table says")); + } + count(match answer { + Ok(_) => "locate: located".into(), + Err(e) => format!("locate: {}", variant(e)), + }); + } + Ok(()) +} + +fn variant(e: &GptError) -> String { + let debug = format!("{e:?}"); + debug.split(['(', ' ']).next().unwrap_or_default().to_string() +} + +#[test] +fn a_resealed_table_with_any_value_bent_never_panics_and_hands_back_only_partitions() { + let mut tally = BTreeMap::new(); + for i in 0..ITERATIONS { + let seed = SEED ^ i.wrapping_mul(0x9E37_79B9_7F4A_7C15); + let mut rng = Rng::new(seed); + let mut layout = table::valid(&mut rng, &SHAPE); + // One in ten stays as UEFI laid it out. + if !rng.one_in(10) { + table::mutate(&mut rng, &mut layout); + } + let mut img = table::image(&layout); + let mut targets: Vec = layout.primary.entries.iter().map(|e| e.unique).collect(); + targets.truncate(3); + targets.push(rng.guid()); + + let run = catch_unwind(AssertUnwindSafe(|| observe(&mut img, &targets))); + let Ok((listed, located)) = run else { + panic!("the parser panicked on seed {seed:#x} (iteration {i}): {layout:#?}"); + }; + let copies = OnDisk::both(&img); + if let Err(why) = judge(&copies, img.lba_count, &listed, &located, &mut tally) { + panic!("seed {seed:#x} (iteration {i}): {why}\n{layout:#?}"); + } + } + + for (what, n) in &tally { + eprintln!("{n:>8} {what}"); + } + // Every refusal past both CRCs, and acceptance, has to have happened. + for reached in [ + "list: partition", + "list: no partition", + "locate: located", + "locate: PartitionRange", + "locate: PartitionOverlap", + "locate: DuplicateUniqueGuid", + "locate: NotFound", + "locate: UsableRange", + "locate: UsableRangeCoversBackup", + "locate: EntryArrayMisplaced", + "locate: EntryArrayTooBig", + "locate: EntrySize", + "locate: HeaderSize", + "locate: HeaderMisplaced", + "locate: NoProtectiveMbr", + ] { + assert!(tally.get(reached).copied().unwrap_or(0) > 0, "{ITERATIONS} tables never reached {reached:?}"); + } + let located = |crc: bool| -> u64 { + tally.iter().filter(|(k, _)| k.starts_with("locate: ") && (!crc || k.ends_with("Crc"))).map(|(_, n)| n).sum() + }; + let (crc, parses) = (located(true), located(false)); + assert!(crc * 10 < parses, "{crc} of {parses} locates stopped at a checksum: the reseal is not sealing"); +} diff --git a/toyos-gpt/tests/oracle.rs b/toyos-gpt/tests/oracle.rs new file mode 100644 index 0000000000..998c115ef6 --- /dev/null +++ b/toyos-gpt/tests/oracle.rs @@ -0,0 +1,199 @@ +//! toyos-gpt against the `gpt` crate: agreement on UEFI tables, and only named differences on bent ones. + +mod table; + +use std::collections::BTreeMap; +use std::io::Cursor; + +use table::{Image, Layout, Rng, Shape}; +use toyos_gpt::{GptError, Guid}; + +/// Linux filesystem data, a type both readers name. +const LINUX_FS: Guid = + Guid::from_fields(0x0FC6_3DAF, 0x8483, 0x4772, [0x8E, 0x79, 0x3D, 0x69, 0xD8, 0x47, 0x7D, 0xE4]); + +const SHAPE: Shape<'static> = Shape { + lba_sizes: &[512, 4096], + types: &[Guid::EFI_SYSTEM, Guid::MICROSOFT_BASIC, LINUX_FS], + // `gpt` reads 128-byte entries whatever the header says. + entry_sizes: &[128], + backup: false, + floored: false, +}; + +const VALID: u64 = 2_000; +const BENT: u64 = 4_000; +const SEED: u64 = 0x6770_745F_6F72_636C; + +/// One entry: index, type as text, unique GUID, first and last block. +type Row = (u32, String, Guid, u64, u64); + +/// toyos-gpt's reading. +struct Ours { + disk: Guid, + placed: BTreeMap, + unplaced: BTreeMap, +} + +fn ours(img: &mut Image) -> Result { + let mut out = [None; 64]; + let scan = toyos_gpt::list(img, &mut out)?; + assert!(scan.matched as usize <= out.len(), "a table with more entries than this file's slice"); + let mut read = Ours { disk: scan.disk_guid, placed: BTreeMap::new(), unplaced: BTreeMap::new() }; + for entry in out.iter().flatten() { + match entry { + Ok(p) => { + let row = (p.index(), p.type_guid().to_string(), p.unique_guid(), p.first_lba(), p.last_lba()); + read.placed.insert(p.index(), row); + } + Err(u) => { + read.unplaced.insert(u.index, (u.index, u.type_guid.to_string(), u.unique_guid, u.first, u.last)); + } + } + } + Ok(read) +} + +/// The `gpt` crate's reading. +enum Theirs { + Refused(String), + /// Not asked: it allocates the array its header claims before checking it. + NotAsked, + Read { disk: Guid, rows: BTreeMap }, +} + +fn theirs(img: &Image) -> Theirs { + let lb = gpt::disk::LogicalBlockSize::try_from(u64::from(img.lba_bytes)).expect("512 or 4096"); + let mut dev = Cursor::new(img.bytes.as_slice()); + let header = match gpt::header::read_header_from_arbitrary_device(&mut dev, lb) { + Ok(header) => header, + Err(e) => return Theirs::Refused(e.to_string()), + }; + if u64::from(header.num_parts) * u64::from(header.part_size) > 1 << 20 { + return Theirs::NotAsked; + } + match gpt::partition::file_read_partitions(&mut dev, &header, lb) { + Err(e) => Theirs::Refused(e.to_string()), + Ok(parts) => Theirs::Read { + disk: Guid(header.disk_guid.to_bytes_le()), + // `gpt` keys its entries from 1. + rows: parts + .iter() + .map(|(&key, p)| { + let row = (key - 1, p.part_type_guid.guid.to_string(), Guid(p.part_guid.to_bytes_le()), p.first_lba, p.last_lba); + (key - 1, row) + }) + .collect(), + }, + } +} + +/// The name of the difference between the two readings, or why it has none. +fn differ(layout: &Layout, img: &Image, ours: &Result, theirs: &Theirs) -> Result<&'static str, String> { + let t = &layout.primary; + let unnamed = gpt::partition_types::Type::default().guid; + match (ours, theirs) { + (Err(_), Theirs::Refused(_)) => Ok("both refuse"), + (Err(_), Theirs::NotAsked) => Ok("an array over 1 MiB: gpt would allocate it, toyos-gpt refuses"), + (Ok(_), Theirs::NotAsked) => Err("gpt was not asked, and toyos-gpt read an array over 1 MiB".into()), + (Err(e), Theirs::Read { .. }) => match e { + GptError::NoProtectiveMbr => Ok("gpt reads no protective MBR"), + GptError::UnsupportedRevision(_) | GptError::HeaderReserved(_) | GptError::HeaderMisplaced(_) => { + Ok("gpt checks no revision, reserved word or header position") + } + GptError::HeaderSize(_) | GptError::EntrySize(_) => Ok("gpt checks no header or entry size"), + GptError::UsableRange { .. } + | GptError::UsableRangeCoversBackup { .. } + | GptError::EntryArrayMisplaced { .. } + | GptError::EntryArrayTooBig { .. } => Ok("gpt checks no layout"), + GptError::DeviceTooSmall(_) | GptError::ReadFailed(_) if img.lba_count != layout.lba_count => { + Ok("the device answers a size the image is not") + } + other => Err(format!("toyos-gpt refused with {other:?}, and gpt read the table")), + }, + (Ok(_), Theirs::Refused(why)) if t.header_bytes != 92 => { + let _ = why; + Ok("gpt takes the header CRC over 92 bytes whatever header_size says") + } + (Ok(_), Theirs::Refused(why)) => Err(format!("toyos-gpt read the table, and gpt refused it: {why}")), + (Ok(o), Theirs::Read { disk, rows }) => { + if t.entry_bytes != 128 { + return Ok("gpt strides 128 bytes whatever the entry size"); + } + if *disk != o.disk { + return Err(format!("disk GUID {} against gpt's {disk}", o.disk)); + } + let mut named = "agree"; + for (index, row) in rows { + match (o.placed.get(index), o.unplaced.get(index)) { + (Some(mine), None) if mine == row => {} + // A type only toyos-gpt names: gpt answers the zero GUID. + (Some(mine), None) if row.1 == unnamed && (&mine.0, &mine.2, mine.3, mine.4) == (&row.0, &row.2, row.3, row.4) => { + named = "a type gpt's own table does not name"; + } + (None, Some(stated)) if (stated.2, stated.3, stated.4) == (row.2, row.3, row.4) => { + named = "an entry toyos-gpt places as no partition: gpt checks no range"; + } + // gpt counts an entry by any non-zero byte, toyos-gpt by its type GUID. + (None, None) if row.1 == unnamed => named = "a zero type over non-zero bytes: gpt lists it", + (mine, stated) => { + return Err(format!("entry {index}: gpt reads {row:?}, toyos-gpt {mine:?} / {stated:?}")); + } + } + } + if let Some(index) = o.placed.keys().chain(o.unplaced.keys()).find(|i| !rows.contains_key(i)) { + return Err(format!("toyos-gpt reads entry {index} and gpt does not")); + } + Ok(named) + } + } +} + +/// The generator's types are ones both readers spell alike. +#[test] +fn both_readers_spell_the_types_alike() { + for (ours, theirs) in [ + (Guid::EFI_SYSTEM, gpt::partition_types::EFI), + (Guid::MICROSOFT_BASIC, gpt::partition_types::BASIC), + (LINUX_FS, gpt::partition_types::LINUX_FS), + ] { + assert_eq!(ours.to_string(), theirs.guid); + } +} + +#[test] +fn on_tables_uefi_lays_out_both_readers_agree_entry_for_entry() { + for i in 0..VALID { + let seed = SEED ^ i.wrapping_mul(0x9E37_79B9_7F4A_7C15); + let layout = table::valid(&mut Rng::new(seed), &SHAPE); + let mut img = table::image(&layout); + let mine = ours(&mut img); + let named = differ(&layout, &img, &mine, &theirs(&img)); + assert_eq!(named, Ok("agree"), "seed {seed:#x}: {layout:#?}"); + let placed = mine.map(|o| o.placed.len()).unwrap_or_default(); + assert_eq!(placed, layout.primary.entries.len(), "seed {seed:#x}"); + } +} + +#[test] +fn on_bent_tables_every_disagreement_is_a_named_difference() { + let mut tally: BTreeMap<&'static str, u64> = BTreeMap::new(); + for i in 0..BENT { + let seed = SEED ^ !i.wrapping_mul(0x9E37_79B9_7F4A_7C15); + let mut rng = Rng::new(seed); + let mut layout = table::valid(&mut rng, &SHAPE); + table::mutate(&mut rng, &mut layout); + let mut img = table::image(&layout); + let mine = ours(&mut img); + match differ(&layout, &img, &mine, &theirs(&img)) { + Ok(named) => *tally.entry(named).or_default() += 1, + Err(why) => panic!("seed {seed:#x}: {why}\n{layout:#?}"), + } + } + for (named, n) in &tally { + eprintln!("{n:>6} {named}"); + } + for reached in ["agree", "both refuse", "an entry toyos-gpt places as no partition: gpt checks no range"] { + assert!(tally.contains_key(reached), "{BENT} bent tables never reached {reached:?}"); + } +} diff --git a/toyos-gpt/tests/parse.rs b/toyos-gpt/tests/parse.rs index b623a054c0..fbdbc1d967 100644 --- a/toyos-gpt/tests/parse.rs +++ b/toyos-gpt/tests/parse.rs @@ -6,7 +6,10 @@ //! none of the broken ones panics, allocates by a number the disk chose, or //! returns a partition anyway. -use toyos_gpt::{crc32, GptError, Guid, Located, Sectors}; +mod table; + +use table::{Image, Layout, RawEntry, Table}; +use toyos_gpt::{GptError, Guid, Located, Stated}; const LBA: u32 = 512; const ENTRY: u32 = 128; @@ -26,183 +29,53 @@ fn guid(n: u8) -> Guid { Guid(b) } -#[derive(Clone, Copy)] -struct Entry { - type_guid: Guid, - unique: Guid, - first: u64, - last: u64, -} - -impl Entry { - fn new(type_guid: Guid, unique: Guid, first: u64, last: u64) -> Self { - Self { type_guid, unique, first, last } - } +fn entry(index: u32, type_guid: Guid, unique: Guid, first: u64, last: u64) -> RawEntry { + RawEntry { index, type_guid, unique, first, last } } -struct Builder { - lba_bytes: u32, - lba_count: u64, - entry_count: u32, - entry_bytes: u32, - entry_array_lba: u64, - first_usable: u64, - last_usable: u64, - revision: u32, - header_bytes: u32, - reserved: u32, - my_lba: u64, - entries: Vec, - hybrid_mbr: bool, - no_mbr_signature: bool, - /// Write a second, independent copy of the header and the array at the - /// top of the device — LBA `lba_count - 1` and just below it — the way a - /// real disk carries one. `false` by default and unchanged by it: every - /// test above this field was written against a disk with no backup at - /// all, and adding one silently would let a fallback this crate does not - /// yet have paper over a primary this suite meant to break. - backup: bool, -} - -impl Default for Builder { - fn default() -> Self { - Self { - lba_bytes: LBA, - lba_count: DISK_LBAS, - entry_count: 128, - entry_bytes: ENTRY, - entry_array_lba: ARRAY_LBA, - first_usable: FIRST_USABLE, - last_usable: DISK_LBAS - FIRST_USABLE, +/// The disk every test here breaks one thing of, `edit`ed, with no backup +/// unless the edit [`mirrored`] one: a fallback must not paper over a primary +/// a test meant to break. +fn disk(edit: impl FnOnce(&mut Layout)) -> Image { + let mut layout = Layout { + lba_bytes: LBA, + lba_count: DISK_LBAS, + primary: Table { revision: 0x0001_0000, header_bytes: 92, reserved: 0, my_lba: 1, + first_usable: FIRST_USABLE, + last_usable: DISK_LBAS - FIRST_USABLE, + disk_guid: guid(0x5D), + entry_array_lba: ARRAY_LBA, + entry_count: 128, + entry_bytes: ENTRY, entries: vec![ // Two ESP-typed decoys before the real one, and the real one // is neither first nor an obvious pick: a matcher that keys on // the type GUID, or takes the first used entry, or takes the // biggest, gets a different answer than the one asserted. - Entry::new(TYPE_ESP, guid(0xA1), 40, 99), - Entry::new(TYPE_OTHER, guid(0xB2), 100, 199), - Entry::new(TYPE_ESP, guid(0xC3), 200, 299), - Entry::new(TYPE_ESP, guid(0xD4), 300, 1999), + entry(0, TYPE_ESP, guid(0xA1), 40, 99), + entry(1, TYPE_OTHER, guid(0xB2), 100, 199), + entry(2, TYPE_ESP, guid(0xC3), 200, 299), + entry(3, TYPE_ESP, guid(0xD4), 300, 1999), ], - hybrid_mbr: false, - no_mbr_signature: false, - backup: false, - } - } + }, + backup: None, + reported_lba_count: DISK_LBAS, + granularity: 1, + mbr_type: 0xEE, + mbr_signature: [0x55, 0xAA], + }; + edit(&mut layout); + layout.reported_lba_count = layout.lba_count; + table::image(&layout) } -impl Builder { - fn build(&self) -> Image { - let lba = self.lba_bytes as usize; - let mut disk = vec![0u8; lba * self.lba_count as usize]; - - if !self.no_mbr_signature { - disk[510] = 0x55; - disk[511] = 0xAA; - } - disk[446 + 4] = 0xEE; - disk[446 + 8..446 + 12].copy_from_slice(&1u32.to_le_bytes()); - disk[446 + 12..446 + 16].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes()); - if self.hybrid_mbr { - disk[446 + 16 + 4] = 0x83; - } - - // A header may name an array LBA this disk does not have; the parser - // is what has to notice, so the builder just writes no entries. - let array_at = (self.entry_array_lba as usize).saturating_mul(lba).min(disk.len()); - for (i, e) in self.entries.iter().enumerate() { - let at = array_at + i * self.entry_bytes as usize; - if at + 128 > disk.len() { - break; - } - disk[at..at + 16].copy_from_slice(&e.type_guid.0); - disk[at + 16..at + 32].copy_from_slice(&e.unique.0); - disk[at + 32..at + 40].copy_from_slice(&e.first.to_le_bytes()); - disk[at + 40..at + 48].copy_from_slice(&e.last.to_le_bytes()); - } - - let array_bytes = (self.entry_count as usize).saturating_mul(self.entry_bytes as usize); - let array_crc = if array_at.saturating_add(array_bytes) <= disk.len() { - crc32(&disk[array_at..array_at + array_bytes]) - } else { - 0 - }; - - let mut h = vec![0u8; lba]; - h[..8].copy_from_slice(b"EFI PART"); - h[8..12].copy_from_slice(&self.revision.to_le_bytes()); - h[12..16].copy_from_slice(&self.header_bytes.to_le_bytes()); - h[20..24].copy_from_slice(&self.reserved.to_le_bytes()); - h[24..32].copy_from_slice(&self.my_lba.to_le_bytes()); - h[32..40].copy_from_slice(&(self.lba_count - 1).to_le_bytes()); - h[40..48].copy_from_slice(&self.first_usable.to_le_bytes()); - h[48..56].copy_from_slice(&self.last_usable.to_le_bytes()); - h[56..72].copy_from_slice(&guid(0x5D).0); - h[72..80].copy_from_slice(&self.entry_array_lba.to_le_bytes()); - h[80..84].copy_from_slice(&self.entry_count.to_le_bytes()); - h[84..88].copy_from_slice(&self.entry_bytes.to_le_bytes()); - h[88..92].copy_from_slice(&array_crc.to_le_bytes()); - let size = (self.header_bytes as usize).min(lba).max(16); - let crc = crc32(&h[..size]); - h[16..20].copy_from_slice(&crc.to_le_bytes()); - disk[lba..lba * 2].copy_from_slice(&h); - - if self.backup { - // The backup array sits directly below the backup header, at the - // top of the device — the mirror of the primary's layout, where - // the array follows the header. Same entries, same CRC: this - // builds an honest mirror, not an independent second table, because the - // point of `backup` is a torn front recovering from an intact - // back, not two disks disagreeing. - let array_lbas = (self.entry_count as u64 * self.entry_bytes as u64).div_ceil(lba as u64); - let backup_header_lba = self.lba_count - 1; - let backup_array_lba = backup_header_lba - array_lbas; - let backup_array_at = (backup_array_lba as usize).saturating_mul(lba); - for (i, e) in self.entries.iter().enumerate() { - let at = backup_array_at + i * self.entry_bytes as usize; - if at + 128 > disk.len() { - break; - } - disk[at..at + 16].copy_from_slice(&e.type_guid.0); - disk[at + 16..at + 32].copy_from_slice(&e.unique.0); - disk[at + 32..at + 40].copy_from_slice(&e.first.to_le_bytes()); - disk[at + 40..at + 48].copy_from_slice(&e.last.to_le_bytes()); - } - let backup_array_crc = crc32(&disk[backup_array_at..backup_array_at + array_bytes]); - - let mut hb = vec![0u8; lba]; - hb[..8].copy_from_slice(b"EFI PART"); - hb[8..12].copy_from_slice(&self.revision.to_le_bytes()); - hb[12..16].copy_from_slice(&self.header_bytes.to_le_bytes()); - hb[20..24].copy_from_slice(&self.reserved.to_le_bytes()); - hb[24..32].copy_from_slice(&backup_header_lba.to_le_bytes()); - hb[32..40].copy_from_slice(&1u64.to_le_bytes()); // AlternateLBA: the primary, at LBA 1. - hb[40..48].copy_from_slice(&self.first_usable.to_le_bytes()); - hb[48..56].copy_from_slice(&self.last_usable.to_le_bytes()); - hb[56..72].copy_from_slice(&guid(0x5D).0); - hb[72..80].copy_from_slice(&backup_array_lba.to_le_bytes()); - hb[80..84].copy_from_slice(&self.entry_count.to_le_bytes()); - hb[84..88].copy_from_slice(&self.entry_bytes.to_le_bytes()); - hb[88..92].copy_from_slice(&backup_array_crc.to_le_bytes()); - let hb_crc = crc32(&hb[..size]); - hb[16..20].copy_from_slice(&hb_crc.to_le_bytes()); - let backup_header_at = backup_header_lba as usize * lba; - disk[backup_header_at..backup_header_at + lba].copy_from_slice(&hb); - } - - Image { lba_bytes: self.lba_bytes, lba_count: self.lba_count, bytes: disk, fail_at: None } - } -} - -struct Image { - lba_bytes: u32, - lba_count: u64, - bytes: Vec, - fail_at: Option, +/// A backup that mirrors the primary as it stands. +fn mirrored(layout: &mut Layout) { + layout.backup = Some(layout.primary.mirror(layout.lba_bytes, layout.lba_count)); } impl Image { @@ -215,46 +88,23 @@ impl Image { fn locate_type( &mut self, target: Guid, - out: &mut [toyos_gpt::Partition], + out: &mut [Option], ) -> Result { toyos_gpt::locate_type(self, target, out) } } -impl Sectors for Image { - fn lba_bytes(&self) -> u32 { - self.lba_bytes - } - fn lba_count(&self) -> u64 { - self.lba_count - } - fn lba_count_granularity(&self) -> core::num::NonZeroU64 { - core::num::NonZeroU64::MIN - } - fn read_lba(&mut self, lba: u64, buf: &mut [u8]) -> bool { - if self.fail_at == Some(lba) { - return false; - } - let at = lba as usize * self.lba_bytes as usize; - let Some(src) = self.bytes.get(at..at + buf.len()) else { - return false; - }; - buf.copy_from_slice(src); - true - } -} - #[test] fn finds_the_partition_by_unique_guid() { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); let found = img.locate(guid(0xC3)).expect("the table has this GUID"); - assert_eq!(found.partition.index, 2); - assert_eq!(found.partition.first_lba, 200); - assert_eq!(found.partition.last_lba, 299); - assert_eq!(found.partition.lba_count(), 100); - assert_eq!(found.used_entries, 4); - assert!(found.partition.is_efi_system()); - assert_eq!(found.disk_guid, guid(0x5D)); + assert_eq!(found.partition().index(), 2); + assert_eq!(found.partition().first_lba(), 200); + assert_eq!(found.partition().last_lba(), 299); + assert_eq!(found.partition().lba_count().get(), 100); + assert_eq!(found.used_entries(), 4); + assert!(found.partition().is_efi_system()); + assert_eq!(found.disk_guid(), guid(0x5D)); } /// The one that matters: three of the four entries are ESPs, so anything @@ -269,9 +119,9 @@ fn each_guid_finds_its_own_entry() { (guid(0xD4), 3, 300, 1999), ]; for (g, index, first, last) in want { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); let found = img.locate(g).expect("present"); - assert_eq!((found.partition.index, found.partition.first_lba, found.partition.last_lba), (index, first, last)); + assert_eq!((found.partition().index(), found.partition().first_lba(), found.partition().last_lba()), (index, first, last)); } } @@ -280,55 +130,38 @@ fn each_guid_finds_its_own_entry() { /// gets a different set than the one asserted. #[test] fn a_type_scan_lists_every_entry_of_that_type() { - let mut img = Builder::default().build(); - let mut out = [toyos_gpt::Partition { - index: 0, - type_guid: Guid::ZERO, - unique_guid: Guid::ZERO, - first_lba: 0, - last_lba: 0, - }; 4]; + let mut img = disk(|_| {}); + let mut out = [None; 4]; let scan = img.locate_type(TYPE_ESP, &mut out).expect("the table parses"); - assert_eq!((scan.matched, scan.listed, scan.used_entries), (3, 3, 4)); + assert_eq!((scan.matched, scan.used_entries), (3, 4)); assert_eq!(scan.disk_guid, guid(0x5D)); - let found: Vec = out[..scan.listed].iter().map(|p| p.unique_guid).collect(); + let found: Vec = out.iter().flatten().flatten().map(|p| p.unique_guid()).collect(); assert_eq!(found, vec![guid(0xA1), guid(0xC3), guid(0xD4)]); // A type nothing carries is not an error; it is an empty set. let none = img.locate_type(guid(0x77), &mut out).expect("the table parses"); - assert_eq!((none.matched, none.listed), (0, 0)); + assert_eq!(none.matched, 0); + assert!(out.iter().all(Option::is_none)); } /// A slice too small does not truncate silently: the count of matches is the /// table's, not the caller's, so the caller can tell it was not shown them all. #[test] fn a_type_scan_says_how_many_it_could_not_hand_back() { - let mut img = Builder::default().build(); - let mut out = [toyos_gpt::Partition { - index: 0, - type_guid: Guid::ZERO, - unique_guid: Guid::ZERO, - first_lba: 0, - last_lba: 0, - }; 1]; + let mut img = disk(|_| {}); + let mut out = [None; 1]; let scan = img.locate_type(TYPE_ESP, &mut out).expect("the table parses"); - assert_eq!((scan.matched, scan.listed), (3, 1)); - assert_eq!(out[0].unique_guid, guid(0xA1)); + assert_eq!(scan.matched, 3); + assert_eq!(out[0].and_then(Result::ok).map(|p| p.unique_guid()), Some(guid(0xA1))); } /// The scan is held to the same CRC as the search: a damaged array yields no /// candidates at all, rather than the ones read before the checksum failed. #[test] fn a_type_scan_over_a_damaged_array_is_refused() { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); *img.at(ARRAY_LBA, 3) ^= 0x01; - let mut out = [toyos_gpt::Partition { - index: 0, - type_guid: Guid::ZERO, - unique_guid: Guid::ZERO, - first_lba: 0, - last_lba: 0, - }; 4]; + let mut out = [None; 4]; assert!(matches!( img.locate_type(TYPE_ESP, &mut out), Err(GptError::EntryArrayCrc { .. }) @@ -337,22 +170,22 @@ fn a_type_scan_over_a_damaged_array_is_refused() { #[test] fn absent_guid_is_not_found_and_says_how_many_there_were() { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); assert_eq!(img.locate(guid(0xEE)), Err(GptError::NotFound { used_entries: 4 })); } #[test] fn a_zero_guid_matches_nothing() { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); assert_eq!(img.locate(Guid::ZERO), Err(GptError::NotFound { used_entries: 4 })); } #[test] fn no_protective_mbr() { - let mut img = Builder { no_mbr_signature: true, ..Default::default() }.build(); + let mut img = disk(|l| l.mbr_signature = [0, 0]); assert_eq!(img.locate(guid(0xC3)), Err(GptError::NoProtectiveMbr)); - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); *img.at(0, 446 + 4) = 0x07; assert_eq!(img.locate(guid(0xC3)), Err(GptError::NoProtectiveMbr)); } @@ -360,46 +193,47 @@ fn no_protective_mbr() { /// A protective record next to a real one means two tables describe this disk. #[test] fn hybrid_mbr_is_refused() { - let mut img = Builder { hybrid_mbr: true, ..Default::default() }.build(); + let mut img = disk(|_| {}); + *img.at(0, 446 + 16 + 4) = 0x83; assert_eq!(img.locate(guid(0xC3)), Err(GptError::NoProtectiveMbr)); } #[test] fn header_signature() { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); *img.at(1, 0) = b'X'; assert_eq!(img.locate(guid(0xC3)), Err(GptError::NoHeader)); } #[test] fn header_revision() { - let mut img = Builder { revision: 0x0002_0000, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.revision = 0x0002_0000); assert_eq!(img.locate(guid(0xC3)), Err(GptError::UnsupportedRevision(0x0002_0000))); } #[test] fn header_size_bounds() { for bad in [0u32, 91, 513, u32::MAX] { - let mut img = Builder { header_bytes: bad, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.header_bytes = bad); assert_eq!(img.locate(guid(0xC3)), Err(GptError::HeaderSize(bad)), "header_size {bad}"); } } #[test] fn header_reserved_word() { - let mut img = Builder { reserved: 1, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.reserved = 1); assert_eq!(img.locate(guid(0xC3)), Err(GptError::HeaderReserved(1))); } #[test] fn header_must_claim_lba_one() { - let mut img = Builder { my_lba: 2, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.my_lba = 2); assert_eq!(img.locate(guid(0xC3)), Err(GptError::HeaderMisplaced(2))); } #[test] fn one_flipped_bit_in_the_header() { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); *img.at(1, 80) ^= 0x01; match img.locate(guid(0xC3)) { Err(GptError::HeaderCrc { .. }) => {} @@ -409,7 +243,7 @@ fn one_flipped_bit_in_the_header() { #[test] fn one_flipped_bit_in_the_entry_array() { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); *img.at(ARRAY_LBA, 32) ^= 0x01; match img.locate(guid(0xC3)) { Err(GptError::EntryArrayCrc { .. }) => {} @@ -422,18 +256,18 @@ fn one_flipped_bit_in_the_entry_array() { /// not be read as an entry either. #[test] fn the_array_ends_where_the_header_says() { - let mut img = Builder { entry_count: 3, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.entry_count = 3); // Entry 3 is now outside the array. It is still on the disk. assert_eq!(img.locate(guid(0xD4)), Err(GptError::NotFound { used_entries: 3 })); *img.at(ARRAY_LBA, 3 * 128 + 1) ^= 0xFF; let found = img.locate(guid(0xC3)).expect("still parses"); - assert_eq!(found.used_entries, 3); + assert_eq!(found.used_entries(), 3); } #[test] fn entry_size_must_be_a_power_of_two_multiple_of_128_that_fits_a_block() { for bad in [0u32, 1, 64, 127, 192, 1024, u32::MAX] { - let mut img = Builder { entry_bytes: bad, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.entry_bytes = bad); assert_eq!(img.locate(guid(0xC3)), Err(GptError::EntrySize(bad)), "entry size {bad}"); } } @@ -441,7 +275,7 @@ fn entry_size_must_be_a_power_of_two_multiple_of_128_that_fits_a_block() { #[test] fn a_billion_entries_is_refused_not_read() { for bad in [u32::MAX, 1_000_000_000, 1025] { - let mut img = Builder { entry_count: bad, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.entry_count = bad); assert_eq!( img.locate(guid(0xC3)), Err(GptError::EntryArrayTooBig { entries: bad, entry_size: ENTRY }), @@ -455,23 +289,20 @@ fn a_billion_entries_is_refused_not_read() { #[test] fn the_array_ceiling_is_where_it_says_it_is() { let at_ceiling = (toyos_gpt::MAX_ENTRY_ARRAY_BYTES / ENTRY as u64) as u32; - let over = Builder { entry_count: at_ceiling + 1, ..Default::default() }.build(); - let mut over = over; + let mut over = disk(|l| l.primary.entry_count = at_ceiling + 1); assert!(matches!(over.locate(guid(0xC3)), Err(GptError::EntryArrayTooBig { .. }))); - let mut ok = Builder { - entry_count: at_ceiling, - first_usable: 2 + toyos_gpt::MAX_ENTRY_ARRAY_BYTES / LBA as u64, - ..Default::default() - } - .build(); + let mut ok = disk(|l| { + l.primary.entry_count = at_ceiling; + l.primary.first_usable = 2 + toyos_gpt::MAX_ENTRY_ARRAY_BYTES / LBA as u64; + }); // Not TooBig: it is refused, if at all, for a different reason. assert!(!matches!(ok.locate(guid(0xC3)), Err(GptError::EntryArrayTooBig { .. }))); } #[test] fn zero_entries_is_refused() { - let mut img = Builder { entry_count: 0, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.entry_count = 0); assert_eq!( img.locate(guid(0xC3)), Err(GptError::EntryArrayTooBig { entries: 0, entry_size: ENTRY }) @@ -481,38 +312,38 @@ fn zero_entries_is_refused() { #[test] fn the_array_may_not_sit_on_the_header_or_past_the_usable_range() { for bad_lba in [0u64, 1] { - let mut img = Builder { entry_array_lba: bad_lba, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.entry_array_lba = bad_lba); assert!( matches!(img.locate(guid(0xC3)), Err(GptError::EntryArrayMisplaced { .. })), "array at LBA {bad_lba}" ); } // Starts legally, ends past the first usable block. - let mut img = Builder { first_usable: 20, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.first_usable = 20); assert!(matches!(img.locate(guid(0xC3)), Err(GptError::EntryArrayMisplaced { .. }))); } #[test] fn an_array_lba_near_the_top_of_the_range_does_not_wrap() { - let mut img = Builder { entry_array_lba: u64::MAX - 1, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.entry_array_lba = u64::MAX - 1); assert!(matches!(img.locate(guid(0xC3)), Err(GptError::EntryArrayMisplaced { .. }))); } #[test] fn usable_range_must_be_a_range_inside_the_device() { - let mut inverted = Builder { first_usable: 500, last_usable: 100, ..Default::default() }.build(); + let mut inverted = disk(|l| { l.primary.first_usable = 500; l.primary.last_usable = 100 }); assert_eq!( inverted.locate(guid(0xC3)), Err(GptError::UsableRange { first: 500, last: 100 }) ); - let mut past_end = Builder { last_usable: DISK_LBAS, ..Default::default() }.build(); + let mut past_end = disk(|l| l.primary.last_usable = DISK_LBAS); assert_eq!( past_end.locate(guid(0xC3)), Err(GptError::UsableRange { first: FIRST_USABLE, last: DISK_LBAS }) ); - let mut over_the_table = Builder { first_usable: 1, ..Default::default() }.build(); + let mut over_the_table = disk(|l| l.primary.first_usable = 1); assert_eq!( over_the_table.locate(guid(0xC3)), Err(GptError::UsableRange { first: 1, last: DISK_LBAS - FIRST_USABLE }) @@ -521,9 +352,7 @@ fn usable_range_must_be_a_range_inside_the_device() { #[test] fn a_partition_outside_the_disk_is_refused() { - let mut b = Builder::default(); - b.entries[2] = Entry::new(TYPE_ESP, guid(0xC3), 200, u64::MAX); - let mut img = b.build(); + let mut img = disk(|l| l.primary.entries[2] = entry(2, TYPE_ESP, guid(0xC3), 200, u64::MAX)); assert_eq!( img.locate(guid(0xC3)), Err(GptError::PartitionRange { first: 200, last: u64::MAX }) @@ -532,9 +361,7 @@ fn a_partition_outside_the_disk_is_refused() { #[test] fn a_backwards_partition_is_refused() { - let mut b = Builder::default(); - b.entries[2] = Entry::new(TYPE_ESP, guid(0xC3), 900, 800); - let mut img = b.build(); + let mut img = disk(|l| l.primary.entries[2] = entry(2, TYPE_ESP, guid(0xC3), 900, 800)); assert_eq!(img.locate(guid(0xC3)), Err(GptError::PartitionRange { first: 900, last: 800 })); } @@ -542,24 +369,18 @@ fn a_backwards_partition_is_refused() { /// the caller's next act is to write to it. #[test] fn a_partition_over_the_table_is_refused() { - let mut b = Builder::default(); - b.entries[2] = Entry::new(TYPE_ESP, guid(0xC3), 3, 299); - let mut img = b.build(); + let mut img = disk(|l| l.primary.entries[2] = entry(2, TYPE_ESP, guid(0xC3), 3, 299)); assert_eq!(img.locate(guid(0xC3)), Err(GptError::PartitionRange { first: 3, last: 299 })); } #[test] fn an_overlapping_neighbour_is_refused() { - let mut b = Builder::default(); - b.entries[3] = Entry::new(TYPE_OTHER, guid(0xD4), 250, 400); - let mut img = b.build(); + let mut img = disk(|l| l.primary.entries[3] = entry(3, TYPE_OTHER, guid(0xD4), 250, 400)); assert_eq!(img.locate(guid(0xC3)), Err(GptError::PartitionOverlap { index: 3 })); // And the overlap is found when it comes *before* the match too, which is // the case a single streaming pass would miss. - let mut b = Builder::default(); - b.entries[0] = Entry::new(TYPE_OTHER, guid(0xA1), 40, 250); - let mut img = b.build(); + let mut img = disk(|l| l.primary.entries[0] = entry(0, TYPE_OTHER, guid(0xA1), 40, 250)); assert_eq!(img.locate(guid(0xC3)), Err(GptError::PartitionOverlap { index: 0 })); } @@ -569,14 +390,14 @@ fn an_overlapping_neighbour_is_refused() { /// than refuse a disk that is otherwise fine. #[test] fn a_damaged_primary_falls_back_to_a_good_backup() { - let mut img = Builder { backup: true, ..Default::default() }.build(); + let mut img = disk(mirrored); *img.at(1, 0) = b'X'; let found = img.locate(guid(0xC3)).expect("the backup carries this GUID"); - assert_eq!(found.partition.index, 2); - assert_eq!(found.partition.first_lba, 200); - assert_eq!(found.partition.last_lba, 299); - assert_eq!(found.used_entries, 4); - assert_eq!(found.disk_guid, guid(0x5D)); + assert_eq!(found.partition().index(), 2); + assert_eq!(found.partition().first_lba(), 200); + assert_eq!(found.partition().last_lba(), 299); + assert_eq!(found.used_entries(), 4); + assert_eq!(found.disk_guid(), guid(0x5D)); } /// Both copies gone must be a named refusal, not a panic and not a made-up @@ -585,7 +406,7 @@ fn a_damaged_primary_falls_back_to_a_good_backup() { /// was unreadable. #[test] fn both_copies_damaged_is_refused_by_name() { - let mut img = Builder { backup: true, ..Default::default() }.build(); + let mut img = disk(mirrored); *img.at(1, 0) = b'X'; *img.at(DISK_LBAS - 1, 0) = b'X'; assert_eq!(img.locate(guid(0xC3)), Err(GptError::NoHeader)); @@ -596,14 +417,14 @@ fn both_copies_damaged_is_refused_by_name() { /// alone, so a `NotFound` is not in the set of errors this falls back on. #[test] fn a_valid_primary_that_lacks_the_guid_is_not_retried_against_the_backup() { - let mut img = Builder { backup: true, ..Default::default() }.build(); + let mut img = disk(mirrored); assert_eq!(img.locate(guid(0xEE)), Err(GptError::NotFound { used_entries: 4 })); } #[test] fn a_read_that_does_not_happen_is_an_error() { for lba in [0u64, 1, ARRAY_LBA] { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); img.fail_at = Some(lba); assert_eq!(img.locate(guid(0xC3)), Err(GptError::ReadFailed(lba))); } @@ -612,7 +433,7 @@ fn a_read_that_does_not_happen_is_an_error() { #[test] fn block_sizes_outside_the_supported_range() { for bad in [0u32, 128, 500, 8192, u32::MAX] { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); img.lba_bytes = bad; assert_eq!(img.locate(guid(0xC3)), Err(GptError::UnsupportedLbaSize(bad))); } @@ -620,27 +441,19 @@ fn block_sizes_outside_the_supported_range() { #[test] fn a_four_kibibyte_block_device_parses() { - let mut img = Builder { - lba_bytes: 4096, - lba_count: 512, - entry_array_lba: 2, - first_usable: 6, - last_usable: 500, - entries: vec![ - Entry::new(TYPE_OTHER, guid(0x11), 10, 20), - Entry::new(TYPE_ESP, guid(0x22), 21, 400), - ], - ..Default::default() - } - .build(); + let mut img = disk(|l| { + (l.lba_bytes, l.lba_count) = (4096, 512); + (l.primary.first_usable, l.primary.last_usable) = (6, 500); + l.primary.entries = vec![entry(0, TYPE_OTHER, guid(0x11), 10, 20), entry(1, TYPE_ESP, guid(0x22), 21, 400)]; + }); let found = img.locate(guid(0x22)).expect("present"); - assert_eq!((found.partition.index, found.partition.first_lba), (1, 21)); - assert_eq!(found.used_entries, 2); + assert_eq!((found.partition().index(), found.partition().first_lba()), (1, 21)); + assert_eq!(found.used_entries(), 2); } #[test] fn a_device_with_no_room_for_a_table() { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); img.lba_count = 2; assert_eq!(img.locate(guid(0xC3)), Err(GptError::DeviceTooSmall(2))); } @@ -651,7 +464,7 @@ fn a_device_with_no_room_for_a_table() { /// simply *return*. #[test] fn no_byte_of_the_table_can_panic_the_parser() { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); let reach = (ARRAY_LBA as usize + 32) * LBA as usize; let mut located = 0; for at in 0..reach { @@ -675,9 +488,11 @@ fn no_byte_of_the_table_can_panic_the_parser() { /// exact reader concedes none of them for a coarser reader it does not have. #[test] fn a_usable_range_reaching_the_backup_gpt_is_refused() { - let mut b = Builder { last_usable: DISK_LBAS - 2, backup: true, ..Default::default() }; - b.entries[3] = Entry::new(TYPE_ESP, guid(0xD4), 300, DISK_LBAS - 2); - let mut img = b.build(); + let mut img = disk(|l| { + l.primary.last_usable = DISK_LBAS - 2; + l.primary.entries[3] = entry(3, TYPE_ESP, guid(0xD4), 300, DISK_LBAS - 2); + mirrored(l); + }); assert_eq!( img.locate(guid(0xD4)), Err(GptError::UsableRangeCoversBackup { @@ -686,9 +501,9 @@ fn a_usable_range_reaching_the_backup_gpt_is_refused() { }) ); - let mut img = Builder { last_usable: DISK_LBAS - 34, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.last_usable = DISK_LBAS - 34); img.locate(guid(0xC3)).expect("the last usable LBA below the mirror was refused"); - let mut img = Builder { last_usable: DISK_LBAS - 33, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.last_usable = DISK_LBAS - 33); assert_eq!( img.locate(guid(0xC3)), Err(GptError::UsableRangeCoversBackup { @@ -704,32 +519,14 @@ fn a_usable_range_reaching_the_backup_gpt_is_refused() { /// bound's edge, must parse. The unconceded bound refused every such disk. #[test] fn an_honest_table_on_a_floored_device_view_parses() { - struct Floored(Image, u64); - impl Sectors for Floored { - fn lba_bytes(&self) -> u32 { - self.0.lba_bytes() - } - fn lba_count(&self) -> u64 { - self.1 - } - fn lba_count_granularity(&self) -> core::num::NonZeroU64 { - core::num::NonZeroU64::new(8).expect("8 is nonzero") - } - fn read_lba(&mut self, lba: u64, buf: &mut [u8]) -> bool { - self.0.read_lba(lba, buf) - } - } - - let img = Builder { - lba_count: 2055, - last_usable: 2055 - 34, - backup: true, - ..Default::default() - } - .build(); - let mut floored = Floored(img, 2048); - let found = toyos_gpt::locate(&mut floored, guid(0xC3)).expect("an honest disk lost /boot"); - assert_eq!(found.partition.index, 2); + let mut img = disk(|l| { + l.lba_count = 2055; + l.primary.last_usable = 2055 - 34; + mirrored(l); + }); + (img.lba_count, img.granularity) = (2048, 8); + let found = img.locate(guid(0xC3)).expect("an honest disk lost /boot"); + assert_eq!(found.partition().index(), 2); } /// UEFI gives every entry a `UniquePartitionGUID` that must be unique. Two @@ -737,24 +534,23 @@ fn an_honest_table_on_a_floored_device_view_parses() { /// first-wins — either one could be the partition the firmware meant. #[test] fn two_entries_claiming_the_target_guid_are_refused() { - let mut b = Builder::default(); - b.entries[3] = Entry::new(TYPE_OTHER, guid(0xC3), 300, 1999); - let mut img = b.build(); + let mut img = disk(|l| l.primary.entries[3] = entry(3, TYPE_OTHER, guid(0xC3), 300, 1999)); assert_eq!( img.locate(guid(0xC3)), Err(GptError::DuplicateUniqueGuid { first: 2, second: 3 }) ); // A duplicate of a GUID nobody asked for does not refuse the answer. - assert_eq!(img.locate(guid(0xB2)).map(|f| f.partition.index), Ok(1)); + assert_eq!(img.locate(guid(0xB2)).map(|f| f.partition().index()), Ok(1)); } /// `entry_count` is the table's own byte: 8 entries make a 2-LBA array, whose /// first block remains the usable range's exact ceiling. #[test] fn a_tiny_entry_array_cannot_buy_the_backup_header() { - let mut b = Builder { entry_count: 8, last_usable: DISK_LBAS - 1, ..Default::default() }; - b.entries[3] = Entry::new(TYPE_ESP, guid(0xD4), 300, DISK_LBAS - 1); - let mut img = b.build(); + let mut img = disk(|l| { + (l.primary.entry_count, l.primary.last_usable) = (8, DISK_LBAS - 1); + l.primary.entries[3] = entry(3, TYPE_ESP, guid(0xD4), 300, DISK_LBAS - 1); + }); assert_eq!( img.locate(guid(0xD4)), Err(GptError::UsableRangeCoversBackup { @@ -768,19 +564,38 @@ fn a_tiny_entry_array_cannot_buy_the_backup_header() { /// ones, whatever its type. #[test] fn a_list_is_every_used_entry_in_order() { - let mut img = Builder::default().build(); - let mut out = [toyos_gpt::Partition { - index: 0, - type_guid: Guid::ZERO, - unique_guid: Guid::ZERO, - first_lba: 0, - last_lba: 0, - }; 8]; + let mut img = disk(|_| {}); + let mut out = [None; 8]; let scan = toyos_gpt::list(&mut img, &mut out).expect("the table parses"); - assert_eq!((scan.matched, scan.listed, scan.used_entries), (4, 4, 4)); - let found: Vec<(u32, Guid)> = out[..scan.listed].iter().map(|p| (p.index, p.unique_guid)).collect(); + assert_eq!((scan.matched, scan.used_entries), (4, 4)); + let found: Vec<(u32, Guid)> = out.iter().flatten().flatten().map(|p| (p.index(), p.unique_guid())).collect(); assert_eq!( found, vec![(0, guid(0xA1)), (1, guid(0xB2)), (2, guid(0xC3)), (3, guid(0xD4))] ); } + +/// A scan clears the caller's slice before it fills it: every slot it did not +/// fill is `None`, whatever the caller left there. +#[test] +fn a_list_leaves_no_slot_it_did_not_fill() { + let mut img = disk(|_| {}); + let bogus = Stated { index: 99, type_guid: TYPE_ESP, unique_guid: guid(0xEE), first: 1, last: 0 }; + let mut out = [Some(Err(bogus)); 8]; + toyos_gpt::list(&mut img, &mut out).expect("the table parses"); + assert_eq!(out.iter().flatten().count(), 4); +} + +/// A primary whose array CRC fails is walked before the failure is known, and +/// retried against the backup: nothing the primary's walk put in the slice +/// survives the retry. +#[test] +fn a_backup_retry_leaves_no_slot_of_the_primary() { + let mut img = disk(mirrored); + // A fifth used entry, in the primary's array only, so its CRC fails. + *img.at(ARRAY_LBA, 4 * 128) = 0x01; + let mut out = [None; 8]; + let scan = toyos_gpt::list(&mut img, &mut out).expect("the backup parses"); + assert_eq!(scan.used_entries, 4); + assert_eq!(out.iter().flatten().count(), 4); +} diff --git a/toyos-gpt/tests/table/mod.rs b/toyos-gpt/tests/table/mod.rs new file mode 100644 index 0000000000..4d1c0ebd48 --- /dev/null +++ b/toyos-gpt/tests/table/mod.rs @@ -0,0 +1,410 @@ +//! Seeded GPT layouts, value mutations of them, and images with both CRC32s resealed. + +#![allow(dead_code)] + +use toyos_gpt::{crc32, Guid, Sectors}; + +/// splitmix64, so a red run is reproducible from its seed. +pub struct Rng(u64); + +impl Rng { + pub fn new(seed: u64) -> Self { + Self(seed) + } + + pub fn next(&mut self) -> u64 { + self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) + } + + /// Uniform in `0..n`; `n` is never zero here. + pub fn below(&mut self, n: u64) -> u64 { + self.next() % n + } + + pub fn range(&mut self, lo: u64, hi: u64) -> u64 { + lo + self.below(hi - lo + 1) + } + + pub fn one_in(&mut self, n: u64) -> bool { + self.below(n) == 0 + } + + pub fn pick(&mut self, from: &[T]) -> T { + from[self.below(from.len() as u64) as usize] + } + + pub fn guid(&mut self) -> Guid { + let mut b = [0u8; 16]; + b[..8].copy_from_slice(&self.next().to_le_bytes()); + b[8..].copy_from_slice(&self.next().to_le_bytes()); + b[0] |= 1; + Guid(b) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct RawEntry { + pub index: u32, + pub type_guid: Guid, + pub unique: Guid, + pub first: u64, + pub last: u64, +} + +/// One copy of the table, every field as the image will state it. +#[derive(Clone, Debug)] +pub struct Table { + pub revision: u32, + pub header_bytes: u32, + pub reserved: u32, + pub my_lba: u64, + pub first_usable: u64, + pub last_usable: u64, + pub disk_guid: Guid, + pub entry_array_lba: u64, + pub entry_count: u32, + pub entry_bytes: u32, + /// The used entries, at their own indices. + pub entries: Vec, +} + +impl Table { + /// This copy as the backup of a disk of `lba_count` blocks of `lba_bytes` states it. + pub fn mirror(&self, lba_bytes: u32, lba_count: u64) -> Table { + let array_lbas = (u64::from(self.entry_count) * u64::from(self.entry_bytes)).div_ceil(u64::from(lba_bytes)); + Table { my_lba: lba_count - 1, entry_array_lba: lba_count - 1 - array_lbas, ..self.clone() } + } +} + +/// A disk: its geometry, the primary copy, and the backup where it has one. +#[derive(Clone, Debug)] +pub struct Layout { + pub lba_bytes: u32, + pub lba_count: u64, + pub primary: Table, + pub backup: Option, + /// What the device answers for its block count, and the granularity it floors by. + pub reported_lba_count: u64, + pub granularity: u64, + pub mbr_type: u8, + pub mbr_signature: [u8; 2], +} + +/// What a generated layout may be. +pub struct Shape<'a> { + pub lba_sizes: &'a [u32], + pub types: &'a [Guid], + pub entry_sizes: &'a [u32], + /// Some layouts carry no backup copy. + pub backup: bool, + /// Some devices floor their block count, as the kernel's 4 KiB reader does. + pub floored: bool, +} + +/// A table UEFI 2.11 §5.3 accepts, with up to eight disjoint partitions at random indices. +pub fn valid(rng: &mut Rng, shape: &Shape<'_>) -> Layout { + let lba_bytes = rng.pick(shape.lba_sizes); + let entry_bytes = rng.pick(shape.entry_sizes).min(lba_bytes); + let entry_count = if rng.one_in(3) { 128 } else { rng.range(1, 160) as u32 }; + let array_lbas = (u64::from(entry_count) * u64::from(entry_bytes)).div_ceil(u64::from(lba_bytes)); + let first_usable = 2 + array_lbas + rng.below(3); + let data = if lba_bytes == 512 { rng.range(8, 400) } else { rng.range(8, 48) }; + let lba_count = first_usable + data + array_lbas + 1; + let last_usable = lba_count - 2 - array_lbas; + + let used = rng.below(u64::from(entry_count.min(8)) + 1) as usize; + let mut cuts: Vec = (0..2 * used).map(|_| rng.range(first_usable, last_usable)).collect(); + cuts.sort_unstable(); + let mut indices: Vec = Vec::new(); + while indices.len() < used { + let i = rng.below(u64::from(entry_count)) as u32; + if !indices.contains(&i) { + indices.push(i); + } + } + // Disjoint: each partition starts past the previous one's end. + let mut entries = Vec::new(); + let mut floor = first_usable; + for (k, index) in indices.into_iter().enumerate() { + let first = cuts[2 * k].max(floor); + let last = cuts[2 * k + 1].max(first); + if last > last_usable { + break; + } + floor = last + 1; + entries.push(RawEntry { index, type_guid: rng.pick(shape.types), unique: rng.guid(), first, last }); + } + + let primary = Table { + revision: 0x0001_0000, + header_bytes: 92, + reserved: 0, + my_lba: 1, + first_usable, + last_usable, + disk_guid: rng.guid(), + entry_array_lba: 2, + entry_count, + entry_bytes, + entries, + }; + let backup = (shape.backup && !rng.one_in(3)).then(|| primary.mirror(lba_bytes, lba_count)); + let granularity = if shape.floored && lba_bytes == 512 && rng.one_in(4) { 8 } else { 1 }; + Layout { + lba_bytes, + lba_count, + primary, + backup, + reported_lba_count: lba_count / granularity * granularity, + granularity, + mbr_type: 0xEE, + mbr_signature: [0x55, 0xAA], + } +} + +/// Values at the edges of what `t` and the disk make meaningful, and past them. +fn edge(rng: &mut Rng, t: &Table, lba_count: u64) -> u64 { + let (fu, lu) = (t.first_usable, t.last_usable); + let near = [ + 0, + 1, + 2, + fu.wrapping_sub(1), + fu, + fu.wrapping_add(1), + lu.wrapping_sub(1), + lu, + lu.wrapping_add(1), + lba_count.wrapping_sub(2), + lba_count.wrapping_sub(1), + lba_count, + lba_count.wrapping_add(1), + u64::from(u32::MAX), + u64::MAX - 1, + u64::MAX, + ]; + match rng.below(4) { + 0 => rng.below(lba_count.max(1)), + 1 => rng.next(), + _ => rng.pick(&near), + } +} + +/// Bend one header field or one entry's value. +fn mutate_table(rng: &mut Rng, t: &mut Table, lba_bytes: u32, lba_count: u64) { + match rng.below(14) { + 0 => t.first_usable = edge(rng, t, lba_count), + 1 => t.last_usable = edge(rng, t, lba_count), + 2 => t.entry_array_lba = edge(rng, t, lba_count), + 3 => { + let random = rng.next() as u32; + t.entry_count = rng.pick(&[0, 1, 2, 3, 4, 7, 8, 127, 128, 129, 1024, 1025, u32::MAX, random]) + } + 4 => t.entry_bytes = rng.pick(&[0, 1, 64, 127, 128, 192, 256, 512, 1024, 4096, 8192, u32::MAX]), + 5 => t.header_bytes = rng.pick(&[0, 91, 92, 93, 128, lba_bytes, lba_bytes + 1, u32::MAX]), + 6 => t.my_lba = rng.pick(&[0, 1, 2, lba_count - 1, lba_count, u64::MAX]), + 7 => t.revision = rng.pick(&[0x0001_0000, 0x0001_0001, 0, u32::MAX]), + 8 => t.reserved = rng.pick(&[0, 1, u32::MAX]), + _ => mutate_entry(rng, t, lba_count), + } +} + +fn mutate_entry(rng: &mut Rng, t: &mut Table, lba_count: u64) { + if t.entries.is_empty() || rng.one_in(6) { + let index = rng.below(u64::from(t.entry_count.clamp(1, 256))) as u32; + t.entries.retain(|e| e.index != index); + let (first, last) = (edge(rng, t, lba_count), edge(rng, t, lba_count)); + t.entries.push(RawEntry { index, type_guid: Guid::EFI_SYSTEM, unique: rng.guid(), first, last }); + return; + } + let k = rng.below(t.entries.len() as u64) as usize; + let other = t.entries[rng.below(t.entries.len() as u64) as usize]; + let value = edge(rng, t, lba_count); + let e = &mut t.entries[k]; + match rng.below(9) { + 0 => e.first = value, + 1 => e.last = value, + // The finding's own shape, and its `+ 1` twin. + 2 => (e.first, e.last) = (e.last, e.first), + 3 => (e.first, e.last) = (e.last.wrapping_add(1), e.last), + 4 => (e.first, e.last) = (0, u64::MAX), + // Onto a neighbour's blocks. + 5 => (e.first, e.last) = (other.last, other.last.max(e.last)), + 6 => e.unique = other.unique, + 7 => e.type_guid = Guid::ZERO, + _ => e.type_guid = Guid::TOYOS_DATA, + } +} + +/// Bend 1 to 3 values of the layout, mostly of the primary copy. +pub fn mutate(rng: &mut Rng, layout: &mut Layout) { + for _ in 0..rng.range(1, 3) { + let (lba_bytes, lba_count) = (layout.lba_bytes, layout.lba_count); + match rng.below(40) { + 0 | 1 => layout.reported_lba_count = edge(rng, &layout.primary, lba_count).min(lba_count + 64), + 2 => layout.mbr_type = rng.pick(&[0x00, 0x07]), + 3 => layout.mbr_signature = [0x55, 0xAB], + 4..=11 if layout.backup.is_some() => { + let backup = layout.backup.as_mut().expect("checked"); + mutate_table(rng, backup, lba_bytes, lba_count) + } + _ => mutate_table(rng, &mut layout.primary, lba_bytes, lba_count), + } + } +} + +/// The image `layout` states, with both copies' CRCs computed over it. +pub fn image(layout: &Layout) -> Image { + let lba = layout.lba_bytes as usize; + let mut disk = vec![0u8; lba * layout.lba_count as usize]; + disk[446 + 4] = layout.mbr_type; + disk[446 + 8..446 + 12].copy_from_slice(&1u32.to_le_bytes()); + disk[446 + 12..446 + 16].copy_from_slice(&u32::try_from(layout.lba_count - 1).unwrap_or(u32::MAX).to_le_bytes()); + disk[510..512].copy_from_slice(&layout.mbr_signature); + + // Each copy at its own place whatever its `my_lba` says; the primary last. + let copies: Vec<(u64, &Table)> = + layout.backup.iter().map(|b| (layout.lba_count - 1, b)).chain([(1, &layout.primary)]).collect(); + for (_, t) in &copies { + let stride = (t.entry_bytes as usize).max(128); + for e in &t.entries { + let at = (t.entry_array_lba as usize) + .checked_mul(lba) + .and_then(|a| a.checked_add(e.index as usize * stride)); + let Some(slot) = at.and_then(|at| disk.get_mut(at..at + 48)) else { continue }; + slot[..16].copy_from_slice(&e.type_guid.0); + slot[16..32].copy_from_slice(&e.unique.0); + slot[32..40].copy_from_slice(&e.first.to_le_bytes()); + slot[40..48].copy_from_slice(&e.last.to_le_bytes()); + } + } + for &(header_lba, t) in &copies { + let at = header_lba as usize * lba; + let mut h = vec![0u8; lba]; + h[..8].copy_from_slice(b"EFI PART"); + h[8..12].copy_from_slice(&t.revision.to_le_bytes()); + h[12..16].copy_from_slice(&t.header_bytes.to_le_bytes()); + h[20..24].copy_from_slice(&t.reserved.to_le_bytes()); + h[24..32].copy_from_slice(&t.my_lba.to_le_bytes()); + let alternate = if header_lba == 1 { layout.lba_count - 1 } else { 1 }; + h[32..40].copy_from_slice(&alternate.to_le_bytes()); + h[40..48].copy_from_slice(&t.first_usable.to_le_bytes()); + h[48..56].copy_from_slice(&t.last_usable.to_le_bytes()); + h[56..72].copy_from_slice(&t.disk_guid.0); + h[72..80].copy_from_slice(&t.entry_array_lba.to_le_bytes()); + h[80..84].copy_from_slice(&t.entry_count.to_le_bytes()); + h[84..88].copy_from_slice(&t.entry_bytes.to_le_bytes()); + let array_bytes = u64::from(t.entry_count) * u64::from(t.entry_bytes); + let array = (t.entry_array_lba as usize) + .checked_mul(lba) + .zip(usize::try_from(array_bytes).ok()) + .and_then(|(at, len)| disk.get(at..at.checked_add(len)?)); + if let Some(array) = array { + h[88..92].copy_from_slice(&crc32(array).to_le_bytes()); + } + if let Some(covered) = h.get(..t.header_bytes as usize) { + let crc = crc32(covered); + h[16..20].copy_from_slice(&crc.to_le_bytes()); + } + disk[at..at + lba].copy_from_slice(&h); + } + Image { + lba_bytes: layout.lba_bytes, + lba_count: layout.reported_lba_count, + granularity: layout.granularity, + bytes: disk, + fail_at: None, + } +} + +pub struct Image { + pub lba_bytes: u32, + pub lba_count: u64, + pub granularity: u64, + pub bytes: Vec, + /// The one block whose read does not happen. + pub fail_at: Option, +} + +impl Sectors for Image { + fn lba_bytes(&self) -> u32 { + self.lba_bytes + } + fn lba_count(&self) -> u64 { + self.lba_count + } + fn lba_count_granularity(&self) -> core::num::NonZeroU64 { + core::num::NonZeroU64::new(self.granularity).expect("1 or 8") + } + fn read_lba(&mut self, lba: u64, buf: &mut [u8]) -> bool { + if self.fail_at == Some(lba) { + return false; + } + let at = (lba as usize).checked_mul(self.lba_bytes as usize); + match at.and_then(|at| self.bytes.get(at..at.checked_add(buf.len())?)) { + Some(src) => { + buf.copy_from_slice(src); + true + } + None => false, + } + } +} + +/// One copy of the table as this file reads it, trusting nothing the parser decided. +#[derive(Debug)] +pub struct OnDisk { + pub first_usable: u64, + pub last_usable: u64, + pub entries: Vec, +} + +impl OnDisk { + /// Every copy an image carries that this file can read. + pub fn both(img: &Image) -> Vec { + let lba = img.lba_bytes as usize; + let last = img.bytes.len() / lba - 1; + [1, last].into_iter().filter_map(|at| OnDisk::at(&img.bytes, lba, at)).collect() + } + + fn at(disk: &[u8], lba: usize, header_lba: usize) -> Option { + let h = disk.get(header_lba * lba..(header_lba + 1) * lba)?; + if &h[..8] != b"EFI PART" { + return None; + } + let u64_at = |b: &[u8], at: usize| u64::from_le_bytes(b[at..at + 8].try_into().expect("8")); + let u32_at = |b: &[u8], at: usize| u32::from_le_bytes(b[at..at + 4].try_into().expect("4")); + let (count, size) = (u32_at(h, 80) as usize, u32_at(h, 84) as usize); + if size < 48 || count.checked_mul(size)? > 1 << 20 { + return None; + } + let array_at = usize::try_from(u64_at(h, 72)).ok()?.checked_mul(lba)?; + let array = disk.get(array_at..array_at.checked_add(count * size)?)?; + let entries = array + .chunks_exact(size) + .enumerate() + .map(|(i, e)| RawEntry { + index: i as u32, + type_guid: Guid(e[..16].try_into().expect("16")), + unique: Guid(e[16..32].try_into().expect("16")), + first: u64_at(e, 32), + last: u64_at(e, 40), + }) + .filter(|e| !e.type_guid.is_zero()) + .collect(); + Some(OnDisk { first_usable: u64_at(h, 40), last_usable: u64_at(h, 48), entries }) + } + + pub fn entry(&self, index: u32) -> Option<&RawEntry> { + self.entries.iter().find(|e| e.index == index) + } + + /// Whether `e`'s blocks are a range inside this copy's usable blocks. + pub fn places(&self, e: &RawEntry) -> bool { + self.first_usable <= e.first && e.first <= e.last && e.last <= self.last_usable + } +} diff --git a/userland/blockd/src/main.rs b/userland/blockd/src/main.rs index c6a18a9319..1a5d0802d9 100644 --- a/userland/blockd/src/main.rs +++ b/userland/blockd/src/main.rs @@ -125,37 +125,34 @@ impl toyos_gpt::Sectors for Sectors<'_> { fn read_table(ctrl: &mut Controller) -> Vec { let per = BLOCK_BYTES as u64 / ctrl.lba_bytes as u64; let mut sectors = Sectors { ctrl, block: vec![0u8; BLOCK_BYTES] }; - const BLANK: toyos_gpt::Partition = toyos_gpt::Partition { - index: 0, - type_guid: toyos_gpt::Guid([0; 16]), - unique_guid: toyos_gpt::Guid([0; 16]), - first_lba: 0, - last_lba: 0, - }; - let mut found = [BLANK; 128]; - let scan = match toyos_gpt::list(&mut sectors, &mut found) { - Ok(scan) => scan, - Err(why) => { - println!("blockd: the disk carries no partition table this driver reads: {why:?}"); - return Vec::new(); - } - }; + let mut found = [None; 128]; + if let Err(why) = toyos_gpt::list(&mut sectors, &mut found) { + println!("blockd: the disk carries no partition table this driver reads: {why:?}"); + return Vec::new(); + } let mut parts = Vec::new(); - for listed in &found[..scan.listed] { - let unique = listed.unique_guid.0; - let span = match toyos_gpt::locate(&mut sectors, listed.unique_guid) { + for entry in found.iter().flatten() { + let listed = match entry { + Ok(listed) => listed, + Err(unplaced) => { + let span = Err(format!("its blocks are no partition on this disk: {unplaced:?}")); + parts.push(Part { unique: unplaced.unique_guid.0, span }); + continue; + } + }; + let unique = listed.unique_guid().0; + let span = match toyos_gpt::locate(&mut sectors, listed.unique_guid()) { Err(why) => Err(format!("its own table refuses it: {why:?}")), Ok(located) => { - let p = located.partition; - if p.first_lba % per != 0 || p.lba_count() % per != 0 { + let p = located.partition(); + let (first, count) = (p.first_lba(), p.lba_count().get()); + if first % per != 0 || count % per != 0 { Err(format!( - "LBA {}+{} is not whole 4 KiB blocks of {}-byte sectors", - p.first_lba, - p.lba_count(), + "LBA {first}+{count} is not whole 4 KiB blocks of {}-byte sectors", sectors.ctrl.lba_bytes )) } else { - Ok((p.first_lba / per, p.lba_count() / per)) + Ok((first / per, count / per)) } } };