diff --git a/Cargo.lock b/Cargo.lock index ead84e08d33..4173d037315 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -990,9 +990,9 @@ version = "0.0.0" dependencies = [ "toyos", "toyos-abi", + "toyos-blockdev", "toyos-blockring", "toyos-blockserve", - "toyos-disk", ] [[package]] @@ -1300,6 +1300,7 @@ dependencies = [ "diskserver", "toyos", "toyos-abi", + "toyos-blockdev", "toyos-blockring", "toyos-fat32", "toyos-fat32-check", @@ -4045,6 +4046,7 @@ dependencies = [ "diskserver", "toyos", "toyos-abi", + "toyos-blockdev", "toyos-blockring", "toyos-gpt", "toyos-logstream", @@ -4385,6 +4387,10 @@ dependencies = [ name = "toyos-blackbox" version = "0.1.0" +[[package]] +name = "toyos-blockdev" +version = "0.1.0" + [[package]] name = "toyos-blockhold" version = "0.1.0" @@ -4436,6 +4442,7 @@ dependencies = [ "toml", "toyos-abi", "toyos-blackbox", + "toyos-blockdev", "toyos-crypto", "toyos-elf", "toyos-fat32", @@ -4509,10 +4516,6 @@ dependencies = [ "toyos-net-wire", ] -[[package]] -name = "toyos-disk" -version = "0.1.0" - [[package]] name = "toyos-dma" version = "0.1.0" @@ -4541,6 +4544,7 @@ name = "toyos-fat32" version = "0.1.0" dependencies = [ "fatfs", + "toyos-blockdev", "toyos-fat32-check", "toyos-tmpdir", "toyos-wallclock", @@ -5086,8 +5090,8 @@ version = "0.1.0" dependencies = [ "diskserver", "toyos", + "toyos-blockdev", "toyos-crypto", - "toyos-disk", "toyos-fat32", "toyos-update", ] @@ -5096,8 +5100,8 @@ dependencies = [ name = "update-copies" version = "0.1.0" dependencies = [ + "toyos-blockdev", "toyos-crypto", - "toyos-disk", "toyos-fat32", "toyos-fat32-check", "toyos-update", diff --git a/Cargo.toml b/Cargo.toml index 5f946a9f75e..69a0dc6e519 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,13 +38,13 @@ members = [ "lib/acpiserver-api", "lib/bcachefs", "lib/blackbox", + "lib/blockdev", "lib/blockhold", "lib/blockring", "lib/blockserve", "lib/bootmap", "lib/crypto", "lib/device-memory", - "lib/disk", "lib/elf", "lib/edid", "lib/elide", @@ -220,6 +220,9 @@ toyos-symbols = { path = "lib/symbols" } # `llvm-objdump`, is a C++ tool where a Rust one does the job. iced-x86 = { version = "1.21", default-features = false, features = ["std", "decoder", "instr_info"] } toyos-fat32 = { path = "lib/fat32" } +# The device `toyos-fat32` writes and reads a volume through: here, the bytes +# being built. +toyos-blockdev = { path = "lib/blockdev" } # The names the loader looks a CPU's microcode up by, which `microcode` writes # each patch it splits out of a vendor's file under. toyos-microcode = { path = "lib/microcode" } diff --git a/build/bin/imgstat.rs b/build/bin/imgstat.rs index a496995ec5b..4ad45a56545 100644 --- a/build/bin/imgstat.rs +++ b/build/bin/imgstat.rs @@ -12,34 +12,8 @@ use std::collections::BTreeMap; use std::path::PathBuf; use bcachefs::{Mounted, VecBlockIO}; -use toyos_fat32::{BlockAccess, Fat32, IoError}; - -/// A partition, in memory, byte-addressed. Read-only: this reports. -struct Volume(Vec); - -impl BlockAccess for Volume { - fn capacity(&self) -> u64 { - self.0.len() as u64 - } - - fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<(), IoError> { - let start = usize::try_from(offset).map_err(|_| IoError::Device)?; - let end = start.checked_add(buf.len()).ok_or(IoError::Device)?; - if end > self.0.len() { - return Err(IoError::Device); - } - buf.copy_from_slice(&self.0[start..end]); - Ok(()) - } - - fn write_at(&mut self, _offset: u64, _buf: &[u8]) -> Result<(), IoError> { - Err(IoError::Device) - } - - fn flush(&mut self) -> Result<(), IoError> { - Ok(()) - } -} +use toyos_blockdev::{block_on_ready, Bytes, BLOCK}; +use toyos_fat32::Fat32; /// GPT geometry, as `build/image.rs` writes it. const LBA: usize = 512; @@ -74,9 +48,15 @@ fn main() { (last - first + 1) * LBA as u64 ); - let bytes = disk[first as usize * LBA..(last as usize + 1) * LBA].to_vec(); - if let Ok(mut fs) = Fat32::mount(Volume(bytes.clone())) { - match fs.walk("", 64) { + let mut bytes = disk[first as usize * LBA..(last as usize + 1) * LBA].to_vec(); + let walked = bytes.len().is_multiple_of(BLOCK).then(|| { + block_on_ready(async { + let mut fs = Fat32::mount(Bytes::new(&mut bytes[..])).await.ok()?; + Some(fs.walk("", 64).await) + }) + }); + if let Some(Some(walked)) = walked { + match walked { Ok(entries) => { for (file, size) in &entries { println!(" {file:40} {size}"); diff --git a/build/build.rs b/build/build.rs index 8c57267ccbe..1ec8ed0f7af 100644 --- a/build/build.rs +++ b/build/build.rs @@ -2769,7 +2769,9 @@ mod tests { /// negative gates select at run time, `toyos-crypto`'s `portable`, `wpa2` /// and `rustls-provider`, which `--ci host` tests as forms of its own, and /// of the guest libraries, `toyos-font`'s `ttf`, its TrueType rendering, - /// and `toyos-pci-claim`'s `host-grant`, which netstack's host tests build. + /// `toyos-pci-claim`'s `host-grant`, which netstack's host tests build, and + /// `toyos-blockdev`'s `model`, the crash model a filesystem's tests check + /// its writes against, which the crate's own tests build regardless. const NOT_A_CONTROL: &[&str] = &[ "core", "default", @@ -2778,6 +2780,7 @@ mod tests { "flaws", "host-grant", "loom", + "model", "portable", "rustc-dep-of-std", "rustls-provider", diff --git a/build/fatformat.rs b/build/fatformat.rs index b1219263b52..edcd86c4760 100644 --- a/build/fatformat.rs +++ b/build/fatformat.rs @@ -180,13 +180,15 @@ mod tests { fn an_empty_volume_mounts_free_and_checks_clean() { for bytes in SIZES { let mut volume = format(bytes, *b"TOYOS-LOG "); - let mut fs = toyos_fat32::Fat32::mount(crate::image::VolumeIo(&mut volume)).expect("it mounts"); - assert!(fs.walk("", 8).expect("it walks").is_empty(), "{bytes}: an empty volume lists files"); - let geometry = *fs.geometry(); - let cluster = u64::from(geometry.bytes_per_cluster()); - assert_eq!(fs.free_bytes().expect("it counts"), (u64::from(geometry.cluster_count) - 1) * cluster, "{bytes}"); - fs.sync().expect("it records the count"); - drop(fs); + toyos_blockdev::block_on_ready(async { + let mut fs = toyos_fat32::Fat32::mount(toyos_blockdev::Bytes::new(&mut volume[..])).await.expect("it mounts"); + assert!(fs.walk("", 8).await.expect("it walks").is_empty(), "{bytes}: an empty volume lists files"); + let geometry = *fs.geometry(); + let cluster = u64::from(geometry.bytes_per_cluster()); + let free = fs.free_bytes().await.expect("it counts"); + assert_eq!(free, (u64::from(geometry.cluster_count) - 1) * cluster, "{bytes}"); + fs.sync().await.expect("it records the count"); + }); let complaints = toyos_fat32_check::check(&volume); assert!(complaints.is_empty(), "{bytes}:\n{}", toyos_fat32_check::describe(&complaints)); } diff --git a/build/image.rs b/build/image.rs index 3c44d0af849..19e894cb1a1 100644 --- a/build/image.rs +++ b/build/image.rs @@ -5,7 +5,8 @@ use std::path::Path; use bcachefs::{BlockBuf, BlockIO, BlockNum, Formatted, FsUuid, Superblock, VecBlockIO}; use crate::arch::Arch; -use toyos_fat32::{BlockAccess, Fat32, FatTime, IoError}; +use toyos_blockdev::{block_on_ready, Bytes, BLOCK}; +use toyos_fat32::{Fat32, FatTime}; use toyos_gpt::Guid; /// The image that goes on the ROOT partition, named by a UUID **derived, never @@ -372,11 +373,14 @@ pub fn read_file_on(file: &mut std::fs::File, guid: [u8; 16], name: &str) -> Res file.seek(SeekFrom::Start(start)) .and_then(|_| file.read_exact(&mut volume)) .map_err(|e| format!("reading the volume at byte {start}: {e}"))?; - let mut fs = Fat32::mount(VolumeIo(&mut volume)).map_err(|e| format!("the volume does not mount: {e}"))?; - let mut found = fs.open(name).map_err(|e| format!("the volume has no {name}: {e}"))?; - let mut bytes = vec![0u8; usize::try_from(found.len()).unwrap_or(usize::MAX)]; - fs.read(&mut found, 0, &mut bytes).map_err(|e| format!("reading {name}: {e}"))?; - Ok(bytes) + let device = device_of(&mut volume)?; + block_on_ready(async { + let mut fs = Fat32::mount(device).await.map_err(|e| format!("the volume does not mount: {e}"))?; + let mut found = fs.open(name).await.map_err(|e| format!("the volume has no {name}: {e}"))?; + let mut bytes = vec![0u8; usize::try_from(found.len()).unwrap_or(usize::MAX)]; + fs.read(&mut found, 0, &mut bytes).await.map_err(|e| format!("reading {name}: {e}"))?; + Ok(bytes) + }) } /// Every file in the root of the image's log volume (its one Microsoft basic @@ -389,17 +393,29 @@ pub fn log_volume_files(file: &mut std::fs::File) -> Result file.seek(SeekFrom::Start(start)) .and_then(|_| file.read_exact(&mut volume)) .map_err(|e| format!("reading the log volume at byte {start}: {e}"))?; - let mut fs = Fat32::mount(VolumeIo(&mut volume)).map_err(|e| format!("the log volume does not mount: {e}"))?; - let entries = fs.read_dir("/", usize::MAX).map_err(|e| format!("listing the log volume: {e}"))?; - let mut files = Vec::new(); - for entry in entries.into_iter().filter(|e| !e.is_dir) { - let path = format!("/{}", entry.name); - let mut found = fs.open(&path).map_err(|e| format!("opening {path}: {e}"))?; - let mut bytes = vec![0u8; usize::try_from(found.len()).unwrap_or(usize::MAX)]; - fs.read(&mut found, 0, &mut bytes).map_err(|e| format!("reading {path}: {e}"))?; - files.push((entry.name, bytes)); + let device = device_of(&mut volume)?; + block_on_ready(async { + let mut fs = Fat32::mount(device).await.map_err(|e| format!("the log volume does not mount: {e}"))?; + let entries = fs.read_dir("/", usize::MAX).await.map_err(|e| format!("listing the log volume: {e}"))?; + let mut files = Vec::new(); + for entry in entries.into_iter().filter(|e| !e.is_dir) { + let path = format!("/{}", entry.name); + let mut found = fs.open(&path).await.map_err(|e| format!("opening {path}: {e}"))?; + let mut bytes = vec![0u8; usize::try_from(found.len()).unwrap_or(usize::MAX)]; + fs.read(&mut found, 0, &mut bytes).await.map_err(|e| format!("reading {path}: {e}"))?; + files.push((entry.name, bytes)); + } + Ok(files) + }) +} + +/// A volume read off an image, as the device `toyos-fat32` mounts: whole +/// blocks, which every partition this build writes is ([`PARTITION_ALIGN`]). +fn device_of(volume: &mut [u8]) -> Result, String> { + match volume.len().is_multiple_of(BLOCK) { + true => Ok(Bytes::new(volume)), + false => Err(format!("a {}-byte volume, which is no whole {BLOCK}-byte blocks", volume.len())), } - Ok(files) } /// Why a scan's `out[0]` and `matched` count did not pick out exactly one @@ -521,47 +537,6 @@ fn format_fat32(bytes: usize, label: &str) -> Vec { crate::fatformat::format(bytes, fat_label(label)) } -/// The volume being built, as [`toyos_fat32`] sees it. -/// -/// Byte-addressed with no block size of its own, because there is none: the -/// volume is a `Vec` in this process. The kernel's adapter is where the -/// read-modify-write against 4 KiB device blocks lives, and `toyos-fat32`'s own -/// host suite is where that shape is exercised. -pub(crate) struct VolumeIo<'a>(pub(crate) &'a mut [u8]); - -impl VolumeIo<'_> { - fn range(&self, offset: u64, len: usize) -> Result, IoError> { - let start = usize::try_from(offset).map_err(|_| IoError::Device)?; - let end = start.checked_add(len).ok_or(IoError::Device)?; - if end > self.0.len() { - return Err(IoError::Device); - } - Ok(start..end) - } -} - -impl BlockAccess for VolumeIo<'_> { - fn capacity(&self) -> u64 { - self.0.len() as u64 - } - - fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<(), IoError> { - let at = self.range(offset, buf.len())?; - buf.copy_from_slice(&self.0[at]); - Ok(()) - } - - fn write_at(&mut self, offset: u64, buf: &[u8]) -> Result<(), IoError> { - let at = self.range(offset, buf.len())?; - self.0[at].copy_from_slice(buf); - Ok(()) - } - - fn flush(&mut self) -> Result<(), IoError> { - Ok(()) - } -} - /// When the build ran, which is what the files on the stick are dated. /// /// A host with a clock behind 1980 or past 2107 gets FAT's nearest @@ -587,24 +562,23 @@ fn build_time() -> FatTime { /// counts the FAT when the volume arrived without a hint and `sync` writes it. fn populate(volume: &mut [u8], label: &str, files: &[(&str, &[u8])]) { let time = build_time(); - let mut fs = Fat32::mount(VolumeIo(volume)) - .unwrap_or_else(|e| panic!("the freshly formatted {label} volume does not mount: {e}")); - for (path, data) in files { - if let Some((dir, _)) = path.rsplit_once('/') { - fs.create_dir_all(dir, time) - .unwrap_or_else(|e| panic!("creating {dir}/ on {label}: {e}")); + block_on_ready(async { + let mut fs = Fat32::mount(Bytes::new(volume)) + .await + .unwrap_or_else(|e| panic!("the freshly formatted {label} volume does not mount: {e}")); + for (path, data) in files { + if let Some((dir, _)) = path.rsplit_once('/') { + fs.create_dir_all(dir, time).await.unwrap_or_else(|e| panic!("creating {dir}/ on {label}: {e}")); + } + let mut file = fs.create(path, time).await.unwrap_or_else(|e| panic!("creating {path} on {label}: {e}")); + fs.write(&mut file, 0, data) + .await + .unwrap_or_else(|e| panic!("writing {} bytes to {path} on {label}: {e}", data.len())); + fs.flush_meta(&mut file, time).await.unwrap_or_else(|e| panic!("recording {path} on {label}: {e}")); } - let mut file = fs - .create(path, time) - .unwrap_or_else(|e| panic!("creating {path} on {label}: {e}")); - fs.write(&mut file, 0, data) - .unwrap_or_else(|e| panic!("writing {} bytes to {path} on {label}: {e}", data.len())); - fs.flush_meta(&mut file, time) - .unwrap_or_else(|e| panic!("recording {path} on {label}: {e}")); - } - fs.free_bytes() - .unwrap_or_else(|e| panic!("counting the {label} volume's free clusters: {e}")); - fs.sync().unwrap_or_else(|e| panic!("syncing the {label} volume: {e}")); + fs.free_bytes().await.unwrap_or_else(|e| panic!("counting the {label} volume's free clusters: {e}")); + fs.sync().await.unwrap_or_else(|e| panic!("syncing the {label} volume: {e}")); + }) } /// The partition firmware boots from: the bootloader, and the name of the @@ -1112,10 +1086,12 @@ mod tests { } /// Every file on `volume`, sorted. - fn files_of(mut volume: Vec) -> Vec { - let mut fs = Fat32::mount(VolumeIo(&mut volume)).expect("mount a volume we just built"); - let mut found: Vec = fs - .walk("", 64) + fn files_of(volume: Vec) -> Vec { + let walked = block_on_ready(async { + let mut fs = Fat32::mount(Bytes::new(volume)).await.expect("mount a volume we just built"); + fs.walk("", 64).await + }); + let mut found: Vec = walked .expect("walk the volume") .into_iter() .map(|(path, _)| path.trim_start_matches('/').to_string()) diff --git a/issues/nothing-reopens-a-diskserver-session-over-a-used-page.md b/issues/nothing-reopens-a-diskserver-session-over-a-used-page.md index 897ff1d5c0d..f5a811cfbb0 100644 --- a/issues/nothing-reopens-a-diskserver-session-over-a-used-page.md +++ b/issues/nothing-reopens-a-diskserver-session-over-a-used-page.md @@ -19,8 +19,9 @@ and `wait` (`system/diskserver/src/session.rs`) hold a `Region`, a `Connection` and a `Poller`, so no host reaches them. A page whose completion ring holds the dead session's answer ends the session only through `drain`'s refusal of a completion for no ticket in flight (`Violation::Tag`, through -`client.complete`); `disk::Served` then answers its caller `Gone`, which a file -server's caller hears as `Io`. That nothing else ends it is +`client.complete`); `disk::Served` then reconnects and asks again, and answers +its caller `Gone` once a reconnect fails or the request has been refused across +more than `MAX_RECONNECTS` ends, which a file server's caller hears as `Io`. That nothing else ends it is `issues/a-block-session-waits-forever-on-a-server-past-its-submission-ring.md`. **Exit**: a host test reopens a client's session over a page a killed server diff --git a/issues/the-storage-programs-are-never-linted.md b/issues/the-storage-programs-are-never-linted.md new file mode 100644 index 00000000000..c08b4233d13 --- /dev/null +++ b/issues/the-storage-programs-are-never-linted.md @@ -0,0 +1,48 @@ +--- +status: open +kind: tooling +opened: 2026-10-22 +--- + +# fileserver and diskserver hold RefCells across awaits, and no clippy shape lints them + +The block I/O under the file protocol is async (`toyos-blockdev`), and the +storage design requires clippy's `await_holding_refcell_ref` wherever a +`RefCell` meets those awaits. The lint is on by default. The gate's +`--workspace` shape excludes every program (`build/hostws.rs`'s +`guest_packages`), though, and `build/clippy.rs` has no other shape that +reaches these programs. So the lint never runs on the two programs that hold +the most RefCell-and-await code: + +- **fileserver**: `src/cache.rs`'s `Cache`, the async device every volume + goes through. +- **diskserver**: `src/disk.rs`'s `Served`, the client library's async + device over a session. + +update's install, `Cached` among it, is linted, because `update-copies` is a +host member that compiles that module under `--all-targets`. Its `main.rs` is +not linted. + +`issues/userland-programs-are-never-linted.md` does not cover these: its exit +names the crates that `build/userlandhost.rs`'s survey gates, and all three +programs carry an `exempt` manifest. + +**Evidence.** +- The lint bites when it is run. A `RefCell` borrow held across + `disk.flush().await` in `Cache::flush` makes `cargo clippy -p fileserver + --lib -- -D warnings` exit 101 with `await_holding_refcell_ref` (PR #1025's + `mut-refcell` control). +- Nothing runs it, and the programs are not clean today. `cargo clippy -p + fileserver --all-targets -- -W warnings` reports 14 findings on lines older + than #1025: `collapsible_if` most often, plus `large_enum_variant` on + `data::Probed` and `len_without_is_empty` on `volume::Out`. + `-p diskserver` reports one (`manual_is_multiple_of`, `src/session.rs`). + +**Owner:** the storage track. C2 puts several tasks on fileserver's executor, +so it needs this lint before it lands. + +**Exit:** a shape in `build/clippy.rs`, which both `--clippy` and `--ci host` +run, lints fileserver's and diskserver's host-built targets (and update's +`main.rs`, if it can build on the host) with warnings denied, and their +findings are fixed. A `RefCell` borrow planted across an await in +`Cache::flush` reds `--clippy`. diff --git a/lib/blockdev/Cargo.toml b/lib/blockdev/Cargo.toml new file mode 100644 index 00000000000..972bfc7a8ff --- /dev/null +++ b/lib/blockdev/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "toyos-blockdev" +description = "A block device as its filesystems await it: the trait, the single-threaded executor that drives it, a device over memory, and the crash model filesystems are tested against." +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" + +[features] +# The crash model (`src/model.rs`), for the tests of what is written through a +# device. This crate's own tests build it regardless. +model = [] + +[lib] +doctest = false diff --git a/lib/blockdev/src/bytes.rs b/lib/blockdev/src/bytes.rs new file mode 100644 index 00000000000..fb6350107a8 --- /dev/null +++ b/lib/blockdev/src/bytes.rs @@ -0,0 +1,84 @@ +use core::cell::{Ref, RefCell}; + +use crate::{within, Block, BlockDev, Error, BLOCK}; + +/// A device over memory, which answers every request as it is asked: an image +/// being built, or one read whole. A flush has nothing to do. +pub struct Bytes { + bytes: RefCell, + blocks: u64, +} + +impl + AsMut<[u8]>> Bytes { + /// `bytes` is whole blocks. + pub fn new(bytes: B) -> Self { + let len = bytes.as_ref().len(); + assert!(len.is_multiple_of(BLOCK), "a device over {len} bytes, which are no whole blocks"); + Self { blocks: (len / BLOCK) as u64, bytes: RefCell::new(bytes) } + } + + /// What the device holds now. + pub fn get(&self) -> Ref<'_, B> { + self.bytes.borrow() + } + + pub fn into_inner(self) -> B { + self.bytes.into_inner() + } + + fn at(&self, lba: u64, count: usize) -> Result { + within(lba, count, self.blocks)?; + Ok(lba as usize * BLOCK) + } +} + +impl + AsMut<[u8]>> BlockDev for Bytes { + fn blocks(&self) -> u64 { + self.blocks + } + + async fn read(&self, lba: u64, out: &mut [Block]) -> Result<(), Error> { + let at = self.at(lba, out.len())?; + let out = out.as_flattened_mut(); + out.copy_from_slice(&self.bytes.borrow().as_ref()[at..at + out.len()]); + Ok(()) + } + + async fn write(&self, lba: u64, data: &[Block]) -> Result<(), Error> { + let at = self.at(lba, data.len())?; + let data = data.as_flattened(); + self.bytes.borrow_mut().as_mut()[at..at + data.len()].copy_from_slice(data); + Ok(()) + } + + async fn flush(&self) -> Result<(), Error> { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::block_on_ready; + use alloc::vec; + + #[test] + fn blocks_read_back_and_a_request_past_the_end_is_refused() { + let dev = Bytes::new(vec![0u8; 4 * BLOCK]); + block_on_ready(async { + dev.write(1, &[[7; BLOCK], [8; BLOCK]]).await.unwrap(); + let mut out = [[0; BLOCK]; 3]; + dev.read(0, &mut out).await.unwrap(); + assert_eq!(out, [[0; BLOCK], [7; BLOCK], [8; BLOCK]]); + assert_eq!(dev.write(3, &[[1; BLOCK], [1; BLOCK]]).await, Err(Error::Range)); + assert_eq!(dev.read(u64::MAX, &mut out).await, Err(Error::Range)); + }); + assert!(dev.get()[3 * BLOCK..].iter().all(|&b| b == 0), "a refused write wrote"); + } + + #[test] + #[should_panic(expected = "no whole blocks")] + fn a_device_over_part_of_a_block_is_refused() { + Bytes::new(vec![0u8; BLOCK + 512]); + } +} diff --git a/lib/blockdev/src/executor.rs b/lib/blockdev/src/executor.rs new file mode 100644 index 00000000000..ba4e022d7d1 --- /dev/null +++ b/lib/blockdev/src/executor.rs @@ -0,0 +1,214 @@ +//! One thread's tasks, polled when woken. +//! +//! **Nothing here waits.** A task that cannot go on is pending until a device +//! answers one of its requests; what makes a device answer is its owner's — +//! the block service's session, read when its connection is — so +//! [`Executor::run_until_stalled`] returns when no task can go on, and +//! [`Executor::block_on`] calls the `wait` it is given then. + +use alloc::boxed::Box; +use alloc::sync::Arc; +use alloc::task::Wake; +use alloc::vec::Vec; +use core::cell::RefCell; +use core::future::Future; +use core::pin::{pin, Pin}; +use core::sync::atomic::{AtomicBool, Ordering}; +use core::task::{Context, Poll, Waker}; + +/// Set by a wake, cleared by the poll it asks for. +struct Woken(AtomicBool); + +impl Woken { + fn new() -> Arc { + Arc::new(Self(AtomicBool::new(true))) + } + + fn take(&self) -> bool { + self.0.swap(false, Ordering::Acquire) + } +} + +impl Wake for Woken { + fn wake(self: Arc) { + self.0.store(true, Ordering::Release); + } + + fn wake_by_ref(self: &Arc) { + self.0.store(true, Ordering::Release); + } +} + +struct Task<'a> { + future: Pin + 'a>>, + woken: Arc, + waker: Waker, +} + +/// Tasks that borrow for `'a`, run on the thread that drives this. +#[derive(Default)] +pub struct Executor<'a> { + tasks: RefCell>>, + /// Spawned since the last pass, which a task may do while it is polled. + spawned: RefCell>>, +} + +impl<'a> Executor<'a> { + pub fn new() -> Self { + Self::default() + } + + pub fn spawn(&self, future: impl Future + 'a) { + let woken = Woken::new(); + let waker = Waker::from(Arc::clone(&woken)); + self.spawned.borrow_mut().push(Task { future: Box::pin(future), woken, waker }); + } + + /// Poll every woken task, again and again, until none is woken. Answers + /// how many tasks have not finished. + pub fn run_until_stalled(&self) -> usize { + while self.pass() {} + self.tasks.borrow().len() + } + + /// `future` polled, beside every spawned task, until it is ready. When + /// nothing can go on, `wait` is called, and must return once something + /// may: a device that answers only when its owner reads it is read there. + pub fn block_on(&self, future: F, mut wait: impl FnMut()) -> F::Output { + let mut future = pin!(future); + let woken = Woken::new(); + let waker = Waker::from(Arc::clone(&woken)); + let mut cx = Context::from_waker(&waker); + loop { + if woken.take() + && let Poll::Ready(out) = future.as_mut().poll(&mut cx) + { + return out; + } + let ran = self.pass(); + if !ran && !woken.0.load(Ordering::Acquire) { + wait(); + } + } + } + + /// One poll of each task woken since its last; whether any was. + fn pass(&self) -> bool { + let mut tasks = self.tasks.borrow_mut(); + tasks.append(&mut self.spawned.borrow_mut()); + let mut ran = false; + tasks.retain_mut(|task| { + if !task.woken.take() { + return true; + } + ran = true; + task.future.as_mut().poll(&mut Context::from_waker(&task.waker)).is_pending() + }); + ran || !self.spawned.borrow().is_empty() + } +} + +/// [`Executor::block_on`] on an executor of its own. +pub fn block_on(future: F, wait: impl FnMut()) -> F::Output { + Executor::new().block_on(future, wait) +} + +/// [`block_on`] over devices that answer every request as it is asked, such +/// as [`crate::Bytes`]: a future that stalls there waits on nothing that will +/// ever wake it, which is a bug. +pub fn block_on_ready(future: F) -> F::Output { + block_on(future, || panic!("a future over devices that are always ready stalled")) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::rc::Rc; + use core::cell::Cell; + + /// Pending until `open`, then ready; wakes whoever polled it last when + /// opened. + #[derive(Default)] + struct Gate { + open: Cell, + waiter: RefCell>, + } + + impl Gate { + fn open(&self) { + self.open.set(true); + if let Some(w) = self.waiter.take() { + w.wake(); + } + } + + async fn pass(&self) { + core::future::poll_fn(|cx| { + if self.open.get() { + Poll::Ready(()) + } else { + *self.waiter.borrow_mut() = Some(cx.waker().clone()); + Poll::Pending + } + }) + .await; + } + } + + #[test] + fn a_stalled_task_is_left_until_it_is_woken() { + let gate = Gate::default(); + let done = Cell::new(0); + let exec = Executor::new(); + exec.spawn(async { + gate.pass().await; + done.set(done.get() + 1); + }); + exec.spawn(async { done.set(done.get() + 1) }); + assert_eq!(exec.run_until_stalled(), 1); + assert_eq!(done.get(), 1); + assert_eq!(exec.run_until_stalled(), 1, "nothing woke it"); + gate.open(); + assert_eq!(exec.run_until_stalled(), 0); + assert_eq!(done.get(), 2); + } + + #[test] + fn a_task_spawned_by_a_task_runs_in_the_same_drive() { + let ran = Rc::new(Cell::new(false)); + let exec = Rc::new(Executor::new()); + let (inner, flag) = (Rc::clone(&exec), Rc::clone(&ran)); + exec.spawn(async move { + inner.spawn(async move { flag.set(true) }); + }); + assert_eq!(exec.run_until_stalled(), 0); + assert!(ran.get()); + } + + #[test] + fn block_on_waits_only_when_nothing_can_go_on_and_runs_spawned_tasks() { + let gate = Gate::default(); + let side = Cell::new(false); + let waits = Cell::new(0); + let exec = Executor::new(); + exec.spawn(async { side.set(true) }); + let out = exec.block_on( + async { + gate.pass().await; + 7 + }, + || { + waits.set(waits.get() + 1); + gate.open(); + }, + ); + assert_eq!((out, waits.get(), side.get()), (7, 1, true)); + } + + #[test] + #[should_panic(expected = "always ready stalled")] + fn a_stall_over_ready_devices_is_a_bug() { + let gate = Gate::default(); + block_on_ready(gate.pass()); + } +} diff --git a/lib/blockdev/src/lib.rs b/lib/blockdev/src/lib.rs new file mode 100644 index 00000000000..2bc3621dcea --- /dev/null +++ b/lib/blockdev/src/lib.rs @@ -0,0 +1,105 @@ +//! A block device as a filesystem awaits it ([`BlockDev`]), the executor that +//! drives those awaits ([`executor`]), a device over memory ([`Bytes`]) and, +//! behind the `model` feature, the crash model a filesystem's ordering is +//! checked against ([`model`]). +//! +//! **A block is [`BLOCK`] bytes on every device**: the block service refuses +//! a partition that is not whole 4 KiB blocks before it serves it, so nothing +//! above this has a second block size. What a device keeps whole across a +//! power loss can be smaller, and is the crash model's to say. +//! +//! **Methods take `&self`**: several tasks may have requests on one device at +//! once, and requests in flight together are unordered. A caller that needs +//! one to follow another awaits the first. + +#![no_std] +#![forbid(unsafe_code)] + +extern crate alloc; + +mod bytes; +pub mod executor; +#[cfg(any(test, feature = "model"))] +pub mod model; + +pub use bytes::Bytes; +pub use executor::{block_on, block_on_ready, Executor}; + +use alloc::rc::Rc; + +/// The bytes of one block. +pub const BLOCK: usize = 4096; + +/// One block. +pub type Block = [u8; BLOCK]; + +/// Why a device did not do what it was asked. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Error { + /// The device's own word: the transfer was attempted and did not happen, + /// or a flush left writes the device would not keep. A write answered + /// this may or may not have reached the medium. + Device, + /// The device is gone and will not come back. + Gone, + /// Past the end of the device. + Range, + /// A write this holder's grant does not make. + ReadOnly, +} + +/// A device of [`BlockDev::blocks`] blocks. +// Every future runs on this crate's single-threaded executor, so none needs +// to be `Send`: the bound the lint asks to be stated is one no caller wants. +#[allow(async_fn_in_trait)] +pub trait BlockDev { + fn blocks(&self) -> u64; + + /// Blocks `lba..lba + out.len()` into `out`. + async fn read(&self, lba: u64, out: &mut [Block]) -> Result<(), Error>; + + /// `data` at `lba`, complete once the device has taken it. Durable only + /// once a [`BlockDev::flush`] asked after this completed has completed. + async fn write(&self, lba: u64, data: &[Block]) -> Result<(), Error>; + + /// Every write that completed before this was asked, on the medium. + async fn flush(&self) -> Result<(), Error>; +} + +/// `Ok` if `count` blocks from `lba` lie on a device of `blocks` blocks. +pub fn within(lba: u64, count: usize, blocks: u64) -> Result<(), Error> { + match lba.checked_add(count as u64) { + Some(end) if end <= blocks => Ok(()), + _ => Err(Error::Range), + } +} + +impl BlockDev for &D { + fn blocks(&self) -> u64 { + (**self).blocks() + } + async fn read(&self, lba: u64, out: &mut [Block]) -> Result<(), Error> { + (**self).read(lba, out).await + } + async fn write(&self, lba: u64, data: &[Block]) -> Result<(), Error> { + (**self).write(lba, data).await + } + async fn flush(&self) -> Result<(), Error> { + (**self).flush().await + } +} + +impl BlockDev for Rc { + fn blocks(&self) -> u64 { + (**self).blocks() + } + async fn read(&self, lba: u64, out: &mut [Block]) -> Result<(), Error> { + (**self).read(lba, out).await + } + async fn write(&self, lba: u64, data: &[Block]) -> Result<(), Error> { + (**self).write(lba, data).await + } + async fn flush(&self) -> Result<(), Error> { + (**self).flush().await + } +} diff --git a/lib/blockdev/src/model.rs b/lib/blockdev/src/model.rs new file mode 100644 index 00000000000..1a09199a511 --- /dev/null +++ b/lib/blockdev/src/model.rs @@ -0,0 +1,349 @@ +//! A device as a crash leaves it, for checking what a filesystem writes +//! through one. +//! +//! **A completed write is held in a volatile cache until a flush covers it.** +//! A crash keeps every flushed write and, of the writes no flush covered, any +//! subset of their atomic units: nothing orders how a cache reaches the +//! medium, nor the units of one command. So each unit some unflushed write +//! covers holds, after the crash, either what was durable or what any one of +//! those writes carried; a write kept in part is a torn write. +//! +//! **The unit is the device's**, never the format's block: one logical block +//! (512 bytes on a 512-byte-LBA device) unless the device reports a larger +//! unit it writes whole across a power loss. +//! +//! [`Model::check`] judges every medium a crash could leave now when there +//! are at most its budget of them, and that many drawn from a seed otherwise. +//! A [`Violation`] names the seed and the state, so a red run is replayed. + +use alloc::string::String; +use alloc::vec::Vec; +use core::cell::RefCell; +use core::fmt; + +use crate::{within, Block, BlockDev, Error, BLOCK}; + +/// A device whose every request completes as it is asked. +pub struct Model { + state: RefCell, + blocks: u64, + unit: usize, +} + +struct State { + durable: Vec, + /// In the order they completed. + unflushed: Vec, +} + +struct Unflushed { + /// The byte the write begins at. + at: usize, + data: Vec, +} + +/// How [`Model::check`] reached the states it judged. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Search { + /// Every state there is, this many. + Every(u64), + /// This many, drawn from the seed: more states than the budget. + Sampled { seed: u64, samples: u64 }, +} + +/// A crash state the judge refused. +#[derive(Debug)] +pub struct Violation { + pub search: Search, + /// The state's index among every state, or its sample number. + pub state: u64, + pub why: String, + /// The medium the crash left. + pub image: Vec, +} + +impl fmt::Display for Violation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.search { + Search::Every(n) => write!(f, "crash state {} of every {n}: {}", self.state, self.why), + Search::Sampled { seed, samples } => { + write!(f, "crash sample {} of {samples} from seed {seed:#x}: {}", self.state, self.why) + } + } + } +} + +/// One unit some unflushed write covers: where it is, and each write's +/// version of it as `(write, offset in its data)`. +struct Unit { + at: usize, + versions: Vec<(usize, usize)>, +} + +impl Model { + /// A device holding `image` durably, whose writes are kept whole in + /// `unit`-byte pieces. + pub fn new(image: Vec, unit: usize) -> Self { + assert!(unit > 0 && BLOCK.is_multiple_of(unit), "an atomic unit of {unit} bytes does not divide a block"); + assert!(image.len().is_multiple_of(BLOCK), "a device over {} bytes, which are no whole blocks", image.len()); + let blocks = (image.len() / BLOCK) as u64; + Self { state: RefCell::new(State { durable: image, unflushed: Vec::new() }), blocks, unit } + } + + /// Writes no flush has covered. + pub fn unflushed(&self) -> usize { + self.state.borrow().unflushed.len() + } + + /// `judge` asked of every medium a crash now could leave, or of `budget` + /// of them drawn from `seed` when there are more; the first it refuses. + pub fn check( + &self, + budget: u64, + seed: u64, + mut judge: impl FnMut(&[u8]) -> Result<(), String>, + ) -> Result { + let state = self.state.borrow(); + let units = self.units(&state); + let total = units.iter().try_fold(1u64, |n, u| n.checked_mul(u.versions.len() as u64 + 1)); + let search = match total { + Some(n) if n <= budget => Search::Every(n), + _ => Search::Sampled { seed, samples: budget }, + }; + let states = match search { + Search::Every(n) => n, + Search::Sampled { samples, .. } => samples, + }; + let mut choice = Vec::with_capacity(units.len()); + for index in 0..states { + choice.clear(); + match search { + Search::Every(_) => { + let mut rest = index; + for u in &units { + let radix = u.versions.len() as u64 + 1; + choice.push((rest % radix) as usize); + rest /= radix; + } + } + Search::Sampled { seed, .. } => { + let mut rng = SplitMix(seed ^ index.wrapping_mul(0x9E37_79B9_7F4A_7C15)); + choice.extend(units.iter().map(|u| (rng.next() % (u.versions.len() as u64 + 1)) as usize)); + } + } + let image = self.medium(&state, &units, &choice); + if let Err(why) = judge(&image) { + return Err(Violation { search, state: index, why, image }); + } + } + Ok(search) + } + + fn units(&self, state: &State) -> Vec { + let mut units: Vec = Vec::new(); + for (w, write) in state.unflushed.iter().enumerate() { + for offset in (0..write.data.len()).step_by(self.unit) { + let at = write.at + offset; + match units.binary_search_by_key(&at, |u| u.at) { + Ok(i) => units[i].versions.push((w, offset)), + Err(i) => units.insert(i, Unit { at, versions: alloc::vec![(w, offset)] }), + } + } + } + units + } + + /// The durable bytes, with each unit's `choice` over them: 0 keeps the + /// durable unit, `k` the `k`th version. + fn medium(&self, state: &State, units: &[Unit], choice: &[usize]) -> Vec { + let mut image = state.durable.clone(); + for (u, &c) in units.iter().zip(choice) { + if let Some(&(w, offset)) = c.checked_sub(1).map(|k| &u.versions[k]) { + image[u.at..u.at + self.unit].copy_from_slice(&state.unflushed[w].data[offset..offset + self.unit]); + } + } + image + } +} + +impl BlockDev for Model { + fn blocks(&self) -> u64 { + self.blocks + } + + /// What the device answers now: the durable bytes under every unflushed + /// write, the latest last. + async fn read(&self, lba: u64, out: &mut [Block]) -> Result<(), Error> { + within(lba, out.len(), self.blocks)?; + let state = self.state.borrow(); + let (start, out) = (lba as usize * BLOCK, out.as_flattened_mut()); + let end = start + out.len(); + out.copy_from_slice(&state.durable[start..end]); + for w in &state.unflushed { + let (from, to) = (w.at.max(start), (w.at + w.data.len()).min(end)); + if from < to { + out[from - start..to - start].copy_from_slice(&w.data[from - w.at..to - w.at]); + } + } + Ok(()) + } + + async fn write(&self, lba: u64, data: &[Block]) -> Result<(), Error> { + within(lba, data.len(), self.blocks)?; + let at = lba as usize * BLOCK; + self.state.borrow_mut().unflushed.push(Unflushed { at, data: data.as_flattened().to_vec() }); + Ok(()) + } + + async fn flush(&self) -> Result<(), Error> { + let mut state = self.state.borrow_mut(); + let State { durable, unflushed } = &mut *state; + for w in unflushed.drain(..) { + durable[w.at..w.at + w.data.len()].copy_from_slice(&w.data); + } + Ok(()) + } +} + +/// SplitMix64 (Steele, Lea and Flood, "Fast splittable pseudorandom number +/// generators", OOPSLA 2014): a sample's choices from its seed alone. +struct SplitMix(u64); + +impl SplitMix { + 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) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::block_on_ready; + use alloc::collections::BTreeSet; + use alloc::format; + use alloc::vec; + + const RECORD: u64 = 1; + const MARKER: u64 = 2; + const SEED: u64 = 0x5EED_B10C; + + /// A toy commit: a record, then a marker naming it, with or without a + /// flush between them. + async fn commit(dev: &Model, flush_between: bool) { + dev.write(RECORD, &[[0xAA; BLOCK]]).await.unwrap(); + if flush_between { + dev.flush().await.unwrap(); + } + dev.write(MARKER, &[[0x11; BLOCK]]).await.unwrap(); + } + + /// The toy's recovery rule: a marker is never on the medium without the + /// whole record it names. + fn committed_is_whole(image: &[u8]) -> Result<(), String> { + let block = |n: u64| &image[n as usize * BLOCK..][..BLOCK]; + if block(MARKER).iter().any(|&b| b != 0) && block(RECORD).iter().any(|&b| b != 0xAA) { + return Err("the marker is on the medium and its record is not whole".into()); + } + Ok(()) + } + + /// The model's own control, red: with no flush between record and + /// marker, some crash keeps the marker and not the record. + #[test] + fn a_marker_written_without_a_flush_after_its_record_is_found_without_it() { + let dev = Model::new(vec![0; 4 * BLOCK], 512); + block_on_ready(commit(&dev, false)); + let found = dev.check(1 << 20, SEED, committed_is_whole).expect_err("no crash separated marker and record"); + assert!(matches!(found.search, Search::Every(_)), "{found}"); + assert!(committed_is_whole(&found.image).is_err()); + } + + /// The same control, green: a flush between them leaves no crash in + /// which the marker is without its record. + #[test] + fn a_marker_written_after_its_record_was_flushed_is_never_without_it() { + let dev = Model::new(vec![0; 4 * BLOCK], 512); + block_on_ready(commit(&dev, true)); + let search = dev.check(1 << 20, SEED, committed_is_whole).unwrap_or_else(|v| panic!("{v}")); + assert_eq!(search, Search::Every(1 << 8), "the marker's eight units, each kept or not"); + } + + /// One block written at one LBA on a device with 512-byte units: a crash + /// keeps any subset of its eight sectors, torn ones included. On a device + /// that keeps the whole block across a power loss, only none or all. + #[test] + fn a_write_at_one_lba_tears_at_the_device_unit_and_only_there() { + for (unit, states) in [(512, 256), (BLOCK, 2)] { + let dev = Model::new(vec![0; 2 * BLOCK], unit); + block_on_ready(dev.write(1, &[[0xFF; BLOCK]])).unwrap(); + let mut seen = BTreeSet::new(); + let search = dev + .check(1 << 16, SEED, |image| { + let kept: u8 = (0..8).fold(0, |k, s| k | u8::from(image[BLOCK + s * 512] == 0xFF) << s); + seen.insert(kept); + Ok(()) + }) + .unwrap(); + assert_eq!(search, Search::Every(states)); + let want: BTreeSet = match unit { + 512 => (0..=255).collect(), + _ => [0, 0xFF].into(), + }; + assert_eq!(seen, want, "a {unit}-byte unit"); + } + } + + /// Two writes to one unit, neither flushed: a crash leaves the durable + /// bytes or either write's, since a cache may have written the older one + /// and not the newer. A read answers the newer. + #[test] + fn an_overwritten_unflushed_unit_may_crash_to_either_version() { + let dev = Model::new(vec![0; BLOCK], BLOCK); + let read = block_on_ready(async { + dev.write(0, &[[1; BLOCK]]).await.unwrap(); + dev.write(0, &[[2; BLOCK]]).await.unwrap(); + let mut out = [[0; BLOCK]]; + dev.read(0, &mut out).await.unwrap(); + out[0][0] + }); + assert_eq!(read, 2); + let mut seen = BTreeSet::new(); + dev.check(16, SEED, |image| { + seen.insert(image[0]); + Ok(()) + }) + .unwrap(); + assert_eq!(seen, [0, 1, 2].into()); + block_on_ready(dev.flush()).unwrap(); + let flushed = dev.check(16, SEED, |image| if image[0] == 2 { Ok(()) } else { Err(format!("{}", image[0])) }); + assert_eq!(flushed.unwrap(), Search::Every(1), "a flushed write is the only state"); + } + + /// More states than the budget are sampled from the seed, the same ones + /// each run, and a refusal names the seed it came from. + #[test] + fn past_the_budget_states_are_drawn_from_the_seed_and_a_refusal_names_it() { + let dev = Model::new(vec![0; 64 * BLOCK], 512); + block_on_ready(dev.write(0, &[[9; BLOCK]; 64])).unwrap(); + let draw = |seed| { + let mut images = Vec::new(); + let search = dev + .check(32, seed, |image| { + images.push(image.iter().map(|&b| u32::from(b == 9)).sum::()); + Ok(()) + }) + .unwrap(); + (search, images) + }; + let (search, first) = draw(SEED); + assert_eq!(search, Search::Sampled { seed: SEED, samples: 32 }); + assert_eq!(draw(SEED).1, first, "a seed draws the same states"); + assert_ne!(draw(SEED + 1).1, first); + let refused = dev.check(32, SEED, |_| Err("refused".into())).unwrap_err(); + assert_eq!(format!("{refused}"), format!("crash sample 0 of 32 from seed {SEED:#x}: refused")); + } +} diff --git a/lib/disk/Cargo.toml b/lib/disk/Cargo.toml deleted file mode 100644 index fa5f26ba467..00000000000 --- a/lib/disk/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "toyos-disk" -description = "A partition as its reader and writer holds it, in whole 4 KiB blocks, whoever serves it: the trait, pure." -version = "0.1.0" -edition = "2021" -license = "MIT OR Apache-2.0" - -[lib] -doctest = false diff --git a/lib/disk/src/lib.rs b/lib/disk/src/lib.rs deleted file mode 100644 index 5fabead500b..00000000000 --- a/lib/disk/src/lib.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! A partition as a program that reads and writes its blocks holds it, in -//! whole 4 KiB blocks, whoever serves it: the one trait the block service's -//! sessions, a claim on the kernel's disk and every test's memory implement. -//! -//! **A block is 4 KiB here, on every source.** A partition that is not whole -//! 4 KiB blocks is refused before it is served, so nothing below has a second -//! block size. - -pub const BLOCK: usize = 4096; - -/// Why a disk did not do what it was asked. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum DiskError { - /// The device's own word: the transfer was attempted and did not happen, - /// or a flush left writes the device would not keep. - Device, - /// The block service is gone and would not come back. - Gone, - /// Past the end of the partition. - Range, - /// A write the block service's grant to this holder does not make. - ReadOnly, -} - -/// A partition, a block at a time or several. -pub trait Disk { - fn blocks(&self) -> u64; - /// `out.len()` is whole blocks. - fn read(&mut self, first: u64, out: &mut [u8]) -> Result<(), DiskError>; - /// `data.len()` is whole blocks; durable once [`Disk::flush`] answers `Ok`. - fn write(&mut self, first: u64, data: &[u8]) -> Result<(), DiskError>; - /// Every write answered before this, out of the device's cache. - fn flush(&mut self) -> Result<(), DiskError>; -} - -/// How many blocks `len` bytes from `first` are, or `Range` past the end of -/// a partition `blocks` long. -pub fn span(first: u64, len: usize, blocks: u64) -> Result { - assert!(len.is_multiple_of(BLOCK), "a transfer of {len} bytes is no whole blocks"); - let count = (len / BLOCK) as u64; - match first.checked_add(count) { - Some(end) if end <= blocks => Ok(count), - _ => Err(DiskError::Range), - } -} diff --git a/lib/fat32/Cargo.toml b/lib/fat32/Cargo.toml index 8653ba59fe4..4894d9859f4 100644 --- a/lib/fat32/Cargo.toml +++ b/lib/fat32/Cargo.toml @@ -1,27 +1,28 @@ -# A member of the host workspace (root `Cargo.toml`): the -# kernel depends on it by path and its tests run on the host, with no -# target-triple juggling. Membership adds no dependency edge — a +# A member of the host workspace (root `Cargo.toml`): its tests run on the +# host, with no target-triple juggling. Membership adds no dependency edge — a # workspace shares a lockfile and nothing else. # -# **One dependency, and the rule it is the exception to is the reason for it.** -# A filesystem driver that pulls in a crate graph cannot be audited for the one -# property that matters here, which is that no path reachable from on-disk -# bytes can panic. So the graph stays a single leaf: `toyos-wallclock` is ours, -# `no_std`, `forbid(unsafe_code)`, and depends on nothing itself. What the edge -# replaced was a third copy of Hinnant's algorithm — byte-identical to that -# crate's, reachable from a directory entry's raw bit-fields, and read by -# nothing outside `src/time.rs`. The audit is now over arithmetic that nine +# **Two dependencies, both leaves, and the rule they are the exception to is +# the reason for it.** A filesystem driver that pulls in a crate graph cannot +# be audited for the one property that matters here, which is that no path +# reachable from on-disk bytes can panic. So the graph stays two leaves of +# ours, each `no_std`, `forbid(unsafe_code)` and depending on nothing: +# `toyos-blockdev`, the device every volume is read through, and +# `toyos-wallclock`, whose edge replaced a third copy of Hinnant's algorithm — +# byte-identical to that crate's, reachable from a directory entry's raw +# bit-fields, and read by nothing outside `src/time.rs`. The audit is now over arithmetic that nine # host tests stand behind, and `every_bit_pattern_decodes` is where its # totality against hostile bytes is still asserted here. [package] name = "toyos-fat32" -description = "FAT32 driver, read and write, over a byte-addressed volume; no format path by design." +description = "FAT32 driver, read and write, over a block device; no format path by design." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" [dependencies] +toyos-blockdev = { path = "../blockdev" } toyos-wallclock = { path = "../wallclock" } [dev-dependencies] diff --git a/lib/fat32/src/boot.rs b/lib/fat32/src/boot.rs index d5c4a3aae0b..21117e88824 100644 --- a/lib/fat32/src/boot.rs +++ b/lib/fat32/src/boot.rs @@ -1,4 +1,5 @@ -use crate::device::BlockAccess; +use toyos_blockdev::BlockDev; +use crate::device::Volume as _; use crate::error::Error; /// Below this cluster count the volume is FAT12 or FAT16, whatever its boot @@ -280,10 +281,10 @@ impl FsInfo { /// A bad FSInfo does not fail the mount. It carries no information the FAT /// does not already hold, so refusing to mount over it would turn a /// cosmetic inconsistency into an unbootable machine. - pub fn read(dev: &mut D, geom: &Geometry, buf: &mut [u8]) -> FsInfo { + pub async fn read(dev: &D, geom: &Geometry, buf: &mut [u8]) -> FsInfo { let Some(sector) = geom.fsinfo_sector else { return FsInfo::UNKNOWN }; let Some(buf) = buf.get_mut(..geom.bytes_per_sector as usize) else { return FsInfo::UNKNOWN }; - if dev.read_at(geom.sector_offset(sector), buf).is_err() { + if dev.read_at(geom.sector_offset(sector), buf).await.is_err() { return FsInfo::UNKNOWN; } if u32_at(buf, 0) != 0x4161_5252 @@ -307,11 +308,11 @@ impl FsInfo { /// and the reserved regions belong to whoever formatted the volume, and /// rewriting them from our own idea of the layout is how a driver corrupts /// a filesystem it only meant to update a counter in. - pub fn write(&self, dev: &mut D, geom: &Geometry, buf: &mut [u8]) -> Result<(), Error> { + pub async fn write(&self, dev: &D, geom: &Geometry, buf: &mut [u8]) -> Result<(), Error> { let Some(sector) = geom.fsinfo_sector else { return Ok(()) }; let Some(buf) = buf.get_mut(..geom.bytes_per_sector as usize) else { return Ok(()) }; let offset = geom.sector_offset(sector); - dev.read_at(offset, buf)?; + dev.read_at(offset, buf).await?; if u32_at(buf, 0) != 0x4161_5252 || u32_at(buf, 484) != 0x6141_7272 { return Ok(()); } @@ -322,7 +323,7 @@ impl FsInfo { }; put(buf, 488, self.free_count.unwrap_or(0xFFFF_FFFF)); put(buf, 492, self.next_free.map_or(0xFFFF_FFFF, Cluster::raw)); - dev.write_at(offset, buf)?; + dev.write_at(offset, buf).await?; Ok(()) } } diff --git a/lib/fat32/src/device.rs b/lib/fat32/src/device.rs index 324adeccf58..45d7052c897 100644 --- a/lib/fat32/src/device.rs +++ b/lib/fat32/src/device.rs @@ -1,43 +1,132 @@ -/// The volume this crate reads and writes. -/// -/// Byte-addressed, and deliberately so. Two block sizes meet here and neither -/// belongs in this crate: the kernel's `BlockDevice` does I/O in 4096-byte -/// blocks, while an ESP's sectors are whatever its BPB says — usually 512. If -/// this trait spoke sectors, the implementor would have to know the BPB's -/// sector size to serve a request, and it cannot: parsing the BPB is what the -/// first call to this trait is *for*. Bytes are the only unit both sides agree -/// on before anything has been read. -/// -/// So an implementor bridges to its own block size, including read-modify-write -/// for a partial block. `Fat32` reasons in BPB sectors and converts to byte -/// offsets at the boundary; nothing here assumes 512, or 4096, or any -/// particular alignment of a request. -/// -/// Offsets are relative to the start of the volume, not the disk. A partition -/// is the implementor's business. -pub trait BlockAccess { - /// Bytes in the volume. Used once, at mount, to reject a boot sector that - /// describes more volume than exists. - fn capacity(&self) -> u64; - - /// Fill `buf` from `offset`. Reading past [`capacity`](Self::capacity) is - /// an [`IoError`], not a short read — this crate never asks for bytes it - /// has not already bounded against the volume, so a truncated answer would - /// mean a bug on one side or the other. - fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<(), IoError>; - - fn write_at(&mut self, offset: u64, buf: &[u8]) -> Result<(), IoError>; - - /// Make every prior write durable. - fn flush(&mut self) -> Result<(), IoError>; +//! The volume's bytes, over a [`BlockDev`]'s blocks. +//! +//! **This crate reasons in BPB sectors and reaches the device in bytes**: a +//! sector is whatever the boot sector says, usually 512, and parsing it is +//! what the first read is for. So a request here is any byte range, and it +//! goes to the device as one request for the whole blocks it touches, a part +//! block read before it is written. Nothing is kept between requests: a cache, +//! where a volume wants one, is a device under this. +//! +//! The volume starts at the device's first byte, and every offset is bounded +//! by the geometry before it is asked for. + +use alloc::vec; +use alloc::vec::Vec; + +use toyos_blockdev::{Block, BlockDev, Error, BLOCK}; + +/// The blocks a byte range lies on: the first, how many, and where in the +/// first the range begins. +fn blocks_of(offset: u64, len: usize) -> Result<(u64, usize, usize), Error> { + let end = offset.checked_add(len as u64).ok_or(Error::Range)?; + let first = offset / BLOCK as u64; + let count = usize::try_from(end.div_ceil(BLOCK as u64) - first).map_err(|_| Error::Range)?; + Ok((first, count, (offset % BLOCK as u64) as usize)) +} + +pub(crate) trait Volume { + async fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), Error>; + async fn write_at(&self, offset: u64, buf: &[u8]) -> Result<(), Error>; +} + +impl Volume for D { + async fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), Error> { + let (first, count, at) = blocks_of(offset, buf.len())?; + if at == 0 && buf.len().is_multiple_of(BLOCK) { + return self.read(first, buf.as_chunks_mut::().0).await; + } + let mut span: Vec = vec![[0; BLOCK]; count]; + self.read(first, &mut span).await?; + buf.copy_from_slice(&span.as_flattened()[at..at + buf.len()]); + Ok(()) + } + + async fn write_at(&self, offset: u64, buf: &[u8]) -> Result<(), Error> { + let (first, count, at) = blocks_of(offset, buf.len())?; + if at == 0 && buf.len().is_multiple_of(BLOCK) { + return self.write(first, buf.as_chunks::().0).await; + } + let mut span: Vec = vec![[0; BLOCK]; count]; + // What the range leaves of its first and last blocks is another + // structure's, read before it is written back. + let last = count - 1; + if at != 0 { + self.read(first, &mut span[..1]).await?; + } + if !(at + buf.len()).is_multiple_of(BLOCK) && !(last == 0 && at != 0) { + self.read(first + last as u64, &mut span[last..]).await?; + } + span.as_flattened_mut()[at..at + buf.len()].copy_from_slice(buf); + self.write(first, &span).await + } } -/// The volume did not do it. -/// -/// This crate treats every refused write as of unknown outcome — see -/// `crate::repair`. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum IoError { - /// The device refused, failed, or would not answer. - Device, +#[cfg(test)] +mod tests { + use super::*; + use core::cell::Cell; + use toyos_blockdev::{block_on_ready, Bytes}; + + /// Four blocks holding `0x5A`, the second of which refuses every read. + struct Refusing { + bytes: Bytes>, + refused: Cell, + } + + impl BlockDev for Refusing { + fn blocks(&self) -> u64 { + self.bytes.blocks() + } + async fn read(&self, lba: u64, out: &mut [Block]) -> Result<(), Error> { + if (lba..lba + out.len() as u64).contains(&self.refused.get()) { + return Err(Error::Device); + } + self.bytes.read(lba, out).await + } + async fn write(&self, lba: u64, data: &[Block]) -> Result<(), Error> { + self.bytes.write(lba, data).await + } + async fn flush(&self) -> Result<(), Error> { + Ok(()) + } + } + + fn refusing() -> Refusing { + Refusing { bytes: Bytes::new(vec![0x5A; 4 * BLOCK]), refused: Cell::new(1) } + } + + /// A read the device refused is the device's word to the caller, never a + /// buffer of zeros: a caller that took zeros for data merges its bytes + /// into them and writes the result over what the medium held. + #[test] + fn a_refused_read_is_an_error_and_never_zeros() { + let dev = refusing(); + let mut buf = [0xEE; 100]; + assert_eq!(block_on_ready(dev.read_at(BLOCK as u64 + 10, &mut buf)), Err(Error::Device)); + let mut fine = [0; 100]; + block_on_ready(dev.read_at(10, &mut fine)).unwrap(); + assert!(fine.iter().all(|&x| x == 0x5A), "the readable block reads as written"); + } + + /// A write that covers part of a block reads the rest of it first; when + /// that read is refused the write is refused whole, and nothing reaches + /// the block — the rest of it is another structure's. + #[test] + fn a_partial_write_over_an_unreadable_block_writes_nothing() { + let dev = refusing(); + assert_eq!(block_on_ready(dev.write_at(BLOCK as u64 + 10, &[1, 2, 3])), Err(Error::Device)); + assert!(dev.bytes.get().iter().all(|&x| x == 0x5A), "the unreadable block was written over"); + } + + /// A part block at either end of a range keeps the bytes the range does + /// not cover. + #[test] + fn a_write_across_part_blocks_keeps_what_it_does_not_cover() { + let dev = Bytes::new(vec![0x5A; 3 * BLOCK]); + block_on_ready(dev.write_at(BLOCK as u64 - 2, &[1; BLOCK + 4])).unwrap(); + let held = dev.get(); + assert!(held[..BLOCK - 2].iter().all(|&x| x == 0x5A)); + assert!(held[BLOCK - 2..2 * BLOCK + 2].iter().all(|&x| x == 1)); + assert!(held[2 * BLOCK + 2..].iter().all(|&x| x == 0x5A)); + } } diff --git a/lib/fat32/src/dir.rs b/lib/fat32/src/dir.rs index 653f7c687db..dc1914327f4 100644 --- a/lib/fat32/src/dir.rs +++ b/lib/fat32/src/dir.rs @@ -1,7 +1,8 @@ use alloc::string::String; use crate::boot::Cluster; -use crate::device::BlockAccess; +use toyos_blockdev::BlockDev; +use crate::device::Volume as _; use crate::error::Error; use crate::fat::END_OF_CHAIN; use crate::fs::{Fat32, Loc}; @@ -163,7 +164,7 @@ impl EntryCursor { EntryCursor { dir_start, cluster: dir_start, cluster_index: 0 } } - pub fn offset_of( + pub async fn offset_of( &mut self, fs: &mut Fat32, index: u32, @@ -177,7 +178,7 @@ impl EntryCursor { self.cluster_index = 0; } while self.cluster_index < want_cluster { - match fs.next_cluster(self.cluster)? { + match fs.next_cluster(self.cluster).await? { Some(next) => { self.cluster = next; self.cluster_index += 1; @@ -248,7 +249,7 @@ impl DirScan { /// The next real entry, skipping free slots, long-name runs, volume /// labels and the dot entries. - pub fn next(&mut self, fs: &mut Fat32) -> Result, Error> { + pub async fn next(&mut self, fs: &mut Fat32) -> Result, Error> { if self.done { return Ok(None); } @@ -257,11 +258,11 @@ impl DirScan { self.done = true; return Err(Error::CorruptDirectory); } - let Some(offset) = self.cursor.offset_of(fs, self.index)? else { + let Some(offset) = self.cursor.offset_of(fs, self.index).await? else { self.done = true; return Ok(None); }; - let raw = fs.read_entry_at(offset)?; + let raw = fs.read_entry_at(offset).await?; let index = self.index; self.index += 1; @@ -368,12 +369,12 @@ impl DirScan { } } -impl Fat32 { - pub(crate) fn read_entry_at(&mut self, offset: u64) -> Result { +impl Fat32 { + pub(crate) async fn read_entry_at(&mut self, offset: u64) -> Result { let bps = self.geometry().bytes_per_sector as u64; let sector = offset / bps * bps; let within = (offset - sector) as usize; - self.load_sector(sector)?; + self.load_sector(sector).await?; let bytes = self .sector() .get(within..within + ENTRY_SIZE as usize) @@ -384,34 +385,34 @@ impl Fat32 { } /// Write a directory entry, queueing what it held as the repair. - pub(crate) fn write_entry_at(&mut self, offset: u64, entry: &RawEntry) -> Result<(), Error> { - let raw = self.read_entry_at(offset)?; + pub(crate) async fn write_entry_at(&mut self, offset: u64, entry: &RawEntry) -> Result<(), Error> { + let raw = self.read_entry_at(offset).await?; self.queue(Repair::Entry { offset, raw }); - self.put_entry_at(offset, entry) + self.put_entry_at(offset, entry).await } /// Write a directory entry with no repair: for the repair itself, and for /// a cluster this call claimed, which nothing reaches until it succeeds. - pub(crate) fn put_entry_at(&mut self, offset: u64, entry: &RawEntry) -> Result<(), Error> { + pub(crate) async fn put_entry_at(&mut self, offset: u64, entry: &RawEntry) -> Result<(), Error> { self.invalidate_sector(); - self.device().write_at(offset, &entry.0)?; + self.dev.write_at(offset, &entry.0).await?; Ok(()) } /// Entries the directory's currently allocated clusters can hold. - fn dir_capacity(&mut self, dir_start: Cluster) -> Result { + async fn dir_capacity(&mut self, dir_start: Cluster) -> Result { let per_cluster = self.geometry().bytes_per_cluster() / ENTRY_SIZE; let max_clusters = (MAX_DIR_ENTRIES / per_cluster).max(1) as u64; - let clusters = self.chain_len(dir_start, max_clusters)?; + let clusters = self.chain_len(dir_start, max_clusters).await?; Ok((clusters as u32).saturating_mul(per_cluster).min(MAX_DIR_ENTRIES)) } /// The index of the first run of `count` consecutive free slots, growing /// the directory by a cluster if there is no such run. - fn find_free_run(&mut self, dir_start: Cluster, count: u32) -> Result { - let mut capacity = self.dir_capacity(dir_start)?; + async fn find_free_run(&mut self, dir_start: Cluster, count: u32) -> Result { + let mut capacity = self.dir_capacity(dir_start).await?; loop { - if let Some(index) = self.scan_free_run(dir_start, count, capacity)? { + if let Some(index) = self.scan_free_run(dir_start, count, capacity).await? { return Ok(index); } let per_cluster = self.geometry().bytes_per_cluster() / ENTRY_SIZE; @@ -419,15 +420,15 @@ impl Fat32 { return Err(Error::NoSpace); } let max_clusters = (MAX_DIR_ENTRIES / per_cluster).max(1) as u64; - let last = self.chain_last(dir_start, max_clusters)?; - let new = self.alloc_zeroed_cluster()?; + let last = self.chain_last(dir_start, max_clusters).await?; + let new = self.alloc_zeroed_cluster().await?; self.queue(Repair::Put { cluster: last, value: END_OF_CHAIN }); - self.set_fat_entry(last, new.raw())?; + self.set_fat_entry(last, new.raw()).await?; capacity += per_cluster; } } - fn scan_free_run( + async fn scan_free_run( &mut self, dir_start: Cluster, count: u32, @@ -437,8 +438,8 @@ impl Fat32 { let mut run_start: Option = None; let mut index = 0; while index < capacity { - let Some(offset) = cursor.offset_of(self, index)? else { break }; - let raw = self.read_entry_at(offset)?; + let Some(offset) = cursor.offset_of(self, index).await? else { break }; + let raw = self.read_entry_at(offset).await?; if raw.is_free() { let start = *run_start.get_or_insert(index); if raw.is_end() { @@ -457,9 +458,9 @@ impl Fat32 { Ok(None) } - fn short_name_taken(&mut self, dir_start: Cluster, short: &ShortName) -> Result { + async fn short_name_taken(&mut self, dir_start: Cluster, short: &ShortName) -> Result { let mut scan = DirScan::new(dir_start); - while let Some(loc) = scan.next(self)? { + while let Some(loc) = scan.next(self).await? { if &loc.raw.short() == short { return Ok(true); } @@ -479,7 +480,7 @@ impl Fat32 { /// `groups` entries too high for any name with a long-name run, and would /// have orphaned those entries the first time something erased through a /// handle. - pub(crate) fn insert_entry( + pub(crate) async fn insert_entry( &mut self, dir_start: Cluster, name: &str, @@ -493,7 +494,7 @@ impl Fat32 { let mut found = false; for n in 0..MAX_SHORT_NAME_CANDIDATES { let candidate = name::candidate(&basis, name, n); - if !self.short_name_taken(dir_start, &candidate)? { + if !self.short_name_taken(dir_start, &candidate).await? { short = candidate; found = true; break; @@ -509,7 +510,7 @@ impl Fat32 { name::lfn_groups(name)? }; let total = groups as u32 + 1; - let start = self.find_free_run(dir_start, total)?; + let start = self.find_free_run(dir_start, total).await?; let checksum = name::lfn_checksum(&short); let mut cursor = EntryCursor::new(dir_start); @@ -528,41 +529,41 @@ impl Fat32 { } } let index = start + (groups - 1 - g) as u32; - let offset = cursor.offset_of(self, index)?.ok_or(Error::NoSpace)?; - self.write_entry_at(offset, &raw)?; + let offset = cursor.offset_of(self, index).await?.ok_or(Error::NoSpace)?; + self.write_entry_at(offset, &raw).await?; } let mut entry = *template; entry.set_short(&short); entry.0[12] = 0; let index = start + groups as u32; - let offset = cursor.offset_of(self, index)?.ok_or(Error::NoSpace)?; - self.write_entry_at(offset, &entry)?; + let offset = cursor.offset_of(self, index).await?.ok_or(Error::NoSpace)?; + self.write_entry_at(offset, &entry).await?; Ok((Loc { dir_start, first_index: start, index, entry_offset: offset }, entry)) } /// Mark every entry of a run free. Does not touch the cluster chain — a /// rename moves an entry and must not free what it still points at. - pub(crate) fn erase_entries(&mut self, loc: Loc) -> Result<(), Error> { + pub(crate) async fn erase_entries(&mut self, loc: Loc) -> Result<(), Error> { let mut cursor = EntryCursor::new(loc.dir_start); for index in loc.first_index..=loc.index { - let Some(offset) = cursor.offset_of(self, index)? else { break }; - let mut raw = self.read_entry_at(offset)?; + let Some(offset) = cursor.offset_of(self, index).await? else { break }; + let mut raw = self.read_entry_at(offset).await?; raw.0[0] = FREE; - self.write_entry_at(offset, &raw)?; + self.write_entry_at(offset, &raw).await?; } Ok(()) } /// Whether a directory holds nothing but `.`, `..` and free slots. - pub(crate) fn dir_is_empty(&mut self, dir_start: Cluster) -> Result { + pub(crate) async fn dir_is_empty(&mut self, dir_start: Cluster) -> Result { let mut scan = DirScan::new(dir_start); - Ok(scan.next(self)?.is_none()) + Ok(scan.next(self).await?.is_none()) } /// Write the `.` and `..` pair a new directory must begin with, into a /// cluster this call claimed and nothing reaches yet. - pub(crate) fn init_dot_entries( + pub(crate) async fn init_dot_entries( &mut self, cluster: Cluster, parent: Cluster, @@ -579,8 +580,8 @@ impl Fat32 { raw.set_first_cluster(target); raw.set_create_time(time); raw.set_write_time(time); - let offset = cursor.offset_of(self, index as u32)?.ok_or(Error::NoSpace)?; - self.put_entry_at(offset, &raw)?; + let offset = cursor.offset_of(self, index as u32).await?.ok_or(Error::NoSpace)?; + self.put_entry_at(offset, &raw).await?; } Ok(()) } @@ -593,16 +594,16 @@ impl Fat32 { } /// Repoint a directory's `..` entry after it moves to a new parent. - pub(crate) fn set_dot_dot(&mut self, cluster: Cluster, parent: Cluster) -> Result<(), Error> { + pub(crate) async fn set_dot_dot(&mut self, cluster: Cluster, parent: Cluster) -> Result<(), Error> { let parent = self.dotdot_target(parent); let mut cursor = EntryCursor::new(cluster); - let Some(offset) = cursor.offset_of(self, 1)? else { return Err(Error::CorruptDirectory) }; - let mut raw = self.read_entry_at(offset)?; + let Some(offset) = cursor.offset_of(self, 1).await? else { return Err(Error::CorruptDirectory) }; + let mut raw = self.read_entry_at(offset).await?; if raw.0[..2] != *b".." { return Err(Error::CorruptDirectory); } raw.set_first_cluster(parent); - self.write_entry_at(offset, &raw) + self.write_entry_at(offset, &raw).await } } diff --git a/lib/fat32/src/error.rs b/lib/fat32/src/error.rs index ef36e24172c..9327cfa83c9 100644 --- a/lib/fat32/src/error.rs +++ b/lib/fat32/src/error.rs @@ -1,5 +1,3 @@ -use crate::device::IoError; - /// Everything that can go wrong, as data rather than a panic. /// /// Exhaustive on purpose: an adapter mapping these to `SyscallError` should @@ -47,11 +45,11 @@ pub enum Error { LimitExceeded, } -impl From for Error { - fn from(e: IoError) -> Self { - match e { - IoError::Device => Error::Io, - } +/// Every device refusal is one word here: this crate reads every refused +/// write as of unknown outcome (`crate::repair`), whatever the device said. +impl From for Error { + fn from(_: toyos_blockdev::Error) -> Self { + Error::Io } } diff --git a/lib/fat32/src/fat.rs b/lib/fat32/src/fat.rs index 593ce06b2ca..69c3574931b 100644 --- a/lib/fat32/src/fat.rs +++ b/lib/fat32/src/fat.rs @@ -1,5 +1,6 @@ use crate::boot::Cluster; -use crate::device::BlockAccess; +use toyos_blockdev::BlockDev; +use crate::device::Volume as _; use crate::error::Error; use crate::fs::Fat32; use crate::repair::Repair; @@ -14,16 +15,16 @@ pub(crate) const END_OF_CHAIN: u32 = 0x0FFF_FFFF; /// reads before it writes. const ENTRY_MASK: u32 = 0x0FFF_FFFF; -impl Fat32 { +impl Fat32 { /// One FAT entry, masked to its 28 significant bits. /// /// Reads the *active* FAT — the mirrors are written but never read, since /// a driver that reads one FAT and writes another has two answers for the /// same question and no way to say which is right. - pub(crate) fn fat_entry(&mut self, cluster: Cluster) -> Result { + pub(crate) async fn fat_entry(&mut self, cluster: Cluster) -> Result { let fat = self.geom.active_fat.unwrap_or(0); let mut raw = [0u8; 4]; - self.dev.read_at(self.geom.fat_entry_offset(fat, cluster), &mut raw)?; + self.dev.read_at(self.geom.fat_entry_offset(fat, cluster), &mut raw).await?; Ok(u32::from_le_bytes(raw) & ENTRY_MASK) } @@ -35,25 +36,25 @@ impl Fat32 { /// ([`crate::repair`]), and re-driving it rewrites every copy. The active /// copy goes last so that a refusal of the first write leaves the copy /// every read serves from as it was until the repair lands. - pub(crate) fn set_fat_entry(&mut self, cluster: Cluster, value: u32) -> Result<(), Error> { + pub(crate) async fn set_fat_entry(&mut self, cluster: Cluster, value: u32) -> Result<(), Error> { self.invalidate_sector(); let active = self.geom.active_fat.unwrap_or(0); for fat in self.geom.fat_mirrors() { if fat != active { - self.write_fat_entry(fat, cluster, value)?; + self.write_fat_entry(fat, cluster, value).await?; } } - self.write_fat_entry(active, cluster, value) + self.write_fat_entry(active, cluster, value).await } /// One FAT copy's entry, preserving the four reserved high bits the format /// leaves to whoever made the volume. - fn write_fat_entry(&mut self, fat: u32, cluster: Cluster, value: u32) -> Result<(), Error> { + async fn write_fat_entry(&mut self, fat: u32, cluster: Cluster, value: u32) -> Result<(), Error> { let offset = self.geom.fat_entry_offset(fat, cluster); let mut raw = [0u8; 4]; - self.dev.read_at(offset, &mut raw)?; + self.dev.read_at(offset, &mut raw).await?; let reserved = u32::from_le_bytes(raw) & !ENTRY_MASK; - Ok(self.dev.write_at(offset, &(reserved | (value & ENTRY_MASK)).to_le_bytes())?) + Ok(self.dev.write_at(offset, &(reserved | (value & ENTRY_MASK)).to_le_bytes()).await?) } /// The next cluster in a chain, or `None` at the end. @@ -68,8 +69,8 @@ impl Fat32 { /// A cluster linking to itself is rejected here because the check is free. /// A longer cycle is not detected — see [`Self::advance`] for why the /// bound rather than the detection is what makes that safe. - pub(crate) fn next_cluster(&mut self, cluster: Cluster) -> Result, Error> { - let v = self.fat_entry(cluster)?; + pub(crate) async fn next_cluster(&mut self, cluster: Cluster) -> Result, Error> { + let v = self.fat_entry(cluster).await?; if v >= 0x0FFF_FFF8 { return Ok(None); } @@ -100,14 +101,14 @@ impl Fat32 { /// instead is that no cycle can make a *write* unsound, and that is /// [`Self::verify_acyclic`] and [`Self::free_chain`]'s anchor rather than /// anything here. - pub(crate) fn advance( + pub(crate) async fn advance( &mut self, cluster: Cluster, steps: u64, ) -> Result, Error> { let mut c = cluster; for _ in 0..steps { - match self.next_cluster(c)? { + match self.next_cluster(c).await? { Some(next) => c = next, None => return Ok(None), } @@ -120,11 +121,11 @@ impl Fat32 { /// The refusal is what bounds a directory walk: a cyclic directory chain /// is indistinguishable from a very long one until you have counted past /// what the structure can legally be. - pub(crate) fn chain_len(&mut self, start: Cluster, limit: u64) -> Result { + pub(crate) async fn chain_len(&mut self, start: Cluster, limit: u64) -> Result { let mut c = start; let mut n = 1u64; loop { - match self.next_cluster(c)? { + match self.next_cluster(c).await? { Some(next) => { n += 1; if n > limit { @@ -138,10 +139,10 @@ impl Fat32 { } /// The last cluster of a chain, walking at most `limit` links. - pub(crate) fn chain_last(&mut self, start: Cluster, limit: u64) -> Result { + pub(crate) async fn chain_last(&mut self, start: Cluster, limit: u64) -> Result { let mut c = start; for _ in 0..limit { - match self.next_cluster(c)? { + match self.next_cluster(c).await? { Some(next) => c = next, None => return Ok(c), } @@ -155,7 +156,7 @@ impl Fat32 { /// Scans the FAT a sector at a time from the FSInfo hint, wrapping once. /// The hint is only a starting point: a hostile FSInfo can make the scan /// begin in the wrong place, which costs a wrap and nothing else. - pub(crate) fn alloc_cluster(&mut self) -> Result { + pub(crate) async fn alloc_cluster(&mut self) -> Result { let bps = self.geom.bytes_per_sector as u64; let entries_per_sector = bps / 4; let last_sector = self.geom.max_cluster() as u64 * 4 / bps; @@ -169,7 +170,7 @@ impl Fat32 { let base = sector * entries_per_sector; let offset = self.geom.fat_base_offset(fat) + sector * bps; let buf = self.scratch.get_mut(..bps as usize).ok_or(Error::CorruptChain)?; - self.dev.read_at(offset, buf)?; + self.dev.read_at(offset, buf).await?; for e in 0..entries_per_sector { let Ok(number) = u32::try_from(base + e) else { continue }; @@ -186,7 +187,7 @@ impl Fat32 { self.fsinfo.next_free = self.geom.cluster(number.saturating_add(1)); self.fsinfo.free_count = self.fsinfo.free_count.map(|n| n.saturating_sub(1)); self.fsinfo.dirty = true; - self.set_fat_entry(cluster, END_OF_CHAIN)?; + self.set_fat_entry(cluster, END_OF_CHAIN).await?; return Ok(cluster); } } @@ -199,10 +200,10 @@ impl Fat32 { /// The link is written after the new cluster is claimed and terminated, so /// a stop between the two leaks a cluster rather than producing a chain /// that runs into free space, which is a filesystem two files can share. - pub(crate) fn append_cluster(&mut self, last: Cluster) -> Result { - let new = self.alloc_cluster()?; + pub(crate) async fn append_cluster(&mut self, last: Cluster) -> Result { + let new = self.alloc_cluster().await?; self.queue(Repair::Put { cluster: last, value: END_OF_CHAIN }); - self.set_fat_entry(last, new.raw())?; + self.set_fat_entry(last, new.raw()).await?; Ok(new) } @@ -227,7 +228,7 @@ impl Fat32 { /// retained cluster closes the exit. /// /// Bounded by the volume's cluster count, which no legal chain exceeds. - pub(crate) fn free_chain( + pub(crate) async fn free_chain( &mut self, start: Cluster, anchor: Option, @@ -238,7 +239,7 @@ impl Fat32 { if Some(c) == anchor { return Err(Error::CorruptChain); } - let next = match self.next_cluster(c) { + let next = match self.next_cluster(c).await { Ok(next) => next, Err(Error::CorruptChain) => return Err(Error::CorruptChain), Err(e) => { @@ -250,7 +251,7 @@ impl Fat32 { self.queue(Repair::Free { start: n, anchor }); } self.queue(Repair::Put { cluster: c, value: 0 }); - self.set_fat_entry(c, 0)?; + self.set_fat_entry(c, 0).await?; self.repair.truncate(mark); self.released(c); match next { @@ -277,14 +278,14 @@ impl Fat32 { /// already walks the whole chain, so this adds half a walk to an /// operation that is O(chain) either way, and it runs *before* anything /// is written, so a cyclic chain leaves the volume untouched. - pub(crate) fn verify_acyclic(&mut self, start: Cluster) -> Result<(), Error> { + pub(crate) async fn verify_acyclic(&mut self, start: Cluster) -> Result<(), Error> { let mut slow = start; let mut fast = start; for _ in 0..self.geom.cluster_count as u64 { - let Some(once) = self.next_cluster(fast)? else { return Ok(()) }; - let Some(twice) = self.next_cluster(once)? else { return Ok(()) }; + let Some(once) = self.next_cluster(fast).await? else { return Ok(()) }; + let Some(twice) = self.next_cluster(once).await? else { return Ok(()) }; fast = twice; - slow = self.next_cluster(slow)?.ok_or(Error::CorruptChain)?; + slow = self.next_cluster(slow).await?.ok_or(Error::CorruptChain)?; if slow == fast { return Err(Error::CorruptChain); } @@ -297,13 +298,13 @@ impl Fat32 { /// /// A commit from its first write, like [`Self::free_chain`]: a refused /// terminator leaves itself and then the tail's free queued. - pub(crate) fn truncate_chain(&mut self, cluster: Cluster, tail: Cluster) -> Result<(), Error> { + pub(crate) async fn truncate_chain(&mut self, cluster: Cluster, tail: Cluster) -> Result<(), Error> { let mark = self.repair.len(); self.queue(Repair::Free { start: tail, anchor: Some(cluster) }); self.queue(Repair::Put { cluster, value: END_OF_CHAIN }); - self.set_fat_entry(cluster, END_OF_CHAIN)?; + self.set_fat_entry(cluster, END_OF_CHAIN).await?; self.repair.truncate(mark); - self.free_chain(tail, Some(cluster)) + self.free_chain(tail, Some(cluster)).await } /// Allocate a cluster with its contents zeroed, for a new directory. @@ -312,20 +313,20 @@ impl Fat32 { /// it allocates — but a directory's free slots are recognised by being /// zero, so a new directory cluster full of stale bytes would read as /// entries. - pub(crate) fn alloc_zeroed_cluster(&mut self) -> Result { - let cluster = self.alloc_cluster()?; - self.zero_cluster(cluster)?; + pub(crate) async fn alloc_zeroed_cluster(&mut self) -> Result { + let cluster = self.alloc_cluster().await?; + self.zero_cluster(cluster).await?; Ok(cluster) } - pub(crate) fn zero_cluster(&mut self, cluster: Cluster) -> Result<(), Error> { + pub(crate) async fn zero_cluster(&mut self, cluster: Cluster) -> Result<(), Error> { let bps = self.geom.bytes_per_sector as usize; let base = self.geom.cluster_offset(cluster); self.invalidate_sector(); let buf = self.scratch.get_mut(..bps).ok_or(Error::CorruptChain)?; buf.fill(0); for s in 0..self.geom.sectors_per_cluster as u64 { - self.dev.write_at(base + s * bps as u64, &self.scratch[..bps])?; + self.dev.write_at(base + s * bps as u64, &self.scratch[..bps]).await?; } Ok(()) } @@ -333,7 +334,7 @@ impl Fat32 { /// Count free clusters by scanning the FAT. /// /// Used when FSInfo has no answer, and to produce one worth writing back. - pub(crate) fn count_free(&mut self) -> Result { + pub(crate) async fn count_free(&mut self) -> Result { let bps = self.geom.bytes_per_sector as u64; let entries_per_sector = bps / 4; let last_sector = self.geom.max_cluster() as u64 * 4 / bps; @@ -344,7 +345,7 @@ impl Fat32 { for sector in 0..=last_sector { let offset = self.geom.fat_base_offset(fat) + sector * bps; let buf = self.scratch.get_mut(..bps as usize).ok_or(Error::CorruptChain)?; - self.dev.read_at(offset, buf)?; + self.dev.read_at(offset, buf).await?; let base = sector * entries_per_sector; for e in 0..entries_per_sector { let Ok(number) = u32::try_from(base + e) else { continue }; diff --git a/lib/fat32/src/fs.rs b/lib/fat32/src/fs.rs index 72cdd5ea19e..4cf66180158 100644 --- a/lib/fat32/src/fs.rs +++ b/lib/fat32/src/fs.rs @@ -5,7 +5,8 @@ use alloc::vec; use alloc::vec::Vec; use crate::boot::{Cluster, FsInfo, Geometry}; -use crate::device::BlockAccess; +use toyos_blockdev::BlockDev; +use crate::device::Volume as _; use crate::dir::{DirScan, RawEntry, ATTR_ARCHIVE, ATTR_DIRECTORY, ATTR_READ_ONLY, MAX_DIR_ENTRIES}; use crate::error::Error; use crate::fat::END_OF_CHAIN; @@ -28,7 +29,7 @@ const MAX_WALK_DEPTH: usize = 32; static ZEROS: [u8; 512] = [0u8; 512]; /// A mounted FAT32 volume. -pub struct Fat32 { +pub struct Fat32 { pub(crate) dev: D, pub(crate) geom: Geometry, pub(crate) fsinfo: FsInfo, @@ -226,23 +227,23 @@ fn split_parent(path: &str) -> (&str, &str) { } } -impl Fat32 { +impl Fat32 { /// Read and validate the boot sector without taking ownership. /// - /// The kernel decides what to do with a partition before it mounts it, and + /// A server decides what to do with a partition before it mounts it, and /// that decision must not require a mount that might already have written /// something. Nothing on the mount path writes, so this is a total read — /// the crate as a whole writes plenty, which is its headline. - pub fn probe(dev: &mut D) -> Result { + pub async fn probe(dev: &D) -> Result { let mut boot = [0u8; 512]; - dev.read_at(0, &mut boot)?; - Geometry::parse(&boot, dev.capacity()) + dev.read_at(0, &mut boot).await?; + Geometry::parse(&boot, dev.blocks().saturating_mul(toyos_blockdev::BLOCK as u64)) } - pub fn mount(mut dev: D) -> Result, Error> { - let geom = Self::probe(&mut dev)?; + pub async fn mount(dev: D) -> Result, Error> { + let geom = Self::probe(&dev).await?; let mut scratch = vec![0u8; geom.bytes_per_sector as usize]; - let fsinfo = FsInfo::read(&mut dev, &geom, &mut scratch); + let fsinfo = FsInfo::read(&dev, &geom, &mut scratch).await; Ok(Fat32 { dev, geom, @@ -268,13 +269,13 @@ impl Fat32 { self.dev } - pub(crate) fn load_sector(&mut self, offset: u64) -> Result<(), Error> { + pub(crate) async fn load_sector(&mut self, offset: u64) -> Result<(), Error> { if self.scratch_at == Some(offset) { return Ok(()); } let bps = self.geom.bytes_per_sector as usize; let buf = self.scratch.get_mut(..bps).ok_or(Error::Io)?; - self.dev.read_at(offset, buf)?; + self.dev.read_at(offset, buf).await?; self.scratch_at = Some(offset); Ok(()) } @@ -289,13 +290,13 @@ impl Fat32 { // ---------------------------------------------------------------- lookup - fn find_in_dir( + async fn find_in_dir( &mut self, dir_start: Cluster, name: &str, ) -> Result, Error> { let mut scan = DirScan::new(dir_start); - while let Some(found) = scan.next(self)? { + while let Some(found) = scan.next(self).await? { if scan.name_eq(&found, name) { let loc = Loc { dir_start, @@ -334,14 +335,14 @@ impl Fat32 { Ok(Node { raw: *raw, first_cluster: first, loc: Some(loc) }) } - fn resolve(&mut self, path: &str) -> Result { + async fn resolve(&mut self, path: &str) -> Result { let mut node = self.root_node(); for comp in components(path) { if !node.raw.is_dir() { return Err(Error::NotADirectory); } let dir_start = node.first_cluster.ok_or(Error::NotADirectory)?; - let Some((raw, loc)) = self.find_in_dir(dir_start, comp)? else { + let Some((raw, loc)) = self.find_in_dir(dir_start, comp).await? else { return Err(Error::NotFound); }; node = self.node_from_entry(&raw, loc)?; @@ -349,16 +350,16 @@ impl Fat32 { Ok(node) } - fn resolve_dir(&mut self, path: &str) -> Result { - let node = self.resolve(path)?; + async fn resolve_dir(&mut self, path: &str) -> Result { + let node = self.resolve(path).await?; if !node.raw.is_dir() { return Err(Error::NotADirectory); } node.first_cluster.ok_or(Error::CorruptDirectory) } - pub fn metadata(&mut self, path: &str) -> Result { - let node = self.resolve(path)?; + pub async fn metadata(&mut self, path: &str) -> Result { + let node = self.resolve(path).await?; Ok(Metadata { len: if node.raw.is_dir() { 0 } else { node.raw.size() as u64 }, is_dir: node.raw.is_dir(), @@ -367,8 +368,8 @@ impl Fat32 { }) } - pub fn exists(&mut self, path: &str) -> Result { - match self.resolve(path) { + pub async fn exists(&mut self, path: &str) -> Result { + match self.resolve(path).await { Ok(_) => Ok(true), Err(Error::NotFound) | Err(Error::NotADirectory) => Ok(false), Err(e) => Err(e), @@ -378,9 +379,9 @@ impl Fat32 { /// Whether two paths name the same directory entry — identity is the entry's /// location, so case and 8.3/long variants of one name match. `false` when /// `b` is absent. - pub fn same_entry(&mut self, a: &str, b: &str) -> Result { - let a_loc = self.resolve(a)?.loc; - let b_loc = match self.resolve(b) { + pub async fn same_entry(&mut self, a: &str, b: &str) -> Result { + let a_loc = self.resolve(a).await?.loc; + let b_loc = match self.resolve(b).await { Ok(node) => node.loc, Err(Error::NotFound) | Err(Error::NotADirectory) => return Ok(false), Err(e) => return Err(e), @@ -392,8 +393,8 @@ impl Fat32 { /// identity, so every spelling of one name answers it and no other entry /// shares it while that one exists. 0 for the root, which has no entry and /// whose offset no entry can have. - pub fn entry_offset(&mut self, path: &str) -> Result { - Ok(self.resolve(path)?.loc.map_or(0, |loc| loc.entry_offset)) + pub async fn entry_offset(&mut self, path: &str) -> Result { + Ok(self.resolve(path).await?.loc.map_or(0, |loc| loc.entry_offset)) } /// Every entry of one directory, refusing above `limit`. @@ -401,11 +402,11 @@ impl Fat32 { /// Refuses rather than truncating: a listing short of the truth is worse /// than no listing, because a caller checking that a name is absent gets a /// confident wrong answer. - pub fn read_dir(&mut self, path: &str, limit: usize) -> Result, Error> { - let dir_start = self.resolve_dir(path)?; + pub async fn read_dir(&mut self, path: &str, limit: usize) -> Result, Error> { + let dir_start = self.resolve_dir(path).await?; let mut out = Vec::new(); let mut scan = DirScan::new(dir_start); - while let Some(loc) = scan.next(self)? { + while let Some(loc) = scan.next(self).await? { if out.len() == limit { return Err(Error::LimitExceeded); } @@ -433,7 +434,7 @@ impl Fat32 { /// because the tree is on-disk data and a crafted volume can make it a /// graph. `limit` bounds files and directories alike; either exceeding it /// abandons the whole listing. - pub fn walk(&mut self, under: &str, limit: usize) -> Result, Error> { + pub async fn walk(&mut self, under: &str, limit: usize) -> Result, Error> { let mut out = Vec::new(); let mut visited = BTreeSet::new(); let mut queue: Vec<(Cluster, String, usize)> = Vec::new(); @@ -441,7 +442,7 @@ impl Fat32 { let (start, start_prefix) = if under.is_empty() { (self.geom.root(), String::new()) } else { - let node = self.resolve(under)?; + let node = self.resolve(under).await?; // Before the cluster is asked for: an empty file's first cluster is // `None`, which is what having no data looks like and not a corrupt // directory. @@ -465,7 +466,7 @@ impl Fat32 { while let Some((cluster, prefix, depth)) = queue.pop() { let mut scan = DirScan::new(cluster); - while let Some(loc) = scan.next(self)? { + while let Some(loc) = scan.next(self).await? { let name = scan.name_string(&loc); let mut path = String::with_capacity(prefix.len() + name.len() + 1); path.push_str(&prefix); @@ -501,8 +502,8 @@ impl Fat32 { /// Open a file. Directories are refused — nothing here reads a directory /// as a byte stream, and letting a caller try would hand it 32-byte /// entries it has no way to interpret. - pub fn open(&mut self, path: &str) -> Result { - let node = self.resolve(path)?; + pub async fn open(&mut self, path: &str) -> Result { + let node = self.resolve(path).await?; if node.raw.is_dir() { return Err(Error::IsADirectory); } @@ -531,8 +532,8 @@ impl Fat32 { /// /// The cost is one 32-byte read, and it is almost always the sector /// already in `scratch`. - fn live_entry(&mut self, f: &File) -> Result { - let raw = self.read_entry_at(f.loc.entry_offset)?; + async fn live_entry(&mut self, f: &File) -> Result { + let raw = self.read_entry_at(f.loc.entry_offset).await?; if raw.is_free() || raw.is_lfn() || EntryIdentity::of(&raw) != f.identity @@ -545,7 +546,7 @@ impl Fat32 { /// The cluster holding chain index `index`, walking from the handle's last /// known position when that is not behind it. - fn cluster_at(&mut self, f: &mut File, index: u32) -> Result, Error> { + async fn cluster_at(&mut self, f: &mut File, index: u32) -> Result, Error> { let Some(first) = f.first_cluster else { return Ok(None) }; let (from, steps) = match f.hint { Some((hint_index, hint_cluster)) if hint_index <= index => { @@ -553,7 +554,7 @@ impl Fat32 { } _ => (first, index as u64), }; - let found = self.advance(from, steps)?; + let found = self.advance(from, steps).await?; if let Some(c) = found { f.hint = Some((index, c)); } @@ -563,11 +564,11 @@ impl Fat32 { /// How many clusters past `index` are physically contiguous with it, up to /// `want`. Turns a sequential read of a defragmented file into one device /// call per run instead of one per cluster. - fn contiguous_run(&mut self, start: Cluster, want: u64) -> Result<(u64, Cluster), Error> { + async fn contiguous_run(&mut self, start: Cluster, want: u64) -> Result<(u64, Cluster), Error> { let mut run = 1u64; let mut last = start; while run < want { - match self.next_cluster(last)? { + match self.next_cluster(last).await? { Some(next) if next.raw() == last.raw() + 1 => { last = next; run += 1; @@ -578,7 +579,7 @@ impl Fat32 { Ok((run, last)) } - pub fn read(&mut self, f: &mut File, offset: u64, buf: &mut [u8]) -> Result { + pub async fn read(&mut self, f: &mut File, offset: u64, buf: &mut [u8]) -> Result { if offset >= f.size as u64 { return Ok(0); } @@ -590,15 +591,15 @@ impl Fat32 { let pos = offset + done as u64; let index = (pos / bpc) as u32; let within = pos % bpc; - let cluster = self.cluster_at(f, index)?.ok_or(Error::CorruptChain)?; + let cluster = self.cluster_at(f, index).await?.ok_or(Error::CorruptChain)?; let want = (within + (n - done) as u64).div_ceil(bpc); - let (run, last) = self.contiguous_run(cluster, want)?; + let (run, last) = self.contiguous_run(cluster, want).await?; f.hint = Some((index + run as u32 - 1, last)); let chunk = ((run * bpc - within) as usize).min(n - done); let dst = buf.get_mut(done..done + chunk).ok_or(Error::Io)?; let at = self.geom.cluster_offset(cluster) + within; - self.dev.read_at(at, dst)?; + self.dev.read_at(at, dst).await?; done += chunk; } Ok(n) @@ -613,7 +614,7 @@ impl Fat32 { /// /// The repair it leaves is two steps however far it grew: terminate the /// chain where it ended, then free what was grown past it. - fn ensure_capacity(&mut self, f: &mut File, need: u64) -> Result<(), Error> { + async fn ensure_capacity(&mut self, f: &mut File, need: u64) -> Result<(), Error> { if need == 0 { return Ok(()); } @@ -627,7 +628,7 @@ impl Fat32 { let first = match f.first_cluster { Some(c) => c, None => { - let c = self.alloc_cluster()?; + let c = self.alloc_cluster().await?; self.repair.truncate(mark); self.queue(Repair::Free { start: c, anchor: None }); grown = Some((None, c)); @@ -638,10 +639,10 @@ impl Fat32 { }; let (mut index, mut cluster) = f.hint.unwrap_or((0, first)); while (index as u64) + 1 < want { - cluster = match self.next_cluster(cluster)? { + cluster = match self.next_cluster(cluster).await? { Some(next) => next, None => { - let new = self.append_cluster(cluster)?; + let new = self.append_cluster(cluster).await?; let (end, head) = *grown.get_or_insert((Some(cluster), new)); self.repair.truncate(mark); self.queue(Repair::Free { start: head, anchor: None }); @@ -658,23 +659,23 @@ impl Fat32 { } /// Write bytes into space that is already allocated. - fn write_allocated(&mut self, f: &mut File, offset: u64, data: &[u8]) -> Result<(), Error> { + async fn write_allocated(&mut self, f: &mut File, offset: u64, data: &[u8]) -> Result<(), Error> { let bpc = self.geom.bytes_per_cluster() as u64; let mut done = 0usize; while done < data.len() { let pos = offset + done as u64; let index = (pos / bpc) as u32; let within = pos % bpc; - let cluster = self.cluster_at(f, index)?.ok_or(Error::CorruptChain)?; + let cluster = self.cluster_at(f, index).await?.ok_or(Error::CorruptChain)?; let want = (within + (data.len() - done) as u64).div_ceil(bpc); - let (run, last) = self.contiguous_run(cluster, want)?; + let (run, last) = self.contiguous_run(cluster, want).await?; f.hint = Some((index + run as u32 - 1, last)); let chunk = ((run * bpc - within) as usize).min(data.len() - done); let src = data.get(done..done + chunk).ok_or(Error::Io)?; let at = self.geom.cluster_offset(cluster) + within; self.invalidate_sector(); - self.dev.write_at(at, src)?; + self.dev.write_at(at, src).await?; done += chunk; } Ok(()) @@ -683,11 +684,11 @@ impl Fat32 { /// FAT has no holes, so a write past the end has to fill what it skipped. /// Newly allocated clusters hold whatever the last file to own them left /// behind, and handing that to a reader is a cross-file data leak. - fn zero_range(&mut self, f: &mut File, from: u64, to: u64) -> Result<(), Error> { + async fn zero_range(&mut self, f: &mut File, from: u64, to: u64) -> Result<(), Error> { let mut at = from; while at < to { let chunk = ((to - at) as usize).min(ZEROS.len()); - self.write_allocated(f, at, &ZEROS[..chunk])?; + self.write_allocated(f, at, &ZEROS[..chunk]).await?; at += chunk as u64; } Ok(()) @@ -712,7 +713,7 @@ impl Fat32 { /// Only the handle is updated; the directory entry still holds the old /// size until [`Fat32::flush_meta`]. That split is what lets a caller /// write a file in pages and pay for one metadata update. - pub fn write(&mut self, f: &mut File, offset: u64, data: &[u8]) -> Result<(), Error> { + pub async fn write(&mut self, f: &mut File, offset: u64, data: &[u8]) -> Result<(), Error> { let end = offset.checked_add(data.len() as u64).ok_or(Error::TooLarge)?; if end > MAX_FILE_SIZE { return Err(Error::TooLarge); @@ -721,14 +722,14 @@ impl Fat32 { return Ok(()); } let before = *f; - self.atomic(|fs| { - fs.live_entry(f)?; - fs.write_inner(f, offset, data, end, before.size as u64) - }) + self.atomic(async |fs| { + fs.live_entry(f).await?; + fs.write_inner(f, offset, data, end, before.size as u64).await + }).await .inspect_err(|_| *f = before) } - fn write_inner( + async fn write_inner( &mut self, f: &mut File, offset: u64, @@ -736,11 +737,11 @@ impl Fat32 { end: u64, old_size: u64, ) -> Result<(), Error> { - self.ensure_capacity(f, end)?; + self.ensure_capacity(f, end).await?; if offset > old_size { - self.zero_range(f, old_size, offset)?; + self.zero_range(f, old_size, offset).await?; } - self.write_allocated(f, offset, data)?; + self.write_allocated(f, offset, data).await?; if end > old_size { f.size = end as u32; } @@ -753,23 +754,23 @@ impl Fat32 { /// A growth that fails is undone like a failed [`Self::write`]. A shrink /// that fails once it has begun freeing has shrunk: the handle says so, /// and the rest of the free is queued (see [`Self::free_chain`]). - pub fn set_len(&mut self, f: &mut File, len: u64) -> Result<(), Error> { + pub async fn set_len(&mut self, f: &mut File, len: u64) -> Result<(), Error> { if len > MAX_FILE_SIZE { return Err(Error::TooLarge); } let before = *f; - self.atomic(|fs| { - fs.live_entry(f)?; + self.atomic(async |fs| { + fs.live_entry(f).await?; let old = f.size as u64; if len > old { - fs.ensure_capacity(f, len)?; - fs.zero_range(f, old, len)?; + fs.ensure_capacity(f, len).await?; + fs.zero_range(f, old, len).await?; f.size = len as u32; } else if len < old { - fs.shrink_chain(f, len)?; + fs.shrink_chain(f, len).await?; } Ok(()) - }) + }).await .inspect_err(|_| { if len > before.size as u64 { *f = before; @@ -781,7 +782,7 @@ impl Fat32 { /// the handle once the repair carries the shrink: a refusal before that /// has written nothing, and the handle's old size is what makes the retry /// shrink again. - fn shrink_chain(&mut self, f: &mut File, len: u64) -> Result<(), Error> { + async fn shrink_chain(&mut self, f: &mut File, len: u64) -> Result<(), Error> { let bpc = self.geom.bytes_per_cluster() as u64; let keep = len.div_ceil(bpc); f.hint = None; @@ -792,17 +793,17 @@ impl Fat32 { // Before anything is written: this is the one operation that keeps // part of a chain and frees the rest, and a loop between the two // halves is how live clusters reach the allocator. - self.verify_acyclic(first)?; + self.verify_acyclic(first).await?; if keep == 0 { f.first_cluster = None; f.size = len as u32; - return self.free_chain(first, None); + return self.free_chain(first, None).await; } - let last = self.advance(first, keep - 1)?.ok_or(Error::CorruptChain)?; - let tail = self.next_cluster(last)?; + let last = self.advance(first, keep - 1).await?.ok_or(Error::CorruptChain)?; + let tail = self.next_cluster(last).await?; f.size = len as u32; match tail { - Some(tail) => self.truncate_chain(last, tail), + Some(tail) => self.truncate_chain(last, tail).await, None => Ok(()), } } @@ -812,16 +813,16 @@ impl Fat32 { /// /// Refuses if the entry no longer holds the cluster this handle last saw /// written there — see [`File`]. - pub fn flush_meta(&mut self, f: &mut File, time: FatTime) -> Result<(), Error> { - self.atomic(|fs| fs.record_meta(f, time)) + pub async fn flush_meta(&mut self, f: &mut File, time: FatTime) -> Result<(), Error> { + self.atomic(async |fs| fs.record_meta(f, time).await).await } - fn record_meta(&mut self, f: &mut File, time: FatTime) -> Result<(), Error> { - let mut raw = self.live_entry(f)?; + async fn record_meta(&mut self, f: &mut File, time: FatTime) -> Result<(), Error> { + let mut raw = self.live_entry(f).await?; raw.set_first_cluster(f.first_cluster.map_or(0, Cluster::raw)); raw.set_size(f.size); raw.set_write_time(time); - self.write_entry_at(f.loc.entry_offset, &raw)?; + self.write_entry_at(f.loc.entry_offset, &raw).await?; f.entry_cluster = f.first_cluster; f.entry_size = f.size; Ok(()) @@ -842,15 +843,15 @@ impl Fat32 { /// A caller that does not reach this — a reset, a panic — leaves the /// inconsistency, which is the honest limit: FAT has no journal and this /// crate does not pretend to one. - pub fn reconcile(&mut self, f: &mut File, time: FatTime) -> Result<(), Error> { - self.atomic(|fs| { - fs.live_entry(f)?; + pub async fn reconcile(&mut self, f: &mut File, time: FatTime) -> Result<(), Error> { + self.atomic(async |fs| { + fs.live_entry(f).await?; // Before the entry: an entry naming a size the chain does not cover // is a reader walking off the end of a chain, which is worse than // the slack this is here to remove. - fs.shrink_chain(f, f.size as u64)?; - fs.record_meta(f, time) - }) + fs.shrink_chain(f, f.size as u64).await?; + fs.record_meta(f, time).await + }).await } /// The device byte ranges holding a file's data, coalesced. @@ -859,8 +860,8 @@ impl Fat32 { /// back through this crate. Bounded by `max`: a fragmented file has one /// extent per cluster, and how many of those a caller can hold is the /// caller's business, not the volume's. - pub fn extents(&mut self, path: &str, max: usize) -> Result, Error> { - let node = self.resolve(path)?; + pub async fn extents(&mut self, path: &str, max: usize) -> Result, Error> { + let node = self.resolve(path).await?; if node.raw.is_dir() { return Err(Error::IsADirectory); } @@ -874,7 +875,7 @@ impl Fat32 { let mut cluster = node.first_cluster.ok_or(Error::CorruptDirectory)?; while covered < size { let want = (size - covered).div_ceil(bpc); - let (run, last) = self.contiguous_run(cluster, want)?; + let (run, last) = self.contiguous_run(cluster, want).await?; let len = (run * bpc).min(size - covered); if out.len() == max { return Err(Error::LimitExceeded); @@ -884,39 +885,39 @@ impl Fat32 { if covered >= size { break; } - cluster = self.next_cluster(last)?.ok_or(Error::CorruptChain)?; + cluster = self.next_cluster(last).await?.ok_or(Error::CorruptChain)?; } Ok(out) } // ------------------------------------------------------------ namespace - fn parent_of(&mut self, path: &str) -> Result<(Cluster, String), Error> { + async fn parent_of(&mut self, path: &str) -> Result<(Cluster, String), Error> { let (parent, name) = split_parent(path); if name.is_empty() { return Err(Error::InvalidName); } - let dir = self.resolve_dir(parent)?; + let dir = self.resolve_dir(parent).await?; Ok((dir, String::from(name))) } /// Create an empty file. Fails if anything of that name already exists — /// a create that silently opened an existing file would let a caller /// believe it owns bytes another writer put there. - pub fn create(&mut self, path: &str, time: FatTime) -> Result { - self.atomic(|fs| fs.create_file(path, time)) + pub async fn create(&mut self, path: &str, time: FatTime) -> Result { + self.atomic(async |fs| fs.create_file(path, time).await).await } - fn create_file(&mut self, path: &str, time: FatTime) -> Result { - let (dir, name) = self.parent_of(path)?; - if self.find_in_dir(dir, &name)?.is_some() { + async fn create_file(&mut self, path: &str, time: FatTime) -> Result { + let (dir, name) = self.parent_of(path).await?; + if self.find_in_dir(dir, &name).await?.is_some() { return Err(Error::AlreadyExists); } let mut template = RawEntry::zeroed(); template.set_attr(ATTR_ARCHIVE); template.set_create_time(time); template.set_write_time(time); - let (loc, written) = self.insert_entry(dir, &name, &template)?; + let (loc, written) = self.insert_entry(dir, &name, &template).await?; Ok(File { loc, identity: EntryIdentity::of(&written), @@ -928,13 +929,13 @@ impl Fat32 { }) } - pub fn create_dir(&mut self, path: &str, time: FatTime) -> Result<(), Error> { - self.atomic(|fs| fs.make_dir(path, time)) + pub async fn create_dir(&mut self, path: &str, time: FatTime) -> Result<(), Error> { + self.atomic(async |fs| fs.make_dir(path, time).await).await } - fn make_dir(&mut self, path: &str, time: FatTime) -> Result<(), Error> { - let (dir, name) = self.parent_of(path)?; - if self.find_in_dir(dir, &name)?.is_some() { + async fn make_dir(&mut self, path: &str, time: FatTime) -> Result<(), Error> { + let (dir, name) = self.parent_of(path).await?; + if self.find_in_dir(dir, &name).await?.is_some() { return Err(Error::AlreadyExists); } name::validate_component(&name)?; @@ -942,30 +943,30 @@ impl Fat32 { // The cluster is prepared before the entry that names it exists, so a // stop here leaks a cluster rather than leaving a directory entry // pointing at uninitialised bytes that would read as entries. - let cluster = self.alloc_zeroed_cluster()?; - self.init_dot_entries(cluster, dir, time)?; + let cluster = self.alloc_zeroed_cluster().await?; + self.init_dot_entries(cluster, dir, time).await?; let mut template = RawEntry::zeroed(); template.set_attr(ATTR_DIRECTORY); template.set_first_cluster(cluster.raw()); template.set_create_time(time); template.set_write_time(time); - self.insert_entry(dir, &name, &template)?; + self.insert_entry(dir, &name, &template).await?; Ok(()) } /// Create every missing directory along a path. - pub fn create_dir_all(&mut self, path: &str, time: FatTime) -> Result<(), Error> { + pub async fn create_dir_all(&mut self, path: &str, time: FatTime) -> Result<(), Error> { let mut so_far = String::new(); for comp in components(path) { if !so_far.is_empty() { so_far.push('/'); } so_far.push_str(comp); - match self.metadata(&so_far) { + match self.metadata(&so_far).await { Ok(m) if m.is_dir => continue, Ok(_) => return Err(Error::NotADirectory), - Err(Error::NotFound) => self.create_dir(&so_far, time)?, + Err(Error::NotFound) => self.create_dir(&so_far, time).await?, Err(e) => return Err(e), } } @@ -989,17 +990,17 @@ impl Fat32 { /// corrupt part way through the free is the one `Err` after the erase — /// the name is gone, what could not be followed leaks, and that answer is /// its only record. - pub fn remove(&mut self, path: &str) -> Result<(), Error> { - self.atomic(|fs| { - let (dir, name) = fs.parent_of(path)?; - let Some((raw, loc)) = fs.find_in_dir(dir, &name)? else { + pub async fn remove(&mut self, path: &str) -> Result<(), Error> { + self.atomic(async |fs| { + let (dir, name) = fs.parent_of(path).await?; + let Some((raw, loc)) = fs.find_in_dir(dir, &name).await? else { return Err(Error::NotFound); }; if raw.is_dir() { return Err(Error::IsADirectory); } - fs.erase_committed(loc, fs.geom.cluster(raw.first_cluster())) - }) + fs.erase_committed(loc, fs.geom.cluster(raw.first_cluster())).await + }).await } /// Delete an empty directory. @@ -1007,10 +1008,10 @@ impl Fat32 { /// Same order as [`Self::remove`]. A directory entry whose cluster is /// outside the volume has no contents to check and none to free, so it is /// simply erased — refusing would make it permanent. - pub fn remove_dir(&mut self, path: &str) -> Result<(), Error> { - self.atomic(|fs| { - let (dir, name) = fs.parent_of(path)?; - let Some((raw, loc)) = fs.find_in_dir(dir, &name)? else { + pub async fn remove_dir(&mut self, path: &str) -> Result<(), Error> { + self.atomic(async |fs| { + let (dir, name) = fs.parent_of(path).await?; + let Some((raw, loc)) = fs.find_in_dir(dir, &name).await? else { return Err(Error::NotFound); }; if !raw.is_dir() { @@ -1018,18 +1019,18 @@ impl Fat32 { } let cluster = fs.geom.cluster(raw.first_cluster()); if let Some(c) = cluster { - if !fs.dir_is_empty(c)? { + if !fs.dir_is_empty(c).await? { return Err(Error::DirectoryNotEmpty); } } - fs.erase_committed(loc, cluster) - }) + fs.erase_committed(loc, cluster).await + }).await } /// Erase an entry run, then queue the free of the chain it named: undone /// up to the last erase, carried forward from it by [`Self::atomic`]. - fn erase_committed(&mut self, loc: Loc, chain: Option) -> Result<(), Error> { - self.erase_entries(loc)?; + async fn erase_committed(&mut self, loc: Loc, chain: Option) -> Result<(), Error> { + self.erase_entries(loc).await?; self.commit(); if let Some(c) = chain { self.queue(Repair::Free { start: c, anchor: None }); @@ -1043,31 +1044,31 @@ impl Fat32 { /// replacement atomic, and a rename that deletes the destination first has /// a window in which neither name resolves — which is worse than an error /// the caller can act on. - pub fn rename(&mut self, from: &str, to: &str) -> Result<(), Error> { - self.atomic(|fs| fs.move_entry(from, to)) + pub async fn rename(&mut self, from: &str, to: &str) -> Result<(), Error> { + self.atomic(async |fs| fs.move_entry(from, to).await).await } - fn move_entry(&mut self, from: &str, to: &str) -> Result<(), Error> { - let node = self.resolve(from)?; + async fn move_entry(&mut self, from: &str, to: &str) -> Result<(), Error> { + let node = self.resolve(from).await?; let loc = node.loc.ok_or(Error::IsADirectory)?; - let (new_dir, new_name) = self.parent_of(to)?; - if self.find_in_dir(new_dir, &new_name)?.is_some() { + let (new_dir, new_name) = self.parent_of(to).await?; + if self.find_in_dir(new_dir, &new_name).await?.is_some() { return Err(Error::AlreadyExists); } // Moving a directory into itself would detach the subtree: the entry // naming it would live inside the tree it names. let moved_dir = if node.raw.is_dir() { node.first_cluster } else { None }; if let Some(c) = moved_dir { - if self.is_ancestor(c, new_dir)? { + if self.is_ancestor(c, new_dir).await? { return Err(Error::InvalidName); } } - self.insert_entry(new_dir, &new_name, &node.raw)?; - self.erase_entries(loc)?; + self.insert_entry(new_dir, &new_name, &node.raw).await?; + self.erase_entries(loc).await?; if let Some(c) = moved_dir { if new_dir != loc.dir_start { - self.set_dot_dot(c, new_dir)?; + self.set_dot_dot(c, new_dir).await?; } } Ok(()) @@ -1078,18 +1079,18 @@ impl Fat32 { /// The staged name costs four directory entries, claimed in `to`'s directory /// before anything is freed: an overwrite refuses on a full directory or /// volume where freeing the destination first would have made its own room. - pub fn replace_rename(&mut self, from: &str, to: &str) -> Result { - if !self.exists(to)? { - self.rename(from, to)?; + pub async fn replace_rename(&mut self, from: &str, to: &str) -> Result { + if !self.exists(to).await? { + self.rename(from, to).await?; return Ok(Replaced { temporary: None }); } - let temporary = self.replacement_temporary(to)?; - self.rename(to, &temporary)?; - if let Err(cause) = self.rename(from, to) { + let temporary = self.replacement_temporary(to).await?; + self.rename(to, &temporary).await?; + if let Err(cause) = self.rename(from, to).await { // The restore is the only thing that can put `to` back, so discarding // its failure leaves a destination nothing can name. - return Err(match self.rename(&temporary, to) { + return Err(match self.rename(&temporary, to).await { Ok(()) => ReplaceFailed { cause, stranded: None }, Err(_) => ReplaceFailed { cause, stranded: Some(temporary) }, }); @@ -1097,19 +1098,19 @@ impl Fat32 { Ok(Replaced { temporary: Some(temporary) }) } - pub fn release_replaced(&mut self, replaced: Replaced) -> Result<(), Error> { + pub async fn release_replaced(&mut self, replaced: Replaced) -> Result<(), Error> { match replaced.temporary { - Some(temporary) => self.remove(&temporary), + Some(temporary) => self.remove(&temporary).await, None => Ok(()), } } - fn replacement_temporary(&mut self, path: &str) -> Result { + async fn replacement_temporary(&mut self, path: &str) -> Result { let (parent, _) = split_parent(path); for sequence in 0..=MAX_DIR_ENTRIES { let leaf = format!(".toyos-replaced-{sequence:08x}.tmp"); let candidate = if parent.is_empty() { leaf } else { format!("{parent}/{leaf}") }; - if !self.exists(&candidate)? { + if !self.exists(&candidate).await? { return Ok(candidate); } } @@ -1117,7 +1118,7 @@ impl Fat32 { } /// Whether `dir` is `ancestor` or lives beneath it. - fn is_ancestor(&mut self, ancestor: Cluster, dir: Cluster) -> Result { + async fn is_ancestor(&mut self, ancestor: Cluster, dir: Cluster) -> Result { let mut queue = vec![ancestor]; let mut visited = BTreeSet::new(); visited.insert(ancestor); @@ -1127,7 +1128,7 @@ impl Fat32 { return Ok(true); } let mut scan = DirScan::new(cluster); - while let Some(loc) = scan.next(self)? { + while let Some(loc) = scan.next(self).await? { budget = budget.checked_sub(1).ok_or(Error::LimitExceeded)?; if !loc.raw.is_dir() { continue; @@ -1150,18 +1151,18 @@ impl Fat32 { /// a sync is a caller about to rely on the volume as it stands. A volume /// mounted with no count has it taken from the FAT rather than written as /// unknown. FSInfo is written before the flush so the flush covers it. - pub fn sync(&mut self) -> Result<(), Error> { - self.settle()?; + pub async fn sync(&mut self) -> Result<(), Error> { + self.settle().await?; if self.fsinfo.dirty { if self.fsinfo.free_count.is_none() { - self.fsinfo.free_count = Some(self.count_free()?); + self.fsinfo.free_count = Some(self.count_free().await?); } let Fat32 { dev, geom, fsinfo, scratch, scratch_at, .. } = self; *scratch_at = None; - fsinfo.write(dev, geom, scratch)?; + fsinfo.write(dev, geom, scratch).await?; fsinfo.dirty = false; } - self.dev.flush()?; + self.dev.flush().await?; Ok(()) } @@ -1170,12 +1171,12 @@ impl Fat32 { /// Uses the FSInfo hint when the volume had one and counts the FAT when it /// did not. The count is cached in the hint so the scan happens once, over /// a FAT whose refused writes have been re-driven. - pub fn free_bytes(&mut self) -> Result { + pub async fn free_bytes(&mut self) -> Result { let free = match self.fsinfo.free_count { Some(n) => n, None => { - self.settle()?; - let n = self.count_free()?; + self.settle().await?; + let n = self.count_free().await?; self.fsinfo.free_count = Some(n); self.fsinfo.dirty = true; n diff --git a/lib/fat32/src/lib.rs b/lib/fat32/src/lib.rs index 9eae7629358..f18b2e7df22 100644 --- a/lib/fat32/src/lib.rs +++ b/lib/fat32/src/lib.rs @@ -47,18 +47,22 @@ //! error from its own `create_symlink`, and `read_link` must always answer //! `None`. Succeeding silently would leave a caller holding a regular file //! it believes is a link. -//! - **No caching.** The kernel's page cache sits directly under -//! [`BlockAccess`]. A second cache here would be a coherence hazard for no -//! gain, since every FAT sector this crate re-reads is a hit in that one. +//! - **No caching.** A volume that wants a cache mounts over one: the file +//! server's is a [`BlockDev`](toyos_blockdev::BlockDev) under this crate. A +//! second cache here would be a coherence hazard for no gain, since every +//! FAT sector this crate re-reads is a hit in that one. //! - **No FAT12/FAT16.** The cluster count decides the FAT type, per the //! specification, and a volume with fewer than [`MIN_FAT32_CLUSTERS`] //! clusters is not FAT32 no matter what its boot sector says. //! //! # Shape //! -//! [`Fat32`] owns a [`BlockAccess`] and nothing else that persists. Path-based -//! calls ([`Fat32::create`], [`Fat32::remove`], [`Fat32::metadata`], …) resolve -//! from the root every time; [`Fat32::open`] hands back a [`File`] that caches +//! [`Fat32`] owns a [`BlockDev`](toyos_blockdev::BlockDev) and nothing else +//! that persists, and every call that reaches it is `async` (`src/device.rs` +//! is how bytes become its blocks); a caller with nothing else to run drives +//! one with `toyos_blockdev::block_on`. Path-based calls ([`Fat32::create`], +//! [`Fat32::remove`], [`Fat32::metadata`], …) resolve from the root every +//! time; [`Fat32::open`] hands back a [`File`] that caches //! the directory-entry location and a chain position, so repeated I/O on one //! file does not re-resolve the path or re-walk the chain from cluster zero. //! A [`File`] is plain data with no lifetime tie to the volume, so it can go @@ -82,7 +86,6 @@ mod repair; mod time; pub use boot::{Cluster, Geometry, MIN_FAT32_CLUSTERS}; -pub use device::{BlockAccess, IoError}; pub use dir::MAX_DIR_ENTRIES; pub use error::Error; pub use fs::{DirEntry, Extent, Fat32, File, Metadata, ReplaceFailed, Replaced}; diff --git a/lib/fat32/src/repair.rs b/lib/fat32/src/repair.rs index a2af2fb2e04..85e74476ded 100644 --- a/lib/fat32/src/repair.rs +++ b/lib/fat32/src/repair.rs @@ -3,7 +3,7 @@ //! //! A refused write may already be on the medium. So a refusal says //! nothing about which of the two states an entry is in, and nothing here reads -//! to find out — a cache under [`BlockAccess`] may still serve the bytes the +//! to find out — a cache under the [`BlockDev`] may still serve the bytes the //! refusal did not update. What is known is the state the call wanted and the //! state it started from, and writing either one is idempotent. //! @@ -52,7 +52,7 @@ //! on the mount path repairs them. use crate::boot::Cluster; -use crate::device::BlockAccess; +use toyos_blockdev::BlockDev; use crate::dir::RawEntry; use crate::error::Error; use crate::fs::Fat32; @@ -82,21 +82,21 @@ pub(crate) enum Repair { Entry { offset: u64, raw: RawEntry }, } -impl Fat32 { +impl Fat32 { /// Run one mutating call so that an `Err` leaves the volume where the /// repair takes it. /// /// A call inside another panics: the inner one's success would discard a /// repair its caller still needs, so a call built from other calls uses /// their bodies. - pub(crate) fn atomic( + pub(crate) async fn atomic( &mut self, - op: impl FnOnce(&mut Self) -> Result, + op: impl AsyncFnOnce(&mut Self) -> Result, ) -> Result { assert!(!self.in_call, "toyos-fat32: a mutating call nested inside another"); - self.settle()?; + self.settle().await?; self.in_call = true; - let result = op(self); + let result = op(self).await; self.in_call = false; let committed = core::mem::take(&mut self.committed); match result { @@ -105,9 +105,9 @@ impl Fat32 { // device still refuses stays queued for the next call to finish // and report. Ok(v) if committed => { - let mut driven = self.settle(); + let mut driven = self.settle().await; if driven == Err(Error::Io) { - driven = self.settle(); + driven = self.settle().await; } match driven { Ok(()) | Err(Error::Io) => Ok(v), @@ -121,7 +121,7 @@ impl Fat32 { Err(e) if self.repair.is_empty() => Err(e), // An unlanded re-drive is what the volume is waiting on, so it is // the answer rather than the error that started the rollback. - Err(e) => self.settle().map_or_else(Err, |()| Err(e)), + Err(e) => self.settle().await.map_or_else(Err, |()| Err(e)), } } @@ -148,23 +148,23 @@ impl Fat32 { } /// Re-drive every queued repair, the most recent write's first. - pub(crate) fn settle(&mut self) -> Result<(), Error> { + pub(crate) async fn settle(&mut self) -> Result<(), Error> { while let Some(&step) = self.repair.last() { match step { Repair::Put { cluster, value } => { - self.set_fat_entry(cluster, value)?; + self.set_fat_entry(cluster, value).await?; self.repair.pop(); if value == 0 { self.released(cluster); } } Repair::Entry { offset, raw } => { - self.put_entry_at(offset, &raw)?; + self.put_entry_at(offset, &raw).await?; self.repair.pop(); } Repair::Free { start, anchor } => { self.repair.pop(); - self.free_chain(start, anchor)?; + self.free_chain(start, anchor).await?; } } } diff --git a/lib/fat32/tests/common/mod.rs b/lib/fat32/tests/common/mod.rs index e228aafe57f..d94fc84aa52 100644 --- a/lib/fat32/tests/common/mod.rs +++ b/lib/fat32/tests/common/mod.rs @@ -8,97 +8,73 @@ //! code with this crate — so the ground truth on both sides of every test is //! something other than the driver under test. //! -//! **A device here standing in for the kernel adapter models that adapter's -//! cache wherever the two can differ.** A write refused after it reached the -//! medium leaves `FatDevice`'s resident blocks and the medium disagreeing, so a -//! device that refuses writes that way serves reads through [`AdapterCache`] -//! and never straight from the medium, and a judge of the volume reads the -//! medium and never through the crate's device. A device that refuses only -//! before a write is issued keeps the two equal and needs no model. +//! Every device here answers each request as it is asked, so a call is driven +//! with [`now`]. #![allow(dead_code)] +use std::cell::{Cell, RefCell}; use std::collections::HashMap; use std::fs::{File, OpenOptions}; +use std::future::Future; use std::io::Read; use std::os::unix::fs::FileExt; use std::path::{Path, PathBuf}; -use toyos_fat32::{BlockAccess, IoError}; +use toyos_blockdev::{block_on_ready, within, Block, BlockDev, Error as DevError, BLOCK}; use toyos_tmpdir::TempDir; +/// A call on a volume over devices that answer every request as it is asked. +pub fn now(call: F) -> F::Output { + block_on_ready(call) +} + +/// `buf.len()` bytes from `offset` of `dev`, read in the whole blocks they lie +/// on: the device's own bytes, not the crate's reading of them. +pub fn read_at(dev: &D, offset: u64, buf: &mut [u8]) { + let first = offset / BLOCK as u64; + let count = (offset + buf.len() as u64).div_ceil(BLOCK as u64) - first; + let mut blocks = vec![[0u8; BLOCK]; count as usize]; + now(dev.read(first, &mut blocks)).expect("a device read"); + let at = (offset % BLOCK as u64) as usize; + buf.copy_from_slice(&blocks.as_flattened()[at..at + buf.len()]); +} + // ---------------------------------------------------------------- devices -/// A device backed by a file, which only ever issues whole 4096-byte block -/// reads and writes to it. -/// -/// This is the kernel's `BlockDevice` shape, and it is what the tests run on -/// so that the claim in [`BlockAccess`]'s documentation — that a caller with a -/// 4096-byte block size can serve a driver working in 512-byte sectors — is -/// exercised rather than asserted. Every partial request becomes a -/// read-modify-write here, exactly as it will in the kernel adapter. +/// A device backed by a file, in whole blocks: what the crate's own bridge +/// from its sectors to a device's blocks is exercised against. pub struct BlockyFile { file: File, - capacity: u64, - block: usize, + blocks: u64, } impl BlockyFile { - pub fn open(path: &Path, block: usize) -> BlockyFile { + pub fn open(path: &Path) -> BlockyFile { let file = OpenOptions::new().read(true).write(true).open(path).expect("open image"); - let capacity = file.metadata().expect("stat image").len(); - BlockyFile { file, capacity, block } - } - - fn check(&self, offset: u64, len: usize) -> Result<(), IoError> { - let end = offset.checked_add(len as u64).ok_or(IoError::Device)?; - if end > self.capacity { - return Err(IoError::Device); - } - Ok(()) + let len = file.metadata().expect("stat image").len(); + assert!(len.is_multiple_of(BLOCK as u64), "{}: {len} bytes, no whole blocks", path.display()); + BlockyFile { file, blocks: len / BLOCK as u64 } } } -impl BlockAccess for BlockyFile { - fn capacity(&self) -> u64 { - self.capacity +impl BlockDev for BlockyFile { + fn blocks(&self) -> u64 { + self.blocks } - fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<(), IoError> { - self.check(offset, buf.len())?; - let mut block = vec![0u8; self.block]; - let mut done = 0usize; - while done < buf.len() { - let pos = offset + done as u64; - let base = pos / self.block as u64 * self.block as u64; - let within = (pos - base) as usize; - let n = (self.block - within).min(buf.len() - done); - self.file.read_exact_at(&mut block, base).map_err(|_| IoError::Device)?; - buf[done..done + n].copy_from_slice(&block[within..within + n]); - done += n; - } - Ok(()) + async fn read(&self, lba: u64, out: &mut [Block]) -> Result<(), DevError> { + within(lba, out.len(), self.blocks)?; + self.file.read_exact_at(out.as_flattened_mut(), lba * BLOCK as u64).map_err(|_| DevError::Device) } - fn write_at(&mut self, offset: u64, buf: &[u8]) -> Result<(), IoError> { - self.check(offset, buf.len())?; - let mut block = vec![0u8; self.block]; - let mut done = 0usize; - while done < buf.len() { - let pos = offset + done as u64; - let base = pos / self.block as u64 * self.block as u64; - let within = (pos - base) as usize; - let n = (self.block - within).min(buf.len() - done); - self.file.read_exact_at(&mut block, base).map_err(|_| IoError::Device)?; - block[within..within + n].copy_from_slice(&buf[done..done + n]); - self.file.write_all_at(&block, base).map_err(|_| IoError::Device)?; - done += n; - } - Ok(()) + async fn write(&self, lba: u64, data: &[Block]) -> Result<(), DevError> { + within(lba, data.len(), self.blocks)?; + self.file.write_all_at(data.as_flattened(), lba * BLOCK as u64).map_err(|_| DevError::Device) } - fn flush(&mut self) -> Result<(), IoError> { - self.file.sync_all().map_err(|_| IoError::Device) + async fn flush(&self) -> Result<(), DevError> { + self.file.sync_all().map_err(|_| DevError::Device) } } @@ -111,39 +87,38 @@ impl BlockAccess for BlockyFile { /// zeroes. #[derive(Clone)] pub struct SparseDevice { - sectors: HashMap, - capacity: u64, + sectors: RefCell>, + blocks: u64, + /// A read whose last byte is past this is refused. pub fail_reads_past: Option, - /// Whether [`BlockAccess::flush`] refuses, which is the one call + /// Whether [`BlockDev::flush`] refuses, which is the one call /// `Fat32::sync` makes into the device after the FSInfo write. pub flush_refuses: bool, /// The volume's own declared size, when the device is deliberately larger - /// than it — a partition with slack, or an adapter that reports the whole - /// device. Requests past it are still served, and counted. + /// than it — a partition with slack. Requests for blocks wholly past it + /// are still served, and counted. pub volume_bytes: Option, - /// Requests the crate made outside the volume. Must stay zero: the - /// `BlockAccess` contract says the crate never asks for bytes it has not - /// bounded, and an adapter is entitled to build on that. Counting rather - /// than refusing is the point — refusing would let a crate that asks look - /// identical to one that does not. - pub out_of_volume: u32, + /// Requests the crate made for blocks outside the volume. Must stay zero: + /// the crate never asks for bytes it has not bounded, and a device is + /// entitled to build on that. Counting rather than refusing is the point — + /// refusing would let a crate that asks look identical to one that does + /// not. + pub out_of_volume: Cell, } impl SparseDevice { - pub fn from_prefix(prefix: &[u8], capacity: u64) -> SparseDevice { + /// `prefix` at the start of a device of `bytes`, whole blocks. + pub fn from_prefix(prefix: &[u8], bytes: u64) -> SparseDevice { + assert!(bytes.is_multiple_of(BLOCK as u64), "a device of {bytes} bytes, no whole blocks"); let mut dev = SparseDevice { - sectors: HashMap::new(), - capacity, + sectors: RefCell::new(HashMap::new()), + blocks: bytes / BLOCK as u64, fail_reads_past: None, flush_refuses: false, volume_bytes: None, - out_of_volume: 0, + out_of_volume: Cell::new(0), }; - for (i, chunk) in prefix.chunks(512).enumerate() { - let mut s = [0u8; 512]; - s[..chunk.len()].copy_from_slice(chunk); - dev.sectors.insert(i as u64, s); - } + dev.poke(0, prefix); dev } @@ -154,32 +129,39 @@ impl SparseDevice { } pub fn poke(&mut self, offset: u64, bytes: &[u8]) { + Self::put(self.sectors.get_mut(), offset, bytes); + } + + fn put(sectors: &mut HashMap, offset: u64, bytes: &[u8]) { let mut done = 0usize; while done < bytes.len() { let pos = offset + done as u64; let sector = pos / 512; let within = (pos % 512) as usize; let n = (512 - within).min(bytes.len() - done); - let slot = self.sectors.entry(sector).or_insert([0u8; 512]); + let slot = sectors.entry(sector).or_insert([0u8; 512]); slot[within..within + n].copy_from_slice(&bytes[done..done + n]); done += n; } } - fn note_range(&mut self, end: u64) { - if self.volume_bytes.is_some_and(|v| end > v) { - self.out_of_volume += 1; + /// Count a request for blocks `lba..lba + count` that lies wholly past + /// the volume. + fn note_range(&self, lba: u64, count: usize) { + if count > 0 && self.volume_bytes.is_some_and(|v| lba * BLOCK as u64 >= v.next_multiple_of(BLOCK as u64)) { + self.out_of_volume.set(self.out_of_volume.get() + 1); } } fn read_bytes(&self, offset: u64, buf: &mut [u8]) { + let sectors = self.sectors.borrow(); let mut done = 0usize; while done < buf.len() { let pos = offset + done as u64; let sector = pos / 512; let within = (pos % 512) as usize; let n = (512 - within).min(buf.len() - done); - match self.sectors.get(§or) { + match sectors.get(§or) { Some(s) => buf[done..done + n].copy_from_slice(&s[within..within + n]), None => buf[done..done + n].fill(0), } @@ -188,215 +170,37 @@ impl SparseDevice { } } -impl BlockAccess for SparseDevice { - fn capacity(&self) -> u64 { - self.capacity +impl BlockDev for SparseDevice { + fn blocks(&self) -> u64 { + self.blocks } - fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<(), IoError> { - let end = offset.checked_add(buf.len() as u64).ok_or(IoError::Device)?; - if end > self.capacity { - return Err(IoError::Device); - } - if let Some(limit) = self.fail_reads_past { - if end > limit { - return Err(IoError::Device); - } + async fn read(&self, lba: u64, out: &mut [Block]) -> Result<(), DevError> { + within(lba, out.len(), self.blocks)?; + let end = (lba + out.len() as u64) * BLOCK as u64; + if self.fail_reads_past.is_some_and(|limit| end > limit) { + return Err(DevError::Device); } - self.note_range(end); - self.read_bytes(offset, buf); + self.note_range(lba, out.len()); + self.read_bytes(lba * BLOCK as u64, out.as_flattened_mut()); Ok(()) } - fn write_at(&mut self, offset: u64, buf: &[u8]) -> Result<(), IoError> { - let end = offset.checked_add(buf.len() as u64).ok_or(IoError::Device)?; - if end > self.capacity { - return Err(IoError::Device); - } - self.note_range(end); - self.poke(offset, buf); + async fn write(&self, lba: u64, data: &[Block]) -> Result<(), DevError> { + within(lba, data.len(), self.blocks)?; + self.note_range(lba, data.len()); + Self::put(&mut self.sectors.borrow_mut(), lba * BLOCK as u64, data.as_flattened()); Ok(()) } - fn flush(&mut self) -> Result<(), IoError> { + async fn flush(&self) -> Result<(), DevError> { if self.flush_refuses { - return Err(IoError::Device); + return Err(DevError::Device); } Ok(()) } } -pub const ADAPTER_BLOCK: usize = 4096; - -pub const ADAPTER_RESIDENT_BLOCKS: usize = 8; - -/// Where a refused block write got to. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Landing { - /// The write never reached the medium. - NotReached, - /// The write reached the medium and was answered as refused anyway. - Reached, -} - -/// A medium in memory behind `FatDevice`'s resident blocks, served and written -/// the way `FatDevice::read_at` and `FatDevice::write_at` serve and write it. -/// -/// A request moves in 4 KiB blocks. A whole block bypasses the resident copy, -/// and a whole-block write forgets it. A partial block is read through `load`, -/// which serves the resident copy and fills it from the medium on a miss, and -/// is written read-modify-write over that copy: the whole block reaches the -/// medium, carrying the resident neighbours with it, and is retained only once -/// the write answers. Eight blocks are resident, evicted round-robin. -/// -/// A refusal falls on a request's first block write, and the request stops -/// there as the adapter's `?` stops it. A request refused at a later block, the -/// earlier ones written and retained, is not generated, which is sound only -/// while no metadata write crosses a block. -pub struct AdapterCache { - /// Padded to whole blocks, as the partition under a volume is. - medium: Vec, - /// The volume's own length. - len: usize, - resident: Vec, - tags: [Option; ADAPTER_RESIDENT_BLOCKS], - next_victim: usize, - scratch: Vec, -} - -impl AdapterCache { - pub fn new(mut medium: Vec) -> AdapterCache { - let len = medium.len(); - medium.resize(len.next_multiple_of(ADAPTER_BLOCK), 0); - AdapterCache { - medium, - len, - resident: vec![0u8; ADAPTER_RESIDENT_BLOCKS * ADAPTER_BLOCK], - tags: [None; ADAPTER_RESIDENT_BLOCKS], - next_victim: 0, - scratch: vec![0u8; ADAPTER_BLOCK], - } - } - - pub fn len(&self) -> u64 { - self.len as u64 - } - - /// The volume as the medium holds it, which is what a judge reads. - pub fn medium(&self) -> &[u8] { - &self.medium[..self.len] - } - - pub fn into_medium(mut self) -> Vec { - self.medium.truncate(self.len); - self.medium - } - - /// Change the medium behind the adapter's back, dropping the blocks it - /// touches from the resident copy. - pub fn poke(&mut self, offset: usize, bytes: &[u8]) { - self.medium[offset..offset + bytes.len()].copy_from_slice(bytes); - let first = offset / ADAPTER_BLOCK; - let last = (offset + bytes.len()).div_ceil(ADAPTER_BLOCK); - self.forget(first, last - first); - } - - fn slot_of(&self, block: usize) -> Option { - self.tags.iter().position(|&t| t == Some(block)) - } - - fn load(&mut self, block: usize) { - if let Some(slot) = self.slot_of(block) { - let at = slot * ADAPTER_BLOCK; - self.scratch.copy_from_slice(&self.resident[at..at + ADAPTER_BLOCK]); - return; - } - let at = block * ADAPTER_BLOCK; - self.scratch.copy_from_slice(&self.medium[at..at + ADAPTER_BLOCK]); - self.retain(block); - } - - fn retain(&mut self, block: usize) { - let slot = self.slot_of(block).unwrap_or_else(|| { - let s = self.next_victim; - self.next_victim = (s + 1) % ADAPTER_RESIDENT_BLOCKS; - s - }); - let at = slot * ADAPTER_BLOCK; - self.resident[at..at + ADAPTER_BLOCK].copy_from_slice(&self.scratch); - self.tags[slot] = Some(block); - } - - fn forget(&mut self, first: usize, count: usize) { - for tag in &mut self.tags { - if tag.is_some_and(|b| b >= first && b < first + count) { - *tag = None; - } - } - } - - /// `FatDevice::read_at`, over a range the caller has bounded. - pub fn read(&mut self, offset: u64, buf: &mut [u8]) { - let base = offset as usize; - let mut done = 0usize; - while done < buf.len() { - let at = base + done; - let block = at / ADAPTER_BLOCK; - let within = at % ADAPTER_BLOCK; - let left = buf.len() - done; - if within == 0 && left >= ADAPTER_BLOCK { - let end = done + left / ADAPTER_BLOCK * ADAPTER_BLOCK; - buf[done..end].copy_from_slice(&self.medium[at..at + (end - done)]); - done = end; - } else { - let n = (ADAPTER_BLOCK - within).min(left); - self.load(block); - buf[done..done + n].copy_from_slice(&self.scratch[within..within + n]); - done += n; - } - } - } - - /// `FatDevice::write_at`, over a range the caller has bounded: every block - /// written, or with `refusal` the first block write refused where it got - /// to and nothing after it issued. - pub fn write(&mut self, offset: u64, buf: &[u8], refusal: Option) { - let base = offset as usize; - let reaches = refusal != Some(Landing::NotReached); - let mut done = 0usize; - while done < buf.len() { - let at = base + done; - let block = at / ADAPTER_BLOCK; - let within = at % ADAPTER_BLOCK; - let left = buf.len() - done; - if within == 0 && left >= ADAPTER_BLOCK { - let count = left / ADAPTER_BLOCK; - self.forget(block, count); - let issued = if refusal.is_some() { ADAPTER_BLOCK } else { count * ADAPTER_BLOCK }; - if reaches { - self.medium[at..at + issued].copy_from_slice(&buf[done..done + issued]); - } - done += count * ADAPTER_BLOCK; - } else { - let n = (ADAPTER_BLOCK - within).min(left); - self.load(block); - self.scratch[within..within + n].copy_from_slice(&buf[done..done + n]); - if reaches { - let start = block * ADAPTER_BLOCK; - self.medium[start..start + ADAPTER_BLOCK].copy_from_slice(&self.scratch); - } - if refusal.is_none() { - self.retain(block); - } - done += n; - } - if refusal.is_some() { - return; - } - } - } -} - // ----------------------------------------------------------------- images pub struct Image { @@ -437,7 +241,7 @@ impl Image { } pub fn device(&self) -> BlockyFile { - BlockyFile::open(&self.path, 4096) + BlockyFile::open(&self.path) } pub fn bytes(&self, len: usize) -> Vec { @@ -477,15 +281,15 @@ impl Image { /// A free function rather than `unwrap_err`, because that would need `Debug` /// on the whole filesystem — and a `Debug` on `Fat32` exists only to serve /// this line, which is not a reason for a public trait impl. -pub fn mount_err(dev: D) -> toyos_fat32::Error { - match toyos_fat32::Fat32::mount(dev) { +pub fn mount_err(dev: D) -> toyos_fat32::Error { + match now(toyos_fat32::Fat32::mount(dev)) { Ok(_) => panic!("mount succeeded on a volume that should have been refused"), Err(e) => e, } } -pub fn sorted_walk(fs: &mut toyos_fat32::Fat32) -> Vec<(String, u64)> { - let mut v = fs.walk("", 4096).expect("walk"); +pub fn sorted_walk(fs: &mut toyos_fat32::Fat32) -> Vec<(String, u64)> { + let mut v = now(fs.walk("", 4096)).expect("walk"); v.sort(); v } @@ -509,24 +313,24 @@ pub fn walk_expectation(files: &[(String, Vec)]) -> Vec<(String, u64)> { } /// Read a whole file through the crate. -pub fn read_all(fs: &mut toyos_fat32::Fat32, path: &str) -> Vec { - let mut f = fs.open(path).unwrap_or_else(|e| panic!("open {path}: {e}")); +pub fn read_all(fs: &mut toyos_fat32::Fat32, path: &str) -> Vec { + let mut f = now(fs.open(path)).unwrap_or_else(|e| panic!("open {path}: {e}")); let mut out = vec![0u8; f.len() as usize]; - let n = fs.read(&mut f, 0, &mut out).unwrap_or_else(|e| panic!("read {path}: {e}")); + let n = now(fs.read(&mut f, 0, &mut out)).unwrap_or_else(|e| panic!("read {path}: {e}")); assert_eq!(n, out.len(), "short read of {path}"); out } /// Write a whole file through the crate, creating it. -pub fn write_new( +pub fn write_new( fs: &mut toyos_fat32::Fat32, path: &str, data: &[u8], time: toyos_fat32::FatTime, ) { - let mut f = fs.create(path, time).unwrap_or_else(|e| panic!("create {path}: {e}")); - fs.write(&mut f, 0, data).unwrap_or_else(|e| panic!("write {path}: {e}")); - fs.flush_meta(&mut f, time).unwrap_or_else(|e| panic!("flush {path}: {e}")); + let mut f = now(fs.create(path, time)).unwrap_or_else(|e| panic!("create {path}: {e}")); + now(fs.write(&mut f, 0, data)).unwrap_or_else(|e| panic!("write {path}: {e}")); + now(fs.flush_meta(&mut f, time)).unwrap_or_else(|e| panic!("flush {path}: {e}")); } /// Assert every FAT holds the same bytes on `medium`, the volume from its @@ -556,7 +360,7 @@ pub fn assert_fats_agree(g: &toyos_fat32::Geometry, medium: &[u8]) { /// reports the *long* name, and duplicate short names are invisible from /// there — which is why a build that stopped uniquifying them once passed /// everything, `fsck_msdos` and a real mount included. -pub fn short_names_in( +pub fn short_names_in( fs: &mut toyos_fat32::Fat32, first_cluster: u32, ) -> Vec<[u8; 11]> { @@ -565,7 +369,7 @@ pub fn short_names_in( let mut cluster = g.cluster(first_cluster).expect("directory cluster is in the volume"); let mut buf = vec![0u8; g.bytes_per_cluster() as usize]; for _ in 0..4096 { - fs.device().read_at(g.cluster_offset(cluster), &mut buf).expect("read directory cluster"); + read_at(fs.device(), g.cluster_offset(cluster), &mut buf); for entry in buf.as_chunks::<32>().0 { if entry[0] == 0x00 { return out; @@ -580,9 +384,7 @@ pub fn short_names_in( out.push(short); } let mut link = [0u8; 4]; - fs.device() - .read_at(g.fat_entry_offset(0, cluster), &mut link) - .expect("read chain link"); + read_at(fs.device(), g.fat_entry_offset(0, cluster), &mut link); match g.cluster(u32::from_le_bytes(link) & 0x0FFF_FFFF) { Some(next) => cluster = next, None => return out, diff --git a/lib/fat32/tests/host_read.rs b/lib/fat32/tests/host_read.rs index 84723c8e1ef..36392ca547d 100644 --- a/lib/fat32/tests/host_read.rs +++ b/lib/fat32/tests/host_read.rs @@ -5,7 +5,7 @@ mod common; -use common::{pattern, read_all, sorted_walk, walk_expectation, Image}; +use common::{now, pattern, read_all, sorted_walk, walk_expectation, Image}; use toyos_fat32::{Error, Fat32}; /// One cluster on the 512-byte-cluster image, so the sizes below are stated in @@ -38,7 +38,7 @@ fn populated() -> Fixture { #[test] fn walk_sees_exactly_what_the_host_wrote() { let fx = populated(); - let mut fs = Fat32::mount(fx.image.device()).expect("mount"); + let mut fs = now(Fat32::mount(fx.image.device())).expect("mount"); assert_eq!(sorted_walk(&mut fs), walk_expectation(&fx.files)); } @@ -48,8 +48,8 @@ fn walk_sees_exactly_what_the_host_wrote() { #[test] fn lowercase_short_names_survive() { let fx = populated(); - let mut fs = Fat32::mount(fx.image.device()).expect("mount"); - let names: Vec = fs.read_dir("", 64).expect("read_dir").into_iter().map(|e| e.name).collect(); + let mut fs = now(Fat32::mount(fx.image.device())).expect("mount"); + let names: Vec = now(fs.read_dir("", 64)).expect("read_dir").into_iter().map(|e| e.name).collect(); assert!(names.contains(&String::from("hello.txt")), "{names:?}"); assert!(names.contains(&String::from("UPPER.TXT")), "{names:?}"); } @@ -57,20 +57,20 @@ fn lowercase_short_names_survive() { #[test] fn every_file_reads_back_byte_for_byte() { let fx = populated(); - let mut fs = Fat32::mount(fx.image.device()).expect("mount"); + let mut fs = now(Fat32::mount(fx.image.device())).expect("mount"); for (name, data) in &fx.files { assert_eq!(&read_all(&mut fs, name), data, "contents of {name}"); - assert_eq!(fs.metadata(name).expect("metadata").len, data.len() as u64, "size of {name}"); + assert_eq!(now(fs.metadata(name)).expect("metadata").len, data.len() as u64, "size of {name}"); } } #[test] fn reads_land_correctly_at_every_offset_and_length() { let fx = populated(); - let mut fs = Fat32::mount(fx.image.device()).expect("mount"); + let mut fs = now(Fat32::mount(fx.image.device())).expect("mount"); let name = "sub/nested/deep.bin"; let truth = &fx.files.iter().find(|(n, _)| n == name).expect("fixture").1; - let mut f = fs.open(name).expect("open"); + let mut f = now(fs.open(name)).expect("open"); // Offsets and lengths chosen around the cluster boundary, which is where // a read that computes its run wrong stops being wrong by a whole cluster @@ -78,7 +78,7 @@ fn reads_land_correctly_at_every_offset_and_length() { for &offset in &[0u64, 1, 511, 512, 513, 1023, 1024, 65_536, 300 * 1024 - 1] { for &len in &[1usize, 7, 511, 512, 513, 4096, 100_000] { let mut buf = vec![0u8; len]; - let n = fs.read(&mut f, offset, &mut buf).expect("read"); + let n = now(fs.read(&mut f, offset, &mut buf)).expect("read"); let want = &truth[(offset as usize).min(truth.len())..] [..n.min(truth.len() - (offset as usize).min(truth.len()))]; assert_eq!(&buf[..n], want, "offset {offset} len {len}"); @@ -91,18 +91,18 @@ fn reads_land_correctly_at_every_offset_and_length() { #[test] fn subdirectories_list_independently() { let fx = populated(); - let mut fs = Fat32::mount(fx.image.device()).expect("mount"); + let mut fs = now(Fat32::mount(fx.image.device())).expect("mount"); - let mut top: Vec = fs.read_dir("sub", 64).expect("read_dir").into_iter().map(|e| e.name).collect(); + let mut top: Vec = now(fs.read_dir("sub", 64)).expect("read_dir").into_iter().map(|e| e.name).collect(); top.sort(); assert_eq!(top, vec![String::from("nested"), String::from("one.txt")]); - let nested = fs.read_dir("sub/nested", 64).expect("read_dir"); + let nested = now(fs.read_dir("sub/nested", 64)).expect("read_dir"); assert_eq!(nested.len(), 2); assert!(nested.iter().any(|e| e.name == "deep.bin" && e.len == 300 * 1024)); - assert!(fs.metadata("sub").expect("metadata").is_dir); - assert!(!fs.metadata("sub/one.txt").expect("metadata").is_dir); + assert!(now(fs.metadata("sub")).expect("metadata").is_dir); + assert!(!now(fs.metadata("sub/one.txt")).expect("metadata").is_dir); } /// The volume label lives in the root directory as an entry with a name field @@ -110,21 +110,21 @@ fn subdirectories_list_independently() { #[test] fn the_volume_label_is_not_a_file() { let fx = populated(); - let mut fs = Fat32::mount(fx.image.device()).expect("mount"); - let names: Vec = fs.read_dir("", 64).expect("read_dir").into_iter().map(|e| e.name).collect(); + let mut fs = now(Fat32::mount(fx.image.device())).expect("mount"); + let names: Vec = now(fs.read_dir("", 64)).expect("read_dir").into_iter().map(|e| e.name).collect(); assert!(!names.iter().any(|n| n.starts_with("TF")), "label leaked into the listing: {names:?}"); } #[test] fn missing_paths_are_not_found_rather_than_anything_else() { let fx = populated(); - let mut fs = Fat32::mount(fx.image.device()).expect("mount"); - assert_eq!(fs.open("nope.txt").unwrap_err(), Error::NotFound); - assert_eq!(fs.open("sub/nope.txt").unwrap_err(), Error::NotFound); - assert_eq!(fs.metadata("sub/one.txt/deeper").unwrap_err(), Error::NotADirectory); - assert_eq!(fs.open("sub").unwrap_err(), Error::IsADirectory); - assert!(!fs.exists("nope.txt").expect("exists")); - assert!(fs.exists("sub/one.txt").expect("exists")); + let mut fs = now(Fat32::mount(fx.image.device())).expect("mount"); + assert_eq!(now(fs.open("nope.txt")).unwrap_err(), Error::NotFound); + assert_eq!(now(fs.open("sub/nope.txt")).unwrap_err(), Error::NotFound); + assert_eq!(now(fs.metadata("sub/one.txt/deeper")).unwrap_err(), Error::NotADirectory); + assert_eq!(now(fs.open("sub")).unwrap_err(), Error::IsADirectory); + assert!(!now(fs.exists("nope.txt")).expect("exists")); + assert!(now(fs.exists("sub/one.txt")).expect("exists")); } /// Names are matched case-insensitively, which is what firmware does when it @@ -132,21 +132,21 @@ fn missing_paths_are_not_found_rather_than_anything_else() { #[test] fn lookup_ignores_case() { let fx = populated(); - let mut fs = Fat32::mount(fx.image.device()).expect("mount"); - assert!(fs.exists("HELLO.TXT").expect("exists")); - assert!(fs.exists("upper.txt").expect("exists")); - assert!(fs.exists("SUB/NESTED/DEEP.BIN").expect("exists")); - assert!(fs.exists("a VERY long file name that needs several LFN entries.TXT").expect("exists")); + let mut fs = now(Fat32::mount(fx.image.device())).expect("mount"); + assert!(now(fs.exists("HELLO.TXT")).expect("exists")); + assert!(now(fs.exists("upper.txt")).expect("exists")); + assert!(now(fs.exists("SUB/NESTED/DEEP.BIN")).expect("exists")); + assert!(now(fs.exists("a VERY long file name that needs several LFN entries.TXT")).expect("exists")); } #[test] fn extents_cover_the_file_and_point_at_its_bytes() { let fx = populated(); - let mut fs = Fat32::mount(fx.image.device()).expect("mount"); + let mut fs = now(Fat32::mount(fx.image.device())).expect("mount"); let name = "sub/nested/deep.bin"; let truth = &fx.files.iter().find(|(n, _)| n == name).expect("fixture").1; - let extents = fs.extents(name, 4096).expect("extents"); + let extents = now(fs.extents(name, 4096)).expect("extents"); assert_eq!(extents.iter().map(|e| e.len).sum::(), truth.len() as u64); // macOS lays a fresh file down contiguously, so this is one extent. That // is worth recording: the coalescing is doing something, not returning a @@ -158,14 +158,13 @@ fn extents_cover_the_file_and_point_at_its_bytes() { let mut assembled = Vec::new(); for e in &extents { let mut buf = vec![0u8; e.len as usize]; - use toyos_fat32::BlockAccess; - fs.device().read_at(e.offset, &mut buf).expect("device read"); + common::read_at(fs.device(), e.offset, &mut buf); assembled.extend_from_slice(&buf); } assert_eq!(&assembled, truth); - assert_eq!(fs.extents(name, 0).unwrap_err(), Error::LimitExceeded); - assert!(fs.extents("empty.bin", 4096).expect("extents").is_empty()); + assert_eq!(now(fs.extents(name, 0)).unwrap_err(), Error::LimitExceeded); + assert!(now(fs.extents("empty.bin", 4096)).expect("extents").is_empty()); } /// A 4 KiB cluster volume is a different code path only in arithmetic, which @@ -180,7 +179,7 @@ fn a_four_kib_cluster_volume_reads_the_same() { // 65_525 clusters of 4 KiB is the floor for FAT32, so this volume cannot // be smaller. It is sparse, so it costs what is written to it. let image = Image::fixture("read-4k"); - let mut fs = Fat32::mount(image.device()).expect("mount"); + let mut fs = now(Fat32::mount(image.device())).expect("mount"); assert_eq!(fs.geometry().bytes_per_cluster(), 4096); for (name, data) in &files { assert_eq!(&read_all(&mut fs, name), data, "{name}"); diff --git a/lib/fat32/tests/host_write.rs b/lib/fat32/tests/host_write.rs index 4d4bb214031..5639e1c63c3 100644 --- a/lib/fat32/tests/host_write.rs +++ b/lib/fat32/tests/host_write.rs @@ -13,7 +13,7 @@ use std::collections::BTreeMap; use std::fs; use std::io::{Cursor, Read}; -use common::{pattern, read_all, sorted_walk, walk_expectation, write_new, Image}; +use common::{now, pattern, read_all, sorted_walk, walk_expectation, write_new, Image}; use toyos_fat32::{Error, Fat32, FatTime}; /// 2024-06-01 12:34:56, so every entry this crate stamps is checkable rather @@ -82,13 +82,13 @@ fn the_host_accepts_a_volume_we_populated() { let image = image(); { - let mut fs = Fat32::mount(image.device()).expect("mount"); - fs.create_dir("dir", stamp()).expect("mkdir dir"); - fs.create_dir("dir/deeper", stamp()).expect("mkdir deeper"); + let mut fs = now(Fat32::mount(image.device())).expect("mount"); + now(fs.create_dir("dir", stamp())).expect("mkdir dir"); + now(fs.create_dir("dir/deeper", stamp())).expect("mkdir deeper"); for (name, data) in &files { write_new(&mut fs, name, data, stamp()); } - fs.sync().expect("sync"); + now(fs.sync()).expect("sync"); } image.fsck(); @@ -109,9 +109,9 @@ fn the_host_accepts_a_volume_we_populated() { fn the_host_sees_the_timestamp_we_stamped() { let image = image(); { - let mut fs = Fat32::mount(image.device()).expect("mount"); + let mut fs = now(Fat32::mount(image.device())).expect("mount"); write_new(&mut fs, "dated.txt", b"x", stamp()); - fs.sync().expect("sync"); + now(fs.sync()).expect("sync"); } image.fsck(); let host = host(&image); @@ -135,11 +135,11 @@ fn files_crossing_cluster_boundaries_are_intact() { let image = image(); let lengths = [1usize, 511, 512, 513, 1023, 1024, 1025, 4096, 8191]; { - let mut fs = Fat32::mount(image.device()).expect("mount"); + let mut fs = now(Fat32::mount(image.device())).expect("mount"); for len in lengths { write_new(&mut fs, &format!("len{len}.bin"), &pattern(len, len as u64), stamp()); } - fs.sync().expect("sync"); + now(fs.sync()).expect("sync"); } image.fsck(); let tree = host_tree(&image); @@ -156,12 +156,12 @@ fn a_directory_grows_into_further_clusters() { let image = image(); let names: Vec = (0..60).map(|i| format!("A Long Enough Name To Need Entries {i:03}.log")).collect(); { - let mut fs = Fat32::mount(image.device()).expect("mount"); - fs.create_dir("logs", stamp()).expect("mkdir"); + let mut fs = now(Fat32::mount(image.device())).expect("mount"); + now(fs.create_dir("logs", stamp())).expect("mkdir"); for (i, name) in names.iter().enumerate() { write_new(&mut fs, &format!("logs/{name}"), &pattern(100, i as u64 + 1), stamp()); } - fs.sync().expect("sync"); + now(fs.sync()).expect("sync"); } image.fsck(); let tree = host_tree(&image); @@ -182,11 +182,11 @@ fn colliding_short_names_stay_unique() { let image = image(); let names: Vec = (0..40).map(|i| format!("boot-{i:04}.log")).collect(); { - let mut fs = Fat32::mount(image.device()).expect("mount"); + let mut fs = now(Fat32::mount(image.device())).expect("mount"); for (i, name) in names.iter().enumerate() { write_new(&mut fs, name, format!("entry {i}").as_bytes(), stamp()); } - fs.sync().expect("sync"); + now(fs.sync()).expect("sync"); } image.fsck(); let tree = host_tree(&image); @@ -198,7 +198,7 @@ fn colliding_short_names_stay_unique() { // `fsck_msdos` did before the checker replaced it. Asked here through the // crate's own device as well, because a name this crate generated is what // is under test and the failure names the eleven bytes. - let mut fs = Fat32::mount(image.device()).expect("remount"); + let mut fs = now(Fat32::mount(image.device())).expect("remount"); let root = fs.geometry().root_cluster; let mut shorts = common::short_names_in(&mut fs, root); assert_eq!(shorts.len(), names.len()); @@ -214,16 +214,16 @@ fn colliding_short_names_stay_unique() { fn every_fat_copy_stays_in_step() { let image = image(); { - let mut fs = Fat32::mount(image.device()).expect("mount"); + let mut fs = now(Fat32::mount(image.device())).expect("mount"); assert_eq!(fs.geometry().num_fats, 2, "the corpus has one FAT, so this proves nothing"); - fs.create_dir("d", stamp()).expect("mkdir"); + now(fs.create_dir("d", stamp())).expect("mkdir"); write_new(&mut fs, "d/allocating.bin", &pattern(200_000, 91), stamp()); write_new(&mut fs, "d/second.bin", &pattern(30_000, 92), stamp()); - fs.remove("d/allocating.bin").expect("remove"); - let mut f = fs.open("d/second.bin").expect("open"); - fs.set_len(&mut f, 100).expect("truncate"); - fs.flush_meta(&mut f, stamp()).expect("flush"); - fs.sync().expect("sync"); + now(fs.remove("d/allocating.bin")).expect("remove"); + let mut f = now(fs.open("d/second.bin")).expect("open"); + now(fs.set_len(&mut f, 100)).expect("truncate"); + now(fs.flush_meta(&mut f, stamp())).expect("flush"); + now(fs.sync()).expect("sync"); image.assert_fats_agree(fs.geometry()); } image.fsck(); @@ -234,13 +234,13 @@ fn appending_extends_the_chain() { let image = image(); let chunk = pattern(700, 21); { - let mut fs = Fat32::mount(image.device()).expect("mount"); - let mut f = fs.create("grown.bin", stamp()).expect("create"); + let mut fs = now(Fat32::mount(image.device())).expect("mount"); + let mut f = now(fs.create("grown.bin", stamp())).expect("create"); for i in 0..50 { - fs.write(&mut f, i * chunk.len() as u64, &chunk).expect("append"); + now(fs.write(&mut f, i * chunk.len() as u64, &chunk)).expect("append"); } - fs.flush_meta(&mut f, stamp()).expect("flush"); - fs.sync().expect("sync"); + now(fs.flush_meta(&mut f, stamp())).expect("flush"); + now(fs.sync()).expect("sync"); } image.fsck(); let want: Vec = chunk.iter().cycle().take(50 * chunk.len()).copied().collect(); @@ -254,19 +254,19 @@ fn appending_extends_the_chain() { fn deleted_clusters_come_back() { let image = image(); { - let mut fs = Fat32::mount(image.device()).expect("mount"); - let before = fs.free_bytes().expect("free"); + let mut fs = now(Fat32::mount(image.device())).expect("mount"); + let before = now(fs.free_bytes()).expect("free"); write_new(&mut fs, "big.bin", &pattern(2_000_000, 31), stamp()); - let during = fs.free_bytes().expect("free"); + let during = now(fs.free_bytes()).expect("free"); assert!(during < before, "{during} not less than {before}"); - fs.remove("big.bin").expect("remove"); - let after = fs.free_bytes().expect("free"); + now(fs.remove("big.bin")).expect("remove"); + let after = now(fs.free_bytes()).expect("free"); assert_eq!(after, before, "space not returned"); write_new(&mut fs, "second.bin", &pattern(2_000_000, 32), stamp()); - fs.sync().expect("sync"); + now(fs.sync()).expect("sync"); } image.fsck(); let tree = host_tree(&image); @@ -281,12 +281,12 @@ fn a_full_volume_refuses_cleanly() { let image = image(); let mut written = 0u64; { - let mut fs = Fat32::mount(image.device()).expect("mount"); + let mut fs = now(Fat32::mount(image.device())).expect("mount"); let chunk = pattern(256 * 1024, 41); let mut hit_the_wall = false; for i in 0..1000 { let name = format!("fill{i:04}.bin"); - let mut f = match fs.create(&name, stamp()) { + let mut f = match now(fs.create(&name, stamp())) { Ok(f) => f, Err(Error::NoSpace) => { hit_the_wall = true; @@ -294,15 +294,15 @@ fn a_full_volume_refuses_cleanly() { } Err(e) => panic!("create {name}: {e}"), }; - match fs.write(&mut f, 0, &chunk) { + match now(fs.write(&mut f, 0, &chunk)) { Ok(()) => { - fs.flush_meta(&mut f, stamp()).expect("flush"); + now(fs.flush_meta(&mut f, stamp())).expect("flush"); written += chunk.len() as u64; } Err(Error::NoSpace) => { // The partial chain is still the file's; recording the // size it actually reached is what keeps the volume sound. - fs.flush_meta(&mut f, stamp()).expect("flush"); + now(fs.flush_meta(&mut f, stamp())).expect("flush"); hit_the_wall = true; break; } @@ -311,7 +311,7 @@ fn a_full_volume_refuses_cleanly() { } assert!(hit_the_wall, "volume never filled after {written} bytes"); assert!(written > 32 * 1024 * 1024, "only {written} bytes fit in a 64 MiB volume"); - fs.sync().expect("sync"); + now(fs.sync()).expect("sync"); } image.fsck(); } @@ -320,12 +320,12 @@ fn a_full_volume_refuses_cleanly() { fn rename_moves_a_file_between_directories() { let image = image(); { - let mut fs = Fat32::mount(image.device()).expect("mount"); - fs.create_dir("from", stamp()).expect("mkdir"); - fs.create_dir("to", stamp()).expect("mkdir"); + let mut fs = now(Fat32::mount(image.device())).expect("mount"); + now(fs.create_dir("from", stamp())).expect("mkdir"); + now(fs.create_dir("to", stamp())).expect("mkdir"); write_new(&mut fs, "from/Original Long Name.txt", b"payload", stamp()); - fs.rename("from/Original Long Name.txt", "to/Renamed Long Name.txt").expect("rename"); - fs.sync().expect("sync"); + now(fs.rename("from/Original Long Name.txt", "to/Renamed Long Name.txt")).expect("rename"); + now(fs.sync()).expect("sync"); } image.fsck(); let tree = host_tree(&image); @@ -340,12 +340,12 @@ fn rename_moves_a_file_between_directories() { fn rename_repoints_a_moved_directorys_parent() { let image = image(); { - let mut fs = Fat32::mount(image.device()).expect("mount"); - fs.create_dir_all("a/movable", stamp()).expect("mkdir"); - fs.create_dir("b", stamp()).expect("mkdir"); + let mut fs = now(Fat32::mount(image.device())).expect("mount"); + now(fs.create_dir_all("a/movable", stamp())).expect("mkdir"); + now(fs.create_dir("b", stamp())).expect("mkdir"); write_new(&mut fs, "a/movable/file.txt", b"inside", stamp()); - fs.rename("a/movable", "b/moved").expect("rename"); - fs.sync().expect("sync"); + now(fs.rename("a/movable", "b/moved")).expect("rename"); + now(fs.sync()).expect("sync"); } image.fsck(); assert_eq!(host_tree(&image)["b/moved/file.txt"], b"inside"); @@ -355,21 +355,21 @@ fn rename_repoints_a_moved_directorys_parent() { fn truncation_releases_clusters_and_the_host_agrees() { let image = image(); { - let mut fs = Fat32::mount(image.device()).expect("mount"); + let mut fs = now(Fat32::mount(image.device())).expect("mount"); write_new(&mut fs, "shrink.bin", &pattern(100_000, 51), stamp()); - let full = fs.free_bytes().expect("free"); + let full = now(fs.free_bytes()).expect("free"); - let mut f = fs.open("shrink.bin").expect("open"); - fs.set_len(&mut f, 1000).expect("truncate"); - fs.flush_meta(&mut f, stamp()).expect("flush"); - assert!(fs.free_bytes().expect("free") > full, "truncation freed nothing"); + let mut f = now(fs.open("shrink.bin")).expect("open"); + now(fs.set_len(&mut f, 1000)).expect("truncate"); + now(fs.flush_meta(&mut f, stamp())).expect("flush"); + assert!(now(fs.free_bytes()).expect("free") > full, "truncation freed nothing"); // Growing again must zero-fill rather than expose whatever the freed // clusters still held. - let mut g = fs.open("shrink.bin").expect("open"); - fs.set_len(&mut g, 5000).expect("grow"); - fs.flush_meta(&mut g, stamp()).expect("flush"); - fs.sync().expect("sync"); + let mut g = now(fs.open("shrink.bin")).expect("open"); + now(fs.set_len(&mut g, 5000)).expect("grow"); + now(fs.flush_meta(&mut g, stamp())).expect("flush"); + now(fs.sync()).expect("sync"); } image.fsck(); let got = &host_tree(&image)["shrink.bin"]; @@ -384,17 +384,17 @@ fn truncation_releases_clusters_and_the_host_agrees() { fn sparse_writes_zero_the_gap() { let image = image(); { - let mut fs = Fat32::mount(image.device()).expect("mount"); + let mut fs = now(Fat32::mount(image.device())).expect("mount"); // Dirty a region first, then free it, so the clusters the sparse write // gets are not already zero. write_new(&mut fs, "scratch.bin", &pattern(100_000, 61), stamp()); - fs.remove("scratch.bin").expect("remove"); + now(fs.remove("scratch.bin")).expect("remove"); - let mut f = fs.create("holes.bin", stamp()).expect("create"); - fs.write(&mut f, 50_000, b"tail").expect("write far"); - fs.write(&mut f, 0, b"head").expect("write near"); - fs.flush_meta(&mut f, stamp()).expect("flush"); - fs.sync().expect("sync"); + let mut f = now(fs.create("holes.bin", stamp())).expect("create"); + now(fs.write(&mut f, 50_000, b"tail")).expect("write far"); + now(fs.write(&mut f, 0, b"head")).expect("write near"); + now(fs.flush_meta(&mut f, stamp())).expect("flush"); + now(fs.sync()).expect("sync"); } image.fsck(); let got = &host_tree(&image)["holes.bin"]; @@ -408,15 +408,15 @@ fn sparse_writes_zero_the_gap() { fn empty_directories_are_removable_and_full_ones_are_not() { let image = image(); { - let mut fs = Fat32::mount(image.device()).expect("mount"); - fs.create_dir("keep", stamp()).expect("mkdir"); - fs.create_dir("gone", stamp()).expect("mkdir"); + let mut fs = now(Fat32::mount(image.device())).expect("mount"); + now(fs.create_dir("keep", stamp())).expect("mkdir"); + now(fs.create_dir("gone", stamp())).expect("mkdir"); write_new(&mut fs, "keep/file.txt", b"x", stamp()); - assert_eq!(fs.remove_dir("keep").unwrap_err(), Error::DirectoryNotEmpty); - assert_eq!(fs.remove("keep").unwrap_err(), Error::IsADirectory); - assert_eq!(fs.remove_dir("keep/file.txt").unwrap_err(), Error::NotADirectory); - fs.remove_dir("gone").expect("rmdir"); - fs.sync().expect("sync"); + assert_eq!(now(fs.remove_dir("keep")).unwrap_err(), Error::DirectoryNotEmpty); + assert_eq!(now(fs.remove("keep")).unwrap_err(), Error::IsADirectory); + assert_eq!(now(fs.remove_dir("keep/file.txt")).unwrap_err(), Error::NotADirectory); + now(fs.remove_dir("gone")).expect("rmdir"); + now(fs.sync()).expect("sync"); } image.fsck(); let tree = host_tree(&image); @@ -435,16 +435,16 @@ fn our_own_reader_agrees_with_the_host_on_our_own_volume() { ("d/nested name.bin", pattern(1_500, 73)), ]; { - let mut fs = Fat32::mount(image.device()).expect("mount"); - fs.create_dir("d", stamp()).expect("mkdir"); + let mut fs = now(Fat32::mount(image.device())).expect("mount"); + now(fs.create_dir("d", stamp())).expect("mkdir"); for (n, d) in &files { write_new(&mut fs, n, d, stamp()); } - fs.sync().expect("sync"); + now(fs.sync()).expect("sync"); } image.fsck(); - let mut fs = Fat32::mount(image.device()).expect("remount"); + let mut fs = now(Fat32::mount(image.device())).expect("remount"); let owned: Vec<(String, Vec)> = files.iter().map(|(n, d)| ((*n).into(), d.clone())).collect(); assert_eq!(sorted_walk(&mut fs), walk_expectation(&owned)); @@ -464,11 +464,11 @@ fn our_own_reader_agrees_with_the_host_on_our_own_volume() { fn creating_an_existing_name_in_another_case_is_refused() { let image = image(); { - let mut fs = Fat32::mount(image.device()).expect("mount"); + let mut fs = now(Fat32::mount(image.device())).expect("mount"); write_new(&mut fs, "Report.TXT", b"first", stamp()); - assert_eq!(fs.create("report.txt", stamp()).unwrap_err(), Error::AlreadyExists); - assert_eq!(fs.create("REPORT.TXT", stamp()).unwrap_err(), Error::AlreadyExists); - fs.sync().expect("sync"); + assert_eq!(now(fs.create("report.txt", stamp())).unwrap_err(), Error::AlreadyExists); + assert_eq!(now(fs.create("REPORT.TXT", stamp())).unwrap_err(), Error::AlreadyExists); + now(fs.sync()).expect("sync"); } image.fsck(); assert_eq!(host_tree(&image).keys().collect::>(), ["Report.TXT"]); @@ -480,13 +480,13 @@ fn four_kib_clusters_write_correctly() { let image = Image::fixture("blank-4k"); let big = pattern(300_000, 81); { - let mut fs = Fat32::mount(image.device()).expect("mount"); + let mut fs = now(Fat32::mount(image.device())).expect("mount"); assert_eq!(fs.geometry().bytes_per_cluster(), 4096); - fs.create_dir_all("x/y/z", stamp()).expect("mkdir"); + now(fs.create_dir_all("x/y/z", stamp())).expect("mkdir"); write_new(&mut fs, "x/y/z/Big File.bin", &big, stamp()); write_new(&mut fs, "edge4095.bin", &pattern(4095, 82), stamp()); write_new(&mut fs, "edge4097.bin", &pattern(4097, 83), stamp()); - fs.sync().expect("sync"); + now(fs.sync()).expect("sync"); } image.fsck(); let tree = host_tree(&image); @@ -502,18 +502,18 @@ fn a_log_file_written_a_line_at_a_time() { let image = image(); let lines: Vec = (0..500).map(|i| format!("[{i:04}] a line of kernel log output\n")).collect(); { - let mut fs = Fat32::mount(image.device()).expect("mount"); - fs.create_dir("TOYOS", stamp()).expect("mkdir"); - let mut f = fs.create("TOYOS/boot.log", stamp()).expect("create"); + let mut fs = now(Fat32::mount(image.device())).expect("mount"); + now(fs.create_dir("TOYOS", stamp())).expect("mkdir"); + let mut f = now(fs.create("TOYOS/boot.log", stamp())).expect("create"); let mut at = 0u64; for line in &lines { - fs.write(&mut f, at, line.as_bytes()).expect("append line"); + now(fs.write(&mut f, at, line.as_bytes())).expect("append line"); at += line.len() as u64; // A crash-safe logger flushes metadata as it goes; this is also // what makes the entry's size track the data. - fs.flush_meta(&mut f, stamp()).expect("flush"); + now(fs.flush_meta(&mut f, stamp())).expect("flush"); } - fs.sync().expect("sync"); + now(fs.sync()).expect("sync"); } image.fsck(); assert_eq!(host_tree(&image)["TOYOS/boot.log"], lines.concat().into_bytes()); @@ -523,15 +523,15 @@ fn a_log_file_written_a_line_at_a_time() { #[test] fn same_entry_is_case_insensitive_identity() { let image = image(); - let mut fs = Fat32::mount(image.device()).expect("mount"); - fs.create("Report.TXT", stamp()).expect("create"); - fs.create("Other.bin", stamp()).expect("create"); - - assert!(fs.same_entry("Report.TXT", "Report.TXT").expect("exact")); - assert!(fs.same_entry("Report.TXT", "report.txt").expect("case only")); - assert!(fs.same_entry("REPORT.txt", "Report.TXT").expect("case only, other way")); - assert!(!fs.same_entry("Report.TXT", "Other.bin").expect("distinct entries")); - assert!(!fs.same_entry("Report.TXT", "absent.bin").expect("destination absent")); + let mut fs = now(Fat32::mount(image.device())).expect("mount"); + now(fs.create("Report.TXT", stamp())).expect("create"); + now(fs.create("Other.bin", stamp())).expect("create"); + + assert!(now(fs.same_entry("Report.TXT", "Report.TXT")).expect("exact")); + assert!(now(fs.same_entry("Report.TXT", "report.txt")).expect("case only")); + assert!(now(fs.same_entry("REPORT.txt", "Report.TXT")).expect("case only, other way")); + assert!(!now(fs.same_entry("Report.TXT", "Other.bin")).expect("distinct entries")); + assert!(!now(fs.same_entry("Report.TXT", "absent.bin")).expect("destination absent")); } /// What the checker says about the volume as it stands on disk right now. @@ -556,21 +556,21 @@ fn complaints(image: &Image) -> Vec { #[test] fn a_chain_that_outran_its_entry_is_reconciled() { let image = image(); - let mut fs = Fat32::mount(image.device()).expect("mount"); - let mut f = fs.create("BOOT.LOG", stamp()).expect("create"); + let mut fs = now(Fat32::mount(image.device())).expect("mount"); + let mut f = now(fs.create("BOOT.LOG", stamp())).expect("create"); let recorded = pattern(345 * 512 - 100, 3); - fs.write(&mut f, 0, &recorded).expect("write"); - fs.flush_meta(&mut f, stamp()).expect("flush_meta"); - fs.sync().expect("sync"); + now(fs.write(&mut f, 0, &recorded)).expect("write"); + now(fs.flush_meta(&mut f, stamp())).expect("flush_meta"); + now(fs.sync()).expect("sync"); assert!(!f.needs_reconcile(), "a flushed handle has nothing to reconcile"); assert!(complaints(&image).is_empty(), "the flushed volume is clean"); // Three clusters more, and no flush behind them: the window a machine that // stops here leaves open. let more = pattern(3 * 512, 5); - fs.write(&mut f, recorded.len() as u64, &more).expect("append"); - fs.sync().expect("sync"); + now(fs.write(&mut f, recorded.len() as u64, &more)).expect("append"); + now(fs.sync()).expect("sync"); assert!(f.needs_reconcile(), "the chain now covers bytes the entry does not"); let said = complaints(&image); assert!( @@ -578,8 +578,8 @@ fn a_chain_that_outran_its_entry_is_reconciled() { "the checker did not name the size-against-chain shape: {said:?}" ); - fs.reconcile(&mut f, stamp()).expect("reconcile"); - fs.sync().expect("sync"); + now(fs.reconcile(&mut f, stamp())).expect("reconcile"); + now(fs.sync()).expect("sync"); assert!(!f.needs_reconcile()); drop(fs); image.fsck(); @@ -595,20 +595,20 @@ fn a_chain_that_outran_its_entry_is_reconciled() { #[test] fn a_chain_no_entry_reaches_is_reconciled() { let image = image(); - let mut fs = Fat32::mount(image.device()).expect("mount"); - let mut f = fs.create("STICK.BIN", stamp()).expect("create"); + let mut fs = now(Fat32::mount(image.device())).expect("mount"); + let mut f = now(fs.create("STICK.BIN", stamp())).expect("create"); let data = pattern(4088 * 512, 11); - fs.write(&mut f, 0, &data).expect("write"); - fs.sync().expect("sync"); + now(fs.write(&mut f, 0, &data)).expect("write"); + now(fs.sync()).expect("sync"); let said = complaints(&image); assert!( said.iter().any(|c| c.contains("no directory entry reaches them")), "the checker did not name the orphaned-chain shape: {said:?}" ); - fs.reconcile(&mut f, stamp()).expect("reconcile"); - fs.sync().expect("sync"); + now(fs.reconcile(&mut f, stamp())).expect("reconcile"); + now(fs.sync()).expect("sync"); drop(fs); image.fsck(); assert_eq!(host_tree(&image)["STICK.BIN"], data); diff --git a/lib/fat32/tests/hostile.rs b/lib/fat32/tests/hostile.rs index a815c43e8dc..19394dfe70e 100644 --- a/lib/fat32/tests/hostile.rs +++ b/lib/fat32/tests/hostile.rs @@ -28,8 +28,8 @@ mod common; use std::sync::OnceLock; -use common::{pattern, Image, SparseDevice}; -use toyos_fat32::{BlockAccess, Error, Fat32, FatTime, MAX_DIR_ENTRIES}; +use common::{now, pattern, Image, SparseDevice}; +use toyos_fat32::{Error, Fat32, FatTime, MAX_DIR_ENTRIES}; /// Enough of the volume to hold the reserved sectors, both FATs, and the first /// thousand data clusters. Everything past it reads as zeroes, which is what @@ -105,37 +105,37 @@ impl Layout { /// The point is that none of them panics, hangs, or allocates by a number the /// volume supplied. Errors are expected and ignored; a panic fails the test. fn exercise(dev: SparseDevice) { - let Ok(mut fs) = Fat32::mount(dev) else { return }; + let Ok(mut fs) = now(Fat32::mount(dev)) else { return }; let t = FatTime::EPOCH; - let _ = fs.free_bytes(); - let _ = fs.walk("", 2048); + let _ = now(fs.free_bytes()); + let _ = now(fs.walk("", 2048)); - if let Ok(entries) = fs.read_dir("", 2048) { + if let Ok(entries) = now(fs.read_dir("", 2048)) { for e in entries.iter().take(32) { - let _ = fs.metadata(&e.name); - let _ = fs.read_dir(&e.name, 2048); - let _ = fs.extents(&e.name, 64); - if let Ok(mut f) = fs.open(&e.name) { + let _ = now(fs.metadata(&e.name)); + let _ = now(fs.read_dir(&e.name, 2048)); + let _ = now(fs.extents(&e.name, 64)); + if let Ok(mut f) = now(fs.open(&e.name)) { let mut buf = vec![0u8; 4096]; - let _ = fs.read(&mut f, 0, &mut buf); - let _ = fs.read(&mut f, 3000, &mut buf); - let _ = fs.read(&mut f, u32::MAX as u64 - 10, &mut buf); + let _ = now(fs.read(&mut f, 0, &mut buf)); + let _ = now(fs.read(&mut f, 3000, &mut buf)); + let _ = now(fs.read(&mut f, u32::MAX as u64 - 10, &mut buf)); } } } - if let Ok(mut f) = fs.create("probe.tmp", t) { - let _ = fs.write(&mut f, 0, &[1, 2, 3, 4]); - let _ = fs.write(&mut f, 100_000, &[5, 6]); - let _ = fs.set_len(&mut f, 10); - let _ = fs.flush_meta(&mut f, t); + if let Ok(mut f) = now(fs.create("probe.tmp", t)) { + let _ = now(fs.write(&mut f, 0, &[1, 2, 3, 4])); + let _ = now(fs.write(&mut f, 100_000, &[5, 6])); + let _ = now(fs.set_len(&mut f, 10)); + let _ = now(fs.flush_meta(&mut f, t)); } - let _ = fs.create_dir("probedir", t); - let _ = fs.create_dir_all("a/b/c", t); - let _ = fs.rename("probe.tmp", "probedir/moved.tmp"); - let _ = fs.remove("probedir/moved.tmp"); - let _ = fs.remove_dir("probedir"); - let _ = fs.sync(); + let _ = now(fs.create_dir("probedir", t)); + let _ = now(fs.create_dir_all("a/b/c", t)); + let _ = now(fs.rename("probe.tmp", "probedir/moved.tmp")); + let _ = now(fs.remove("probedir/moved.tmp")); + let _ = now(fs.remove_dir("probedir")); + let _ = now(fs.sync()); } // ------------------------------------------------------- the corpus itself @@ -143,8 +143,8 @@ fn exercise(dev: SparseDevice) { /// If this fails, every test below is testing against a fantasy. #[test] fn the_corpus_is_a_volume_we_can_read() { - let mut fs = Fat32::mount(pristine()).expect("mount"); - let mut names: Vec = fs.walk("", 1024).expect("walk").into_iter().map(|(n, _)| n).collect(); + let mut fs = now(Fat32::mount(pristine())).expect("mount"); + let mut names: Vec = now(fs.walk("", 1024)).expect("walk").into_iter().map(|(n, _)| n).collect(); names.sort(); assert_eq!( names, @@ -229,14 +229,17 @@ fn a_volume_larger_than_its_device_is_truncated() { let dev = SparseDevice::from_prefix(prefix, cap / 2); assert_eq!(common::mount_err(dev), Error::Truncated); - // And the boundary: one sector short of what the boot sector declares. + // And the boundary: the fewest whole blocks that hold what the boot + // sector declares mount, and one block fewer, the least a device can be + // short by, is refused. let dev = SparseDevice::from_prefix(prefix, *cap); let total = u32::from_le_bytes(dev.peek(32, 4).try_into().expect("4 bytes")) as u64; let l = layout(); - let dev = SparseDevice::from_prefix(prefix, total * l.bps - 1); + let holds = (total * l.bps).next_multiple_of(toyos_blockdev::BLOCK as u64); + let dev = SparseDevice::from_prefix(prefix, holds - toyos_blockdev::BLOCK as u64); assert_eq!(common::mount_err(dev), Error::Truncated); - let dev = SparseDevice::from_prefix(prefix, total * l.bps); - assert!(Fat32::mount(dev).is_ok(), "exactly the declared size must mount"); + let dev = SparseDevice::from_prefix(prefix, holds); + assert!(now(Fat32::mount(dev)).is_ok(), "the blocks that hold the declared size must mount"); } /// A device that stops answering part way through. Nothing may panic on the @@ -252,8 +255,8 @@ fn a_device_that_fails_mid_read_reports_it() { let mut dev = pristine(); // Enough for the boot sector and FSInfo, not enough for the root cluster. dev.fail_reads_past = Some(l.first_data * l.bps); - let mut fs = Fat32::mount(dev).expect("mount"); - assert_eq!(fs.walk("", 1024).unwrap_err(), Error::Io); + let mut fs = now(Fat32::mount(dev)).expect("mount"); + assert_eq!(now(fs.walk("", 1024)).unwrap_err(), Error::Io); } /// The flush is the call `/system/bin/logkeeper`'s durability claim rests on: a @@ -262,11 +265,11 @@ fn a_device_that_fails_mid_read_reports_it() { fn a_refused_flush_is_io() { let mut dev = pristine(); dev.flush_refuses = true; - let mut fs = Fat32::mount(dev).expect("mount"); - assert_eq!(fs.sync().unwrap_err(), Error::Io); + let mut fs = now(Fat32::mount(dev)).expect("mount"); + assert_eq!(now(fs.sync()).unwrap_err(), Error::Io); - let mut fs = Fat32::mount(pristine()).expect("mount"); - assert_eq!(fs.sync(), Ok(())); + let mut fs = now(Fat32::mount(pristine())).expect("mount"); + assert_eq!(now(fs.sync()), Ok(())); } // ----------------------------------------------------------- broken chains @@ -275,16 +278,16 @@ fn a_refused_flush_is_io() { fn a_cluster_that_links_to_itself_is_refused() { let l = layout(); let mut dev = pristine(); - let mut fs = Fat32::mount(pristine()).expect("mount"); - let first = fs.extents("A Long Name For Entries.bin", 64).expect("extents")[0].offset; + let mut fs = now(Fat32::mount(pristine())).expect("mount"); + let first = now(fs.extents("A Long Name For Entries.bin", 64)).expect("extents")[0].offset; let cluster = ((first / l.bps - l.first_data) / l.spc + 2) as u32; drop(fs); l.set_chain(&mut dev, cluster, cluster); - let mut fs = Fat32::mount(dev).expect("mount"); - let mut f = fs.open("A Long Name For Entries.bin").expect("open"); + let mut fs = now(Fat32::mount(dev)).expect("mount"); + let mut f = now(fs.open("A Long Name For Entries.bin")).expect("open"); let mut buf = vec![0u8; 20_000]; - assert_eq!(fs.read(&mut f, 0, &mut buf).unwrap_err(), Error::CorruptChain); + assert_eq!(now(fs.read(&mut f, 0, &mut buf)).unwrap_err(), Error::CorruptChain); } /// A longer cycle is bounded rather than detected — see `Fat32::advance`. What @@ -293,37 +296,37 @@ fn a_cluster_that_links_to_itself_is_refused() { #[test] fn a_longer_cycle_is_bounded_rather_than_endless() { let l = layout(); - let mut fs = Fat32::mount(pristine()).expect("mount"); - let ext = fs.extents("A Long Name For Entries.bin", 64).expect("extents"); + let mut fs = now(Fat32::mount(pristine())).expect("mount"); + let ext = now(fs.extents("A Long Name For Entries.bin", 64)).expect("extents"); let first = ((ext[0].offset / l.bps - l.first_data) / l.spc + 2) as u32; drop(fs); let mut dev = pristine(); // …→ first+3 → first, a four-cluster ring under a 20,000-byte file. l.set_chain(&mut dev, first + 3, first); - let mut fs = Fat32::mount(dev).expect("mount"); - let mut f = fs.open("A Long Name For Entries.bin").expect("open"); + let mut fs = now(Fat32::mount(dev)).expect("mount"); + let mut f = now(fs.open("A Long Name For Entries.bin")).expect("open"); let mut buf = vec![0u8; 20_000]; - let n = fs.read(&mut f, 0, &mut buf).expect("read"); + let n = now(fs.read(&mut f, 0, &mut buf)).expect("read"); assert_eq!(n, 20_000, "the size field still bounds the read"); } #[test] fn a_chain_link_outside_the_volume_is_refused() { let l = layout(); - let mut fs = Fat32::mount(pristine()).expect("mount"); - let ext = fs.extents("A Long Name For Entries.bin", 64).expect("extents"); + let mut fs = now(Fat32::mount(pristine())).expect("mount"); + let ext = now(fs.extents("A Long Name For Entries.bin", 64)).expect("extents"); let first = ((ext[0].offset / l.bps - l.first_data) / l.spc + 2) as u32; drop(fs); for bad in [0u32, 1, 0x0FFF_FFF7, 0x0F00_0000] { let mut dev = pristine(); l.set_chain(&mut dev, first, bad); - let mut fs = Fat32::mount(dev).expect("mount"); - let mut f = fs.open("A Long Name For Entries.bin").expect("open"); + let mut fs = now(Fat32::mount(dev)).expect("mount"); + let mut f = now(fs.open("A Long Name For Entries.bin")).expect("open"); let mut buf = vec![0u8; 20_000]; assert_eq!( - fs.read(&mut f, 0, &mut buf).unwrap_err(), + now(fs.read(&mut f, 0, &mut buf)).unwrap_err(), Error::CorruptChain, "link value {bad:#x} was followed" ); @@ -351,8 +354,8 @@ fn a_directory_entry_pointing_outside_the_volume_is_refused() { dev.poke(at + 26, &[0xFF, 0xFF]); dev.poke(at + 20, &[0xFF, 0x0F]); - let mut fs = Fat32::mount(dev).expect("mount"); - assert_eq!(fs.walk("", 1024).unwrap_err(), Error::CorruptDirectory); + let mut fs = now(Fat32::mount(dev)).expect("mount"); + assert_eq!(now(fs.walk("", 1024)).unwrap_err(), Error::CorruptDirectory); } /// One cluster of a directory, packed with entries and no end marker, so a @@ -388,8 +391,8 @@ fn a_cyclic_directory_chain_terminates() { l.set_chain(&mut dev, root, root + 1); l.set_chain(&mut dev, root + 1, root); - let mut fs = Fat32::mount(dev).expect("mount"); - let err = fs.walk("", 1024).unwrap_err(); + let mut fs = now(Fat32::mount(dev)).expect("mount"); + let err = now(fs.walk("", 1024)).unwrap_err(); assert!( matches!(err, Error::CorruptDirectory | Error::CorruptChain | Error::LimitExceeded), "unexpected {err:?}" @@ -397,7 +400,7 @@ fn a_cyclic_directory_chain_terminates() { // The other way in. Creating an entry both scans the directory for a // short-name collision and measures its chain, and either bound is enough // to refuse — the scan happens first, so that is the one seen here. - let err = fs.create("new.txt", FatTime::EPOCH).map(|_| ()).unwrap_err(); + let err = now(fs.create("new.txt", FatTime::EPOCH)).map(|_| ()).unwrap_err(); assert!(matches!(err, Error::CorruptDirectory | Error::CorruptChain), "unexpected {err:?}"); } @@ -417,10 +420,10 @@ fn a_directory_tree_cycle_terminates() { break; } } - let mut fs = Fat32::mount(dev).expect("mount"); + let mut fs = now(Fat32::mount(dev)).expect("mount"); // Either answer is sound: the visited set stops the descent, and the // budget stops a tree that is merely enormous. - match fs.walk("", 1024) { + match now(fs.walk("", 1024)) { Ok(files) => assert!(files.len() <= 1024), Err(e) => assert!(matches!(e, Error::LimitExceeded | Error::CorruptDirectory)), } @@ -451,46 +454,46 @@ fn a_directory_longer_than_the_entry_bound_is_refused() { dev.poke(l.cluster(base + needed), &filler); l.set_chain(&mut dev, base + needed, 0x0FFF_FFFF); - let mut fs = Fat32::mount(dev).expect("mount"); - let err = fs.read_dir("", 1_000_000).unwrap_err(); + let mut fs = now(Fat32::mount(dev)).expect("mount"); + let err = now(fs.read_dir("", 1_000_000)).unwrap_err(); assert_eq!(err, Error::CorruptDirectory); // The listing bound is the caller's; this one is the crate's, and it has // to hold even when the caller offers a bound larger than it. - assert_eq!(fs.walk("", 1_000_000).unwrap_err(), Error::CorruptDirectory); + assert_eq!(now(fs.walk("", 1_000_000)).unwrap_err(), Error::CorruptDirectory); } /// The caller's own bound has to hold too, and it has to refuse rather than /// hand back a listing that is short of the truth. #[test] fn the_caller_limit_refuses_rather_than_truncates() { - let mut fs = Fat32::mount(pristine()).expect("mount"); - assert_eq!(fs.walk("", 1).unwrap_err(), Error::LimitExceeded); - assert_eq!(fs.read_dir("", 1).unwrap_err(), Error::LimitExceeded); - assert_eq!(fs.read_dir("", 0).unwrap_err(), Error::LimitExceeded); - assert!(fs.walk("", 64).is_ok()); + let mut fs = now(Fat32::mount(pristine())).expect("mount"); + assert_eq!(now(fs.walk("", 1)).unwrap_err(), Error::LimitExceeded); + assert_eq!(now(fs.read_dir("", 1)).unwrap_err(), Error::LimitExceeded); + assert_eq!(now(fs.read_dir("", 0)).unwrap_err(), Error::LimitExceeded); + assert!(now(fs.walk("", 64)).is_ok()); } /// And it is spent on the named subtree, so a bound the whole volume exceeds /// still lists a directory inside it. #[test] fn the_caller_limit_is_the_named_directorys_and_not_the_volumes() { - let mut fs = Fat32::mount(pristine()).expect("mount"); - let whole = fs.walk("", 64).expect("walk"); + let mut fs = now(Fat32::mount(pristine())).expect("mount"); + let whole = now(fs.walk("", 64)).expect("walk"); assert_eq!(whole.len(), 6, "the corpus walked to {whole:?}"); - assert_eq!(fs.walk("", whole.len() - 1).unwrap_err(), Error::LimitExceeded); + assert_eq!(now(fs.walk("", whole.len() - 1)).unwrap_err(), Error::LimitExceeded); assert_eq!( - fs.walk("sub/deeper", whole.len() - 1).expect("walk one directory"), + now(fs.walk("sub/deeper", whole.len() - 1)).expect("walk one directory"), vec![(String::from("sub/deeper/"), 0), (String::from("sub/deeper/leaf.txt"), 4)], ); - assert_eq!(fs.walk("sub/deeper", 1).unwrap_err(), Error::LimitExceeded); + assert_eq!(now(fs.walk("sub/deeper", 1)).unwrap_err(), Error::LimitExceeded); assert_eq!( - fs.walk("plain.txt", 1).expect("walk a file"), + now(fs.walk("plain.txt", 1)).expect("walk a file"), vec![(String::from("plain.txt"), 12)], ); // A file's own entry is an entry, so the caller's bound governs it too. - assert_eq!(fs.walk("plain.txt", 0).unwrap_err(), Error::LimitExceeded); - assert_eq!(fs.walk("nope", 64).unwrap_err(), Error::NotFound); + assert_eq!(now(fs.walk("plain.txt", 0)).unwrap_err(), Error::LimitExceeded); + assert_eq!(now(fs.walk("nope", 64)).unwrap_err(), Error::NotFound); } /// A zero-length file has no first cluster, which is what no data looks like. @@ -502,20 +505,20 @@ fn the_caller_limit_is_the_named_directorys_and_not_the_volumes() { fn an_empty_file_is_not_a_corrupt_directory() { let image = Image::fixture("empty-file"); image.fsck(); - let mut fs = Fat32::mount(SparseDevice::from_prefix(&image.bytes(PREFIX_BYTES), image.size())) + let mut fs = now(Fat32::mount(SparseDevice::from_prefix(&image.bytes(PREFIX_BYTES), image.size()))) .expect("mount"); - let mut whole = fs.walk("", 64).expect("walk the volume"); + let mut whole = now(fs.walk("", 64)).expect("walk the volume"); whole.sort(); assert_eq!( whole, vec![(String::from("empty.txt"), 0), (String::from("full.txt"), 12)], ); assert_eq!( - fs.walk("empty.txt", 64).expect("walk the empty file"), + now(fs.walk("empty.txt", 64)).expect("walk the empty file"), vec![(String::from("empty.txt"), 0)], ); - assert_eq!(fs.metadata("empty.txt").expect("metadata").len, 0); + assert_eq!(now(fs.metadata("empty.txt")).expect("metadata").len, 0); } // -------------------------------------------------------------- long names @@ -538,8 +541,8 @@ fn a_long_name_with_a_wrong_checksum_falls_back_to_the_short_one() { } assert!(patched, "the corpus has no long-name entries to break"); - let mut fs = Fat32::mount(dev).expect("mount"); - let names: Vec = fs.read_dir("", 256).expect("read_dir").into_iter().map(|e| e.name).collect(); + let mut fs = now(Fat32::mount(dev)).expect("mount"); + let names: Vec = now(fs.read_dir("", 256)).expect("read_dir").into_iter().map(|e| e.name).collect(); assert!(names.iter().any(|n| n == "plain.txt"), "{names:?}"); assert!(!names.iter().any(|n| n.contains(' ')), "a broken run was trusted: {names:?}"); assert!(names.len() >= 3, "entries disappeared: {names:?}"); @@ -559,8 +562,8 @@ fn nonsense_long_name_ordinals_do_not_break_the_scan() { dev.poke(root + i * 32, &[ord]); } } - let mut fs = Fat32::mount(dev).expect("mount"); - let entries = fs.read_dir("", 256).expect("read_dir"); + let mut fs = now(Fat32::mount(dev)).expect("mount"); + let entries = now(fs.read_dir("", 256)).expect("read_dir"); assert!(entries.iter().any(|e| e.name == "plain.txt"), "ordinal {ord:#x}"); } } @@ -583,8 +586,8 @@ fn lone_surrogates_in_a_long_name_produce_a_name() { } } } - let mut fs = Fat32::mount(dev).expect("mount"); - let entries = fs.read_dir("", 256).expect("read_dir"); + let mut fs = now(Fat32::mount(dev)).expect("mount"); + let entries = now(fs.read_dir("", 256)).expect("read_dir"); assert!(!entries.is_empty()); } @@ -667,40 +670,40 @@ fn random_damage_to_directory_entries_never_panics() { /// sanitised, so nobody ends up with a file under a name they did not choose. #[test] fn impossible_names_are_refused_not_mangled() { - let mut fs = Fat32::mount(pristine()).expect("mount"); + let mut fs = now(Fat32::mount(pristine())).expect("mount"); let t = FatTime::EPOCH; let too_long: String = core::iter::repeat_n('x', 256).collect(); for bad in ["", ".", "..", "ab.txt", "a|b.txt", "a?b.txt", "trailing.", "trailing "] { - let e = fs.create(bad, t).map(|_| ()).unwrap_err(); + let e = now(fs.create(bad, t)).map(|_| ()).unwrap_err(); assert!(matches!(e, Error::InvalidName | Error::AlreadyExists), "{bad:?} gave {e:?}"); } - assert_eq!(fs.create(&too_long, t).map(|_| ()).unwrap_err(), Error::InvalidName); - assert_eq!(fs.create("", t).map(|_| ()).unwrap_err(), Error::InvalidName); + assert_eq!(now(fs.create(&too_long, t)).map(|_| ()).unwrap_err(), Error::InvalidName); + assert_eq!(now(fs.create("", t)).map(|_| ()).unwrap_err(), Error::InvalidName); } /// A handle that outlives what it named. Writing through it must not resize /// whatever took its directory slot. #[test] fn a_stale_handle_is_refused() { - let mut fs = Fat32::mount(pristine()).expect("mount"); + let mut fs = now(Fat32::mount(pristine())).expect("mount"); let t = FatTime::EPOCH; - let mut f = fs.open("plain.txt").expect("open"); - fs.remove("plain.txt").expect("remove"); - assert_eq!(fs.flush_meta(&mut f, t).unwrap_err(), Error::NotFound); + let mut f = now(fs.open("plain.txt")).expect("open"); + now(fs.remove("plain.txt")).expect("remove"); + assert_eq!(now(fs.flush_meta(&mut f, t)).unwrap_err(), Error::NotFound); } /// FAT32's size field is 32 bits. A write past it has nowhere to record its /// length, and must say so before it allocates anything. #[test] fn writes_past_the_format_limit_are_refused() { - let mut fs = Fat32::mount(pristine()).expect("mount"); + let mut fs = now(Fat32::mount(pristine())).expect("mount"); let t = FatTime::EPOCH; - let mut f = fs.create("huge.bin", t).expect("create"); - let before = fs.free_bytes().expect("free"); - assert_eq!(fs.write(&mut f, u32::MAX as u64, &[1, 2, 3]).unwrap_err(), Error::TooLarge); - assert_eq!(fs.write(&mut f, u64::MAX, &[1]).unwrap_err(), Error::TooLarge); - assert_eq!(fs.set_len(&mut f, 1 << 40).unwrap_err(), Error::TooLarge); - assert_eq!(fs.free_bytes().expect("free"), before, "a refused write still allocated"); + let mut f = now(fs.create("huge.bin", t)).expect("create"); + let before = now(fs.free_bytes()).expect("free"); + assert_eq!(now(fs.write(&mut f, u32::MAX as u64, &[1, 2, 3])).unwrap_err(), Error::TooLarge); + assert_eq!(now(fs.write(&mut f, u64::MAX, &[1])).unwrap_err(), Error::TooLarge); + assert_eq!(now(fs.set_len(&mut f, 1 << 40)).unwrap_err(), Error::TooLarge); + assert_eq!(now(fs.free_bytes()).expect("free"), before, "a refused write still allocated"); } // ------------------------------------- the write path, after the 2026-08-01 audit @@ -741,10 +744,8 @@ fn a_crafted_zero_size_entry_cannot_write_outside_the_volume() { let (prefix, _) = corpus(); // Twice the volume, so an out-of-volume offset is inside the device and // the device cannot mask the bug by refusing. - let volume = l.bps * u64::from(u32::from_le_bytes( - SparseDevice::from_prefix(prefix, 1).peek(32, 4).try_into().expect("4 bytes"), - )); - let mut dev = SparseDevice::from_prefix(prefix, volume * 2); + let volume = l.bps * u64::from(u32::from_le_bytes(prefix[32..36].try_into().expect("4 bytes"))); + let mut dev = SparseDevice::from_prefix(prefix, (volume * 2).next_multiple_of(toyos_blockdev::BLOCK as u64)); dev.volume_bytes = Some(volume); let mut entry = [0u8; 32]; @@ -754,16 +755,16 @@ fn a_crafted_zero_size_entry_cannot_write_outside_the_volume() { entry[20..22].copy_from_slice(&0x2000u16.to_le_bytes()); craft_root_entry(&mut dev, &l, &entry); - let mut fs = Fat32::mount(dev).expect("mount"); - assert_eq!(fs.open("CRAFTED.BIN").map(|_| ()).unwrap_err(), Error::CorruptDirectory); - assert_eq!(fs.metadata("CRAFTED.BIN").unwrap_err(), Error::CorruptDirectory); - assert_eq!(fs.extents("CRAFTED.BIN", 16).unwrap_err(), Error::CorruptDirectory); + let mut fs = now(Fat32::mount(dev)).expect("mount"); + assert_eq!(now(fs.open("CRAFTED.BIN")).map(|_| ()).unwrap_err(), Error::CorruptDirectory); + assert_eq!(now(fs.metadata("CRAFTED.BIN")).unwrap_err(), Error::CorruptDirectory); + assert_eq!(now(fs.extents("CRAFTED.BIN", 16)).unwrap_err(), Error::CorruptDirectory); // A listing validates only what it follows, so the crafted entry is // *reported*: it is on the disk, and saying so computes no offset from it. // One bad entry making a whole directory unlistable would be a denial of // service on a volume that is otherwise fine. - assert!(fs.walk("", 1024).expect("walk").iter().any(|(n, _)| n == "CRAFTED.BIN")); - assert_eq!(fs.device().out_of_volume, 0, "the crate asked for bytes outside the volume"); + assert!(now(fs.walk("", 1024)).expect("walk").iter().any(|(n, _)| n == "CRAFTED.BIN")); + assert_eq!(fs.device().out_of_volume.get(), 0, "the crate asked for bytes outside the volume"); } /// F6, the other half of the same entry: it must still be deletable. @@ -782,17 +783,17 @@ fn a_crafted_entry_can_still_be_deleted() { entry[20..22].copy_from_slice(&0x0FFFu16.to_le_bytes()); craft_root_entry(&mut dev, &l, &entry); - let mut fs = Fat32::mount(dev).expect("mount"); - assert!(fs.read_dir("", 256).expect("read_dir").iter().any(|e| e.name == "CRAFTED.BIN")); - fs.remove("CRAFTED.BIN").expect("a crafted entry must still be removable"); - assert!(!fs.read_dir("", 256).expect("read_dir").iter().any(|e| e.name == "CRAFTED.BIN")); - assert_eq!(fs.metadata("CRAFTED.BIN").unwrap_err(), Error::NotFound); + let mut fs = now(Fat32::mount(dev)).expect("mount"); + assert!(now(fs.read_dir("", 256)).expect("read_dir").iter().any(|e| e.name == "CRAFTED.BIN")); + now(fs.remove("CRAFTED.BIN")).expect("a crafted entry must still be removable"); + assert!(!now(fs.read_dir("", 256)).expect("read_dir").iter().any(|e| e.name == "CRAFTED.BIN")); + assert_eq!(now(fs.metadata("CRAFTED.BIN")).unwrap_err(), Error::NotFound); } /// The head cluster of the corpus file that spans several clusters. fn multi_cluster_head(l: &Layout) -> u32 { - let mut fs = Fat32::mount(pristine()).expect("mount"); - let ext = fs.extents("A Long Name For Entries.bin", 64).expect("extents"); + let mut fs = now(Fat32::mount(pristine())).expect("mount"); + let ext = now(fs.extents("A Long Name For Entries.bin", 64)).expect("extents"); ((ext[0].offset / l.bps - l.first_data) / l.spc + 2) as u32 } @@ -811,9 +812,9 @@ fn truncating_a_cyclic_chain_does_not_free_the_clusters_it_keeps() { // head → head+1 → head+2 → head, a rho closing above the truncation point. l.set_chain(&mut dev, head + 2, head); - let mut fs = Fat32::mount(dev).expect("mount"); - let mut f = fs.open("A Long Name For Entries.bin").expect("open"); - assert_eq!(fs.set_len(&mut f, 600).unwrap_err(), Error::CorruptChain); + let mut fs = now(Fat32::mount(dev)).expect("mount"); + let mut f = now(fs.open("A Long Name For Entries.bin")).expect("open"); + assert_eq!(now(fs.set_len(&mut f, 600)).unwrap_err(), Error::CorruptChain); // The kept cluster must still be allocated. Reading it back through the // FAT is the only way to see this: the directory entry looks fine either @@ -821,7 +822,7 @@ fn truncating_a_cyclic_chain_does_not_free_the_clusters_it_keeps() { let g = *fs.geometry(); let mut link = [0u8; 4]; let cluster = g.cluster(head).expect("head is in the volume"); - fs.device().read_at(g.fat_entry_offset(0, cluster), &mut link).expect("read link"); + common::read_at(fs.device(), g.fat_entry_offset(0, cluster), &mut link); assert_ne!( u32::from_le_bytes(link) & 0x0FFF_FFFF, 0, @@ -842,10 +843,10 @@ fn removing_a_cyclic_chain_never_leaves_an_entry_naming_free_clusters() { let mut dev = pristine(); l.set_chain(&mut dev, head + 2, head); - let mut fs = Fat32::mount(dev).expect("mount"); - let result = fs.remove("A Long Name For Entries.bin"); + let mut fs = now(Fat32::mount(dev)).expect("mount"); + let result = now(fs.remove("A Long Name For Entries.bin")); assert_eq!( - fs.metadata("A Long Name For Entries.bin").unwrap_err(), + now(fs.metadata("A Long Name For Entries.bin")).unwrap_err(), Error::NotFound, "the entry survived a remove that reported {result:?}" ); @@ -861,19 +862,19 @@ fn removing_a_cyclic_chain_never_leaves_an_entry_naming_free_clusters() { /// the `is_free` branch and never reaches the comparison at all. #[test] fn a_stale_handle_over_a_reused_empty_slot_is_refused() { - let mut fs = Fat32::mount(pristine()).expect("mount"); + let mut fs = now(Fat32::mount(pristine())).expect("mount"); let t = FatTime::EPOCH; - let mut stale = fs.create("AAA.TXT", t).expect("create"); - fs.flush_meta(&mut stale, t).expect("flush"); - fs.remove("AAA.TXT").expect("remove"); + let mut stale = now(fs.create("AAA.TXT", t)).expect("create"); + now(fs.flush_meta(&mut stale, t)).expect("flush"); + now(fs.remove("AAA.TXT")).expect("remove"); - let mut newcomer = fs.create("BBB.TXT", t).expect("create"); - fs.flush_meta(&mut newcomer, t).expect("flush"); + let mut newcomer = now(fs.create("BBB.TXT", t)).expect("create"); + now(fs.flush_meta(&mut newcomer, t)).expect("flush"); - assert_eq!(fs.write(&mut stale, 0, &[1u8; 4096]).unwrap_err(), Error::NotFound); - assert_eq!(fs.flush_meta(&mut stale, t).unwrap_err(), Error::NotFound); - assert_eq!(fs.metadata("BBB.TXT").expect("metadata").len, 0, "the newcomer was rewritten"); + assert_eq!(now(fs.write(&mut stale, 0, &[1u8; 4096])).unwrap_err(), Error::NotFound); + assert_eq!(now(fs.flush_meta(&mut stale, t)).unwrap_err(), Error::NotFound); + assert_eq!(now(fs.metadata("BBB.TXT")).expect("metadata").len, 0, "the newcomer was rewritten"); } /// F5. Writing through a handle whose entry is gone. @@ -884,14 +885,14 @@ fn a_stale_handle_over_a_reused_empty_slot_is_refused() { /// this write did not fail, so no rollback ran. #[test] fn a_write_through_a_dead_handle_allocates_nothing() { - let mut fs = Fat32::mount(pristine()).expect("mount"); + let mut fs = now(Fat32::mount(pristine())).expect("mount"); let t = FatTime::EPOCH; - let mut f = fs.create("DOOMED.BIN", t).expect("create"); - fs.flush_meta(&mut f, t).expect("flush"); - fs.remove("DOOMED.BIN").expect("remove"); - - let before = fs.free_bytes().expect("free"); - assert_eq!(fs.write(&mut f, 0, &[7u8; 65_536]).unwrap_err(), Error::NotFound); - assert_eq!(fs.set_len(&mut f, 65_536).unwrap_err(), Error::NotFound); - assert_eq!(fs.free_bytes().expect("free"), before, "a dead handle took clusters with it"); + let mut f = now(fs.create("DOOMED.BIN", t)).expect("create"); + now(fs.flush_meta(&mut f, t)).expect("flush"); + now(fs.remove("DOOMED.BIN")).expect("remove"); + + let before = now(fs.free_bytes()).expect("free"); + assert_eq!(now(fs.write(&mut f, 0, &[7u8; 65_536])).unwrap_err(), Error::NotFound); + assert_eq!(now(fs.set_len(&mut f, 65_536)).unwrap_err(), Error::NotFound); + assert_eq!(now(fs.free_bytes()).expect("free"), before, "a dead handle took clusters with it"); } diff --git a/lib/fat32/tests/refused_writes.rs b/lib/fat32/tests/refused_writes.rs index d90fc1e130c..65c167c822c 100644 --- a/lib/fat32/tests/refused_writes.rs +++ b/lib/fat32/tests/refused_writes.rs @@ -4,13 +4,13 @@ //! A refused write may already be on the medium. So each //! write is refused twice over — once before it reaches the bytes, once after — //! and then in bursts that also refuse the call's own repair and the next -//! attempts, as a stopping machine refuses every retry for a while. Reads and -//! writes go through a model of the kernel adapter's resident blocks, which a -//! refused write does not update, so reading back cannot stand in for knowing; -//! a read is refused too, at every read of a truncation. The judge is -//! `toyos-fat32-check`, written from fatgen103 and sharing no code with the -//! writer, over a volume that same crate's fixture built from the -//! specification. +//! attempts, as a stopping machine refuses every retry for a while. A write is +//! the device's, whole blocks, and a refused one has landed whole or not at +//! all: the call hears the same refusal either way, so reading back cannot +//! stand in for knowing. A read is refused too, at every read of a +//! truncation. The judge is `toyos-fat32-check`, written from fatgen103 and +//! sharing no code with the writer, over a volume that same crate's fixture +//! built from the specification. //! //! What must hold: after a single refusal the call has left the volume //! consistent by the time it returns; after any refusals, retrying until the @@ -22,11 +22,12 @@ mod common; #[path = "../check/tests/common/mod.rs"] mod spec_volume; +use std::cell::{Cell, Ref, RefCell}; + +use common::now; use spec_volume::{fat_offset, Volume, BYTES_PER_SECTOR, CLUSTERS, FAT_SECTORS, NUM_FATS}; -use common::{AdapterCache, Landing}; -use toyos_fat32::{ - BlockAccess, Error, Fat32, FatTime, File, IoError, MAX_LFN_CHARS, MAX_REPAIR_STEPS, -}; +use toyos_blockdev::{within, Block, BlockDev, Error as DevError, BLOCK}; +use toyos_fat32::{Error, Fat32, FatTime, File, MAX_LFN_CHARS, MAX_REPAIR_STEPS}; use toyos_fat32_check::Complaint; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -37,103 +38,150 @@ enum Outcome { Landed, } -impl Outcome { - fn landing(self) -> Landing { - match self { - Outcome::Refused => Landing::NotReached, - Outcome::Landed => Landing::Reached, - } +/// One write as the device took it: where, and the bytes it would replace. +struct Write<'a> { + offset: u64, + before: &'a [u8], + after: &'a [u8], +} + +impl Write<'_> { + fn touches(&self, lo: u64, hi: u64) -> bool { + self.offset < hi && self.offset + self.after.len() as u64 > lo + } + + /// Where each 4-byte word this write changes lies: a FAT entry, or a + /// directory entry's part. + fn changed_words(&self) -> impl Iterator + '_ { + (0..self.after.len()).step_by(4).filter(|&i| self.before[i..i + 4] != self.after[i..i + 4]).map(|i| self.offset + i as u64) } } -type Plan = Box Option>; +type Plan = Box) -> Option>; /// Whether to refuse read `index` since the plan was armed. type ReadPlan = Box bool>; type Call = fn(&mut Fat32, &mut Option, u32) -> Result<(), Error>; -/// The volume behind the kernel adapter's resident blocks, refusing whichever -/// reads and writes the plans name. -/// -/// A refused write does not update the resident copy, even when it landed on -/// the medium, so a call that reads to learn a refused write's outcome learns -/// the wrong one here, as it would on the stick. +/// The volume, refusing whichever reads and writes the plans name. struct Faulty { - /// The medium, which the checker judges, and the adapter's view of it. - cache: AdapterCache, + /// The medium, which the checker judges, padded to whole blocks as the + /// partition under a volume is. + medium: RefCell>, + /// The volume's own length. + len: usize, /// Writes since the plan was armed. - writes: u64, - plan: Option, + writes: Cell, + plan: RefCell>, /// Reads since the read plan was armed. - reads: u64, - read_plan: Option, - refused: u32, + reads: Cell, + read_plan: RefCell>, + refused: Cell, } impl Faulty { - fn new(bytes: Vec) -> Faulty { - Faulty { cache: AdapterCache::new(bytes), writes: 0, plan: None, reads: 0, read_plan: None, refused: 0 } + fn new(mut bytes: Vec) -> Faulty { + let len = bytes.len(); + bytes.resize(len.next_multiple_of(BLOCK), 0); + Faulty { + medium: RefCell::new(bytes), + len, + writes: Cell::new(0), + plan: RefCell::new(None), + reads: Cell::new(0), + read_plan: RefCell::new(None), + refused: Cell::new(0), + } + } + + /// The volume as the medium holds it, which is what a judge reads. + fn medium(&self) -> Ref<'_, [u8]> { + Ref::map(self.medium.borrow(), |m| &m[..self.len]) + } + + fn into_medium(self) -> Vec { + let mut medium = self.medium.into_inner(); + medium.truncate(self.len); + medium } - fn medium(&self) -> &[u8] { - self.cache.medium() + /// Change the medium behind the volume's back. + fn poke(&mut self, offset: usize, bytes: &[u8]) { + self.medium.get_mut()[offset..offset + bytes.len()].copy_from_slice(bytes); } fn arm(&mut self, plan: Plan) { - self.writes = 0; - self.refused = 0; - self.plan = Some(plan); + self.writes.set(0); + self.refused.set(0); + *self.plan.get_mut() = Some(plan); } fn arm_reads(&mut self, plan: ReadPlan) { - self.reads = 0; - self.refused = 0; - self.read_plan = Some(plan); + self.reads.set(0); + self.refused.set(0); + *self.read_plan.get_mut() = Some(plan); } - fn bound(&self, offset: u64, len: usize) -> Result<(), IoError> { - let end = offset.checked_add(len as u64).ok_or(IoError::Device)?; - if end > self.cache.len() { - return Err(IoError::Device); - } - Ok(()) + fn disarm(&mut self) { + *self.plan.get_mut() = None; + } + + fn refused(&self) -> u32 { + self.refused.get() + } + + fn reads(&self) -> u64 { + self.reads.get() + } + + fn blocks_of(&self) -> u64 { + (self.medium.borrow().len() / BLOCK) as u64 } } -impl BlockAccess for Faulty { - fn capacity(&self) -> u64 { - self.cache.len() +impl BlockDev for Faulty { + fn blocks(&self) -> u64 { + self.blocks_of() } - fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<(), IoError> { - self.bound(offset, buf.len())?; - let index = self.reads; - self.reads += 1; - if self.read_plan.as_mut().is_some_and(|p| p(index)) { - self.refused += 1; - return Err(IoError::Device); + async fn read(&self, lba: u64, out: &mut [Block]) -> Result<(), DevError> { + within(lba, out.len(), self.blocks_of())?; + let index = self.reads.get(); + self.reads.set(index + 1); + if self.read_plan.borrow_mut().as_mut().is_some_and(|p| p(index)) { + self.refused.set(self.refused.get() + 1); + return Err(DevError::Device); } - self.cache.read(offset, buf); + let at = lba as usize * BLOCK; + let out = out.as_flattened_mut(); + out.copy_from_slice(&self.medium.borrow()[at..at + out.len()]); Ok(()) } - fn write_at(&mut self, offset: u64, buf: &[u8]) -> Result<(), IoError> { - self.bound(offset, buf.len())?; - let index = self.writes; - self.writes += 1; - let verdict = self.plan.as_mut().and_then(|p| p(index, offset, buf.len())); - self.cache.write(offset, buf, verdict.map(Outcome::landing)); + async fn write(&self, lba: u64, data: &[Block]) -> Result<(), DevError> { + within(lba, data.len(), self.blocks_of())?; + let index = self.writes.get(); + self.writes.set(index + 1); + let (at, after) = (lba as usize * BLOCK, data.as_flattened()); + let verdict = { + let medium = self.medium.borrow(); + let write = Write { offset: at as u64, before: &medium[at..at + after.len()], after }; + self.plan.borrow_mut().as_mut().and_then(|p| p(index, &write)) + }; + if verdict != Some(Outcome::Refused) { + self.medium.borrow_mut()[at..at + after.len()].copy_from_slice(after); + } match verdict { None => Ok(()), Some(_) => { - self.refused += 1; - Err(IoError::Device) + self.refused.set(self.refused.get() + 1); + Err(DevError::Device) } } } - fn flush(&mut self) -> Result<(), IoError> { + async fn flush(&self) -> Result<(), DevError> { Ok(()) } } @@ -154,8 +202,8 @@ fn complaints(bytes: &[u8], synced: bool) -> Vec { fn assert_clean(fs: &mut Fat32, synced: bool, context: &str) { let g = *fs.geometry(); - common::assert_fats_agree(&g, fs.device().medium()); - let said = complaints(fs.device().medium(), synced); + common::assert_fats_agree(&g, &fs.device().medium()); + let said = complaints(&fs.device().medium(), synced); assert!(said.is_empty(), "{context}:\n{}", toyos_fat32_check::describe(&said)); } @@ -179,16 +227,16 @@ struct Scenario { fn fixture(s: &Scenario) -> (Vec, Option) { let mut v = Volume::new(); v.finish(); - let mut fs = Fat32::mount(Faulty::new(v.bytes)).expect("mount the fixture"); + let mut fs = now(Fat32::mount(Faulty::new(v.bytes))).expect("mount the fixture"); let handle = (s.setup)(&mut fs); - fs.sync().expect("sync the setup"); + now(fs.sync()).expect("sync the setup"); assert_clean(&mut fs, true, &format!("{}: the setup itself", s.name)); - (fs.into_device().cache.into_medium(), handle) + (fs.into_device().into_medium(), handle) } /// Refuse `count` writes from write `from`, each with `outcome`. fn burst(from: u64, count: u64, outcome: Outcome) -> Plan { - Box::new(move |i, _, _| (i >= from && i - from < count).then_some(outcome)) + Box::new(move |i, _| (i >= from && i - from < count).then_some(outcome)) } /// Every write of `s.call`, refused each way, alone and in bursts. Returns how @@ -243,12 +291,12 @@ fn refused_case( single: bool, context: &str, ) -> bool { - let mut fs = Fat32::mount(Faulty::new(base.to_vec())).expect("mount"); + let mut fs = now(Fat32::mount(Faulty::new(base.to_vec()))).expect("mount"); let mut h = handle; arm(fs.device()); let first = (s.call)(&mut fs, &mut h, 0); - if fs.device().refused == 0 { + if fs.device().refused() == 0 { assert_eq!(first, Ok(()), "{context}: nothing was refused"); return false; } @@ -269,7 +317,7 @@ fn refused_case( result = (s.call)(&mut fs, &mut h, attempt); } let mut syncs = 0; - while let Err(e) = fs.sync() { + while let Err(e) = now(fs.sync()) { syncs += 1; assert!(syncs <= 24, "{context}: sync still refused: {e}"); } @@ -283,16 +331,16 @@ fn refused_case( const LOG: &str = "dated.log"; fn one_cluster_logged(fs: &mut Fat32) -> Option { - let mut f = fs.create(LOG, stamp()).expect("create"); - fs.write(&mut f, 0, &common::pattern(512, 1)).expect("first cluster"); - fs.flush_meta(&mut f, stamp()).expect("flush"); + let mut f = now(fs.create(LOG, stamp())).expect("create"); + now(fs.write(&mut f, 0, &common::pattern(512, 1))).expect("first cluster"); + now(fs.flush_meta(&mut f, stamp())).expect("flush"); Some(f) } fn append_two_clusters(fs: &mut Fat32, h: &mut Option, _: u32) -> Result<(), Error> { let f = h.as_mut().expect("a handle"); - fs.write(f, 512, &common::pattern(1024, 2))?; - fs.flush_meta(f, stamp()) + now(fs.write(f, 512, &common::pattern(1024, 2)))?; + now(fs.flush_meta(f, stamp())) } fn appended_in_place(fs: &mut Fat32, _: &mut Option) { @@ -301,7 +349,7 @@ fn appended_in_place(fs: &mut Fat32, _: &mut Option) { assert_eq!(common::read_all(fs, LOG), want); // One run: the retry claimed the clusters the refusal gave back, rather // than stepping over one it had leaked. - assert_eq!(fs.extents(LOG, 8).expect("extents").len(), 1, "the retry did not reuse the claim"); + assert_eq!(now(fs.extents(LOG, 8)).expect("extents").len(), 1, "the retry did not reuse the claim"); } /// `append_cluster`: claim, then link, per cluster; then the entry. @@ -315,18 +363,18 @@ fn first_allocation() -> Scenario { name: "first allocation", commits: false, setup: |fs| { - let mut f = fs.create(LOG, stamp()).expect("create"); - fs.flush_meta(&mut f, stamp()).expect("flush"); + let mut f = now(fs.create(LOG, stamp())).expect("create"); + now(fs.flush_meta(&mut f, stamp())).expect("flush"); Some(f) }, call: |fs, h, _| { let f = h.as_mut().expect("a handle"); - fs.write(f, 0, &common::pattern(1536, 3))?; - fs.flush_meta(f, stamp()) + now(fs.write(f, 0, &common::pattern(1536, 3)))?; + now(fs.flush_meta(f, stamp())) }, verify: |fs, _| { assert_eq!(common::read_all(fs, LOG), common::pattern(1536, 3)); - assert_eq!(fs.extents(LOG, 8).expect("extents").len(), 1); + assert_eq!(now(fs.extents(LOG, 8)).expect("extents").len(), 1); }, } } @@ -341,7 +389,7 @@ fn append_and_sync() -> Scenario { setup: one_cluster_logged, call: |fs, h, a| { append_two_clusters(fs, h, a)?; - fs.sync() + now(fs.sync()) }, verify: appended_in_place, } @@ -354,15 +402,15 @@ fn truncate() -> Scenario { name: "truncate", commits: false, setup: |fs| { - let mut f = fs.create(LOG, stamp()).expect("create"); - fs.write(&mut f, 0, &common::pattern(5 * 512, 5)).expect("write"); - fs.flush_meta(&mut f, stamp()).expect("flush"); + let mut f = now(fs.create(LOG, stamp())).expect("create"); + now(fs.write(&mut f, 0, &common::pattern(5 * 512, 5))).expect("write"); + now(fs.flush_meta(&mut f, stamp())).expect("flush"); Some(f) }, call: |fs, h, _| { let f = h.as_mut().expect("a handle"); - fs.set_len(f, 700)?; - fs.flush_meta(f, stamp()) + now(fs.set_len(f, 700))?; + now(fs.flush_meta(f, stamp())) }, verify: |fs, _| { assert_eq!(common::read_all(fs, LOG), common::pattern(5 * 512, 5)[..700].to_vec()); @@ -379,13 +427,13 @@ fn truncate_to_zero() -> Scenario { setup: truncate().setup, call: |fs, h, _| { let f = h.as_mut().expect("a handle"); - fs.set_len(f, 0)?; - fs.flush_meta(f, stamp()) + now(fs.set_len(f, 0))?; + now(fs.flush_meta(f, stamp())) }, verify: |fs, _| { - assert_eq!(fs.metadata(LOG).expect("metadata").len, 0); - assert!(fs.extents(LOG, 8).expect("extents").is_empty(), "the entry still names a chain"); - let free = fs.free_bytes().expect("free"); + assert_eq!(now(fs.metadata(LOG)).expect("metadata").len, 0); + assert!(now(fs.extents(LOG, 8)).expect("extents").is_empty(), "the entry still names a chain"); + let free = now(fs.free_bytes()).expect("free"); assert_eq!(free, fs.total_bytes() - 2 * 512, "only the root's two clusters stay taken"); }, } @@ -399,15 +447,15 @@ const ROOT_SLOTS: u32 = 32; fn full_root(fs: &mut Fat32) -> Option { for i in 0..ROOT_SLOTS { - let mut f = fs.create(&format!("F{i:02}.TXT"), stamp()).expect("fill the root"); - fs.flush_meta(&mut f, stamp()).expect("flush"); + let mut f = now(fs.create(&format!("F{i:02}.TXT"), stamp())).expect("fill the root"); + now(fs.flush_meta(&mut f, stamp())).expect("flush"); } None } fn root_intact(fs: &mut Fat32) { for i in 0..ROOT_SLOTS { - assert!(fs.exists(&format!("F{i:02}.TXT")).expect("exists"), "F{i:02}.TXT went missing"); + assert!(now(fs.exists(&format!("F{i:02}.TXT"))).expect("exists"), "F{i:02}.TXT went missing"); } } @@ -418,10 +466,10 @@ fn extend_dir() -> Scenario { name: "extend-dir", commits: false, setup: full_root, - call: |fs, _, _| fs.create("A name long enough for three entries.txt", stamp()).map(drop), + call: |fs, _, _| now(fs.create("A name long enough for three entries.txt", stamp())).map(drop), verify: |fs, _| { root_intact(fs); - let m = fs.metadata("A name long enough for three entries.txt").expect("created"); + let m = now(fs.metadata("A name long enough for three entries.txt")).expect("created"); assert_eq!(m.len, 0); }, } @@ -435,10 +483,10 @@ fn long_name_create() -> Scenario { name: "create", commits: false, setup: |_| None, - call: |fs, _, _| fs.create("2026-09-21-083539.log", stamp()).map(drop), + call: |fs, _, _| now(fs.create("2026-09-21-083539.log", stamp())).map(drop), verify: |fs, _| { - assert_eq!(fs.read_dir("", 64).expect("read_dir").len(), 1, "one name, once"); - assert!(fs.exists("2026-09-21-083539.log").expect("exists")); + assert_eq!(now(fs.read_dir("", 64)).expect("read_dir").len(), 1, "one name, once"); + assert!(now(fs.exists("2026-09-21-083539.log")).expect("exists")); }, } } @@ -450,13 +498,13 @@ fn nested_create_dir() -> Scenario { name: "create_dir", commits: false, setup: full_root, - call: |fs, _, _| fs.create_dir("A directory with a long name", stamp()), + call: |fs, _, _| now(fs.create_dir("A directory with a long name", stamp())), verify: |fs, _| { root_intact(fs); - assert!(fs.metadata("A directory with a long name").expect("created").is_dir); + assert!(now(fs.metadata("A directory with a long name")).expect("created").is_dir); common::write_new(fs, "A directory with a long name/inner.bin", &[7u8; 700], stamp()); - fs.sync().expect("sync"); - let said = complaints(fs.device().medium(), true); + now(fs.sync()).expect("sync"); + let said = complaints(&fs.device().medium(), true); assert!(said.is_empty(), "{}", toyos_fat32_check::describe(&said)); }, } @@ -469,14 +517,14 @@ fn rename() -> Scenario { name: "rename", commits: false, setup: |fs| { - fs.create_dir("into", stamp()).expect("mkdir"); - fs.create_dir("moving", stamp()).expect("mkdir"); + now(fs.create_dir("into", stamp())).expect("mkdir"); + now(fs.create_dir("moving", stamp())).expect("mkdir"); common::write_new(fs, "moving/kept.bin", &common::pattern(700, 6), stamp()); None }, - call: |fs, _, _| fs.rename("moving", "into/A moved directory"), + call: |fs, _, _| now(fs.rename("moving", "into/A moved directory")), verify: |fs, _| { - assert!(!fs.exists("moving").expect("exists")); + assert!(!now(fs.exists("moving")).expect("exists")); assert_eq!(common::read_all(fs, "into/A moved directory/kept.bin"), common::pattern(700, 6)); }, } @@ -495,10 +543,10 @@ fn remove() -> Scenario { common::write_new(fs, DOOMED, &common::pattern(5 * 512, 4), stamp()); None }, - call: |fs, _, _| fs.remove(DOOMED), + call: |fs, _, _| now(fs.remove(DOOMED)), verify: |fs, _| { - assert!(!fs.exists(DOOMED).expect("exists")); - let free = fs.free_bytes().expect("free"); + assert!(!now(fs.exists(DOOMED)).expect("exists")); + let free = now(fs.free_bytes()).expect("free"); assert_eq!(free, fs.total_bytes() - 2 * 512, "only the root's two clusters stay taken"); }, } @@ -510,13 +558,13 @@ fn remove_dir() -> Scenario { name: "remove_dir", commits: true, setup: |fs| { - fs.create_dir("A directory with a long name", stamp()).expect("mkdir"); + now(fs.create_dir("A directory with a long name", stamp())).expect("mkdir"); None }, - call: |fs, _, _| fs.remove_dir("A directory with a long name"), + call: |fs, _, _| now(fs.remove_dir("A directory with a long name")), verify: |fs, _| { - assert!(!fs.exists("A directory with a long name").expect("exists")); - assert_eq!(fs.free_bytes().expect("free"), fs.total_bytes() - 2 * 512); + assert!(!now(fs.exists("A directory with a long name")).expect("exists")); + assert_eq!(now(fs.free_bytes()).expect("free"), fs.total_bytes() - 2 * 512); }, } } @@ -638,14 +686,14 @@ fn a_stop_at_any_write_leaves_only_the_named_windows() { let moved = allocation_moved(&s, &base, handle); for outcome in [Outcome::Refused, Outcome::Landed] { for from in 0u64.. { - let mut fs = Fat32::mount(Faulty::new(base.clone())).expect("mount"); + let mut fs = now(Fat32::mount(Faulty::new(base.clone()))).expect("mount"); let mut h = handle; fs.device().arm(burst(from, u64::MAX, outcome)); let _ = (s.call)(&mut fs, &mut h, 0); - if fs.device().refused == 0 { + if fs.device().refused() == 0 { break; } - let said = complaints(fs.device().medium(), false); + let said = complaints(&fs.device().medium(), false); let context = format!("{}: stopped at write {from}, {outcome:?}", s.name); let splits = said.iter().filter(|c| matches!(c, Complaint::FatMirror { .. })).count(); assert!(splits <= 1, "{context}: {splits} split entries"); @@ -683,10 +731,12 @@ fn a_stop_at_any_write_leaves_only_the_named_windows() { // ----------------------------------------------- the shape a stop produces -/// Where FAT copy 0 lives on the fixture. +/// The blocks FAT copy 0 alone lives in on the fixture: its last block is the +/// mirror's first too. fn active_fat() -> (u64, u64) { let lo = fat_offset(0, 0) as u64; - (lo, lo + (FAT_SECTORS * BYTES_PER_SECTOR) as u64) + let end = lo + (FAT_SECTORS * BYTES_PER_SECTOR) as u64; + (lo, end - end % BLOCK as u64) } /// T14 run 139 and `quiesce_leaves_the_volume_whole`'s forced shape, on the @@ -699,50 +749,50 @@ fn a_stop_that_refuses_every_retry_for_a_while_leaks_nothing() { assert_eq!(NUM_FATS, 2); let s = append(); let (base, handle) = fixture(&s); - let mut fs = Fat32::mount(Faulty::new(base)).expect("mount"); + let mut fs = now(Fat32::mount(Faulty::new(base))).expect("mount"); let mut h = handle; let (lo, hi) = active_fat(); - let active = move |offset: u64, len: usize| offset < hi && offset + len as u64 > lo; + let active = move |w: &Write<'_>| w.touches(lo, hi); // Attempt 1 lets its first active-FAT write through — the claim — and // refuses every one after it. let mut seen = 0u32; - fs.device().arm(Box::new(move |_, offset, len| { - if !active(offset, len) { + fs.device().arm(Box::new(move |_, w| { + if !active(w) { return None; } seen += 1; (seen > 1).then_some(Outcome::Refused) })); assert_eq!(append_two_clusters(&mut fs, &mut h, 0), Err(Error::Io)); - assert!(fs.device().refused >= 2, "the link and the rollback's re-drive were both refused"); + assert!(fs.device().refused() >= 2, "the link and the rollback's re-drive were both refused"); assert!(fs.pending_repair() > 0, "the refused rollback is queued"); // Each later attempt, and a sync, meets the first attempt's repair still // unlanded. for attempt in 2..=9 { - fs.device().arm(Box::new(move |_, offset, len| active(offset, len).then_some(Outcome::Refused))); + fs.device().arm(Box::new(move |_, w| active(w).then_some(Outcome::Refused))); assert_eq!(append_two_clusters(&mut fs, &mut h, attempt), Err(Error::Io)); - assert_eq!(fs.sync(), Err(Error::Io), "sync on attempt {attempt}"); + assert_eq!(now(fs.sync()), Err(Error::Io), "sync on attempt {attempt}"); assert!(fs.pending_repair() > 0, "the repair, still pending on attempt {attempt}"); } - fs.device().plan = None; + fs.device().disarm(); append_two_clusters(&mut fs, &mut h, 10).expect("attempt 10 on an answering device"); assert_eq!(fs.pending_repair(), 0); let mut seen = 0u32; - fs.device().arm(Box::new(move |_, offset, len| { - if !active(offset, len) { + fs.device().arm(Box::new(move |_, w| { + if !active(w) { return None; } seen += 1; (seen > 1).then_some(Outcome::Refused) })); let f = h.as_mut().expect("a handle"); - assert_eq!(fs.write(f, 1536, &common::pattern(1024, 7)), Err(Error::Io)); + assert_eq!(now(fs.write(f, 1536, &common::pattern(1024, 7))), Err(Error::Io)); assert!(fs.pending_repair() > 0, "the later refusal's repair is queued"); - fs.device().plan = None; - fs.sync().expect("sync lands the later repair"); + fs.device().disarm(); + now(fs.sync()).expect("sync lands the later repair"); assert_eq!(fs.pending_repair(), 0); assert_clean(&mut fs, true, "after the tenth attempt and the refused write after it"); (s.verify)(&mut fs, &mut h); @@ -752,10 +802,10 @@ fn a_stop_that_refuses_every_retry_for_a_while_leaks_nothing() { /// clean run of `s.call` claims and frees, which bounds what a stop inside it /// can orphan. fn allocation_moved(s: &Scenario, base: &[u8], handle: Option) -> u32 { - let mut fs = Fat32::mount(Faulty::new(base.to_vec())).expect("mount"); + let mut fs = now(Fat32::mount(Faulty::new(base.to_vec()))).expect("mount"); let mut h = handle; (s.call)(&mut fs, &mut h, 0).expect("the call on an answering device"); - let after = fs.into_device().cache.into_medium(); + let after = fs.into_device().into_medium(); let taken = |bytes: &[u8], cluster: u32| { let at = fat_offset(0, cluster); u32::from_le_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]]) & 0x0FFF_FFFF != 0 @@ -776,26 +826,26 @@ fn the_deepest_repair_reaches_its_bound_and_no_further() { let (from, to) = (format!("from/{}", long('L')), format!("into/{}", long('M'))); let mut v = Volume::new(); v.finish(); - let mut fs = Fat32::mount(Faulty::new(v.bytes)).expect("mount"); - fs.create_dir("into", stamp()).expect("mkdir"); + let mut fs = now(Fat32::mount(Faulty::new(v.bytes))).expect("mount"); + now(fs.create_dir("into", stamp())).expect("mkdir"); // One 512-byte cluster: `.`, `..` and these fourteen fill it. for i in 0..14 { - let mut f = fs.create(&format!("into/F{i:02}.TXT"), stamp()).expect("fill"); - fs.flush_meta(&mut f, stamp()).expect("flush"); + let mut f = now(fs.create(&format!("into/F{i:02}.TXT"), stamp())).expect("fill"); + now(fs.flush_meta(&mut f, stamp())).expect("flush"); } - fs.create_dir("from", stamp()).expect("mkdir"); - fs.create_dir(&from, stamp()).expect("mkdir"); - fs.sync().expect("sync"); - let base = fs.into_device().cache.into_medium(); + now(fs.create_dir("from", stamp())).expect("mkdir"); + now(fs.create_dir(&from, stamp())).expect("mkdir"); + now(fs.sync()).expect("sync"); + let base = fs.into_device().into_medium(); let mut deepest = 0; for stop in 0u64.. { - let mut fs = Fat32::mount(Faulty::new(base.clone())).expect("mount"); + let mut fs = now(Fat32::mount(Faulty::new(base.clone()))).expect("mount"); fs.device().arm(burst(stop, u64::MAX, Outcome::Refused)); - let result = fs.rename(&from, &to); - if fs.device().refused == 0 { + let result = now(fs.rename(&from, &to)); + if fs.device().refused() == 0 { result.expect("the rename on an answering device"); - assert!(fs.metadata(&to).expect("moved").is_dir); + assert!(now(fs.metadata(&to)).expect("moved").is_dir); break; } deepest = deepest.max(fs.pending_repair()); @@ -811,19 +861,19 @@ fn the_deepest_repair_reaches_its_bound_and_no_further() { fn a_call_that_refused_nothing_keeps_the_free_count() { let mut v = Volume::new(); v.finish(); - let mut fs = Fat32::mount(Faulty::new(v.bytes)).expect("mount"); - let before = fs.free_bytes().expect("free"); - fs.sync().expect("sync the counted hint"); - let mut f = fs.create(LOG, stamp()).expect("create"); + let mut fs = now(Fat32::mount(Faulty::new(v.bytes))).expect("mount"); + let before = now(fs.free_bytes()).expect("free"); + now(fs.sync()).expect("sync the counted hint"); + let mut f = now(fs.create(LOG, stamp())).expect("create"); let too_big = vec![0u8; (before + 512) as usize]; - assert_eq!(fs.write(&mut f, 0, &too_big), Err(Error::NoSpace)); + assert_eq!(now(fs.write(&mut f, 0, &too_big)), Err(Error::NoSpace)); assert_eq!(fs.pending_repair(), 0); fs.device().arm_reads(Box::new(|_| false)); - fs.sync().expect("sync"); - let reads = fs.device().reads; + now(fs.sync()).expect("sync"); + let reads = fs.device().reads(); assert!(reads < FAT_SECTORS as u64, "the sync read {reads} times: it counted the FAT again"); - assert_eq!(fs.free_bytes().expect("free"), before); + assert_eq!(now(fs.free_bytes()).expect("free"), before); assert_clean(&mut fs, true, "after the refused write and its sync"); } @@ -834,25 +884,29 @@ fn a_call_that_refused_nothing_keeps_the_free_count() { fn an_unlanded_rollback_is_the_answer() { let mut v = Volume::new(); v.finish(); - let mut fs = Fat32::mount(Faulty::new(v.bytes)).expect("mount"); - let before = fs.free_bytes().expect("free"); - let mut f = fs.create(LOG, stamp()).expect("create"); + let mut fs = now(Fat32::mount(Faulty::new(v.bytes))).expect("mount"); + let before = now(fs.free_bytes()).expect("free"); + let mut f = now(fs.create(LOG, stamp())).expect("create"); // Every FAT entry the growth claims is written as a claim, then as a link; // the third write to one is its rollback's free. let mut seen = std::collections::HashMap::::new(); - fs.device().arm(Box::new(move |_, offset, _| { - let n = seen.entry(offset).or_insert(0); - *n += 1; - (*n >= 3).then_some(Outcome::Refused) + fs.device().arm(Box::new(move |_, w| { + let mut third = false; + for word in w.changed_words() { + let n = seen.entry(word).or_insert(0); + *n += 1; + third |= *n >= 3; + } + third.then_some(Outcome::Refused) })); let too_big = vec![0u8; (before + 512) as usize]; - assert_eq!(fs.write(&mut f, 0, &too_big), Err(Error::Io)); + assert_eq!(now(fs.write(&mut f, 0, &too_big)), Err(Error::Io)); assert!(fs.pending_repair() > 0); - fs.device().plan = None; - fs.sync().expect("sync lands the rollback"); + fs.device().disarm(); + now(fs.sync()).expect("sync lands the rollback"); assert_eq!(fs.pending_repair(), 0); - assert_eq!(fs.free_bytes().expect("free"), before); + assert_eq!(now(fs.free_bytes()).expect("free"), before); assert_clean(&mut fs, true, "after the rollback landed"); } @@ -863,20 +917,20 @@ fn an_unlanded_rollback_is_the_answer() { fn a_remove_reports_a_corrupt_chain_under_the_name_it_erased() { let mut v = Volume::new(); v.finish(); - let mut fs = Fat32::mount(Faulty::new(v.bytes)).expect("mount"); + let mut fs = now(Fat32::mount(Faulty::new(v.bytes))).expect("mount"); common::write_new(&mut fs, DOOMED, &common::pattern(5 * 512, 4), stamp()); let third = { - let f = fs.extents(DOOMED, 8).expect("extents"); + let f = now(fs.extents(DOOMED, 8)).expect("extents"); assert_eq!(f.len(), 1, "one run"); ((f[0].offset as usize - spec_volume::cluster_offset(2)) / spec_volume::BYTES_PER_CLUSTER) as u32 + 2 + 2 }; // Cluster 1 is reserved, so a link to it is a link to nothing. for copy in 0..NUM_FATS { let at = fat_offset(copy, third); - fs.device().cache.poke(at, &1u32.to_le_bytes()); + fs.device().poke(at, &1u32.to_le_bytes()); } - assert_eq!(fs.remove(DOOMED), Err(Error::CorruptChain)); - assert!(!fs.exists(DOOMED).expect("exists"), "the name is gone either way"); + assert_eq!(now(fs.remove(DOOMED)), Err(Error::CorruptChain)); + assert!(!now(fs.exists(DOOMED)).expect("exists"), "the name is gone either way"); assert_eq!(fs.pending_repair(), 0); } @@ -887,27 +941,28 @@ fn a_remove_reports_a_corrupt_chain_under_the_name_it_erased() { fn a_remove_whose_free_the_device_fails_answers_ok_and_the_next_call_the_failure() { let s = remove(); let (base, _) = fixture(&s); - let mut fs = Fat32::mount(Faulty::new(base)).expect("mount"); + let mut fs = now(Fat32::mount(Faulty::new(base))).expect("mount"); let lo = fat_offset(0, 0) as u64; let hi = fat_offset(NUM_FATS, 0) as u64; - let fat = move |offset: u64, len: usize| offset < hi && offset + len as u64 > lo; - fs.device().arm(Box::new(move |_, offset, len| fat(offset, len).then_some(Outcome::Refused))); + // By the bytes it changes: the root directory's first sectors share a block + // with the mirror's last. + fs.device().arm(Box::new(move |_, w| w.changed_words().any(|at| (lo..hi).contains(&at)).then_some(Outcome::Refused))); - assert_eq!(fs.remove(DOOMED), Ok(())); - assert!(fs.device().refused >= 2, "the free was driven twice and failed both times"); + assert_eq!(now(fs.remove(DOOMED)), Ok(())); + assert!(fs.device().refused() >= 2, "the free was driven twice and failed both times"); assert!(fs.pending_repair() > 0, "the free is queued"); - assert!(!fs.exists(DOOMED).expect("exists"), "the name is gone"); + assert!(!now(fs.exists(DOOMED)).expect("exists"), "the name is gone"); - let refused = fs.device().refused; - assert_eq!(fs.create("after.txt", stamp()).map(drop), Err(Error::Io)); - assert!(fs.device().refused > refused, "the next call drove the free first"); - assert!(!fs.exists("after.txt").expect("exists"), "a call behind an unlanded free starts nothing"); - assert_eq!(fs.sync(), Err(Error::Io)); + let refused = fs.device().refused(); + assert_eq!(now(fs.create("after.txt", stamp())).map(drop), Err(Error::Io)); + assert!(fs.device().refused() > refused, "the next call drove the free first"); + assert!(!now(fs.exists("after.txt")).expect("exists"), "a call behind an unlanded free starts nothing"); + assert_eq!(now(fs.sync()), Err(Error::Io)); - fs.device().plan = None; - fs.create("after.txt", stamp()).expect("create once the device answers"); + fs.device().disarm(); + now(fs.create("after.txt", stamp())).expect("create once the device answers"); assert_eq!(fs.pending_repair(), 0); - fs.sync().expect("sync"); + now(fs.sync()).expect("sync"); assert_clean(&mut fs, true, "after the failed free landed"); (s.verify)(&mut fs, &mut None); } diff --git a/lib/fat32/tests/replace_rename.rs b/lib/fat32/tests/replace_rename.rs index 615f68f1df5..0337db79db5 100644 --- a/lib/fat32/tests/replace_rename.rs +++ b/lib/fat32/tests/replace_rename.rs @@ -5,10 +5,19 @@ mod spec_volume; use spec_volume::{cluster_offset, Volume, DIR_FST_CLUS_HI, DIR_FST_CLUS_LO, ROOT_CLUSTER}; -use toyos_fat32::{BlockAccess, Error, Fat32, IoError}; +use std::cell::{Cell, RefCell}; +use toyos_blockdev::{block_on_ready as now, within, Block, BlockDev, Error as DevError, BLOCK}; +use toyos_fat32::{Error, Fat32}; + +/// The volume, padded to whole blocks, refusing the first write that puts the +/// source's cluster in the destination's entry and, when asked, the first +/// after it that makes a live entry name `rollback_cluster`. A write is told +/// by the entries it changes, since it reaches the device as whole blocks. struct RefuseSourceAtDestination { - bytes: Vec, + bytes: RefCell>, + /// The volume's own length. + len: usize, destination_entry: u64, source_cluster: u32, /// Set to refuse the rollback too. Any live short entry naming this @@ -16,67 +25,86 @@ struct RefuseSourceAtDestination { /// staging rename's own writes are all behind that point, and the refused /// move's repair writes the slots it took back as erased ones. rollback_cluster: Option, - refused_source: bool, - refused_rollback: bool, + refused_source: Cell, + refused_rollback: Cell, } impl RefuseSourceAtDestination { - fn check(&self, offset: u64, len: usize) -> Result<(usize, usize), IoError> { - let start = usize::try_from(offset).map_err(|_| IoError::Device)?; - let end = start.checked_add(len).ok_or(IoError::Device)?; - if end > self.bytes.len() { - return Err(IoError::Device); + fn new(mut bytes: Vec, destination_entry: u64, source_cluster: u32, rollback_cluster: Option) -> Self { + let len = bytes.len(); + bytes.resize(len.next_multiple_of(BLOCK), 0); + Self { + bytes: RefCell::new(bytes), + len, + destination_entry, + source_cluster, + rollback_cluster, + refused_source: Cell::new(false), + refused_rollback: Cell::new(false), } - Ok((start, end)) + } + + fn volume(&self) -> Vec { + self.bytes.borrow()[..self.len].to_vec() + } + + /// Whether the write of `after` at byte `at` is one this device refuses. + fn refuses(&self, at: usize, after: &[u8]) -> bool { + let bytes = self.bytes.borrow(); + let changed = (0..after.len()).step_by(32).filter(|&i| bytes[at + i..at + i + 32] != after[i..i + 32]); + for i in changed { + let entry = &after[i..i + 32]; + let high = u16::from_le_bytes([entry[DIR_FST_CLUS_HI], entry[DIR_FST_CLUS_HI + 1]]) as u32; + let low = u16::from_le_bytes([entry[DIR_FST_CLUS_LO], entry[DIR_FST_CLUS_LO + 1]]) as u32; + let cluster = (high << 16) | low; + if !self.refused_source.get() && (at + i) as u64 == self.destination_entry && cluster == self.source_cluster { + self.refused_source.set(true); + return true; + } + if self.refused_source.get() + && !self.refused_rollback.get() + && entry[0] != 0xE5 + && self.rollback_cluster == Some(cluster) + { + self.refused_rollback.set(true); + return true; + } + } + false } } -impl BlockAccess for RefuseSourceAtDestination { - fn capacity(&self) -> u64 { - self.bytes.len() as u64 +impl BlockDev for RefuseSourceAtDestination { + fn blocks(&self) -> u64 { + (self.bytes.borrow().len() / BLOCK) as u64 } - fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<(), IoError> { - let (start, end) = self.check(offset, buf.len())?; - buf.copy_from_slice(&self.bytes[start..end]); + async fn read(&self, lba: u64, out: &mut [Block]) -> Result<(), DevError> { + within(lba, out.len(), self.blocks())?; + let (at, out) = (lba as usize * BLOCK, out.as_flattened_mut()); + out.copy_from_slice(&self.bytes.borrow()[at..at + out.len()]); Ok(()) } - fn write_at(&mut self, offset: u64, buf: &[u8]) -> Result<(), IoError> { - let cluster = if buf.len() == 32 { - let high = u16::from_le_bytes([buf[DIR_FST_CLUS_HI], buf[DIR_FST_CLUS_HI + 1]]) as u32; - let low = u16::from_le_bytes([buf[DIR_FST_CLUS_LO], buf[DIR_FST_CLUS_LO + 1]]) as u32; - (high << 16) | low - } else { - 0 - }; - if !self.refused_source && offset == self.destination_entry && cluster == self.source_cluster { - self.refused_source = true; - return Err(IoError::Device); - } - if self.refused_source - && !self.refused_rollback - && buf.len() == 32 - && buf[0] != 0xE5 - && self.rollback_cluster == Some(cluster) - { - self.refused_rollback = true; - return Err(IoError::Device); + async fn write(&self, lba: u64, data: &[Block]) -> Result<(), DevError> { + within(lba, data.len(), self.blocks())?; + let (at, data) = (lba as usize * BLOCK, data.as_flattened()); + if self.refuses(at, data) { + return Err(DevError::Device); } - let (start, end) = self.check(offset, buf.len())?; - self.bytes[start..end].copy_from_slice(buf); + self.bytes.borrow_mut()[at..at + data.len()].copy_from_slice(data); Ok(()) } - fn flush(&mut self) -> Result<(), IoError> { + async fn flush(&self) -> Result<(), DevError> { Ok(()) } } fn read_all(fs: &mut Fat32, path: &str) -> Vec { - let mut file = fs.open(path).expect("open file"); + let mut file = now(fs.open(path)).expect("open file"); let mut bytes = vec![0; file.len() as usize]; - fs.read(&mut file, 0, &mut bytes).expect("read file"); + now(fs.read(&mut file, 0, &mut bytes)).expect("read file"); bytes } @@ -108,17 +136,9 @@ fn source_move_failure_restores_destination_before_any_free() { let destination = b"destination survives"; let (volume, source_cluster, _, destination_entry) = staged(source, destination); - let dev = RefuseSourceAtDestination { - bytes: volume.bytes, - destination_entry, - source_cluster, - rollback_cluster: None, - refused_source: false, - refused_rollback: false, - }; - let mut fs = Fat32::mount(dev).expect("mount"); - let failed = fs - .replace_rename("source.txt", "destination.txt") + let dev = RefuseSourceAtDestination::new(volume.bytes, destination_entry, source_cluster, None); + let mut fs = now(Fat32::mount(dev)).expect("mount"); + let failed = now(fs.replace_rename("source.txt", "destination.txt")) .expect_err("the refused source move must not report success"); assert_eq!(failed.cause, Error::Io); assert_eq!(failed.stranded, None, "the rollback succeeded, so nothing is stranded"); @@ -126,8 +146,8 @@ fn source_move_failure_restores_destination_before_any_free() { assert_eq!(read_all(&mut fs, "destination.txt"), destination); let dev = fs.into_device(); - assert!(dev.refused_source, "the device error did not hit the source move"); - let complaints = toyos_fat32_check::check(&dev.bytes); + assert!(dev.refused_source.get(), "the device error did not hit the source move"); + let complaints = toyos_fat32_check::check(&dev.volume()); assert!( complaints.is_empty(), "{}", @@ -145,30 +165,22 @@ fn a_failed_rollback_names_where_the_destination_went() { let (volume, source_cluster, destination_cluster, destination_entry) = staged(source, destination); - let dev = RefuseSourceAtDestination { - bytes: volume.bytes, - destination_entry, - source_cluster, - rollback_cluster: Some(destination_cluster), - refused_source: false, - refused_rollback: false, - }; - let mut fs = Fat32::mount(dev).expect("mount"); - let failed = fs - .replace_rename("source.txt", "destination.txt") + let dev = RefuseSourceAtDestination::new(volume.bytes, destination_entry, source_cluster, Some(destination_cluster)); + let mut fs = now(Fat32::mount(dev)).expect("mount"); + let failed = now(fs.replace_rename("source.txt", "destination.txt")) .expect_err("both refusals must reach the caller"); assert_eq!(failed.cause, Error::Io); let stranded = failed.stranded.expect("the failed rollback must name the staging file"); assert_eq!(stranded, ".toyos-replaced-00000000.tmp"); // What the report claims, checked against the volume. - assert_eq!(fs.exists("destination.txt"), Ok(false)); + assert_eq!(now(fs.exists("destination.txt")), Ok(false)); assert_eq!(read_all(&mut fs, "source.txt"), source); assert_eq!(read_all(&mut fs, &stranded), destination); let dev = fs.into_device(); - assert!(dev.refused_source && dev.refused_rollback, "both refusals must have fired"); - let complaints = toyos_fat32_check::check(&dev.bytes); + assert!(dev.refused_source.get() && dev.refused_rollback.get(), "both refusals must have fired"); + let complaints = toyos_fat32_check::check(&dev.volume()); assert!( complaints.is_empty(), "{}", diff --git a/lib/update/src/slots.rs b/lib/update/src/slots.rs index 07a45754bbb..195c8735591 100644 --- a/lib/update/src/slots.rs +++ b/lib/update/src/slots.rs @@ -185,33 +185,6 @@ pub fn current(copies: [&[u8; BLOCK]; 2]) -> Result<(Table, usize), Unreadable> } } -/// [`current`] of a slot table's partition whose first [`COPIES`] blocks -/// `fill` reads, whatever reaches the partition. -pub fn read( - fill: impl FnOnce(&mut [[u8; BLOCK]; COPIES as usize]) -> Result<(), E>, -) -> Result<(Table, usize), NotRead> { - let mut copies = [[0; BLOCK]; COPIES as usize]; - fill(&mut copies).map_err(NotRead::Read)?; - current([&copies[0], &copies[1]]).map_err(NotRead::Unreadable) -} - -/// Why [`read`] has no table. -#[derive(Debug)] -pub enum NotRead { - /// The blocks would not read, in the reader's word. - Read(E), - Unreadable(Unreadable), -} - -impl core::fmt::Display for NotRead { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::Read(why) => write!(f, "the slot table would not read: {why:?}"), - Self::Unreadable(why) => write!(f, "the slot table's partition holds {why}"), - } - } -} - /// What a writer puts where, to make `next` the table: the copy that is not /// `current`'s, one sequence past it. pub fn next_write(current: (Table, usize), mut next: Table) -> (usize, [u8; BLOCK]) { diff --git a/system/diskserver/Cargo.toml b/system/diskserver/Cargo.toml index 4b5ea2034c8..1bcd64f4b7e 100644 --- a/system/diskserver/Cargo.toml +++ b/system/diskserver/Cargo.toml @@ -21,8 +21,8 @@ toyos = { path = "../../sdk/toyos" } toyos-blockring = { path = "../../lib/blockring" } # The session region, which a client maps as its driver does. toyos-blockserve = { path = "../../lib/blockserve" } -# The trait every partition is read and written through. -toyos-disk = { path = "../../lib/disk" } +# The device a served partition is. +toyos-blockdev = { path = "../../lib/blockdev" } [package.metadata.toyos.host] exempt.owns = "the block service between ToyOS disk drivers and their clients" diff --git a/system/diskserver/src/disk.rs b/system/diskserver/src/disk.rs index faf2b732348..85a2f34063f 100644 --- a/system/diskserver/src/disk.rs +++ b/system/diskserver/src/disk.rs @@ -1,106 +1,239 @@ -//! A partition as a program that reads and writes its blocks holds it: one -//! the block service serves ([`Served`]). +//! A partition the block service serves, as a [`BlockDev`]: [`Served`]. //! -//! **A block service that ends is survived, not hidden**: a [`Served`] disk -//! whose session ended reconnects through the same name and issues again every -//! write no flush had covered (`toyos_blockring::client`), then asks what it -//! was asking. What was on the wire when the session ended is asked again -//! because a block write names its whole destination and means the same thing -//! twice. - -use crate::{Error as SessionError, Outcome, Session}; +//! **Answers arrive when its owner reads them**: a request is put on the +//! session when it is asked, and its answer reaches the task awaiting it at +//! [`Served::progress`], which the owner calls when the session's connection +//! is readable or, as a [`toyos_blockdev::block_on`]'s `wait`, through +//! [`Served::wait`]. +//! +//! **A block service that ends is survived, not hidden**: the end is found at +//! [`Served::progress`], which reconnects through the same name; the session +//! issues again every write no flush had covered (`toyos_blockring::client`), +//! and a request that was on the wire is asked again, because a block request +//! names its whole destination and means the same thing twice. A request +//! refused across more than [`MAX_RECONNECTS`] ends is answered +//! [`Error::Gone`], and so is every request once a reconnect fails: what was +//! waiting for the next session would otherwise go out under requests asked +//! since. + +use std::cell::RefCell; +use std::collections::{BTreeMap, BTreeSet}; +use std::future::poll_fn; +use std::task::{Poll, Waker}; + +use toyos_blockdev::{within, Block, BlockDev, Error, BLOCK}; use toyos_blockring::MAX_REQUEST_BLOCKS; -pub use toyos_disk::{span, Disk, DiskError, BLOCK}; +use crate::session::{Answer, Error as SessionError, Session, Unsent, Waited}; +use crate::{Outcome, Ticket}; + +/// What [`Served`] drives: a session's requests and their answers, as +/// [`Session`] keeps them, whose contract this is. A flush takes no arena. +pub trait Link { + fn blocks(&self) -> u64; + fn submit_read(&mut self, lba: u64, blocks: u32) -> Result; + fn submit_write(&mut self, lba: u64, data: &[u8]) -> Result; + fn submit_flush(&mut self) -> Result; + fn wait(&mut self, block: bool) -> Waited; + fn reconnect(&mut self) -> Result<(), SessionError>; +} + +impl Link for Session { + fn blocks(&self) -> u64 { + Session::blocks(self) + } + fn submit_read(&mut self, lba: u64, blocks: u32) -> Result { + Session::submit_read(self, lba, blocks) + } + fn submit_write(&mut self, lba: u64, data: &[u8]) -> Result { + Session::submit_write(self, lba, data) + } + fn submit_flush(&mut self) -> Result { + Session::submit_flush(self) + } + fn wait(&mut self, block: bool) -> Waited { + Session::wait(self, block) + } + fn reconnect(&mut self) -> Result<(), SessionError> { + Session::reconnect(self) + } +} + +/// How many times one request is asked again across block service restarts +/// before the disk is called gone: the supervisor gives up on a service that +/// ends faster than this. +pub const MAX_RECONNECTS: u32 = 4; /// A partition the block service serves. -pub struct Served { - session: Session, +pub struct Served { + link: RefCell, blocks: u64, + state: RefCell, } -/// How many times one request is asked again across block service restarts -/// before the disk is called gone: the supervisor gives up on a service that ends -/// faster than this. -const MAX_RECONNECTS: u32 = 4; +#[derive(Default)] +struct State { + /// Asked, and not yet taken by the task that asked. + asked: BTreeSet, + answered: BTreeMap, + waiting: BTreeMap, + /// A reconnect failed: nothing is asked again. + gone: bool, +} -impl Served { - pub fn new(session: Session) -> Self { - let blocks = session.blocks(); - Self { session, blocks } +/// Why a request was not answered. +enum NotAnswered { + /// The session's arena has no room: a flush releases what acknowledged + /// writes hold. + Full, + Gone, +} + +impl Served { + pub fn new(link: L) -> Self { + let blocks = link.blocks(); + Self { link: RefCell::new(link), blocks, state: RefCell::default() } } - /// Ask, and across a session's end, reconnect and ask again. - fn asked(&mut self, mut ask: impl FnMut(&mut Session) -> Result) -> Result { - let mut reconnects = 0; + /// Whether a request is waiting for its answer. + pub fn pending(&self) -> bool { + !self.state.borrow().asked.is_empty() + } + + /// Every answer the session has, each to the task awaiting it; with + /// `block`, once there is one or the session has ended. A session that + /// ended is reconnected here. + pub fn progress(&self, block: bool) { + let Waited { answers, ended } = self.link.borrow_mut().wait(block); + let mut woken = Vec::new(); + { + let mut state = self.state.borrow_mut(); + for answer in answers { + woken.extend(state.waiting.remove(&answer.ticket)); + state.answered.insert(answer.ticket, answer); + } + } + if ended && !self.state.borrow().gone { + println!("the block service ended; reconnecting"); + if let Err(why) = self.link.borrow_mut().reconnect() { + println!("the block service would not take the session back ({why:?}); the disk is gone"); + let mut state = self.state.borrow_mut(); + state.gone = true; + woken.extend(std::mem::take(&mut state.waiting).into_values()); + } + } + woken.into_iter().for_each(Waker::wake); + } + + /// [`Served::progress`], blocking: the `wait` of a + /// [`toyos_blockdev::block_on`] over this disk alone. + pub fn wait(&self) { + assert!(self.pending(), "diskserver: a wait on a disk with nothing asked of it would never end"); + self.progress(true); + } + + /// One request, asked again after each session end it was on the wire + /// for. + async fn ask(&self, mut submit: impl FnMut(&mut L) -> Result) -> Result { + let mut refusals = 0; + loop { + if self.state.borrow().gone { + return Err(NotAnswered::Gone); + } + let ticket = match submit(&mut self.link.borrow_mut()) { + Ok(ticket) => ticket, + Err(Unsent::ArenaFull) => return Err(NotAnswered::Full), + // `progress` reconnects as it finds an end, so a session + // still ended is one that would not reconnect. + Err(Unsent::Ended) => return Err(NotAnswered::Gone), + }; + self.state.borrow_mut().asked.insert(ticket); + let answer = self.answer(ticket).await?; + if answer.outcome != Outcome::Refused { + return Ok(answer); + } + refusals += 1; + if refusals > MAX_RECONNECTS { + println!("a block request was refused across {refusals} session ends; the disk is gone"); + return Err(NotAnswered::Gone); + } + } + } + + async fn answer(&self, ticket: Ticket) -> Result { + poll_fn(|cx| { + let mut state = self.state.borrow_mut(); + if let Some(answer) = state.answered.remove(&ticket) { + state.asked.remove(&ticket); + return Poll::Ready(Ok(answer)); + } + if state.gone { + state.asked.remove(&ticket); + return Poll::Ready(Err(NotAnswered::Gone)); + } + state.waiting.insert(ticket, cx.waker().clone()); + Poll::Pending + }) + .await + } + + /// [`Served::ask`], flushing first wherever the arena has no room. + async fn ask_room(&self, mut submit: impl FnMut(&mut L) -> Result) -> Result { loop { - match ask(&mut self.session) { + match self.ask(&mut submit).await { Ok(answer) => return Ok(answer), - Err(SessionError::Ended) if reconnects < MAX_RECONNECTS => { - reconnects += 1; - println!("the block service ended; reconnecting ({reconnects} of {MAX_RECONNECTS})"); - if let Err(why) = self.session.reconnect() { - println!("the block service would not take the session back: {why:?}"); - return Err(DiskError::Gone); - } - } - Err(_) => return Err(DiskError::Gone), + Err(NotAnswered::Full) => self.flush().await?, + Err(NotAnswered::Gone) => return Err(Error::Gone), } } } } -impl Disk for Served { +impl BlockDev for Served { fn blocks(&self) -> u64 { self.blocks } - fn read(&mut self, first: u64, out: &mut [u8]) -> Result<(), DiskError> { - span(first, out.len(), self.blocks)?; + async fn read(&self, lba: u64, out: &mut [Block]) -> Result<(), Error> { + within(lba, out.len(), self.blocks)?; let per = MAX_REQUEST_BLOCKS as usize; - for (i, chunk) in out.chunks_mut(BLOCK * per).enumerate() { - let n = (chunk.len() / BLOCK) as u32; - let at = first + (i * per) as u64; - let (outcome, data) = self.asked(|s| s.read(at, n))?; - match (outcome, data) { + for (i, chunk) in out.chunks_mut(per).enumerate() { + let (at, n) = (lba + (i * per) as u64, chunk.len() as u32); + let answer = self.ask_room(|link| link.submit_read(at, n)).await?; + let chunk = chunk.as_flattened_mut(); + match (answer.outcome, answer.data) { (Outcome::Done, Some(data)) if data.len() == chunk.len() => chunk.copy_from_slice(&data), - (Outcome::Refused, _) => return Err(DiskError::Gone), - _ => return Err(DiskError::Device), + _ => return Err(Error::Device), } } Ok(()) } - fn write(&mut self, first: u64, data: &[u8]) -> Result<(), DiskError> { - span(first, data.len(), self.blocks)?; + async fn write(&self, lba: u64, data: &[Block]) -> Result<(), Error> { + within(lba, data.len(), self.blocks)?; let per = MAX_REQUEST_BLOCKS as usize; - for (i, chunk) in data.chunks(BLOCK * per).enumerate() { - let at = first + (i * per) as u64; - let mut outcome = self.asked(|s| s.write(at, chunk))?; - // On the wire when the session ended: the write names its whole - // destination, so it is written again rather than guessed at. - if outcome == Outcome::Refused { - outcome = self.asked(|s| s.write(at, chunk))?; - } - match outcome { + for (i, chunk) in data.chunks(per).enumerate() { + let at = lba + (i * per) as u64; + match self.ask_room(|link| link.submit_write(at, chunk.as_flattened())).await?.outcome { Outcome::Done => {} - Outcome::ReadOnly => return Err(DiskError::ReadOnly), - _ => return Err(DiskError::Device), + Outcome::ReadOnly => return Err(Error::ReadOnly), + _ => return Err(Error::Device), } } Ok(()) } - fn flush(&mut self) -> Result<(), DiskError> { - for _ in 0..=MAX_RECONNECTS { - match self.asked(Session::flush)? { - Outcome::Durable => return Ok(()), - // The session ended under it: the client issues its covered - // writes again first on the new one, then the flush is asked. - Outcome::Refused => continue, - _ => return Err(DiskError::Device), - } + async fn flush(&self) -> Result<(), Error> { + match self.ask(Link::submit_flush).await { + Ok(Answer { outcome: Outcome::Durable, .. }) => Ok(()), + Ok(_) => Err(Error::Device), + Err(NotAnswered::Gone) => Err(Error::Gone), + Err(NotAnswered::Full) => unreachable!("diskserver: a flush took arena room"), } - Err(DiskError::Gone) } } + +const _: () = assert!(BLOCK == toyos_blockring::BLOCK_BYTES); + +#[cfg(test)] +mod tests; diff --git a/system/diskserver/src/disk/tests.rs b/system/diskserver/src/disk/tests.rs new file mode 100644 index 00000000000..fa70af1b8ab --- /dev/null +++ b/system/diskserver/src/disk/tests.rs @@ -0,0 +1,253 @@ +//! [`Served`] over a session these tests script: what it asks again across a +//! session's end, when it calls the disk gone, and that each answer reaches +//! the task that asked for it. + +use std::cell::Cell; +use std::collections::VecDeque; +use std::rc::Rc; + +use toyos_blockdev::{block_on, Block, BlockDev, Error, Executor, BLOCK}; + +use super::{Link, Served, MAX_RECONNECTS}; +use crate::session::{Answer, Error as SessionError, Unsent, Waited}; +use crate::{Outcome, Ticket}; + +#[derive(Clone, Debug, PartialEq, Eq)] +enum Asked { + Read { lba: u64, blocks: u32 }, + Write { lba: u64, data: Vec }, + Flush, +} + +/// A session over a partition in memory. Everything asked is on the wire at +/// once and answered at the next wait, the latest first, unless the session +/// is scripted to end there. +struct Script { + disk: Vec, + next: Ticket, + wire: Vec<(Ticket, Asked)>, + /// Every request, in the order it was asked. + log: Vec, + /// At each of these waits, counted from 0, the session ends with what is + /// on the wire. + ends: VecDeque, + waits: u32, + up: bool, + /// Each reconnect's answer, in turn; `Ok` once they run out. + reconnects: VecDeque>, + reconnected: u32, + /// Writes the next this many write submissions refuse for want of room. + full: u32, + read_only: bool, +} + +impl Script { + fn new(blocks: usize) -> Self { + Self { + disk: vec![0; blocks * BLOCK], + next: 0, + wire: Vec::new(), + log: Vec::new(), + ends: VecDeque::new(), + waits: 0, + up: true, + reconnects: VecDeque::new(), + reconnected: 0, + full: 0, + read_only: false, + } + } + + fn ask(&mut self, asked: Asked) -> Result { + if !self.up { + return Err(Unsent::Ended); + } + let ticket = self.next; + self.next += 1; + self.log.push(asked.clone()); + self.wire.push((ticket, asked)); + Ok(ticket) + } + + fn answer(&mut self, ticket: Ticket, asked: Asked) -> Answer { + let (outcome, data) = match asked { + Asked::Read { lba, blocks } => { + let at = lba as usize * BLOCK; + (Outcome::Done, Some(self.disk[at..at + blocks as usize * BLOCK].to_vec())) + } + Asked::Write { .. } if self.read_only => (Outcome::ReadOnly, None), + Asked::Write { lba, data } => { + let at = lba as usize * BLOCK; + self.disk[at..at + data.len()].copy_from_slice(&data); + (Outcome::Done, None) + } + Asked::Flush => (Outcome::Durable, None), + }; + Answer { ticket, outcome, data } + } +} + +impl Link for Script { + fn blocks(&self) -> u64 { + (self.disk.len() / BLOCK) as u64 + } + + fn submit_read(&mut self, lba: u64, blocks: u32) -> Result { + self.ask(Asked::Read { lba, blocks }) + } + + fn submit_write(&mut self, lba: u64, data: &[u8]) -> Result { + if self.full > 0 && self.up { + self.full -= 1; + return Err(Unsent::ArenaFull); + } + self.ask(Asked::Write { lba, data: data.to_vec() }) + } + + fn submit_flush(&mut self) -> Result { + self.ask(Asked::Flush) + } + + fn wait(&mut self, _block: bool) -> Waited { + if !self.up { + return Waited { answers: Vec::new(), ended: true }; + } + let wait = self.waits; + self.waits += 1; + let wire = std::mem::take(&mut self.wire); + if self.ends.front() == Some(&wait) { + self.ends.pop_front(); + self.up = false; + let answers = wire.into_iter().map(|(ticket, _)| Answer { ticket, outcome: Outcome::Refused, data: None }); + return Waited { answers: answers.collect(), ended: true }; + } + Waited { answers: wire.into_iter().rev().map(|(t, a)| self.answer(t, a)).collect(), ended: false } + } + + fn reconnect(&mut self) -> Result<(), SessionError> { + assert!(!self.up, "a reconnect while the session is up"); + self.reconnected += 1; + let answer = self.reconnects.pop_front().unwrap_or(Ok(())); + self.up = answer.is_ok(); + answer + } +} + +fn now(disk: &Served