//! 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, } 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 { 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, first_cluster: u32, } impl DirectoryLocation { fn root(first_cluster: u32) -> Self { Self { entry: None, first_cluster, } } fn from_entry(entry: FileEntryLocation) -> Result { 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, } /// 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>, /// 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 { 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 { 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 { 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 { let was_dirty = self.is_volume_dirty()?; if !was_dirty { self.set_volume_dirty(true)?; } Ok(was_dirty) } fn finish_write_transaction(&mut self, was_dirty: bool, result: Result) -> Result { 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> { 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 { // 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> { 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> { self.read_cluster_chain_limited(first_cluster, self.cluster_count as usize) } fn clusters_for_allocation(&mut self, location: &FileEntryLocation) -> Result> { 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> { 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> { 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> { 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> { 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> { 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 { 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 = 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> { 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 = (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> { 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 { 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(¤t, 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(¤t, &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 { 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 { 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 { // 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> { 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 = (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> { 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> { 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> { 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, ¤t_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> { 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> { 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> { 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> { 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, current_cluster_index: usize, cluster_buffer: Vec, 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 { 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 { 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 { 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 { // 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 { 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, /// 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)>, } impl<'a> ExfatFileReader<'a> { /// Open a file for reading from root directory pub fn open(fs: &'a mut ExfatFs, name: &str) -> Result { 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 { 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 { 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 { 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 { 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(&mut self, name: &str, writer: &mut W) -> Result { 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( &mut self, path: &str, writer: &mut W, ) -> Result { 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(reader: &mut ExfatFileReader, writer: &mut W) -> Result { 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( &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( &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( &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(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::() == 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 = (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(()) } }