#![cfg(feature = "write")]
use hadris_fat::file::ShortFileName;
#[test]
fn test_short_filename_from_long_name_simple() {
let sfn = ShortFileName::from_long_name("test.txt", 0).unwrap();
assert_eq!(sfn.as_str(), "TEST .TXT");
}
#[test]
fn test_short_filename_from_long_name_with_suffix() {
let sfn = ShortFileName::from_long_name("test.txt", 1).unwrap();
assert!(sfn.as_str().contains("~1"));
}
#[test]
fn test_short_filename_from_long_name_no_extension() {
let sfn = ShortFileName::from_long_name("README", 0).unwrap();
assert!(sfn.as_str().starts_with("README"));
}
#[test]
fn test_short_filename_from_long_name_long_base() {
let sfn = ShortFileName::from_long_name("verylongfilename.txt", 1).unwrap();
assert!(sfn.as_str().starts_with("VERYLO~1"));
}
#[test]
fn test_short_filename_from_long_name_lowercase() {
let sfn = ShortFileName::from_long_name("lowercase.dat", 0).unwrap();
assert_eq!(sfn.as_str(), "LOWERCAS.DAT");
}
#[test]
fn test_short_filename_from_long_name_special_chars() {
let sfn = ShortFileName::from_long_name("my file.txt", 0).unwrap();
assert!(!sfn.as_str().contains(' ') || sfn.as_str().chars().filter(|c| *c == ' ').count() <= 3);
}
#[cfg(feature = "std")]
mod datetime_tests {
use hadris_fat::FatDateTime;
#[test]
fn test_fat_datetime_now() {
let dt = FatDateTime::now();
assert!(dt.date > 0);
}
#[test]
fn test_fat_datetime_new() {
let dt = FatDateTime::new(2024, 6, 15, 14, 30, 45);
let (date, time, _) = dt.to_raw();
let year = ((date >> 9) & 0x7F) + 1980;
let month = (date >> 5) & 0x0F;
let day = date & 0x1F;
assert_eq!(year, 2024);
assert_eq!(month, 6);
assert_eq!(day, 15);
let hour = (time >> 11) & 0x1F;
let minute = (time >> 5) & 0x3F;
let second = (time & 0x1F) * 2;
assert_eq!(hour, 14);
assert_eq!(minute, 30);
assert!(second == 44 || second == 46);
}
}
#[cfg(feature = "std")]
mod fat32_image {
use std::io::Cursor;
const SECTOR_SIZE: usize = 512;
const SECTORS_PER_CLUSTER: u8 = 1;
const CLUSTER_SIZE: usize = SECTOR_SIZE * SECTORS_PER_CLUSTER as usize;
const RESERVED_SECTORS: u16 = 32;
const FAT_COUNT: u8 = 2;
const SECTORS_PER_FAT: u32 = 128;
const ROOT_CLUSTER: u32 = 2;
const FSINFO_LEAD_SIG: u32 = 0x41615252;
const FSINFO_STRUC_SIG: u32 = 0x61417272;
const FSINFO_TRAIL_SIG: u32 = 0xAA550000;
pub fn create_fat32_image() -> Cursor<Vec<u8>> {
let data_start_sector = RESERVED_SECTORS as u32 + FAT_COUNT as u32 * SECTORS_PER_FAT;
let total_data_clusters: u32 = 256; let total_sectors = data_start_sector + total_data_clusters;
let total_size = total_sectors as usize * SECTOR_SIZE;
let mut image = vec![0u8; total_size];
write_boot_sector(&mut image);
write_fsinfo_sector(&mut image, total_data_clusters - 1);
let fat_start = RESERVED_SECTORS as usize * SECTOR_SIZE;
write_fat_table(&mut image, fat_start);
let fat2_start = fat_start + SECTORS_PER_FAT as usize * SECTOR_SIZE;
write_fat_table(&mut image, fat2_start);
Cursor::new(image)
}
fn write_boot_sector(image: &mut [u8]) {
image[0] = 0xEB;
image[1] = 0x58;
image[2] = 0x90;
image[3..11].copy_from_slice(b"HADRISFT");
image[11..13].copy_from_slice(&(SECTOR_SIZE as u16).to_le_bytes());
image[13] = SECTORS_PER_CLUSTER;
image[14..16].copy_from_slice(&RESERVED_SECTORS.to_le_bytes());
image[16] = FAT_COUNT;
image[17..19].copy_from_slice(&0u16.to_le_bytes());
image[19..21].copy_from_slice(&0u16.to_le_bytes());
image[21] = 0xF8;
image[22..24].copy_from_slice(&0u16.to_le_bytes());
image[24..26].copy_from_slice(&63u16.to_le_bytes());
image[26..28].copy_from_slice(&255u16.to_le_bytes());
image[28..32].copy_from_slice(&0u32.to_le_bytes());
let total_sectors = RESERVED_SECTORS as u32 + FAT_COUNT as u32 * SECTORS_PER_FAT + 256;
image[32..36].copy_from_slice(&total_sectors.to_le_bytes());
image[36..40].copy_from_slice(&SECTORS_PER_FAT.to_le_bytes());
image[40..42].copy_from_slice(&0u16.to_le_bytes());
image[42..44].copy_from_slice(&0u16.to_le_bytes());
image[44..48].copy_from_slice(&ROOT_CLUSTER.to_le_bytes());
image[48..50].copy_from_slice(&1u16.to_le_bytes());
image[50..52].copy_from_slice(&6u16.to_le_bytes());
image[64] = 0x80;
image[65] = 0;
image[66] = 0x29;
image[67..71].copy_from_slice(&0x12345678u32.to_le_bytes());
image[71..82].copy_from_slice(b"TEST ");
image[82..90].copy_from_slice(b"FAT32 ");
image[510] = 0x55;
image[511] = 0xAA;
}
fn write_fsinfo_sector(image: &mut [u8], free_clusters: u32) {
let offset = SECTOR_SIZE;
image[offset..offset + 4].copy_from_slice(&FSINFO_LEAD_SIG.to_le_bytes());
image[offset + 484..offset + 488].copy_from_slice(&FSINFO_STRUC_SIG.to_le_bytes());
image[offset + 488..offset + 492].copy_from_slice(&free_clusters.to_le_bytes());
image[offset + 492..offset + 496].copy_from_slice(&3u32.to_le_bytes());
image[offset + 508..offset + 512].copy_from_slice(&FSINFO_TRAIL_SIG.to_le_bytes());
}
fn write_fat_table(image: &mut [u8], fat_start: usize) {
image[fat_start..fat_start + 4].copy_from_slice(&0x0FFFFFF8u32.to_le_bytes());
image[fat_start + 4..fat_start + 8].copy_from_slice(&0x0FFFFFFFu32.to_le_bytes());
image[fat_start + 8..fat_start + 12].copy_from_slice(&0x0FFFFFF8u32.to_le_bytes());
}
pub const fn cluster_size() -> usize {
CLUSTER_SIZE
}
pub const fn fat_start_bytes() -> usize {
RESERVED_SECTORS as usize * SECTOR_SIZE
}
pub const fn fat2_start_bytes() -> usize {
fat_start_bytes() + SECTORS_PER_FAT as usize * SECTOR_SIZE
}
pub const fn data_start_bytes() -> usize {
fat2_start_bytes() + SECTORS_PER_FAT as usize * SECTOR_SIZE
}
}
#[cfg(feature = "std")]
mod fat16_image {
use std::io::Cursor;
const SECTOR_SIZE: usize = 512;
const SECTORS_PER_CLUSTER: u8 = 1;
const CLUSTER_SIZE: usize = SECTOR_SIZE * SECTORS_PER_CLUSTER as usize;
const RESERVED_SECTORS: u16 = 1;
const FAT_COUNT: u8 = 2;
const SECTORS_PER_FAT: u16 = 32;
const ROOT_ENTRY_COUNT: u16 = 512;
pub fn create_fat16_image() -> Cursor<Vec<u8>> {
let root_dir_sectors = (ROOT_ENTRY_COUNT as usize * 32).div_ceil(SECTOR_SIZE);
let data_start_sector = RESERVED_SECTORS as usize
+ FAT_COUNT as usize * SECTORS_PER_FAT as usize
+ root_dir_sectors;
let total_data_clusters: usize = 8192; let total_sectors = data_start_sector + total_data_clusters;
let total_size = total_sectors * SECTOR_SIZE;
let mut image = vec![0u8; total_size];
write_boot_sector(&mut image, total_sectors as u32);
let fat_start = RESERVED_SECTORS as usize * SECTOR_SIZE;
write_fat_table(&mut image, fat_start);
let fat2_start = fat_start + SECTORS_PER_FAT as usize * SECTOR_SIZE;
write_fat_table(&mut image, fat2_start);
Cursor::new(image)
}
fn write_boot_sector(image: &mut [u8], total_sectors: u32) {
image[0] = 0xEB;
image[1] = 0x3C;
image[2] = 0x90;
image[3..11].copy_from_slice(b"HADRISFT");
image[11..13].copy_from_slice(&(SECTOR_SIZE as u16).to_le_bytes());
image[13] = SECTORS_PER_CLUSTER;
image[14..16].copy_from_slice(&RESERVED_SECTORS.to_le_bytes());
image[16] = FAT_COUNT;
image[17..19].copy_from_slice(&ROOT_ENTRY_COUNT.to_le_bytes());
if total_sectors <= 65535 {
image[19..21].copy_from_slice(&(total_sectors as u16).to_le_bytes());
} else {
image[19..21].copy_from_slice(&0u16.to_le_bytes());
}
image[21] = 0xF8;
image[22..24].copy_from_slice(&SECTORS_PER_FAT.to_le_bytes());
image[24..26].copy_from_slice(&63u16.to_le_bytes());
image[26..28].copy_from_slice(&255u16.to_le_bytes());
image[28..32].copy_from_slice(&0u32.to_le_bytes());
if total_sectors > 65535 {
image[32..36].copy_from_slice(&total_sectors.to_le_bytes());
} else {
image[32..36].copy_from_slice(&0u32.to_le_bytes());
}
image[36] = 0x80;
image[37] = 0;
image[38] = 0x29;
image[39..43].copy_from_slice(&0x12345678u32.to_le_bytes());
image[43..54].copy_from_slice(b"TEST ");
image[54..62].copy_from_slice(b"FAT16 ");
image[510] = 0x55;
image[511] = 0xAA;
}
fn write_fat_table(image: &mut [u8], fat_start: usize) {
image[fat_start..fat_start + 2].copy_from_slice(&0xFFF8u16.to_le_bytes());
image[fat_start + 2..fat_start + 4].copy_from_slice(&0xFFFFu16.to_le_bytes());
}
pub const fn cluster_size() -> usize {
CLUSTER_SIZE
}
}
#[cfg(feature = "std")]
mod fat12_image {
use std::io::Cursor;
const SECTOR_SIZE: usize = 512;
const SECTORS_PER_CLUSTER: u8 = 1;
const CLUSTER_SIZE: usize = SECTOR_SIZE * SECTORS_PER_CLUSTER as usize;
const RESERVED_SECTORS: u16 = 1;
const FAT_COUNT: u8 = 2;
const SECTORS_PER_FAT: u16 = 9;
const ROOT_ENTRY_COUNT: u16 = 224;
pub fn create_fat12_image() -> Cursor<Vec<u8>> {
let root_dir_sectors = (ROOT_ENTRY_COUNT as usize * 32).div_ceil(SECTOR_SIZE);
let _data_start_sector = RESERVED_SECTORS as usize
+ FAT_COUNT as usize * SECTORS_PER_FAT as usize
+ root_dir_sectors;
let total_sectors: usize = 2880;
let total_size = total_sectors * SECTOR_SIZE;
let mut image = vec![0u8; total_size];
write_boot_sector(&mut image, total_sectors as u16);
let fat_start = RESERVED_SECTORS as usize * SECTOR_SIZE;
write_fat_table(&mut image, fat_start);
let fat2_start = fat_start + SECTORS_PER_FAT as usize * SECTOR_SIZE;
write_fat_table(&mut image, fat2_start);
Cursor::new(image)
}
fn write_boot_sector(image: &mut [u8], total_sectors: u16) {
image[0] = 0xEB;
image[1] = 0x3C;
image[2] = 0x90;
image[3..11].copy_from_slice(b"HADRISFT");
image[11..13].copy_from_slice(&(SECTOR_SIZE as u16).to_le_bytes());
image[13] = SECTORS_PER_CLUSTER;
image[14..16].copy_from_slice(&RESERVED_SECTORS.to_le_bytes());
image[16] = FAT_COUNT;
image[17..19].copy_from_slice(&ROOT_ENTRY_COUNT.to_le_bytes());
image[19..21].copy_from_slice(&total_sectors.to_le_bytes());
image[21] = 0xF0;
image[22..24].copy_from_slice(&SECTORS_PER_FAT.to_le_bytes());
image[24..26].copy_from_slice(&18u16.to_le_bytes());
image[26..28].copy_from_slice(&2u16.to_le_bytes());
image[28..32].copy_from_slice(&0u32.to_le_bytes());
image[32..36].copy_from_slice(&0u32.to_le_bytes());
image[36] = 0x00;
image[37] = 0;
image[38] = 0x29;
image[39..43].copy_from_slice(&0x12345678u32.to_le_bytes());
image[43..54].copy_from_slice(b"TEST ");
image[54..62].copy_from_slice(b"FAT12 ");
image[510] = 0x55;
image[511] = 0xAA;
}
fn write_fat_table(image: &mut [u8], fat_start: usize) {
image[fat_start] = 0xF0; image[fat_start + 1] = 0xFF; image[fat_start + 2] = 0xFF;
}
pub const fn cluster_size() -> usize {
CLUSTER_SIZE
}
}
#[cfg(feature = "std")]
mod integration_tests {
use super::fat32_image::{cluster_size, create_fat32_image};
use hadris_fat::time::StaticTimeProvider;
use hadris_fat::{Error, FatDateTime, FatVolume, FatVolumeWriteExt};
#[test]
fn test_builder_static_time_provider_propagates_to_created_entries() {
let pinned = FatDateTime::new(2030, 6, 15, 12, 0, 0);
let provider: &'static StaticTimeProvider = Box::leak(Box::new(StaticTimeProvider(pinned)));
let image = create_fat32_image();
let fs = FatVolume::builder(image)
.time_provider(provider)
.open()
.expect("builder open should succeed");
let root = fs.root_dir();
let entry = fs
.create_file(&root, "STAMP.TXT")
.expect("create_file should succeed");
assert_eq!(entry.created().date, pinned.date);
assert_eq!(entry.created().time, pinned.time);
assert_eq!(entry.modified().date, pinned.date);
assert_eq!(entry.accessed_date(), pinned.date);
}
#[test]
fn test_set_times_patches_only_supplied_fields() {
let initial = FatDateTime::new(2030, 1, 1, 0, 0, 0);
let provider: &'static StaticTimeProvider =
Box::leak(Box::new(StaticTimeProvider(initial)));
let image = create_fat32_image();
let fs = FatVolume::builder(image)
.time_provider(provider)
.open()
.expect("builder open should succeed");
let root = fs.root_dir();
let _ = fs
.create_file(&root, "PATCH.TXT")
.expect("create_file should succeed");
let new_modified = FatDateTime::new(2031, 12, 31, 23, 59, 58);
let root = fs.root_dir();
let entry = root
.find("PATCH.TXT")
.expect("find should succeed")
.expect("entry must exist after create");
fs.set_times(&entry, Some(new_modified), None, None)
.expect("set_times should succeed");
let root = fs.root_dir();
let after = root
.find("PATCH.TXT")
.expect("find should succeed")
.expect("entry must still exist");
assert_eq!(after.modified().date, new_modified.date);
assert_eq!(after.modified().time, new_modified.time);
assert_eq!(after.created().date, initial.date);
assert_eq!(after.accessed_date(), initial.date);
}
#[test]
fn test_read_status_flags_on_clean_volume() {
let image = create_fat32_image();
let fs = FatVolume::open(image).expect("open");
let flags = fs.read_status_flags().expect("read_status_flags");
assert!(!flags.dirty, "freshly created volume must not be dirty");
assert!(
!flags.io_errors,
"freshly created volume must report no I/O errors"
);
}
#[test]
fn test_rename_across_directories_preserves_cluster_chain() {
let image = create_fat32_image();
let fs = FatVolume::open(image).expect("open");
let root = fs.root_dir();
let _ = fs.create_dir(&root, "SRC").expect("mkdir SRC");
let _ = fs.create_dir(&root, "DST").expect("mkdir DST");
let src = fs.open_dir_path("/SRC").expect("open SRC");
let _ = fs.create_file(&src, "A.TXT").expect("create A.TXT");
let a_entry = src
.find("A.TXT")
.expect("find ok")
.expect("A.TXT must exist");
let original_cluster = a_entry.cluster();
let dst = fs.open_dir_path("/DST").expect("open DST");
let new_entry = fs
.rename(&a_entry, &dst, "B.TXT")
.expect("cross-dir rename should succeed");
let src = fs.open_dir_path("/SRC").expect("re-open SRC");
assert!(
src.find("A.TXT").expect("find ok").is_none(),
"A.TXT must be gone from SRC after rename"
);
let dst = fs.open_dir_path("/DST").expect("re-open DST");
let found = dst
.find("B.TXT")
.expect("find ok")
.expect("B.TXT must exist in DST");
assert_eq!(
new_entry.cluster(),
original_cluster,
"rename returned entry should keep the original cluster"
);
assert_eq!(
found.cluster(),
original_cluster,
"on-disk B.TXT should keep the original cluster"
);
}
#[test]
fn test_set_attributes_blocks_immutable_bit_flips() {
use hadris_fat::raw::DirEntryAttrFlags;
let image = create_fat32_image();
let fs = FatVolume::open(image).expect("open");
let root = fs.root_dir();
let _ = fs
.create_file(&root, "PROT.TXT")
.expect("create_file should succeed");
let root = fs.root_dir();
let entry = root
.find("PROT.TXT")
.expect("find ok")
.expect("entry must exist");
let with_ro = entry.attributes() | DirEntryAttrFlags::READ_ONLY;
fs.set_attributes(&entry, with_ro)
.expect("toggling READ_ONLY should be allowed");
let bad = entry.attributes() | DirEntryAttrFlags::DIRECTORY;
match fs.set_attributes(&entry, bad) {
Err(Error::InvalidAttributeChange { bit: "DIRECTORY" }) => {}
other => panic!("expected DIRECTORY rejection, got {other:?}"),
}
let bad = entry.attributes() | DirEntryAttrFlags::VOLUME_ID;
match fs.set_attributes(&entry, bad) {
Err(Error::InvalidAttributeChange { bit: "VOLUME_ID" }) => {}
other => panic!("expected VOLUME_ID rejection, got {other:?}"),
}
}
#[test]
fn test_open_fat32_image() {
let image = create_fat32_image();
let fs = FatVolume::open(image).expect("Failed to open FAT32 image");
let root = fs.root_dir();
let entries: Vec<_> = root.entries().collect();
assert!(entries.is_empty(), "Root directory should be empty");
}
#[test]
fn test_create_file() {
let image = create_fat32_image();
let fs = FatVolume::open(image).expect("Failed to open FAT32 image");
let root = fs.root_dir();
let entry = fs
.create_file(&root, "TEST.TXT")
.expect("Failed to create file");
assert!(entry.is_file());
assert_eq!(entry.len(), 0);
let root = fs.root_dir();
let found = root.find("TEST.TXT").expect("Find failed");
assert!(found.is_some(), "File should be found in directory");
}
#[test]
fn test_create_file_already_exists() {
let image = create_fat32_image();
let fs = FatVolume::open(image).expect("Failed to open FAT32 image");
let root = fs.root_dir();
fs.create_file(&root, "TEST.TXT")
.expect("Failed to create file");
let result = fs.create_file(&root, "TEST.TXT");
match result {
Err(Error::AlreadyExists) => {}
_ => panic!("Expected AlreadyExists error"),
}
}
#[test]
fn test_write_file_content() {
let image = create_fat32_image();
let fs = FatVolume::open(image).expect("Failed to open FAT32 image");
let root = fs.root_dir();
let entry = fs
.create_file(&root, "HELLO.TXT")
.expect("Failed to create file");
let content = b"Hello, FAT32 World!";
{
let mut writer = fs.write_file(&entry).expect("Failed to get writer");
writer.write(content).expect("Failed to write");
writer.finish().expect("Failed to finish");
}
let root = fs.root_dir();
let entry = root
.find("HELLO.TXT")
.expect("Find failed")
.expect("File not found");
assert_eq!(entry.len(), content.len() as u64);
use hadris_fat::FatVolumeReadExt;
let mut reader = fs.read_file(&entry).expect("Failed to get reader");
let mut buf = vec![0u8; content.len()];
let mut total = 0;
while total < buf.len() {
let n = reader.read(&mut buf[total..]).expect("Read failed");
if n == 0 {
break;
}
total += n;
}
assert_eq!(&buf[..total], content);
}
#[test]
fn test_write_file_multiple_clusters() {
let image = create_fat32_image();
let fs = FatVolume::open(image).expect("Failed to open FAT32 image");
let root = fs.root_dir();
let entry = fs
.create_file(&root, "BIG.DAT")
.expect("Failed to create file");
let cluster_sz = cluster_size();
let content: Vec<u8> = (0..cluster_sz * 3).map(|i| (i % 256) as u8).collect();
{
let mut writer = fs.write_file(&entry).expect("Failed to get writer");
writer.write(&content).expect("Failed to write");
writer.finish().expect("Failed to finish");
}
let root = fs.root_dir();
let entry = root
.find("BIG.DAT")
.expect("Find failed")
.expect("File not found");
assert_eq!(entry.len(), content.len() as u64);
use hadris_fat::FatVolumeReadExt;
let mut reader = fs.read_file(&entry).expect("Failed to get reader");
let read_content = reader.read_to_vec().expect("Read failed");
assert_eq!(read_content, content);
}
#[test]
fn test_create_directory() {
let image = create_fat32_image();
let fs = FatVolume::open(image).expect("Failed to open FAT32 image");
let root = fs.root_dir();
let subdir = fs
.create_dir(&root, "SUBDIR")
.expect("Failed to create directory");
let entries: Vec<_> = subdir.entries().collect();
let names: Vec<_> = entries
.iter()
.filter_map(|e| e.as_ref().ok())
.map(|e| e.name().trim_end_matches(' ').to_string())
.collect();
assert!(
names.iter().any(|n| n.starts_with('.')),
"Directory should have . entry: {names:?}"
);
assert!(
names.iter().any(|n| n.starts_with("..")),
"Directory should have .. entry: {names:?}"
);
}
#[test]
fn test_create_file_in_subdirectory() {
let image = create_fat32_image();
let fs = FatVolume::open(image).expect("Failed to open FAT32 image");
let root = fs.root_dir();
let subdir = fs
.create_dir(&root, "MYDIR")
.expect("Failed to create directory");
let entry = fs
.create_file(&subdir, "FILE.TXT")
.expect("Failed to create file");
assert!(entry.is_file());
let found = subdir.find("FILE.TXT").expect("Find failed");
assert!(found.is_some());
let root = fs.root_dir();
let found_in_root = root.find("FILE.TXT").expect("Find failed");
assert!(found_in_root.is_none());
}
#[test]
fn test_delete_file() {
let image = create_fat32_image();
let fs = FatVolume::open(image).expect("Failed to open FAT32 image");
let root = fs.root_dir();
let entry = fs
.create_file(&root, "DELETE.ME")
.expect("Failed to create file");
{
let mut writer = fs.write_file(&entry).expect("Failed to get writer");
writer.write(b"temporary data").expect("Failed to write");
writer.finish().expect("Failed to finish");
}
let root = fs.root_dir();
let entry = root
.find("DELETE.ME")
.expect("Find failed")
.expect("File not found");
fs.delete(&entry).expect("Failed to delete");
let root = fs.root_dir();
let found = root.find("DELETE.ME").expect("Find failed");
assert!(found.is_none(), "File should be deleted");
}
#[test]
fn test_delete_empty_directory() {
let image = create_fat32_image();
let fs = FatVolume::open(image).expect("Failed to open FAT32 image");
let root = fs.root_dir();
let _subdir = fs
.create_dir(&root, "EMPTYDIR")
.expect("Failed to create directory");
let root = fs.root_dir();
let entry = root
.find("EMPTYDIR")
.expect("Find failed")
.expect("Dir not found");
fs.delete(&entry).expect("Failed to delete empty directory");
let root = fs.root_dir();
let found = root.find("EMPTYDIR").expect("Find failed");
assert!(found.is_none(), "Directory should be deleted");
}
#[test]
fn test_delete_non_empty_directory_fails() {
let image = create_fat32_image();
let fs = FatVolume::open(image).expect("Failed to open FAT32 image");
let root = fs.root_dir();
let subdir = fs
.create_dir(&root, "HASFILE")
.expect("Failed to create directory");
fs.create_file(&subdir, "INSIDE.TXT")
.expect("Failed to create file");
let root = fs.root_dir();
let entry = root
.find("HASFILE")
.expect("Find failed")
.expect("Dir not found");
let result = fs.delete(&entry);
match result {
Err(Error::DirectoryNotEmpty) => {}
_ => panic!("Expected DirectoryNotEmpty error"),
}
}
#[test]
fn test_create_multiple_files() {
let image = create_fat32_image();
let fs = FatVolume::open(image).expect("Failed to open FAT32 image");
let root = fs.root_dir();
for i in 0..10 {
let name = format!("FILE{i}.TXT");
fs.create_file(&root, &name).expect("Failed to create file");
}
let root = fs.root_dir();
let entries: Vec<_> = root.entries().collect();
assert_eq!(entries.len(), 10, "Should have 10 files");
}
#[test]
fn test_case_insensitive_find() {
let image = create_fat32_image();
let fs = FatVolume::open(image).expect("Failed to open FAT32 image");
let root = fs.root_dir();
fs.create_file(&root, "MYFILE.TXT")
.expect("Failed to create file");
let root = fs.root_dir();
assert!(root.find("MYFILE.TXT").expect("Find failed").is_some());
assert!(root.find("myfile.txt").expect("Find failed").is_some());
assert!(root.find("MyFile.Txt").expect("Find failed").is_some());
}
#[test]
fn test_write_then_append() {
let image = create_fat32_image();
let fs = FatVolume::open(image).expect("Failed to open FAT32 image");
let root = fs.root_dir();
let entry = fs
.create_file(&root, "APPEND.TXT")
.expect("Failed to create file");
{
let mut writer = fs.write_file(&entry).expect("Failed to get writer");
writer.write(b"First part. ").expect("Failed to write");
writer.finish().expect("Failed to finish");
}
let root = fs.root_dir();
let entry = root
.find("APPEND.TXT")
.expect("Find failed")
.expect("File not found");
use hadris_fat::FatVolumeReadExt;
let mut reader = fs.read_file(&entry).expect("Failed to get reader");
let content = reader.read_to_vec().expect("Read failed");
assert_eq!(&content, b"First part. ");
}
}
#[cfg(feature = "std")]
mod fat16_integration_tests {
use super::fat16_image::{cluster_size, create_fat16_image};
use hadris_fat::{Error, FatType, FatVolume, FatVolumeWriteExt};
#[test]
fn test_open_fat16_image() {
let image = create_fat16_image();
let fs = FatVolume::open(image).expect("Failed to open FAT16 image");
assert!(
matches!(fs.fat_type(), FatType::Fat16),
"Expected FAT16, got {:?}",
fs.fat_type()
);
let root = fs.root_dir();
let entries: Vec<_> = root.entries().collect();
assert!(entries.is_empty(), "Root directory should be empty");
}
#[test]
fn test_fat16_create_file() {
let image = create_fat16_image();
let fs = FatVolume::open(image).expect("Failed to open FAT16 image");
let root = fs.root_dir();
let entry = fs
.create_file(&root, "TEST.TXT")
.expect("Failed to create file");
assert!(entry.is_file());
assert_eq!(entry.len(), 0);
let root = fs.root_dir();
let found = root.find("TEST.TXT").expect("Find failed");
assert!(found.is_some(), "File should be found in directory");
}
#[test]
fn test_fat16_write_file_content() {
let image = create_fat16_image();
let fs = FatVolume::open(image).expect("Failed to open FAT16 image");
let root = fs.root_dir();
let entry = fs
.create_file(&root, "HELLO.TXT")
.expect("Failed to create file");
let content = b"Hello, FAT16 World!";
{
let mut writer = fs.write_file(&entry).expect("Failed to get writer");
writer.write(content).expect("Failed to write");
writer.finish().expect("Failed to finish");
}
let root = fs.root_dir();
let entry = root
.find("HELLO.TXT")
.expect("Find failed")
.expect("File not found");
assert_eq!(entry.len(), content.len() as u64);
use hadris_fat::FatVolumeReadExt;
let mut reader = fs.read_file(&entry).expect("Failed to get reader");
let mut buf = vec![0u8; content.len()];
let mut total = 0;
while total < buf.len() {
let n = reader.read(&mut buf[total..]).expect("Read failed");
if n == 0 {
break;
}
total += n;
}
assert_eq!(&buf[..total], content);
}
#[test]
fn test_fat16_write_file_multiple_clusters() {
let image = create_fat16_image();
let fs = FatVolume::open(image).expect("Failed to open FAT16 image");
let root = fs.root_dir();
let entry = fs
.create_file(&root, "BIG.DAT")
.expect("Failed to create file");
let cluster_sz = cluster_size();
let content: Vec<u8> = (0..cluster_sz * 3).map(|i| (i % 256) as u8).collect();
{
let mut writer = fs.write_file(&entry).expect("Failed to get writer");
writer.write(&content).expect("Failed to write");
writer.finish().expect("Failed to finish");
}
let root = fs.root_dir();
let entry = root
.find("BIG.DAT")
.expect("Find failed")
.expect("File not found");
assert_eq!(entry.len(), content.len() as u64);
use hadris_fat::FatVolumeReadExt;
let mut reader = fs.read_file(&entry).expect("Failed to get reader");
let read_content = reader.read_to_vec().expect("Read failed");
assert_eq!(read_content, content);
}
#[test]
fn test_fat16_create_directory() {
let image = create_fat16_image();
let fs = FatVolume::open(image).expect("Failed to open FAT16 image");
let root = fs.root_dir();
let subdir = fs
.create_dir(&root, "SUBDIR")
.expect("Failed to create directory");
let entries: Vec<_> = subdir.entries().collect();
let names: Vec<_> = entries
.iter()
.filter_map(|e| e.as_ref().ok())
.map(|e| e.name().trim_end_matches(' ').to_string())
.collect();
assert!(
names.iter().any(|n| n.starts_with('.')),
"Directory should have . entry: {names:?}"
);
assert!(
names.iter().any(|n| n.starts_with("..")),
"Directory should have .. entry: {names:?}"
);
}
#[test]
fn test_fat16_create_file_in_subdirectory() {
let image = create_fat16_image();
let fs = FatVolume::open(image).expect("Failed to open FAT16 image");
let root = fs.root_dir();
let subdir = fs
.create_dir(&root, "MYDIR")
.expect("Failed to create directory");
let entry = fs
.create_file(&subdir, "FILE.TXT")
.expect("Failed to create file");
assert!(entry.is_file());
let found = subdir.find("FILE.TXT").expect("Find failed");
assert!(found.is_some());
let root = fs.root_dir();
let found_in_root = root.find("FILE.TXT").expect("Find failed");
assert!(found_in_root.is_none());
}
#[test]
fn test_fat16_delete_file() {
let image = create_fat16_image();
let fs = FatVolume::open(image).expect("Failed to open FAT16 image");
let root = fs.root_dir();
let entry = fs
.create_file(&root, "DELETE.ME")
.expect("Failed to create file");
{
let mut writer = fs.write_file(&entry).expect("Failed to get writer");
writer.write(b"temporary data").expect("Failed to write");
writer.finish().expect("Failed to finish");
}
let root = fs.root_dir();
let entry = root
.find("DELETE.ME")
.expect("Find failed")
.expect("File not found");
fs.delete(&entry).expect("Failed to delete");
let root = fs.root_dir();
let found = root.find("DELETE.ME").expect("Find failed");
assert!(found.is_none(), "File should be deleted");
}
#[test]
fn test_fat16_create_multiple_files() {
let image = create_fat16_image();
let fs = FatVolume::open(image).expect("Failed to open FAT16 image");
let root = fs.root_dir();
for i in 0..10 {
let name = format!("FILE{i}.TXT");
fs.create_file(&root, &name).expect("Failed to create file");
}
let root = fs.root_dir();
let entries: Vec<_> = root.entries().collect();
assert_eq!(entries.len(), 10, "Should have 10 files");
}
#[test]
fn test_fat16_file_already_exists() {
let image = create_fat16_image();
let fs = FatVolume::open(image).expect("Failed to open FAT16 image");
let root = fs.root_dir();
fs.create_file(&root, "TEST.TXT")
.expect("Failed to create file");
let result = fs.create_file(&root, "TEST.TXT");
match result {
Err(Error::AlreadyExists) => {}
_ => panic!("Expected AlreadyExists error"),
}
}
}
#[cfg(feature = "std")]
mod fat12_integration_tests {
use super::fat12_image::{cluster_size, create_fat12_image};
use hadris_fat::{Error, FatType, FatVolume, FatVolumeWriteExt};
#[test]
fn test_open_fat12_image() {
let image = create_fat12_image();
let fs = FatVolume::open(image).expect("Failed to open FAT12 image");
assert!(
matches!(fs.fat_type(), FatType::Fat12),
"Expected FAT12, got {:?}",
fs.fat_type()
);
let root = fs.root_dir();
let entries: Vec<_> = root.entries().collect();
assert!(entries.is_empty(), "Root directory should be empty");
}
#[test]
fn test_fat12_create_file() {
let image = create_fat12_image();
let fs = FatVolume::open(image).expect("Failed to open FAT12 image");
let root = fs.root_dir();
let entry = fs
.create_file(&root, "TEST.TXT")
.expect("Failed to create file");
assert!(entry.is_file());
assert_eq!(entry.len(), 0);
let root = fs.root_dir();
let found = root.find("TEST.TXT").expect("Find failed");
assert!(found.is_some(), "File should be found in directory");
}
#[test]
fn test_fat12_write_file_content() {
let image = create_fat12_image();
let fs = FatVolume::open(image).expect("Failed to open FAT12 image");
let root = fs.root_dir();
let entry = fs
.create_file(&root, "HELLO.TXT")
.expect("Failed to create file");
let content = b"Hello, FAT12 World!";
{
let mut writer = fs.write_file(&entry).expect("Failed to get writer");
writer.write(content).expect("Failed to write");
writer.finish().expect("Failed to finish");
}
let root = fs.root_dir();
let entry = root
.find("HELLO.TXT")
.expect("Find failed")
.expect("File not found");
assert_eq!(entry.len(), content.len() as u64);
use hadris_fat::FatVolumeReadExt;
let mut reader = fs.read_file(&entry).expect("Failed to get reader");
let mut buf = vec![0u8; content.len()];
let mut total = 0;
while total < buf.len() {
let n = reader.read(&mut buf[total..]).expect("Read failed");
if n == 0 {
break;
}
total += n;
}
assert_eq!(&buf[..total], content);
}
#[test]
fn test_fat12_write_file_multiple_clusters() {
let image = create_fat12_image();
let fs = FatVolume::open(image).expect("Failed to open FAT12 image");
let root = fs.root_dir();
let entry = fs
.create_file(&root, "BIG.DAT")
.expect("Failed to create file");
let cluster_sz = cluster_size();
let content: Vec<u8> = (0..cluster_sz * 3).map(|i| (i % 256) as u8).collect();
{
let mut writer = fs.write_file(&entry).expect("Failed to get writer");
writer.write(&content).expect("Failed to write");
writer.finish().expect("Failed to finish");
}
let root = fs.root_dir();
let entry = root
.find("BIG.DAT")
.expect("Find failed")
.expect("File not found");
assert_eq!(entry.len(), content.len() as u64);
use hadris_fat::FatVolumeReadExt;
let mut reader = fs.read_file(&entry).expect("Failed to get reader");
let read_content = reader.read_to_vec().expect("Read failed");
assert_eq!(read_content, content);
}
#[test]
fn test_fat12_create_directory() {
let image = create_fat12_image();
let fs = FatVolume::open(image).expect("Failed to open FAT12 image");
let root = fs.root_dir();
let subdir = fs
.create_dir(&root, "SUBDIR")
.expect("Failed to create directory");
let entries: Vec<_> = subdir.entries().collect();
let names: Vec<_> = entries
.iter()
.filter_map(|e| e.as_ref().ok())
.map(|e| e.name().trim_end_matches(' ').to_string())
.collect();
assert!(
names.iter().any(|n| n.starts_with('.')),
"Directory should have . entry: {names:?}"
);
assert!(
names.iter().any(|n| n.starts_with("..")),
"Directory should have .. entry: {names:?}"
);
}
#[test]
fn test_fat12_create_file_in_subdirectory() {
let image = create_fat12_image();
let fs = FatVolume::open(image).expect("Failed to open FAT12 image");
let root = fs.root_dir();
let subdir = fs
.create_dir(&root, "MYDIR")
.expect("Failed to create directory");
let entry = fs
.create_file(&subdir, "FILE.TXT")
.expect("Failed to create file");
assert!(entry.is_file());
let found = subdir.find("FILE.TXT").expect("Find failed");
assert!(found.is_some());
let root = fs.root_dir();
let found_in_root = root.find("FILE.TXT").expect("Find failed");
assert!(found_in_root.is_none());
}
#[test]
fn test_fat12_delete_file() {
let image = create_fat12_image();
let fs = FatVolume::open(image).expect("Failed to open FAT12 image");
let root = fs.root_dir();
let entry = fs
.create_file(&root, "DELETE.ME")
.expect("Failed to create file");
{
let mut writer = fs.write_file(&entry).expect("Failed to get writer");
writer.write(b"temporary data").expect("Failed to write");
writer.finish().expect("Failed to finish");
}
let root = fs.root_dir();
let entry = root
.find("DELETE.ME")
.expect("Find failed")
.expect("File not found");
fs.delete(&entry).expect("Failed to delete");
let root = fs.root_dir();
let found = root.find("DELETE.ME").expect("Find failed");
assert!(found.is_none(), "File should be deleted");
}
#[test]
fn test_fat12_create_multiple_files() {
let image = create_fat12_image();
let fs = FatVolume::open(image).expect("Failed to open FAT12 image");
let root = fs.root_dir();
for i in 0..10 {
let name = format!("FILE{i}.TXT");
fs.create_file(&root, &name).expect("Failed to create file");
}
let root = fs.root_dir();
let entries: Vec<_> = root.entries().collect();
assert_eq!(entries.len(), 10, "Should have 10 files");
}
#[test]
fn test_fat12_file_already_exists() {
let image = create_fat12_image();
let fs = FatVolume::open(image).expect("Failed to open FAT12 image");
let root = fs.root_dir();
fs.create_file(&root, "TEST.TXT")
.expect("Failed to create file");
let result = fs.create_file(&root, "TEST.TXT");
match result {
Err(Error::AlreadyExists) => {}
_ => panic!("Expected AlreadyExists error"),
}
}
#[test]
fn test_fat12_cluster_chain() {
let image = create_fat12_image();
let fs = FatVolume::open(image).expect("Failed to open FAT12 image");
let root = fs.root_dir();
let cluster_sz = cluster_size();
let content: Vec<u8> = (0..cluster_sz * 5).map(|i| (i % 256) as u8).collect();
let entry = fs
.create_file(&root, "CHAIN.DAT")
.expect("Failed to create file");
{
let mut writer = fs.write_file(&entry).expect("Failed to get writer");
writer.write(&content).expect("Failed to write");
writer.finish().expect("Failed to finish");
}
let root = fs.root_dir();
let entry = root
.find("CHAIN.DAT")
.expect("Find failed")
.expect("File not found");
assert_eq!(entry.len(), content.len() as u64);
use hadris_fat::FatVolumeReadExt;
let mut reader = fs.read_file(&entry).expect("Failed to get reader");
let read_content = reader.read_to_vec().expect("Read failed");
assert_eq!(read_content, content, "FAT12 cluster chain read mismatch");
}
}
#[cfg(feature = "std")]
mod corrupt_image_tests {
use super::fat32_image::{
cluster_size, create_fat32_image, data_start_bytes, fat_start_bytes, fat2_start_bytes,
};
use hadris_fat::{Error, FatVolume, FatVolumeReadExt};
use std::io::Cursor;
fn patch_fat_entry(image: &mut [u8], cluster: u32, value: u32) {
let fat1 = fat_start_bytes() + cluster as usize * 4;
let fat2 = fat2_start_bytes() + cluster as usize * 4;
let bytes = value.to_le_bytes();
image[fat1..fat1 + 4].copy_from_slice(&bytes);
image[fat2..fat2 + 4].copy_from_slice(&bytes);
}
#[test]
fn test_read_returns_cluster_loop_on_2cycle() {
let image = create_fat32_image();
let mut bytes = image.into_inner();
patch_fat_entry(&mut bytes, 3, 4);
patch_fat_entry(&mut bytes, 4, 3);
let entry_offset = data_start_bytes();
let mut entry = [0u8; 32];
entry[0..11].copy_from_slice(b"LOOP DAT");
entry[11] = 0x20; entry[26..28].copy_from_slice(&3u16.to_le_bytes());
entry[28..32].copy_from_slice(&u32::MAX.to_le_bytes());
bytes[entry_offset..entry_offset + 32].copy_from_slice(&entry);
let fs = FatVolume::open(Cursor::new(bytes)).expect("mount");
let root = fs.root_dir();
let file = root.find("LOOP.DAT").expect("find").expect("entry present");
let mut reader = fs.read_file(&file).expect("reader");
let mut scratch = vec![0u8; cluster_size()];
let mut last = Ok(0usize);
for _ in 0..10_000 {
match reader.read(&mut scratch) {
Ok(0) => panic!("reader hit EOF without detecting cycle"),
Ok(_) => continue,
Err(e) => {
last = Err(e);
break;
}
}
}
match last {
Err(Error::ClusterLoop { .. }) => {}
Err(other) => panic!("expected ClusterLoop, got {other:?}"),
Ok(_) => panic!("expected ClusterLoop, no error after 10k reads"),
}
}
#[cfg(feature = "lfn")]
#[test]
fn test_long_name_roundtrips_via_lfn_entries() {
let image = create_fat32_image();
let fs = FatVolume::open(image).expect("open");
let root = fs.root_dir();
let long_name = "My Long Notes.txt";
fs.create_file(&root, long_name).expect("create_file");
let root = fs.root_dir();
let entry = root
.find(long_name)
.expect("find did not error")
.expect("entry should be found by long name");
let long_matched = entry
.long_name()
.map(|lfn| lfn.eq_str(long_name))
.unwrap_or(false);
assert!(
long_matched,
"LongFileName should round-trip the original name; got {:?}",
entry.long_name()
);
}
#[cfg(feature = "lfn")]
#[test]
fn test_short_compliant_name_skips_lfn() {
let image = create_fat32_image();
let fs = FatVolume::open(image).expect("open");
let root = fs.root_dir();
fs.create_file(&root, "TEST.TXT").expect("create");
let root = fs.root_dir();
let entry = root.find("TEST.TXT").expect("find").expect("entry");
assert!(
entry.long_name().is_none(),
"names that fit 8.3 should not produce LFN entries"
);
}
#[cfg(feature = "lfn")]
#[test]
fn test_lowercase_short_name_uses_nt_case_flags() {
use hadris_fat::raw::NtCaseFlags;
fn normalize_8_3(name: &str) -> String {
match name.split_once('.') {
Some((base, ext)) => {
let base = base.trim_end_matches(' ');
let ext = ext.trim_end_matches(' ');
if ext.is_empty() {
base.to_string()
} else {
format!("{base}.{ext}")
}
}
None => name.trim_end_matches(' ').to_string(),
}
}
let image = create_fat32_image();
let fs = FatVolume::open(image).expect("open");
for (name, expect_flags) in [
(
"readme.txt",
NtCaseFlags::LOWER_BASE | NtCaseFlags::LOWER_EXT,
),
("kernel.C", NtCaseFlags::LOWER_BASE),
("makefile", NtCaseFlags::LOWER_BASE),
("BOOT.BIN", NtCaseFlags::empty()),
] {
fs.create_file(&fs.root_dir(), name).expect("create");
let entry = fs
.root_dir()
.find(name)
.expect("find")
.expect("entry present");
assert!(
entry.long_name().is_none(),
"{name}: case-only differences must not spend an LFN entry"
);
assert_eq!(entry.nt_case(), expect_flags, "{name}: NT case flags");
assert_eq!(
normalize_8_3(&entry.name()),
name,
"{name}: display name round-trips case"
);
}
}
#[test]
fn test_fat16_allocate_and_extend_chain_links_clusters() {
use hadris_fat::Fat16;
use std::io::Cursor;
let mut rw = Cursor::new(vec![0u8; 512]);
let fat = Fat16::new(0, 512, 1, 32);
let first = fat.allocate_chain(&mut rw, 3, 2).expect("allocate_chain");
assert_eq!(first, 2, "first cluster starts at the hint");
assert_eq!(fat.next_cluster(&mut rw, 2).unwrap(), Some(3));
assert_eq!(fat.next_cluster(&mut rw, 3).unwrap(), Some(4));
assert_eq!(
fat.next_cluster(&mut rw, 4).unwrap(),
None,
"chain terminates"
);
let new_first = fat.extend_chain(&mut rw, 4, 2, 5).expect("extend_chain");
assert_eq!(
fat.next_cluster(&mut rw, 4).unwrap(),
Some(new_first as u32),
"old tail now links to the extension"
);
let mut count = 1;
let mut cur = first as u32;
while let Some(next) = fat.next_cluster(&mut rw, cur as usize).unwrap() {
count += 1;
cur = next;
}
assert_eq!(count, 5, "chain length after extend");
}
#[cfg(feature = "lfn")]
#[test]
fn test_mixed_case_short_name_falls_back_to_lfn() {
let image = create_fat32_image();
let fs = FatVolume::open(image).expect("open");
fs.create_file(&fs.root_dir(), "ReadMe.txt")
.expect("create");
let entry = fs
.root_dir()
.find("ReadMe.txt")
.expect("find")
.expect("entry");
let lfn = entry.long_name().expect("mixed case requires an LFN");
assert!(lfn.eq_str("ReadMe.txt"));
assert_eq!(&*entry.name(), "ReadMe.txt");
}
#[cfg(feature = "cache")]
#[test]
fn test_with_fat_cache_roundtrips_byte_identical() {
use hadris_fat::FatVolumeWriteExt;
let payload: Vec<u8> = (0..(cluster_size() * 3))
.map(|i| (i & 0xFF) as u8)
.collect();
let read_back = |with_cache: bool| -> Vec<u8> {
let image = create_fat32_image();
let fs = if with_cache {
FatVolume::builder(image)
.fat_cache(8)
.open()
.expect("builder open")
} else {
FatVolume::builder(image).open().expect("builder open")
};
let root = fs.root_dir();
let entry = fs.create_file(&root, "CACHE.DAT").expect("create");
{
let mut writer = fs.write_file(&entry).expect("writer");
writer.write(&payload).expect("write");
writer.finish().expect("finish");
}
let root = fs.root_dir();
let entry = root.find("CACHE.DAT").expect("find").expect("entry");
let mut reader = fs.read_file(&entry).expect("reader");
reader.read_to_vec().expect("read")
};
let without = read_back(false);
let with = read_back(true);
assert_eq!(
with, without,
"cache should not affect read/write semantics"
);
assert_eq!(with, payload, "round-trip mismatch with cache enabled");
}
#[test]
fn test_fsinfo_unknown_sentinels_mount_successfully() {
let image = create_fat32_image();
let mut bytes = image.into_inner();
let fsi = 512;
bytes[fsi + 488..fsi + 492].copy_from_slice(&0xFFFFFFFFu32.to_le_bytes());
bytes[fsi + 492..fsi + 496].copy_from_slice(&0xFFFFFFFFu32.to_le_bytes());
let fs =
FatVolume::open(Cursor::new(bytes)).expect("mount must succeed with unknown sentinels");
assert_eq!(
fs.free_cluster_count(),
None,
"free_cluster_count() should report None when FSInfo says unknown"
);
assert_eq!(
fs.next_free_cluster_hint(),
None,
"next_free_cluster_hint() should report None when FSInfo says unknown"
);
}
#[test]
fn test_truncated_image_returns_boot_sector_context() {
let tiny = Cursor::new(vec![0u8; 16]);
let err = FatVolume::open(tiny).expect_err("mount must fail on tiny image");
match &err {
Error::IoContext { op, .. } => {
assert!(
op.contains("boot sector"),
"expected op to mention 'boot sector', got {op:?}"
);
}
other => panic!("expected IoContext, got {other:?}"),
}
assert!(
format!("{err}").contains("boot sector"),
"Display should mention 'boot sector', got {err}"
);
}
#[test]
fn test_dir_iter_returns_cluster_loop_on_root_cycle() {
let image = create_fat32_image();
let mut bytes = image.into_inner();
patch_fat_entry(&mut bytes, 2, 3);
patch_fat_entry(&mut bytes, 3, 2);
let cs = cluster_size();
let cluster2_start = data_start_bytes();
for i in 0..(cs / 32) {
bytes[cluster2_start + i * 32] = 0xE5;
}
let cluster3_start = data_start_bytes() + cs;
for i in 0..(cs / 32) {
bytes[cluster3_start + i * 32] = 0xE5;
}
let fs = FatVolume::open(Cursor::new(bytes)).expect("mount");
let root = fs.root_dir();
let mut saw_loop = false;
for entry in root.entries() {
if let Err(Error::ClusterLoop { .. }) = entry {
saw_loop = true;
break;
}
}
assert!(saw_loop, "expected directory iter to surface ClusterLoop");
}
}
#[cfg(all(feature = "std", feature = "lfn"))]
mod lfn_write_edge_tests {
use super::fat32_image::{create_fat32_image, data_start_bytes};
use hadris_fat::raw::{DirEntryAttrFlags, RawDirectoryEntry};
use hadris_fat::{Error, FatVolume};
use std::io::Cursor;
const LFN_ATTR: u8 = DirEntryAttrFlags::LONG_NAME.bits();
fn fresh_fat32_bytes() -> Vec<u8> {
create_fat32_image().into_inner()
}
fn read_root_slot(bytes: &[u8], i: usize) -> [u8; 32] {
let pos = data_start_bytes() + i * 32;
bytes[pos..pos + 32].try_into().unwrap()
}
fn lfn_unit(slot: &[u8; 32], n: usize) -> u16 {
let off = match n {
0..=4 => 1 + n * 2,
5..=10 => 14 + (n - 5) * 2,
11..=12 => 28 + (n - 11) * 2,
_ => panic!("LFN unit index out of range: {n}"),
};
u16::from_le_bytes([slot[off], slot[off + 1]])
}
fn short_checksum(name: &[u8; 11]) -> u8 {
let mut sum: u8 = 0;
for &b in name {
sum = sum.rotate_right(1).wrapping_add(b);
}
sum
}
#[test]
fn lfn_supplementary_plane_emoji_roundtrips() {
let mut bytes = fresh_fat32_bytes();
let long_name = "Star \u{1F31F} Notes.txt";
{
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::open(cursor).expect("open");
fs.create_file(&fs.root_dir(), long_name).expect("create");
}
let cursor = Cursor::new(&bytes[..]);
let fs = FatVolume::open(cursor).expect("re-open");
let entry = fs.root_dir().find(long_name).expect("find").expect("entry");
let lfn = entry.long_name().expect("LFN required");
assert!(lfn.eq_str(long_name), "LFN must round-trip surrogate pairs");
}
#[test]
fn lfn_overlong_name_returns_invalid_filename() {
let mut bytes = fresh_fat32_bytes();
let long: String = std::iter::repeat_n('a', 256).collect();
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::open(cursor).expect("open");
match fs.create_file(&fs.root_dir(), &long) {
Err(Error::InvalidFilename) => {}
Err(other) => panic!("expected InvalidFilename, got {other:?}"),
Ok(_) => panic!("expected error for 256-unit name"),
}
}
#[test]
fn lfn_long_name_15_entries_exact_fill_roundtrips() {
let mut bytes = fresh_fat32_bytes();
let long: String = std::iter::repeat_n('a', 195).collect();
{
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::open(cursor).expect("open");
fs.create_file(&fs.root_dir(), &long)
.expect("create 195-char name");
}
let slot0 = read_root_slot(&bytes, 0);
assert_eq!(slot0[11], LFN_ATTR);
for i in 0..13 {
let unit = lfn_unit(&slot0, i);
assert_eq!(
unit, b'a' as u16,
"exact-fill last entry must contain only real chars; unit {i} was {unit:#06x}"
);
}
let cursor = Cursor::new(&bytes[..]);
let fs = FatVolume::open(cursor).expect("re-open");
let entry = fs.root_dir().find(&long).expect("find").expect("entry");
assert!(entry.long_name().expect("lfn").eq_str(&long));
}
#[test]
fn lfn_run_crosses_cluster_boundary() {
let mut bytes = fresh_fat32_bytes();
let too_long: String = std::iter::repeat_n('a', 208).collect();
{
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::open(cursor).expect("open");
fs.create_file(&fs.root_dir(), &too_long)
.expect("cross-cluster LFN create");
}
let cursor = Cursor::new(&bytes[..]);
let fs = FatVolume::open(cursor).expect("re-open");
let entry = fs.root_dir().find(&too_long).expect("find").expect("entry");
assert!(entry.long_name().expect("lfn").eq_str(&too_long));
}
#[test]
fn lfn_padding_uses_terminator_then_filler() {
let mut bytes = fresh_fat32_bytes();
let name = "longishname.tx"; {
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::open(cursor).expect("open");
fs.create_file(&fs.root_dir(), name).expect("create");
}
let slot0 = read_root_slot(&bytes, 0);
assert_eq!(slot0[11], LFN_ATTR, "slot 0 must be an LFN entry");
assert_eq!(lfn_unit(&slot0, 0), b'x' as u16);
assert_eq!(lfn_unit(&slot0, 1), 0x0000, "expected terminator at unit 1");
for i in 2..13 {
assert_eq!(
lfn_unit(&slot0, i),
0xFFFF,
"expected 0xFFFF filler at unit {i}, got {:#06x}",
lfn_unit(&slot0, i)
);
}
}
#[test]
fn lfn_checksum_matches_short_name() {
let mut bytes = fresh_fat32_bytes();
let name = "Mixed-Case Name.dat";
{
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::open(cursor).expect("open");
fs.create_file(&fs.root_dir(), name).expect("create");
}
let mut lfn_checksums = vec![];
let mut short_name_bytes = [0u8; 11];
for i in 0..21 {
let slot = read_root_slot(&bytes, i);
if slot[11] == LFN_ATTR {
lfn_checksums.push(slot[13]);
} else if slot[0] != 0x00 && slot[0] != 0xE5 {
short_name_bytes.copy_from_slice(&slot[0..11]);
break;
}
}
assert!(
!lfn_checksums.is_empty(),
"expected LFN entries for {name:?}"
);
let expected = short_checksum(&short_name_bytes);
for (i, c) in lfn_checksums.iter().enumerate() {
assert_eq!(
*c, expected,
"LFN slot {i} checksum {c:#04x} != short-name checksum {expected:#04x}"
);
}
}
#[test]
fn lfn_create_then_delete_marks_all_lfn_slots() {
let mut bytes = fresh_fat32_bytes();
let name = "Long Mixed Notes.txt"; {
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::open(cursor).expect("open");
let _entry = fs.create_file(&fs.root_dir(), name).expect("create");
let entry = fs.root_dir().find(name).expect("find").expect("entry");
fs.delete(&entry).expect("delete");
}
for i in 0..21 {
let slot = read_root_slot(&bytes, i);
if slot[0] == 0x00 {
break;
}
assert_eq!(
slot[0], 0xE5,
"slot {i} should be marked deleted (got first byte {:#04x})",
slot[0]
);
}
}
#[test]
fn lfn_find_does_not_resurrect_deleted_long_name() {
let mut bytes = fresh_fat32_bytes();
let name = "Long Mixed Notes.txt";
{
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::open(cursor).expect("open");
let _ = fs.create_file(&fs.root_dir(), name).expect("create");
let entry = fs.root_dir().find(name).expect("find").expect("entry");
fs.delete(&entry).expect("delete");
}
let cursor = Cursor::new(&bytes[..]);
let fs = FatVolume::open(cursor).expect("re-open");
assert!(
fs.root_dir().find(name).expect("find").is_none(),
"deleted long-name file must not be findable"
);
}
#[test]
fn raw_directory_entry_is_32_bytes() {
assert_eq!(core::mem::size_of::<RawDirectoryEntry>(), 32);
}
}
#[cfg(feature = "std")]
mod fs_metadata_tests {
use super::fat32_image::{
create_fat32_image, data_start_bytes, fat_start_bytes, fat2_start_bytes,
};
use hadris_fat::FatVolume;
use hadris_fat::oem::Cp437OemCpConverter;
use std::io::Cursor;
fn patch_fat1_both_copies(bytes: &mut [u8], value: u32) {
let v = value.to_le_bytes();
let off1 = fat_start_bytes() + 4; let off2 = fat2_start_bytes() + 4; bytes[off1..off1 + 4].copy_from_slice(&v);
bytes[off2..off2 + 4].copy_from_slice(&v);
}
#[test]
fn read_root_label_returns_none_when_no_label_entry() {
let bytes = create_fat32_image().into_inner();
let fs = FatVolume::open(Cursor::new(bytes)).expect("open");
assert!(
fs.read_root_label().expect("read_root_label ok").is_none(),
"default fixture has no root label entry"
);
}
#[test]
fn set_root_label_then_read_root_label_round_trips() {
let mut bytes = create_fat32_image().into_inner();
let dir = data_start_bytes();
bytes[dir..dir + 11].copy_from_slice(b"OLD_LABEL ");
bytes[dir + 11] = 0x08;
{
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::open(cursor).expect("open");
fs.set_root_label(b"NEW_LABEL ").expect("set_root_label");
}
assert_eq!(&bytes[dir..dir + 11], b"NEW_LABEL ");
let fs = FatVolume::open(Cursor::new(&bytes[..])).expect("re-open");
assert_eq!(
fs.read_root_label().expect("read_root_label").unwrap(),
*b"NEW_LABEL "
);
}
#[test]
fn read_status_flags_dirty_bit_surfaces_when_cleared() {
let mut bytes = create_fat32_image().into_inner();
patch_fat1_both_copies(&mut bytes, 0x0FFFFFFFu32 & !0x0800_0000);
let fs = FatVolume::open(Cursor::new(bytes)).expect("open");
let flags = fs.read_status_flags().expect("read_status_flags");
assert!(
flags.dirty,
"dirty bit cleared on disk must surface as dirty=true"
);
assert!(!flags.io_errors);
}
#[test]
fn read_status_flags_io_errors_bit_surfaces_when_cleared() {
let mut bytes = create_fat32_image().into_inner();
patch_fat1_both_copies(&mut bytes, 0x0FFFFFFFu32 & !0x0400_0000);
let fs = FatVolume::open(Cursor::new(bytes)).expect("open");
let flags = fs.read_status_flags().expect("read_status_flags");
assert!(!flags.dirty);
assert!(
flags.io_errors,
"io_errors bit cleared on disk must surface as io_errors=true"
);
}
#[test]
fn read_status_flags_both_bits_cleared_reports_both() {
let mut bytes = create_fat32_image().into_inner();
patch_fat1_both_copies(&mut bytes, 0x0FFFFFFFu32 & !0x0C00_0000);
let fs = FatVolume::open(Cursor::new(bytes)).expect("open");
let flags = fs.read_status_flags().expect("read_status_flags");
assert!(flags.dirty && flags.io_errors);
}
static CP437: Cp437OemCpConverter = Cp437OemCpConverter;
#[test]
fn cp437_oem_converter_encodes_latin_in_short_name() {
let mut bytes = create_fat32_image().into_inner();
{
let cursor = Cursor::new(&mut bytes[..]);
let fs = FatVolume::builder(cursor)
.oem_converter(&CP437)
.open()
.expect("open");
fs.create_file(&fs.root_dir(), "café.txt").expect("create");
}
let mut found = false;
for i in 0..16 {
let pos = data_start_bytes() + i * 32;
let attr = bytes[pos + 11];
if bytes[pos] == 0x00 || bytes[pos] == 0xE5 {
continue;
}
if attr == 0x0F {
continue; }
let third_byte = bytes[pos + 3];
assert_eq!(
third_byte, 0x82,
"with CP437 converter, name[3] should be CP437 'é' (0x82), got {third_byte:#04x}"
);
found = true;
break;
}
assert!(found, "expected to find a short entry for café.txt");
}
}
#[cfg(feature = "std")]
mod iocontext_tests {
use super::fat32_image::create_fat32_image;
use hadris_fat::{Error, FatVolume};
use std::io::Cursor;
#[test]
fn truncated_fsinfo_returns_iocontext_with_fsinfo_op() {
let bytes = create_fat32_image().into_inner();
let truncated = &bytes[..512];
let cursor = Cursor::new(truncated.to_vec());
let err = FatVolume::open(cursor).expect_err("must fail on missing FSInfo");
match &err {
Error::IoContext { op, sector, .. } => {
assert!(
op.contains("FSInfo"),
"expected op to mention 'FSInfo', got {op:?}"
);
assert_eq!(*sector, Some(1));
}
other => panic!("expected IoContext, got {other:?}"),
}
assert!(format!("{err}").contains("FSInfo"));
}
}
#[cfg(all(feature = "std", feature = "dirty-file-panic"))]
mod dirty_file_panic_tests {
use super::fat32_image::create_fat32_image;
use hadris_fat::{FatVolume, FatVolumeWriteExt};
use std::io::Cursor;
#[test]
#[should_panic]
fn dropping_writer_without_finish_panics() {
let bytes = create_fat32_image().into_inner();
let fs = FatVolume::open(Cursor::new(bytes)).expect("open");
let entry = fs.create_file(&fs.root_dir(), "TEST.TXT").expect("create");
let mut writer = fs.write_file(&entry).expect("writer");
writer.write(b"oops").expect("write");
}
#[test]
fn calling_finish_does_not_panic() {
let bytes = create_fat32_image().into_inner();
let fs = FatVolume::open(Cursor::new(bytes)).expect("open");
let entry = fs.create_file(&fs.root_dir(), "OK.TXT").expect("create");
let mut writer = fs.write_file(&entry).expect("writer");
writer.write(b"clean exit").expect("write");
writer.finish().expect("finish");
}
}
#[cfg(all(feature = "std", feature = "lfn"))]
mod lfn_cluster_boundary_tests {
use hadris_fat::format::{FatFormatOptions, FatTypeSelection, FatVolumeFormatter};
use std::io::Cursor;
fn fat32_spc1_fs() -> hadris_fat::FatVolume<Cursor<Vec<u8>>> {
let size = 48 * 1024 * 1024;
let opts = FatFormatOptions::new(size as u64)
.fat_type(FatTypeSelection::Fat32)
.sectors_per_cluster(1)
.volume_label("SPC1");
FatVolumeFormatter::format(Cursor::new(vec![0u8; size]), opts).expect("format FAT32 spc=1")
}
#[test]
fn long_name_exceeding_one_cluster_roundtrips_and_deletes() {
let fs = fat32_spc1_fs();
let long_name = format!("{}.txt", "A".repeat(200));
let entry = fs
.create_file(&fs.root_dir(), &long_name)
.expect("a 17-entry run should span two clusters");
assert!(
fs.root_dir().find(&long_name).expect("find").is_some(),
"cross-cluster name must round-trip"
);
fs.delete(&entry).expect("delete cross-cluster LFN run");
fs.create_file(&fs.root_dir(), "OK.TXT")
.expect("short-name create must succeed after deletion");
let root = fs.root_dir();
let mut names = Vec::new();
let mut it = root.entries();
while let Some(Ok(entry)) = it.next_entry() {
names.push(entry.name().replace(' ', ""));
}
assert!(
names.iter().any(|n| n == "OK.TXT"),
"OK.TXT should be listed (directory intact): {names:?}",
);
assert!(
!names.iter().any(|n| n.starts_with("AAAA")),
"the deleted long name must leave no visible entries behind: {names:?}",
);
}
#[test]
fn maximum_length_name_spans_clusters() {
let fs = fat32_spc1_fs();
let name: String = std::iter::repeat_n('m', 255).collect();
let entry = fs
.create_file(&fs.root_dir(), &name)
.expect("255-unit LFN should use 20 LFN slots plus one short slot");
assert!(
fs.root_dir().find(&name).expect("find").is_some(),
"maximum-length name must round-trip"
);
fs.delete(&entry).expect("delete maximum-length name");
assert!(
fs.root_dir()
.find(&name)
.expect("find after delete")
.is_none(),
"maximum-length name must be fully removed"
);
}
#[test]
fn create_directory_run_crosses_cluster_boundary() {
let fs = fat32_spc1_fs();
for i in 0..15 {
fs.create_file(&fs.root_dir(), &format!("F{i:02}.TXT"))
.expect("fill root slot");
}
let name = "Long Folder Name";
fs.create_dir(&fs.root_dir(), name)
.expect("directory LFN should cross into a new cluster");
let entry = fs
.root_dir()
.find(name)
.expect("find")
.expect("directory entry");
assert!(entry.is_directory());
fs.delete(&entry).expect("delete cross-cluster directory");
assert!(
fs.root_dir()
.find(name)
.expect("find after delete")
.is_none()
);
}
#[test]
fn rename_run_crosses_cluster_boundary() {
let fs = fat32_spc1_fs();
let source = fs
.create_file(&fs.root_dir(), "SOURCE.TXT")
.expect("create source");
for i in 0..14 {
fs.create_file(&fs.root_dir(), &format!("F{i:02}.TXT"))
.expect("fill root slot");
}
let new_name = "Renamed Across Boundary.txt";
let renamed = fs
.rename(&source, &fs.root_dir(), new_name)
.expect("rename LFN should cross into a new cluster");
assert!(
fs.root_dir()
.find("SOURCE.TXT")
.expect("find old")
.is_none()
);
assert!(fs.root_dir().find(new_name).expect("find new").is_some());
fs.delete(&renamed).expect("delete renamed entry");
}
}