use fs_core::{
BlockDevice, BlockRead, CachingDevice, CallbackDevice, CountingDevice, FileDevice,
OwnedRwSlice, OwnedSlice, ReadOnlyDevice, SliceReader,
};
use std::fs::File;
use std::io::Write;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::sync::Arc;
mod common;
fn tmp_image(bytes: &[u8]) -> String {
static C: AtomicU32 = AtomicU32::new(0);
let n = C.fetch_add(1, Ordering::Relaxed);
let p = std::env::temp_dir()
.join(format!(
"fs_core_sizecontract_{}_{n}.img",
std::process::id()
))
.to_string_lossy()
.into_owned();
File::create(&p).unwrap().write_all(bytes).unwrap();
p
}
struct Movable {
data: Vec<u8>,
reported: AtomicU64,
}
impl Movable {
fn new(len: usize) -> Self {
Movable {
data: vec![7u8; len],
reported: AtomicU64::new(len as u64),
}
}
fn report(&self, n: u64) {
self.reported.store(n, Ordering::SeqCst);
}
}
impl BlockRead for Movable {
fn read_at(&self, offset: u64, buf: &mut [u8]) -> fs_core::Result<()> {
common::read_into(&self.data, offset, buf)
}
fn size_bytes(&self) -> u64 {
self.reported.load(Ordering::SeqCst)
}
}
impl BlockDevice for Movable {
fn is_writable(&self) -> bool {
true
}
}
fn exercise(dev: &dyn BlockDevice) {
let mut buf = [0u8; 8];
let _ = dev.read_at(0, &mut buf);
let _ = dev.read_at(u64::MAX - 4, &mut buf);
let _ = dev.read_at(dev.size_bytes().saturating_sub(2), &mut buf);
let _ = dev.write_at(0, &[1u8; 8]);
let _ = dev.write_at(dev.size_bytes().saturating_sub(2), &[1u8; 8]);
let _ = dev.flush();
let _ = dev.is_writable();
}
fn assert_stable(name: &str, dev: &dyn BlockDevice) {
let before = dev.size_bytes();
assert!(
before > 0,
"{name}: precondition -- a zero-sized device cannot show a size moving"
);
exercise(dev);
assert_eq!(
dev.size_bytes(),
before,
"{name} moved its reported size across a round of trait operations. \
Callers read it once, act on it, and read it again expecting the same \
answer -- CachingDevice bounds a read against it and then clamps each \
block it fetches against it."
);
}
#[test]
fn the_devices_that_own_their_size_report_a_stable_one() {
let path = tmp_image(&vec![3u8; 4096]);
let file = FileDevice::open_rw(&path).unwrap();
assert_stable("FileDevice", &file);
{
use std::io::Write as _;
let mut grow = std::fs::OpenOptions::new()
.append(true)
.open(&path)
.expect("open a second handle on the backing file");
grow.write_all(&[9u8; 6]).expect("grow the backing file");
grow.flush().expect("flush the growth");
}
assert_eq!(
std::fs::metadata(&path)
.expect("stat the backing file")
.len(),
4102,
"precondition -- the backing file must really have grown, or the assertion \
below says nothing about the device in front of it"
);
assert_eq!(
file.size_bytes(),
4096,
"FileDevice took its size at construction and must keep reporting that. A \
live metadata().len() would follow the file here."
);
let _ = std::fs::remove_file(&path);
let rw_backing = Arc::new(Movable::new(4096));
let parent: Arc<dyn BlockDevice> = rw_backing.clone();
let rw_slice = OwnedRwSlice::new(parent, 512, 1024);
assert_stable("OwnedRwSlice", &rw_slice);
assert_survives_the_backing_moving("OwnedRwSlice", &rw_slice, &rw_backing);
let ro_backing = Arc::new(Movable::new(4096));
let ro: Arc<dyn BlockRead> = ro_backing.clone();
let owned = OwnedSlice::new(ro, 512, 1024);
assert_stable("OwnedSlice", &ro_as_device(&owned));
assert_survives_the_backing_moving("OwnedSlice", &owned, &ro_backing);
let backing = Movable::new(4096);
let slice = SliceReader::new(&backing, 512, 1024);
assert_stable("SliceReader", &ro_as_device(&slice));
assert_survives_the_backing_moving("SliceReader", &slice, &backing);
}
fn assert_survives_the_backing_moving(name: &str, dev: &dyn BlockRead, backing: &Movable) {
let before = dev.size_bytes();
assert!(
before > 0,
"{name}: precondition -- a zero-length device cannot show a length moving"
);
backing.report(before / 2);
assert_ne!(
backing.size_bytes(),
before,
"{name}: precondition -- the BACKING must actually report something different, \
or this asserts nothing about the device in front of it"
);
assert_eq!(
dev.size_bytes(),
before,
"{name} followed its parent's new size. It took its length at construction, so \
it must keep reporting that -- a length recomputed from the parent on every \
call is the bug this file exists to catch, and it passed every other \
assertion here."
);
}
fn ro_as_device<T: BlockRead>(inner: &T) -> ReadOnlyDevice<&T> {
ReadOnlyDevice::new(inner)
}
#[test]
fn a_wrapper_forwards_its_inner_devices_size_rather_than_snapshotting_it() {
let inner = Arc::new(Movable::new(4096));
let counting = CountingDevice::new(inner.clone() as Arc<dyn BlockRead>);
let readonly = ReadOnlyDevice::new(inner.clone() as Arc<dyn BlockRead>);
let caching = CachingDevice::new(inner.clone() as Arc<dyn BlockDevice>, 512, 4);
let arced: Arc<dyn BlockRead> = inner.clone();
let boxed: Box<dyn BlockRead> = Box::new(inner.clone());
let by_ref: &Movable = &inner;
assert_eq!(counting.size_bytes(), 4096, "control: before the move");
assert_eq!(readonly.size_bytes(), 4096, "control: before the move");
assert_eq!(caching.size_bytes(), 4096, "control: before the move");
assert_eq!(arced.size_bytes(), 4096, "control: before the move");
assert_eq!(boxed.size_bytes(), 4096, "control: before the move");
assert_eq!(
BlockRead::size_bytes(&by_ref),
4096,
"control: before the move"
);
inner.report(2048);
for (name, got) in [
("CountingDevice", counting.size_bytes()),
("ReadOnlyDevice", readonly.size_bytes()),
("CachingDevice", caching.size_bytes()),
("Arc<dyn BlockRead>", arced.size_bytes()),
("Box<dyn BlockRead>", boxed.size_bytes()),
("&T", BlockRead::size_bytes(&by_ref)),
] {
assert_eq!(
got, 2048,
"{name} must report the device's CURRENT size, not one snapshotted at \
construction. A wrapper cannot hold a moving device still, and pretending \
to would hide the violation instead of letting the cache catch it."
);
}
}
#[test]
fn callback_devices_size_is_a_field_the_caller_owns() {
let mut dev = CallbackDevice {
size: 12345,
read: Box::new(|_, _| Ok(())),
write: None,
flush: None,
};
assert_eq!(dev.size_bytes(), 12345, "control: as constructed");
dev.size = 99;
assert_eq!(
dev.size_bytes(),
99,
"the field is public and size_bytes reads it, so a caller can move the \
reported size after construction. Nothing in the type prevents that, and \
the trait's documentation must not claim otherwise."
);
}