Files
One-KVM/libs/ventoy-img-rs/src/exfat/ops.rs

3067 lines
108 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! exFAT filesystem operations
//!
//! Complete exFAT file operations: read, write, delete files.
//! Supports streaming write for large files.
//! Supports subdirectories and file overwriting.
use crate::error::{Result, VentoyError};
use crate::exfat::unicode;
use crate::partition::PartitionLayout;
use std::collections::HashSet;
use std::fs::{File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::Path;
/// FAT entry values
const FAT_ENTRY_FREE: u32 = 0x00000000;
const FAT_ENTRY_END_OF_CHAIN: u32 = 0xFFFFFFFF;
const VOLUME_DIRTY_FLAG: u16 = 0x0002;
const VOLUME_FLAGS_OFFSET: u64 = 106;
const NO_FAT_CHAIN_FLAG: u8 = 0x02;
const MAX_DIRECTORY_SIZE: u64 = 256 * 1024 * 1024;
/// Directory entry types
const ENTRY_TYPE_END: u8 = 0x00;
const ENTRY_TYPE_BITMAP: u8 = 0x81;
const ENTRY_TYPE_UPCASE: u8 = 0x82;
const ENTRY_TYPE_FILE: u8 = 0x85;
const ENTRY_TYPE_STREAM: u8 = 0xC0;
const ENTRY_TYPE_FILE_NAME: u8 = 0xC1;
const ENTRY_TYPE_DELETED_FILE: u8 = 0x05;
const ENTRY_TYPE_DELETED_STREAM: u8 = 0x40;
const ENTRY_TYPE_DELETED_NAME: u8 = 0x41;
/// File attributes
const ATTR_DIRECTORY: u16 = 0x10;
const ATTR_ARCHIVE: u16 = 0x20;
/// FAT cache size (number of entries, 8192 entries = 32KB)
const FAT_CACHE_ENTRIES: usize = 8192;
/// FAT table segment cache for reducing disk I/O
struct FatCache {
/// First cluster number in this cache segment
start_cluster: u32,
/// Cached FAT entries
entries: Vec<u32>,
}
impl FatCache {
/// Create a new empty FAT cache
fn new() -> Self {
Self {
start_cluster: 0,
entries: Vec::new(),
}
}
/// Check if a cluster is in the cache
fn contains(&self, cluster: u32) -> bool {
if self.entries.is_empty() {
return false;
}
cluster >= self.start_cluster && cluster < self.start_cluster + self.entries.len() as u32
}
/// Get a FAT entry from cache (if present)
fn get(&self, cluster: u32) -> Option<u32> {
if self.contains(cluster) {
let index = (cluster - self.start_cluster) as usize;
Some(self.entries[index])
} else {
None
}
}
/// Update a single entry in the cache (for write operations)
fn update(&mut self, cluster: u32, value: u32) {
if self.contains(cluster) {
let index = (cluster - self.start_cluster) as usize;
self.entries[index] = value;
}
}
}
// ==================== Path Utilities ====================
/// Parse a path into components
fn parse_path(path: &str) -> Vec<&str> {
path.trim_matches('/')
.split('/')
.filter(|s| !s.is_empty())
.collect()
}
/// File information
#[derive(Debug, Clone)]
pub struct FileInfo {
pub name: String,
pub size: u64,
pub is_directory: bool,
/// Path from root (for recursive listing)
pub path: String,
}
/// Location of a file entry in the directory
#[derive(Debug, Clone)]
struct FileEntryLocation {
/// Cluster containing the directory
directory_cluster: u32,
/// Byte offset within the cluster where the file entry starts
entry_offset: u32,
/// First cluster of file data
first_cluster: u32,
/// File size in bytes
data_length: u64,
/// Number of secondary entries (stream + name entries)
secondary_count: u8,
/// Whether this is a directory
is_directory: bool,
/// Whether the allocation is contiguous and its FAT entries must be ignored
no_fat_chain: bool,
}
/// A directory allocation. The root directory has no containing file entry.
#[derive(Debug, Clone)]
struct DirectoryLocation {
entry: Option<FileEntryLocation>,
first_cluster: u32,
}
impl DirectoryLocation {
fn root(first_cluster: u32) -> Self {
Self {
entry: None,
first_cluster,
}
}
fn from_entry(entry: FileEntryLocation) -> Result<Self> {
if !entry.is_directory {
return Err(VentoyError::FilesystemError(
"Entry is not a directory".to_string(),
));
}
Ok(Self {
first_cluster: entry.first_cluster,
entry: Some(entry),
})
}
}
/// Result of resolving a path
#[derive(Debug, Clone)]
struct ResolvedPath {
/// The parent directory containing the target entry
parent: DirectoryLocation,
/// The name of the target file/directory
name: String,
/// The location if the target exists
location: Option<FileEntryLocation>,
}
/// exFAT filesystem with full read/write support
#[allow(dead_code)]
pub struct ExfatFs {
file: File,
partition_offset: u64,
// Boot sector cached parameters
bytes_per_sector: u32,
sectors_per_cluster: u32,
cluster_size: u32,
fat_offset: u32,
fat_length: u32,
cluster_heap_offset: u32,
cluster_count: u32,
first_cluster_of_root: u32,
allocation_bitmap_first_cluster: u32,
allocation_bitmap_size: u64,
upcase_table_first_cluster: u32,
upcase_table_size: u64,
upcase_table_checksum: u32,
// Performance caches
/// FAT table segment cache
fat_cache: FatCache,
/// Allocation bitmap cache (loaded on first access)
bitmap_cache: Option<Vec<u8>>,
/// Whether the bitmap cache has been modified
bitmap_dirty: bool,
}
impl ExfatFs {
/// Open exFAT filesystem from image file
pub fn open(path: &Path, layout: &PartitionLayout) -> Result<Self> {
let mut file = OpenOptions::new()
.read(true)
.write(true)
.open(path)
.map_err(VentoyError::Io)?;
let partition_offset = layout.data_offset();
// Read and parse boot sector
let mut boot_sector = [0u8; 512];
file.seek(SeekFrom::Start(partition_offset))?;
file.read_exact(&mut boot_sector)?;
// Verify exFAT signature
if &boot_sector[3..11] != b"EXFAT " {
return Err(VentoyError::FilesystemError(
"Invalid exFAT signature".to_string(),
));
}
// Parse boot sector fields
let fat_offset = u32::from_le_bytes(boot_sector[80..84].try_into().unwrap());
let fat_length = u32::from_le_bytes(boot_sector[84..88].try_into().unwrap());
let cluster_heap_offset = u32::from_le_bytes(boot_sector[88..92].try_into().unwrap());
let cluster_count = u32::from_le_bytes(boot_sector[92..96].try_into().unwrap());
let first_cluster_of_root = u32::from_le_bytes(boot_sector[96..100].try_into().unwrap());
let bytes_per_sector_shift = boot_sector[108];
let sectors_per_cluster_shift = boot_sector[109];
if !(9..=12).contains(&bytes_per_sector_shift)
|| u16::from(bytes_per_sector_shift) + u16::from(sectors_per_cluster_shift) > 25
|| cluster_count == 0
{
return Err(VentoyError::FilesystemError(
"Invalid exFAT boot-sector geometry".to_string(),
));
}
let bytes_per_sector =
1u32.checked_shl(bytes_per_sector_shift.into())
.ok_or_else(|| {
VentoyError::FilesystemError("Invalid bytes-per-sector shift".to_string())
})?;
let sectors_per_cluster = 1u32
.checked_shl(sectors_per_cluster_shift.into())
.ok_or_else(|| {
VentoyError::FilesystemError("Invalid sectors-per-cluster shift".to_string())
})?;
let cluster_size = bytes_per_sector
.checked_mul(sectors_per_cluster)
.ok_or_else(|| VentoyError::FilesystemError("Cluster size overflow".to_string()))?;
let fat_bytes = u64::from(fat_length) * u64::from(bytes_per_sector);
let required_fat_bytes = (u64::from(cluster_count) + 2) * 4;
let heap_end_sector = u64::from(cluster_heap_offset)
.checked_add(u64::from(cluster_count) * u64::from(sectors_per_cluster))
.ok_or_else(|| {
VentoyError::FilesystemError("Cluster heap geometry overflow".to_string())
})?;
if fat_bytes < required_fat_bytes || heap_end_sector > layout.data_size_sectors {
return Err(VentoyError::FilesystemError(
"Invalid exFAT FAT or cluster-heap geometry".to_string(),
));
}
let mut fs = Self {
file,
partition_offset,
bytes_per_sector,
sectors_per_cluster,
cluster_size,
fat_offset,
fat_length,
cluster_heap_offset,
cluster_count,
first_cluster_of_root,
allocation_bitmap_first_cluster: 2,
allocation_bitmap_size: ((cluster_count + 7) / 8) as u64,
upcase_table_first_cluster: 0,
upcase_table_size: 0,
upcase_table_checksum: 0,
// Initialize caches
fat_cache: FatCache::new(),
bitmap_cache: None,
bitmap_dirty: false,
};
fs.discover_root_metadata_entries()?;
Ok(fs)
}
fn discover_root_metadata_entries(&mut self) -> Result<()> {
let mut bitmap_found = false;
let mut upcase_found = false;
let root = DirectoryLocation::root(self.first_cluster_of_root);
let root_clusters = self.directory_clusters(&root)?;
'outer: for &cluster in &root_clusters {
let cluster_data = self.read_cluster(cluster)?;
let mut i = 0;
while i + 32 <= cluster_data.len() {
let entry_type = cluster_data[i];
match entry_type {
ENTRY_TYPE_END => break 'outer,
ENTRY_TYPE_BITMAP => {
let first_cluster =
u32::from_le_bytes(cluster_data[i + 20..i + 24].try_into().unwrap());
let size =
u64::from_le_bytes(cluster_data[i + 24..i + 32].try_into().unwrap());
if first_cluster < 2 || size == 0 {
return Err(VentoyError::FilesystemError(
"Invalid exFAT allocation bitmap entry".to_string(),
));
}
self.allocation_bitmap_first_cluster = first_cluster;
self.allocation_bitmap_size = size;
bitmap_found = true;
}
ENTRY_TYPE_UPCASE => {
let checksum =
u32::from_le_bytes(cluster_data[i + 4..i + 8].try_into().unwrap());
let first_cluster =
u32::from_le_bytes(cluster_data[i + 20..i + 24].try_into().unwrap());
let size =
u64::from_le_bytes(cluster_data[i + 24..i + 32].try_into().unwrap());
if first_cluster < 2 || size == 0 {
return Err(VentoyError::FilesystemError(
"Invalid exFAT upcase table entry".to_string(),
));
}
self.upcase_table_first_cluster = first_cluster;
self.upcase_table_size = size;
self.upcase_table_checksum = checksum;
upcase_found = true;
}
ENTRY_TYPE_FILE => {
let secondary_count = cluster_data[i + 1] as usize;
i += (1 + secondary_count) * 32;
continue;
}
_ => {}
}
i += 32;
}
}
if !bitmap_found {
return Err(VentoyError::FilesystemError(
"exFAT allocation bitmap entry not found".to_string(),
));
}
if !upcase_found {
return Err(VentoyError::FilesystemError(
"exFAT upcase table entry not found".to_string(),
));
}
let min_bitmap_size = ((self.cluster_count + 7) / 8) as u64;
if self.allocation_bitmap_size < min_bitmap_size {
return Err(VentoyError::FilesystemError(format!(
"exFAT allocation bitmap too small: {} bytes, need at least {}",
self.allocation_bitmap_size, min_bitmap_size
)));
}
let max_bitmap_size = min_bitmap_size + self.cluster_size as u64 - 1;
if self.allocation_bitmap_size > max_bitmap_size {
return Err(VentoyError::FilesystemError(format!(
"exFAT allocation bitmap is unreasonably large: {} bytes",
self.allocation_bitmap_size
)));
}
Ok(())
}
fn volume_flags_offset(&self) -> u64 {
self.partition_offset + VOLUME_FLAGS_OFFSET
}
fn read_volume_flags(&mut self) -> Result<u16> {
let mut bytes = [0u8; 2];
self.file
.seek(SeekFrom::Start(self.volume_flags_offset()))?;
self.file.read_exact(&mut bytes)?;
Ok(u16::from_le_bytes(bytes))
}
fn is_volume_dirty(&mut self) -> Result<bool> {
Ok((self.read_volume_flags()? & VOLUME_DIRTY_FLAG) != 0)
}
fn set_volume_dirty(&mut self, dirty: bool) -> Result<()> {
let mut flags = self.read_volume_flags()?;
if dirty {
flags |= VOLUME_DIRTY_FLAG;
} else {
flags &= !VOLUME_DIRTY_FLAG;
}
self.file
.seek(SeekFrom::Start(self.volume_flags_offset()))?;
self.file.write_all(&flags.to_le_bytes())?;
self.file.flush()?;
Ok(())
}
fn begin_write_transaction(&mut self) -> Result<bool> {
let was_dirty = self.is_volume_dirty()?;
if !was_dirty {
self.set_volume_dirty(true)?;
}
Ok(was_dirty)
}
fn finish_write_transaction<T>(&mut self, was_dirty: bool, result: Result<T>) -> Result<T> {
match result {
Ok(value) => {
self.file.flush()?;
if !was_dirty {
self.set_volume_dirty(false)?;
}
Ok(value)
}
Err(err) => {
let _ = self.file.flush();
Err(err)
}
}
}
// ==================== Cluster I/O Operations ====================
/// Convert cluster number to absolute byte offset
fn cluster_to_offset(&self, cluster: u32) -> u64 {
// Clusters start at 2, so subtract 2 to get heap-relative index
let cluster_index = (cluster - 2) as u64;
self.partition_offset
+ self.cluster_heap_offset as u64 * self.bytes_per_sector as u64
+ cluster_index * self.cluster_size as u64
}
/// Read a cluster's data
fn read_cluster(&mut self, cluster: u32) -> Result<Vec<u8>> {
let offset = self.cluster_to_offset(cluster);
self.file.seek(SeekFrom::Start(offset))?;
let mut data = vec![0u8; self.cluster_size as usize];
self.file.read_exact(&mut data)?;
Ok(data)
}
/// Write data to a cluster
fn write_cluster(&mut self, cluster: u32, data: &[u8]) -> Result<()> {
if data.len() > self.cluster_size as usize {
return Err(VentoyError::FilesystemError(
"Data exceeds cluster size".to_string(),
));
}
let offset = self.cluster_to_offset(cluster);
self.file.seek(SeekFrom::Start(offset))?;
self.file.write_all(data)?;
// Pad with zeros if data is smaller than cluster
if data.len() < self.cluster_size as usize {
let padding = vec![0u8; self.cluster_size as usize - data.len()];
self.file.write_all(&padding)?;
}
Ok(())
}
// ==================== FAT Table Operations ====================
/// Get the byte offset of a FAT entry
fn fat_entry_offset(&self, cluster: u32) -> u64 {
self.partition_offset
+ self.fat_offset as u64 * self.bytes_per_sector as u64
+ cluster as u64 * 4
}
/// Load a FAT segment into cache starting from the given cluster
fn load_fat_segment(&mut self, start_cluster: u32) -> Result<()> {
// Calculate how many entries we can read
let max_cluster = self.cluster_count + 2;
let entries_to_read = FAT_CACHE_ENTRIES.min((max_cluster - start_cluster) as usize);
if entries_to_read == 0 {
return Ok(());
}
// Read FAT entries in bulk (4 bytes per entry)
let offset = self.fat_entry_offset(start_cluster);
self.file.seek(SeekFrom::Start(offset))?;
let mut buffer = vec![0u8; entries_to_read * 4];
self.file.read_exact(&mut buffer)?;
// Parse entries
let mut entries = Vec::with_capacity(entries_to_read);
for chunk in buffer.chunks_exact(4) {
entries.push(u32::from_le_bytes(chunk.try_into().unwrap()));
}
self.fat_cache.start_cluster = start_cluster;
self.fat_cache.entries = entries;
Ok(())
}
/// Read a FAT entry (with caching)
fn read_fat_entry(&mut self, cluster: u32) -> Result<u32> {
// Try cache first
if let Some(entry) = self.fat_cache.get(cluster) {
return Ok(entry);
}
// Cache miss - load a new segment starting from this cluster
self.load_fat_segment(cluster)?;
// Should be in cache now
self.fat_cache.get(cluster).ok_or_else(|| {
VentoyError::FilesystemError(format!(
"Failed to cache FAT entry for cluster {}",
cluster
))
})
}
/// Write a FAT entry (updates cache if present)
fn write_fat_entry(&mut self, cluster: u32, value: u32) -> Result<()> {
let offset = self.fat_entry_offset(cluster);
self.file.seek(SeekFrom::Start(offset))?;
self.file.write_all(&value.to_le_bytes())?;
// Update cache if this entry is cached
self.fat_cache.update(cluster, value);
Ok(())
}
fn max_cluster(&self) -> u32 {
self.cluster_count + 1
}
fn validate_cluster(&self, cluster: u32) -> Result<()> {
if cluster < 2 || cluster > self.max_cluster() {
return Err(VentoyError::FilesystemError(format!(
"Cluster {} is outside the cluster heap (2..={})",
cluster,
self.max_cluster()
)));
}
Ok(())
}
/// Read a FAT chain with an explicit upper bound and cycle detection.
fn read_cluster_chain_limited(
&mut self,
first_cluster: u32,
max_clusters: usize,
) -> Result<Vec<u32>> {
if max_clusters == 0 {
return Err(VentoyError::FilesystemError(
"Cluster chain limit must be greater than zero".to_string(),
));
}
self.validate_cluster(first_cluster)?;
let mut chain = Vec::new();
chain.try_reserve(max_clusters.min(1024)).map_err(|_| {
VentoyError::FilesystemError("Unable to reserve cluster chain memory".to_string())
})?;
let mut visited = HashSet::new();
let mut current = first_cluster;
loop {
self.validate_cluster(current)?;
if !visited.insert(current) {
return Err(VentoyError::FilesystemError(format!(
"FAT chain contains a cycle at cluster {}",
current
)));
}
if chain.len() >= max_clusters {
return Err(VentoyError::FilesystemError(format!(
"FAT chain exceeds the {} cluster limit",
max_clusters
)));
}
chain.push(current);
let next = self.read_fat_entry(current)?;
if next == FAT_ENTRY_END_OF_CHAIN {
return Ok(chain);
}
if next == FAT_ENTRY_FREE || next == 0xFFFFFFF7 || next >= 0xFFFFFFF8 {
return Err(VentoyError::FilesystemError(format!(
"Invalid FAT entry {:#010X} after cluster {}",
next, current
)));
}
current = next;
}
}
/// Read the entire cluster chain starting from a cluster.
fn read_cluster_chain(&mut self, first_cluster: u32) -> Result<Vec<u32>> {
self.read_cluster_chain_limited(first_cluster, self.cluster_count as usize)
}
fn clusters_for_allocation(&mut self, location: &FileEntryLocation) -> Result<Vec<u32>> {
if location.data_length == 0 {
if location.first_cluster != 0 || location.no_fat_chain {
return Err(VentoyError::FilesystemError(
"Zero-length allocation has invalid cluster metadata".to_string(),
));
}
return Ok(Vec::new());
}
self.validate_cluster(location.first_cluster)?;
let cluster_size = self.cluster_size as u64;
let cluster_count_u64 = location.data_length.div_ceil(cluster_size);
let cluster_count = usize::try_from(cluster_count_u64).map_err(|_| {
VentoyError::FilesystemError("Allocation cluster count is too large".to_string())
})?;
if location.no_fat_chain {
let last_cluster = u64::from(location.first_cluster)
.checked_add(cluster_count_u64 - 1)
.ok_or_else(|| {
VentoyError::FilesystemError(
"Contiguous allocation cluster range overflow".to_string(),
)
})?;
if last_cluster > u64::from(self.max_cluster()) {
return Err(VentoyError::FilesystemError(format!(
"Contiguous allocation ends outside the cluster heap at {}",
last_cluster
)));
}
let mut clusters = Vec::new();
clusters.try_reserve_exact(cluster_count).map_err(|_| {
VentoyError::FilesystemError(
"Unable to reserve contiguous allocation metadata".to_string(),
)
})?;
clusters.extend(
location.first_cluster
..=u32::try_from(last_cluster).expect("validated cluster range"),
);
return Ok(clusters);
}
let chain = self.read_cluster_chain_limited(location.first_cluster, cluster_count)?;
if chain.len() != cluster_count {
return Err(VentoyError::FilesystemError(format!(
"FAT chain has {} clusters, allocation needs {}",
chain.len(),
cluster_count
)));
}
Ok(chain)
}
fn directory_clusters(&mut self, directory: &DirectoryLocation) -> Result<Vec<u32>> {
let max_clusters = MAX_DIRECTORY_SIZE.div_ceil(self.cluster_size as u64) as usize;
match directory.entry.as_ref() {
Some(entry) if entry.no_fat_chain => {
if entry.data_length > MAX_DIRECTORY_SIZE {
return Err(VentoyError::FilesystemError(format!(
"Directory allocation exceeds the {} byte limit",
MAX_DIRECTORY_SIZE
)));
}
self.clusters_for_allocation(entry)
}
_ => self.read_cluster_chain_limited(directory.first_cluster, max_clusters),
}
}
// ==================== Allocation Bitmap Operations ====================
fn read_cluster_chain_bytes(&mut self, first_cluster: u32, byte_len: u64) -> Result<Vec<u8>> {
let chain = self.read_cluster_chain(first_cluster)?;
if chain.is_empty() {
return Err(VentoyError::FilesystemError(
"Empty cluster chain".to_string(),
));
}
let capacity = byte_len.min(chain.len() as u64 * self.cluster_size as u64) as usize;
let mut data = Vec::with_capacity(capacity);
for &cluster in &chain {
let cluster_data = self.read_cluster(cluster)?;
data.extend_from_slice(&cluster_data);
if data.len() >= byte_len as usize {
data.truncate(byte_len as usize);
break;
}
}
if data.len() < byte_len as usize {
return Err(VentoyError::FilesystemError(format!(
"Cluster chain for {} is shorter than expected: {} < {} bytes",
first_cluster,
data.len(),
byte_len
)));
}
Ok(data)
}
fn write_cluster_chain_bytes(
&mut self,
first_cluster: u32,
byte_len: u64,
data: &[u8],
) -> Result<()> {
if data.len() < byte_len as usize {
return Err(VentoyError::FilesystemError(format!(
"Not enough data to write cluster chain: {} < {} bytes",
data.len(),
byte_len
)));
}
let chain = self.read_cluster_chain(first_cluster)?;
if chain.is_empty() {
return Err(VentoyError::FilesystemError(
"Empty cluster chain".to_string(),
));
}
let mut bytes_written = 0usize;
let bytes_to_write = byte_len as usize;
for &cluster in &chain {
let end = (bytes_written + self.cluster_size as usize).min(bytes_to_write);
if bytes_written >= end {
break;
}
self.write_cluster(cluster, &data[bytes_written..end])?;
bytes_written = end;
}
if bytes_written < bytes_to_write {
return Err(VentoyError::FilesystemError(format!(
"Cluster chain for {} is shorter than expected: {} < {} bytes",
first_cluster, bytes_written, bytes_to_write
)));
}
Ok(())
}
/// Read the allocation bitmap (with caching)
fn read_bitmap(&mut self) -> Result<Vec<u8>> {
if let Some(ref bitmap) = self.bitmap_cache {
return Ok(bitmap.clone());
}
let bitmap = self.read_cluster_chain_bytes(
self.allocation_bitmap_first_cluster,
self.allocation_bitmap_size,
)?;
self.bitmap_cache = Some(bitmap.clone());
Ok(bitmap)
}
/// Get a mutable reference to the cached bitmap, loading if necessary
fn get_bitmap_mut(&mut self) -> Result<&mut Vec<u8>> {
if self.bitmap_cache.is_none() {
let bitmap = self.read_cluster_chain_bytes(
self.allocation_bitmap_first_cluster,
self.allocation_bitmap_size,
)?;
self.bitmap_cache = Some(bitmap);
}
Ok(self.bitmap_cache.as_mut().unwrap())
}
/// Write the allocation bitmap (with cache management)
#[allow(dead_code)]
fn write_bitmap(&mut self, bitmap: &[u8]) -> Result<()> {
self.write_cluster_chain_bytes(
self.allocation_bitmap_first_cluster,
self.allocation_bitmap_size,
bitmap,
)?;
self.bitmap_cache = Some(bitmap.to_vec());
self.bitmap_dirty = false;
Ok(())
}
/// Flush dirty bitmap to disk if needed
#[allow(dead_code)]
fn flush_bitmap(&mut self) -> Result<()> {
if self.bitmap_dirty {
if let Some(bitmap) = self.bitmap_cache.take() {
self.write_cluster_chain_bytes(
self.allocation_bitmap_first_cluster,
self.allocation_bitmap_size,
&bitmap,
)?;
self.bitmap_cache = Some(bitmap);
self.bitmap_dirty = false;
}
}
Ok(())
}
/// Check if a cluster is allocated
fn is_cluster_allocated(bitmap: &[u8], cluster: u32) -> bool {
let index = (cluster - 2) as usize;
let byte_index = index / 8;
let bit_index = index % 8;
if byte_index >= bitmap.len() {
return false;
}
(bitmap[byte_index] & (1 << bit_index)) != 0
}
/// Set cluster allocation status in bitmap
fn set_cluster_allocated(bitmap: &mut [u8], cluster: u32, allocated: bool) {
let index = (cluster - 2) as usize;
let byte_index = index / 8;
let bit_index = index % 8;
if byte_index < bitmap.len() {
if allocated {
bitmap[byte_index] |= 1 << bit_index;
} else {
bitmap[byte_index] &= !(1 << bit_index);
}
}
}
/// Find free clusters
fn find_free_clusters(&mut self, count: usize) -> Result<Vec<u32>> {
let bitmap = self.read_bitmap()?;
let mut free_clusters = Vec::with_capacity(count);
for cluster in 2..self.cluster_count + 2 {
if !Self::is_cluster_allocated(&bitmap, cluster) {
free_clusters.push(cluster);
if free_clusters.len() >= count {
break;
}
}
}
if free_clusters.len() < count {
return Err(VentoyError::FilesystemError(format!(
"Not enough free space: need {} clusters, found {}",
count,
free_clusters.len()
)));
}
Ok(free_clusters)
}
/// Allocate clusters and create a chain
fn allocate_clusters(&mut self, count: usize) -> Result<u32> {
if count == 0 {
return Err(VentoyError::FilesystemError(
"Cannot allocate 0 clusters".to_string(),
));
}
let clusters = self.find_free_clusters(count)?;
let first_cluster = clusters[0];
// Update bitmap using cache
{
let bitmap = self.get_bitmap_mut()?;
for &cluster in &clusters {
Self::set_cluster_allocated(bitmap, cluster, true);
}
}
// Flush bitmap immediately for data integrity
self.flush_bitmap_now()?;
// Create FAT chain
for i in 0..clusters.len() {
let next = if i + 1 < clusters.len() {
clusters[i + 1]
} else {
FAT_ENTRY_END_OF_CHAIN
};
self.write_fat_entry(clusters[i], next)?;
}
Ok(first_cluster)
}
fn free_clusters(&mut self, clusters: &[u32], clear_fat: bool) -> Result<()> {
if clusters.is_empty() {
return Ok(());
}
// Update bitmap using cache
{
let bitmap = self.get_bitmap_mut()?;
for &cluster in clusters {
Self::set_cluster_allocated(bitmap, cluster, false);
}
}
// Flush bitmap immediately for data integrity
self.flush_bitmap_now()?;
// Clear FAT entries and update cache
if clear_fat {
for &cluster in clusters {
self.write_fat_entry(cluster, FAT_ENTRY_FREE)?;
}
}
Ok(())
}
fn free_allocation(&mut self, location: &FileEntryLocation) -> Result<()> {
let clusters = self.clusters_for_allocation(location)?;
self.free_clusters(&clusters, !location.no_fat_chain)
}
fn free_directory_allocation(&mut self, location: &FileEntryLocation) -> Result<()> {
let directory = DirectoryLocation::from_entry(location.clone())?;
let clusters = self.directory_clusters(&directory)?;
self.free_clusters(&clusters, !location.no_fat_chain)
}
/// Flush bitmap to disk immediately
fn flush_bitmap_now(&mut self) -> Result<()> {
if let Some(bitmap) = self.bitmap_cache.take() {
self.write_cluster_chain_bytes(
self.allocation_bitmap_first_cluster,
self.allocation_bitmap_size,
&bitmap,
)?;
self.bitmap_cache = Some(bitmap);
}
Ok(())
}
// ==================== Directory Entry Operations ====================
/// Calculate name hash for exFAT (used in Stream Extension entry)
///
/// Uses Unicode-aware uppercase conversion for proper international support.
fn calculate_name_hash(name: &str) -> u16 {
unicode::calculate_name_hash(name)
}
/// Calculate entry set checksum
fn calculate_entry_set_checksum(entries: &[[u8; 32]]) -> u16 {
let mut checksum: u16 = 0;
for (entry_idx, entry) in entries.iter().enumerate() {
for (byte_idx, &byte) in entry.iter().enumerate() {
// Skip checksum field in first entry (bytes 2-3)
if entry_idx == 0 && (byte_idx == 2 || byte_idx == 3) {
continue;
}
checksum = checksum.rotate_right(1).wrapping_add(byte as u16);
}
}
checksum
}
/// Create file directory entries for a new file
fn create_file_entries(
name: &str,
first_cluster: u32,
size: u64,
is_dir: bool,
) -> Vec<[u8; 32]> {
let name_utf16: Vec<u16> = name.encode_utf16().collect();
let name_entries_needed = (name_utf16.len() + 14) / 15; // 15 chars per name entry
let secondary_count = 1 + name_entries_needed; // Stream + Name entries
let mut entries = Vec::with_capacity(2 + name_entries_needed);
// 1. File Directory Entry (0x85)
let mut file_entry = [0u8; 32];
file_entry[0] = ENTRY_TYPE_FILE;
file_entry[1] = secondary_count as u8;
// Checksum at bytes 2-3 (filled later)
let attrs: u16 = if is_dir { ATTR_DIRECTORY } else { ATTR_ARCHIVE };
file_entry[4..6].copy_from_slice(&attrs.to_le_bytes());
// Timestamps (simplified - use current time)
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as u32)
.unwrap_or(0);
// DOS timestamp format (simplified)
let dos_time =
((now / 2) & 0x1F) | (((now / 60) & 0x3F) << 5) | (((now / 3600) & 0x1F) << 11);
let dos_date = 1 | (1 << 5) | ((45) << 9); // Jan 1, 2025
file_entry[8..12]
.copy_from_slice(&(dos_date as u32 | ((dos_time as u32) << 16)).to_le_bytes());
file_entry[12..16]
.copy_from_slice(&(dos_date as u32 | ((dos_time as u32) << 16)).to_le_bytes());
file_entry[16..20]
.copy_from_slice(&(dos_date as u32 | ((dos_time as u32) << 16)).to_le_bytes());
entries.push(file_entry);
// 2. Stream Extension Entry (0xC0)
let mut stream_entry = [0u8; 32];
stream_entry[0] = ENTRY_TYPE_STREAM;
// AllocationPossible. Files created here always use FAT chains when allocated.
stream_entry[1] = 0x01;
stream_entry[3] = name_utf16.len() as u8; // NameLength
let name_hash = Self::calculate_name_hash(name);
stream_entry[4..6].copy_from_slice(&name_hash.to_le_bytes());
// ValidDataLength
stream_entry[8..16].copy_from_slice(&size.to_le_bytes());
// Reserved at 16-19
stream_entry[20..24].copy_from_slice(&first_cluster.to_le_bytes());
// DataLength
stream_entry[24..32].copy_from_slice(&size.to_le_bytes());
entries.push(stream_entry);
// 3. File Name Entries (0xC1)
let mut char_index = 0;
for _ in 0..name_entries_needed {
let mut name_entry = [0u8; 32];
name_entry[0] = ENTRY_TYPE_FILE_NAME;
name_entry[1] = 0; // GeneralSecondaryFlags
for i in 0..15 {
if char_index < name_utf16.len() {
let offset = 2 + i * 2;
name_entry[offset..offset + 2]
.copy_from_slice(&name_utf16[char_index].to_le_bytes());
char_index += 1;
}
}
entries.push(name_entry);
}
// Calculate and set checksum
let checksum = Self::calculate_entry_set_checksum(&entries);
entries[0][2..4].copy_from_slice(&checksum.to_le_bytes());
entries
}
fn root_directory(&self) -> DirectoryLocation {
DirectoryLocation::root(self.first_cluster_of_root)
}
/// Find a file entry in a specific directory
fn find_entry_in_directory(
&mut self,
directory: &DirectoryLocation,
name: &str,
) -> Result<Option<FileEntryLocation>> {
let target_name_lower = name.to_lowercase();
let dir_clusters = self.directory_clusters(directory)?;
for &cluster in &dir_clusters {
let cluster_data = self.read_cluster(cluster)?;
let mut i = 0;
while i < cluster_data.len() {
let entry_type = cluster_data[i];
if entry_type == ENTRY_TYPE_END {
// End of directory - no more entries in any cluster
return Ok(None);
}
if entry_type == ENTRY_TYPE_FILE {
let secondary_count = cluster_data[i + 1] as usize;
let attrs = u16::from_le_bytes(cluster_data[i + 4..i + 6].try_into().unwrap());
let is_directory = (attrs & ATTR_DIRECTORY) != 0;
let mut file_name = String::new();
let mut first_cluster = 0u32;
let mut data_length = 0u64;
let mut no_fat_chain = false;
// Parse secondary entries
for j in 1..=secondary_count {
let entry_offset = i + j * 32;
if entry_offset + 32 > cluster_data.len() {
break;
}
let sec_type = cluster_data[entry_offset];
if sec_type == ENTRY_TYPE_STREAM {
no_fat_chain =
(cluster_data[entry_offset + 1] & NO_FAT_CHAIN_FLAG) != 0;
first_cluster = u32::from_le_bytes(
cluster_data[entry_offset + 20..entry_offset + 24]
.try_into()
.unwrap(),
);
data_length = u64::from_le_bytes(
cluster_data[entry_offset + 24..entry_offset + 32]
.try_into()
.unwrap(),
);
} else if sec_type == ENTRY_TYPE_FILE_NAME {
let name_chars: Vec<u16> = (2..32)
.step_by(2)
.map(|k| {
u16::from_le_bytes([
cluster_data[entry_offset + k],
cluster_data[entry_offset + k + 1],
])
})
.take_while(|&c| c != 0)
.collect();
file_name.extend(char::decode_utf16(name_chars).filter_map(|r| r.ok()));
}
}
if file_name.to_lowercase() == target_name_lower {
return Ok(Some(FileEntryLocation {
directory_cluster: cluster,
entry_offset: i as u32,
first_cluster,
data_length,
secondary_count: secondary_count as u8,
is_directory,
no_fat_chain,
}));
}
i += (1 + secondary_count) * 32;
} else {
i += 32;
}
}
}
Ok(None)
}
/// Find a file entry in the root directory (backward compatible)
fn find_file_entry(&mut self, name: &str) -> Result<Option<FileEntryLocation>> {
let root = self.root_directory();
self.find_entry_in_directory(&root, name)
}
/// Resolve a path to its parent directory cluster and target name
///
/// Returns the parent directory cluster and the target name.
/// If create_parents is true, creates intermediate directories as needed.
fn resolve_path(&mut self, path: &str, create_parents: bool) -> Result<ResolvedPath> {
let components = parse_path(path);
if components.is_empty() {
return Err(VentoyError::FilesystemError("Empty path".to_string()));
}
let mut current = self.root_directory();
// Navigate through all but the last component (which is the target)
for (idx, &component) in components.iter().take(components.len() - 1).enumerate() {
match self.find_entry_in_directory(&current, component)? {
Some(entry) => {
if !entry.is_directory {
return Err(VentoyError::FilesystemError(format!(
"'{}' is not a directory",
component
)));
}
current = DirectoryLocation::from_entry(entry)?;
}
None => {
if create_parents {
current = self.create_directory_in(&mut current, component)?;
} else {
let partial_path = components[..=idx].join("/");
return Err(VentoyError::FilesystemError(format!(
"Directory '{}' not found",
partial_path
)));
}
}
}
}
let target_name = components.last().unwrap().to_string();
let location = self.find_entry_in_directory(&current, &target_name)?;
Ok(ResolvedPath {
parent: current,
name: target_name,
location,
})
}
fn update_entry_allocation(
&mut self,
location: &mut FileEntryLocation,
no_fat_chain: bool,
data_length: u64,
) -> Result<()> {
let mut cluster_data = self.read_cluster(location.directory_cluster)?;
let set_start = location.entry_offset as usize;
let entry_count = 1 + location.secondary_count as usize;
let set_end = set_start
.checked_add(entry_count * 32)
.ok_or_else(|| VentoyError::FilesystemError("Directory entry overflow".to_string()))?;
if set_end > cluster_data.len() {
return Err(VentoyError::FilesystemError(
"Directory entry set crosses an unsupported cluster boundary".to_string(),
));
}
let stream_offset = (1..entry_count)
.map(|index| set_start + index * 32)
.find(|&offset| cluster_data[offset] == ENTRY_TYPE_STREAM)
.ok_or_else(|| {
VentoyError::FilesystemError("Stream extension entry not found".to_string())
})?;
if no_fat_chain {
cluster_data[stream_offset + 1] |= NO_FAT_CHAIN_FLAG;
} else {
cluster_data[stream_offset + 1] &= !NO_FAT_CHAIN_FLAG;
}
cluster_data[stream_offset + 8..stream_offset + 16]
.copy_from_slice(&data_length.to_le_bytes());
cluster_data[stream_offset + 24..stream_offset + 32]
.copy_from_slice(&data_length.to_le_bytes());
let entries: Vec<[u8; 32]> = cluster_data[set_start..set_end]
.chunks_exact(32)
.map(|entry| entry.try_into().expect("32-byte directory entry"))
.collect();
let checksum = Self::calculate_entry_set_checksum(&entries);
cluster_data[set_start + 2..set_start + 4].copy_from_slice(&checksum.to_le_bytes());
self.write_cluster(location.directory_cluster, &cluster_data)?;
location.no_fat_chain = no_fat_chain;
location.data_length = data_length;
Ok(())
}
fn extend_directory(&mut self, directory: &mut DirectoryLocation) -> Result<u32> {
let existing = self.directory_clusters(directory)?;
let last_cluster = *existing.last().ok_or_else(|| {
VentoyError::FilesystemError("Empty directory allocation".to_string())
})?;
let new_length = if directory.entry.is_some() {
let length = (existing.len() as u64 + 1)
.checked_mul(self.cluster_size as u64)
.ok_or_else(|| {
VentoyError::FilesystemError("Directory size overflow".to_string())
})?;
if length > MAX_DIRECTORY_SIZE {
return Err(VentoyError::FilesystemError(format!(
"Directory exceeds the {} byte limit",
MAX_DIRECTORY_SIZE
)));
}
Some(length)
} else {
None
};
let new_cluster = self.allocate_clusters(1)?;
if directory
.entry
.as_ref()
.is_some_and(|entry| entry.no_fat_chain)
{
for pair in existing.windows(2) {
self.write_fat_entry(pair[0], pair[1])?;
}
}
self.write_fat_entry(last_cluster, new_cluster)?;
if let (Some(entry), Some(new_length)) = (directory.entry.as_mut(), new_length) {
self.update_entry_allocation(entry, false, new_length)?;
}
let empty_cluster = vec![0u8; self.cluster_size as usize];
self.write_cluster(new_cluster, &empty_cluster)?;
Ok(new_cluster)
}
/// Find a free slot in a directory for new entries
///
/// Returns (cluster, offset_within_cluster)
///
/// If no free slot is found in existing clusters, this method will
/// automatically extend the directory by allocating a new cluster.
fn find_free_slot_in_directory(
&mut self,
directory: &mut DirectoryLocation,
entries_needed: usize,
) -> Result<(u32, u32)> {
let dir_clusters = self.directory_clusters(directory)?;
for &cluster in &dir_clusters {
let cluster_data = self.read_cluster(cluster)?;
let mut i = 0;
let mut consecutive_free = 0;
let mut slot_start = 0;
while i < cluster_data.len() {
let entry_type = cluster_data[i];
if entry_type == ENTRY_TYPE_END
|| entry_type == 0x00
|| entry_type == ENTRY_TYPE_DELETED_FILE
|| entry_type == ENTRY_TYPE_DELETED_STREAM
|| entry_type == ENTRY_TYPE_DELETED_NAME
{
if consecutive_free == 0 {
slot_start = i;
}
consecutive_free += 1;
if consecutive_free >= entries_needed {
return Ok((cluster, slot_start as u32));
}
} else if entry_type == ENTRY_TYPE_FILE {
let secondary_count = cluster_data[i + 1] as usize;
i += (1 + secondary_count) * 32;
consecutive_free = 0;
continue;
} else {
consecutive_free = 0;
}
i += 32;
}
// Check if we have enough space at the end of this cluster
if consecutive_free >= entries_needed {
return Ok((cluster, slot_start as u32));
}
}
// No space found in existing clusters - extend the directory
let new_cluster = self.extend_directory(directory)?;
// Clear any END markers in previous clusters
// This is critical: when we extend a directory, we need to clear any END markers
// that may exist in previous clusters, otherwise list_files will stop prematurely
for &cluster in &dir_clusters {
let mut cluster_data = self.read_cluster(cluster)?;
// Scan for END markers and replace them with inactive entries. Leaving an
// END marker before later directory clusters makes hosts stop early; using
// an in-use invalid type can make strict hosts treat the directory as bad.
for i in (0..cluster_data.len()).step_by(32) {
if cluster_data[i] == ENTRY_TYPE_END {
cluster_data[i] = ENTRY_TYPE_DELETED_FILE;
}
}
self.write_cluster(cluster, &cluster_data)?;
}
// Return the first slot in the new cluster (offset 0)
Ok((new_cluster, 0))
}
/// Find a free slot in the root directory for new entries (backward compatible)
#[allow(dead_code)]
fn find_free_directory_slot(&mut self, entries_needed: usize) -> Result<u32> {
let mut root = self.root_directory();
let (_, offset) = self.find_free_slot_in_directory(&mut root, entries_needed)?;
Ok(offset)
}
/// Create an entry in a specific directory
fn create_entry_in_directory(
&mut self,
directory: &mut DirectoryLocation,
name: &str,
first_cluster: u32,
size: u64,
is_dir: bool,
) -> Result<()> {
let entries = Self::create_file_entries(name, first_cluster, size, is_dir);
let (slot_cluster, slot_offset) =
self.find_free_slot_in_directory(directory, entries.len())?;
let mut cluster_data = self.read_cluster(slot_cluster)?;
// Write entries to the slot
for (i, entry) in entries.iter().enumerate() {
let offset = slot_offset as usize + i * 32;
cluster_data[offset..offset + 32].copy_from_slice(entry);
}
self.write_cluster(slot_cluster, &cluster_data)?;
Ok(())
}
/// Create a file entry in the root directory (backward compatible)
#[allow(dead_code)]
fn create_file_entry(&mut self, name: &str, first_cluster: u32, size: u64) -> Result<()> {
let mut root = self.root_directory();
self.create_entry_in_directory(&mut root, name, first_cluster, size, false)
}
/// Create a directory in a specific parent directory
///
/// Returns the allocation descriptor of the new directory.
fn create_directory_in(
&mut self,
parent: &mut DirectoryLocation,
name: &str,
) -> Result<DirectoryLocation> {
// Validate name
if name.is_empty() || name.len() > 255 {
return Err(VentoyError::FilesystemError(
"Invalid directory name length".to_string(),
));
}
// Check if already exists
if self.find_entry_in_directory(parent, name)?.is_some() {
return Err(VentoyError::FilesystemError(format!(
"Entry '{}' already exists",
name
)));
}
// Allocate a cluster for the new directory
let dir_cluster = self.allocate_clusters(1)?;
// Initialize the directory cluster with zeros (empty directory)
let empty_cluster = vec![0u8; self.cluster_size as usize];
self.write_cluster(dir_cluster, &empty_cluster)?;
// exFAT directories have allocated data. A zero-length directory stream
// makes Windows treat the directory as corrupt even when the cluster
// chain and child entries are otherwise valid.
self.create_entry_in_directory(parent, name, dir_cluster, self.cluster_size as u64, true)?;
self.file.flush()?;
let entry = self.find_entry_in_directory(parent, name)?.ok_or_else(|| {
VentoyError::FilesystemError("Created directory not found".to_string())
})?;
DirectoryLocation::from_entry(entry)
}
/// Delete a file entry (mark as deleted)
fn delete_file_entry(&mut self, location: &FileEntryLocation) -> Result<()> {
let mut cluster_data = self.read_cluster(location.directory_cluster)?;
let offset = location.entry_offset as usize;
// Mark file entry as deleted
cluster_data[offset] = ENTRY_TYPE_DELETED_FILE;
// Mark secondary entries as deleted
for i in 1..=location.secondary_count as usize {
let entry_offset = offset + i * 32;
if entry_offset < cluster_data.len() {
let entry_type = cluster_data[entry_offset];
cluster_data[entry_offset] = match entry_type {
ENTRY_TYPE_STREAM => ENTRY_TYPE_DELETED_STREAM,
ENTRY_TYPE_FILE_NAME => ENTRY_TYPE_DELETED_NAME,
_ => entry_type & 0x7F, // Clear in-use bit
};
}
}
self.write_cluster(location.directory_cluster, &cluster_data)?;
Ok(())
}
// ==================== Public File Operations ====================
/// List files in a specific directory allocation.
fn list_files_in_directory(
&mut self,
directory: &DirectoryLocation,
current_path: &str,
) -> Result<Vec<FileInfo>> {
let dir_clusters = self.directory_clusters(directory)?;
let mut files = Vec::new();
for (cluster_idx, &cluster) in dir_clusters.iter().enumerate() {
let cluster_data = self.read_cluster(cluster)?;
let mut i = 0;
while i < cluster_data.len() {
let entry_type = cluster_data[i];
// ENTRY_TYPE_END marks end of directory entries
// But in extended directories, we might have empty clusters that start with 0x00
// Only treat 0x00 as end marker if we're in the first cluster
if entry_type == ENTRY_TYPE_END {
if cluster_idx == 0 {
return Ok(files);
}
// In non-first clusters, skip the empty entry and continue
}
if entry_type == ENTRY_TYPE_FILE {
let secondary_count = cluster_data[i + 1] as usize;
let attrs = u16::from_le_bytes(cluster_data[i + 4..i + 6].try_into().unwrap());
let is_directory = (attrs & ATTR_DIRECTORY) != 0;
let mut file_name = String::new();
let mut file_size = 0u64;
for j in 1..=secondary_count {
let entry_offset = i + j * 32;
if entry_offset + 32 > cluster_data.len() {
break;
}
let sec_type = cluster_data[entry_offset];
if sec_type == ENTRY_TYPE_STREAM {
file_size = u64::from_le_bytes(
cluster_data[entry_offset + 24..entry_offset + 32]
.try_into()
.unwrap(),
);
} else if sec_type == ENTRY_TYPE_FILE_NAME {
let name_chars: Vec<u16> = (2..32)
.step_by(2)
.map(|k| {
u16::from_le_bytes([
cluster_data[entry_offset + k],
cluster_data[entry_offset + k + 1],
])
})
.take_while(|&c| c != 0)
.collect();
file_name.extend(char::decode_utf16(name_chars).filter_map(|r| r.ok()));
}
}
if !file_name.is_empty() {
let full_path = if current_path.is_empty() {
file_name.clone()
} else {
format!("{}/{}", current_path, file_name)
};
files.push(FileInfo {
name: file_name,
size: file_size,
is_directory,
path: full_path,
});
}
i += (1 + secondary_count) * 32;
} else {
i += 32;
}
}
}
Ok(files)
}
/// List files in root directory
pub fn list_files(&mut self) -> Result<Vec<FileInfo>> {
let root = self.root_directory();
self.list_files_in_directory(&root, "")
}
/// List files in a specific directory path
pub fn list_files_at(&mut self, path: &str) -> Result<Vec<FileInfo>> {
if path.is_empty() || path == "/" {
return self.list_files();
}
let resolved = self.resolve_path(path, false)?;
match resolved.location {
Some(loc) if loc.is_directory => self.list_files_in_directory(
&DirectoryLocation::from_entry(loc)?,
path.trim_matches('/'),
),
Some(_) => Err(VentoyError::FilesystemError(format!(
"'{}' is not a directory",
path
))),
None => Err(VentoyError::FilesystemError(format!(
"Directory '{}' not found",
path
))),
}
}
/// List all files recursively
pub fn list_files_recursive(&mut self) -> Result<Vec<FileInfo>> {
let mut all_files = Vec::new();
let mut dirs_to_visit = vec![(self.root_directory(), String::new())];
while let Some((directory, current_path)) = dirs_to_visit.pop() {
let files = self.list_files_in_directory(&directory, &current_path)?;
for file in files {
if file.is_directory {
if let Some(loc) = self.find_entry_in_directory(&directory, &file.name)? {
dirs_to_visit
.push((DirectoryLocation::from_entry(loc)?, file.path.clone()));
}
}
all_files.push(file);
}
}
Ok(all_files)
}
/// Write file data to allocated clusters and create directory entry
fn write_file_data_and_entry(
&mut self,
directory: &mut DirectoryLocation,
name: &str,
data: &[u8],
) -> Result<()> {
// Calculate clusters needed
let clusters_needed = if data.is_empty() {
0
} else {
((data.len() as u64 + self.cluster_size as u64 - 1) / self.cluster_size as u64) as usize
};
// Allocate clusters
let first_cluster = if clusters_needed > 0 {
let first = self.allocate_clusters(clusters_needed)?;
// Write file data
let chain = self.read_cluster_chain(first)?;
let mut data_offset = 0;
for &cluster in &chain {
let chunk_size = (data.len() - data_offset).min(self.cluster_size as usize);
let chunk = &data[data_offset..data_offset + chunk_size];
self.write_cluster(cluster, chunk)?;
data_offset += chunk_size;
}
first
} else {
0
};
// Create directory entry
self.create_entry_in_directory(directory, name, first_cluster, data.len() as u64, false)?;
self.file.flush()?;
Ok(())
}
/// Write a file to the filesystem (root directory, no overwrite)
pub fn write_file(&mut self, name: &str, data: &[u8]) -> Result<()> {
let was_dirty = self.begin_write_transaction()?;
let result = (|| {
// Validate filename
if name.is_empty() || name.len() > 255 {
return Err(VentoyError::FilesystemError(
"Invalid filename length".to_string(),
));
}
// Check if file already exists
if self.find_file_entry(name)?.is_some() {
return Err(VentoyError::FilesystemError(format!(
"File '{}' already exists",
name
)));
}
let mut root = self.root_directory();
self.write_file_data_and_entry(&mut root, name, data)
})();
self.finish_write_transaction(was_dirty, result)
}
/// Write a file to the filesystem with overwrite option
pub fn write_file_overwrite(&mut self, name: &str, data: &[u8], overwrite: bool) -> Result<()> {
let was_dirty = self.begin_write_transaction()?;
let result = (|| {
// Validate filename
if name.is_empty() || name.len() > 255 {
return Err(VentoyError::FilesystemError(
"Invalid filename length".to_string(),
));
}
// Check if file already exists
if self.find_file_entry(name)?.is_some() {
if overwrite {
self.delete_file(name)?;
} else {
return Err(VentoyError::FilesystemError(format!(
"File '{}' already exists",
name
)));
}
}
let mut root = self.root_directory();
self.write_file_data_and_entry(&mut root, name, data)
})();
self.finish_write_transaction(was_dirty, result)
}
/// Write a file to a specific path
///
/// Path can include directories, e.g., "iso/linux/ubuntu.iso"
/// If create_parents is true, intermediate directories will be created.
/// If overwrite is true, existing files will be replaced.
pub fn write_file_path(
&mut self,
path: &str,
data: &[u8],
create_parents: bool,
overwrite: bool,
) -> Result<()> {
let was_dirty = self.begin_write_transaction()?;
let result = (|| {
let mut resolved = self.resolve_path(path, create_parents)?;
// Validate filename
if resolved.name.is_empty() || resolved.name.len() > 255 {
return Err(VentoyError::FilesystemError(
"Invalid filename length".to_string(),
));
}
// Handle existing file
if let Some(location) = resolved.location {
if location.is_directory {
return Err(VentoyError::FilesystemError(format!(
"'{}' is a directory",
path
)));
}
if overwrite {
// Delete existing file
if location.first_cluster >= 2 {
self.free_allocation(&location)?;
}
self.delete_file_entry(&location)?;
} else {
return Err(VentoyError::FilesystemError(format!(
"File '{}' already exists",
path
)));
}
}
self.write_file_data_and_entry(&mut resolved.parent, &resolved.name, data)
})();
self.finish_write_transaction(was_dirty, result)
}
/// Read file data from a location
fn read_file_from_location(&mut self, location: &FileEntryLocation) -> Result<Vec<u8>> {
if location.data_length == 0 {
return Ok(Vec::new());
}
let chain = self.clusters_for_allocation(location)?;
let data_length = usize::try_from(location.data_length).map_err(|_| {
VentoyError::FilesystemError("File is too large to read into memory".to_string())
})?;
let mut data = Vec::new();
data.try_reserve_exact(data_length).map_err(|_| {
VentoyError::FilesystemError("File is too large to read into memory".to_string())
})?;
for &cluster in &chain {
let cluster_data = self.read_cluster(cluster)?;
let remaining = data_length - data.len();
let chunk_size = remaining.min(self.cluster_size as usize);
data.extend_from_slice(&cluster_data[..chunk_size]);
}
Ok(data)
}
/// Read a file from the filesystem (root directory)
pub fn read_file(&mut self, name: &str) -> Result<Vec<u8>> {
let location = self
.find_file_entry(name)?
.ok_or_else(|| VentoyError::FilesystemError(format!("File '{}' not found", name)))?;
self.read_file_from_location(&location)
}
/// Read a file from a specific path
pub fn read_file_path(&mut self, path: &str) -> Result<Vec<u8>> {
let resolved = self.resolve_path(path, false)?;
match resolved.location {
Some(loc) if loc.is_directory => Err(VentoyError::FilesystemError(format!(
"'{}' is a directory",
path
))),
Some(loc) => self.read_file_from_location(&loc),
None => Err(VentoyError::FilesystemError(format!(
"File '{}' not found",
path
))),
}
}
/// Get file information at a specific path without reading content
///
/// Returns None if the file doesn't exist.
pub fn get_file_info_path(&mut self, path: &str) -> Result<Option<FileInfo>> {
let resolved = self.resolve_path(path, false)?;
match resolved.location {
Some(loc) => {
// Extract just the filename from the path
let name = path.rsplit('/').next().unwrap_or(path).to_string();
Ok(Some(FileInfo {
name,
size: loc.data_length,
is_directory: loc.is_directory,
path: path.to_string(),
}))
}
None => Ok(None),
}
}
/// Delete a file from the filesystem (root directory)
pub fn delete_file(&mut self, name: &str) -> Result<()> {
let was_dirty = self.begin_write_transaction()?;
let result = (|| {
let location = self.find_file_entry(name)?.ok_or_else(|| {
VentoyError::FilesystemError(format!("File '{}' not found", name))
})?;
self.free_allocation(&location)?;
// Delete directory entry
self.delete_file_entry(&location)?;
self.file.flush()?;
Ok(())
})();
self.finish_write_transaction(was_dirty, result)
}
/// Delete a file or directory at a specific path
pub fn delete_path(&mut self, path: &str) -> Result<()> {
let was_dirty = self.begin_write_transaction()?;
let result = (|| {
let resolved = self.resolve_path(path, false)?;
let location = resolved
.location
.ok_or_else(|| VentoyError::FilesystemError(format!("'{}' not found", path)))?;
// If it's a directory, check if it's empty
if location.is_directory {
let directory = DirectoryLocation::from_entry(location.clone())?;
let contents = self.list_files_in_directory(&directory, "")?;
if !contents.is_empty() {
return Err(VentoyError::FilesystemError(format!(
"Directory '{}' is not empty",
path
)));
}
}
if location.is_directory {
self.free_directory_allocation(&location)?;
} else {
self.free_allocation(&location)?;
}
// Delete directory entry
self.delete_file_entry(&location)?;
self.file.flush()?;
Ok(())
})();
self.finish_write_transaction(was_dirty, result)
}
/// Delete a directory and all its contents recursively
pub fn delete_recursive(&mut self, path: &str) -> Result<()> {
let was_dirty = self.begin_write_transaction()?;
let result = (|| {
let resolved = self.resolve_path(path, false)?;
let location = resolved
.location
.ok_or_else(|| VentoyError::FilesystemError(format!("'{}' not found", path)))?;
if location.is_directory {
// Get all contents and delete them first
let directory = DirectoryLocation::from_entry(location.clone())?;
let contents = self.list_files_in_directory(&directory, "")?;
for item in contents {
let item_path = if path.ends_with('/') {
format!("{}{}", path, item.name)
} else {
format!("{}/{}", path, item.name)
};
self.delete_recursive(&item_path)?;
}
}
if location.is_directory {
self.free_directory_allocation(&location)?;
} else {
self.free_allocation(&location)?;
}
self.delete_file_entry(&location)?;
self.file.flush()?;
Ok(())
})();
self.finish_write_transaction(was_dirty, result)
}
/// Create a directory at a specific path
///
/// If create_parents is true, creates all intermediate directories (mkdir -p behavior)
pub fn create_directory(&mut self, path: &str, create_parents: bool) -> Result<()> {
let was_dirty = self.begin_write_transaction()?;
let result = (|| {
let mut resolved = self.resolve_path(path, create_parents)?;
if resolved.location.is_some() {
return Err(VentoyError::FilesystemError(format!(
"'{}' already exists",
path
)));
}
self.create_directory_in(&mut resolved.parent, &resolved.name)?;
Ok(())
})();
self.finish_write_transaction(was_dirty, result)
}
}
/// Streaming file writer for large files
///
/// This allows writing large files without loading them entirely into memory.
pub struct ExfatFileWriter<'a> {
fs: &'a mut ExfatFs,
name: String,
directory: DirectoryLocation,
total_size: u64,
allocated_clusters: Vec<u32>,
current_cluster_index: usize,
cluster_buffer: Vec<u8>,
bytes_written: u64,
}
impl<'a> ExfatFileWriter<'a> {
/// Create a new file writer (writes to root directory)
///
/// The total_size must be known in advance to allocate clusters.
pub fn create(fs: &'a mut ExfatFs, name: &str, total_size: u64) -> Result<Self> {
let root = fs.root_directory();
Self::create_in_directory(fs, root, name, total_size, false)
}
/// Create a new file writer with overwrite option
pub fn create_overwrite(
fs: &'a mut ExfatFs,
name: &str,
total_size: u64,
overwrite: bool,
) -> Result<Self> {
let root = fs.root_directory();
Self::create_in_directory(fs, root, name, total_size, overwrite)
}
/// Create a file writer for a specific path
///
/// If create_parents is true, intermediate directories will be created.
/// If overwrite is true, existing files will be replaced.
pub fn create_at_path(
fs: &'a mut ExfatFs,
path: &str,
total_size: u64,
create_parents: bool,
overwrite: bool,
) -> Result<Self> {
let resolved = fs.resolve_path(path, create_parents)?;
// Handle existing file
if let Some(location) = resolved.location {
if location.is_directory {
return Err(VentoyError::FilesystemError(format!(
"'{}' is a directory",
path
)));
}
if overwrite {
fs.free_allocation(&location)?;
fs.delete_file_entry(&location)?;
} else {
return Err(VentoyError::FilesystemError(format!(
"File '{}' already exists",
path
)));
}
}
Self::create_in_directory(fs, resolved.parent, &resolved.name, total_size, false)
}
/// Internal: Create a file writer in a specific directory
fn create_in_directory(
fs: &'a mut ExfatFs,
directory: DirectoryLocation,
name: &str,
total_size: u64,
overwrite: bool,
) -> Result<Self> {
// Validate filename
if name.is_empty() || name.len() > 255 {
return Err(VentoyError::FilesystemError(
"Invalid filename length".to_string(),
));
}
// Check if file already exists
if let Some(location) = fs.find_entry_in_directory(&directory, name)? {
if overwrite {
fs.free_allocation(&location)?;
fs.delete_file_entry(&location)?;
} else {
return Err(VentoyError::FilesystemError(format!(
"File '{}' already exists",
name
)));
}
}
// Calculate and allocate clusters
let clusters_needed = if total_size == 0 {
0
} else {
((total_size + fs.cluster_size as u64 - 1) / fs.cluster_size as u64) as usize
};
let allocated_clusters = if clusters_needed > 0 {
let first = fs.allocate_clusters(clusters_needed)?;
fs.read_cluster_chain(first)?
} else {
Vec::new()
};
let cluster_size = fs.cluster_size as usize;
Ok(Self {
fs,
name: name.to_string(),
directory,
total_size,
allocated_clusters,
current_cluster_index: 0,
cluster_buffer: Vec::with_capacity(cluster_size),
bytes_written: 0,
})
}
/// Write data to the file
///
/// Returns the number of bytes written.
pub fn write(&mut self, data: &[u8]) -> Result<usize> {
let cluster_size = self.fs.cluster_size as usize;
let mut data_offset = 0;
while data_offset < data.len() && self.bytes_written < self.total_size {
// Fill cluster buffer
let space_in_buffer = cluster_size - self.cluster_buffer.len();
let remaining_to_write = (self.total_size - self.bytes_written) as usize;
let chunk_size = space_in_buffer
.min(data.len() - data_offset)
.min(remaining_to_write);
self.cluster_buffer
.extend_from_slice(&data[data_offset..data_offset + chunk_size]);
data_offset += chunk_size;
self.bytes_written += chunk_size as u64;
// Write cluster if buffer is full
if self.cluster_buffer.len() >= cluster_size {
if self.current_cluster_index < self.allocated_clusters.len() {
let cluster = self.allocated_clusters[self.current_cluster_index];
self.fs.write_cluster(cluster, &self.cluster_buffer)?;
self.current_cluster_index += 1;
self.cluster_buffer.clear();
}
}
}
Ok(data_offset)
}
/// Finish writing and create the directory entry
///
/// This must be called after all data has been written.
pub fn finish(mut self) -> Result<()> {
// Write any remaining data in buffer
if !self.cluster_buffer.is_empty()
&& self.current_cluster_index < self.allocated_clusters.len()
{
let cluster = self.allocated_clusters[self.current_cluster_index];
self.fs.write_cluster(cluster, &self.cluster_buffer)?;
}
// Create directory entry
let first_cluster = if self.allocated_clusters.is_empty() {
0
} else {
self.allocated_clusters[0]
};
self.fs.create_entry_in_directory(
&mut self.directory,
&self.name,
first_cluster,
self.total_size,
false,
)?;
self.fs.file.flush()?;
Ok(())
}
/// Get the number of bytes written so far
pub fn bytes_written(&self) -> u64 {
self.bytes_written
}
}
/// Streaming file reader for large files
///
/// This allows reading large files without loading them entirely into memory.
/// Implements `std::io::Read` and `std::io::Seek` traits for compatibility
/// with standard I/O operations.
pub struct ExfatFileReader<'a> {
fs: &'a mut ExfatFs,
/// Cluster chain for this file
cluster_chain: Vec<u32>,
/// Total file size in bytes
file_size: u64,
/// Current position in the file (byte offset from start)
position: u64,
/// Cached current cluster data
cluster_cache: Option<(u32, Vec<u8>)>,
}
impl<'a> ExfatFileReader<'a> {
/// Open a file for reading from root directory
pub fn open(fs: &'a mut ExfatFs, name: &str) -> Result<Self> {
let location = fs
.find_file_entry(name)?
.ok_or_else(|| VentoyError::FilesystemError(format!("File '{}' not found", name)))?;
if location.is_directory {
return Err(VentoyError::FilesystemError(format!(
"'{}' is a directory",
name
)));
}
Self::from_location(fs, &location)
}
/// Open a file for reading from a specific path
pub fn open_path(fs: &'a mut ExfatFs, path: &str) -> Result<Self> {
let resolved = fs.resolve_path(path, false)?;
match resolved.location {
Some(loc) if loc.is_directory => Err(VentoyError::FilesystemError(format!(
"'{}' is a directory",
path
))),
Some(loc) => Self::from_location(fs, &loc),
None => Err(VentoyError::FilesystemError(format!(
"File '{}' not found",
path
))),
}
}
/// Internal: Create reader from file entry location
fn from_location(fs: &'a mut ExfatFs, location: &FileEntryLocation) -> Result<Self> {
let cluster_chain = fs.clusters_for_allocation(location)?;
Ok(Self {
fs,
cluster_chain,
file_size: location.data_length,
position: 0,
cluster_cache: None,
})
}
/// Get the total file size
pub fn file_size(&self) -> u64 {
self.file_size
}
/// Get the current position in the file
pub fn position(&self) -> u64 {
self.position
}
/// Get the remaining bytes to read
pub fn remaining(&self) -> u64 {
self.file_size.saturating_sub(self.position)
}
/// Get cluster size for this filesystem
fn cluster_size(&self) -> u64 {
self.fs.cluster_size as u64
}
/// Read the cluster at the given index, using cache if available
fn read_cluster_cached(&mut self, cluster_index: usize) -> Result<&[u8]> {
if cluster_index >= self.cluster_chain.len() {
return Err(VentoyError::FilesystemError(
"Cluster index out of range".to_string(),
));
}
let cluster_num = self.cluster_chain[cluster_index];
// Check if we have this cluster cached
if let Some((cached_cluster, ref data)) = self.cluster_cache {
if cached_cluster == cluster_num {
return Ok(unsafe { &*(data.as_slice() as *const [u8]) });
}
}
// Read the cluster
let data = self.fs.read_cluster(cluster_num)?;
self.cluster_cache = Some((cluster_num, data));
Ok(self.cluster_cache.as_ref().unwrap().1.as_slice())
}
}
impl<'a> Read for ExfatFileReader<'a> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.position >= self.file_size || buf.is_empty() {
return Ok(0);
}
let cluster_size = self.cluster_size();
let file_size = self.file_size;
let mut bytes_read = 0;
while bytes_read < buf.len() && self.position < file_size {
// Calculate which cluster we're in and the offset within it
let cluster_index = (self.position / cluster_size) as usize;
let offset_in_cluster = (self.position % cluster_size) as usize;
// Calculate how much we can read from this cluster
let remaining_in_cluster = cluster_size as usize - offset_in_cluster;
let remaining_in_file = (file_size - self.position) as usize;
let remaining_in_buf = buf.len() - bytes_read;
let to_read = remaining_in_cluster
.min(remaining_in_file)
.min(remaining_in_buf);
// Read the cluster and copy data
{
let cluster_data = self
.read_cluster_cached(cluster_index)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
// Copy data to buffer
buf[bytes_read..bytes_read + to_read]
.copy_from_slice(&cluster_data[offset_in_cluster..offset_in_cluster + to_read]);
}
bytes_read += to_read;
self.position += to_read as u64;
}
Ok(bytes_read)
}
}
impl<'a> Seek for ExfatFileReader<'a> {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
let new_pos = match pos {
SeekFrom::Start(offset) => offset as i64,
SeekFrom::End(offset) => self.file_size as i64 + offset,
SeekFrom::Current(offset) => self.position as i64 + offset,
};
if new_pos < 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Seek to negative position",
));
}
// Allow seeking past end of file (like regular files)
self.position = new_pos as u64;
Ok(self.position)
}
}
impl ExfatFs {
/// Read a file to a writer (streaming)
///
/// This is useful for reading large files without loading them into memory.
pub fn read_file_to_writer<W: Write>(&mut self, name: &str, writer: &mut W) -> Result<u64> {
let mut reader = ExfatFileReader::open(self, name)?;
Self::do_stream_read(&mut reader, writer)
}
/// Read a file from a path to a writer (streaming)
pub fn read_file_path_to_writer<W: Write>(
&mut self,
path: &str,
writer: &mut W,
) -> Result<u64> {
let mut reader = ExfatFileReader::open_path(self, path)?;
Self::do_stream_read(&mut reader, writer)
}
/// Internal: Stream read from reader to writer
fn do_stream_read<W: Write>(reader: &mut ExfatFileReader, writer: &mut W) -> Result<u64> {
let mut buffer = vec![0u8; 64 * 1024]; // 64KB buffer
let mut total_bytes = 0u64;
loop {
let bytes_read = reader.read(&mut buffer).map_err(|e| VentoyError::Io(e))?;
if bytes_read == 0 {
break;
}
writer
.write_all(&buffer[..bytes_read])
.map_err(VentoyError::Io)?;
total_bytes += bytes_read as u64;
}
Ok(total_bytes)
}
}
impl ExfatFs {
/// Write a file from a reader (streaming)
///
/// This is useful for writing large files without loading them into memory.
pub fn write_file_from_reader<R: Read>(
&mut self,
name: &str,
reader: &mut R,
size: u64,
) -> Result<()> {
let was_dirty = self.begin_write_transaction()?;
let result = (|| {
let mut writer = ExfatFileWriter::create(self, name, size)?;
Self::do_stream_write(&mut writer, reader)?;
writer.finish()
})();
self.finish_write_transaction(was_dirty, result)
}
/// Write a file from a reader with overwrite option
pub fn write_file_from_reader_overwrite<R: Read>(
&mut self,
name: &str,
reader: &mut R,
size: u64,
overwrite: bool,
) -> Result<()> {
let was_dirty = self.begin_write_transaction()?;
let result = (|| {
let mut writer = ExfatFileWriter::create_overwrite(self, name, size, overwrite)?;
Self::do_stream_write(&mut writer, reader)?;
writer.finish()
})();
self.finish_write_transaction(was_dirty, result)
}
/// Write a file from a reader to a specific path
///
/// If create_parents is true, intermediate directories will be created.
/// If overwrite is true, existing files will be replaced.
pub fn write_file_from_reader_path<R: Read>(
&mut self,
path: &str,
reader: &mut R,
size: u64,
create_parents: bool,
overwrite: bool,
) -> Result<()> {
let was_dirty = self.begin_write_transaction()?;
let result = (|| {
let mut writer =
ExfatFileWriter::create_at_path(self, path, size, create_parents, overwrite)?;
Self::do_stream_write(&mut writer, reader)?;
writer.finish()
})();
self.finish_write_transaction(was_dirty, result)
}
/// Internal: Stream write from reader to writer
fn do_stream_write<R: Read>(writer: &mut ExfatFileWriter, reader: &mut R) -> Result<()> {
let mut buffer = vec![0u8; 64 * 1024]; // 64KB buffer
loop {
let bytes_read = reader.read(&mut buffer).map_err(VentoyError::Io)?;
if bytes_read == 0 {
break;
}
writer.write(&buffer[..bytes_read])?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::partition::PartitionLayout;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use tempfile::NamedTempFile;
fn cluster_offset(
partition_offset: u64,
cluster_heap_offset: u32,
cluster_size: u64,
cluster: u32,
) -> u64 {
partition_offset + cluster_heap_offset as u64 * 512 + (cluster - 2) as u64 * cluster_size
}
fn format_test_image(size: u64) -> (NamedTempFile, PartitionLayout) {
let temp_file = NamedTempFile::new().unwrap();
let layout = PartitionLayout::calculate(size).unwrap();
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(temp_file.path())
.unwrap();
file.set_len(size).unwrap();
crate::exfat::format::format_exfat(
&mut file,
layout.data_offset(),
layout.data_size(),
"TEST",
)
.unwrap();
(temp_file, layout)
}
#[test]
fn test_no_fat_chain_directory_ignores_large_stale_fat_chain() -> Result<()> {
const IMAGE_SIZE: u64 = 256 * 1024 * 1024 * 1024;
const STALE_CHAIN_CLUSTERS: usize = 131_074;
let (temp_file, layout) = format_test_image(IMAGE_SIZE);
let path = temp_file.path();
let mut fs = ExfatFs::open(path, &layout)?;
fs.create_directory("/external", true)?;
let root = fs.root_directory();
let mut external = fs
.find_entry_in_directory(&root, "external")?
.expect("external directory");
assert_eq!(fs.cluster_size, 128 * 1024);
fs.update_entry_allocation(&mut external, true, fs.cluster_size as u64)?;
let first_cluster = external.first_cluster;
drop(fs);
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)?;
let mut boot_sector = [0u8; 512];
file.seek(SeekFrom::Start(layout.data_offset()))?;
file.read_exact(&mut boot_sector)?;
let fat_offset = u32::from_le_bytes(boot_sector[80..84].try_into().unwrap());
let mut stale_fat = Vec::with_capacity(STALE_CHAIN_CLUSTERS * 4);
for index in 0..STALE_CHAIN_CLUSTERS {
let next = if index + 1 == STALE_CHAIN_CLUSTERS {
FAT_ENTRY_END_OF_CHAIN
} else {
first_cluster + index as u32 + 1
};
stale_fat.extend_from_slice(&next.to_le_bytes());
}
file.seek(SeekFrom::Start(
layout.data_offset() + fat_offset as u64 * 512 + first_cluster as u64 * 4,
))?;
file.write_all(&stale_fat)?;
file.flush()?;
drop(file);
let mut fs = ExfatFs::open(path, &layout)?;
let files = fs.list_files_at("/external")?;
assert!(files.is_empty());
// The old reservation formula requested exactly 11,450,624,704 bytes on 64-bit.
if std::mem::size_of::<FileInfo>() == 64 {
let old_reservation = (STALE_CHAIN_CLUSTERS * (fs.cluster_size as usize / 96) + 1) * 64;
assert_eq!(old_reservation, 11_450_624_704);
}
Ok(())
}
#[test]
fn test_no_fat_chain_file_reads_and_frees_contiguous_allocation() -> Result<()> {
let (temp_file, layout) = format_test_image(64 * 1024 * 1024);
let mut fs = ExfatFs::open(temp_file.path(), &layout)?;
let data = vec![0x5A; fs.cluster_size as usize * 3 - 17];
fs.write_file("contiguous.bin", &data)?;
let mut location = fs
.find_file_entry("contiguous.bin")?
.expect("contiguous file");
let original_clusters = fs.read_cluster_chain(location.first_cluster)?;
assert_eq!(original_clusters.len(), 3);
assert!(original_clusters
.windows(2)
.all(|pair| pair[1] == pair[0] + 1));
let stale_first = fs.allocate_clusters(2)?;
fs.update_entry_allocation(&mut location, true, data.len() as u64)?;
fs.write_fat_entry(location.first_cluster, stale_first)?;
assert_eq!(fs.read_file("contiguous.bin")?, data);
fs.delete_file("contiguous.bin")?;
let bitmap = fs.read_bitmap()?;
assert!(original_clusters
.iter()
.all(|&cluster| !ExfatFs::is_cluster_allocated(&bitmap, cluster)));
assert!(ExfatFs::is_cluster_allocated(&bitmap, stale_first));
assert_eq!(fs.read_fat_entry(location.first_cluster)?, stale_first);
Ok(())
}
#[test]
fn test_no_fat_chain_directory_supports_management_and_recursive_delete() -> Result<()> {
let (temp_file, layout) = format_test_image(64 * 1024 * 1024);
let mut fs = ExfatFs::open(temp_file.path(), &layout)?;
fs.create_directory("/external", true)?;
fs.write_file_path("/external/first.txt", b"first", false, false)?;
let root = fs.root_directory();
let mut external = fs
.find_entry_in_directory(&root, "external")?
.expect("external directory");
let stale_first = fs.allocate_clusters(2)?;
fs.update_entry_allocation(&mut external, true, fs.cluster_size as u64)?;
fs.write_fat_entry(external.first_cluster, stale_first)?;
assert_eq!(fs.list_files_at("/external")?.len(), 1);
fs.write_file_path("/external/second.txt", b"second", false, false)?;
let recursive = fs.list_files_recursive()?;
assert!(recursive
.iter()
.any(|file| file.path == "external/first.txt"));
assert!(recursive
.iter()
.any(|file| file.path == "external/second.txt"));
let directory_cluster = external.first_cluster;
fs.delete_recursive("/external")?;
let bitmap = fs.read_bitmap()?;
assert!(!ExfatFs::is_cluster_allocated(&bitmap, directory_cluster));
assert!(ExfatFs::is_cluster_allocated(&bitmap, stale_first));
Ok(())
}
#[test]
fn test_full_no_fat_chain_directory_converts_to_fat_chain() -> Result<()> {
let (temp_file, layout) = format_test_image(64 * 1024 * 1024);
let path = temp_file.path();
let mut fs = ExfatFs::open(path, &layout)?;
fs.create_directory("/external", true)?;
let root = fs.root_directory();
let mut external = fs
.find_entry_in_directory(&root, "external")?
.expect("external directory");
fs.update_entry_allocation(&mut external, true, fs.cluster_size as u64)?;
for index in 0..43 {
fs.write_file_path(&format!("/external/f{index}.txt"), &[], false, false)?;
}
let root = fs.root_directory();
let external = fs
.find_entry_in_directory(&root, "external")?
.expect("external directory");
assert!(!external.no_fat_chain);
assert_eq!(external.data_length, fs.cluster_size as u64 * 2);
let directory = DirectoryLocation::from_entry(external)?;
assert_eq!(fs.directory_clusters(&directory)?.len(), 2);
drop(fs);
let mut reopened = ExfatFs::open(path, &layout)?;
assert_eq!(reopened.list_files_at("/external")?.len(), 43);
Ok(())
}
#[test]
fn test_fat_chain_cycle_is_rejected() -> Result<()> {
let (temp_file, layout) = format_test_image(64 * 1024 * 1024);
let mut fs = ExfatFs::open(temp_file.path(), &layout)?;
let first = fs.allocate_clusters(2)?;
let second = fs.read_fat_entry(first)?;
fs.write_fat_entry(second, first)?;
let error = fs.read_cluster_chain(first).unwrap_err();
assert!(error.to_string().contains("cycle"));
Ok(())
}
/// Test directory extension when filling up a directory cluster
#[test]
fn test_directory_extension() -> Result<()> {
// Create a small test image (64MB minimum) with small cluster size (4KB)
// This makes it easier to fill up a directory cluster
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
// Create 64MB image (minimum size)
let size = 64 * 1024 * 1024u64;
let layout = PartitionLayout::calculate(size).unwrap();
// Initialize file
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
.unwrap();
file.set_len(size).unwrap();
// Format data partition (this will use 4KB clusters for 64MB volume)
crate::exfat::format::format_exfat(
&mut file,
layout.data_offset(),
layout.data_size(),
"TEST",
)
.unwrap();
drop(file);
// Open filesystem
let mut fs = ExfatFs::open(path, &layout).unwrap();
// Calculate how many files fit in one 4KB cluster
// Each file needs: 1 file entry (32 bytes) + 1 stream entry (32 bytes) + name entries
// For short names (e.g., "f1.txt"), we need 1 name entry (32 bytes)
// Total: 3 * 32 = 96 bytes per file
// 4KB = 4096 bytes, so ~42 files per cluster
// But root directory already has volume label, bitmap, upcase entries
// Let's add 50 small files to ensure we exceed one cluster
// Add many small files to fill up the first directory cluster
for i in 0..50 {
let filename = format!("file{}.txt", i);
let data = format!("content {}", i);
let mut cursor = Cursor::new(data.as_bytes());
fs.write_file_from_reader(&filename, &mut cursor, data.len() as u64)?;
}
// Verify all files were created
let files = fs.list_files().unwrap();
assert_eq!(files.len(), 50, "Expected 50 files, found {}", files.len());
// Verify we can read all files back
for i in 0..50 {
let filename = format!("file{}.txt", i);
let expected = format!("content {}", i);
let data = fs.read_file(&filename).unwrap();
let content = String::from_utf8(data).unwrap();
assert_eq!(content, expected, "File {} content mismatch", filename);
}
// Verify directory chain was extended
// Root directory starts at one cluster, should now have multiple clusters
let root_chain = fs.read_cluster_chain(fs.first_cluster_of_root).unwrap();
assert!(
root_chain.len() > 1,
"Expected directory to extend beyond 1 cluster, got {} clusters",
root_chain.len()
);
Ok(())
}
/// Test streaming file reader
#[test]
fn test_streaming_read() -> Result<()> {
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
// Create 64MB image
let size = 64 * 1024 * 1024u64;
let layout = PartitionLayout::calculate(size).unwrap();
// Initialize file
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
.unwrap();
file.set_len(size).unwrap();
// Format data partition
crate::exfat::format::format_exfat(
&mut file,
layout.data_offset(),
layout.data_size(),
"TEST",
)
.unwrap();
drop(file);
// Open filesystem and write test file
let mut fs = ExfatFs::open(path, &layout).unwrap();
// Create test data spanning multiple clusters (4KB cluster size for 64MB)
// Create 20KB of test data (5 clusters)
let test_data: Vec<u8> = (0..20480).map(|i| (i % 256) as u8).collect();
let mut cursor = Cursor::new(&test_data);
fs.write_file_from_reader("large_file.bin", &mut cursor, test_data.len() as u64)?;
// Test 1: Stream read entire file using ExfatFileReader
{
let mut reader = ExfatFileReader::open(&mut fs, "large_file.bin")?;
assert_eq!(reader.file_size(), test_data.len() as u64);
assert_eq!(reader.position(), 0);
let mut read_data = Vec::new();
let bytes_read = reader
.read_to_end(&mut read_data)
.map_err(|e| VentoyError::Io(e))?;
assert_eq!(bytes_read, test_data.len());
assert_eq!(read_data, test_data);
}
// Test 2: Stream read with small buffer (simulating streaming)
{
let mut reader = ExfatFileReader::open(&mut fs, "large_file.bin")?;
let mut read_data = Vec::new();
let mut buffer = [0u8; 1024]; // 1KB buffer
loop {
let n = reader.read(&mut buffer).map_err(|e| VentoyError::Io(e))?;
if n == 0 {
break;
}
read_data.extend_from_slice(&buffer[..n]);
}
assert_eq!(read_data, test_data);
}
// Test 3: Seek operations
{
let mut reader = ExfatFileReader::open(&mut fs, "large_file.bin")?;
// Seek to middle
reader
.seek(SeekFrom::Start(10000))
.map_err(|e| VentoyError::Io(e))?;
assert_eq!(reader.position(), 10000);
let mut buffer = [0u8; 10];
reader
.read_exact(&mut buffer)
.map_err(|e| VentoyError::Io(e))?;
assert_eq!(&buffer, &test_data[10000..10010]);
// Seek from current position
reader
.seek(SeekFrom::Current(-5))
.map_err(|e| VentoyError::Io(e))?;
assert_eq!(reader.position(), 10005);
// Seek from end
reader
.seek(SeekFrom::End(-100))
.map_err(|e| VentoyError::Io(e))?;
assert_eq!(reader.position(), test_data.len() as u64 - 100);
reader
.read_exact(&mut buffer)
.map_err(|e| VentoyError::Io(e))?;
let expected_start = test_data.len() - 100;
assert_eq!(&buffer, &test_data[expected_start..expected_start + 10]);
}
// Test 4: read_file_to_writer streaming API
{
let mut output = Vec::new();
fs.read_file_to_writer("large_file.bin", &mut output)?;
assert_eq!(output, test_data);
}
Ok(())
}
#[test]
fn test_write_transactions_clear_volume_dirty_on_success() -> Result<()> {
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
let size = 64 * 1024 * 1024u64;
let layout = PartitionLayout::calculate(size).unwrap();
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
.unwrap();
file.set_len(size).unwrap();
crate::exfat::format::format_exfat(
&mut file,
layout.data_offset(),
layout.data_size(),
"TEST",
)
.unwrap();
drop(file);
let mut fs = ExfatFs::open(path, &layout).unwrap();
assert!(!fs.is_volume_dirty()?);
let data = b"uploaded from web";
let mut cursor = Cursor::new(data);
fs.write_file_from_reader_path(
"/uploads/test.txt",
&mut cursor,
data.len() as u64,
true,
true,
)?;
assert!(!fs.is_volume_dirty()?);
fs.delete_recursive("/uploads")?;
assert!(!fs.is_volume_dirty()?);
Ok(())
}
#[test]
fn test_created_directory_has_allocated_data_length() -> Result<()> {
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
let size = 64 * 1024 * 1024u64;
let layout = PartitionLayout::calculate(size).unwrap();
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
.unwrap();
file.set_len(size).unwrap();
crate::exfat::format::format_exfat(
&mut file,
layout.data_offset(),
layout.data_size(),
"TEST",
)
.unwrap();
drop(file);
let mut fs = ExfatFs::open(path, &layout).unwrap();
fs.create_directory("/uploads", true)?;
let root = fs.root_directory();
let location = fs
.find_entry_in_directory(&root, "uploads")?
.expect("created directory entry should exist");
assert!(location.is_directory);
assert!(location.first_cluster >= 2);
assert_eq!(location.data_length, fs.cluster_size as u64);
Ok(())
}
#[test]
fn test_open_uses_bitmap_location_from_root_directory() -> Result<()> {
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
let size = 64 * 1024 * 1024u64;
let layout = PartitionLayout::calculate(size).unwrap();
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
.unwrap();
file.set_len(size).unwrap();
crate::exfat::format::format_exfat(
&mut file,
layout.data_offset(),
layout.data_size(),
"TEST",
)
.unwrap();
let mut boot_sector = [0u8; 512];
file.seek(SeekFrom::Start(layout.data_offset())).unwrap();
file.read_exact(&mut boot_sector).unwrap();
let fat_offset = u32::from_le_bytes(boot_sector[80..84].try_into().unwrap());
let cluster_heap_offset = u32::from_le_bytes(boot_sector[88..92].try_into().unwrap());
let first_cluster_of_root = u32::from_le_bytes(boot_sector[96..100].try_into().unwrap());
let cluster_size = (1u64 << boot_sector[109]) * 512;
let relocated_bitmap_cluster = first_cluster_of_root + 1;
let mut bitmap = vec![0u8; cluster_size as usize];
file.seek(SeekFrom::Start(cluster_offset(
layout.data_offset(),
cluster_heap_offset,
cluster_size,
2,
)))
.unwrap();
file.read_exact(&mut bitmap).unwrap();
let relocated_index = (relocated_bitmap_cluster - 2) as usize;
bitmap[relocated_index / 8] |= 1 << (relocated_index % 8);
file.seek(SeekFrom::Start(cluster_offset(
layout.data_offset(),
cluster_heap_offset,
cluster_size,
relocated_bitmap_cluster,
)))
.unwrap();
file.write_all(&bitmap).unwrap();
let relocated_fat_offset =
layout.data_offset() + fat_offset as u64 * 512 + relocated_bitmap_cluster as u64 * 4;
file.seek(SeekFrom::Start(relocated_fat_offset)).unwrap();
file.write_all(&FAT_ENTRY_END_OF_CHAIN.to_le_bytes())
.unwrap();
let root_offset = cluster_offset(
layout.data_offset(),
cluster_heap_offset,
cluster_size,
first_cluster_of_root,
);
file.seek(SeekFrom::Start(root_offset + 32 + 20)).unwrap();
file.write_all(&relocated_bitmap_cluster.to_le_bytes())
.unwrap();
file.flush().unwrap();
drop(file);
let mut fs = ExfatFs::open(path, &layout).unwrap();
assert_eq!(fs.allocation_bitmap_first_cluster, relocated_bitmap_cluster);
assert!(ExfatFs::is_cluster_allocated(
&fs.read_bitmap()?,
relocated_bitmap_cluster
));
let data = b"uses relocated bitmap";
let mut cursor = Cursor::new(data);
fs.write_file_from_reader("relocated.txt", &mut cursor, data.len() as u64)?;
assert_eq!(fs.read_file("relocated.txt")?, &data[..]);
Ok(())
}
/// Test Unicode file names (CJK, Cyrillic, emoji, etc.)
#[test]
fn test_unicode_filenames() -> Result<()> {
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
// Create 64MB image
let size = 64 * 1024 * 1024u64;
let layout = PartitionLayout::calculate(size).unwrap();
// Initialize file
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
.unwrap();
file.set_len(size).unwrap();
// Format data partition
crate::exfat::format::format_exfat(
&mut file,
layout.data_offset(),
layout.data_size(),
"TEST",
)
.unwrap();
drop(file);
// Open filesystem
let mut fs = ExfatFs::open(path, &layout).unwrap();
// Test 1: CJK characters (Chinese, Japanese, Korean)
let cjk_content = b"Hello from CJK file";
fs.write_file("中文文件.txt", cjk_content)?;
fs.write_file("日本語ファイル.txt", cjk_content)?;
fs.write_file("한국어파일.txt", cjk_content)?;
// Verify CJK files exist and can be read
let read_data = fs.read_file("中文文件.txt")?;
assert_eq!(read_data, cjk_content);
let read_data = fs.read_file("日本語ファイル.txt")?;
assert_eq!(read_data, cjk_content);
let read_data = fs.read_file("한국어파일.txt")?;
assert_eq!(read_data, cjk_content);
// Test 2: Cyrillic characters (Russian)
let cyrillic_content = b"Cyrillic content";
fs.write_file("Русский файл.txt", cyrillic_content)?;
let read_data = fs.read_file("Русский файл.txt")?;
assert_eq!(read_data, cyrillic_content);
// Test 3: Latin Extended (accented characters)
let latin_content = b"Latin extended content";
fs.write_file("Ñoño_Résumé_Naïve.txt", latin_content)?;
let read_data = fs.read_file("Ñoño_Résumé_Naïve.txt")?;
assert_eq!(read_data, latin_content);
// Test 4: Greek characters
let greek_content = b"Greek content";
fs.write_file("Ελληνικά.txt", greek_content)?;
let read_data = fs.read_file("Ελληνικά.txt")?;
assert_eq!(read_data, greek_content);
// Test 5: Emoji (surrogate pairs in UTF-16)
let emoji_content = b"Emoji content";
fs.write_file("😀🎉🚀.txt", emoji_content)?;
let read_data = fs.read_file("😀🎉🚀.txt")?;
assert_eq!(read_data, emoji_content);
// Test 6: Mixed script file name
let mixed_content = b"Mixed content";
fs.write_file("Hello世界Привет🌍.txt", mixed_content)?;
let read_data = fs.read_file("Hello世界Привет🌍.txt")?;
assert_eq!(read_data, mixed_content);
// Test 7: List all files and verify Unicode names are preserved
let files = fs.list_files()?;
let file_names: Vec<&str> = files.iter().map(|f| f.name.as_str()).collect();
assert!(file_names.contains(&"中文文件.txt"));
assert!(file_names.contains(&"日本語ファイル.txt"));
assert!(file_names.contains(&"한국어파일.txt"));
assert!(file_names.contains(&"Русский файл.txt"));
assert!(file_names.contains(&"Ñoño_Résumé_Naïve.txt"));
assert!(file_names.contains(&"Ελληνικά.txt"));
assert!(file_names.contains(&"😀🎉🚀.txt"));
assert!(file_names.contains(&"Hello世界Привет🌍.txt"));
// Test 8: Delete Unicode file
fs.delete_file("😀🎉🚀.txt")?;
let files = fs.list_files()?;
let file_names: Vec<&str> = files.iter().map(|f| f.name.as_str()).collect();
assert!(!file_names.contains(&"😀🎉🚀.txt"));
Ok(())
}
}