#![cfg(all(feature = "cache", feature = "write", feature = "std"))]
use std::io::Cursor;
use hadris_fat::format::{FatFormatOptions, FatTypeSelection, FatVolumeFormatter};
use hadris_fat::{Error, FatVolume, FatVolumeReadExt};
const FAT32_SIZE: usize = 40 * 1024 * 1024;
struct FatLayout {
fat_start: usize,
fat_size: usize,
fat_count: usize,
sector_size: usize,
}
impl FatLayout {
fn new(opts: &FatFormatOptions) -> Self {
let params = FatVolumeFormatter::calculate_params(opts).expect("calc params");
let sector_size = params.sector_size;
Self {
fat_start: params.reserved_sectors as usize * sector_size,
fat_size: params.sectors_per_fat as usize * sector_size,
fat_count: params.fat_count as usize,
sector_size,
}
}
fn fat32_entry_offset(&self, copy: usize, cluster: u32) -> usize {
self.fat_start + copy * self.fat_size + cluster as usize * 4
}
fn fat32_sector_of(&self, cluster: u32) -> usize {
(cluster as usize * 4) / self.sector_size
}
}
fn format_into_buffer(buffer: &mut [u8], opts: &FatFormatOptions) -> FatLayout {
let layout = FatLayout::new(opts);
{
let cursor = Cursor::new(&mut buffer[..]);
let _fs = FatVolumeFormatter::format(cursor, opts.clone()).expect("format");
}
layout
}
fn patch_fat32_entry_all_copies(buffer: &mut [u8], layout: &FatLayout, cluster: u32, value: u32) {
let bytes = value.to_le_bytes();
for copy in 0..layout.fat_count {
let off = layout.fat32_entry_offset(copy, cluster);
buffer[off..off + 4].copy_from_slice(&bytes);
}
}
fn read_fat32_entry_raw(buffer: &[u8], layout: &FatLayout, cluster: u32) -> u32 {
let off = layout.fat32_entry_offset(0, cluster);
u32::from_le_bytes(buffer[off..off + 4].try_into().unwrap())
}
fn fat32_options() -> FatFormatOptions {
FatFormatOptions::new(FAT32_SIZE as u64).fat_type(FatTypeSelection::Fat32)
}
#[test]
fn cache_round_trips_fat32_chain() {
let mut bytes = vec![0u8; FAT32_SIZE];
let opts = fat32_options();
let layout = format_into_buffer(&mut bytes, &opts);
patch_fat32_entry_all_copies(&mut bytes, &layout, 5, 6);
patch_fat32_entry_all_copies(&mut bytes, &layout, 6, 7);
patch_fat32_entry_all_copies(&mut bytes, &layout, 7, 0x0FFF_FFFF);
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::builder(cursor)
.fat_cache(8)
.open()
.expect("open");
let chain = fs
.with_cached_fat(|cached, disk| cached.read_chain(disk, 5))
.expect("cache installed")
.expect("read_chain ok");
assert_eq!(chain, vec![5, 6, 7]);
}
#[test]
fn cache_writes_persist_across_remount_after_flush() {
let mut bytes = vec![0u8; FAT32_SIZE];
let opts = fat32_options();
let layout = format_into_buffer(&mut bytes, &opts);
{
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::builder(cursor)
.fat_cache(8)
.open()
.expect("open");
fs.with_fat_cache_locked(|cache, disk| {
cache
.write_fat32_entry(disk, 100, 0x0BEE_F123)
.expect("write_fat32_entry");
})
.expect("with_fat_cache_locked");
fs.flush().expect("flush");
}
let observed = read_fat32_entry_raw(&bytes, &layout, 100);
assert_eq!(
observed & 0x0FFF_FFFF,
0x0BEE_F123,
"post-flush FAT[100] must persist what we wrote through the cache"
);
let _ = FatVolume::open(Cursor::new(&bytes[..])).expect("re-open after flush");
}
#[test]
fn cache_fat32_writes_preserve_reserved_high_bits() {
let mut bytes = vec![0u8; FAT32_SIZE];
let opts = fat32_options();
let layout = format_into_buffer(&mut bytes, &opts);
patch_fat32_entry_all_copies(&mut bytes, &layout, 100, 0xA000_0000);
{
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::builder(cursor)
.fat_cache(8)
.open()
.expect("open");
fs.with_fat_cache_locked(|cache, disk| {
cache
.write_fat32_entry(disk, 100, 0x0FFF_FFF7)
.expect("write_fat32_entry");
})
.expect("with_fat_cache_locked");
fs.flush().expect("flush");
}
for copy in 0..layout.fat_count {
let offset = layout.fat32_entry_offset(copy, 100);
assert_eq!(
u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap()),
0xAFFF_FFF7
);
}
}
#[test]
fn cache_dirty_eviction_does_not_lose_data() {
let mut bytes = vec![0u8; FAT32_SIZE];
let opts = fat32_options();
let layout = format_into_buffer(&mut bytes, &opts);
let writes: &[(u32, u32)] = &[
(10, 0x0AAA_AAAA), (200, 0x0BBB_BBBB), (400, 0x0CCC_CCCC), ];
let s0 = layout.fat32_sector_of(writes[0].0);
let s1 = layout.fat32_sector_of(writes[1].0);
let s2 = layout.fat32_sector_of(writes[2].0);
assert_ne!(s0, s1);
assert_ne!(s1, s2);
assert_ne!(s0, s2);
{
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::builder(cursor)
.fat_cache(2) .open()
.expect("open");
fs.with_fat_cache_locked(|cache, disk| {
for &(cluster, value) in writes {
cache
.write_fat32_entry(disk, cluster as usize, value)
.expect("write_fat32_entry");
}
assert!(cache.stats().evictions >= 1);
assert!(cache.stats().dirty_writes >= 1);
})
.expect("with_fat_cache_locked");
fs.flush().expect("flush");
}
for &(cluster, value) in writes {
let observed = read_fat32_entry_raw(&bytes, &layout, cluster);
assert_eq!(
observed & 0x0FFF_FFFF,
value & 0x0FFF_FFFF,
"FAT[{cluster}] must persist value 0x{value:08x}"
);
}
}
#[test]
fn cache_dirty_eviction_writes_to_all_fat_copies() {
let mut bytes = vec![0u8; FAT32_SIZE];
let opts = fat32_options();
let layout = format_into_buffer(&mut bytes, &opts);
assert!(
layout.fat_count >= 2,
"this test assumes the formatter writes >= 2 FAT copies"
);
let cluster = 50u32;
let value = 0x0DEA_DBEE_u32 & 0x0FFF_FFFF;
{
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::builder(cursor)
.fat_cache(1) .open()
.expect("open");
fs.with_fat_cache_locked(|cache, disk| {
cache
.write_fat32_entry(disk, cluster as usize, value)
.expect("write");
cache
.write_fat32_entry(disk, 200, 0)
.expect("write triggers eviction");
})
.expect("with_fat_cache_locked");
fs.flush().expect("flush");
}
for copy in 0..layout.fat_count {
let off = layout.fat32_entry_offset(copy, cluster);
let observed = u32::from_le_bytes(bytes[off..off + 4].try_into().unwrap());
assert_eq!(
observed & 0x0FFF_FFFF,
value,
"FAT copy {copy} at cluster {cluster} must reflect cached write"
);
}
}
#[test]
fn cache_read_returns_cache_dirty_eviction_when_all_dirty() {
let mut bytes = vec![0u8; FAT32_SIZE];
let opts = fat32_options();
let _layout = format_into_buffer(&mut bytes, &opts);
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::builder(cursor)
.fat_cache(2)
.open()
.expect("open");
fs.with_fat_cache_locked(|cache, disk| {
cache.write_fat32_entry(disk, 10, 0x0AAA_AAAA).unwrap();
cache.write_fat32_entry(disk, 200, 0x0BBB_BBBB).unwrap();
let err = cache.read_fat32_entry(disk, 400).unwrap_err();
match err {
Error::CacheDirtyEviction { .. } => {}
other => panic!("expected CacheDirtyEviction, got {other:?}"),
}
})
.expect("with_fat_cache_locked");
fs.flush().expect("flush");
fs.with_fat_cache_locked(|cache, disk| {
let _val = cache.read_fat32_entry(disk, 400).expect("read after flush");
})
.expect("with_fat_cache_locked");
}
#[test]
fn cached_fat_read_chain_returns_cluster_loop_on_cycle() {
let mut bytes = vec![0u8; FAT32_SIZE];
let opts = fat32_options();
let layout = format_into_buffer(&mut bytes, &opts);
patch_fat32_entry_all_copies(&mut bytes, &layout, 3, 4);
patch_fat32_entry_all_copies(&mut bytes, &layout, 4, 3);
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::builder(cursor)
.fat_cache(4)
.open()
.expect("open");
let result = fs
.with_cached_fat(|cached, disk| cached.read_chain(disk, 3))
.expect("cache installed");
match result {
Err(Error::ClusterLoop { .. }) => {}
Err(other) => panic!("expected ClusterLoop, got {other:?}"),
Ok(chain) => panic!("expected ClusterLoop, got chain {chain:?}"),
}
}
#[test]
fn cached_fat_next_cluster_on_bad_cluster_marker() {
let mut bytes = vec![0u8; FAT32_SIZE];
let opts = fat32_options();
let layout = format_into_buffer(&mut bytes, &opts);
patch_fat32_entry_all_copies(&mut bytes, &layout, 5, 0x0FFF_FFF7);
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::builder(cursor)
.fat_cache(4)
.open()
.expect("open");
let result = fs
.with_cached_fat(|cached, disk| cached.next_cluster(disk, 5))
.expect("cache installed");
match result {
Err(Error::BadCluster { cluster }) => assert_eq!(cluster, 5),
other => panic!("expected BadCluster, got {other:?}"),
}
}
#[test]
fn cached_fat_next_cluster_out_of_bounds() {
let mut bytes = vec![0u8; FAT32_SIZE];
let opts = fat32_options();
let layout = format_into_buffer(&mut bytes, &opts);
patch_fat32_entry_all_copies(&mut bytes, &layout, 5, 0x0FFF_0000);
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::builder(cursor)
.fat_cache(4)
.open()
.expect("open");
let result = fs
.with_cached_fat(|cached, disk| cached.next_cluster(disk, 5))
.expect("cache installed");
match result {
Err(Error::ClusterOutOfBounds { .. }) => {}
other => panic!("expected ClusterOutOfBounds, got {other:?}"),
}
}
#[test]
fn read_status_flags_consults_cache() {
let mut bytes = vec![0u8; FAT32_SIZE];
let opts = fat32_options();
let _layout = format_into_buffer(&mut bytes, &opts);
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::builder(cursor)
.fat_cache(16)
.open()
.expect("open");
fs.with_fat_cache_locked(|cache, _| cache.reset_stats())
.expect("cache installed");
let _ = fs.read_status_flags().expect("read_status_flags 1");
let _ = fs.read_status_flags().expect("read_status_flags 2");
let stats = fs
.with_fat_cache_locked(|cache, _| cache.stats())
.expect("cache installed");
assert!(
stats.hits >= 1,
"expected at least one cache hit after two read_status_flags() calls, got stats {stats:?}"
);
assert!(
stats.misses >= 1,
"expected at least one cache miss seeding the sector, got stats {stats:?}"
);
}
#[test]
fn read_file_chain_walk_consults_cache() {
use hadris_fat::FatVolumeWriteExt;
let mut bytes = vec![0u8; FAT32_SIZE];
let opts = fat32_options();
let _layout = format_into_buffer(&mut bytes, &opts);
let payload_len: usize = 64 * 1024;
{
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::open(cursor).expect("open");
let payload = vec![0xABu8; payload_len];
let root = fs.root_dir();
let entry = fs.create_file(&root, "BIG.BIN").expect("create_file");
let mut writer = fs.write_file(&entry).expect("write_file");
writer.write(&payload).expect("write");
writer.finish().expect("finish writer");
}
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::builder(cursor)
.fat_cache(16)
.open()
.expect("open with cache");
fs.with_fat_cache_locked(|cache, _| cache.reset_stats())
.expect("cache installed");
let entry = fs
.root_dir()
.find("BIG.BIN")
.expect("find ok")
.expect("find Some");
let mut reader = fs.read_file(&entry).expect("read_file");
let buf = reader.read_to_vec().expect("read_to_vec");
assert_eq!(buf.len(), payload_len);
let stats = fs
.with_fat_cache_locked(|cache, _| cache.stats())
.expect("cache installed");
assert!(
stats.hits > 0,
"expected cache hits when read_file walked a multi-cluster chain, got {stats:?}"
);
assert!(
stats.misses >= 1,
"expected at least one miss seeding the FAT sector, got {stats:?}"
);
}
#[test]
fn writes_then_reads_through_cache_are_consistent() {
use hadris_fat::FatVolumeWriteExt;
let mut bytes = vec![0u8; FAT32_SIZE];
let opts = fat32_options();
let _layout = format_into_buffer(&mut bytes, &opts);
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::builder(cursor)
.fat_cache(16)
.open()
.expect("open with cache");
let payload = vec![0xCDu8; 64 * 1024];
let _ = fs.read_status_flags().expect("read_status_flags");
let root = fs.root_dir();
let _ = root.find("does_not_exist").expect("find ok");
let entry = fs.create_file(&root, "DATA.BIN").expect("create_file");
{
let mut writer = fs.write_file(&entry).expect("write_file");
writer.write(&payload).expect("write");
writer.finish().expect("finish");
}
fs.flush().expect("flush cache");
let found = root.find("DATA.BIN").expect("find ok").expect("find Some");
let mut reader = fs.read_file(&found).expect("read_file");
let observed = reader.read_to_vec().expect("read_to_vec");
assert_eq!(observed, payload, "cached read must reflect cached writes");
}
#[test]
fn with_fat_cache_zero_treats_as_no_cache() {
let mut bytes = vec![0u8; FAT32_SIZE];
let opts = fat32_options();
let _layout = format_into_buffer(&mut bytes, &opts);
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::builder(cursor)
.fat_cache(0)
.open()
.expect("open");
assert!(
fs.fat_cache().is_none(),
"fat_cache(0) must install no cache"
);
assert!(
fs.with_cached_fat(|_, _| ()).is_none(),
"with_cached_fat must return None when no cache is installed"
);
}
#[test]
fn cache_stats_increment_on_hit_miss_eviction() {
let mut bytes = vec![0u8; FAT32_SIZE];
let opts = fat32_options();
let layout = format_into_buffer(&mut bytes, &opts);
patch_fat32_entry_all_copies(&mut bytes, &layout, 5, 6);
patch_fat32_entry_all_copies(&mut bytes, &layout, 6, 7);
patch_fat32_entry_all_copies(&mut bytes, &layout, 7, 8);
patch_fat32_entry_all_copies(&mut bytes, &layout, 8, 0x0FFF_FFFF);
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::builder(cursor)
.fat_cache(2)
.open()
.expect("open");
let _ = fs
.with_cached_fat(|cached, disk| cached.read_chain(disk, 5))
.expect("cache installed")
.expect("chain ok");
let stats_after_walk = fs
.with_fat_cache_locked(|cache, _| cache.stats())
.expect("locked");
assert!(
stats_after_walk.misses >= 1,
"first chain walk should record at least one miss"
);
let _ = fs
.with_cached_fat(|cached, disk| cached.read_chain(disk, 5))
.expect("cache installed")
.expect("chain ok");
let stats_after_replay = fs
.with_fat_cache_locked(|cache, _| cache.stats())
.expect("locked");
assert!(
stats_after_replay.hits > stats_after_walk.hits,
"replaying the chain must register additional cache hits"
);
let prev_evictions = stats_after_replay.evictions;
fs.with_fat_cache_locked(|cache, disk| {
for cluster in [200u32, 400, 600] {
let _ = cache.read_fat32_entry(disk, cluster as usize);
}
})
.expect("with_fat_cache_locked");
let stats_after_evict = fs
.with_fat_cache_locked(|cache, _| cache.stats())
.expect("locked");
assert!(
stats_after_evict.evictions > prev_evictions,
"exceeding capacity must register at least one eviction (had {prev_evictions}, now {})",
stats_after_evict.evictions
);
}