pub mod boot;
pub mod dir;
pub mod fat;
pub mod format;
pub mod upcase;
use boot::BootSector;
use dir::{ENTRY_SIZE, FileEntrySet, RawSlot};
use fat::Fat;
use upcase::Upcase;
use crate::Result;
use crate::block::BlockDevice;
pub use format::FormatOpts;
const SCRATCH_BUF_BYTES: usize = 64 * 1024;
struct RootMetadata {
volume_label: String,
upcase: Upcase,
bitmap_info: Option<(u32, u64)>,
}
pub struct Exfat {
boot: BootSector,
fat: Fat,
upcase: Upcase,
volume_label: String,
bitmap: Vec<u8>,
bitmap_first_cluster: u32,
bitmap_data_length: u64,
next_free_hint: u32,
fat_dirty: bool,
bitmap_dirty: bool,
}
impl Exfat {
pub fn open(dev: &mut dyn BlockDevice) -> Result<Self> {
let mut bs = [0u8; boot::BOOT_SECTOR_PARSE_SIZE];
dev.read_at(0, &mut bs)?;
let boot = BootSector::decode(&bs)?;
let fat_off = boot.fat_byte_offset();
let fat_len = boot.fat_byte_length() as usize;
if fat_len == 0 {
return Err(crate::Error::InvalidImage(
"exfat: FatLength is zero".into(),
));
}
let mut fat_bytes = vec![0u8; fat_len];
dev.read_at(fat_off, &mut fat_bytes)?;
let fat = Fat::decode(&fat_bytes);
let mut tmp = Self {
boot,
fat,
upcase: Upcase::default(),
volume_label: String::new(),
bitmap: Vec::new(),
bitmap_first_cluster: 0,
bitmap_data_length: 0,
next_free_hint: 2,
fat_dirty: false,
bitmap_dirty: false,
};
let root_bytes = tmp.read_chain_bytes(
dev,
tmp.boot.first_cluster_of_root_directory,
false,
None,
)?;
let RootMetadata {
volume_label,
upcase,
bitmap_info,
} = tmp.scan_root_metadata(dev, &root_bytes)?;
tmp.upcase = upcase;
tmp.volume_label = volume_label;
if let Some((first_cluster, data_length)) = bitmap_info {
let bm = tmp.read_chain_bytes(dev, first_cluster, false, Some(data_length))?;
tmp.bitmap_first_cluster = first_cluster;
tmp.bitmap_data_length = data_length;
tmp.bitmap = bm;
}
Ok(tmp)
}
pub fn format(dev: &mut dyn BlockDevice, opts: &FormatOpts) -> Result<Self> {
let geom = format::compute_geometry(dev.total_size(), opts)?;
let ss = geom.bytes_per_sector as usize;
format::write_boot_region(dev, &geom, 0)?;
format::write_boot_region(dev, &geom, 12 * ss as u64)?;
let mut fat = Fat::new_blank(geom.cluster_count as usize + 2);
const CL_BITMAP: u32 = 2;
const CL_UPCASE: u32 = 3;
const CL_ROOT: u32 = 4;
fat.set_raw(CL_BITMAP, fat::EOC);
fat.set_raw(CL_UPCASE, fat::EOC);
fat.set_raw(CL_ROOT, fat::EOC);
let bitmap_byte_len = (geom.cluster_count as u64).div_ceil(8);
let mut bitmap = vec![0u8; bitmap_byte_len as usize];
set_bitmap_bit(&mut bitmap, CL_BITMAP, true);
set_bitmap_bit(&mut bitmap, CL_UPCASE, true);
set_bitmap_bit(&mut bitmap, CL_ROOT, true);
let (upcase_bytes, upcase_csum) = format::make_ascii_upcase_table();
let upcase = Upcase::decode(&upcase_bytes, upcase_bytes.len() as u64)?;
let mut root = Vec::new();
if !opts.volume_label.is_empty() {
root.extend_from_slice(&format::make_volume_label_entry(&opts.volume_label));
}
root.extend_from_slice(&format::make_bitmap_entry(CL_BITMAP, bitmap_byte_len));
root.extend_from_slice(&format::make_upcase_entry(
upcase_csum,
CL_UPCASE,
upcase_bytes.len() as u64,
));
let fat_bytes = fat.encode();
let fat_byte_off = geom.fat_byte_offset();
let fat_byte_len = geom.fat_byte_length() as usize;
let mut fat_image = vec![0u8; fat_byte_len];
let n_copy = fat_bytes.len().min(fat_byte_len);
fat_image[..n_copy].copy_from_slice(&fat_bytes[..n_copy]);
dev.write_at(fat_byte_off, &fat_image)?;
let bpc = geom.bytes_per_cluster as usize;
let bm_off = geom.cluster_byte_offset(CL_BITMAP);
let up_off = geom.cluster_byte_offset(CL_UPCASE);
let root_off = geom.cluster_byte_offset(CL_ROOT);
let zero_cluster = vec![0u8; bpc];
dev.write_at(bm_off, &zero_cluster)?;
dev.write_at(up_off, &zero_cluster)?;
dev.write_at(root_off, &zero_cluster)?;
dev.write_at(bm_off, &bitmap)?;
dev.write_at(up_off, &upcase_bytes)?;
dev.write_at(root_off, &root)?;
let mut bs_buf = [0u8; boot::BOOT_SECTOR_PARSE_SIZE];
let mb = format::make_main_boot_sector(&geom, boot::BOOT_SECTOR_PARSE_SIZE);
bs_buf.copy_from_slice(&mb[..boot::BOOT_SECTOR_PARSE_SIZE]);
let boot = BootSector::decode(&bs_buf)?;
Ok(Self {
boot,
fat,
upcase,
volume_label: opts.volume_label.clone(),
bitmap,
bitmap_first_cluster: CL_BITMAP,
bitmap_data_length: bitmap_byte_len,
next_free_hint: 5, fat_dirty: false,
bitmap_dirty: false,
})
}
pub fn total_bytes(&self) -> u64 {
self.boot.volume_length * self.boot.bytes_per_sector() as u64
}
pub fn cluster_size(&self) -> u32 {
self.boot.bytes_per_cluster()
}
pub fn sectors_per_cluster(&self) -> u32 {
self.boot.sectors_per_cluster()
}
pub fn root_directory_cluster(&self) -> u32 {
self.boot.first_cluster_of_root_directory
}
pub fn volume_label(&self) -> &str {
&self.volume_label
}
pub fn list_path(
&self,
dev: &mut dyn BlockDevice,
path: &str,
) -> Result<Vec<crate::fs::DirEntry>> {
let dir_cluster = self.resolve_dir(dev, path)?;
let bytes = self.read_chain_bytes(dev, dir_cluster, false, None)?;
let mut out = Vec::new();
for entry in iter_file_sets(&bytes)? {
let kind = if entry.is_directory {
crate::fs::EntryKind::Dir
} else {
crate::fs::EntryKind::Regular
};
out.push(crate::fs::DirEntry {
name: entry.name,
inode: entry.first_cluster,
kind,
size: if entry.is_directory {
0
} else {
entry.valid_data_length
},
});
}
Ok(out)
}
pub fn open_file_reader<'a>(
&self,
dev: &'a mut dyn BlockDevice,
path: &str,
) -> Result<ExfatFileReader<'a>> {
let (entry, _parent_cluster) = self.resolve_entry(dev, path)?;
if entry.is_directory {
return Err(crate::Error::InvalidArgument(format!(
"exfat: {path:?} is a directory, not a file"
)));
}
let cluster_bytes = self.boot.bytes_per_cluster() as u64;
let chain =
self.build_data_chain(entry.first_cluster, entry.no_fat_chain(), entry.data_length)?;
let remaining = entry.valid_data_length;
Ok(ExfatFileReader {
dev,
chain,
cluster_heap_offset: self.boot.cluster_heap_byte_offset(),
cluster_bytes,
remaining,
cluster_idx: 0,
cluster_off: 0,
})
}
fn resolve_dir(&self, dev: &mut dyn BlockDevice, path: &str) -> Result<u32> {
let mut cluster = self.boot.first_cluster_of_root_directory;
for part in split_path(path) {
let bytes = self.read_chain_bytes(dev, cluster, false, None)?;
let entries = iter_file_sets(&bytes)?;
let next = entries
.into_iter()
.find(|e| self.name_matches(&e.name_utf16, part))
.ok_or_else(|| {
crate::Error::InvalidArgument(format!(
"exfat: no such entry {part:?} under {path:?}"
))
})?;
if !next.is_directory {
return Err(crate::Error::InvalidArgument(format!(
"exfat: {part:?} is not a directory"
)));
}
cluster = next.first_cluster;
}
Ok(cluster)
}
fn resolve_entry(&self, dev: &mut dyn BlockDevice, path: &str) -> Result<(FileEntrySet, u32)> {
let parts = split_path(path);
if parts.is_empty() {
return Err(crate::Error::InvalidArgument(
"exfat: cannot resolve root as a file entry".into(),
));
}
let mut cluster = self.boot.first_cluster_of_root_directory;
let (last, prefix) = parts.split_last().unwrap();
for part in prefix {
let bytes = self.read_chain_bytes(dev, cluster, false, None)?;
let entries = iter_file_sets(&bytes)?;
let next = entries
.into_iter()
.find(|e| self.name_matches(&e.name_utf16, part))
.ok_or_else(|| {
crate::Error::InvalidArgument(format!(
"exfat: no such entry {part:?} under {path:?}"
))
})?;
if !next.is_directory {
return Err(crate::Error::InvalidArgument(format!(
"exfat: {part:?} is not a directory"
)));
}
cluster = next.first_cluster;
}
let bytes = self.read_chain_bytes(dev, cluster, false, None)?;
let entries = iter_file_sets(&bytes)?;
let found = entries
.into_iter()
.find(|e| self.name_matches(&e.name_utf16, last))
.ok_or_else(|| {
crate::Error::InvalidArgument(format!(
"exfat: no such entry {last:?} under {path:?}"
))
})?;
Ok((found, cluster))
}
fn name_matches(&self, on_disk: &[u16], query: &str) -> bool {
let q: Vec<u16> = query.encode_utf16().collect();
self.upcase.eq_ignore_case(on_disk, &q)
}
fn build_data_chain(
&self,
first_cluster: u32,
no_fat_chain: bool,
data_length: u64,
) -> Result<Vec<u32>> {
if data_length == 0 {
return Ok(Vec::new());
}
let cb = self.boot.bytes_per_cluster() as u64;
let n = data_length.div_ceil(cb) as u32;
if no_fat_chain {
return Ok((0..n).map(|i| first_cluster + i).collect());
}
let chain = self.fat.chain(first_cluster)?;
if (chain.len() as u32) < n {
return Err(crate::Error::InvalidImage(format!(
"exfat: FAT chain has {} clusters but file needs {n}",
chain.len()
)));
}
Ok(chain.into_iter().take(n as usize).collect())
}
fn read_chain_bytes(
&self,
dev: &mut dyn BlockDevice,
first_cluster: u32,
no_fat_chain: bool,
hint_byte_len: Option<u64>,
) -> Result<Vec<u8>> {
if first_cluster == 0 {
return Ok(Vec::new());
}
let cb = self.boot.bytes_per_cluster() as u64;
let chain = if no_fat_chain {
let n = match hint_byte_len {
Some(b) if b > 0 => b.div_ceil(cb) as u32,
_ => 1,
};
(0..n).map(|i| first_cluster + i).collect()
} else {
self.fat.chain(first_cluster)?
};
let total = chain.len() as u64 * cb;
let out_len = match hint_byte_len {
Some(b) => b.min(total),
None => total,
};
let mut out = vec![0u8; out_len as usize];
let mut pos = 0usize;
for &c in &chain {
if pos >= out.len() {
break;
}
let take = (out.len() - pos).min(cb as usize);
let off = self.boot.cluster_byte_offset(c);
dev.read_at(off, &mut out[pos..pos + take])?;
pos += take;
}
Ok(out)
}
fn scan_root_metadata(
&self,
dev: &mut dyn BlockDevice,
root_bytes: &[u8],
) -> Result<RootMetadata> {
let mut volume_label = String::new();
let mut upcase = Upcase::ascii();
let mut bitmap_info: Option<(u32, u64)> = None;
let mut i = 0;
while i + ENTRY_SIZE <= root_bytes.len() {
let slot: &[u8; ENTRY_SIZE] = (&root_bytes[i..i + ENTRY_SIZE]).try_into().unwrap();
match dir::classify_slot(slot) {
RawSlot::EndOfDirectory => break,
RawSlot::Unused => {
i += ENTRY_SIZE;
}
RawSlot::VolumeLabel(units) => {
volume_label = dir::decode_volume_label(&units);
i += ENTRY_SIZE;
}
RawSlot::UpcaseTable {
first_cluster,
data_length,
..
} => {
let raw = self.read_chain_bytes(
dev,
first_cluster,
false,
Some(data_length),
)?;
upcase = match Upcase::decode(&raw, data_length) {
Ok(u) => u,
Err(_) => Upcase::ascii(),
};
i += ENTRY_SIZE;
}
RawSlot::AllocationBitmap {
first_cluster,
data_length,
..
} => {
bitmap_info = Some((first_cluster, data_length));
i += ENTRY_SIZE;
}
RawSlot::File {
secondary_count, ..
} => {
i += (1 + secondary_count as usize) * ENTRY_SIZE;
}
RawSlot::Other { .. } => {
i += ENTRY_SIZE;
}
}
}
Ok(RootMetadata {
volume_label,
upcase,
bitmap_info,
})
}
fn alloc_cluster(&mut self) -> Result<u32> {
let max = (self.boot.cluster_count + 2) as usize;
let start = self.next_free_hint.max(2) as usize;
for cluster in start..max {
if self.fat.raw(cluster as u32) == Some(fat::FREE) {
self.fat.set_raw(cluster as u32, fat::EOC);
self.fat_dirty = true;
set_bitmap_bit(&mut self.bitmap, cluster as u32, true);
self.bitmap_dirty = true;
self.next_free_hint = (cluster as u32).saturating_add(1);
return Ok(cluster as u32);
}
}
for cluster in 2..start {
if self.fat.raw(cluster as u32) == Some(fat::FREE) {
self.fat.set_raw(cluster as u32, fat::EOC);
self.fat_dirty = true;
set_bitmap_bit(&mut self.bitmap, cluster as u32, true);
self.bitmap_dirty = true;
self.next_free_hint = (cluster as u32).saturating_add(1);
return Ok(cluster as u32);
}
}
Err(crate::Error::InvalidArgument(
"exfat: out of clusters".into(),
))
}
fn alloc_chain(&mut self, n: u32) -> Result<u32> {
if n == 0 {
return Err(crate::Error::InvalidArgument(
"exfat: alloc_chain(0)".into(),
));
}
let first = self.alloc_cluster()?;
let mut prev = first;
for _ in 1..n {
let next = self.alloc_cluster()?;
self.fat.set_raw(prev, next);
prev = next;
}
Ok(first)
}
fn free_chain(&mut self, first_cluster: u32) -> Result<()> {
if first_cluster < 2 {
return Ok(());
}
let max = self.boot.cluster_count + 2;
let mut cur = first_cluster;
let mut steps = 0u64;
loop {
if cur < 2 || cur >= max {
break;
}
steps += 1;
if steps > self.boot.cluster_count as u64 + 2 {
return Err(crate::Error::InvalidImage(
"exfat: cluster chain loops while freeing".into(),
));
}
let raw = self.fat.raw(cur).unwrap_or(fat::FREE);
self.fat.set_raw(cur, fat::FREE);
set_bitmap_bit(&mut self.bitmap, cur, false);
if self.next_free_hint > cur {
self.next_free_hint = cur;
}
match fat::classify(raw) {
fat::FatEntry::Eoc | fat::FatEntry::Free | fat::FatEntry::Bad => break,
fat::FatEntry::Next(n) => cur = n,
}
}
self.fat_dirty = true;
self.bitmap_dirty = true;
Ok(())
}
fn dir_chain(&self, first_cluster: u32) -> Result<Vec<u32>> {
self.fat.chain(first_cluster)
}
fn read_dir_bytes(&self, dev: &mut dyn BlockDevice, first_cluster: u32) -> Result<Vec<u8>> {
self.read_chain_bytes(dev, first_cluster, false, None)
}
fn split_path_for_create<'p>(
&self,
dev: &mut dyn BlockDevice,
path: &'p str,
) -> Result<(u32, &'p str)> {
let parts = split_path(path);
if parts.is_empty() {
return Err(crate::Error::InvalidArgument(
"exfat: cannot create root".into(),
));
}
let (last, prefix) = parts.split_last().unwrap();
let mut cluster = self.boot.first_cluster_of_root_directory;
for part in prefix {
let bytes = self.read_dir_bytes(dev, cluster)?;
let next = iter_file_sets(&bytes)?
.into_iter()
.find(|e| self.name_matches(&e.name_utf16, part))
.ok_or_else(|| {
crate::Error::InvalidArgument(format!(
"exfat: no such entry {part:?} under {path:?}"
))
})?;
if !next.is_directory {
return Err(crate::Error::InvalidArgument(format!(
"exfat: {part:?} is not a directory"
)));
}
cluster = next.first_cluster;
}
Ok((cluster, last))
}
fn append_to_directory(
&mut self,
dev: &mut dyn BlockDevice,
first_cluster: u32,
entry_set_bytes: &[u8],
) -> Result<()> {
let cb = self.boot.bytes_per_cluster() as usize;
let need_slots = entry_set_bytes.len() / ENTRY_SIZE;
if entry_set_bytes.len() % ENTRY_SIZE != 0 || need_slots == 0 {
return Err(crate::Error::InvalidArgument(
"exfat: entry set must be a non-empty multiple of 32 bytes".into(),
));
}
let chain = self.dir_chain(first_cluster)?;
for &cluster in &chain {
let off = self.boot.cluster_byte_offset(cluster);
let mut buf = vec![0u8; cb];
dev.read_at(off, &mut buf)?;
let mut i = 0;
while i + need_slots * ENTRY_SIZE <= cb {
let mut all_free = true;
for j in 0..need_slots {
let slot_off = i + j * ENTRY_SIZE;
let t = buf[slot_off];
if t != 0x00 && t & dir::ENTRY_INUSE != 0 {
all_free = false;
break;
}
}
if all_free {
buf[i..i + entry_set_bytes.len()].copy_from_slice(entry_set_bytes);
dev.write_at(off, &buf)?;
return Ok(());
}
let t = buf[i];
if t & dir::ENTRY_INUSE != 0 && t == dir::ENTRY_FILE {
let sec = buf[i + 1] as usize;
i += (1 + sec) * ENTRY_SIZE;
} else {
i += ENTRY_SIZE;
}
}
}
for &cluster in &chain {
let off = self.boot.cluster_byte_offset(cluster);
let mut buf = vec![0u8; cb];
dev.read_at(off, &mut buf)?;
let mut changed = false;
let mut i = 0;
while i + ENTRY_SIZE <= cb {
let t = buf[i];
if t == 0x00 {
buf[i] = 0x05; changed = true;
i += ENTRY_SIZE;
} else if t & dir::ENTRY_INUSE != 0 && t == dir::ENTRY_FILE {
let sec = buf[i + 1] as usize;
i += (1 + sec) * ENTRY_SIZE;
} else {
i += ENTRY_SIZE;
}
}
if changed {
dev.write_at(off, &buf)?;
}
}
let new_cluster = self.alloc_cluster()?;
let last = *chain.last().unwrap();
self.fat.set_raw(last, new_cluster);
self.fat_dirty = true;
let mut buf = vec![0u8; cb];
buf[..entry_set_bytes.len()].copy_from_slice(entry_set_bytes);
let off = self.boot.cluster_byte_offset(new_cluster);
dev.write_at(off, &buf)?;
Ok(())
}
fn find_entry_in_dir(
&self,
dev: &mut dyn BlockDevice,
first_cluster: u32,
name: &str,
) -> Result<Option<(u64, FileEntrySet, usize)>> {
let bytes = self.read_dir_bytes(dev, first_cluster)?;
let mut i = 0;
while i + ENTRY_SIZE <= bytes.len() {
let slot: &[u8; ENTRY_SIZE] = (&bytes[i..i + ENTRY_SIZE]).try_into().unwrap();
match dir::classify_slot(slot) {
RawSlot::EndOfDirectory => break,
RawSlot::Unused => {
i += ENTRY_SIZE;
}
RawSlot::File {
secondary_count, ..
} => {
let total = (1 + secondary_count as usize) * ENTRY_SIZE;
if i + total > bytes.len() {
break;
}
let set = dir::parse_file_set(&bytes[i..i + total])?;
if self.name_matches(&set.name_utf16, name) {
return Ok(Some((i as u64, set, total)));
}
i += total;
}
_ => {
i += ENTRY_SIZE;
}
}
}
Ok(None)
}
fn dir_pos_to_disk_offset(&self, first_cluster: u32, pos_in_dir: u64) -> Result<u64> {
let cb = self.boot.bytes_per_cluster() as u64;
let chain = self.dir_chain(first_cluster)?;
let cluster_idx = (pos_in_dir / cb) as usize;
let cluster_off = pos_in_dir % cb;
if cluster_idx >= chain.len() {
return Err(crate::Error::InvalidImage(
"exfat: dir position past chain".into(),
));
}
Ok(self.boot.cluster_byte_offset(chain[cluster_idx]) + cluster_off)
}
fn clear_entry_set(
&mut self,
dev: &mut dyn BlockDevice,
parent_first_cluster: u32,
pos_in_dir: u64,
total_bytes: usize,
) -> Result<()> {
let cb = self.boot.bytes_per_cluster() as u64;
let n_slots = total_bytes / ENTRY_SIZE;
let chain = self.dir_chain(parent_first_cluster)?;
for k in 0..n_slots {
let p = pos_in_dir + (k as u64) * ENTRY_SIZE as u64;
let cluster_idx = (p / cb) as usize;
let cluster_off = p % cb;
let cluster = chain[cluster_idx];
let disk_off = self.boot.cluster_byte_offset(cluster) + cluster_off;
let mut byte = [0u8; 1];
dev.read_at(disk_off, &mut byte)?;
byte[0] &= !dir::ENTRY_INUSE;
dev.write_at(disk_off, &byte)?;
}
Ok(())
}
fn stream_into_chain(
&self,
dev: &mut dyn BlockDevice,
chain: &[u32],
reader: &mut dyn std::io::Read,
total_len: u64,
) -> Result<()> {
let cb = self.boot.bytes_per_cluster() as u64;
let mut scratch = vec![0u8; SCRATCH_BUF_BYTES];
let mut remaining = total_len;
let mut cluster_idx = 0usize;
let mut cluster_off: u64 = 0;
while remaining > 0 {
if cluster_idx >= chain.len() {
return Err(crate::Error::InvalidImage(
"exfat: writer exhausted cluster chain".into(),
));
}
let want = (remaining as usize)
.min(scratch.len())
.min((cb - cluster_off) as usize);
let mut got = 0;
while got < want {
let n = reader
.read(&mut scratch[got..want])
.map_err(crate::Error::Io)?;
if n == 0 {
return Err(crate::Error::InvalidArgument(
"exfat: reader produced fewer bytes than declared length".into(),
));
}
got += n;
}
let disk_off = self.boot.cluster_byte_offset(chain[cluster_idx]) + cluster_off;
dev.write_at(disk_off, &scratch[..got])?;
remaining -= got as u64;
cluster_off += got as u64;
if cluster_off == cb {
cluster_idx += 1;
cluster_off = 0;
}
}
if !chain.is_empty() && cluster_off > 0 && cluster_off < cb {
let zeros = vec![0u8; (cb - cluster_off) as usize];
let disk_off = self.boot.cluster_byte_offset(chain[cluster_idx]) + cluster_off;
dev.write_at(disk_off, &zeros)?;
}
Ok(())
}
fn name_hash_for(&self, name: &str) -> u16 {
let units: Vec<u16> = name.encode_utf16().collect();
let upcased = self.upcase.up_slice(&units);
let mut bytes = Vec::with_capacity(upcased.len() * 2);
for u in &upcased {
bytes.extend_from_slice(&u.to_le_bytes());
}
dir::name_hash(&bytes)
}
pub fn create_file_in(
&mut self,
dev: &mut dyn BlockDevice,
dir_cluster: u32,
name: &str,
reader: &mut dyn std::io::Read,
data_length: u64,
timestamp: u32,
) -> Result<u32> {
if name.is_empty() || name.contains('/') || name.contains('\\') {
return Err(crate::Error::InvalidArgument(format!(
"exfat: invalid file name {name:?}"
)));
}
let cb = self.boot.bytes_per_cluster() as u64;
let (first_cluster, chain) = if data_length > 0 {
let n_clusters = data_length.div_ceil(cb) as u32;
let first = self.alloc_chain(n_clusters)?;
let chain = self.dir_chain(first)?;
(first, chain)
} else {
(0, Vec::new())
};
if data_length > 0 {
self.stream_into_chain(dev, &chain, reader, data_length)?;
}
let secondary_flags = if data_length == 0 {
0
} else {
dir::SECFLAG_ALLOC_POSSIBLE
};
let entry = format::make_file_entry_set(
name,
false,
secondary_flags,
first_cluster,
data_length,
data_length,
timestamp,
self.name_hash_for(name),
);
self.append_to_directory(dev, dir_cluster, &entry)?;
Ok(first_cluster)
}
pub fn create_file(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
reader: &mut dyn std::io::Read,
data_length: u64,
timestamp: u32,
) -> Result<u32> {
let (parent_cluster, name) = {
let (c, n) = self.split_path_for_create(dev, path)?;
(c, n.to_string())
};
self.create_file_in(dev, parent_cluster, &name, reader, data_length, timestamp)
}
pub fn create_dir_in(
&mut self,
dev: &mut dyn BlockDevice,
dir_cluster: u32,
name: &str,
timestamp: u32,
) -> Result<u32> {
if name.is_empty() || name.contains('/') || name.contains('\\') {
return Err(crate::Error::InvalidArgument(format!(
"exfat: invalid directory name {name:?}"
)));
}
let cb = self.boot.bytes_per_cluster();
let new_cluster = self.alloc_cluster()?;
let zeros = vec![0u8; cb as usize];
dev.write_at(self.boot.cluster_byte_offset(new_cluster), &zeros)?;
let entry = format::make_file_entry_set(
name,
true,
dir::SECFLAG_ALLOC_POSSIBLE,
new_cluster,
cb as u64,
cb as u64,
timestamp,
self.name_hash_for(name),
);
self.append_to_directory(dev, dir_cluster, &entry)?;
Ok(new_cluster)
}
pub fn create_dir(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
timestamp: u32,
) -> Result<u32> {
let (parent_cluster, name) = {
let (c, n) = self.split_path_for_create(dev, path)?;
(c, n.to_string())
};
self.create_dir_in(dev, parent_cluster, &name, timestamp)
}
pub fn remove(&mut self, dev: &mut dyn BlockDevice, path: &str) -> Result<()> {
let parts = split_path(path);
if parts.is_empty() {
return Err(crate::Error::InvalidArgument(
"exfat: cannot remove root".into(),
));
}
let (last, prefix) = parts.split_last().unwrap();
let mut parent_cluster = self.boot.first_cluster_of_root_directory;
for part in prefix {
let bytes = self.read_dir_bytes(dev, parent_cluster)?;
let next = iter_file_sets(&bytes)?
.into_iter()
.find(|e| self.name_matches(&e.name_utf16, part))
.ok_or_else(|| {
crate::Error::InvalidArgument(format!(
"exfat: no such entry {part:?} under {path:?}"
))
})?;
if !next.is_directory {
return Err(crate::Error::InvalidArgument(format!(
"exfat: {part:?} is not a directory"
)));
}
parent_cluster = next.first_cluster;
}
let (pos, set, total) = self
.find_entry_in_dir(dev, parent_cluster, last)?
.ok_or_else(|| {
crate::Error::InvalidArgument(format!(
"exfat: no such entry {last:?} under {path:?}"
))
})?;
if set.is_directory {
let bytes = self.read_dir_bytes(dev, set.first_cluster)?;
let mut i = 0;
let mut has_entries = false;
while i + ENTRY_SIZE <= bytes.len() {
let slot: &[u8; ENTRY_SIZE] = (&bytes[i..i + ENTRY_SIZE]).try_into().unwrap();
match dir::classify_slot(slot) {
RawSlot::EndOfDirectory => break,
RawSlot::Unused => {
i += ENTRY_SIZE;
}
RawSlot::File { .. } => {
has_entries = true;
break;
}
_ => i += ENTRY_SIZE,
}
}
if has_entries {
return Err(crate::Error::InvalidArgument(format!(
"exfat: directory {last:?} is not empty"
)));
}
}
let _ = self.dir_pos_to_disk_offset(parent_cluster, pos)?; self.clear_entry_set(dev, parent_cluster, pos, total)?;
if set.first_cluster >= 2 {
self.free_chain(set.first_cluster)?;
}
Ok(())
}
pub fn flush(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
if self.fat_dirty {
let fat_off = self.boot.fat_byte_offset();
let fat_byte_len = self.boot.fat_byte_length() as usize;
let mut buf = vec![0u8; fat_byte_len];
let enc = self.fat.encode();
let n = enc.len().min(fat_byte_len);
buf[..n].copy_from_slice(&enc[..n]);
dev.write_at(fat_off, &buf)?;
if self.boot.number_of_fats == 2 {
let backup_off = fat_off + fat_byte_len as u64;
if backup_off + fat_byte_len as u64
<= self.boot.volume_length * self.boot.bytes_per_sector() as u64
{
dev.write_at(backup_off, &buf)?;
}
}
self.fat_dirty = false;
}
if self.bitmap_dirty && self.bitmap_first_cluster >= 2 {
let chain = self.dir_chain(self.bitmap_first_cluster)?;
let cb = self.boot.bytes_per_cluster() as usize;
let bm = &self.bitmap;
let mut pos = 0usize;
for cluster in chain {
if pos >= bm.len() {
break;
}
let take = (bm.len() - pos).min(cb);
let mut chunk = vec![0u8; cb];
chunk[..take].copy_from_slice(&bm[pos..pos + take]);
dev.write_at(self.boot.cluster_byte_offset(cluster), &chunk)?;
pos += take;
}
self.bitmap_dirty = false;
}
dev.sync()?;
Ok(())
}
}
impl crate::fs::Filesystem for Exfat {
fn create_file(
&mut self,
_dev: &mut dyn BlockDevice,
_path: &std::path::Path,
_src: crate::fs::FileSource,
_meta: crate::fs::FileMeta,
) -> Result<()> {
Err(crate::Error::Unsupported(
"exfat: read-only on this trait surface".into(),
))
}
fn create_dir(
&mut self,
_dev: &mut dyn BlockDevice,
_path: &std::path::Path,
_meta: crate::fs::FileMeta,
) -> Result<()> {
Err(crate::Error::Unsupported(
"exfat: read-only on this trait surface".into(),
))
}
fn create_symlink(
&mut self,
_dev: &mut dyn BlockDevice,
_path: &std::path::Path,
_target: &std::path::Path,
_meta: crate::fs::FileMeta,
) -> Result<()> {
Err(crate::Error::Unsupported(
"exfat: read-only on this trait surface".into(),
))
}
fn create_device(
&mut self,
_dev: &mut dyn BlockDevice,
_path: &std::path::Path,
_kind: crate::fs::DeviceKind,
_major: u32,
_minor: u32,
_meta: crate::fs::FileMeta,
) -> Result<()> {
Err(crate::Error::Unsupported(
"exfat: read-only on this trait surface".into(),
))
}
fn remove(&mut self, _dev: &mut dyn BlockDevice, _path: &std::path::Path) -> Result<()> {
Err(crate::Error::Unsupported(
"exfat: read-only on this trait surface".into(),
))
}
fn list(
&mut self,
dev: &mut dyn BlockDevice,
path: &std::path::Path,
) -> Result<Vec<crate::fs::DirEntry>> {
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("exfat: non-UTF-8 path".into()))?;
Exfat::list_path(self, dev, s)
}
fn read_file<'a>(
&'a mut self,
dev: &'a mut dyn BlockDevice,
path: &std::path::Path,
) -> Result<Box<dyn std::io::Read + 'a>> {
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("exfat: non-UTF-8 path".into()))?;
let r = self.open_file_reader(dev, s)?;
Ok(Box::new(r))
}
fn flush(&mut self, _dev: &mut dyn BlockDevice) -> Result<()> {
Ok(())
}
fn supports_mutation(&self) -> bool {
false
}
}
fn set_bitmap_bit(bitmap: &mut [u8], cluster: u32, used: bool) {
if cluster < 2 {
return;
}
let bit = (cluster - 2) as usize;
let byte = bit / 8;
let mask = 1u8 << (bit % 8);
if byte >= bitmap.len() {
return;
}
if used {
bitmap[byte] |= mask;
} else {
bitmap[byte] &= !mask;
}
}
fn iter_file_sets(bytes: &[u8]) -> Result<Vec<FileEntrySet>> {
let mut out = Vec::new();
let mut i = 0;
while i + ENTRY_SIZE <= bytes.len() {
let slot: &[u8; ENTRY_SIZE] = (&bytes[i..i + ENTRY_SIZE]).try_into().unwrap();
match dir::classify_slot(slot) {
RawSlot::EndOfDirectory => break,
RawSlot::Unused => {
i += ENTRY_SIZE;
}
RawSlot::File {
secondary_count, ..
} => {
let total = (1 + secondary_count as usize) * ENTRY_SIZE;
if i + total > bytes.len() {
return Err(crate::Error::InvalidImage(
"exfat: file entry set runs past directory end".into(),
));
}
let set = dir::parse_file_set(&bytes[i..i + total])?;
out.push(set);
i += total;
}
_ => {
i += ENTRY_SIZE;
}
}
}
Ok(out)
}
fn split_path(path: &str) -> Vec<&str> {
path.split(['/', '\\'])
.filter(|p| !p.is_empty() && *p != ".")
.collect()
}
pub struct ExfatFileReader<'a> {
dev: &'a mut dyn BlockDevice,
chain: Vec<u32>,
cluster_heap_offset: u64,
cluster_bytes: u64,
remaining: u64,
cluster_idx: usize,
cluster_off: u64,
}
impl<'a> std::io::Read for ExfatFileReader<'a> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.remaining == 0 || self.cluster_idx >= self.chain.len() {
return Ok(0);
}
let avail_in_cluster = self.cluster_bytes - self.cluster_off;
let want = (buf.len() as u64).min(avail_in_cluster).min(self.remaining) as usize;
let cluster = self.chain[self.cluster_idx];
let cluster_start = self.cluster_heap_offset + (cluster as u64 - 2) * self.cluster_bytes;
let off = cluster_start + self.cluster_off;
self.dev
.read_at(off, &mut buf[..want])
.map_err(std::io::Error::other)?;
self.cluster_off += want as u64;
self.remaining -= want as u64;
if self.cluster_off == self.cluster_bytes {
self.cluster_idx += 1;
self.cluster_off = 0;
}
Ok(want)
}
}
pub fn probe(dev: &mut dyn BlockDevice) -> Result<bool> {
if dev.total_size() < 512 {
return Ok(false);
}
let mut head = [0u8; 16];
dev.read_at(0, &mut head)?;
Ok(&head[3..11] == b"EXFAT ")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::{BlockDevice, MemoryBackend};
const BPS_SHIFT: u8 = 9; const SPC_SHIFT: u8 = 3; const BPS: u32 = 1 << BPS_SHIFT;
const BPC: u32 = BPS << SPC_SHIFT;
const CL_BITMAP: u32 = 2;
const CL_UPCASE: u32 = 3;
const CL_ROOT: u32 = 4;
const CL_HELLO: u32 = 5;
const CL_SUB: u32 = 6;
const CL_XBIN: u32 = 7;
fn build_test_image() -> MemoryBackend {
const FAT_OFFSET_SECTORS: u32 = 64; const FAT_LENGTH_SECTORS: u32 = 8; const CLUSTER_HEAP_OFFSET_SECTORS: u32 = 128; const CLUSTER_COUNT: u32 = 32;
const VOLUME_LENGTH_SECTORS: u64 =
CLUSTER_HEAP_OFFSET_SECTORS as u64 + (CLUSTER_COUNT as u64 * (1u64 << SPC_SHIFT));
let total_bytes = VOLUME_LENGTH_SECTORS * BPS as u64;
let mut dev = MemoryBackend::new(total_bytes);
let mut bs = [0u8; 512];
bs[0..3].copy_from_slice(&[0xEB, 0x76, 0x90]);
bs[3..11].copy_from_slice(b"EXFAT ");
bs[64..72].copy_from_slice(&0u64.to_le_bytes());
bs[72..80].copy_from_slice(&VOLUME_LENGTH_SECTORS.to_le_bytes());
bs[80..84].copy_from_slice(&FAT_OFFSET_SECTORS.to_le_bytes());
bs[84..88].copy_from_slice(&FAT_LENGTH_SECTORS.to_le_bytes());
bs[88..92].copy_from_slice(&CLUSTER_HEAP_OFFSET_SECTORS.to_le_bytes());
bs[92..96].copy_from_slice(&CLUSTER_COUNT.to_le_bytes());
bs[96..100].copy_from_slice(&CL_ROOT.to_le_bytes());
bs[100..104].copy_from_slice(&0xCAFE_F00Du32.to_le_bytes());
bs[104..106].copy_from_slice(&0x0100u16.to_le_bytes());
bs[106..108].copy_from_slice(&0u16.to_le_bytes());
bs[108] = BPS_SHIFT;
bs[109] = SPC_SHIFT;
bs[110] = 1; bs[111] = 0x80;
bs[112] = 0;
bs[510] = 0x55;
bs[511] = 0xAA;
dev.write_at(0, &bs).unwrap();
let fat_bytes_len = FAT_LENGTH_SECTORS as usize * BPS as usize;
let mut fat = vec![0u8; fat_bytes_len];
let write_entry = |fat: &mut [u8], cluster: u32, value: u32| {
let off = cluster as usize * 4;
fat[off..off + 4].copy_from_slice(&value.to_le_bytes());
};
write_entry(&mut fat, 0, 0xFFFFFFF8);
write_entry(&mut fat, 1, 0xFFFFFFFF);
for c in [CL_BITMAP, CL_UPCASE, CL_ROOT, CL_HELLO, CL_SUB, CL_XBIN] {
write_entry(&mut fat, c, 0xFFFFFFFF);
}
let fat_off = FAT_OFFSET_SECTORS as u64 * BPS as u64;
dev.write_at(fat_off, &fat).unwrap();
let cluster_off = |c: u32| -> u64 {
CLUSTER_HEAP_OFFSET_SECTORS as u64 * BPS as u64 + (c as u64 - 2) * BPC as u64
};
let mut upcase = Vec::new();
for i in 0..0x80u16 {
let c = i as u8;
let v = if c.is_ascii_lowercase() {
(c - b'a' + b'A') as u16
} else {
i
};
upcase.extend_from_slice(&v.to_le_bytes());
}
let upcase_len = upcase.len() as u64;
let upcase_checksum = super::upcase::table_checksum(&upcase);
let mut upcase_cluster = vec![0u8; BPC as usize];
upcase_cluster[..upcase.len()].copy_from_slice(&upcase);
dev.write_at(cluster_off(CL_UPCASE), &upcase_cluster)
.unwrap();
let hello_text = b"Hello, exFAT!\n";
let mut hello_cluster = vec![0u8; BPC as usize];
hello_cluster[..hello_text.len()].copy_from_slice(hello_text);
dev.write_at(cluster_off(CL_HELLO), &hello_cluster).unwrap();
let xbin_data: [u8; 12] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
let mut xbin_cluster = vec![0u8; BPC as usize];
xbin_cluster[..xbin_data.len()].copy_from_slice(&xbin_data);
dev.write_at(cluster_off(CL_XBIN), &xbin_cluster).unwrap();
let sub_entries = build_dir_entries(&[("x.bin", false, CL_XBIN, xbin_data.len() as u64)]);
let mut sub_cluster = vec![0u8; BPC as usize];
sub_cluster[..sub_entries.len()].copy_from_slice(&sub_entries);
dev.write_at(cluster_off(CL_SUB), &sub_cluster).unwrap();
let mut root = Vec::new();
{
let label_units: Vec<u16> = "MYVOL".encode_utf16().collect();
let mut e = [0u8; ENTRY_SIZE];
e[0] = dir::ENTRY_VOLUME_LABEL;
e[1] = label_units.len() as u8;
for (i, &u) in label_units.iter().enumerate() {
let off = 2 + i * 2;
e[off..off + 2].copy_from_slice(&u.to_le_bytes());
}
root.extend_from_slice(&e);
}
{
let bitmap_bytes = (CLUSTER_COUNT as u64).div_ceil(8);
let mut e = [0u8; ENTRY_SIZE];
e[0] = dir::ENTRY_ALLOCATION_BITMAP;
e[1] = 0; e[20..24].copy_from_slice(&CL_BITMAP.to_le_bytes());
e[24..32].copy_from_slice(&bitmap_bytes.to_le_bytes());
root.extend_from_slice(&e);
}
{
let mut e = [0u8; ENTRY_SIZE];
e[0] = dir::ENTRY_UPCASE_TABLE;
e[4..8].copy_from_slice(&upcase_checksum.to_le_bytes());
e[20..24].copy_from_slice(&CL_UPCASE.to_le_bytes());
e[24..32].copy_from_slice(&upcase_len.to_le_bytes());
root.extend_from_slice(&e);
}
root.extend_from_slice(&build_dir_entries(&[("hello.txt", false, CL_HELLO, 14)]));
root.extend_from_slice(&build_dir_entries(&[("sub", true, CL_SUB, BPC as u64)]));
let mut root_cluster = vec![0u8; BPC as usize];
root_cluster[..root.len()].copy_from_slice(&root);
dev.write_at(cluster_off(CL_ROOT), &root_cluster).unwrap();
dev
}
fn build_dir_entries(items: &[(&str, bool, u32, u64)]) -> Vec<u8> {
let mut out = Vec::new();
for (name, is_dir, first_cluster, data_length) in items {
let name_units: Vec<u16> = name.encode_utf16().collect();
let n_name_entries = name_units.len().div_ceil(15).max(1);
let secondary_count = (1 + n_name_entries) as u8;
let attr = if *is_dir { dir::ATTR_DIRECTORY } else { 0 };
let mut primary = [0u8; ENTRY_SIZE];
primary[0] = dir::ENTRY_FILE;
primary[1] = secondary_count;
primary[4..6].copy_from_slice(&attr.to_le_bytes());
let mut stream = [0u8; ENTRY_SIZE];
stream[0] = dir::ENTRY_STREAM_EXTENSION;
stream[1] = dir::SECFLAG_ALLOC_POSSIBLE; stream[3] = name_units.len() as u8;
stream[8..16].copy_from_slice(&data_length.to_le_bytes()); stream[20..24].copy_from_slice(&first_cluster.to_le_bytes());
stream[24..32].copy_from_slice(&data_length.to_le_bytes());
let mut names: Vec<[u8; ENTRY_SIZE]> = Vec::new();
for chunk in name_units.chunks(15) {
let mut e = [0u8; ENTRY_SIZE];
e[0] = dir::ENTRY_FILE_NAME;
for (i, &u) in chunk.iter().enumerate() {
let off = 2 + i * 2;
e[off..off + 2].copy_from_slice(&u.to_le_bytes());
}
names.push(e);
}
let mut set = Vec::new();
set.extend_from_slice(&primary);
set.extend_from_slice(&stream);
for n in &names {
set.extend_from_slice(n);
}
let csum = dir::set_checksum(&set);
set[2..4].copy_from_slice(&csum.to_le_bytes());
out.extend_from_slice(&set);
}
out
}
#[test]
fn open_decodes_boot_and_metadata() {
let mut dev = build_test_image();
let fs = Exfat::open(&mut dev).unwrap();
assert_eq!(fs.cluster_size(), BPC);
assert_eq!(fs.sectors_per_cluster(), 8);
assert_eq!(fs.root_directory_cluster(), CL_ROOT);
assert_eq!(fs.volume_label(), "MYVOL");
assert!(fs.total_bytes() > 0);
}
#[test]
fn list_root_returns_files_and_dirs() {
let mut dev = build_test_image();
let fs = Exfat::open(&mut dev).unwrap();
let entries = fs.list_path(&mut dev, "/").unwrap();
let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
assert!(names.contains(&"hello.txt"));
assert!(names.contains(&"sub"));
let sub = entries.iter().find(|e| e.name == "sub").unwrap();
assert_eq!(sub.kind, crate::fs::EntryKind::Dir);
let hello = entries.iter().find(|e| e.name == "hello.txt").unwrap();
assert_eq!(hello.kind, crate::fs::EntryKind::Regular);
}
#[test]
fn list_subdirectory() {
let mut dev = build_test_image();
let fs = Exfat::open(&mut dev).unwrap();
let entries = fs.list_path(&mut dev, "/sub").unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "x.bin");
}
#[test]
fn case_insensitive_lookup() {
let mut dev = build_test_image();
let fs = Exfat::open(&mut dev).unwrap();
let entries = fs.list_path(&mut dev, "/SUB").unwrap();
assert_eq!(entries[0].name, "x.bin");
}
#[test]
fn read_file_returns_contents() {
use std::io::Read;
let mut dev = build_test_image();
let fs = Exfat::open(&mut dev).unwrap();
let mut r = fs.open_file_reader(&mut dev, "/hello.txt").unwrap();
let mut buf = Vec::new();
r.read_to_end(&mut buf).unwrap();
assert_eq!(buf, b"Hello, exFAT!\n");
}
#[test]
fn read_nested_file() {
use std::io::Read;
let mut dev = build_test_image();
let fs = Exfat::open(&mut dev).unwrap();
let mut r = fs.open_file_reader(&mut dev, "/sub/x.bin").unwrap();
let mut buf = Vec::new();
r.read_to_end(&mut buf).unwrap();
assert_eq!(buf, &[0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
}
#[test]
fn probe_recognises_image() {
let mut dev = build_test_image();
assert!(probe(&mut dev).unwrap());
}
#[test]
fn probe_rejects_non_exfat() {
let mut dev = MemoryBackend::new(4096);
assert!(!probe(&mut dev).unwrap());
}
use crate::fs::exfat::format::FormatOpts;
fn fresh_volume(label: &str) -> (MemoryBackend, Exfat) {
let mut dev = MemoryBackend::new(4 * 1024 * 1024);
let opts = FormatOpts {
bytes_per_sector_shift: 9,
sectors_per_cluster_shift: 3,
volume_serial_number: 0xCAFE_BABE,
volume_label: label.to_string(),
};
let fs = Exfat::format(&mut dev, &opts).unwrap();
(dev, fs)
}
#[test]
fn format_produces_openable_volume() {
let (mut dev, _fs) = fresh_volume("WRTEST");
assert!(probe(&mut dev).unwrap());
let fs2 = Exfat::open(&mut dev).unwrap();
assert_eq!(fs2.volume_label(), "WRTEST");
assert!(fs2.cluster_size() >= 512);
let root = fs2.list_path(&mut dev, "/").unwrap();
assert!(root.is_empty(), "fresh root should be empty, got {root:?}");
}
#[test]
fn format_no_label_omits_volume_entry() {
let (mut dev, _fs) = fresh_volume("");
let fs2 = Exfat::open(&mut dev).unwrap();
assert_eq!(fs2.volume_label(), "");
}
#[test]
fn create_file_then_list() {
let (mut dev, mut fs) = fresh_volume("CRT");
let payload = b"hello, exfat writer!\n";
let mut reader: &[u8] = payload;
fs.create_file(&mut dev, "/hello.txt", &mut reader, payload.len() as u64, 0)
.unwrap();
fs.flush(&mut dev).unwrap();
let fs2 = Exfat::open(&mut dev).unwrap();
let entries = fs2.list_path(&mut dev, "/").unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "hello.txt");
assert_eq!(entries[0].kind, crate::fs::EntryKind::Regular);
use std::io::Read;
let mut r = fs2.open_file_reader(&mut dev, "/hello.txt").unwrap();
let mut buf = Vec::new();
r.read_to_end(&mut buf).unwrap();
assert_eq!(buf, payload);
}
#[test]
fn create_empty_file() {
let (mut dev, mut fs) = fresh_volume("EMPTY");
let mut empty: &[u8] = &[];
fs.create_file(&mut dev, "/zero.bin", &mut empty, 0, 0)
.unwrap();
fs.flush(&mut dev).unwrap();
let fs2 = Exfat::open(&mut dev).unwrap();
let entries = fs2.list_path(&mut dev, "/").unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "zero.bin");
}
#[test]
fn create_directory_and_nested_file() {
let (mut dev, mut fs) = fresh_volume("DIRS");
fs.create_dir(&mut dev, "/sub", 0).unwrap();
let payload = b"nested";
let mut reader: &[u8] = payload;
fs.create_file(&mut dev, "/sub/x.bin", &mut reader, payload.len() as u64, 0)
.unwrap();
fs.flush(&mut dev).unwrap();
let fs2 = Exfat::open(&mut dev).unwrap();
let root = fs2.list_path(&mut dev, "/").unwrap();
assert_eq!(root.len(), 1);
assert_eq!(root[0].name, "sub");
assert_eq!(root[0].kind, crate::fs::EntryKind::Dir);
let sub = fs2.list_path(&mut dev, "/sub").unwrap();
assert_eq!(sub.len(), 1);
assert_eq!(sub[0].name, "x.bin");
use std::io::Read;
let mut r = fs2.open_file_reader(&mut dev, "/sub/x.bin").unwrap();
let mut buf = Vec::new();
r.read_to_end(&mut buf).unwrap();
assert_eq!(buf, payload);
}
#[test]
fn multi_cluster_file_streams_correctly() {
let (mut dev, mut fs) = fresh_volume("BIG");
let mut payload = Vec::with_capacity(20 * 1024);
for i in 0..(20 * 1024) {
payload.push((i % 251) as u8);
}
let mut reader: &[u8] = &payload;
fs.create_file(&mut dev, "/big.bin", &mut reader, payload.len() as u64, 0)
.unwrap();
fs.flush(&mut dev).unwrap();
let fs2 = Exfat::open(&mut dev).unwrap();
use std::io::Read;
let mut r = fs2.open_file_reader(&mut dev, "/big.bin").unwrap();
let mut buf = Vec::new();
r.read_to_end(&mut buf).unwrap();
assert_eq!(buf.len(), payload.len());
assert_eq!(buf, payload);
}
#[test]
fn remove_file_frees_clusters() {
let (mut dev, mut fs) = fresh_volume("RM");
let payload = b"to be deleted";
let mut reader: &[u8] = payload;
fs.create_file(
&mut dev,
"/doomed.txt",
&mut reader,
payload.len() as u64,
0,
)
.unwrap();
fs.flush(&mut dev).unwrap();
let used_before: u32 = fs.bitmap.iter().map(|b| b.count_ones()).sum();
fs.remove(&mut dev, "/doomed.txt").unwrap();
fs.flush(&mut dev).unwrap();
let used_after: u32 = fs.bitmap.iter().map(|b| b.count_ones()).sum();
assert!(
used_after < used_before,
"remove should free at least one cluster (before={used_before}, after={used_after})"
);
let fs2 = Exfat::open(&mut dev).unwrap();
let root = fs2.list_path(&mut dev, "/").unwrap();
assert!(
root.is_empty(),
"root must be empty after remove, got {root:?}"
);
}
#[test]
fn remove_empty_directory_succeeds() {
let (mut dev, mut fs) = fresh_volume("RMD");
fs.create_dir(&mut dev, "/empty", 0).unwrap();
fs.flush(&mut dev).unwrap();
fs.remove(&mut dev, "/empty").unwrap();
fs.flush(&mut dev).unwrap();
let fs2 = Exfat::open(&mut dev).unwrap();
assert!(fs2.list_path(&mut dev, "/").unwrap().is_empty());
}
#[test]
fn remove_non_empty_directory_fails() {
let (mut dev, mut fs) = fresh_volume("RMNE");
fs.create_dir(&mut dev, "/sub", 0).unwrap();
let mut empty: &[u8] = &[];
fs.create_file(&mut dev, "/sub/x", &mut empty, 0, 0)
.unwrap();
fs.flush(&mut dev).unwrap();
let err = fs.remove(&mut dev, "/sub").unwrap_err();
match err {
crate::Error::InvalidArgument(msg) => assert!(msg.contains("not empty")),
other => panic!("expected InvalidArgument, got {other:?}"),
}
}
#[test]
fn case_insensitive_lookup_on_writer() {
let (mut dev, mut fs) = fresh_volume("CASE");
let mut reader: &[u8] = b"x";
fs.create_file(&mut dev, "/Hello.TXT", &mut reader, 1, 0)
.unwrap();
fs.flush(&mut dev).unwrap();
let fs2 = Exfat::open(&mut dev).unwrap();
use std::io::Read;
let mut r = fs2.open_file_reader(&mut dev, "/HELLO.txt").unwrap();
let mut buf = Vec::new();
r.read_to_end(&mut buf).unwrap();
assert_eq!(buf, b"x");
}
#[test]
fn many_files_stress_directory_expansion() {
let (mut dev, mut fs) = fresh_volume("MANY");
for i in 0..60u32 {
let name = format!("file_{i:04}.bin");
let mut reader: &[u8] = b"";
fs.create_file(&mut dev, &format!("/{name}"), &mut reader, 0, 0)
.unwrap();
}
fs.flush(&mut dev).unwrap();
let fs2 = Exfat::open(&mut dev).unwrap();
let entries = fs2.list_path(&mut dev, "/").unwrap();
assert_eq!(entries.len(), 60);
}
#[test]
fn flush_persists_fat_and_bitmap() {
let (mut dev, mut fs) = fresh_volume("FLUSH");
let mut reader: &[u8] = b"persistence test";
fs.create_file(&mut dev, "/p.txt", &mut reader, 16, 0)
.unwrap();
fs.flush(&mut dev).unwrap();
let fs2 = Exfat::open(&mut dev).unwrap();
let entries = fs2.list_path(&mut dev, "/").unwrap();
assert_eq!(entries.len(), 1);
let cluster = entries[0].inode;
let bit = (cluster - 2) as usize;
let byte = bit / 8;
let mask = 1u8 << (bit % 8);
assert!(
fs2.bitmap[byte] & mask != 0,
"bitmap bit for cluster {cluster} must be set"
);
}
}