From f79db1f54ea91f0a070659ea492a85792c400aa3 Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sun, 18 Aug 2024 16:21:37 +0300 Subject: [PATCH 01/40] feat(write): implement Write for File & Seek beyond EOF allocates more clusters --- src/fs.rs | 424 ++++++++++++++++++++++++++++++++++++++++++++++++++---- src/io.rs | 8 +- 2 files changed, 405 insertions(+), 27 deletions(-) diff --git a/src/fs.rs b/src/fs.rs index d1ad8ad..907be04 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -272,8 +272,8 @@ struct FSInfoFAT32 { #[serde(with = "BigArray")] _reserved1: [u8; 480], mid_signature: u32, - last_free_cluster: u32, - cluster_width: u32, + free_cluster_count: u32, + first_free_cluster: u32, _reserved2: [u8; 12], trail_signature: u32, } @@ -308,6 +308,12 @@ impl FATType { FATType::ExFAT => 32, } } + + #[inline] + /// How many bytes this [`FATType`] spans across + fn entry_size(&self) -> u32 { + self.bits_per_entry().next_power_of_two() as u32 / 8 + } } #[derive(Debug, Clone, PartialEq)] @@ -324,6 +330,24 @@ enum FATEntry { EOF, } +impl From for u32 { + fn from(value: FATEntry) -> Self { + Self::from(&value) + } +} + +impl From<&FATEntry> for u32 { + fn from(value: &FATEntry) -> Self { + match value { + FATEntry::Free => u32::MIN, + FATEntry::Allocated(cluster) => *cluster, + FATEntry::Reserved => 0xFFFFFF6, + FATEntry::Bad => 0xFFFFFF7, + FATEntry::EOF => u32::MAX, + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone, Copy)] struct SFN { name: [u8; 8], @@ -678,6 +702,15 @@ where } } +impl ops::DerefMut for File<'_, S> +where + S: Read + Write + Seek, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.entry + } +} + impl IOBase for File<'_, S> where S: Read + Write + Seek, @@ -700,6 +733,22 @@ where Ok(()) } + + /// Returns that last cluster in the file's cluster chain + fn last_cluster_in_chain(&mut self) -> Result::Error> { + // we begin from the current cluster to save some time + let mut current_cluster = self.current_cluster; + + loop { + match self.fs.read_nth_FAT_entry(current_cluster)? { + FATEntry::Allocated(next_cluster) => current_cluster = next_cluster, + FATEntry::EOF => break, + _ => unreachable!(), + } + } + + Ok(current_cluster) + } } impl Read for File<'_, S> @@ -787,12 +836,78 @@ where } } +impl Write for File<'_, S> +where + S: Read + Write + Seek, +{ + fn write(&mut self, buf: &[u8]) -> Result { + // allocate clusters + self.seek(SeekFrom::Current(buf.len() as i64))?; + // rewind back to where we were + self.seek(SeekFrom::Current(-(buf.len() as i64)))?; + + let mut bytes_written = 0; + + 'outer: loop { + log::trace!("writing file data to cluster: {}", self.current_cluster); + + let sector_init_offset = u32::try_from(self.offset % self.fs.cluster_size()).unwrap() + / self.fs.sector_size(); + let first_sector_of_cluster = self + .fs + .data_cluster_to_partition_sector(self.current_cluster) + + sector_init_offset; + let last_sector_of_cluster = first_sector_of_cluster + + self.fs.sectors_per_cluster() as u32 + - sector_init_offset + - 1; + for sector in first_sector_of_cluster..=last_sector_of_cluster { + self.fs.read_nth_sector(sector.into())?; + + let start_index = self.offset as usize % self.fs.sector_size() as usize; + + let bytes_to_write = cmp::min( + buf.len() - bytes_written, + self.fs.sector_size() as usize - start_index, + ); + + self.fs.sector_buffer[start_index..start_index + bytes_to_write] + .copy_from_slice(&buf[bytes_written..bytes_written + bytes_to_write]); + self.fs.buffer_modified = true; + + bytes_written += bytes_to_write; + self.offset += bytes_to_write as u64; + + // if we have written as many bytes as we want... + if bytes_written >= buf.len() { + // ...but we must process get the next cluster for future uses, + // we do that before breaking + if self.offset % self.fs.cluster_size() == 0 { + self.next_cluster()?; + } + + break 'outer; + } + } + + self.next_cluster()?; + } + + Ok(bytes_written) + } + + // everything is immediately written to the storage medium + fn flush(&mut self) -> Result<(), Self::Error> { + Ok(()) + } +} + impl Seek for File<'_, S> where S: Read + Write + Seek, { fn seek(&mut self, pos: SeekFrom) -> Result { - let mut offset = match pos { + let offset = match pos { SeekFrom::Start(offset) => offset, SeekFrom::Current(offset) => { let offset = self.offset as i64 + offset; @@ -804,9 +919,60 @@ where } }; - if offset > self.file_size.into() { - log::debug!("Capping cursor offset to file_size"); - offset = self.file_size.into(); + // in case the cursor goes beyond the EOF, allocate more clusters + if offset > (self.file_size as u64).next_multiple_of(self.fs.cluster_size()) { + let clusters_to_allocate = (offset + - (self.file_size as u64).next_multiple_of(self.fs.cluster_size())) + .div_ceil(self.fs.cluster_size()) + + 1; + log::debug!( + "Seeking beyond EOF, allocating {} more clusters", + clusters_to_allocate + ); + + let mut last_cluster_in_chain = self.last_cluster_in_chain()?; + + for clusters_allocated in 0..clusters_to_allocate { + match self.fs.next_free_cluster()? { + Some(next_free_cluster) => { + // we set the last allocated cluster to point to the next free one + self.fs.write_nth_FAT_entry( + last_cluster_in_chain, + FATEntry::Allocated(next_free_cluster), + )?; + // we also set the next free cluster to be EOF + self.fs + .write_nth_FAT_entry(next_free_cluster, FATEntry::EOF)?; + log::trace!( + "cluster {} now points to {}", + last_cluster_in_chain, + next_free_cluster + ); + // now the next free cluster i the last allocated one + last_cluster_in_chain = next_free_cluster; + } + None => { + self.file_size = (((self.file_size as u64) + .next_multiple_of(self.fs.cluster_size()) + - offset) + + clusters_allocated * self.fs.cluster_size()) + as u32; + self.offset = self.file_size.into(); + + log::error!("storage medium full while attempting to allocated more clusters for a File"); + return Err(IOError::new( + ::Kind::new_unexpected_eof(), + "the storage medium is full, can't increase size of file", + )); + } + } + } + + self.file_size = offset as u32; + log::debug!( + "New file size after reallocation is {} bytes", + self.file_size + ); } log::debug!( @@ -823,7 +989,7 @@ where // (perhaps we could follow a similar approach to elm-chan's FATFS, by using a cluster link map table, perhaps as an optional feature) self.offset = 0; self.current_cluster = self.data_cluster; - self.seek(pos)?; + self.seek(SeekFrom::Start(offset))?; } Ordering::Equal => (), Ordering::Greater => { @@ -845,7 +1011,7 @@ where S: Read + Write + Seek, { fn cluster_chain_is_healthy(&mut self) -> Result { - let mut current_cluster = self.entry.data_cluster; + let mut current_cluster = self.data_cluster; let mut cluster_count = 0; loop { @@ -910,6 +1076,7 @@ trait OffsetConversions { struct FSProperties { sector_size: u32, cluster_size: u64, + total_sectors: u32, total_clusters: u32, /// sector offset of the FAT fat_offset: u32, @@ -937,6 +1104,8 @@ where /// The length of this will be the sector size of the FS for all FAT types except FAT12, in that case, it will be double that value sector_buffer: Vec, + /// ANY CHANGES TO THE SECTOR BUFFER SHOULD ALSO SET THIS TO TRUE + buffer_modified: bool, stored_sector: u64, boot_record: BootRecord, @@ -1064,6 +1233,11 @@ where BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), }; + let total_sectors = match boot_record { + BootRecord::FAT(boot_record_fat) => boot_record_fat.total_sectors(), + BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), + }; + let total_clusters = match boot_record { BootRecord::FAT(boot_record_fat) => boot_record_fat.total_clusters(), BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), @@ -1073,6 +1247,7 @@ where sector_size, cluster_size, fat_offset, + total_sectors, total_clusters, first_data_sector, }; @@ -1080,6 +1255,7 @@ where Ok(Self { storage, sector_buffer: buffer[..sector_size as usize].to_vec(), + buffer_modified: false, stored_sector, boot_record, fat_type, @@ -1181,6 +1357,29 @@ where } } +/// Properties about the position of a [`FATEntry`] inside the FAT region +struct FATEntryProps { + fat_sector: u32, + sector_offset: usize, +} + +impl FATEntryProps { + /// Get the [`FATEntryProps`] of the `n`-th [`FATEntry`] of a [`FileSystem`] (`fs`) + pub fn new(n: u32, fs: &FileSystem) -> Self + where + S: Read + Write + Seek, + { + let fat_byte_offset: u32 = n * fs.fat_type.bits_per_entry() as u32 / 8; + let fat_sector: u32 = fs.props.fat_offset + fat_byte_offset / fs.props.sector_size; + let sector_offset: usize = (fat_byte_offset % fs.props.sector_size) as usize; + + FATEntryProps { + fat_sector, + sector_offset, + } + } +} + /// Internal low-level functions impl FileSystem where @@ -1339,48 +1538,109 @@ where Ok(entries) } + /// Gets the next free cluster. Returns an IO [`Result`] + /// If the [`Result`] returns [`Ok`] that contains a [`None`], the drive is full + fn next_free_cluster(&mut self) -> Result, S::Error> { + let start_cluster = match self.boot_record { + BootRecord::FAT(boot_record_fat) => { + // the first 2 entries are reserved + let mut first_free_cluster = 2; + + if let EBR::FAT32(_, fsinfo) = boot_record_fat.ebr { + // a value of u32::MAX denotes unawareness of the first free cluster + // we also do a bit of range checking + // TODO: if this is unknown, figure it out and write it to the FSInfo structure + if fsinfo.first_free_cluster != u32::MAX + && fsinfo.first_free_cluster <= self.props.total_sectors + { + first_free_cluster = fsinfo.first_free_cluster + } + } + + first_free_cluster + } + BootRecord::ExFAT(_) => todo!("ExFAT not yet implemented"), + }; + + let mut current_cluster = start_cluster; + + while current_cluster < self.props.total_clusters { + match self.read_nth_FAT_entry(current_cluster)? { + FATEntry::Free => return Ok(Some(current_cluster)), + _ => (), + } + current_cluster += 1; + } + + Ok(None) + } + /// Read the nth sector from the partition's beginning and store it in [`self.sector_buffer`](Self::sector_buffer) /// /// This function also returns an immutable reference to [`self.sector_buffer`](Self::sector_buffer) fn read_nth_sector(&mut self, n: u64) -> Result<&Vec, S::Error> { // nothing to do if the sector we wanna read is already cached if n != self.stored_sector { + // let's sync the current sector first + self.sync_sector_buffer()?; self.storage.seek(SeekFrom::Start( self.sector_to_partition_offset(n as u32).into(), ))?; self.storage.read_exact(&mut self.sector_buffer)?; + self.storage + .seek(SeekFrom::Current(-i64::from(self.props.sector_size)))?; + self.stored_sector = n; } Ok(&self.sector_buffer) } + fn sync_sector_buffer(&mut self) -> Result<(), S::Error> { + if self.buffer_modified { + log::trace!("syncing sector {:?}", self.stored_sector); + self.storage.write_all(&self.sector_buffer)?; + self.storage + .seek(SeekFrom::Current(-i64::from(self.props.sector_size)))?; + } + self.buffer_modified = false; + + Ok(()) + } + #[allow(non_snake_case)] - fn read_nth_FAT_entry(&mut self, n: u32) -> Result { + /// Returns the bytes occupied by the FAT entry, padded up to 4 + fn internal_read_nth_FAT_entry_to_bytes(&mut self, n: u32) -> Result<[u8; 4], S::Error> { // the size of an entry rounded up to bytes - let entry_size = self.fat_type.bits_per_entry().next_power_of_two() as u32 / 8; - let fat_offset: u32 = n * self.fat_type.bits_per_entry() as u32 / 8; - let fat_sector_offset = self.props.fat_offset + fat_offset / self.props.sector_size; - let entry_offset: usize = (fat_offset % self.props.sector_size) as usize; + let entry_size = self.fat_type.entry_size(); + let entry_props = FATEntryProps::new(n, &self); - self.read_nth_sector(fat_sector_offset.into())?; + self.read_nth_sector(entry_props.fat_sector.into())?; let mut value_bytes = [0_u8; 4]; let bytes_to_read: usize = cmp::min( - entry_offset + entry_size as usize, + entry_props.sector_offset + entry_size as usize, self.sector_size() as usize, - ) - entry_offset; - value_bytes[..bytes_to_read] - .copy_from_slice(&self.sector_buffer[entry_offset..entry_offset + bytes_to_read]); // this shouldn't panic + ) - entry_props.sector_offset; + value_bytes[..bytes_to_read].copy_from_slice( + &self.sector_buffer + [entry_props.sector_offset..entry_props.sector_offset + bytes_to_read], + ); // this shouldn't panic // in FAT12, FAT entries may be split between two different sectors if self.fat_type == FATType::FAT12 && (bytes_to_read as u32) < entry_size { - self.read_nth_sector((fat_sector_offset + 1).into())?; + self.read_nth_sector((entry_props.fat_sector + 1).into())?; value_bytes[bytes_to_read..entry_size as usize] .copy_from_slice(&self.sector_buffer[..(entry_size as usize - bytes_to_read)]); - } + }; + Ok(value_bytes) + } + + #[allow(non_snake_case)] + fn read_nth_FAT_entry(&mut self, n: u32) -> Result { + let value_bytes = self.internal_read_nth_FAT_entry_to_bytes(n)?; let mut value = u32::from_le_bytes(value_bytes); match self.fat_type { // FAT12 entries are split between different bytes @@ -1443,6 +1703,65 @@ where FATType::ExFAT => todo!("ExFAT not yet implemented"), }) } + + #[allow(non_snake_case)] + fn write_nth_FAT_entry(&mut self, n: u32, entry: FATEntry) -> Result<(), S::Error> { + // the size of an entry rounded up to bytes + let entry_size = self.fat_type.entry_size(); + let entry_props = FATEntryProps::new(n, &self); + + let value_bytes = self.internal_read_nth_FAT_entry_to_bytes(n)?; + let mut old_value = u32::from_le_bytes(value_bytes); + + let mut mask = (1 << self.fat_type.bits_per_entry()) - 1; + let mut value: u32 = u32::from(entry.clone()) & mask; + + if self.fat_type == FATType::FAT12 { + if n & 1 != 0 { + // FAT12 entries are split between different bytes + value <<= 4; + mask <<= 4; + } + + // mask the old value and bitwise OR it with the new one + old_value &= !mask; + value |= old_value; + } + + // just in case we aren't in the correct sector + self.read_nth_sector(entry_props.fat_sector.into())?; + + let value_bytes = value.to_le_bytes(); + let bytes_to_write: usize = cmp::min( + entry_props.sector_offset + entry_size as usize, + self.sector_size() as usize, + ) - entry_props.sector_offset; + self.sector_buffer[entry_props.sector_offset..entry_props.sector_offset + bytes_to_write] + .copy_from_slice(&value_bytes[..bytes_to_write]); // this shouldn't panic + self.buffer_modified = true; + + if self.fat_type == FATType::FAT12 && bytes_to_write < entry_size as usize { + // looks like this FAT12 entry spans multiple sectors, we must also update the other one + self.read_nth_sector((entry_props.fat_sector + 1).into())?; + + self.sector_buffer[..(entry_size as usize - bytes_to_write)] + .copy_from_slice(&value_bytes[bytes_to_write..entry_size as usize]); + self.buffer_modified = true; + } + + Ok(()) + } +} + +impl ops::Drop for FileSystem +where + S: Read + Write + Seek, +{ + fn drop(&mut self) { + // nothing to do if these error out while dropping + let _ = self.sync_sector_buffer(); + let _ = self.storage.flush(); + } } #[cfg(all(test, feature = "std"))] @@ -1495,17 +1814,21 @@ mod tests { } static BEE_MOVIE_SCRIPT: &str = include_str!("../tests/bee movie script.txt"); + fn assert_vec_is_bee_movie_script(buf: &Vec) { + let string = str::from_utf8(&buf).unwrap(); + let expected_size = BEE_MOVIE_SCRIPT.len(); + assert_eq!(buf.len(), expected_size); + + assert_eq!(string, BEE_MOVIE_SCRIPT); + } fn assert_file_is_bee_movie_script(file: &mut File<'_, S>) where S: Read + Write + Seek, { - let mut file_string = String::new(); - let bytes_read = file.read_to_string(&mut file_string).unwrap(); + let mut buf = Vec::new(); + file.read_to_end(&mut buf).unwrap(); - let expected_filesize = BEE_MOVIE_SCRIPT.len(); - assert_eq!(bytes_read, expected_filesize); - - assert_eq!(file_string, BEE_MOVIE_SCRIPT); + assert_vec_is_bee_movie_script(&buf); } #[test] @@ -1553,6 +1876,55 @@ mod tests { ); } + #[test] + // this won't actually modify the .img file or the static slices, + // since we run .to_owned(), which basically clones the data in the static slices, + // in order to make the Cursor readable/writable + fn write_to_file() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT12.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let mut file = fs.get_file(PathBuf::from("/root.txt")).unwrap(); + + file.write_all(BEE_MOVIE_SCRIPT.as_bytes()).unwrap(); + file.rewind().unwrap(); + + assert_file_is_bee_movie_script(&mut file); + + // now let's do something else + // this write operations will happen between 2 clusters + const TEXT_OFFSET: u64 = 4598; + const TEXT: &str = "Hello from the other side"; + + file.seek(SeekFrom::Start(TEXT_OFFSET)).unwrap(); + file.write_all(TEXT.as_bytes()).unwrap(); + + // seek back to the start of where we wrote our text + file.seek(SeekFrom::Current(-(TEXT.len() as i64))).unwrap(); + let mut buf = [0_u8; TEXT.len()]; + file.read_exact(&mut buf).unwrap(); + let stored_text = str::from_utf8(&buf).unwrap(); + + assert_eq!(TEXT, stored_text); + + // we are also gonna write the bee movie ten more times to see if FAT12 can correctly handle split entries + for i in 0..10 { + log::debug!("Writing the bee movie script for the {i} consecutive time",); + + let start_offset = file.seek(SeekFrom::End(0)).unwrap(); + + file.write_all(BEE_MOVIE_SCRIPT.as_bytes()).unwrap(); + file.seek(SeekFrom::Start(start_offset)).unwrap(); + + let mut buf = vec![0_u8; BEE_MOVIE_SCRIPT.len()]; + file.read_exact(buf.as_mut_slice()).unwrap(); + + assert_vec_is_bee_movie_script(&buf); + } + } + #[test] fn read_file_in_subdir() { use std::io::Cursor; diff --git a/src/io.rs b/src/io.rs index 7276be9..d0d2fb2 100644 --- a/src/io.rs +++ b/src/io.rs @@ -267,7 +267,13 @@ pub trait Seek: IOBase { /// Seeking can fail, for example because it might involve flushing a buffer. /// /// Seeking to a negative offset is considered an error. - /// Seeking beyond the end of the stream should also be considered an error. + /// + /// Seeking beyond the end of the stream behaviour depends on the implementation. + /// If [`self`] can be extended (because it's a [`File`](fs::File) for example), this shouldn't error out. + /// In the case that a stream is being extended or if the stream can't be extended, + /// this should return an [`IOError`] with an [`IOErrorKind`] of `UnexpectedEOF`. + /// The [`seek`] operation should be considered partially successfull, since some clusters were allocated. + /// In general, for suchs cases, it is recommended that the user first checks if there's enough storage and then perform the action fn seek(&mut self, pos: SeekFrom) -> Result; /// Rewind to the beginning of a stream. From c689a6487ca9b3c3ae71faffe52f5b91719cd3bc Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sun, 18 Aug 2024 16:36:20 +0300 Subject: [PATCH 02/40] chore: remove unnecessary impl --- src/fs.rs | 46 +++++++++++++++++++++------------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/src/fs.rs b/src/fs.rs index 907be04..b25f8f6 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -749,6 +749,27 @@ where Ok(current_cluster) } + + /// Checks whether the cluster chain of this file is healthy or malformed + fn cluster_chain_is_healthy(&mut self) -> Result { + let mut current_cluster = self.data_cluster; + let mut cluster_count = 0; + + loop { + cluster_count += 1; + + if cluster_count * self.fs.cluster_size() >= self.file_size.into() { + break; + } + + match self.fs.read_nth_FAT_entry(current_cluster)? { + FATEntry::Allocated(next_cluster) => current_cluster = next_cluster, + _ => return Ok(false), + }; + } + + Ok(true) + } } impl Read for File<'_, S> @@ -1006,31 +1027,6 @@ where } } -impl File<'_, S> -where - S: Read + Write + Seek, -{ - fn cluster_chain_is_healthy(&mut self) -> Result { - let mut current_cluster = self.data_cluster; - let mut cluster_count = 0; - - loop { - cluster_count += 1; - - if cluster_count * self.fs.cluster_size() >= self.file_size.into() { - break; - } - - match self.fs.read_nth_FAT_entry(current_cluster)? { - FATEntry::Allocated(next_cluster) => current_cluster = next_cluster, - _ => return Ok(false), - }; - } - - Ok(true) - } -} - /// variation of https://stackoverflow.com/a/42067321/19247098 for processing LFNs pub(crate) fn string_from_lfn(utf16_src: &[u16]) -> Result { let nul_range_end = utf16_src From 9775f2a4ad3315e31d9ccd8fd1a63441114e0dd9 Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Mon, 19 Aug 2024 12:44:16 +0300 Subject: [PATCH 03/40] fix: seeking on a File wouldn't always happen correctly --- src/fs.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/fs.rs b/src/fs.rs index b25f8f6..fda9b3d 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1014,9 +1014,7 @@ where } Ordering::Equal => (), Ordering::Greater => { - for _ in (self.offset / self.fs.cluster_size()..offset / self.fs.cluster_size()) - .step_by(self.fs.cluster_size() as usize) - { + for _ in self.offset / self.fs.cluster_size()..offset / self.fs.cluster_size() { self.next_cluster()?; } self.offset = offset; From 6bd35347ab887c679ec410933ababa0d3222be56 Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Mon, 19 Aug 2024 13:16:17 +0300 Subject: [PATCH 04/40] feat(truncate): Public function to truncate a File down to a given size --- src/fs.rs | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 5 deletions(-) diff --git a/src/fs.rs b/src/fs.rs index fda9b3d..27b3580 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -718,18 +718,70 @@ where type Error = S::Error; } +/// Public functions +impl File<'_, S> +where + S: Read + Write + Seek, +{ + /// Truncates the file to a given size, deleting everything past the new EOF + /// + /// If `size` is greater or equal to the current file size + /// till the end of the last cluster allocated, this has no effect + /// + /// Furthermore, if the cursor point is beyond the new EOF, it will be moved there + pub fn truncate(&mut self, size: u32) -> Result<(), ::Error> { + // looks like the new truncated size would be smaller than the current one, so we just return + if size.next_multiple_of(self.fs.props.cluster_size as u32) >= self.file_size { + return Ok(()); + } + + // we store the current offset for later use + let previous_offset = cmp::min(self.offset, size.into()); + + // we seek back to where the EOF will be + self.seek(SeekFrom::Start(size.into()))?; + + // set what the new filesize will be + let previous_size = self.file_size; + self.file_size = size; + + let mut next_cluster_option = self.fs.get_next_cluster(self.current_cluster)?; + + // we set the new last cluster in the chain to be EOF + self.fs + .write_nth_FAT_entry(self.current_cluster, FATEntry::EOF)?; + + // then, we set each cluster after the current one to EOF + while let Some(next_cluster) = next_cluster_option { + next_cluster_option = self.fs.get_next_cluster(next_cluster)?; + + self.fs.write_nth_FAT_entry(next_cluster, FATEntry::Free)?; + } + + // don't forget to seek back to where we started + self.seek(SeekFrom::Start(previous_offset))?; + + log::debug!( + "Successfully truncated file {} from {} to {} bytes", + self.path, + previous_size, + self.file_size + ); + + Ok(()) + } +} + /// Internal functions impl File<'_, S> where S: Read + Write + Seek, { + #[inline] /// Panics if the current cluser doesn't point to another clluster fn next_cluster(&mut self) -> Result<(), ::Error> { - match self.fs.read_nth_FAT_entry(self.current_cluster)? { - FATEntry::Allocated(next_cluster) => self.current_cluster = next_cluster, - // when a `File` is created, `cluster_chain_is_healthy` is called, if it fails, that File is dropped - _ => unreachable!(), - }; + // when a `File` is created, `cluster_chain_is_healthy` is called, if it fails, that File is dropped + self.current_cluster = self.fs.get_next_cluster(self.current_cluster)?.unwrap(); Ok(()) } @@ -1569,6 +1621,15 @@ where Ok(None) } + /// Get the next cluster in a cluster chain, otherwise return [`None`] + fn get_next_cluster(&mut self, cluster: u32) -> Result, S::Error> { + Ok(match self.read_nth_FAT_entry(cluster)? { + FATEntry::Allocated(next_cluster) => Some(next_cluster), + // when a `File` is created, `cluster_chain_is_healthy` is called, if it fails, that File is dropped + _ => None, + }) + } + /// Read the nth sector from the partition's beginning and store it in [`self.sector_buffer`](Self::sector_buffer) /// /// This function also returns an immutable reference to [`self.sector_buffer`](Self::sector_buffer) @@ -1919,6 +1980,27 @@ mod tests { } } + #[test] + fn truncate_file() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT16.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let mut file = fs.get_file(PathBuf::from("/bee movie script.txt")).unwrap(); + + // we are gonna truncate the bee movie script down to 20 000 bytes + const NEW_SIZE: u32 = 20_000; + file.truncate(NEW_SIZE).unwrap(); + + let mut file_string = String::new(); + file.read_to_string(&mut file_string).unwrap(); + let mut expected_string = BEE_MOVIE_SCRIPT.to_string(); + expected_string.truncate(NEW_SIZE as usize); + + assert_eq!(file_string, expected_string); + } + #[test] fn read_file_in_subdir() { use std::io::Cursor; From a8ced4c0fff7fc76b7ba5334208cce81179cdf7b Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Tue, 20 Aug 2024 16:29:06 +0300 Subject: [PATCH 05/40] fix: also write FAT entries to the FAT table copies --- src/error.rs | 7 +++ src/fs.rs | 156 +++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 139 insertions(+), 24 deletions(-) diff --git a/src/error.rs b/src/error.rs index 7cc9c28..8b5a9e3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -98,6 +98,13 @@ pub enum InternalFSError { Perhaps the FSInfo structure or the FAT32 EBR's fat_info field is malformed? */ InvalidFSInfoSig, + /** + The FAT and it's copies do not much. + This is either the result of some bad FAT library that chose to ignore the FAT copies + or perhaps the storage medium has been corrupted (most likely). + Either way, we are not handling this FileSystem + */ + MismatchingFATTables, /// Encountered a malformed cluster chain MalformedClusterChain, } diff --git a/src/fs.rs b/src/fs.rs index 27b3580..f83ba9f 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -78,6 +78,21 @@ impl BootRecord { } } } + + #[allow(non_snake_case)] + fn nth_FAT_table_sector(&self, n: u8) -> u32 { + match self { + BootRecord::FAT(boot_record_fat) => { + boot_record_fat.first_fat_sector() as u32 + + n as u32 * boot_record_fat.fat_sector_size() + } + BootRecord::ExFAT(boot_record_exfat) => { + // this should work, but ExFAT is not yet implemented, so... + todo!("ExFAT not yet implemented"); + boot_record_exfat.fat_count as u32 + n as u32 * boot_record_exfat.fat_len + } + } + } } const BOOT_SIGNATURE: u8 = 0x29; @@ -1126,6 +1141,7 @@ struct FSProperties { total_clusters: u32, /// sector offset of the FAT fat_offset: u32, + fat_table_count: u8, first_data_sector: u32, } @@ -1279,6 +1295,11 @@ where BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), }; + let fat_table_count = match boot_record { + BootRecord::FAT(boot_record_fat) => boot_record_fat.bpb.table_count, + BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), + }; + let total_sectors = match boot_record { BootRecord::FAT(boot_record_fat) => boot_record_fat.total_sectors(), BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), @@ -1293,12 +1314,13 @@ where sector_size, cluster_size, fat_offset, + fat_table_count, total_sectors, total_clusters, first_data_sector, }; - Ok(Self { + let mut fs = Self { storage, sector_buffer: buffer[..sector_size as usize].to_vec(), buffer_modified: false, @@ -1306,7 +1328,15 @@ where boot_record, fat_type, props, - }) + }; + + if !fs.FAT_tables_are_identical()? { + return Err(FSError::InternalFSError( + InternalFSError::MismatchingFATTables, + )); + } + + Ok(fs) } /// Read all the entries of a directory ([`PathBuf`]) into [`Vec`] @@ -1405,7 +1435,8 @@ where /// Properties about the position of a [`FATEntry`] inside the FAT region struct FATEntryProps { - fat_sector: u32, + /// Each `n`th element of the vector points at the corrensponding sector at the `n+1`th FAT table + fat_sectors: Vec, sector_offset: usize, } @@ -1416,11 +1447,16 @@ impl FATEntryProps { S: Read + Write + Seek, { let fat_byte_offset: u32 = n * fs.fat_type.bits_per_entry() as u32 / 8; - let fat_sector: u32 = fs.props.fat_offset + fat_byte_offset / fs.props.sector_size; + let mut fat_sectors = Vec::new(); + for nth_table in 0..fs.props.fat_table_count { + let table_sector_offset = fs.boot_record.nth_FAT_table_sector(nth_table); + let fat_sector = table_sector_offset + fat_byte_offset / fs.props.sector_size; + fat_sectors.push(fat_sector); + } let sector_offset: usize = (fat_byte_offset % fs.props.sector_size) as usize; FATEntryProps { - fat_sector, + fat_sectors, sector_offset, } } @@ -1630,6 +1666,48 @@ where }) } + #[allow(non_snake_case)] + /// Check whether or not the all the FAT tables of the storage medium are identical to each other + fn FAT_tables_are_identical(&mut self) -> Result { + // we could make it work, but we are only testing regular FAT filesystems (for now) + assert_ne!( + self.fat_type, + FATType::ExFAT, + "this function doesn't work with ExFAT" + ); + + /// How many bytes to probe at max for each FAT per iteration (must be a multiple of [`SECTOR_SIZE_MAX`]) + const MAX_PROBE_SIZE: u32 = 1 << 20; + + let fat_byte_size = match self.boot_record { + BootRecord::FAT(boot_record_fat) => boot_record_fat.fat_sector_size(), + BootRecord::ExFAT(_) => unreachable!(), + }; + + for nth_iteration in 0..fat_byte_size.div_ceil(MAX_PROBE_SIZE) { + let mut tables: Vec> = Vec::new(); + + for i in 0..self.props.fat_table_count { + let fat_start = + self.sector_to_partition_offset(self.boot_record.nth_FAT_table_sector(i)); + let current_offset = fat_start + nth_iteration * MAX_PROBE_SIZE; + let bytes_left = fat_byte_size - nth_iteration * MAX_PROBE_SIZE; + + self.storage.seek(SeekFrom::Start(current_offset.into()))?; + let mut buf = vec![0_u8; cmp::min(MAX_PROBE_SIZE, bytes_left) as usize]; + self.storage.read_exact(buf.as_mut_slice())?; + tables.push(buf); + } + + // we check each table with the first one (except the first one ofc) + if !tables.iter().skip(1).all(|buf| buf == &tables[0]) { + return Ok(false); + } + } + + Ok(true) + } + /// Read the nth sector from the partition's beginning and store it in [`self.sector_buffer`](Self::sector_buffer) /// /// This function also returns an immutable reference to [`self.sector_buffer`](Self::sector_buffer) @@ -1670,7 +1748,7 @@ where let entry_size = self.fat_type.entry_size(); let entry_props = FATEntryProps::new(n, &self); - self.read_nth_sector(entry_props.fat_sector.into())?; + self.read_nth_sector(entry_props.fat_sectors[0].into())?; let mut value_bytes = [0_u8; 4]; let bytes_to_read: usize = cmp::min( @@ -1684,7 +1762,7 @@ where // in FAT12, FAT entries may be split between two different sectors if self.fat_type == FATType::FAT12 && (bytes_to_read as u32) < entry_size { - self.read_nth_sector((entry_props.fat_sector + 1).into())?; + self.read_nth_sector((entry_props.fat_sectors[0] + 1).into())?; value_bytes[bytes_to_read..entry_size as usize] .copy_from_slice(&self.sector_buffer[..(entry_size as usize - bytes_to_read)]); @@ -1783,25 +1861,28 @@ where value |= old_value; } - // just in case we aren't in the correct sector - self.read_nth_sector(entry_props.fat_sector.into())?; - - let value_bytes = value.to_le_bytes(); - let bytes_to_write: usize = cmp::min( - entry_props.sector_offset + entry_size as usize, - self.sector_size() as usize, - ) - entry_props.sector_offset; - self.sector_buffer[entry_props.sector_offset..entry_props.sector_offset + bytes_to_write] - .copy_from_slice(&value_bytes[..bytes_to_write]); // this shouldn't panic - self.buffer_modified = true; + // we update all the FAT copies + for fat_sector in entry_props.fat_sectors { + self.read_nth_sector(fat_sector.into())?; + + let value_bytes = value.to_le_bytes(); + let bytes_to_write: usize = cmp::min( + entry_props.sector_offset + entry_size as usize, + self.sector_size() as usize, + ) - entry_props.sector_offset; + self.sector_buffer + [entry_props.sector_offset..entry_props.sector_offset + bytes_to_write] + .copy_from_slice(&value_bytes[..bytes_to_write]); // this shouldn't panic + self.buffer_modified = true; - if self.fat_type == FATType::FAT12 && bytes_to_write < entry_size as usize { - // looks like this FAT12 entry spans multiple sectors, we must also update the other one - self.read_nth_sector((entry_props.fat_sector + 1).into())?; + if self.fat_type == FATType::FAT12 && bytes_to_write < entry_size as usize { + // looks like this FAT12 entry spans multiple sectors, we must also update the other one + self.read_nth_sector((fat_sector + 1).into())?; - self.sector_buffer[..(entry_size as usize - bytes_to_write)] - .copy_from_slice(&value_bytes[bytes_to_write..entry_size as usize]); - self.buffer_modified = true; + self.sector_buffer[..(entry_size as usize - bytes_to_write)] + .copy_from_slice(&value_bytes[bytes_to_write..entry_size as usize]); + self.buffer_modified = true; + } } Ok(()) @@ -1980,6 +2061,33 @@ mod tests { } } + #[test] + #[allow(non_snake_case)] + fn FAT_tables_after_write_are_identical() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT16.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + assert!( + fs.FAT_tables_are_identical().unwrap(), + concat!( + "this should pass. ", + "if it doesn't, either the corresponding .img file's FAT tables aren't identical", + "or the tables_are_identical function doesn't work correctly" + ) + ); + + // let's write the bee movie script to root.txt (why not), check, truncate the file, then check again + let mut file = fs.get_file(PathBuf::from("root.txt")).unwrap(); + + file.write_all(BEE_MOVIE_SCRIPT.as_bytes()).unwrap(); + assert!(file.fs.FAT_tables_are_identical().unwrap()); + + file.truncate(10_000).unwrap(); + assert!(file.fs.FAT_tables_are_identical().unwrap()); + } + #[test] fn truncate_file() { use std::io::Cursor; From 4941c8ce703892a86a67d9b79098f7620cc872a9 Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Tue, 20 Aug 2024 16:32:16 +0300 Subject: [PATCH 06/40] fix: fix commit a8ced4c for no-std contexts --- src/fs.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fs.rs b/src/fs.rs index f83ba9f..9b5aaf7 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -7,6 +7,7 @@ use ::alloc::{ borrow::ToOwned, format, string::{FromUtf16Error, String, ToString}, + vec, vec::*, }; From ad6747e398916feb64b717b1d54ed0195ebe2b70 Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sun, 25 Aug 2024 12:04:47 +0300 Subject: [PATCH 07/40] chore: writing FAT12 entries no longer reads sectors ahead-of-time When writing a FATEntry on a FAT12 filesystem, the bytes the entry spans would be read ahead-of-time & OR-ed to the new entry's bytes. This is no longer the case. FAT12 still reads those bytes since FAT12 entries are 1.5 bytes long, but now they are read individually just before the new byte is written. --- src/fs.rs | 110 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 67 insertions(+), 43 deletions(-) diff --git a/src/fs.rs b/src/fs.rs index 9b5aaf7..7413d6f 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1743,8 +1743,7 @@ where } #[allow(non_snake_case)] - /// Returns the bytes occupied by the FAT entry, padded up to 4 - fn internal_read_nth_FAT_entry_to_bytes(&mut self, n: u32) -> Result<[u8; 4], S::Error> { + fn read_nth_FAT_entry(&mut self, n: u32) -> Result { // the size of an entry rounded up to bytes let entry_size = self.fat_type.entry_size(); let entry_props = FATEntryProps::new(n, &self); @@ -1769,12 +1768,6 @@ where .copy_from_slice(&self.sector_buffer[..(entry_size as usize - bytes_to_read)]); }; - Ok(value_bytes) - } - - #[allow(non_snake_case)] - fn read_nth_FAT_entry(&mut self, n: u32) -> Result { - let value_bytes = self.internal_read_nth_FAT_entry_to_bytes(n)?; let mut value = u32::from_le_bytes(value_bytes); match self.fat_type { // FAT12 entries are split between different bytes @@ -1844,47 +1837,78 @@ where let entry_size = self.fat_type.entry_size(); let entry_props = FATEntryProps::new(n, &self); - let value_bytes = self.internal_read_nth_FAT_entry_to_bytes(n)?; - let mut old_value = u32::from_le_bytes(value_bytes); - - let mut mask = (1 << self.fat_type.bits_per_entry()) - 1; + let mask = (1 << self.fat_type.bits_per_entry()) - 1; let mut value: u32 = u32::from(entry.clone()) & mask; - if self.fat_type == FATType::FAT12 { - if n & 1 != 0 { - // FAT12 entries are split between different bytes - value <<= 4; - mask <<= 4; + match self.fat_type { + FATType::FAT12 => { + let should_shift = n & 1 != 0; + if should_shift { + // FAT12 entries are split between different bytes + value <<= 4; + } + + // we update all the FAT copies + for fat_sector in entry_props.fat_sectors { + self.read_nth_sector(fat_sector.into())?; + + let value_bytes = value.to_le_bytes(); + + let mut first_byte = value_bytes[0]; + + if should_shift { + let mut old_byte = self.sector_buffer[entry_props.sector_offset]; + // ignore the high 4 bytes of the old entry + old_byte &= 0x0F; + // OR it with the new value + first_byte |= old_byte; + } + + self.sector_buffer[entry_props.sector_offset] = first_byte; // this shouldn't panic + self.buffer_modified = true; + + let bytes_left_on_sector: usize = cmp::min( + entry_size as usize, + self.sector_size() as usize - entry_props.sector_offset, + ); + + if bytes_left_on_sector < entry_size as usize { + // looks like this FAT12 entry spans multiple sectors, we must also update the other one + self.read_nth_sector((fat_sector + 1).into())?; + } + + let mut second_byte = value_bytes[1]; + let second_byte_index = + (entry_props.sector_offset + 1) % self.sector_size() as usize; + if !should_shift { + let mut old_byte = self.sector_buffer[second_byte_index]; + // ignore the low 4 bytes of the old entry + old_byte &= 0xF0; + // OR it with the new value + second_byte |= old_byte; + } + + self.sector_buffer[second_byte_index] = second_byte; // this shouldn't panic + self.buffer_modified = true; + } } + FATType::FAT16 | FATType::FAT32 => { + // we update all the FAT copies + for fat_sector in entry_props.fat_sectors { + self.read_nth_sector(fat_sector.into())?; - // mask the old value and bitwise OR it with the new one - old_value &= !mask; - value |= old_value; - } + let value_bytes = value.to_le_bytes(); - // we update all the FAT copies - for fat_sector in entry_props.fat_sectors { - self.read_nth_sector(fat_sector.into())?; - - let value_bytes = value.to_le_bytes(); - let bytes_to_write: usize = cmp::min( - entry_props.sector_offset + entry_size as usize, - self.sector_size() as usize, - ) - entry_props.sector_offset; - self.sector_buffer - [entry_props.sector_offset..entry_props.sector_offset + bytes_to_write] - .copy_from_slice(&value_bytes[..bytes_to_write]); // this shouldn't panic - self.buffer_modified = true; - - if self.fat_type == FATType::FAT12 && bytes_to_write < entry_size as usize { - // looks like this FAT12 entry spans multiple sectors, we must also update the other one - self.read_nth_sector((fat_sector + 1).into())?; - - self.sector_buffer[..(entry_size as usize - bytes_to_write)] - .copy_from_slice(&value_bytes[bytes_to_write..entry_size as usize]); - self.buffer_modified = true; + self.sector_buffer[entry_props.sector_offset + ..entry_props.sector_offset + entry_size as usize] + .copy_from_slice(&value_bytes[..entry_size as usize]); // this shouldn't panic + self.buffer_modified = true; + } } - } + FATType::ExFAT => todo!("ExFAT not yet implemented"), + }; + + assert_eq!(entry, self.read_nth_FAT_entry(n)?); Ok(()) } From 20adcf0d8f55f0250ebc2849b622de5e9071c51c Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:13:51 +0300 Subject: [PATCH 08/40] feat(file): split File struct into separate ROFile & RWFile --- src/error.rs | 13 ++ src/fs.rs | 624 +++++++++++++++++++++++++++++++-------------------- src/io.rs | 7 +- src/lib.rs | 38 +++- 4 files changed, 438 insertions(+), 244 deletions(-) diff --git a/src/error.rs b/src/error.rs index 8b5a9e3..5433a53 100644 --- a/src/error.rs +++ b/src/error.rs @@ -51,6 +51,8 @@ pub trait IOErrorKind: PartialEq + Sized { fn new_interrupted() -> Self; /// Create a new `InvalidData` [`IOErrorKind`] fn new_invalid_data() -> Self; + /// Create a new `ReadOnlyFilesystem` [`IOErrorKind`] + fn new_readonly_filesystem() -> Self; #[inline] /// Check whether this [`IOErrorKind`] is of kind `UnexpectedEOF` @@ -67,6 +69,12 @@ pub trait IOErrorKind: PartialEq + Sized { fn is_invalid_data(&self) -> bool { self == &Self::new_invalid_data() } + // when the `io_error_more` feature gets merged, uncomment this + // /// Check whether this [`IOErrorKind`] is of kind `InvalidData` + // #[inline] + // fn is_readonly_filesystem(&self) -> bool { + // self == &Self::new_readonly_filesystem() + // } } #[cfg(feature = "std")] @@ -83,6 +91,11 @@ impl IOErrorKind for std::io::ErrorKind { fn new_invalid_data() -> Self { std::io::ErrorKind::InvalidData } + #[inline] + fn new_readonly_filesystem() -> Self { + // Unfortunately the ReadOnlyFilesystem ErrorKind is locked behind a feature flag (for now) + std::io::ErrorKind::Other + } } /// An error type that denotes that there is something wrong diff --git a/src/fs.rs b/src/fs.rs index 7413d6f..a4cf147 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -694,48 +694,94 @@ impl ops::Deref for DirEntry { } } -/// A file within the FAT filesystem #[derive(Debug)] -pub struct File<'a, S> -where - S: Read + Write + Seek, -{ - fs: &'a mut FileSystem, +struct FileProps { entry: Properties, /// the byte offset of the R/W pointer offset: u64, current_cluster: u32, } -impl ops::Deref for File<'_, S> +/// A read-only file within a FAT filesystem +#[derive(Debug)] +pub struct ROFile<'a, S> +where + S: Read + Write + Seek, +{ + fs: &'a mut FileSystem, + props: FileProps, +} + +impl ops::Deref for ROFile<'_, S> where S: Read + Write + Seek, { type Target = Properties; fn deref(&self) -> &Self::Target { - &self.entry + &self.props.entry + } +} + +impl ops::DerefMut for ROFile<'_, S> +where + S: Read + Write + Seek, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.props.entry + } +} + +impl IOBase for ROFile<'_, S> +where + S: Read + Write + Seek, +{ + type Error = S::Error; +} + +/// A read-write file within a FAT filesystem +/// +/// The size of the file will be automatically adjusted +/// if the cursor goes beyond EOF. +/// +/// To reduce a file's size, use the [`truncate`](RWFile::truncate) method +#[derive(Debug)] +pub struct RWFile<'a, S> +where + S: Read + Write + Seek, +{ + ro_file: ROFile<'a, S>, +} + +impl<'a, S> ops::Deref for RWFile<'a, S> +where + S: Read + Write + Seek, +{ + type Target = ROFile<'a, S>; + + fn deref(&self) -> &Self::Target { + &self.ro_file } } -impl ops::DerefMut for File<'_, S> +impl ops::DerefMut for RWFile<'_, S> where S: Read + Write + Seek, { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.entry + &mut self.ro_file } } -impl IOBase for File<'_, S> +impl IOBase for RWFile<'_, S> where S: Read + Write + Seek, { type Error = S::Error; } -/// Public functions -impl File<'_, S> +// Public functions +impl RWFile<'_, S> where S: Read + Write + Seek, { @@ -752,7 +798,7 @@ where } // we store the current offset for later use - let previous_offset = cmp::min(self.offset, size.into()); + let previous_offset = cmp::min(self.props.offset, size.into()); // we seek back to where the EOF will be self.seek(SeekFrom::Start(size.into()))?; @@ -761,11 +807,15 @@ where let previous_size = self.file_size; self.file_size = size; - let mut next_cluster_option = self.fs.get_next_cluster(self.current_cluster)?; + let mut next_cluster_option = self + .ro_file + .fs + .get_next_cluster(self.ro_file.props.current_cluster)?; // we set the new last cluster in the chain to be EOF - self.fs - .write_nth_FAT_entry(self.current_cluster, FATEntry::EOF)?; + self.ro_file + .fs + .write_nth_FAT_entry(self.ro_file.props.current_cluster, FATEntry::EOF)?; // then, we set each cluster after the current one to EOF while let Some(next_cluster) = next_cluster_option { @@ -788,16 +838,19 @@ where } } -/// Internal functions -impl File<'_, S> +// Internal functions +impl ROFile<'_, S> where S: Read + Write + Seek, { #[inline] /// Panics if the current cluser doesn't point to another clluster fn next_cluster(&mut self) -> Result<(), ::Error> { - // when a `File` is created, `cluster_chain_is_healthy` is called, if it fails, that File is dropped - self.current_cluster = self.fs.get_next_cluster(self.current_cluster)?.unwrap(); + // when a `ROFile` is created, `cluster_chain_is_healthy` is called, if it fails, that ROFile is dropped + self.props.current_cluster = self + .fs + .get_next_cluster(self.props.current_cluster)? + .unwrap(); Ok(()) } @@ -805,7 +858,7 @@ where /// Returns that last cluster in the file's cluster chain fn last_cluster_in_chain(&mut self) -> Result::Error> { // we begin from the current cluster to save some time - let mut current_cluster = self.current_cluster; + let mut current_cluster = self.props.current_cluster; loop { match self.fs.read_nth_FAT_entry(current_cluster)? { @@ -838,24 +891,41 @@ where Ok(true) } + + fn offset_from_seekfrom(&self, seekfrom: SeekFrom) -> u64 { + match seekfrom { + SeekFrom::Start(offset) => offset, + SeekFrom::Current(offset) => { + let offset = self.props.offset as i64 + offset; + offset.try_into().unwrap_or(u64::MIN) + } + SeekFrom::End(offset) => { + let offset = self.file_size as i64 + offset; + offset.try_into().unwrap_or(u64::MIN) + } + } + } } -impl Read for File<'_, S> +impl Read for ROFile<'_, S> where S: Read + Write + Seek, { fn read(&mut self, buf: &mut [u8]) -> Result { let mut bytes_read = 0; // this is the maximum amount of bytes that can be read - let read_cap = cmp::min(buf.len(), self.file_size as usize - self.offset as usize); - log::debug!("Byte read cap set to {}", read_cap); + let read_cap = cmp::min( + buf.len(), + self.file_size as usize - self.props.offset as usize, + ); 'outer: loop { - let sector_init_offset = u32::try_from(self.offset % self.fs.cluster_size()).unwrap() + let sector_init_offset = u32::try_from(self.props.offset % self.fs.cluster_size()) + .unwrap() / self.fs.sector_size(); let first_sector_of_cluster = self .fs - .data_cluster_to_partition_sector(self.current_cluster) + .data_cluster_to_partition_sector(self.props.current_cluster) + sector_init_offset; let last_sector_of_cluster = first_sector_of_cluster + self.fs.sectors_per_cluster() as u32 @@ -863,7 +933,7 @@ where - 1; log::debug!( "Reading cluster {} from sectors {} to {}", - self.current_cluster, + self.props.current_cluster, first_sector_of_cluster, last_sector_of_cluster ); @@ -871,7 +941,7 @@ where for sector in first_sector_of_cluster..=last_sector_of_cluster { self.fs.read_nth_sector(sector.into())?; - let start_index = self.offset as usize % self.fs.sector_size() as usize; + let start_index = self.props.offset as usize % self.fs.sector_size() as usize; let bytes_to_read = cmp::min( read_cap - bytes_read, self.fs.sector_size() as usize - start_index, @@ -888,14 +958,14 @@ where ); bytes_read += bytes_to_read; - self.offset += bytes_to_read as u64; + self.props.offset += bytes_to_read as u64; // if we have read as many bytes as we want... if bytes_read >= read_cap { // ...but we must process get the next cluster for future uses, // we do that before breaking - if self.offset % self.fs.cluster_size() == 0 - && self.offset < self.file_size.into() + if self.props.offset % self.fs.cluster_size() == 0 + && self.props.offset < self.file_size.into() { self.next_cluster()?; } @@ -912,7 +982,7 @@ where // the default `read_to_end` implementation isn't efficient enough, so we just do this fn read_to_end(&mut self, buf: &mut Vec) -> Result { - let bytes_to_read = self.file_size as usize - self.offset as usize; + let bytes_to_read = self.file_size as usize - self.props.offset as usize; let init_buf_len = buf.len(); // resize buffer to fit the file contents exactly @@ -924,8 +994,32 @@ where Ok(bytes_to_read) } } +impl Read for RWFile<'_, S> +where + S: Read + Write + Seek, +{ + #[inline] + fn read(&mut self, buf: &mut [u8]) -> Result { + self.ro_file.read(buf) + } + + #[inline] + fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Self::Error> { + self.ro_file.read_exact(buf) + } + + #[inline] + fn read_to_end(&mut self, buf: &mut Vec) -> Result { + self.ro_file.read_to_end(buf) + } -impl Write for File<'_, S> + #[inline] + fn read_to_string(&mut self, string: &mut String) -> Result { + self.ro_file.read_to_string(string) + } +} + +impl Write for RWFile<'_, S> where S: Read + Write + Seek, { @@ -938,13 +1032,17 @@ where let mut bytes_written = 0; 'outer: loop { - log::trace!("writing file data to cluster: {}", self.current_cluster); + log::trace!( + "writing file data to cluster: {}", + self.props.current_cluster + ); - let sector_init_offset = u32::try_from(self.offset % self.fs.cluster_size()).unwrap() + let sector_init_offset = u32::try_from(self.props.offset % self.fs.cluster_size()) + .unwrap() / self.fs.sector_size(); let first_sector_of_cluster = self .fs - .data_cluster_to_partition_sector(self.current_cluster) + .data_cluster_to_partition_sector(self.props.current_cluster) + sector_init_offset; let last_sector_of_cluster = first_sector_of_cluster + self.fs.sectors_per_cluster() as u32 @@ -953,7 +1051,7 @@ where for sector in first_sector_of_cluster..=last_sector_of_cluster { self.fs.read_nth_sector(sector.into())?; - let start_index = self.offset as usize % self.fs.sector_size() as usize; + let start_index = self.props.offset as usize % self.fs.sector_size() as usize; let bytes_to_write = cmp::min( buf.len() - bytes_written, @@ -965,13 +1063,13 @@ where self.fs.buffer_modified = true; bytes_written += bytes_to_write; - self.offset += bytes_to_write as u64; + self.props.offset += bytes_to_write as u64; // if we have written as many bytes as we want... if bytes_written >= buf.len() { // ...but we must process get the next cluster for future uses, // we do that before breaking - if self.offset % self.fs.cluster_size() == 0 { + if self.props.offset % self.fs.cluster_size() == 0 { self.next_cluster()?; } @@ -991,22 +1089,57 @@ where } } -impl Seek for File<'_, S> +impl Seek for ROFile<'_, S> where S: Read + Write + Seek, { fn seek(&mut self, pos: SeekFrom) -> Result { - let offset = match pos { - SeekFrom::Start(offset) => offset, - SeekFrom::Current(offset) => { - let offset = self.offset as i64 + offset; - offset.try_into().unwrap_or(u64::MIN) + let offset = self.offset_from_seekfrom(pos); + + // in case the cursor goes beyond the EOF, allocate more clusters + if offset > (self.file_size as u64).next_multiple_of(self.fs.cluster_size()) { + return Err(IOError::new( + ::Kind::new_unexpected_eof(), + "moved past eof in a RO file", + )); + } + + log::trace!( + "Previous cursor offset is {}, new cursor offset is {}", + self.props.offset, + offset + ); + + use cmp::Ordering; + match offset.cmp(&self.props.offset) { + Ordering::Less => { + // here, we basically "rewind" back to the start of the file and then seek to where we want + // this of course has performance issues, so TODO: find a solution that is both memory & time efficient + // (perhaps we could follow a similar approach to elm-chan's FATFS, by using a cluster link map table, perhaps as an optional feature) + self.props.offset = 0; + self.props.current_cluster = self.data_cluster; + self.seek(SeekFrom::Start(offset))?; } - SeekFrom::End(offset) => { - let offset = self.file_size as i64 + offset; - offset.try_into().unwrap_or(u64::MIN) + Ordering::Equal => (), + Ordering::Greater => { + for _ in self.props.offset / self.fs.cluster_size()..offset / self.fs.cluster_size() + { + self.next_cluster()?; + } + self.props.offset = offset; } - }; + } + + Ok(self.props.offset) + } +} + +impl Seek for RWFile<'_, S> +where + S: Read + Write + Seek, +{ + fn seek(&mut self, pos: SeekFrom) -> Result { + let offset = self.offset_from_seekfrom(pos); // in case the cursor goes beyond the EOF, allocate more clusters if offset > (self.file_size as u64).next_multiple_of(self.fs.cluster_size()) { @@ -1046,9 +1179,9 @@ where - offset) + clusters_allocated * self.fs.cluster_size()) as u32; - self.offset = self.file_size.into(); + self.props.offset = self.file_size.into(); - log::error!("storage medium full while attempting to allocated more clusters for a File"); + log::error!("storage medium full while attempting to allocate more clusters for a ROFile"); return Err(IOError::new( ::Kind::new_unexpected_eof(), "the storage medium is full, can't increase size of file", @@ -1064,32 +1197,7 @@ where ); } - log::debug!( - "Previous cursor offset is {}, new cursor offset is {}", - self.offset, - offset - ); - - use cmp::Ordering; - match offset.cmp(&self.offset) { - Ordering::Less => { - // here, we basically "rewind" back to the start of the file and then seek to where we want - // this of course has performance issues, so TODO: find a solution that is both memory & time efficient - // (perhaps we could follow a similar approach to elm-chan's FATFS, by using a cluster link map table, perhaps as an optional feature) - self.offset = 0; - self.current_cluster = self.data_cluster; - self.seek(SeekFrom::Start(offset))?; - } - Ordering::Equal => (), - Ordering::Greater => { - for _ in self.offset / self.fs.cluster_size()..offset / self.fs.cluster_size() { - self.next_cluster()?; - } - self.offset = offset; - } - } - - Ok(self.offset) + self.ro_file.seek(pos) } } @@ -1141,7 +1249,6 @@ struct FSProperties { total_sectors: u32, total_clusters: u32, /// sector offset of the FAT - fat_offset: u32, fat_table_count: u8, first_data_sector: u32, } @@ -1208,7 +1315,7 @@ where } } -/// Public functions +/// Constructors impl FileSystem where S: Read + Write + Seek, @@ -1291,11 +1398,6 @@ where BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), }; - let fat_offset = match boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.first_fat_sector().into(), - BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), - }; - let fat_table_count = match boot_record { BootRecord::FAT(boot_record_fat) => boot_record_fat.bpb.table_count, BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), @@ -1314,7 +1416,6 @@ where let props = FSProperties { sector_size, cluster_size, - fat_offset, fat_table_count, total_sectors, total_clusters, @@ -1339,131 +1440,9 @@ where Ok(fs) } - - /// Read all the entries of a directory ([`PathBuf`]) into [`Vec`] - /// - /// Fails if `path` doesn't represent a directory, or if that directory doesn't exist - pub fn read_dir(&mut self, path: PathBuf) -> FSResult, S::Error> { - if path.is_malformed() { - return Err(FSError::MalformedPath); - } - if !path.is_dir() { - log::error!("Not a directory"); - return Err(FSError::NotADirectory); - } - - let mut entries = self.process_root_dir()?; - - for dir_name in path.clone().into_iter() { - let dir_cluster = match entries.iter().find(|entry| { - entry.name == dir_name && entry.attributes.contains(Attributes::DIRECTORY) - }) { - Some(entry) => entry.data_cluster, - None => { - log::error!("Directory {} not found", path); - return Err(FSError::NotFound); - } - }; - - entries = unsafe { self.process_normal_dir(dir_cluster)? }; - } - - // if we haven't returned by now, that means that the entries vector - // contains what we want, let's map it to DirEntries and return - Ok(entries - .into_iter() - .map(|rawentry| { - let mut entry_path = path.clone(); - - entry_path.push(format!( - "{}{}", - rawentry.name, - if rawentry.is_dir { "/" } else { "" } - )); - DirEntry { - entry: Properties::from_raw(rawentry, entry_path), - } - }) - .collect()) - } - - /// Get a corresponding [`File`] object from a [`PathBuf`] - /// - /// Borrows `&mut self` until that [`File`] object is dropped, effectively locking `self` until that file closed - /// - /// Fails if `path` doesn't represent a file, or if that file doesn't exist - pub fn get_file(&mut self, path: PathBuf) -> FSResult, S::Error> { - if path.is_malformed() { - return Err(FSError::MalformedPath); - } - - if let Some(file_name) = path.file_name() { - let parent_dir = self.read_dir(path.parent())?; - match parent_dir.into_iter().find(|direntry| { - direntry - .path() - .file_name() - .is_some_and(|entry_name| entry_name == file_name) - }) { - Some(direntry) => { - let mut file = File { - fs: self, - offset: 0, - current_cluster: direntry.entry.data_cluster, - entry: direntry.entry, - }; - - if file.cluster_chain_is_healthy()? { - Ok(file) - } else { - log::error!("The cluster chain of a file is malformed"); - Err(FSError::InternalFSError( - InternalFSError::MalformedClusterChain, - )) - } - } - None => { - log::error!("File {} not found", path); - Err(FSError::NotFound) - } - } - } else { - log::error!("Is a directory (not a file)"); - Err(FSError::IsADirectory) - } - } -} - -/// Properties about the position of a [`FATEntry`] inside the FAT region -struct FATEntryProps { - /// Each `n`th element of the vector points at the corrensponding sector at the `n+1`th FAT table - fat_sectors: Vec, - sector_offset: usize, } -impl FATEntryProps { - /// Get the [`FATEntryProps`] of the `n`-th [`FATEntry`] of a [`FileSystem`] (`fs`) - pub fn new(n: u32, fs: &FileSystem) -> Self - where - S: Read + Write + Seek, - { - let fat_byte_offset: u32 = n * fs.fat_type.bits_per_entry() as u32 / 8; - let mut fat_sectors = Vec::new(); - for nth_table in 0..fs.props.fat_table_count { - let table_sector_offset = fs.boot_record.nth_FAT_table_sector(nth_table); - let fat_sector = table_sector_offset + fat_byte_offset / fs.props.sector_size; - fat_sectors.push(fat_sector); - } - let sector_offset: usize = (fat_byte_offset % fs.props.sector_size) as usize; - - FATEntryProps { - fat_sectors, - sector_offset, - } - } -} - -/// Internal low-level functions +/// Internal [`Read`]-related low-level functions impl FileSystem where S: Read + Write + Seek, @@ -1662,7 +1641,7 @@ where fn get_next_cluster(&mut self, cluster: u32) -> Result, S::Error> { Ok(match self.read_nth_FAT_entry(cluster)? { FATEntry::Allocated(next_cluster) => Some(next_cluster), - // when a `File` is created, `cluster_chain_is_healthy` is called, if it fails, that File is dropped + // when a `ROFile` is created, `cluster_chain_is_healthy` is called, if it fails, that ROFile is dropped _ => None, }) } @@ -1730,18 +1709,6 @@ where Ok(&self.sector_buffer) } - fn sync_sector_buffer(&mut self) -> Result<(), S::Error> { - if self.buffer_modified { - log::trace!("syncing sector {:?}", self.stored_sector); - self.storage.write_all(&self.sector_buffer)?; - self.storage - .seek(SeekFrom::Current(-i64::from(self.props.sector_size)))?; - } - self.buffer_modified = false; - - Ok(()) - } - #[allow(non_snake_case)] fn read_nth_FAT_entry(&mut self, n: u32) -> Result { // the size of an entry rounded up to bytes @@ -1830,7 +1797,13 @@ where FATType::ExFAT => todo!("ExFAT not yet implemented"), }) } + } +/// Internal [`Write`]-related low-level functions +impl FileSystem +where + S: Read + Write + Seek, +{ #[allow(non_snake_case)] fn write_nth_FAT_entry(&mut self, n: u32, entry: FATEntry) -> Result<(), S::Error> { // the size of an entry rounded up to bytes @@ -1908,12 +1881,172 @@ where FATType::ExFAT => todo!("ExFAT not yet implemented"), }; - assert_eq!(entry, self.read_nth_FAT_entry(n)?); + Ok(()) + } + + fn sync_sector_buffer(&mut self) -> Result<(), S::Error> { + if self.buffer_modified { + log::trace!("syncing sector {:?}", self.stored_sector); + self.storage.write_all(&self.sector_buffer)?; + self.storage + .seek(SeekFrom::Current(-i64::from(self.props.sector_size)))?; + } + self.buffer_modified = false; Ok(()) } } +/// Public [`Read`]-related functions +impl FileSystem +where + S: Read + Write + Seek, +{ + /// Read all the entries of a directory ([`PathBuf`]) into [`Vec`] + /// + /// Fails if `path` doesn't represent a directory, or if that directory doesn't exist + pub fn read_dir(&mut self, path: PathBuf) -> FSResult, S::Error> { + if path.is_malformed() { + return Err(FSError::MalformedPath); + } + if !path.is_dir() { + log::error!("Not a directory"); + return Err(FSError::NotADirectory); + } + + let mut entries = self.process_root_dir()?; + + for dir_name in path.clone().into_iter() { + let dir_cluster = match entries.iter().find(|entry| { + entry.name == dir_name && entry.attributes.contains(Attributes::DIRECTORY) + }) { + Some(entry) => entry.data_cluster, + None => { + log::error!("Directory {} not found", path); + return Err(FSError::NotFound); + } + }; + + entries = unsafe { self.process_normal_dir(dir_cluster)? }; + } + + // if we haven't returned by now, that means that the entries vector + // contains what we want, let's map it to DirEntries and return + Ok(entries + .into_iter() + .map(|rawentry| { + let mut entry_path = path.clone(); + + entry_path.push(format!( + "{}{}", + rawentry.name, + if rawentry.is_dir { "/" } else { "" } + )); + DirEntry { + entry: Properties::from_raw(rawentry, entry_path), + } + }) + .collect()) + } + + /// Get a corresponding [`ROFile`] object from a [`PathBuf`] + /// + /// Borrows `&mut self` until that [`ROFile`] object is dropped, effectively locking `self` until that file closed + /// + /// Fails if `path` doesn't represent a file, or if that file doesn't exist + pub fn get_ro_file(&mut self, path: PathBuf) -> FSResult, S::Error> { + if path.is_malformed() { + return Err(FSError::MalformedPath); + } + + if let Some(file_name) = path.file_name() { + let parent_dir = self.read_dir(path.parent())?; + match parent_dir.into_iter().find(|direntry| { + direntry + .path() + .file_name() + .is_some_and(|entry_name| entry_name == file_name) + }) { + Some(direntry) => { + let mut file = ROFile { + fs: self, + props: FileProps { + offset: 0, + current_cluster: direntry.entry.data_cluster, + entry: direntry.entry, + }, + }; + + if file.cluster_chain_is_healthy()? { + Ok(file) + } else { + log::error!("The cluster chain of a file is malformed"); + Err(FSError::InternalFSError( + InternalFSError::MalformedClusterChain, + )) + } + } + None => { + log::error!("ROFile {} not found", path); + Err(FSError::NotFound) + } + } + } else { + log::error!("Is a directory (not a file)"); + Err(FSError::IsADirectory) + } + } +} + +/// [`Write`]-related functions +impl FileSystem +where + S: Read + Write + Seek, +{ + /// Get a corresponding [`RWFile`] object from a [`PathBuf`] + /// + /// Borrows `&mut self` until that [`RWFile`] object is dropped, effectively locking `self` until that file closed + /// + /// Fails if `path` doesn't represent a file, or if that file doesn't exist + pub fn get_rw_file(&mut self, path: PathBuf) -> FSResult, S::Error> { + // we first write an empty array to the storage medium + // if the storage has Write functionality, this shouldn't error, + // otherwise it should return an error. + self.storage.write_all(&[])?; + + self.get_ro_file(path).map(|ro_file| RWFile { ro_file }) + } +} + +/// Properties about the position of a [`FATEntry`] inside the FAT region +struct FATEntryProps { + /// Each `n`th element of the vector points at the corrensponding sector at the `n+1`th FAT table + fat_sectors: Vec, + sector_offset: usize, +} + +impl FATEntryProps { + /// Get the [`FATEntryProps`] of the `n`-th [`FATEntry`] of a [`ROFileSystem`] (`fs`) + pub fn new(n: u32, fs: &FileSystem) -> Self + where + S: Read + Write + Seek, + { + let fat_byte_offset: u32 = n * fs.fat_type.bits_per_entry() as u32 / 8; + let mut fat_sectors = Vec::new(); + for nth_table in 0..fs.props.fat_table_count { + let table_sector_offset = fs.boot_record.nth_FAT_table_sector(nth_table); + let fat_sector = table_sector_offset + fat_byte_offset / fs.props.sector_size; + fat_sectors.push(fat_sector); + } + let sector_offset: usize = (fat_byte_offset % fs.props.sector_size) as usize; + + FATEntryProps { + fat_sectors, + sector_offset, + } + } +} + impl ops::Drop for FileSystem where S: Read + Write + Seek, @@ -1944,8 +2077,13 @@ mod tests { let mut storage = Cursor::new(FAT16.to_owned()); let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + let fat_offset = match fs.boot_record { + BootRecord::FAT(boot_record_fat) => boot_record_fat.first_fat_sector(), + BootRecord::ExFAT(_boot_record_exfat) => unreachable!(), + }; + // we manually read the first and second entry of the FAT table - fs.read_nth_sector(fs.props.fat_offset.into()).unwrap(); + fs.read_nth_sector(fat_offset.into()).unwrap(); let first_entry = u16::from_le_bytes(fs.sector_buffer[0..2].try_into().unwrap()); let media_type = if let BootRecord::FAT(boot_record_fat) = fs.boot_record { @@ -1966,7 +2104,7 @@ mod tests { let mut storage = Cursor::new(FAT16.to_owned()); let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - let mut file = fs.get_file(PathBuf::from("/root.txt")).unwrap(); + let mut file = fs.get_ro_file(PathBuf::from("/root.txt")).unwrap(); let mut file_string = String::new(); file.read_to_string(&mut file_string).unwrap(); @@ -1982,7 +2120,7 @@ mod tests { assert_eq!(string, BEE_MOVIE_SCRIPT); } - fn assert_file_is_bee_movie_script(file: &mut File<'_, S>) + fn assert_file_is_bee_movie_script(file: &mut ROFile<'_, S>) where S: Read + Write + Seek, { @@ -1999,7 +2137,9 @@ mod tests { let mut storage = Cursor::new(FAT16.to_owned()); let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - let mut file = fs.get_file(PathBuf::from("/bee movie script.txt")).unwrap(); + let mut file = fs + .get_ro_file(PathBuf::from("/bee movie script.txt")) + .unwrap(); assert_file_is_bee_movie_script(&mut file); } @@ -2014,7 +2154,7 @@ mod tests { let mut fs = FileSystem::from_storage(&mut storage).unwrap(); let mut file = fs - .get_file(PathBuf::from("/GNU ⁄ Linux copypasta.txt")) + .get_ro_file(PathBuf::from("/GNU ⁄ Linux copypasta.txt")) .unwrap(); let mut file_bytes = [0_u8; 4096]; @@ -2047,7 +2187,7 @@ mod tests { let mut storage = Cursor::new(FAT12.to_owned()); let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - let mut file = fs.get_file(PathBuf::from("/root.txt")).unwrap(); + let mut file = fs.get_rw_file(PathBuf::from("/root.txt")).unwrap(); file.write_all(BEE_MOVIE_SCRIPT.as_bytes()).unwrap(); file.rewind().unwrap(); @@ -2104,7 +2244,7 @@ mod tests { ); // let's write the bee movie script to root.txt (why not), check, truncate the file, then check again - let mut file = fs.get_file(PathBuf::from("root.txt")).unwrap(); + let mut file = fs.get_rw_file(PathBuf::from("root.txt")).unwrap(); file.write_all(BEE_MOVIE_SCRIPT.as_bytes()).unwrap(); assert!(file.fs.FAT_tables_are_identical().unwrap()); @@ -2120,7 +2260,9 @@ mod tests { let mut storage = Cursor::new(FAT16.to_owned()); let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - let mut file = fs.get_file(PathBuf::from("/bee movie script.txt")).unwrap(); + let mut file = fs + .get_rw_file(PathBuf::from("/bee movie script.txt")) + .unwrap(); // we are gonna truncate the bee movie script down to 20 000 bytes const NEW_SIZE: u32 = 20_000; @@ -2141,7 +2283,9 @@ mod tests { let mut storage = Cursor::new(FAT16.to_owned()); let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - let mut file = fs.get_file(PathBuf::from("/rootdir/example.txt")).unwrap(); + let mut file = fs + .get_ro_file(PathBuf::from("/rootdir/example.txt")) + .unwrap(); let mut file_string = String::new(); file.read_to_string(&mut file_string).unwrap(); @@ -2156,11 +2300,13 @@ mod tests { let mut storage = Cursor::new(FAT16.to_owned()); let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - let file = fs.get_file(PathBuf::from("/rootdir/example.txt")).unwrap(); + let file = fs + .get_ro_file(PathBuf::from("/rootdir/example.txt")) + .unwrap(); - assert_eq!(datetime!(2024-07-11 13:02:38.15), file.entry.created); - assert_eq!(datetime!(2024-07-11 13:02:38.0), file.entry.modified); - assert_eq!(date!(2024 - 07 - 11), file.entry.accessed); + assert_eq!(datetime!(2024-07-11 13:02:38.15), file.created); + assert_eq!(datetime!(2024-07-11 13:02:38.0), file.modified); + assert_eq!(date!(2024 - 07 - 11), file.accessed); } #[test] @@ -2170,7 +2316,7 @@ mod tests { let mut storage = Cursor::new(FAT12.to_owned()); let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - let mut file = fs.get_file(PathBuf::from("/foo/bar.txt")).unwrap(); + let mut file = fs.get_ro_file(PathBuf::from("/foo/bar.txt")).unwrap(); let mut file_string = String::new(); file.read_to_string(&mut file_string).unwrap(); const EXPECTED_STR: &str = "Hello, World!\n"; @@ -2180,7 +2326,7 @@ mod tests { // one FAT entry of the file we are reading is split between different sectors // this way, we also test for this case let mut file = fs - .get_file(PathBuf::from("/test/bee movie script.txt")) + .get_ro_file(PathBuf::from("/test/bee movie script.txt")) .unwrap(); assert_file_is_bee_movie_script(&mut file); } @@ -2200,7 +2346,7 @@ mod tests { let mut storage = Cursor::new(case.0.to_owned()); let fs = FileSystem::from_storage(&mut storage).unwrap(); - assert_eq!(fs.fat_type, case.1) + assert_eq!(fs.fat_type(), case.1) } } } diff --git a/src/io.rs b/src/io.rs index d0d2fb2..e017ad2 100644 --- a/src/io.rs +++ b/src/io.rs @@ -141,6 +141,11 @@ pub trait Read: IOBase { } /// A simplified version of [`std::io::Write`] for use within a `no_std` context +/// +/// Even if the storage medium doesn't have [`Write`] functionality, it should implement +/// this trait and return an [`IOErrorKind`] of type `ReadOnlyFilesystem` for all methods. +/// This way, in case a [`Write`]-related method is called for a [`FileSystem`](crate::FileSystem), +/// it will return that error. pub trait Write: IOBase { /// Write a buffer into this writer, returning how many bytes were written. /// @@ -272,7 +277,7 @@ pub trait Seek: IOBase { /// If [`self`] can be extended (because it's a [`File`](fs::File) for example), this shouldn't error out. /// In the case that a stream is being extended or if the stream can't be extended, /// this should return an [`IOError`] with an [`IOErrorKind`] of `UnexpectedEOF`. - /// The [`seek`] operation should be considered partially successfull, since some clusters were allocated. + /// The [`seek`](Seek::seek) operation should be considered partially successfull, since some clusters were allocated. /// In general, for suchs cases, it is recommended that the user first checks if there's enough storage and then perform the action fn seek(&mut self, pos: SeekFrom) -> Result; diff --git a/src/lib.rs b/src/lib.rs index 56cae85..ca461d6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ //! - Auto-`impl`s for [`std::io`] traits and structs //! - Easy-to-implement [`io`] traits //! -//! # Examples +//! ## Examples //! ``` //! # // this test fails on a no_std environment, don't run it in such a case //! extern crate simple_fatfs; @@ -39,14 +39,44 @@ //! // the image we currently use has a file named "root.txt" //! // in the root directory. Let's read it //! -//! // please keep in mind that opening a `File` borrows -//! // the parent `FileSystem` until that `File` is dropped -//! let mut file = fs.get_file(PathBuf::from("/root.txt")).unwrap(); +//! // please keep in mind that opening a `ROFile` or `RWFile` borrows +//! // the parent `FileSystem` until that `ROFile` or `RWFile` is dropped +//! let mut file = fs.get_ro_file(PathBuf::from("/root.txt")).unwrap(); //! let mut string = String::new(); //! file.read_to_string(&mut string); //! println!("root.txt contents:\n{}", string); //! } //! ``` +//! +//! ## **FAQ** +//! ### We have [`ROFile`] and [`RWFile`], why can't we also have `ROFileSystem` and `RWFileSystem`? +//! +//! *TL;DR: +//! this is not possible until [RFC 1210 \(specialization\)][`RFC 1210`] +//! [gets implemented](https://github.com/rust-lang/rust/issues/31844) +//! \(it doesn't look like it's going to happen anytime soon\)* +//! +//! One way to implement this would be to have `RWFileSystem` +//! `impl Deref(Mut) for ROFileSystem`. +//! However, we would soon stumble across a major issue. +//! +//! Internally, we use a `read_nth_sector` method that does exactly what it says, +//! but also sync any changes made to the previous sector back to the filesystem. +//! Even if we were to have two separate such methods, one for the R/O filesystem +//! that just reads a sector and another one for the R/W filesystem that also +//! syncs the previous sector, all methods within `ROFileSystem` would use the +//! one that doesn't sync any changes back to the storage medium. Thus, using +//! this method, [`Write`] functionality would become impossible. +//! +//! One solution would to have a different implementation of the `read_nth_sector` +//! method for `ROFileSystem`s for storage mediums that do and don't impl [`Write`]. +//! That's only possible using [specialization][`RFC 1210`]. +//! +//! If you happen to know a way to bypass this restriction, +//! [please open a new issue](https://github.com/Oakchris1955/simple-fatfs/issues) +//! +//! [`RFC 1210`]: https://github.com/rust-lang/rfcs/blob/master/text/1210-impl-specialization.md +//! [`Write`]: crate::io::Write #![cfg_attr(not(feature = "std"), no_std)] // Even inside unsafe functions, we must acknowlegde the usage of unsafe code From f427f9ef0aa4acece9790680240c3ef4741ea038 Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Wed, 28 Aug 2024 14:59:07 +0300 Subject: [PATCH 09/40] fix: correctly implement file attributes There's no reason to expose the VOLUME_ID or the LFN attribute to the end user --- imgs/fat12.img | Bin 1048576 -> 1048576 bytes imgs/fat12.img.check | 2 +- imgs/fat16.img | Bin 16777216 -> 16777216 bytes imgs/fat16.img.check | 2 +- src/error.rs | 2 + src/fs.rs | 149 ++++++++++++++++++++++++++++++++++++++++--- 6 files changed, 143 insertions(+), 12 deletions(-) diff --git a/imgs/fat12.img b/imgs/fat12.img index 399f5b6b77c9519d858676ff7a69ab8fdd37f09f..ebc31ab004afdaca94c730cdadaf90ec97be1340 100644 GIT binary patch delta 133 zcmZo@aA;_7*w7$l<(R>c$&dnssSNxKu6Ya$4F913!t(HRadGui00AWi0i{yuNa;uh z1`yr6OR9aB6eAEb0WmWWvj8zG5VHX>I}mdKF((jn0Wmia^8hg~5c6%{CB^Ud6abd~ BFM$96 delta 133 zcmZo@aA;_7*w7$l^|Xi~9|%hr^ceUVZj~@pfSCmhi3|)3ML_mbfB%q>h&san#~=lU zLk}FDL_CRLU;xq0yQJE8NihO36A&{4F$)m00x=s9vjZ^)5OV@C7Z7s;F%J;)0x{qA JT~hpRPXT98Epz|? diff --git a/imgs/fat12.img.check b/imgs/fat12.img.check index 59ea9c2..328e37d 100644 --- a/imgs/fat12.img.check +++ b/imgs/fat12.img.check @@ -1 +1 @@ -ec293f4087abbd287e6024fc7918836de91ced075428f15688b9fb2462733990 *imgs/fat12.img \ No newline at end of file +d591b06414897e425d4cd02550d22b60ff1b3c16d1dca3fdff532a115eefa47f *imgs/fat12.img \ No newline at end of file diff --git a/imgs/fat16.img b/imgs/fat16.img index a078ddfd26c31520215c6cb62194125b427d04c4..c16bd09e9bf5b1b5996fc9b76330d0e0c7f8a053 100644 GIT binary patch delta 1037 zcmWm8Rdf{u007W4K^W44w4i{}JyH-CobCJ@30#3D8y5{I~i5|59FPXZE>h{PlzDIb%J}Zy;4|JIGLnhRe8HDwAuHL)P7ZRCi`?WPFZsw%0m3LqAqrE3q7>sRic^BG zDM=~5;ak3=G-U`-mU5J*0u`x5WvWn>YE-8NHTj-e)TRz~smBl0rvVLVL}QxJlpkqE zb6U`nRY(34*DrVoAjiGK8F00Rj!h`|hDD8u-faE3F2 zk&I$AV;IXg#xsG5OyU=QWinHk$~2}kgWs6REM_x@xy)le3s}e^7PEw<{LV6#vx1eZ zVl``6%Q_-h&mTmxfhabziOpC2LCJ@30#3D8y5{I~i5|59FPXZE>h{PlzDIb%J}Zy;QjwO8OcOuzTiu;kd0AUoQ5QQm1QHt>u#VNtp zl%y2j@Gaj_nlc0^OF7C@fr?b3GF7NbHL6pCntV?!YEy^0)Z+*0(}0FFqA^Wq%8xXo zIW1^OD_YZrwzQ)?9q33WI@5)&bfY^x=t(bn(}%wNL_hj7fPn-V#9)Rnlwtf#IKvsi zNJcT5F^pv#w**+(=n?B@UnImBU(aFk;l z=L9D?#c9rPmUEov0vEZ&Wv+0QYh33BH@U@a?r@iT+~)xgdBkI$@RVmf=LP@pl2^Ru Q4gd0%{~}`o|A$4r0~4Yhng9R* diff --git a/imgs/fat16.img.check b/imgs/fat16.img.check index 5e88cdc..7f91cc4 100644 --- a/imgs/fat16.img.check +++ b/imgs/fat16.img.check @@ -1 +1 @@ -4eddf671476c0ac64ca0ae3bf8837e07515082f89975ddd2ff43113fde521558 *imgs/fat16.img +894fe3973c733f6a1e2e86661a5ba40c84f926bfd3710b655b49bebd0d22246d *imgs/fat16.img diff --git a/src/error.rs b/src/error.rs index 5433a53..7c54e30 100644 --- a/src/error.rs +++ b/src/error.rs @@ -149,6 +149,8 @@ where NotADirectory, /// Found a directory when we expected a file IsADirectory, + /// This file cannot be modified, as it is read-only + ReadOnlyFile, /// A file or directory wasn't found NotFound, /// An IO error occured diff --git a/src/fs.rs b/src/fs.rs index a4cf147..bc52a5c 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -411,13 +411,13 @@ impl fmt::Display for SFN { } bitflags! { - /// A list of the various attributes specified for a file/directory + /// A list of the various (raw) attributes specified for a file/directory /// /// To check whether a given [`Attributes`] struct contains a flag, use the [`contains()`](Attributes::contains()) method /// /// Generated using [bitflags](https://docs.rs/bitflags/2.6.0/bitflags/) #[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq)] - pub struct Attributes: u8 { + struct RawAttributes: u8 { /// This entry is read-only const READ_ONLY = 0x01; /// This entry is normally hidden @@ -440,6 +440,35 @@ bitflags! { } } +/// A list of the various attributes specified for a file/directory +#[derive(Debug, Clone, Copy)] +pub struct Attributes { + /// This is a read-only file + pub read_only: bool, + /// This file is to be hidden unless a request is issued + /// explicitly requesting inclusion of “hidden files” + pub hidden: bool, + /// This is a system file and shouldn't be listed unless a request + /// is issued explicitly requesting inclusion of system files” + pub system: bool, + /// This file has been modified since last archival + /// or has never been archived. + /// + /// This field should only concern archival software + pub archive: bool, +} + +impl From for Attributes { + fn from(value: RawAttributes) -> Self { + Attributes { + read_only: value.contains(RawAttributes::READ_ONLY), + hidden: value.contains(RawAttributes::HIDDEN), + system: value.contains(RawAttributes::SYSTEM), + archive: value.contains(RawAttributes::ARCHIVE), + } + } +} + const START_YEAR: i32 = 1980; #[bitfield(u16)] @@ -540,7 +569,7 @@ impl TryFrom for PrimitiveDateTime { #[derive(Serialize, Deserialize, Debug, Clone, Copy)] struct FATDirEntry { sfn: SFN, - attributes: Attributes, + attributes: RawAttributes, _reserved: [u8; 1], created: EntryCreationTime, accessed: DateAttribute, @@ -596,7 +625,7 @@ impl LFNEntry { struct RawProperties { name: String, is_dir: bool, - attributes: Attributes, + attributes: RawAttributes, created: PrimitiveDateTime, modified: PrimitiveDateTime, accessed: Date, @@ -669,7 +698,7 @@ impl Properties { fn from_raw(raw: RawProperties, path: PathBuf) -> Self { Properties { path, - attributes: raw.attributes, + attributes: raw.attributes.into(), created: raw.created, modified: raw.modified, accessed: raw.accessed, @@ -1263,6 +1292,34 @@ fn bincode_config() -> impl bincode::Options + Copy { .with_little_endian() } +/// Filter (or not) things like hidden files/directories +/// for FileSystem operations +#[derive(Debug)] +struct FileFilter { + show_hidden: bool, + show_systen: bool, +} + +impl FileFilter { + fn filter(&self, item: &RawProperties) -> bool { + let is_hidden = item.attributes.contains(RawAttributes::HIDDEN); + let is_system = item.attributes.contains(RawAttributes::SYSTEM); + let should_filter = !self.show_hidden && is_hidden || !self.show_systen && is_system; + + !should_filter + } +} + +impl Default for FileFilter { + fn default() -> Self { + // The FAT spec says to filter everything by default + FileFilter { + show_hidden: false, + show_systen: false, + } + } +} + /// An API to process a FAT filesystem #[derive(Debug)] pub struct FileSystem @@ -1282,6 +1339,8 @@ where // since `self.fat_type()` calls like 5 nested functions, we keep this cached and expose it as a public field fat_type: FATType, props: FSProperties, + + filter: FileFilter, } impl OffsetConversions for FileSystem @@ -1315,6 +1374,28 @@ where } } +/// Setter functions +impl FileSystem +where + S: Read + Write + Seek, +{ + /// Whether or not to list hidden files + /// + /// Off by default + #[inline] + pub fn show_hidden(&mut self, show: bool) { + self.filter.show_hidden = show; + } + + /// Whether or not to list system files + /// + /// Off by default + #[inline] + pub fn show_system(&mut self, show: bool) { + self.filter.show_systen = show; + } +} + /// Constructors impl FileSystem where @@ -1430,6 +1511,7 @@ where boot_record, fat_type, props, + filter: FileFilter::default(), }; if !fs.FAT_tables_are_identical()? { @@ -1472,7 +1554,7 @@ where continue; }; - if entry.attributes.contains(Attributes::LFN) { + if entry.attributes.contains(RawAttributes::LFN) { // TODO: perhaps there is a way to utilize the `order` field? let Ok(lfn_entry) = bincode_config().deserialize::(&chunk) else { continue; @@ -1521,7 +1603,7 @@ where ) { entries.push(RawProperties { name: filename, - is_dir: entry.attributes.contains(Attributes::DIRECTORY), + is_dir: entry.attributes.contains(RawAttributes::DIRECTORY), attributes: entry.attributes, created, modified, @@ -1797,7 +1879,7 @@ where FATType::ExFAT => todo!("ExFAT not yet implemented"), }) } - } +} /// Internal [`Write`]-related low-level functions impl FileSystem @@ -1918,7 +2000,7 @@ where for dir_name in path.clone().into_iter() { let dir_cluster = match entries.iter().find(|entry| { - entry.name == dir_name && entry.attributes.contains(Attributes::DIRECTORY) + entry.name == dir_name && entry.attributes.contains(RawAttributes::DIRECTORY) }) { Some(entry) => entry.data_cluster, None => { @@ -1934,6 +2016,7 @@ where // contains what we want, let's map it to DirEntries and return Ok(entries .into_iter() + .filter(|x| self.filter.filter(x)) .map(|rawentry| { let mut entry_path = path.clone(); @@ -2014,7 +2097,12 @@ where // otherwise it should return an error. self.storage.write_all(&[])?; - self.get_ro_file(path).map(|ro_file| RWFile { ro_file }) + let ro_file = self.get_ro_file(path)?; + if ro_file.attributes.read_only { + return Err(FSError::ReadOnlyFile); + }; + + Ok(RWFile { ro_file }) } } @@ -2276,6 +2364,47 @@ mod tests { assert_eq!(file_string, expected_string); } + #[test] + fn read_only_file() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT16.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let file_result = fs.get_rw_file(PathBuf::from("/rootdir/example.txt")); + + match file_result { + Err(err) => match err { + FSError::ReadOnlyFile => (), + _ => panic!("unexpected IOError"), + }, + _ => panic!("file is marked read-only, yet somehow we got a RWFile for it"), + } + } + + #[test] + fn get_hidden_file() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT12.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let file_path = PathBuf::from("/hidden"); + let file_result = fs.get_ro_file(file_path.clone()); + match file_result { + Err(err) => match err { + FSError::NotFound => (), + _ => panic!("unexpected IOError"), + }, + _ => panic!("file should be hidden by default"), + } + + // let's now allow the filesystem to list hidden files + fs.show_hidden(true); + let file = fs.get_ro_file(file_path).unwrap(); + assert!(file.attributes.hidden); + } + #[test] fn read_file_in_subdir() { use std::io::Cursor; From 21d83d667ebf3f1b4753a14e74495089c6458d96 Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Thu, 29 Aug 2024 14:50:33 +0300 Subject: [PATCH 10/40] fix: correctly parse 8.3 & LFN entries that span multiple sectors Also, made code more clear & removed unnecessary `unsafe` keywords --- src/fs.rs | 203 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 115 insertions(+), 88 deletions(-) diff --git a/src/fs.rs b/src/fs.rs index bc52a5c..eb3eac3 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1524,141 +1524,168 @@ where } } -/// Internal [`Read`]-related low-level functions -impl FileSystem -where - S: Read + Write + Seek, -{ - /// Unsafe because the sector number must point to an area with directory entries +#[derive(Debug)] +struct EntryParser { + entries: Vec, + lfn_buf: Vec, + lfn_checksum: Option, +} + +impl Default for EntryParser { + fn default() -> Self { + EntryParser { + entries: Vec::new(), + lfn_buf: Vec::new(), + lfn_checksum: None, + } + } +} + +const UNUSED_ENTRY: u8 = 0xE5; +const LAST_AND_UNUSED_ENTRY: u8 = 0x00; + +impl EntryParser { + /// Parses a sector of 8.3 & LFN entries /// - /// Also the sector number starts from the beginning of the partition - unsafe fn process_entry_sector( + /// Returns a [`Result`] indicating whether or not + /// this sector was the last one in the chain containing entries + fn parse_sector( &mut self, sector: u32, - ) -> FSResult, S::Error> { - let mut entries = Vec::new(); - let mut lfn_buf: Vec = Vec::new(); - let mut lfn_checksum: Option = None; + fs: &mut FileSystem, + ) -> Result::Error> + where + S: Read + Write + Seek, + { + for chunk in fs.read_nth_sector(sector.into())?.chunks(32) { + match chunk[0] { + LAST_AND_UNUSED_ENTRY => return Ok(true), + UNUSED_ENTRY => continue, + _ => (), + }; - 'outer: loop { - for chunk in self.read_nth_sector(sector.into())?.chunks(32) { - match chunk[0] { - // nothing else to read - 0 => break 'outer, - // unused entry - 0xE5 => continue, - _ => (), - }; + let Ok(entry) = bincode_config().deserialize::(&chunk) else { + continue; + }; - let Ok(entry) = bincode_config().deserialize::(&chunk) else { + if entry.attributes.contains(RawAttributes::LFN) { + // TODO: perhaps there is a way to utilize the `order` field? + let Ok(lfn_entry) = bincode_config().deserialize::(&chunk) else { continue; }; - if entry.attributes.contains(RawAttributes::LFN) { - // TODO: perhaps there is a way to utilize the `order` field? - let Ok(lfn_entry) = bincode_config().deserialize::(&chunk) else { - continue; - }; - - // If the signature verification fails, consider this entry corrupted - if !lfn_entry.verify_signature() { - continue; - } + // If the signature verification fails, consider this entry corrupted + if !lfn_entry.verify_signature() { + continue; + } - match lfn_checksum { - Some(checksum) => { - if checksum != lfn_entry.checksum { - lfn_checksum = None; - lfn_buf.clear(); - continue; - } + match self.lfn_checksum { + Some(checksum) => { + if checksum != lfn_entry.checksum { + self.lfn_checksum = None; + self.lfn_buf.clear(); + continue; } - None => lfn_checksum = Some(lfn_entry.checksum), - } - - let char_arr = lfn_entry.get_byte_slice().to_vec(); - if let Ok(temp_str) = string_from_lfn(&char_arr) { - lfn_buf.push(temp_str); } + None => self.lfn_checksum = Some(lfn_entry.checksum), + } - continue; + let char_arr = lfn_entry.get_byte_slice().to_vec(); + if let Ok(temp_str) = string_from_lfn(&char_arr) { + self.lfn_buf.push(temp_str); } - let filename = if !lfn_buf.is_empty() - && lfn_checksum.is_some_and(|checksum| checksum == entry.sfn.gen_checksum()) - { - // for efficiency reasons, we store the LFN string sequences as we read them - let parsed_str: String = lfn_buf.iter().cloned().rev().collect(); - lfn_buf.clear(); - lfn_checksum = None; - parsed_str - } else { - entry.sfn.to_string() - }; + continue; + } - if let (Ok(created), Ok(modified), Ok(accessed)) = ( - entry.created.try_into(), - entry.modified.try_into(), - entry.accessed.try_into(), - ) { - entries.push(RawProperties { - name: filename, - is_dir: entry.attributes.contains(RawAttributes::DIRECTORY), - attributes: entry.attributes, - created, - modified, - accessed, - file_size: entry.file_size, - data_cluster: ((entry.cluster_high as u32) << 16) - + entry.cluster_low as u32, - }) - } + let filename = if !self.lfn_buf.is_empty() + && self + .lfn_checksum + .is_some_and(|checksum| checksum == entry.sfn.gen_checksum()) + { + // for efficiency reasons, we store the LFN string sequences as we read them + let parsed_str: String = self.lfn_buf.iter().cloned().rev().collect(); + self.lfn_buf.clear(); + self.lfn_checksum = None; + parsed_str + } else { + entry.sfn.to_string() + }; + + if let (Ok(created), Ok(modified), Ok(accessed)) = ( + entry.created.try_into(), + entry.modified.try_into(), + entry.accessed.try_into(), + ) { + self.entries.push(RawProperties { + name: filename, + is_dir: entry.attributes.contains(RawAttributes::DIRECTORY), + attributes: entry.attributes, + created, + modified, + accessed, + file_size: entry.file_size, + data_cluster: ((entry.cluster_high as u32) << 16) + entry.cluster_low as u32, + }) } } - Ok(entries) + Ok(false) } + /// Consumes [`Self`](EntryParser) & returns a `Vec` of [`RawProperties`] + /// of the parsed entries + fn finish(self) -> Vec { + self.entries + } +} + +/// Internal [`Read`]-related low-level functions +impl FileSystem +where + S: Read + Write + Seek, +{ fn process_root_dir(&mut self) -> FSResult, S::Error> { match self.boot_record { BootRecord::FAT(boot_record_fat) => match boot_record_fat.ebr { EBR::FAT12_16(_ebr_fat12_16) => { - let mut entries = Vec::new(); + let mut entry_parser = EntryParser::default(); let root_dir_sector = boot_record_fat.first_root_dir_sector(); let sector_count = boot_record_fat.root_dir_sectors(); for sector in root_dir_sector..(root_dir_sector + sector_count) { - let mut new_entries = unsafe { self.process_entry_sector(sector.into())? }; - entries.append(&mut new_entries); + if entry_parser.parse_sector(sector.into(), self)? { + break; + } } - Ok(entries) + Ok(entry_parser.finish()) } EBR::FAT32(ebr_fat32, _) => { let cluster = ebr_fat32.root_cluster; - unsafe { self.process_normal_dir(cluster) } + self.process_normal_dir(cluster) } }, BootRecord::ExFAT(_boot_record_exfat) => todo!(), } } - /// Unsafe for the same reason as [`process_entry_sector`] - unsafe fn process_normal_dir( + fn process_normal_dir( &mut self, mut data_cluster: u32, ) -> FSResult, S::Error> { - let mut entries = Vec::new(); + let mut entry_parser = EntryParser::default(); - loop { + 'outer: loop { // FAT specification, section 6.7 let first_sector_of_cluster = self.data_cluster_to_partition_sector(data_cluster); for sector in first_sector_of_cluster ..(first_sector_of_cluster + self.sectors_per_cluster() as u32) { - let mut new_entries = unsafe { self.process_entry_sector(sector.into())? }; - entries.append(&mut new_entries); + if entry_parser.parse_sector(sector.into(), self)? { + break 'outer; + } } // Read corresponding FAT entry @@ -1679,7 +1706,7 @@ where } } - Ok(entries) + Ok(entry_parser.finish()) } /// Gets the next free cluster. Returns an IO [`Result`] @@ -2009,7 +2036,7 @@ where } }; - entries = unsafe { self.process_normal_dir(dir_cluster)? }; + entries = self.process_normal_dir(dir_cluster)?; } // if we haven't returned by now, that means that the entries vector From f88942b988d2b0d8dcffa913b28828a6a8db40ca Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:23:30 +0300 Subject: [PATCH 11/40] feat: add `remove()` method to RWFile struct --- src/fs.rs | 251 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 235 insertions(+), 16 deletions(-) diff --git a/src/fs.rs b/src/fs.rs index eb3eac3..031d963 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -146,8 +146,7 @@ impl BootRecordFAT { /// The size of the root directory (unless we have FAT32, in which case the size will be 0) /// This calculation will round up pub(crate) fn root_dir_sectors(&self) -> u16 { - // 32 is the size of a directory entry in bytes - ((self.bpb.root_entry_count * 32) + (self.bpb.bytes_per_sector - 1)) + ((self.bpb.root_entry_count * DIRENTRY_SIZE as u16) + (self.bpb.bytes_per_sector - 1)) / self.bpb.bytes_per_sector } @@ -332,6 +331,9 @@ impl FATType { } } +// the first 2 entries are reserved +const RESERVED_FAT_ENTRIES: u32 = 2; + #[derive(Debug, Clone, PartialEq)] enum FATEntry { /// This cluster is free @@ -566,6 +568,9 @@ impl TryFrom for PrimitiveDateTime { } } +// a directory entry occupies 32 bytes +const DIRENTRY_SIZE: usize = 32; + #[derive(Serialize, Deserialize, Debug, Clone, Copy)] struct FATDirEntry { sfn: SFN, @@ -620,6 +625,40 @@ impl LFNEntry { } } +/// The location of a [`FATDirEntry`] within a root directory sector +/// or a data region cluster +#[derive(Debug, Clone)] +enum EntryLocation { + /// Sector offset from the start of the root directory region + RootDirSector(u16), + /// Cluster offset from the start of the data region + DataCluster(u32), +} + +impl EntryLocation { + fn from_partition_sector(sector: u32, fs: &mut FileSystem) -> Self + where + S: Read + Write + Seek, + { + if sector < fs.first_data_sector() { + EntryLocation::RootDirSector((sector - fs.props.first_root_dir_sector as u32) as u16) + } else { + EntryLocation::DataCluster(fs.partition_sector_to_data_cluster(sector)) + } + } +} + +/// The location of a chain of [`FATDirEntry`] +#[derive(Debug)] +struct DirEntryChain { + /// the location of the first corresponding entry + location: EntryLocation, + /// the first entry's index/offset from the start of the sector + index: u32, + /// how many (contiguous) entries this entry chain has + len: u32, +} + /// A resolved file/directory entry (for internal usage only) #[derive(Debug)] struct RawProperties { @@ -631,6 +670,8 @@ struct RawProperties { accessed: Date, file_size: u32, data_cluster: u32, + + chain_props: DirEntryChain, } /// A container for file/directory properties @@ -643,6 +684,9 @@ pub struct Properties { accessed: Date, file_size: u32, data_cluster: u32, + + // internal fields + chain_props: DirEntryChain, } /// Getter methods @@ -704,6 +748,7 @@ impl Properties { accessed: raw.accessed, file_size: raw.file_size, data_cluster: raw.data_cluster, + chain_props: raw.chain_props, } } } @@ -836,10 +881,7 @@ where let previous_size = self.file_size; self.file_size = size; - let mut next_cluster_option = self - .ro_file - .fs - .get_next_cluster(self.ro_file.props.current_cluster)?; + let mut next_cluster_option = self.get_next_cluster()?; // we set the new last cluster in the chain to be EOF self.ro_file @@ -865,6 +907,90 @@ where Ok(()) } + + /// Remove the current file from the [`FileSystem`] + pub fn remove(mut self) -> Result<(), ::Error> { + // we begin by removing the corresponding entries... + let mut entries_freed = 0; + let mut current_offset = self.props.entry.chain_props.index; + + // current_cluster_option is `None` if we are dealing with a root directory entry + let (mut current_sector, current_cluster_option): (u32, Option) = + match self.props.entry.chain_props.location { + EntryLocation::RootDirSector(root_dir_sector) => ( + (root_dir_sector + self.fs.props.first_root_dir_sector).into(), + None, + ), + EntryLocation::DataCluster(data_cluster) => ( + self.fs.data_cluster_to_partition_sector(data_cluster), + Some(data_cluster), + ), + }; + + while entries_freed < self.props.entry.chain_props.len { + if current_sector as u64 != self.fs.stored_sector { + self.fs.read_nth_sector(current_sector.into())?; + } + + // we won't even bother zeroing the entire thing, just the first byte + let byte_offset = current_offset as usize * DIRENTRY_SIZE; + self.fs.sector_buffer[byte_offset] = UNUSED_ENTRY; + self.fs.buffer_modified = true; + + log::trace!( + "freed entry at sector {} with byte offset {}", + current_sector, + byte_offset + ); + + if current_offset + 1 >= (self.fs.sector_size() / DIRENTRY_SIZE as u32) { + // we have moved to a new sector + current_sector += 1; + + match current_cluster_option { + // data region + Some(mut current_cluster) => { + if self.fs.partition_sector_to_data_cluster(current_sector) + != current_cluster + { + current_cluster = self.fs.get_next_cluster(current_cluster)?.unwrap(); + current_sector = + self.fs.data_cluster_to_partition_sector(current_cluster); + } + } + None => (), + } + + current_offset = 0; + } else { + current_offset += 1 + } + + entries_freed += 1; + } + + // ... and then we free the data clusters + + // rewind back to the start of the file + self.rewind()?; + + loop { + let current_cluster = self.props.current_cluster; + let next_cluster_option = self.get_next_cluster()?; + + // free the current cluster + self.fs + .write_nth_FAT_entry(current_cluster, FATEntry::Free)?; + + // proceed to the next one, otherwise break + match next_cluster_option { + Some(next_cluster) => self.props.current_cluster = next_cluster, + None => break, + } + } + + Ok(()) + } } // Internal functions @@ -876,14 +1002,17 @@ where /// Panics if the current cluser doesn't point to another clluster fn next_cluster(&mut self) -> Result<(), ::Error> { // when a `ROFile` is created, `cluster_chain_is_healthy` is called, if it fails, that ROFile is dropped - self.props.current_cluster = self - .fs - .get_next_cluster(self.props.current_cluster)? - .unwrap(); + self.props.current_cluster = self.get_next_cluster()?.unwrap(); Ok(()) } + #[inline] + /// Non-[`panic`]king version of [`next_cluster()`](ROFile::next_cluster) + fn get_next_cluster(&mut self) -> Result, ::Error> { + Ok(self.fs.get_next_cluster(self.props.current_cluster)?) + } + /// Returns that last cluster in the file's cluster chain fn last_cluster_in_chain(&mut self) -> Result::Error> { // we begin from the current cluster to save some time @@ -1257,16 +1386,20 @@ trait OffsetConversions { self.cluster_size() / self.sector_size() as u64 } - // this function assumes that the first sector is also the first sector of the partition #[inline] fn sector_to_partition_offset(&self, sector: u32) -> u32 { sector * self.sector_size() } - // these three functions assume that the first sector (or cluster) is the first sector (or cluster) of the data area #[inline] fn data_cluster_to_partition_sector(&self, cluster: u32) -> u32 { - self.cluster_to_sector((cluster - 2).into()) + self.first_data_sector() + self.cluster_to_sector((cluster - RESERVED_FAT_ENTRIES).into()) + self.first_data_sector() + } + + #[inline] + fn partition_sector_to_data_cluster(&self, sector: u32) -> u32 { + (sector - self.first_data_sector()) / self.sectors_per_cluster() as u32 + + RESERVED_FAT_ENTRIES } } @@ -1279,6 +1412,7 @@ struct FSProperties { total_clusters: u32, /// sector offset of the FAT fat_table_count: u8, + first_root_dir_sector: u16, first_data_sector: u32, } @@ -1474,6 +1608,11 @@ where } }; + let first_root_dir_sector = match boot_record { + BootRecord::FAT(boot_record_fat) => boot_record_fat.first_root_dir_sector().into(), + BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), + }; + let first_data_sector = match boot_record { BootRecord::FAT(boot_record_fat) => boot_record_fat.first_data_sector().into(), BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), @@ -1500,6 +1639,7 @@ where fat_table_count, total_sectors, total_clusters, + first_root_dir_sector, first_data_sector, }; @@ -1529,6 +1669,7 @@ struct EntryParser { entries: Vec, lfn_buf: Vec, lfn_checksum: Option, + current_chain: Option, } impl Default for EntryParser { @@ -1537,6 +1678,7 @@ impl Default for EntryParser { entries: Vec::new(), lfn_buf: Vec::new(), lfn_checksum: None, + current_chain: None, } } } @@ -1545,6 +1687,13 @@ const UNUSED_ENTRY: u8 = 0xE5; const LAST_AND_UNUSED_ENTRY: u8 = 0x00; impl EntryParser { + #[inline] + fn _decrement_parsed_entries_counter(&mut self) { + if let Some(current_chain) = &mut self.current_chain { + current_chain.len -= 1 + } + } + /// Parses a sector of 8.3 & LFN entries /// /// Returns a [`Result`] indicating whether or not @@ -1557,7 +1706,13 @@ impl EntryParser { where S: Read + Write + Seek, { - for chunk in fs.read_nth_sector(sector.into())?.chunks(32) { + let entry_location = EntryLocation::from_partition_sector(sector, fs); + + for (index, chunk) in fs + .read_nth_sector(sector.into())? + .chunks(DIRENTRY_SIZE) + .enumerate() + { match chunk[0] { LAST_AND_UNUSED_ENTRY => return Ok(true), UNUSED_ENTRY => continue, @@ -1568,14 +1723,28 @@ impl EntryParser { continue; }; + // update current entry chain data + match &mut self.current_chain { + Some(current_chain) => current_chain.len += 1, + None => { + self.current_chain = Some(DirEntryChain { + location: entry_location.clone(), + index: index as u32, + len: 1, + }) + } + } + if entry.attributes.contains(RawAttributes::LFN) { // TODO: perhaps there is a way to utilize the `order` field? let Ok(lfn_entry) = bincode_config().deserialize::(&chunk) else { + self._decrement_parsed_entries_counter(); continue; }; // If the signature verification fails, consider this entry corrupted if !lfn_entry.verify_signature() { + self._decrement_parsed_entries_counter(); continue; } @@ -1584,6 +1753,7 @@ impl EntryParser { if checksum != lfn_entry.checksum { self.lfn_checksum = None; self.lfn_buf.clear(); + self.current_chain = None; continue; } } @@ -1626,6 +1796,10 @@ impl EntryParser { accessed, file_size: entry.file_size, data_cluster: ((entry.cluster_high as u32) << 16) + entry.cluster_low as u32, + chain_props: self + .current_chain + .take() + .expect("at this point, this shouldn't be None"), }) } } @@ -1714,8 +1888,7 @@ where fn next_free_cluster(&mut self) -> Result, S::Error> { let start_cluster = match self.boot_record { BootRecord::FAT(boot_record_fat) => { - // the first 2 entries are reserved - let mut first_free_cluster = 2; + let mut first_free_cluster = RESERVED_FAT_ENTRIES; if let EBR::FAT32(_, fsinfo) = boot_record_fat.ebr { // a value of u32::MAX denotes unawareness of the first free cluster @@ -2341,6 +2514,52 @@ mod tests { } } + #[test] + fn remove_root_dir_file() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT16.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + // the bee movie script (here) is in the root directory region + let file_path = PathBuf::from("/bee movie script.txt"); + let file = fs.get_rw_file(file_path.clone()).unwrap(); + file.remove().unwrap(); + + // the file should now be gone + let file_result = fs.get_ro_file(file_path); + match file_result { + Err(err) => match err { + FSError::NotFound => (), + _ => panic!("unexpected IOError: {:?}", err), + }, + _ => panic!("file should have been deleted by now"), + } + } + + #[test] + fn remove_data_region_file() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT12.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + // the bee movie script (here) is in the data region + let file_path = PathBuf::from("/test/bee movie script.txt"); + let file = fs.get_rw_file(file_path.clone()).unwrap(); + file.remove().unwrap(); + + // the file should now be gone + let file_result = fs.get_ro_file(file_path); + match file_result { + Err(err) => match err { + FSError::NotFound => (), + _ => panic!("unexpected IOError: {:?}", err), + }, + _ => panic!("file should have been deleted by now"), + } + } + #[test] #[allow(non_snake_case)] fn FAT_tables_after_write_are_identical() { From a60e55b933881b23be0876dde587298efed0975f Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sat, 31 Aug 2024 12:40:42 +0300 Subject: [PATCH 12/40] fix: `truncate()` now works if the new size is close to the old one --- src/fs.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/fs.rs b/src/fs.rs index 031d963..c40ba9f 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -868,6 +868,10 @@ where pub fn truncate(&mut self, size: u32) -> Result<(), ::Error> { // looks like the new truncated size would be smaller than the current one, so we just return if size.next_multiple_of(self.fs.props.cluster_size as u32) >= self.file_size { + if size < self.file_size { + self.file_size = size; + } + return Ok(()); } From d8a5cfd6c703a170bd5cdb41d7f89a8a7cac72bc Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sat, 31 Aug 2024 12:43:30 +0300 Subject: [PATCH 13/40] fix(fat32): various fat32-related bug fixes Due to those bugs, it would be impossible to actually use this library on a FAT32 filesystem --- src/fs.rs | 21 ++++++++++++++------- src/lib.rs | 1 + src/utils/bits.rs | 6 ++++++ src/utils/mod.rs | 1 + 4 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 src/utils/bits.rs create mode 100644 src/utils/mod.rs diff --git a/src/fs.rs b/src/fs.rs index c40ba9f..45fffc7 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -21,7 +21,7 @@ use serde_big_array::BigArray; use ::time; use time::{Date, PrimitiveDateTime, Time}; -use crate::{error::*, io::prelude::*, path::PathBuf}; +use crate::{error::*, io::prelude::*, path::PathBuf, utils}; /// The minimum size (in bytes) a sector is allowed to have pub const SECTOR_SIZE_MIN: usize = 512; @@ -157,7 +157,7 @@ impl BootRecordFAT { } #[inline] - /// The first sector of the root directory + /// The first sector of the root directory (returns the first data sector on FAT32) pub(crate) fn first_root_dir_sector(&self) -> u16 { self.first_fat_sector() + self.bpb.table_count as u16 * self.fat_sector_size() as u16 } @@ -319,7 +319,8 @@ impl FATType { match self { FATType::FAT12 => 12, FATType::FAT16 => 16, - FATType::FAT32 => 28, + // the high 4 bits are ignored, but are still part of the entry + FATType::FAT32 => 32, FATType::ExFAT => 32, } } @@ -629,7 +630,7 @@ impl LFNEntry { /// or a data region cluster #[derive(Debug, Clone)] enum EntryLocation { - /// Sector offset from the start of the root directory region + /// Sector offset from the start of the root directory region (FAT12/16) RootDirSector(u16), /// Cluster offset from the start of the data region DataCluster(u32), @@ -2070,8 +2071,8 @@ where }, FATType::FAT32 => match value { 0x00000000 => FATEntry::Free, - 0xFFFFFFF7 => FATEntry::Bad, - 0xFFFFFFF8..=0xFFFFFFFE | 0xFFFFFFFF => FATEntry::EOF, + 0x0FFFFFF7 => FATEntry::Bad, + 0x0FFFFFF8..=0xFFFFFFE | 0x0FFFFFFF => FATEntry::EOF, _ => { if (0x00000002..(self.props.total_clusters + 1)).contains(&value.into()) { FATEntry::Allocated(value.into()) @@ -2096,9 +2097,15 @@ where let entry_size = self.fat_type.entry_size(); let entry_props = FATEntryProps::new(n, &self); - let mask = (1 << self.fat_type.bits_per_entry()) - 1; + // the previous solution would overflow, here's a correct implementation + let mask = utils::bits::setbits_u32(self.fat_type.bits_per_entry()); let mut value: u32 = u32::from(entry.clone()) & mask; + if self.fat_type == FATType::FAT32 { + // in FAT32, the high 4 bits are unused + value &= 0x0FFFFFFF; + } + match self.fat_type { FATType::FAT12 => { let should_shift = n & 1 != 0; diff --git a/src/lib.rs b/src/lib.rs index ca461d6..9eaf088 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -99,6 +99,7 @@ mod error; mod fs; pub mod io; mod path; +mod utils; pub use error::*; pub use fs::*; diff --git a/src/utils/bits.rs b/src/utils/bits.rs new file mode 100644 index 0000000..787a352 --- /dev/null +++ b/src/utils/bits.rs @@ -0,0 +1,6 @@ +/// Sets the low `n` bits of a [`u32`] to `1` +/// +/// https://users.rust-lang.org/t/how-to-make-an-integer-with-n-bits-set-without-overflow/63078/3 +pub fn setbits_u32(n: u8) -> u32 { + u32::MAX >> (u32::BITS - u32::from(n)) +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..7909b54 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1 @@ +pub(crate) mod bits; From 418451ac38d5aed787954405a63d52f3440f02d1 Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sat, 31 Aug 2024 12:45:33 +0300 Subject: [PATCH 14/40] test(fat32): create various fat32-related tests --- imgs/fat32.img | Bin 34603008 -> 34603008 bytes imgs/fat32.img.check | 2 +- src/fs.rs | 105 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 1 deletion(-) diff --git a/imgs/fat32.img b/imgs/fat32.img index 6b10a688585f638310e24e02d868b1966b590a82..eb4433ef0937e694dd6a7addc927a94ce99fa822 100644 GIT binary patch delta 64888 zcmeIbTa#pGmZp_OPh&@zgrn;MfoXC_?nUYeeRz^vaoITt< zB3$9&9_@z=g)V0IHCNR963mJp05|-NF$2Sl8_b4ZP(07OzU}U*s%|jMFvE-i_gE#=Wp}*H~9RUeEuyye}~V%&FA;| z`~jZG z{_4lS_s{;{{m=dT|KuP3^#>XF|G5lwrxOJ4_R;#$J-YwjMgkE+|5ty1{PN4QH-q}$ zqZi+de*ewyBl-S|xbnYuuK#bwi1m`AKe+y*7hiFQA3yrLZ`$?#`#WCVJvv>X8Y8Zn z(11T%w~Kc-c>VFyuRcCY1AhGIzyFhQHQ(Yes^&G!F)Jh41Rthp6&f_4Q4GDi}f#m`G^1f zcyc}+&VTvK_x^>e$!xYbS%0_Y;_rXw?+H8a{O`HH4=$FAo567K?DXuXKN(z1XOq?a zYCX9*;G+NecMt!{a}Ey=J?)?VsN1!hj%|n5A+Di)r(^H@UM+eujpR6{OkF>$yQ%xp zIYU3e9^Cl*;?Y~A!>=nYXU6~F(UXrqe);O_fBa#F{*->CG+19vmXpDBHDIjohWCul z`D8L!T`e}VF@Fw*=d+3TxR~9a9K2Ys26xb=$*eqB4c`=(#xaa)d7MxE;!_6uutp?-8 z{QdP{3Dea_~7YqKDb|O=sE-OmXW=( zYpcQ4@b-2x=SGhYUN8s@1#|E|1AaTDonEoIJvsP%vAA}oY2}iyzhLY-M|`y&PM52r z!5c=|d10mpx5MdpaJg8F2RDoPCkLm4+sU%Kn2gp8#_eL6*&8242%o&a zIyv}yd*R8!%i-bR1?^cgK+fqUfBRpBk_n}Q{x+M=*A5~Np(C5`zdt#6b$2*8Lm+6} z&E)Xlo1vq&7;ILD1DZGMX3Fk#K3Fd91|NR-$44K0@BtHc TUBc8fmAmP>S_2O0m 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 NBq0e&_#YB(;|ZLbnDPJs diff --git a/imgs/fat32.img.check b/imgs/fat32.img.check index 15f6590..420c3bc 100644 --- a/imgs/fat32.img.check +++ b/imgs/fat32.img.check @@ -1 +1 @@ -604b7fdf8131868241dc4f33817994838f0da4d9795d400c0120e460cf7897e1 *imgs/fat32.img \ No newline at end of file +5e49c78dfcf001666926151f1a95df991f91729016cc3949fc589702604d2a5f *imgs/fat32.img \ No newline at end of file diff --git a/src/fs.rs b/src/fs.rs index 45fffc7..626e79c 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -2717,6 +2717,111 @@ mod tests { assert_file_is_bee_movie_script(&mut file); } + #[test] + fn read_file_fat32() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT32.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let mut file = fs + .get_ro_file(PathBuf::from("/secret/bee movie script.txt")) + .unwrap(); + + assert_file_is_bee_movie_script(&mut file); + } + + #[test] + fn seek_n_read_fat32() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT32.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let mut file = fs.get_ro_file(PathBuf::from("/hello.txt")).unwrap(); + file.seek(SeekFrom::Start(13)).unwrap(); + + let mut string = String::new(); + file.read_to_string(&mut string).unwrap(); + const EXPECTED_STR: &str = "FAT32 filesystem!!!\n"; + + assert_eq!(string, EXPECTED_STR); + } + + #[test] + fn write_to_fat32_file() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT32.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let mut file = fs.get_rw_file(PathBuf::from("/hello.txt")).unwrap(); + // an arbitrary offset to seek to + const START_OFFSET: u64 = 1436; + file.seek(SeekFrom::Start(START_OFFSET)).unwrap(); + + file.write_all(BEE_MOVIE_SCRIPT.as_bytes()).unwrap(); + + // seek back + file.seek(SeekFrom::Current(-(BEE_MOVIE_SCRIPT.len() as i64))) + .unwrap(); + + // read back what we wrote + let mut string = String::new(); + file.read_to_string(&mut string).unwrap(); + assert_eq!(string, BEE_MOVIE_SCRIPT); + + // let's also read back what was (and hopefully still is) + // at the start of the file + const EXPECTED_STR: &str = "Hello from a FAT32 filesystem!!!\n"; + file.rewind().unwrap(); + let mut buf = [0_u8; EXPECTED_STR.len()]; + file.read_exact(&mut buf).unwrap(); + + let stored_text = str::from_utf8(&buf).unwrap(); + assert_eq!(stored_text, EXPECTED_STR) + } + + #[test] + fn truncate_fat32_file() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT32.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + const EXPECTED_STR: &str = "Hello fr"; + + let mut file = fs.get_rw_file(PathBuf::from("/hello.txt")).unwrap(); + file.truncate(EXPECTED_STR.len() as u32).unwrap(); + + let mut string = String::new(); + file.read_to_string(&mut string).unwrap(); + assert_eq!(string, EXPECTED_STR); + } + + #[test] + fn remove_fat32_file() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT32.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let file_path = PathBuf::from("/secret/bee movie script.txt"); + + let file = fs.get_rw_file(file_path.clone()).unwrap(); + file.remove().unwrap(); + + // the file should now be gone + let file_result = fs.get_ro_file(file_path); + match file_result { + Err(err) => match err { + FSError::NotFound => (), + _ => panic!("unexpected IOError: {:?}", err), + }, + _ => panic!("file should have been deleted by now"), + } + } + #[test] fn assert_img_fat_type() { static TEST_CASES: &[(&[u8], FATType)] = &[ From 8b61fea7f5ecc180446a5e8eafdad85089292d8f Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sun, 1 Sep 2024 15:07:35 +0300 Subject: [PATCH 15/40] refactor: split fs.rs file into multiple modules --- Cargo.toml | 2 +- src/error.rs | 5 +- src/fs.rs | 2843 ------------------------------------------ src/fs/bpb.rs | 275 ++++ src/fs/consts.rs | 8 + src/fs/direntry.rs | 336 +++++ src/fs/file.rs | 604 +++++++++ src/fs/fs.rs | 1140 +++++++++++++++++ src/fs/mod.rs | 13 + src/fs/tests.rs | 486 ++++++++ src/io.rs | 8 +- src/lib.rs | 2 + src/path.rs | 11 +- src/utils/bincode.rs | 11 + src/utils/bits.rs | 2 +- src/utils/mod.rs | 2 + src/utils/string.rs | 14 + 17 files changed, 2901 insertions(+), 2861 deletions(-) delete mode 100644 src/fs.rs create mode 100644 src/fs/bpb.rs create mode 100644 src/fs/consts.rs create mode 100644 src/fs/direntry.rs create mode 100644 src/fs/file.rs create mode 100644 src/fs/fs.rs create mode 100644 src/fs/mod.rs create mode 100644 src/fs/tests.rs create mode 100644 src/utils/bincode.rs create mode 100644 src/utils/string.rs diff --git a/Cargo.toml b/Cargo.toml index 6b37a1f..613312d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ time = { version = "0.3.36", default-features = false, features = [ "alloc", "pa [features] default = ["std"] -std = ["displaydoc/std", "serde/std", "time/std"] +std = [] [dev-dependencies] test-log = "0.2.16" diff --git a/src/error.rs b/src/error.rs index 7c54e30..a5d31b2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,7 +1,4 @@ -#[cfg(not(feature = "std"))] -use core::*; -#[cfg(feature = "std")] -use std::*; +use core::fmt; /// Base error type /// diff --git a/src/fs.rs b/src/fs.rs deleted file mode 100644 index 626e79c..0000000 --- a/src/fs.rs +++ /dev/null @@ -1,2843 +0,0 @@ -#[cfg(not(feature = "std"))] -use core::*; -#[cfg(feature = "std")] -use std::*; - -use ::alloc::{ - borrow::ToOwned, - format, - string::{FromUtf16Error, String, ToString}, - vec, - vec::*, -}; - -use bitfield_struct::bitfield; -use bitflags::bitflags; - -use bincode::Options as _; -use serde::{Deserialize, Serialize}; -use serde_big_array::BigArray; - -use ::time; -use time::{Date, PrimitiveDateTime, Time}; - -use crate::{error::*, io::prelude::*, path::PathBuf, utils}; - -/// The minimum size (in bytes) a sector is allowed to have -pub const SECTOR_SIZE_MIN: usize = 512; -/// The maximum size (in bytes) a sector is allowed to have -pub const SECTOR_SIZE_MAX: usize = 4096; - -/// Place this in the BPB _jmpboot field to hang if a computer attempts to boot this partition -/// The first two bytes jump to 0 on all bit modes and the third byte is just a NOP -const INFINITE_LOOP: [u8; 3] = [0xEB, 0xFE, 0x90]; - -const BPBFAT_SIZE: usize = 36; -#[derive(Serialize, Deserialize, Debug, Clone, Copy)] -struct BPBFAT { - _jmpboot: [u8; 3], - _oem_identifier: [u8; 8], - bytes_per_sector: u16, - sectors_per_cluster: u8, - reserved_sector_count: u16, - table_count: u8, - root_entry_count: u16, - // If this is 0, check `total_sectors_32` - total_sectors_16: u16, - _media_type: u8, - table_size_16: u16, - _sectors_per_track: u16, - _head_side_count: u16, - hidden_sector_count: u32, - total_sectors_32: u32, -} - -#[derive(Debug)] -enum BootRecord { - FAT(BootRecordFAT), - ExFAT(BootRecordExFAT), -} - -impl BootRecord { - #[inline] - /// The FAT type of this file system - pub(crate) fn fat_type(&self) -> FATType { - match self { - BootRecord::FAT(boot_record_fat) => { - let total_clusters = boot_record_fat.total_clusters(); - if total_clusters < 4085 { - FATType::FAT12 - } else if total_clusters < 65525 { - FATType::FAT16 - } else { - FATType::FAT32 - } - } - BootRecord::ExFAT(_boot_record_exfat) => { - todo!("ExFAT not yet implemented"); - FATType::ExFAT - } - } - } - - #[allow(non_snake_case)] - fn nth_FAT_table_sector(&self, n: u8) -> u32 { - match self { - BootRecord::FAT(boot_record_fat) => { - boot_record_fat.first_fat_sector() as u32 - + n as u32 * boot_record_fat.fat_sector_size() - } - BootRecord::ExFAT(boot_record_exfat) => { - // this should work, but ExFAT is not yet implemented, so... - todo!("ExFAT not yet implemented"); - boot_record_exfat.fat_count as u32 + n as u32 * boot_record_exfat.fat_len - } - } - } -} - -const BOOT_SIGNATURE: u8 = 0x29; -const FAT_SIGNATURE: u16 = 0x55AA; - -#[derive(Debug, Clone, Copy)] -struct BootRecordFAT { - bpb: BPBFAT, - ebr: EBR, -} - -impl BootRecordFAT { - #[inline] - fn verify_signature(&self) -> bool { - match self.fat_type() { - FATType::FAT12 | FATType::FAT16 | FATType::FAT32 => match self.ebr { - EBR::FAT12_16(ebr_fat12_16) => { - ebr_fat12_16.boot_signature == BOOT_SIGNATURE - && ebr_fat12_16.signature == FAT_SIGNATURE - } - EBR::FAT32(ebr_fat32, _) => { - ebr_fat32.boot_signature == BOOT_SIGNATURE - && ebr_fat32.signature == FAT_SIGNATURE - } - }, - FATType::ExFAT => todo!("ExFAT not yet implemented"), - } - } - - #[inline] - /// Total sectors in volume (including VBR)s - pub(crate) fn total_sectors(&self) -> u32 { - if self.bpb.total_sectors_16 == 0 { - self.bpb.total_sectors_32 - } else { - self.bpb.total_sectors_16 as u32 - } - } - - #[inline] - /// FAT size in sectors - pub(crate) fn fat_sector_size(&self) -> u32 { - match self.ebr { - EBR::FAT12_16(_ebr_fat12_16) => self.bpb.table_size_16.into(), - EBR::FAT32(ebr_fat32, _) => ebr_fat32.table_size_32, - } - } - - #[inline] - /// The size of the root directory (unless we have FAT32, in which case the size will be 0) - /// This calculation will round up - pub(crate) fn root_dir_sectors(&self) -> u16 { - ((self.bpb.root_entry_count * DIRENTRY_SIZE as u16) + (self.bpb.bytes_per_sector - 1)) - / self.bpb.bytes_per_sector - } - - #[inline] - /// The first sector in the File Allocation Table - pub(crate) fn first_fat_sector(&self) -> u16 { - self.bpb.reserved_sector_count - } - - #[inline] - /// The first sector of the root directory (returns the first data sector on FAT32) - pub(crate) fn first_root_dir_sector(&self) -> u16 { - self.first_fat_sector() + self.bpb.table_count as u16 * self.fat_sector_size() as u16 - } - - #[inline] - /// The first data sector (that is, the first sector in which directories and files may be stored) - pub(crate) fn first_data_sector(&self) -> u16 { - self.first_root_dir_sector() + self.root_dir_sectors() - } - - #[inline] - /// The total number of data sectors - pub(crate) fn total_data_sectors(&self) -> u32 { - self.total_sectors() - (self.bpb.table_count as u32 * self.fat_sector_size()) - + self.root_dir_sectors() as u32 - } - - #[inline] - /// The total number of clusters - pub(crate) fn total_clusters(&self) -> u32 { - self.total_data_sectors() / self.bpb.sectors_per_cluster as u32 - } - - #[inline] - /// The FAT type of this file system - pub(crate) fn fat_type(&self) -> FATType { - if self.bpb.bytes_per_sector == 0 { - todo!("ExFAT not yet implemented"); - FATType::ExFAT - } else { - let total_clusters = self.total_clusters(); - if total_clusters < 4085 { - FATType::FAT12 - } else if total_clusters < 65525 { - FATType::FAT16 - } else { - FATType::FAT32 - } - } - } -} - -#[derive(Debug, Clone, Copy)] -// Everything here is naturally aligned (thank god) -struct BootRecordExFAT { - _dummy_jmp: [u8; 3], - _oem_identifier: [u8; 8], - _zeroed: [u8; 53], - _partition_offset: u64, - volume_len: u64, - fat_offset: u32, - fat_len: u32, - cluster_heap_offset: u32, - cluster_count: u32, - root_dir_cluster: u32, - partition_serial_num: u32, - fs_revision: u16, - flags: u16, - sector_shift: u8, - cluster_shift: u8, - fat_count: u8, - drive_select: u8, - used_percentage: u8, - _reserved: [u8; 7], -} - -const EBR_SIZE: usize = 512 - BPBFAT_SIZE; -#[derive(Clone, Copy)] -enum EBR { - FAT12_16(EBRFAT12_16), - FAT32(EBRFAT32, FSInfoFAT32), -} - -impl fmt::Debug for EBR { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // TODO: find a good way of printing this - write!(f, "FAT12-16/32 Extended boot record...") - } -} - -#[derive(Deserialize, Serialize, Clone, Copy)] -struct EBRFAT12_16 { - _drive_num: u8, - _windows_nt_flags: u8, - boot_signature: u8, - volume_serial_num: u32, - volume_label: [u8; 11], - _system_identifier: [u8; 8], - #[serde(with = "BigArray")] - _boot_code: [u8; 448], - signature: u16, -} - -// FIXME: these might be the other way around -#[derive(Deserialize, Serialize, Debug, Clone, Copy)] -struct FATVersion { - minor: u8, - major: u8, -} - -#[derive(Deserialize, Serialize, Clone, Copy)] -struct EBRFAT32 { - table_size_32: u32, - _extended_flags: u16, - fat_version: FATVersion, - root_cluster: u32, - fat_info: u16, - backup_boot_sector: u16, - _reserved: [u8; 12], - _drive_num: u8, - _windows_nt_flags: u8, - boot_signature: u8, - volume_serial_num: u32, - volume_label: [u8; 11], - _system_ident: [u8; 8], - #[serde(with = "BigArray")] - _boot_code: [u8; 420], - signature: u16, -} - -const FSINFO_LEAD_SIGNATURE: u32 = 0x41615252; -const FSINFO_MID_SIGNATURE: u32 = 0x61417272; -const FSINFO_TRAIL_SIGNAUTE: u32 = 0xAA550000; -#[derive(Serialize, Deserialize, Debug, Clone, Copy)] -struct FSInfoFAT32 { - lead_signature: u32, - #[serde(with = "BigArray")] - _reserved1: [u8; 480], - mid_signature: u32, - free_cluster_count: u32, - first_free_cluster: u32, - _reserved2: [u8; 12], - trail_signature: u32, -} - -impl FSInfoFAT32 { - fn verify_signature(&self) -> bool { - self.lead_signature == FSINFO_LEAD_SIGNATURE - && self.mid_signature == FSINFO_MID_SIGNATURE - && self.trail_signature == FSINFO_TRAIL_SIGNAUTE - } -} - -/// An enum representing different versions of the FAT filesystem -#[derive(Debug, Clone, Copy, PartialEq)] -// no need for enum variant documentation here -#[allow(missing_docs)] -pub enum FATType { - FAT12, - FAT16, - FAT32, - ExFAT, -} - -impl FATType { - #[inline] - /// How many bits this [`FATType`] uses to address clusters in the disk - pub fn bits_per_entry(&self) -> u8 { - match self { - FATType::FAT12 => 12, - FATType::FAT16 => 16, - // the high 4 bits are ignored, but are still part of the entry - FATType::FAT32 => 32, - FATType::ExFAT => 32, - } - } - - #[inline] - /// How many bytes this [`FATType`] spans across - fn entry_size(&self) -> u32 { - self.bits_per_entry().next_power_of_two() as u32 / 8 - } -} - -// the first 2 entries are reserved -const RESERVED_FAT_ENTRIES: u32 = 2; - -#[derive(Debug, Clone, PartialEq)] -enum FATEntry { - /// This cluster is free - Free, - /// This cluster is allocated and the next cluster is the contained value - Allocated(u32), - /// This cluster is reserved - Reserved, - /// This is a bad (defective) cluster - Bad, - /// This cluster is allocated and is the final cluster of the file - EOF, -} - -impl From for u32 { - fn from(value: FATEntry) -> Self { - Self::from(&value) - } -} - -impl From<&FATEntry> for u32 { - fn from(value: &FATEntry) -> Self { - match value { - FATEntry::Free => u32::MIN, - FATEntry::Allocated(cluster) => *cluster, - FATEntry::Reserved => 0xFFFFFF6, - FATEntry::Bad => 0xFFFFFF7, - FATEntry::EOF => u32::MAX, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy)] -struct SFN { - name: [u8; 8], - ext: [u8; 3], -} - -impl SFN { - fn get_byte_slice(&self) -> [u8; 11] { - let mut slice = [0; 11]; - - slice[..8].copy_from_slice(&self.name); - slice[8..].copy_from_slice(&self.ext); - - slice - } - - fn gen_checksum(&self) -> u8 { - let mut sum = 0; - - for c in self.get_byte_slice() { - sum = (if (sum & 1) != 0 { 0x80_u8 } else { 0_u8 }) - .wrapping_add(sum >> 1) - .wrapping_add(c) - } - - log::debug!("SFN checksum: {:X}", sum); - - sum - } -} - -impl fmt::Display for SFN { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // we begin by writing the name (even if it is padded with spaces, they will be trimmed, so we don't care) - write!(f, "{}", String::from_utf8_lossy(&self.name).trim())?; - - // then, if the extension isn't empty (padded with zeroes), we write it too - let ext = String::from_utf8_lossy(&self.ext).trim().to_owned(); - if !ext.is_empty() { - write!(f, ".{}", ext)?; - }; - - Ok(()) - } -} - -bitflags! { - /// A list of the various (raw) attributes specified for a file/directory - /// - /// To check whether a given [`Attributes`] struct contains a flag, use the [`contains()`](Attributes::contains()) method - /// - /// Generated using [bitflags](https://docs.rs/bitflags/2.6.0/bitflags/) - #[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq)] - struct RawAttributes: u8 { - /// This entry is read-only - const READ_ONLY = 0x01; - /// This entry is normally hidden - const HIDDEN = 0x02; - /// This entry is a system file - const SYSTEM = 0x04; - /// This entry represents the volume's ID. - /// This is used internally and the library will never return such an entry - const VOLUME_ID = 0x08; - /// This entry is a directory. You should normally use a [`PathBuf`]s [`is_dir()`](PathBuf::is_dir) method instead - const DIRECTORY = 0x10; - /// This entry is marked to be archived. Used by archiving software for backing up files and directories - const ARCHIVE = 0x20; - - /// This entry is part of a LFN (long filename). Used internally - const LFN = Self::READ_ONLY.bits() | - Self::HIDDEN.bits() | - Self::SYSTEM.bits() | - Self::VOLUME_ID.bits(); - } -} - -/// A list of the various attributes specified for a file/directory -#[derive(Debug, Clone, Copy)] -pub struct Attributes { - /// This is a read-only file - pub read_only: bool, - /// This file is to be hidden unless a request is issued - /// explicitly requesting inclusion of “hidden files” - pub hidden: bool, - /// This is a system file and shouldn't be listed unless a request - /// is issued explicitly requesting inclusion of system files” - pub system: bool, - /// This file has been modified since last archival - /// or has never been archived. - /// - /// This field should only concern archival software - pub archive: bool, -} - -impl From for Attributes { - fn from(value: RawAttributes) -> Self { - Attributes { - read_only: value.contains(RawAttributes::READ_ONLY), - hidden: value.contains(RawAttributes::HIDDEN), - system: value.contains(RawAttributes::SYSTEM), - archive: value.contains(RawAttributes::ARCHIVE), - } - } -} - -const START_YEAR: i32 = 1980; - -#[bitfield(u16)] -#[derive(Serialize, Deserialize)] -struct TimeAttribute { - /// Multiply by 2 - #[bits(5)] - seconds: u8, - #[bits(6)] - minutes: u8, - #[bits(5)] - hour: u8, -} - -#[bitfield(u16)] -#[derive(Serialize, Deserialize)] -struct DateAttribute { - #[bits(5)] - day: u8, - #[bits(4)] - month: u8, - #[bits(7)] - year: u8, -} - -impl TryFrom for Time { - type Error = (); - - fn try_from(value: TimeAttribute) -> Result { - time::parsing::Parsed::new() - .with_hour_24(value.hour()) - .and_then(|parsed| parsed.with_minute(value.minutes())) - .and_then(|parsed| parsed.with_second(value.seconds() * 2)) - .map(|parsed| parsed.try_into().ok()) - .flatten() - .ok_or(()) - } -} - -impl TryFrom for Date { - type Error = (); - - fn try_from(value: DateAttribute) -> Result { - time::parsing::Parsed::new() - .with_year(i32::from(value.year()) + START_YEAR) - .and_then(|parsed| parsed.with_month(value.month().try_into().ok()?)) - .and_then(|parsed| parsed.with_day(num::NonZeroU8::new(value.day())?)) - .map(|parsed| parsed.try_into().ok()) - .flatten() - .ok_or(()) - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy)] -struct EntryCreationTime { - hundredths_of_second: u8, - time: TimeAttribute, - date: DateAttribute, -} - -impl TryFrom for PrimitiveDateTime { - type Error = (); - - fn try_from(value: EntryCreationTime) -> Result { - let mut time: Time = value.time.try_into()?; - - let new_seconds = time.second() + value.hundredths_of_second / 100; - let milliseconds = u16::from(value.hundredths_of_second) % 100 * 10; - time = time - .replace_second(new_seconds) - .map_err(|_| ())? - .replace_millisecond(milliseconds) - .map_err(|_| ())?; - - let date: Date = value.date.try_into()?; - - Ok(PrimitiveDateTime::new(date, time)) - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy)] -struct EntryModificationTime { - time: TimeAttribute, - date: DateAttribute, -} - -impl TryFrom for PrimitiveDateTime { - type Error = (); - - fn try_from(value: EntryModificationTime) -> Result { - Ok(PrimitiveDateTime::new( - value.date.try_into()?, - value.time.try_into()?, - )) - } -} - -// a directory entry occupies 32 bytes -const DIRENTRY_SIZE: usize = 32; - -#[derive(Serialize, Deserialize, Debug, Clone, Copy)] -struct FATDirEntry { - sfn: SFN, - attributes: RawAttributes, - _reserved: [u8; 1], - created: EntryCreationTime, - accessed: DateAttribute, - cluster_high: u16, - modified: EntryModificationTime, - cluster_low: u16, - file_size: u32, -} - -#[derive(Debug, Deserialize, Serialize)] -struct LFNEntry { - /// masked with 0x40 if this is the last entry - order: u8, - first_chars: [u8; 10], - /// Always equals 0x0F - _lfn_attribute: u8, - /// Both OSDev and the FAT specification say this is always 0 - _long_entry_type: u8, - /// If this doesn't match with the computed cksum, then the set of LFNs is considered corrupt - /// - /// A [`LFNEntry`] will be marked as corrupt even if it isn't, if the SFN is modifed by a legacy system, - /// since the new SFN's signature and the one on this field won't (probably) match - checksum: u8, - mid_chars: [u8; 12], - _zeroed: [u8; 2], - last_chars: [u8; 4], -} - -impl LFNEntry { - fn get_byte_slice(&self) -> [u16; 13] { - let mut slice = [0_u8; 13 * mem::size_of::()]; - - slice[..10].copy_from_slice(&self.first_chars); - slice[10..22].copy_from_slice(&self.mid_chars); - slice[22..].copy_from_slice(&self.last_chars); - - let mut out_slice = [0_u16; 13]; - for (i, chunk) in slice.chunks(mem::size_of::()).enumerate() { - out_slice[i] = u16::from_le_bytes(chunk.try_into().unwrap()); - } - - out_slice - } - - #[inline] - fn verify_signature(&self) -> bool { - self._long_entry_type == 0 && self._zeroed.iter().all(|v| *v == 0) - } -} - -/// The location of a [`FATDirEntry`] within a root directory sector -/// or a data region cluster -#[derive(Debug, Clone)] -enum EntryLocation { - /// Sector offset from the start of the root directory region (FAT12/16) - RootDirSector(u16), - /// Cluster offset from the start of the data region - DataCluster(u32), -} - -impl EntryLocation { - fn from_partition_sector(sector: u32, fs: &mut FileSystem) -> Self - where - S: Read + Write + Seek, - { - if sector < fs.first_data_sector() { - EntryLocation::RootDirSector((sector - fs.props.first_root_dir_sector as u32) as u16) - } else { - EntryLocation::DataCluster(fs.partition_sector_to_data_cluster(sector)) - } - } -} - -/// The location of a chain of [`FATDirEntry`] -#[derive(Debug)] -struct DirEntryChain { - /// the location of the first corresponding entry - location: EntryLocation, - /// the first entry's index/offset from the start of the sector - index: u32, - /// how many (contiguous) entries this entry chain has - len: u32, -} - -/// A resolved file/directory entry (for internal usage only) -#[derive(Debug)] -struct RawProperties { - name: String, - is_dir: bool, - attributes: RawAttributes, - created: PrimitiveDateTime, - modified: PrimitiveDateTime, - accessed: Date, - file_size: u32, - data_cluster: u32, - - chain_props: DirEntryChain, -} - -/// A container for file/directory properties -#[derive(Debug)] -pub struct Properties { - path: PathBuf, - attributes: Attributes, - created: PrimitiveDateTime, - modified: PrimitiveDateTime, - accessed: Date, - file_size: u32, - data_cluster: u32, - - // internal fields - chain_props: DirEntryChain, -} - -/// Getter methods -impl Properties { - #[inline] - /// Get the corresponding [`PathBuf`] to this entry - pub fn path(&self) -> &PathBuf { - &self.path - } - - #[inline] - /// Get the corresponding [`Attributes`] to this entry - pub fn attributes(&self) -> &Attributes { - &self.attributes - } - - #[inline] - /// Find out when this entry was created (max resolution: 1ms) - /// - /// Returns a [`PrimitiveDateTime`] from the [`time`] crate - pub fn creation_time(&self) -> &PrimitiveDateTime { - &self.created - } - - #[inline] - /// Find out when this entry was last modified (max resolution: 2 secs) - /// - /// Returns a [`PrimitiveDateTime`] from the [`time`] crate - pub fn modification_time(&self) -> &PrimitiveDateTime { - &self.modified - } - - #[inline] - /// Find out when this entry was last accessed (max resolution: 1 day) - /// - /// Returns a [`Date`] from the [`time`] crate - pub fn last_accessed_date(&self) -> &Date { - &self.accessed - } - - #[inline] - /// Find out the size of this entry - /// - /// Always returns `0` for directories - pub fn file_size(&self) -> u32 { - self.file_size - } -} - -/// Serialization methods -impl Properties { - #[inline] - fn from_raw(raw: RawProperties, path: PathBuf) -> Self { - Properties { - path, - attributes: raw.attributes.into(), - created: raw.created, - modified: raw.modified, - accessed: raw.accessed, - file_size: raw.file_size, - data_cluster: raw.data_cluster, - chain_props: raw.chain_props, - } - } -} - -/// A thin wrapper for [`Properties`] represing a directory entry -#[derive(Debug)] -pub struct DirEntry { - entry: Properties, -} - -impl ops::Deref for DirEntry { - type Target = Properties; - - #[inline] - fn deref(&self) -> &Self::Target { - &self.entry - } -} - -#[derive(Debug)] -struct FileProps { - entry: Properties, - /// the byte offset of the R/W pointer - offset: u64, - current_cluster: u32, -} - -/// A read-only file within a FAT filesystem -#[derive(Debug)] -pub struct ROFile<'a, S> -where - S: Read + Write + Seek, -{ - fs: &'a mut FileSystem, - props: FileProps, -} - -impl ops::Deref for ROFile<'_, S> -where - S: Read + Write + Seek, -{ - type Target = Properties; - - fn deref(&self) -> &Self::Target { - &self.props.entry - } -} - -impl ops::DerefMut for ROFile<'_, S> -where - S: Read + Write + Seek, -{ - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.props.entry - } -} - -impl IOBase for ROFile<'_, S> -where - S: Read + Write + Seek, -{ - type Error = S::Error; -} - -/// A read-write file within a FAT filesystem -/// -/// The size of the file will be automatically adjusted -/// if the cursor goes beyond EOF. -/// -/// To reduce a file's size, use the [`truncate`](RWFile::truncate) method -#[derive(Debug)] -pub struct RWFile<'a, S> -where - S: Read + Write + Seek, -{ - ro_file: ROFile<'a, S>, -} - -impl<'a, S> ops::Deref for RWFile<'a, S> -where - S: Read + Write + Seek, -{ - type Target = ROFile<'a, S>; - - fn deref(&self) -> &Self::Target { - &self.ro_file - } -} - -impl ops::DerefMut for RWFile<'_, S> -where - S: Read + Write + Seek, -{ - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.ro_file - } -} - -impl IOBase for RWFile<'_, S> -where - S: Read + Write + Seek, -{ - type Error = S::Error; -} - -// Public functions -impl RWFile<'_, S> -where - S: Read + Write + Seek, -{ - /// Truncates the file to a given size, deleting everything past the new EOF - /// - /// If `size` is greater or equal to the current file size - /// till the end of the last cluster allocated, this has no effect - /// - /// Furthermore, if the cursor point is beyond the new EOF, it will be moved there - pub fn truncate(&mut self, size: u32) -> Result<(), ::Error> { - // looks like the new truncated size would be smaller than the current one, so we just return - if size.next_multiple_of(self.fs.props.cluster_size as u32) >= self.file_size { - if size < self.file_size { - self.file_size = size; - } - - return Ok(()); - } - - // we store the current offset for later use - let previous_offset = cmp::min(self.props.offset, size.into()); - - // we seek back to where the EOF will be - self.seek(SeekFrom::Start(size.into()))?; - - // set what the new filesize will be - let previous_size = self.file_size; - self.file_size = size; - - let mut next_cluster_option = self.get_next_cluster()?; - - // we set the new last cluster in the chain to be EOF - self.ro_file - .fs - .write_nth_FAT_entry(self.ro_file.props.current_cluster, FATEntry::EOF)?; - - // then, we set each cluster after the current one to EOF - while let Some(next_cluster) = next_cluster_option { - next_cluster_option = self.fs.get_next_cluster(next_cluster)?; - - self.fs.write_nth_FAT_entry(next_cluster, FATEntry::Free)?; - } - - // don't forget to seek back to where we started - self.seek(SeekFrom::Start(previous_offset))?; - - log::debug!( - "Successfully truncated file {} from {} to {} bytes", - self.path, - previous_size, - self.file_size - ); - - Ok(()) - } - - /// Remove the current file from the [`FileSystem`] - pub fn remove(mut self) -> Result<(), ::Error> { - // we begin by removing the corresponding entries... - let mut entries_freed = 0; - let mut current_offset = self.props.entry.chain_props.index; - - // current_cluster_option is `None` if we are dealing with a root directory entry - let (mut current_sector, current_cluster_option): (u32, Option) = - match self.props.entry.chain_props.location { - EntryLocation::RootDirSector(root_dir_sector) => ( - (root_dir_sector + self.fs.props.first_root_dir_sector).into(), - None, - ), - EntryLocation::DataCluster(data_cluster) => ( - self.fs.data_cluster_to_partition_sector(data_cluster), - Some(data_cluster), - ), - }; - - while entries_freed < self.props.entry.chain_props.len { - if current_sector as u64 != self.fs.stored_sector { - self.fs.read_nth_sector(current_sector.into())?; - } - - // we won't even bother zeroing the entire thing, just the first byte - let byte_offset = current_offset as usize * DIRENTRY_SIZE; - self.fs.sector_buffer[byte_offset] = UNUSED_ENTRY; - self.fs.buffer_modified = true; - - log::trace!( - "freed entry at sector {} with byte offset {}", - current_sector, - byte_offset - ); - - if current_offset + 1 >= (self.fs.sector_size() / DIRENTRY_SIZE as u32) { - // we have moved to a new sector - current_sector += 1; - - match current_cluster_option { - // data region - Some(mut current_cluster) => { - if self.fs.partition_sector_to_data_cluster(current_sector) - != current_cluster - { - current_cluster = self.fs.get_next_cluster(current_cluster)?.unwrap(); - current_sector = - self.fs.data_cluster_to_partition_sector(current_cluster); - } - } - None => (), - } - - current_offset = 0; - } else { - current_offset += 1 - } - - entries_freed += 1; - } - - // ... and then we free the data clusters - - // rewind back to the start of the file - self.rewind()?; - - loop { - let current_cluster = self.props.current_cluster; - let next_cluster_option = self.get_next_cluster()?; - - // free the current cluster - self.fs - .write_nth_FAT_entry(current_cluster, FATEntry::Free)?; - - // proceed to the next one, otherwise break - match next_cluster_option { - Some(next_cluster) => self.props.current_cluster = next_cluster, - None => break, - } - } - - Ok(()) - } -} - -// Internal functions -impl ROFile<'_, S> -where - S: Read + Write + Seek, -{ - #[inline] - /// Panics if the current cluser doesn't point to another clluster - fn next_cluster(&mut self) -> Result<(), ::Error> { - // when a `ROFile` is created, `cluster_chain_is_healthy` is called, if it fails, that ROFile is dropped - self.props.current_cluster = self.get_next_cluster()?.unwrap(); - - Ok(()) - } - - #[inline] - /// Non-[`panic`]king version of [`next_cluster()`](ROFile::next_cluster) - fn get_next_cluster(&mut self) -> Result, ::Error> { - Ok(self.fs.get_next_cluster(self.props.current_cluster)?) - } - - /// Returns that last cluster in the file's cluster chain - fn last_cluster_in_chain(&mut self) -> Result::Error> { - // we begin from the current cluster to save some time - let mut current_cluster = self.props.current_cluster; - - loop { - match self.fs.read_nth_FAT_entry(current_cluster)? { - FATEntry::Allocated(next_cluster) => current_cluster = next_cluster, - FATEntry::EOF => break, - _ => unreachable!(), - } - } - - Ok(current_cluster) - } - - /// Checks whether the cluster chain of this file is healthy or malformed - fn cluster_chain_is_healthy(&mut self) -> Result { - let mut current_cluster = self.data_cluster; - let mut cluster_count = 0; - - loop { - cluster_count += 1; - - if cluster_count * self.fs.cluster_size() >= self.file_size.into() { - break; - } - - match self.fs.read_nth_FAT_entry(current_cluster)? { - FATEntry::Allocated(next_cluster) => current_cluster = next_cluster, - _ => return Ok(false), - }; - } - - Ok(true) - } - - fn offset_from_seekfrom(&self, seekfrom: SeekFrom) -> u64 { - match seekfrom { - SeekFrom::Start(offset) => offset, - SeekFrom::Current(offset) => { - let offset = self.props.offset as i64 + offset; - offset.try_into().unwrap_or(u64::MIN) - } - SeekFrom::End(offset) => { - let offset = self.file_size as i64 + offset; - offset.try_into().unwrap_or(u64::MIN) - } - } - } -} - -impl Read for ROFile<'_, S> -where - S: Read + Write + Seek, -{ - fn read(&mut self, buf: &mut [u8]) -> Result { - let mut bytes_read = 0; - // this is the maximum amount of bytes that can be read - let read_cap = cmp::min( - buf.len(), - self.file_size as usize - self.props.offset as usize, - ); - - 'outer: loop { - let sector_init_offset = u32::try_from(self.props.offset % self.fs.cluster_size()) - .unwrap() - / self.fs.sector_size(); - let first_sector_of_cluster = self - .fs - .data_cluster_to_partition_sector(self.props.current_cluster) - + sector_init_offset; - let last_sector_of_cluster = first_sector_of_cluster - + self.fs.sectors_per_cluster() as u32 - - sector_init_offset - - 1; - log::debug!( - "Reading cluster {} from sectors {} to {}", - self.props.current_cluster, - first_sector_of_cluster, - last_sector_of_cluster - ); - - for sector in first_sector_of_cluster..=last_sector_of_cluster { - self.fs.read_nth_sector(sector.into())?; - - let start_index = self.props.offset as usize % self.fs.sector_size() as usize; - let bytes_to_read = cmp::min( - read_cap - bytes_read, - self.fs.sector_size() as usize - start_index, - ); - log::debug!( - "Gonna read {} bytes from sector {} starting at byte {}", - bytes_to_read, - sector, - start_index - ); - - buf[bytes_read..bytes_read + bytes_to_read].copy_from_slice( - &self.fs.sector_buffer[start_index..start_index + bytes_to_read], - ); - - bytes_read += bytes_to_read; - self.props.offset += bytes_to_read as u64; - - // if we have read as many bytes as we want... - if bytes_read >= read_cap { - // ...but we must process get the next cluster for future uses, - // we do that before breaking - if self.props.offset % self.fs.cluster_size() == 0 - && self.props.offset < self.file_size.into() - { - self.next_cluster()?; - } - - break 'outer; - } - } - - self.next_cluster()?; - } - - Ok(bytes_read) - } - - // the default `read_to_end` implementation isn't efficient enough, so we just do this - fn read_to_end(&mut self, buf: &mut Vec) -> Result { - let bytes_to_read = self.file_size as usize - self.props.offset as usize; - let init_buf_len = buf.len(); - - // resize buffer to fit the file contents exactly - buf.resize(init_buf_len + bytes_to_read, 0); - - // this is guaranteed not to raise an EOF (although other error kinds might be raised...) - self.read_exact(&mut buf[init_buf_len..])?; - - Ok(bytes_to_read) - } -} -impl Read for RWFile<'_, S> -where - S: Read + Write + Seek, -{ - #[inline] - fn read(&mut self, buf: &mut [u8]) -> Result { - self.ro_file.read(buf) - } - - #[inline] - fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Self::Error> { - self.ro_file.read_exact(buf) - } - - #[inline] - fn read_to_end(&mut self, buf: &mut Vec) -> Result { - self.ro_file.read_to_end(buf) - } - - #[inline] - fn read_to_string(&mut self, string: &mut String) -> Result { - self.ro_file.read_to_string(string) - } -} - -impl Write for RWFile<'_, S> -where - S: Read + Write + Seek, -{ - fn write(&mut self, buf: &[u8]) -> Result { - // allocate clusters - self.seek(SeekFrom::Current(buf.len() as i64))?; - // rewind back to where we were - self.seek(SeekFrom::Current(-(buf.len() as i64)))?; - - let mut bytes_written = 0; - - 'outer: loop { - log::trace!( - "writing file data to cluster: {}", - self.props.current_cluster - ); - - let sector_init_offset = u32::try_from(self.props.offset % self.fs.cluster_size()) - .unwrap() - / self.fs.sector_size(); - let first_sector_of_cluster = self - .fs - .data_cluster_to_partition_sector(self.props.current_cluster) - + sector_init_offset; - let last_sector_of_cluster = first_sector_of_cluster - + self.fs.sectors_per_cluster() as u32 - - sector_init_offset - - 1; - for sector in first_sector_of_cluster..=last_sector_of_cluster { - self.fs.read_nth_sector(sector.into())?; - - let start_index = self.props.offset as usize % self.fs.sector_size() as usize; - - let bytes_to_write = cmp::min( - buf.len() - bytes_written, - self.fs.sector_size() as usize - start_index, - ); - - self.fs.sector_buffer[start_index..start_index + bytes_to_write] - .copy_from_slice(&buf[bytes_written..bytes_written + bytes_to_write]); - self.fs.buffer_modified = true; - - bytes_written += bytes_to_write; - self.props.offset += bytes_to_write as u64; - - // if we have written as many bytes as we want... - if bytes_written >= buf.len() { - // ...but we must process get the next cluster for future uses, - // we do that before breaking - if self.props.offset % self.fs.cluster_size() == 0 { - self.next_cluster()?; - } - - break 'outer; - } - } - - self.next_cluster()?; - } - - Ok(bytes_written) - } - - // everything is immediately written to the storage medium - fn flush(&mut self) -> Result<(), Self::Error> { - Ok(()) - } -} - -impl Seek for ROFile<'_, S> -where - S: Read + Write + Seek, -{ - fn seek(&mut self, pos: SeekFrom) -> Result { - let offset = self.offset_from_seekfrom(pos); - - // in case the cursor goes beyond the EOF, allocate more clusters - if offset > (self.file_size as u64).next_multiple_of(self.fs.cluster_size()) { - return Err(IOError::new( - ::Kind::new_unexpected_eof(), - "moved past eof in a RO file", - )); - } - - log::trace!( - "Previous cursor offset is {}, new cursor offset is {}", - self.props.offset, - offset - ); - - use cmp::Ordering; - match offset.cmp(&self.props.offset) { - Ordering::Less => { - // here, we basically "rewind" back to the start of the file and then seek to where we want - // this of course has performance issues, so TODO: find a solution that is both memory & time efficient - // (perhaps we could follow a similar approach to elm-chan's FATFS, by using a cluster link map table, perhaps as an optional feature) - self.props.offset = 0; - self.props.current_cluster = self.data_cluster; - self.seek(SeekFrom::Start(offset))?; - } - Ordering::Equal => (), - Ordering::Greater => { - for _ in self.props.offset / self.fs.cluster_size()..offset / self.fs.cluster_size() - { - self.next_cluster()?; - } - self.props.offset = offset; - } - } - - Ok(self.props.offset) - } -} - -impl Seek for RWFile<'_, S> -where - S: Read + Write + Seek, -{ - fn seek(&mut self, pos: SeekFrom) -> Result { - let offset = self.offset_from_seekfrom(pos); - - // in case the cursor goes beyond the EOF, allocate more clusters - if offset > (self.file_size as u64).next_multiple_of(self.fs.cluster_size()) { - let clusters_to_allocate = (offset - - (self.file_size as u64).next_multiple_of(self.fs.cluster_size())) - .div_ceil(self.fs.cluster_size()) - + 1; - log::debug!( - "Seeking beyond EOF, allocating {} more clusters", - clusters_to_allocate - ); - - let mut last_cluster_in_chain = self.last_cluster_in_chain()?; - - for clusters_allocated in 0..clusters_to_allocate { - match self.fs.next_free_cluster()? { - Some(next_free_cluster) => { - // we set the last allocated cluster to point to the next free one - self.fs.write_nth_FAT_entry( - last_cluster_in_chain, - FATEntry::Allocated(next_free_cluster), - )?; - // we also set the next free cluster to be EOF - self.fs - .write_nth_FAT_entry(next_free_cluster, FATEntry::EOF)?; - log::trace!( - "cluster {} now points to {}", - last_cluster_in_chain, - next_free_cluster - ); - // now the next free cluster i the last allocated one - last_cluster_in_chain = next_free_cluster; - } - None => { - self.file_size = (((self.file_size as u64) - .next_multiple_of(self.fs.cluster_size()) - - offset) - + clusters_allocated * self.fs.cluster_size()) - as u32; - self.props.offset = self.file_size.into(); - - log::error!("storage medium full while attempting to allocate more clusters for a ROFile"); - return Err(IOError::new( - ::Kind::new_unexpected_eof(), - "the storage medium is full, can't increase size of file", - )); - } - } - } - - self.file_size = offset as u32; - log::debug!( - "New file size after reallocation is {} bytes", - self.file_size - ); - } - - self.ro_file.seek(pos) - } -} - -/// variation of https://stackoverflow.com/a/42067321/19247098 for processing LFNs -pub(crate) fn string_from_lfn(utf16_src: &[u16]) -> Result { - let nul_range_end = utf16_src - .iter() - .position(|c| *c == 0x0000) - .unwrap_or(utf16_src.len()); // default to length if no `\0` present - - String::from_utf16(&utf16_src[0..nul_range_end]) -} - -trait OffsetConversions { - fn sector_size(&self) -> u32; - fn cluster_size(&self) -> u64; - fn first_data_sector(&self) -> u32; - - #[inline] - fn cluster_to_sector(&self, cluster: u64) -> u32 { - (cluster * self.cluster_size() / self.sector_size() as u64) - .try_into() - .unwrap() - } - - #[inline] - fn sectors_per_cluster(&self) -> u64 { - self.cluster_size() / self.sector_size() as u64 - } - - #[inline] - fn sector_to_partition_offset(&self, sector: u32) -> u32 { - sector * self.sector_size() - } - - #[inline] - fn data_cluster_to_partition_sector(&self, cluster: u32) -> u32 { - self.cluster_to_sector((cluster - RESERVED_FAT_ENTRIES).into()) + self.first_data_sector() - } - - #[inline] - fn partition_sector_to_data_cluster(&self, sector: u32) -> u32 { - (sector - self.first_data_sector()) / self.sectors_per_cluster() as u32 - + RESERVED_FAT_ENTRIES - } -} - -/// Some generic properties common across all FAT versions, like a sector's size, are cached here -#[derive(Debug)] -struct FSProperties { - sector_size: u32, - cluster_size: u64, - total_sectors: u32, - total_clusters: u32, - /// sector offset of the FAT - fat_table_count: u8, - first_root_dir_sector: u16, - first_data_sector: u32, -} - -#[inline] -// an easy way to universally use the same bincode (de)serialization options -fn bincode_config() -> impl bincode::Options + Copy { - // also check https://docs.rs/bincode/1.3.3/bincode/config/index.html#options-struct-vs-bincode-functions - bincode::DefaultOptions::new() - .with_fixint_encoding() - .allow_trailing_bytes() - .with_little_endian() -} - -/// Filter (or not) things like hidden files/directories -/// for FileSystem operations -#[derive(Debug)] -struct FileFilter { - show_hidden: bool, - show_systen: bool, -} - -impl FileFilter { - fn filter(&self, item: &RawProperties) -> bool { - let is_hidden = item.attributes.contains(RawAttributes::HIDDEN); - let is_system = item.attributes.contains(RawAttributes::SYSTEM); - let should_filter = !self.show_hidden && is_hidden || !self.show_systen && is_system; - - !should_filter - } -} - -impl Default for FileFilter { - fn default() -> Self { - // The FAT spec says to filter everything by default - FileFilter { - show_hidden: false, - show_systen: false, - } - } -} - -/// An API to process a FAT filesystem -#[derive(Debug)] -pub struct FileSystem -where - S: Read + Write + Seek, -{ - /// Any struct that implements the [`Read`], [`Write`] & [`Seek`] traits - storage: S, - - /// The length of this will be the sector size of the FS for all FAT types except FAT12, in that case, it will be double that value - sector_buffer: Vec, - /// ANY CHANGES TO THE SECTOR BUFFER SHOULD ALSO SET THIS TO TRUE - buffer_modified: bool, - stored_sector: u64, - - boot_record: BootRecord, - // since `self.fat_type()` calls like 5 nested functions, we keep this cached and expose it as a public field - fat_type: FATType, - props: FSProperties, - - filter: FileFilter, -} - -impl OffsetConversions for FileSystem -where - S: Read + Write + Seek, -{ - #[inline] - fn sector_size(&self) -> u32 { - self.props.sector_size - } - - #[inline] - fn cluster_size(&self) -> u64 { - self.props.cluster_size - } - - #[inline] - fn first_data_sector(&self) -> u32 { - self.props.first_data_sector - } -} - -/// Getter functions -impl FileSystem -where - S: Read + Write + Seek, -{ - /// What is the [`FATType`] of the filesystem - pub fn fat_type(&self) -> FATType { - self.fat_type - } -} - -/// Setter functions -impl FileSystem -where - S: Read + Write + Seek, -{ - /// Whether or not to list hidden files - /// - /// Off by default - #[inline] - pub fn show_hidden(&mut self, show: bool) { - self.filter.show_hidden = show; - } - - /// Whether or not to list system files - /// - /// Off by default - #[inline] - pub fn show_system(&mut self, show: bool) { - self.filter.show_systen = show; - } -} - -/// Constructors -impl FileSystem -where - S: Read + Write + Seek, -{ - /// Create a [`FileSystem`] from a storage object that implements [`Read`], [`Write`] & [`Seek`] - /// - /// Fails if the storage is way too small to support a FAT filesystem. - /// For most use cases, that shouldn't be an issue, you can just call [`.unwrap()`](Result::unwrap) - pub fn from_storage(mut storage: S) -> FSResult { - // Begin by reading the boot record - // We don't know the sector size yet, so we just go with the biggest possible one for now - let mut buffer = [0u8; SECTOR_SIZE_MAX]; - - let bytes_read = storage.read(&mut buffer)?; - let mut stored_sector = 0; - - if bytes_read < 512 { - return Err(FSError::InternalFSError(InternalFSError::StorageTooSmall)); - } - - let bpb: BPBFAT = bincode_config().deserialize(&buffer[..BPBFAT_SIZE])?; - - let ebr = if bpb.table_size_16 == 0 { - let ebr_fat32 = bincode_config() - .deserialize::(&buffer[BPBFAT_SIZE..BPBFAT_SIZE + EBR_SIZE])?; - - storage.seek(SeekFrom::Start( - ebr_fat32.fat_info as u64 * bpb.bytes_per_sector as u64, - ))?; - stored_sector = ebr_fat32.fat_info.into(); - storage.read_exact(&mut buffer[..bpb.bytes_per_sector as usize])?; - let fsinfo = bincode_config() - .deserialize::(&buffer[..bpb.bytes_per_sector as usize])?; - - if !fsinfo.verify_signature() { - log::error!("FAT32 FSInfo has invalid signature(s)"); - return Err(FSError::InternalFSError(InternalFSError::InvalidFSInfoSig)); - } - - EBR::FAT32(ebr_fat32, fsinfo) - } else { - EBR::FAT12_16( - bincode_config() - .deserialize::(&buffer[BPBFAT_SIZE..BPBFAT_SIZE + EBR_SIZE])?, - ) - }; - - // TODO: see how we will handle this for exfat - let boot_record = BootRecord::FAT(BootRecordFAT { bpb, ebr }); - - // verify boot record signature - let fat_type = boot_record.fat_type(); - log::info!("The FAT type of the filesystem is {:?}", fat_type); - - match boot_record { - BootRecord::FAT(boot_record_fat) => { - if boot_record_fat.verify_signature() { - log::error!("FAT boot record has invalid signature(s)"); - return Err(FSError::InternalFSError(InternalFSError::InvalidBPBSig)); - } - } - BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT not yet implemented"), - }; - - let sector_size: u32 = match boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.bpb.bytes_per_sector.into(), - BootRecord::ExFAT(boot_record_exfat) => 1 << boot_record_exfat.sector_shift, - }; - let cluster_size: u64 = match boot_record { - BootRecord::FAT(boot_record_fat) => { - (boot_record_fat.bpb.sectors_per_cluster as u32 * sector_size).into() - } - BootRecord::ExFAT(boot_record_exfat) => { - 1 << (boot_record_exfat.sector_shift + boot_record_exfat.cluster_shift) - } - }; - - let first_root_dir_sector = match boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.first_root_dir_sector().into(), - BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), - }; - - let first_data_sector = match boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.first_data_sector().into(), - BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), - }; - - let fat_table_count = match boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.bpb.table_count, - BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), - }; - - let total_sectors = match boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.total_sectors(), - BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), - }; - - let total_clusters = match boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.total_clusters(), - BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), - }; - - let props = FSProperties { - sector_size, - cluster_size, - fat_table_count, - total_sectors, - total_clusters, - first_root_dir_sector, - first_data_sector, - }; - - let mut fs = Self { - storage, - sector_buffer: buffer[..sector_size as usize].to_vec(), - buffer_modified: false, - stored_sector, - boot_record, - fat_type, - props, - filter: FileFilter::default(), - }; - - if !fs.FAT_tables_are_identical()? { - return Err(FSError::InternalFSError( - InternalFSError::MismatchingFATTables, - )); - } - - Ok(fs) - } -} - -#[derive(Debug)] -struct EntryParser { - entries: Vec, - lfn_buf: Vec, - lfn_checksum: Option, - current_chain: Option, -} - -impl Default for EntryParser { - fn default() -> Self { - EntryParser { - entries: Vec::new(), - lfn_buf: Vec::new(), - lfn_checksum: None, - current_chain: None, - } - } -} - -const UNUSED_ENTRY: u8 = 0xE5; -const LAST_AND_UNUSED_ENTRY: u8 = 0x00; - -impl EntryParser { - #[inline] - fn _decrement_parsed_entries_counter(&mut self) { - if let Some(current_chain) = &mut self.current_chain { - current_chain.len -= 1 - } - } - - /// Parses a sector of 8.3 & LFN entries - /// - /// Returns a [`Result`] indicating whether or not - /// this sector was the last one in the chain containing entries - fn parse_sector( - &mut self, - sector: u32, - fs: &mut FileSystem, - ) -> Result::Error> - where - S: Read + Write + Seek, - { - let entry_location = EntryLocation::from_partition_sector(sector, fs); - - for (index, chunk) in fs - .read_nth_sector(sector.into())? - .chunks(DIRENTRY_SIZE) - .enumerate() - { - match chunk[0] { - LAST_AND_UNUSED_ENTRY => return Ok(true), - UNUSED_ENTRY => continue, - _ => (), - }; - - let Ok(entry) = bincode_config().deserialize::(&chunk) else { - continue; - }; - - // update current entry chain data - match &mut self.current_chain { - Some(current_chain) => current_chain.len += 1, - None => { - self.current_chain = Some(DirEntryChain { - location: entry_location.clone(), - index: index as u32, - len: 1, - }) - } - } - - if entry.attributes.contains(RawAttributes::LFN) { - // TODO: perhaps there is a way to utilize the `order` field? - let Ok(lfn_entry) = bincode_config().deserialize::(&chunk) else { - self._decrement_parsed_entries_counter(); - continue; - }; - - // If the signature verification fails, consider this entry corrupted - if !lfn_entry.verify_signature() { - self._decrement_parsed_entries_counter(); - continue; - } - - match self.lfn_checksum { - Some(checksum) => { - if checksum != lfn_entry.checksum { - self.lfn_checksum = None; - self.lfn_buf.clear(); - self.current_chain = None; - continue; - } - } - None => self.lfn_checksum = Some(lfn_entry.checksum), - } - - let char_arr = lfn_entry.get_byte_slice().to_vec(); - if let Ok(temp_str) = string_from_lfn(&char_arr) { - self.lfn_buf.push(temp_str); - } - - continue; - } - - let filename = if !self.lfn_buf.is_empty() - && self - .lfn_checksum - .is_some_and(|checksum| checksum == entry.sfn.gen_checksum()) - { - // for efficiency reasons, we store the LFN string sequences as we read them - let parsed_str: String = self.lfn_buf.iter().cloned().rev().collect(); - self.lfn_buf.clear(); - self.lfn_checksum = None; - parsed_str - } else { - entry.sfn.to_string() - }; - - if let (Ok(created), Ok(modified), Ok(accessed)) = ( - entry.created.try_into(), - entry.modified.try_into(), - entry.accessed.try_into(), - ) { - self.entries.push(RawProperties { - name: filename, - is_dir: entry.attributes.contains(RawAttributes::DIRECTORY), - attributes: entry.attributes, - created, - modified, - accessed, - file_size: entry.file_size, - data_cluster: ((entry.cluster_high as u32) << 16) + entry.cluster_low as u32, - chain_props: self - .current_chain - .take() - .expect("at this point, this shouldn't be None"), - }) - } - } - - Ok(false) - } - - /// Consumes [`Self`](EntryParser) & returns a `Vec` of [`RawProperties`] - /// of the parsed entries - fn finish(self) -> Vec { - self.entries - } -} - -/// Internal [`Read`]-related low-level functions -impl FileSystem -where - S: Read + Write + Seek, -{ - fn process_root_dir(&mut self) -> FSResult, S::Error> { - match self.boot_record { - BootRecord::FAT(boot_record_fat) => match boot_record_fat.ebr { - EBR::FAT12_16(_ebr_fat12_16) => { - let mut entry_parser = EntryParser::default(); - - let root_dir_sector = boot_record_fat.first_root_dir_sector(); - let sector_count = boot_record_fat.root_dir_sectors(); - - for sector in root_dir_sector..(root_dir_sector + sector_count) { - if entry_parser.parse_sector(sector.into(), self)? { - break; - } - } - - Ok(entry_parser.finish()) - } - EBR::FAT32(ebr_fat32, _) => { - let cluster = ebr_fat32.root_cluster; - self.process_normal_dir(cluster) - } - }, - BootRecord::ExFAT(_boot_record_exfat) => todo!(), - } - } - - fn process_normal_dir( - &mut self, - mut data_cluster: u32, - ) -> FSResult, S::Error> { - let mut entry_parser = EntryParser::default(); - - 'outer: loop { - // FAT specification, section 6.7 - let first_sector_of_cluster = self.data_cluster_to_partition_sector(data_cluster); - for sector in first_sector_of_cluster - ..(first_sector_of_cluster + self.sectors_per_cluster() as u32) - { - if entry_parser.parse_sector(sector.into(), self)? { - break 'outer; - } - } - - // Read corresponding FAT entry - let current_fat_entry = self.read_nth_FAT_entry(data_cluster)?; - - match current_fat_entry { - // we are done here, break the loop - FATEntry::EOF => break, - // this cluster chain goes on, follow it - FATEntry::Allocated(next_cluster) => data_cluster = next_cluster, - // any other case (whether a bad, reserved or free cluster) is invalid, consider this cluster chain malformed - _ => { - log::error!("Cluster chain of directory is malformed"); - return Err(FSError::InternalFSError( - InternalFSError::MalformedClusterChain, - )); - } - } - } - - Ok(entry_parser.finish()) - } - - /// Gets the next free cluster. Returns an IO [`Result`] - /// If the [`Result`] returns [`Ok`] that contains a [`None`], the drive is full - fn next_free_cluster(&mut self) -> Result, S::Error> { - let start_cluster = match self.boot_record { - BootRecord::FAT(boot_record_fat) => { - let mut first_free_cluster = RESERVED_FAT_ENTRIES; - - if let EBR::FAT32(_, fsinfo) = boot_record_fat.ebr { - // a value of u32::MAX denotes unawareness of the first free cluster - // we also do a bit of range checking - // TODO: if this is unknown, figure it out and write it to the FSInfo structure - if fsinfo.first_free_cluster != u32::MAX - && fsinfo.first_free_cluster <= self.props.total_sectors - { - first_free_cluster = fsinfo.first_free_cluster - } - } - - first_free_cluster - } - BootRecord::ExFAT(_) => todo!("ExFAT not yet implemented"), - }; - - let mut current_cluster = start_cluster; - - while current_cluster < self.props.total_clusters { - match self.read_nth_FAT_entry(current_cluster)? { - FATEntry::Free => return Ok(Some(current_cluster)), - _ => (), - } - current_cluster += 1; - } - - Ok(None) - } - - /// Get the next cluster in a cluster chain, otherwise return [`None`] - fn get_next_cluster(&mut self, cluster: u32) -> Result, S::Error> { - Ok(match self.read_nth_FAT_entry(cluster)? { - FATEntry::Allocated(next_cluster) => Some(next_cluster), - // when a `ROFile` is created, `cluster_chain_is_healthy` is called, if it fails, that ROFile is dropped - _ => None, - }) - } - - #[allow(non_snake_case)] - /// Check whether or not the all the FAT tables of the storage medium are identical to each other - fn FAT_tables_are_identical(&mut self) -> Result { - // we could make it work, but we are only testing regular FAT filesystems (for now) - assert_ne!( - self.fat_type, - FATType::ExFAT, - "this function doesn't work with ExFAT" - ); - - /// How many bytes to probe at max for each FAT per iteration (must be a multiple of [`SECTOR_SIZE_MAX`]) - const MAX_PROBE_SIZE: u32 = 1 << 20; - - let fat_byte_size = match self.boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.fat_sector_size(), - BootRecord::ExFAT(_) => unreachable!(), - }; - - for nth_iteration in 0..fat_byte_size.div_ceil(MAX_PROBE_SIZE) { - let mut tables: Vec> = Vec::new(); - - for i in 0..self.props.fat_table_count { - let fat_start = - self.sector_to_partition_offset(self.boot_record.nth_FAT_table_sector(i)); - let current_offset = fat_start + nth_iteration * MAX_PROBE_SIZE; - let bytes_left = fat_byte_size - nth_iteration * MAX_PROBE_SIZE; - - self.storage.seek(SeekFrom::Start(current_offset.into()))?; - let mut buf = vec![0_u8; cmp::min(MAX_PROBE_SIZE, bytes_left) as usize]; - self.storage.read_exact(buf.as_mut_slice())?; - tables.push(buf); - } - - // we check each table with the first one (except the first one ofc) - if !tables.iter().skip(1).all(|buf| buf == &tables[0]) { - return Ok(false); - } - } - - Ok(true) - } - - /// Read the nth sector from the partition's beginning and store it in [`self.sector_buffer`](Self::sector_buffer) - /// - /// This function also returns an immutable reference to [`self.sector_buffer`](Self::sector_buffer) - fn read_nth_sector(&mut self, n: u64) -> Result<&Vec, S::Error> { - // nothing to do if the sector we wanna read is already cached - if n != self.stored_sector { - // let's sync the current sector first - self.sync_sector_buffer()?; - self.storage.seek(SeekFrom::Start( - self.sector_to_partition_offset(n as u32).into(), - ))?; - self.storage.read_exact(&mut self.sector_buffer)?; - self.storage - .seek(SeekFrom::Current(-i64::from(self.props.sector_size)))?; - - self.stored_sector = n; - } - - Ok(&self.sector_buffer) - } - - #[allow(non_snake_case)] - fn read_nth_FAT_entry(&mut self, n: u32) -> Result { - // the size of an entry rounded up to bytes - let entry_size = self.fat_type.entry_size(); - let entry_props = FATEntryProps::new(n, &self); - - self.read_nth_sector(entry_props.fat_sectors[0].into())?; - - let mut value_bytes = [0_u8; 4]; - let bytes_to_read: usize = cmp::min( - entry_props.sector_offset + entry_size as usize, - self.sector_size() as usize, - ) - entry_props.sector_offset; - value_bytes[..bytes_to_read].copy_from_slice( - &self.sector_buffer - [entry_props.sector_offset..entry_props.sector_offset + bytes_to_read], - ); // this shouldn't panic - - // in FAT12, FAT entries may be split between two different sectors - if self.fat_type == FATType::FAT12 && (bytes_to_read as u32) < entry_size { - self.read_nth_sector((entry_props.fat_sectors[0] + 1).into())?; - - value_bytes[bytes_to_read..entry_size as usize] - .copy_from_slice(&self.sector_buffer[..(entry_size as usize - bytes_to_read)]); - }; - - let mut value = u32::from_le_bytes(value_bytes); - match self.fat_type { - // FAT12 entries are split between different bytes - FATType::FAT12 => { - if n & 1 != 0 { - value >>= 4 - } else { - value &= 0xFFF - } - } - // ignore the high 4 bits if this is FAT32 - FATType::FAT32 => value &= 0x0FFFFFFF, - _ => (), - } - - /* - // pad unused bytes with 1s - let padding: u32 = u32::MAX.to_be() << self.fat_type.bits_per_entry(); - value |= padding.to_le(); - */ - - // TODO: perhaps byte padding can replace some redundant code here? - Ok(match self.fat_type { - FATType::FAT12 => match value { - 0x000 => FATEntry::Free, - 0xFF7 => FATEntry::Bad, - 0xFF8..=0xFFE | 0xFFF => FATEntry::EOF, - _ => { - if (0x002..(self.props.total_clusters + 1)).contains(&value.into()) { - FATEntry::Allocated(value.into()) - } else { - FATEntry::Reserved - } - } - }, - FATType::FAT16 => match value { - 0x0000 => FATEntry::Free, - 0xFFF7 => FATEntry::Bad, - 0xFFF8..=0xFFFE | 0xFFFF => FATEntry::EOF, - _ => { - if (0x0002..(self.props.total_clusters + 1)).contains(&value.into()) { - FATEntry::Allocated(value.into()) - } else { - FATEntry::Reserved - } - } - }, - FATType::FAT32 => match value { - 0x00000000 => FATEntry::Free, - 0x0FFFFFF7 => FATEntry::Bad, - 0x0FFFFFF8..=0xFFFFFFE | 0x0FFFFFFF => FATEntry::EOF, - _ => { - if (0x00000002..(self.props.total_clusters + 1)).contains(&value.into()) { - FATEntry::Allocated(value.into()) - } else { - FATEntry::Reserved - } - } - }, - FATType::ExFAT => todo!("ExFAT not yet implemented"), - }) - } -} - -/// Internal [`Write`]-related low-level functions -impl FileSystem -where - S: Read + Write + Seek, -{ - #[allow(non_snake_case)] - fn write_nth_FAT_entry(&mut self, n: u32, entry: FATEntry) -> Result<(), S::Error> { - // the size of an entry rounded up to bytes - let entry_size = self.fat_type.entry_size(); - let entry_props = FATEntryProps::new(n, &self); - - // the previous solution would overflow, here's a correct implementation - let mask = utils::bits::setbits_u32(self.fat_type.bits_per_entry()); - let mut value: u32 = u32::from(entry.clone()) & mask; - - if self.fat_type == FATType::FAT32 { - // in FAT32, the high 4 bits are unused - value &= 0x0FFFFFFF; - } - - match self.fat_type { - FATType::FAT12 => { - let should_shift = n & 1 != 0; - if should_shift { - // FAT12 entries are split between different bytes - value <<= 4; - } - - // we update all the FAT copies - for fat_sector in entry_props.fat_sectors { - self.read_nth_sector(fat_sector.into())?; - - let value_bytes = value.to_le_bytes(); - - let mut first_byte = value_bytes[0]; - - if should_shift { - let mut old_byte = self.sector_buffer[entry_props.sector_offset]; - // ignore the high 4 bytes of the old entry - old_byte &= 0x0F; - // OR it with the new value - first_byte |= old_byte; - } - - self.sector_buffer[entry_props.sector_offset] = first_byte; // this shouldn't panic - self.buffer_modified = true; - - let bytes_left_on_sector: usize = cmp::min( - entry_size as usize, - self.sector_size() as usize - entry_props.sector_offset, - ); - - if bytes_left_on_sector < entry_size as usize { - // looks like this FAT12 entry spans multiple sectors, we must also update the other one - self.read_nth_sector((fat_sector + 1).into())?; - } - - let mut second_byte = value_bytes[1]; - let second_byte_index = - (entry_props.sector_offset + 1) % self.sector_size() as usize; - if !should_shift { - let mut old_byte = self.sector_buffer[second_byte_index]; - // ignore the low 4 bytes of the old entry - old_byte &= 0xF0; - // OR it with the new value - second_byte |= old_byte; - } - - self.sector_buffer[second_byte_index] = second_byte; // this shouldn't panic - self.buffer_modified = true; - } - } - FATType::FAT16 | FATType::FAT32 => { - // we update all the FAT copies - for fat_sector in entry_props.fat_sectors { - self.read_nth_sector(fat_sector.into())?; - - let value_bytes = value.to_le_bytes(); - - self.sector_buffer[entry_props.sector_offset - ..entry_props.sector_offset + entry_size as usize] - .copy_from_slice(&value_bytes[..entry_size as usize]); // this shouldn't panic - self.buffer_modified = true; - } - } - FATType::ExFAT => todo!("ExFAT not yet implemented"), - }; - - Ok(()) - } - - fn sync_sector_buffer(&mut self) -> Result<(), S::Error> { - if self.buffer_modified { - log::trace!("syncing sector {:?}", self.stored_sector); - self.storage.write_all(&self.sector_buffer)?; - self.storage - .seek(SeekFrom::Current(-i64::from(self.props.sector_size)))?; - } - self.buffer_modified = false; - - Ok(()) - } -} - -/// Public [`Read`]-related functions -impl FileSystem -where - S: Read + Write + Seek, -{ - /// Read all the entries of a directory ([`PathBuf`]) into [`Vec`] - /// - /// Fails if `path` doesn't represent a directory, or if that directory doesn't exist - pub fn read_dir(&mut self, path: PathBuf) -> FSResult, S::Error> { - if path.is_malformed() { - return Err(FSError::MalformedPath); - } - if !path.is_dir() { - log::error!("Not a directory"); - return Err(FSError::NotADirectory); - } - - let mut entries = self.process_root_dir()?; - - for dir_name in path.clone().into_iter() { - let dir_cluster = match entries.iter().find(|entry| { - entry.name == dir_name && entry.attributes.contains(RawAttributes::DIRECTORY) - }) { - Some(entry) => entry.data_cluster, - None => { - log::error!("Directory {} not found", path); - return Err(FSError::NotFound); - } - }; - - entries = self.process_normal_dir(dir_cluster)?; - } - - // if we haven't returned by now, that means that the entries vector - // contains what we want, let's map it to DirEntries and return - Ok(entries - .into_iter() - .filter(|x| self.filter.filter(x)) - .map(|rawentry| { - let mut entry_path = path.clone(); - - entry_path.push(format!( - "{}{}", - rawentry.name, - if rawentry.is_dir { "/" } else { "" } - )); - DirEntry { - entry: Properties::from_raw(rawentry, entry_path), - } - }) - .collect()) - } - - /// Get a corresponding [`ROFile`] object from a [`PathBuf`] - /// - /// Borrows `&mut self` until that [`ROFile`] object is dropped, effectively locking `self` until that file closed - /// - /// Fails if `path` doesn't represent a file, or if that file doesn't exist - pub fn get_ro_file(&mut self, path: PathBuf) -> FSResult, S::Error> { - if path.is_malformed() { - return Err(FSError::MalformedPath); - } - - if let Some(file_name) = path.file_name() { - let parent_dir = self.read_dir(path.parent())?; - match parent_dir.into_iter().find(|direntry| { - direntry - .path() - .file_name() - .is_some_and(|entry_name| entry_name == file_name) - }) { - Some(direntry) => { - let mut file = ROFile { - fs: self, - props: FileProps { - offset: 0, - current_cluster: direntry.entry.data_cluster, - entry: direntry.entry, - }, - }; - - if file.cluster_chain_is_healthy()? { - Ok(file) - } else { - log::error!("The cluster chain of a file is malformed"); - Err(FSError::InternalFSError( - InternalFSError::MalformedClusterChain, - )) - } - } - None => { - log::error!("ROFile {} not found", path); - Err(FSError::NotFound) - } - } - } else { - log::error!("Is a directory (not a file)"); - Err(FSError::IsADirectory) - } - } -} - -/// [`Write`]-related functions -impl FileSystem -where - S: Read + Write + Seek, -{ - /// Get a corresponding [`RWFile`] object from a [`PathBuf`] - /// - /// Borrows `&mut self` until that [`RWFile`] object is dropped, effectively locking `self` until that file closed - /// - /// Fails if `path` doesn't represent a file, or if that file doesn't exist - pub fn get_rw_file(&mut self, path: PathBuf) -> FSResult, S::Error> { - // we first write an empty array to the storage medium - // if the storage has Write functionality, this shouldn't error, - // otherwise it should return an error. - self.storage.write_all(&[])?; - - let ro_file = self.get_ro_file(path)?; - if ro_file.attributes.read_only { - return Err(FSError::ReadOnlyFile); - }; - - Ok(RWFile { ro_file }) - } -} - -/// Properties about the position of a [`FATEntry`] inside the FAT region -struct FATEntryProps { - /// Each `n`th element of the vector points at the corrensponding sector at the `n+1`th FAT table - fat_sectors: Vec, - sector_offset: usize, -} - -impl FATEntryProps { - /// Get the [`FATEntryProps`] of the `n`-th [`FATEntry`] of a [`ROFileSystem`] (`fs`) - pub fn new(n: u32, fs: &FileSystem) -> Self - where - S: Read + Write + Seek, - { - let fat_byte_offset: u32 = n * fs.fat_type.bits_per_entry() as u32 / 8; - let mut fat_sectors = Vec::new(); - for nth_table in 0..fs.props.fat_table_count { - let table_sector_offset = fs.boot_record.nth_FAT_table_sector(nth_table); - let fat_sector = table_sector_offset + fat_byte_offset / fs.props.sector_size; - fat_sectors.push(fat_sector); - } - let sector_offset: usize = (fat_byte_offset % fs.props.sector_size) as usize; - - FATEntryProps { - fat_sectors, - sector_offset, - } - } -} - -impl ops::Drop for FileSystem -where - S: Read + Write + Seek, -{ - fn drop(&mut self) { - // nothing to do if these error out while dropping - let _ = self.sync_sector_buffer(); - let _ = self.storage.flush(); - } -} - -#[cfg(all(test, feature = "std"))] -mod tests { - use super::*; - use test_log::test; - use time::macros::*; - - static MINFS: &[u8] = include_bytes!("../imgs/minfs.img"); - static FAT12: &[u8] = include_bytes!("../imgs/fat12.img"); - static FAT16: &[u8] = include_bytes!("../imgs/fat16.img"); - static FAT32: &[u8] = include_bytes!("../imgs/fat32.img"); - - #[test] - #[allow(non_snake_case)] - fn check_FAT_offset() { - use std::io::Cursor; - - let mut storage = Cursor::new(FAT16.to_owned()); - let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - - let fat_offset = match fs.boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.first_fat_sector(), - BootRecord::ExFAT(_boot_record_exfat) => unreachable!(), - }; - - // we manually read the first and second entry of the FAT table - fs.read_nth_sector(fat_offset.into()).unwrap(); - - let first_entry = u16::from_le_bytes(fs.sector_buffer[0..2].try_into().unwrap()); - let media_type = if let BootRecord::FAT(boot_record_fat) = fs.boot_record { - boot_record_fat.bpb._media_type - } else { - unreachable!("this should be a FAT16 filesystem") - }; - assert_eq!(u16::MAX << 8 | media_type as u16, first_entry); - - let second_entry = u16::from_le_bytes(fs.sector_buffer[2..4].try_into().unwrap()); - assert_eq!(u16::MAX, second_entry); - } - - #[test] - fn read_file_in_root_dir() { - use std::io::Cursor; - - let mut storage = Cursor::new(FAT16.to_owned()); - let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - - let mut file = fs.get_ro_file(PathBuf::from("/root.txt")).unwrap(); - - let mut file_string = String::new(); - file.read_to_string(&mut file_string).unwrap(); - const EXPECTED_STR: &str = "I am in the filesystem's root!!!\n\n"; - assert_eq!(file_string, EXPECTED_STR); - } - - static BEE_MOVIE_SCRIPT: &str = include_str!("../tests/bee movie script.txt"); - fn assert_vec_is_bee_movie_script(buf: &Vec) { - let string = str::from_utf8(&buf).unwrap(); - let expected_size = BEE_MOVIE_SCRIPT.len(); - assert_eq!(buf.len(), expected_size); - - assert_eq!(string, BEE_MOVIE_SCRIPT); - } - fn assert_file_is_bee_movie_script(file: &mut ROFile<'_, S>) - where - S: Read + Write + Seek, - { - let mut buf = Vec::new(); - file.read_to_end(&mut buf).unwrap(); - - assert_vec_is_bee_movie_script(&buf); - } - - #[test] - fn read_huge_file() { - use std::io::Cursor; - - let mut storage = Cursor::new(FAT16.to_owned()); - let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - - let mut file = fs - .get_ro_file(PathBuf::from("/bee movie script.txt")) - .unwrap(); - assert_file_is_bee_movie_script(&mut file); - } - - #[test] - fn seek_n_read() { - // this uses the famous "I'd like to interject for a moment" copypasta as a test file - // you can find it online by just searching this term - - use std::io::Cursor; - - let mut storage = Cursor::new(FAT16.to_owned()); - let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - - let mut file = fs - .get_ro_file(PathBuf::from("/GNU ⁄ Linux copypasta.txt")) - .unwrap(); - let mut file_bytes = [0_u8; 4096]; - - // we first perform a forward seek... - const EXPECTED_STR1: &str = "Linux is the kernel"; - file.seek(SeekFrom::Start(792)).unwrap(); - let bytes_read = file.read(&mut file_bytes[..EXPECTED_STR1.len()]).unwrap(); - assert_eq!( - String::from_utf8_lossy(&file_bytes[..bytes_read]), - EXPECTED_STR1 - ); - - // ...then a backward one - const EXPECTED_STR2: &str = "What you're referring to as Linux, is in fact, GNU/Linux"; - file.seek(SeekFrom::Start(39)).unwrap(); - let bytes_read = file.read(&mut file_bytes[..EXPECTED_STR2.len()]).unwrap(); - assert_eq!( - String::from_utf8_lossy(&file_bytes[..bytes_read]), - EXPECTED_STR2 - ); - } - - #[test] - // this won't actually modify the .img file or the static slices, - // since we run .to_owned(), which basically clones the data in the static slices, - // in order to make the Cursor readable/writable - fn write_to_file() { - use std::io::Cursor; - - let mut storage = Cursor::new(FAT12.to_owned()); - let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - - let mut file = fs.get_rw_file(PathBuf::from("/root.txt")).unwrap(); - - file.write_all(BEE_MOVIE_SCRIPT.as_bytes()).unwrap(); - file.rewind().unwrap(); - - assert_file_is_bee_movie_script(&mut file); - - // now let's do something else - // this write operations will happen between 2 clusters - const TEXT_OFFSET: u64 = 4598; - const TEXT: &str = "Hello from the other side"; - - file.seek(SeekFrom::Start(TEXT_OFFSET)).unwrap(); - file.write_all(TEXT.as_bytes()).unwrap(); - - // seek back to the start of where we wrote our text - file.seek(SeekFrom::Current(-(TEXT.len() as i64))).unwrap(); - let mut buf = [0_u8; TEXT.len()]; - file.read_exact(&mut buf).unwrap(); - let stored_text = str::from_utf8(&buf).unwrap(); - - assert_eq!(TEXT, stored_text); - - // we are also gonna write the bee movie ten more times to see if FAT12 can correctly handle split entries - for i in 0..10 { - log::debug!("Writing the bee movie script for the {i} consecutive time",); - - let start_offset = file.seek(SeekFrom::End(0)).unwrap(); - - file.write_all(BEE_MOVIE_SCRIPT.as_bytes()).unwrap(); - file.seek(SeekFrom::Start(start_offset)).unwrap(); - - let mut buf = vec![0_u8; BEE_MOVIE_SCRIPT.len()]; - file.read_exact(buf.as_mut_slice()).unwrap(); - - assert_vec_is_bee_movie_script(&buf); - } - } - - #[test] - fn remove_root_dir_file() { - use std::io::Cursor; - - let mut storage = Cursor::new(FAT16.to_owned()); - let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - - // the bee movie script (here) is in the root directory region - let file_path = PathBuf::from("/bee movie script.txt"); - let file = fs.get_rw_file(file_path.clone()).unwrap(); - file.remove().unwrap(); - - // the file should now be gone - let file_result = fs.get_ro_file(file_path); - match file_result { - Err(err) => match err { - FSError::NotFound => (), - _ => panic!("unexpected IOError: {:?}", err), - }, - _ => panic!("file should have been deleted by now"), - } - } - - #[test] - fn remove_data_region_file() { - use std::io::Cursor; - - let mut storage = Cursor::new(FAT12.to_owned()); - let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - - // the bee movie script (here) is in the data region - let file_path = PathBuf::from("/test/bee movie script.txt"); - let file = fs.get_rw_file(file_path.clone()).unwrap(); - file.remove().unwrap(); - - // the file should now be gone - let file_result = fs.get_ro_file(file_path); - match file_result { - Err(err) => match err { - FSError::NotFound => (), - _ => panic!("unexpected IOError: {:?}", err), - }, - _ => panic!("file should have been deleted by now"), - } - } - - #[test] - #[allow(non_snake_case)] - fn FAT_tables_after_write_are_identical() { - use std::io::Cursor; - - let mut storage = Cursor::new(FAT16.to_owned()); - let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - - assert!( - fs.FAT_tables_are_identical().unwrap(), - concat!( - "this should pass. ", - "if it doesn't, either the corresponding .img file's FAT tables aren't identical", - "or the tables_are_identical function doesn't work correctly" - ) - ); - - // let's write the bee movie script to root.txt (why not), check, truncate the file, then check again - let mut file = fs.get_rw_file(PathBuf::from("root.txt")).unwrap(); - - file.write_all(BEE_MOVIE_SCRIPT.as_bytes()).unwrap(); - assert!(file.fs.FAT_tables_are_identical().unwrap()); - - file.truncate(10_000).unwrap(); - assert!(file.fs.FAT_tables_are_identical().unwrap()); - } - - #[test] - fn truncate_file() { - use std::io::Cursor; - - let mut storage = Cursor::new(FAT16.to_owned()); - let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - - let mut file = fs - .get_rw_file(PathBuf::from("/bee movie script.txt")) - .unwrap(); - - // we are gonna truncate the bee movie script down to 20 000 bytes - const NEW_SIZE: u32 = 20_000; - file.truncate(NEW_SIZE).unwrap(); - - let mut file_string = String::new(); - file.read_to_string(&mut file_string).unwrap(); - let mut expected_string = BEE_MOVIE_SCRIPT.to_string(); - expected_string.truncate(NEW_SIZE as usize); - - assert_eq!(file_string, expected_string); - } - - #[test] - fn read_only_file() { - use std::io::Cursor; - - let mut storage = Cursor::new(FAT16.to_owned()); - let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - - let file_result = fs.get_rw_file(PathBuf::from("/rootdir/example.txt")); - - match file_result { - Err(err) => match err { - FSError::ReadOnlyFile => (), - _ => panic!("unexpected IOError"), - }, - _ => panic!("file is marked read-only, yet somehow we got a RWFile for it"), - } - } - - #[test] - fn get_hidden_file() { - use std::io::Cursor; - - let mut storage = Cursor::new(FAT12.to_owned()); - let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - - let file_path = PathBuf::from("/hidden"); - let file_result = fs.get_ro_file(file_path.clone()); - match file_result { - Err(err) => match err { - FSError::NotFound => (), - _ => panic!("unexpected IOError"), - }, - _ => panic!("file should be hidden by default"), - } - - // let's now allow the filesystem to list hidden files - fs.show_hidden(true); - let file = fs.get_ro_file(file_path).unwrap(); - assert!(file.attributes.hidden); - } - - #[test] - fn read_file_in_subdir() { - use std::io::Cursor; - - let mut storage = Cursor::new(FAT16.to_owned()); - let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - - let mut file = fs - .get_ro_file(PathBuf::from("/rootdir/example.txt")) - .unwrap(); - - let mut file_string = String::new(); - file.read_to_string(&mut file_string).unwrap(); - const EXPECTED_STR: &str = "I am not in the root directory :(\n\n"; - assert_eq!(file_string, EXPECTED_STR); - } - - #[test] - fn check_file_timestamps() { - use std::io::Cursor; - - let mut storage = Cursor::new(FAT16.to_owned()); - let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - - let file = fs - .get_ro_file(PathBuf::from("/rootdir/example.txt")) - .unwrap(); - - assert_eq!(datetime!(2024-07-11 13:02:38.15), file.created); - assert_eq!(datetime!(2024-07-11 13:02:38.0), file.modified); - assert_eq!(date!(2024 - 07 - 11), file.accessed); - } - - #[test] - fn read_file_fat12() { - use std::io::Cursor; - - let mut storage = Cursor::new(FAT12.to_owned()); - let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - - let mut file = fs.get_ro_file(PathBuf::from("/foo/bar.txt")).unwrap(); - let mut file_string = String::new(); - file.read_to_string(&mut file_string).unwrap(); - const EXPECTED_STR: &str = "Hello, World!\n"; - assert_eq!(file_string, EXPECTED_STR); - - // please not that the FAT12 image has been modified so that - // one FAT entry of the file we are reading is split between different sectors - // this way, we also test for this case - let mut file = fs - .get_ro_file(PathBuf::from("/test/bee movie script.txt")) - .unwrap(); - assert_file_is_bee_movie_script(&mut file); - } - - #[test] - fn read_file_fat32() { - use std::io::Cursor; - - let mut storage = Cursor::new(FAT32.to_owned()); - let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - - let mut file = fs - .get_ro_file(PathBuf::from("/secret/bee movie script.txt")) - .unwrap(); - - assert_file_is_bee_movie_script(&mut file); - } - - #[test] - fn seek_n_read_fat32() { - use std::io::Cursor; - - let mut storage = Cursor::new(FAT32.to_owned()); - let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - - let mut file = fs.get_ro_file(PathBuf::from("/hello.txt")).unwrap(); - file.seek(SeekFrom::Start(13)).unwrap(); - - let mut string = String::new(); - file.read_to_string(&mut string).unwrap(); - const EXPECTED_STR: &str = "FAT32 filesystem!!!\n"; - - assert_eq!(string, EXPECTED_STR); - } - - #[test] - fn write_to_fat32_file() { - use std::io::Cursor; - - let mut storage = Cursor::new(FAT32.to_owned()); - let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - - let mut file = fs.get_rw_file(PathBuf::from("/hello.txt")).unwrap(); - // an arbitrary offset to seek to - const START_OFFSET: u64 = 1436; - file.seek(SeekFrom::Start(START_OFFSET)).unwrap(); - - file.write_all(BEE_MOVIE_SCRIPT.as_bytes()).unwrap(); - - // seek back - file.seek(SeekFrom::Current(-(BEE_MOVIE_SCRIPT.len() as i64))) - .unwrap(); - - // read back what we wrote - let mut string = String::new(); - file.read_to_string(&mut string).unwrap(); - assert_eq!(string, BEE_MOVIE_SCRIPT); - - // let's also read back what was (and hopefully still is) - // at the start of the file - const EXPECTED_STR: &str = "Hello from a FAT32 filesystem!!!\n"; - file.rewind().unwrap(); - let mut buf = [0_u8; EXPECTED_STR.len()]; - file.read_exact(&mut buf).unwrap(); - - let stored_text = str::from_utf8(&buf).unwrap(); - assert_eq!(stored_text, EXPECTED_STR) - } - - #[test] - fn truncate_fat32_file() { - use std::io::Cursor; - - let mut storage = Cursor::new(FAT32.to_owned()); - let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - - const EXPECTED_STR: &str = "Hello fr"; - - let mut file = fs.get_rw_file(PathBuf::from("/hello.txt")).unwrap(); - file.truncate(EXPECTED_STR.len() as u32).unwrap(); - - let mut string = String::new(); - file.read_to_string(&mut string).unwrap(); - assert_eq!(string, EXPECTED_STR); - } - - #[test] - fn remove_fat32_file() { - use std::io::Cursor; - - let mut storage = Cursor::new(FAT32.to_owned()); - let mut fs = FileSystem::from_storage(&mut storage).unwrap(); - - let file_path = PathBuf::from("/secret/bee movie script.txt"); - - let file = fs.get_rw_file(file_path.clone()).unwrap(); - file.remove().unwrap(); - - // the file should now be gone - let file_result = fs.get_ro_file(file_path); - match file_result { - Err(err) => match err { - FSError::NotFound => (), - _ => panic!("unexpected IOError: {:?}", err), - }, - _ => panic!("file should have been deleted by now"), - } - } - - #[test] - fn assert_img_fat_type() { - static TEST_CASES: &[(&[u8], FATType)] = &[ - (MINFS, FATType::FAT12), - (FAT12, FATType::FAT12), - (FAT16, FATType::FAT16), - (FAT32, FATType::FAT32), - ]; - - for case in TEST_CASES { - use std::io::Cursor; - - let mut storage = Cursor::new(case.0.to_owned()); - let fs = FileSystem::from_storage(&mut storage).unwrap(); - - assert_eq!(fs.fat_type(), case.1) - } - } -} diff --git a/src/fs/bpb.rs b/src/fs/bpb.rs new file mode 100644 index 0000000..bdaa882 --- /dev/null +++ b/src/fs/bpb.rs @@ -0,0 +1,275 @@ +use super::*; + +use core::fmt; + +use serde::{Deserialize, Serialize}; +use serde_big_array::BigArray; + +pub(crate) const BPBFAT_SIZE: usize = 36; +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +pub(crate) struct BPBFAT { + pub _jmpboot: [u8; 3], + pub _oem_identifier: [u8; 8], + pub bytes_per_sector: u16, + pub sectors_per_cluster: u8, + pub reserved_sector_count: u16, + pub table_count: u8, + pub root_entry_count: u16, + // If this is 0, check `total_sectors_32` + pub total_sectors_16: u16, + pub _media_type: u8, + pub table_size_16: u16, + pub _sectors_per_track: u16, + pub _head_side_count: u16, + pub hidden_sector_count: u32, + pub total_sectors_32: u32, +} + +#[derive(Debug)] +pub(crate) enum BootRecord { + FAT(BootRecordFAT), + ExFAT(BootRecordExFAT), +} + +impl BootRecord { + #[inline] + /// The FAT type of this file system + pub(crate) fn fat_type(&self) -> FATType { + match self { + BootRecord::FAT(boot_record_fat) => { + let total_clusters = boot_record_fat.total_clusters(); + if total_clusters < 4085 { + FATType::FAT12 + } else if total_clusters < 65525 { + FATType::FAT16 + } else { + FATType::FAT32 + } + } + BootRecord::ExFAT(_boot_record_exfat) => { + todo!("ExFAT not yet implemented"); + FATType::ExFAT + } + } + } + + #[allow(non_snake_case)] + pub(crate) fn nth_FAT_table_sector(&self, n: u8) -> u32 { + match self { + BootRecord::FAT(boot_record_fat) => { + boot_record_fat.first_fat_sector() as u32 + + n as u32 * boot_record_fat.fat_sector_size() + } + BootRecord::ExFAT(boot_record_exfat) => { + // this should work, but ExFAT is not yet implemented, so... + todo!("ExFAT not yet implemented"); + boot_record_exfat.fat_count as u32 + n as u32 * boot_record_exfat.fat_len + } + } + } +} + +pub(crate) const BOOT_SIGNATURE: u8 = 0x29; +pub(crate) const FAT_SIGNATURE: u16 = 0x55AA; + +#[derive(Debug, Clone, Copy)] +pub(crate) struct BootRecordFAT { + pub bpb: BPBFAT, + pub ebr: EBR, +} + +impl BootRecordFAT { + #[inline] + pub(crate) fn verify_signature(&self) -> bool { + match self.fat_type() { + FATType::FAT12 | FATType::FAT16 | FATType::FAT32 => match self.ebr { + EBR::FAT12_16(ebr_fat12_16) => { + ebr_fat12_16.boot_signature == BOOT_SIGNATURE + && ebr_fat12_16.signature == FAT_SIGNATURE + } + EBR::FAT32(ebr_fat32, _) => { + ebr_fat32.boot_signature == BOOT_SIGNATURE + && ebr_fat32.signature == FAT_SIGNATURE + } + }, + FATType::ExFAT => todo!("ExFAT not yet implemented"), + } + } + + #[inline] + /// Total sectors in volume (including VBR)s + pub(crate) fn total_sectors(&self) -> u32 { + if self.bpb.total_sectors_16 == 0 { + self.bpb.total_sectors_32 + } else { + self.bpb.total_sectors_16 as u32 + } + } + + #[inline] + /// FAT size in sectors + pub(crate) fn fat_sector_size(&self) -> u32 { + match self.ebr { + EBR::FAT12_16(_ebr_fat12_16) => self.bpb.table_size_16.into(), + EBR::FAT32(ebr_fat32, _) => ebr_fat32.table_size_32, + } + } + + #[inline] + /// The size of the root directory (unless we have FAT32, in which case the size will be 0) + /// This calculation will round up + pub(crate) fn root_dir_sectors(&self) -> u16 { + ((self.bpb.root_entry_count * DIRENTRY_SIZE as u16) + (self.bpb.bytes_per_sector - 1)) + / self.bpb.bytes_per_sector + } + + #[inline] + /// The first sector in the File Allocation Table + pub(crate) fn first_fat_sector(&self) -> u16 { + self.bpb.reserved_sector_count + } + + #[inline] + /// The first sector of the root directory (returns the first data sector on FAT32) + pub(crate) fn first_root_dir_sector(&self) -> u16 { + self.first_fat_sector() + self.bpb.table_count as u16 * self.fat_sector_size() as u16 + } + + #[inline] + /// The first data sector (that is, the first sector in which directories and files may be stored) + pub(crate) fn first_data_sector(&self) -> u16 { + self.first_root_dir_sector() + self.root_dir_sectors() + } + + #[inline] + /// The total number of data sectors + pub(crate) fn total_data_sectors(&self) -> u32 { + self.total_sectors() - (self.bpb.table_count as u32 * self.fat_sector_size()) + + self.root_dir_sectors() as u32 + } + + #[inline] + /// The total number of clusters + pub(crate) fn total_clusters(&self) -> u32 { + self.total_data_sectors() / self.bpb.sectors_per_cluster as u32 + } + + #[inline] + /// The FAT type of this file system + pub(crate) fn fat_type(&self) -> FATType { + if self.bpb.bytes_per_sector == 0 { + todo!("ExFAT not yet implemented"); + FATType::ExFAT + } else { + let total_clusters = self.total_clusters(); + if total_clusters < 4085 { + FATType::FAT12 + } else if total_clusters < 65525 { + FATType::FAT16 + } else { + FATType::FAT32 + } + } + } +} + +#[derive(Debug, Clone, Copy)] +// Everything here is naturally aligned (thank god) +pub(crate) struct BootRecordExFAT { + pub _dummy_jmp: [u8; 3], + pub _oem_identifier: [u8; 8], + pub _zeroed: [u8; 53], + pub _partition_offset: u64, + pub volume_len: u64, + pub fat_offset: u32, + pub fat_len: u32, + pub cluster_heap_offset: u32, + pub cluster_count: u32, + pub root_dir_cluster: u32, + pub partition_serial_num: u32, + pub fs_revision: u16, + pub flags: u16, + pub sector_shift: u8, + pub cluster_shift: u8, + pub fat_count: u8, + pub drive_select: u8, + pub used_percentage: u8, + pub _reserved: [u8; 7], +} + +pub(crate) const EBR_SIZE: usize = 512 - BPBFAT_SIZE; +#[derive(Clone, Copy)] +pub(crate) enum EBR { + FAT12_16(EBRFAT12_16), + FAT32(EBRFAT32, FSInfoFAT32), +} + +impl fmt::Debug for EBR { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // TODO: find a good way of printing this + write!(f, "FAT12-16/32 Extended boot record...") + } +} + +#[derive(Deserialize, Serialize, Clone, Copy)] +pub(crate) struct EBRFAT12_16 { + pub _drive_num: u8, + pub _windows_nt_flags: u8, + pub boot_signature: u8, + pub volume_serial_num: u32, + pub volume_label: [u8; 11], + pub _system_identifier: [u8; 8], + #[serde(with = "BigArray")] + pub _boot_code: [u8; 448], + pub signature: u16, +} + +// FIXME: these might be the other way around +#[derive(Deserialize, Serialize, Debug, Clone, Copy)] +pub(crate) struct FATVersion { + minor: u8, + major: u8, +} + +#[derive(Deserialize, Serialize, Clone, Copy)] +pub(crate) struct EBRFAT32 { + pub table_size_32: u32, + pub _extended_flags: u16, + pub fat_version: FATVersion, + pub root_cluster: u32, + pub fat_info: u16, + pub backup_boot_sector: u16, + pub _reserved: [u8; 12], + pub _drive_num: u8, + pub _windows_nt_flags: u8, + pub boot_signature: u8, + pub volume_serial_num: u32, + pub volume_label: [u8; 11], + pub _system_ident: [u8; 8], + #[serde(with = "BigArray")] + pub _boot_code: [u8; 420], + pub signature: u16, +} + +const FSINFO_LEAD_SIGNATURE: u32 = 0x41615252; +const FSINFO_MID_SIGNATURE: u32 = 0x61417272; +const FSINFO_TRAIL_SIGNAUTE: u32 = 0xAA550000; +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +pub(crate) struct FSInfoFAT32 { + pub lead_signature: u32, + #[serde(with = "BigArray")] + pub _reserved1: [u8; 480], + pub mid_signature: u32, + pub free_cluster_count: u32, + pub first_free_cluster: u32, + pub _reserved2: [u8; 12], + pub trail_signature: u32, +} + +impl FSInfoFAT32 { + pub(crate) fn verify_signature(&self) -> bool { + self.lead_signature == FSINFO_LEAD_SIGNATURE + && self.mid_signature == FSINFO_MID_SIGNATURE + && self.trail_signature == FSINFO_TRAIL_SIGNAUTE + } +} diff --git a/src/fs/consts.rs b/src/fs/consts.rs new file mode 100644 index 0000000..fe93055 --- /dev/null +++ b/src/fs/consts.rs @@ -0,0 +1,8 @@ +/// The minimum size (in bytes) a sector is allowed to have +pub const SECTOR_SIZE_MIN: usize = 512; +/// The maximum size (in bytes) a sector is allowed to have +pub const SECTOR_SIZE_MAX: usize = 4096; + +/// Place this in the BPB _jmpboot field to hang if a computer attempts to boot this partition +/// The first two bytes jump to 0 on all bit modes and the third byte is just a NOP +pub(crate) const INFINITE_LOOP: [u8; 3] = [0xEB, 0xFE, 0x90]; diff --git a/src/fs/direntry.rs b/src/fs/direntry.rs new file mode 100644 index 0000000..7a0b642 --- /dev/null +++ b/src/fs/direntry.rs @@ -0,0 +1,336 @@ +use super::*; + +use crate::io::prelude::*; + +use core::{fmt, mem, num, ops}; + +#[cfg(not(feature = "std"))] +use alloc::{borrow::ToOwned, string::String}; + +use bitfield_struct::bitfield; +use bitflags::bitflags; +use serde::{Deserialize, Serialize}; +use time::{Date, PrimitiveDateTime, Time}; + +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +pub(crate) struct SFN { + name: [u8; 8], + ext: [u8; 3], +} + +impl SFN { + fn get_byte_slice(&self) -> [u8; 11] { + let mut slice = [0; 11]; + + slice[..8].copy_from_slice(&self.name); + slice[8..].copy_from_slice(&self.ext); + + slice + } + + pub(crate) fn gen_checksum(&self) -> u8 { + let mut sum = 0; + + for c in self.get_byte_slice() { + sum = (if (sum & 1) != 0 { 0x80_u8 } else { 0_u8 }) + .wrapping_add(sum >> 1) + .wrapping_add(c) + } + + log::debug!("SFN checksum: {:X}", sum); + + sum + } +} + +impl fmt::Display for SFN { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // we begin by writing the name (even if it is padded with spaces, they will be trimmed, so we don't care) + write!(f, "{}", String::from_utf8_lossy(&self.name).trim())?; + + // then, if the extension isn't empty (padded with zeroes), we write it too + let ext = String::from_utf8_lossy(&self.ext).trim().to_owned(); + if !ext.is_empty() { + write!(f, ".{}", ext)?; + }; + + Ok(()) + } +} + +bitflags! { + /// A list of the various (raw) attributes specified for a file/directory + /// + /// To check whether a given [`Attributes`] struct contains a flag, use the [`contains()`](Attributes::contains()) method + /// + /// Generated using [bitflags](https://docs.rs/bitflags/2.6.0/bitflags/) + #[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq)] + pub(crate) struct RawAttributes: u8 { + /// This entry is read-only + const READ_ONLY = 0x01; + /// This entry is normally hidden + const HIDDEN = 0x02; + /// This entry is a system file + const SYSTEM = 0x04; + /// This entry represents the volume's ID. + /// This is used internally and the library will never return such an entry + const VOLUME_ID = 0x08; + /// This entry is a directory. You should normally use a [`PathBuf`]s [`is_dir()`](PathBuf::is_dir) method instead + const DIRECTORY = 0x10; + /// This entry is marked to be archived. Used by archiving software for backing up files and directories + const ARCHIVE = 0x20; + + /// This entry is part of a LFN (long filename). Used internally + const LFN = Self::READ_ONLY.bits() | + Self::HIDDEN.bits() | + Self::SYSTEM.bits() | + Self::VOLUME_ID.bits(); + } +} + +/// A list of the various attributes specified for a file/directory +#[derive(Debug, Clone, Copy)] +pub struct Attributes { + /// This is a read-only file + pub read_only: bool, + /// This file is to be hidden unless a request is issued + /// explicitly requesting inclusion of “hidden files” + pub hidden: bool, + /// This is a system file and shouldn't be listed unless a request + /// is issued explicitly requesting inclusion of system files” + pub system: bool, + /// This file has been modified since last archival + /// or has never been archived. + /// + /// This field should only concern archival software + pub archive: bool, +} + +impl From for Attributes { + fn from(value: RawAttributes) -> Self { + Attributes { + read_only: value.contains(RawAttributes::READ_ONLY), + hidden: value.contains(RawAttributes::HIDDEN), + system: value.contains(RawAttributes::SYSTEM), + archive: value.contains(RawAttributes::ARCHIVE), + } + } +} + +const START_YEAR: i32 = 1980; + +#[bitfield(u16)] +#[derive(Serialize, Deserialize)] +pub(crate) struct TimeAttribute { + /// Multiply by 2 + #[bits(5)] + seconds: u8, + #[bits(6)] + minutes: u8, + #[bits(5)] + hour: u8, +} + +#[bitfield(u16)] +#[derive(Serialize, Deserialize)] +pub(crate) struct DateAttribute { + #[bits(5)] + day: u8, + #[bits(4)] + month: u8, + #[bits(7)] + year: u8, +} + +impl TryFrom for Time { + type Error = (); + + fn try_from(value: TimeAttribute) -> Result { + time::parsing::Parsed::new() + .with_hour_24(value.hour()) + .and_then(|parsed| parsed.with_minute(value.minutes())) + .and_then(|parsed| parsed.with_second(value.seconds() * 2)) + .map(|parsed| parsed.try_into().ok()) + .flatten() + .ok_or(()) + } +} + +impl TryFrom for Date { + type Error = (); + + fn try_from(value: DateAttribute) -> Result { + time::parsing::Parsed::new() + .with_year(i32::from(value.year()) + START_YEAR) + .and_then(|parsed| parsed.with_month(value.month().try_into().ok()?)) + .and_then(|parsed| parsed.with_day(num::NonZeroU8::new(value.day())?)) + .map(|parsed| parsed.try_into().ok()) + .flatten() + .ok_or(()) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +pub(crate) struct EntryCreationTime { + pub(crate) hundredths_of_second: u8, + pub(crate) time: TimeAttribute, + pub(crate) date: DateAttribute, +} + +impl TryFrom for PrimitiveDateTime { + type Error = (); + + fn try_from(value: EntryCreationTime) -> Result { + let mut time: Time = value.time.try_into()?; + + let new_seconds = time.second() + value.hundredths_of_second / 100; + let milliseconds = u16::from(value.hundredths_of_second) % 100 * 10; + time = time + .replace_second(new_seconds) + .map_err(|_| ())? + .replace_millisecond(milliseconds) + .map_err(|_| ())?; + + let date: Date = value.date.try_into()?; + + Ok(PrimitiveDateTime::new(date, time)) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +pub(crate) struct EntryModificationTime { + pub(crate) time: TimeAttribute, + pub(crate) date: DateAttribute, +} + +impl TryFrom for PrimitiveDateTime { + type Error = (); + + fn try_from(value: EntryModificationTime) -> Result { + Ok(PrimitiveDateTime::new( + value.date.try_into()?, + value.time.try_into()?, + )) + } +} + +// a directory entry occupies 32 bytes +pub(crate) const DIRENTRY_SIZE: usize = 32; + +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +pub(crate) struct FATDirEntry { + pub(crate) sfn: SFN, + pub(crate) attributes: RawAttributes, + pub(crate) _reserved: [u8; 1], + pub(crate) created: EntryCreationTime, + pub(crate) accessed: DateAttribute, + pub(crate) cluster_high: u16, + pub(crate) modified: EntryModificationTime, + pub(crate) cluster_low: u16, + pub(crate) file_size: u32, +} + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct LFNEntry { + /// masked with 0x40 if this is the last entry + pub(crate) order: u8, + pub(crate) first_chars: [u8; 10], + /// Always equals 0x0F + pub(crate) _lfn_attribute: u8, + /// Both OSDev and the FAT specification say this is always 0 + pub(crate) _long_entry_type: u8, + /// If this doesn't match with the computed cksum, then the set of LFNs is considered corrupt + /// + /// A [`LFNEntry`] will be marked as corrupt even if it isn't, if the SFN is modifed by a legacy system, + /// since the new SFN's signature and the one on this field won't (probably) match + pub(crate) checksum: u8, + pub(crate) mid_chars: [u8; 12], + pub(crate) _zeroed: [u8; 2], + pub(crate) last_chars: [u8; 4], +} + +impl LFNEntry { + pub(crate) fn get_byte_slice(&self) -> [u16; 13] { + let mut slice = [0_u8; 13 * mem::size_of::()]; + + slice[..10].copy_from_slice(&self.first_chars); + slice[10..22].copy_from_slice(&self.mid_chars); + slice[22..].copy_from_slice(&self.last_chars); + + let mut out_slice = [0_u16; 13]; + for (i, chunk) in slice.chunks(mem::size_of::()).enumerate() { + out_slice[i] = u16::from_le_bytes(chunk.try_into().unwrap()); + } + + out_slice + } + + #[inline] + pub(crate) fn verify_signature(&self) -> bool { + self._long_entry_type == 0 && self._zeroed.iter().all(|v| *v == 0) + } +} + +/// The location of a [`FATDirEntry`] within a root directory sector +/// or a data region cluster +#[derive(Debug, Clone)] +pub(crate) enum EntryLocation { + /// Sector offset from the start of the root directory region (FAT12/16) + RootDirSector(u16), + /// Cluster offset from the start of the data region + DataCluster(u32), +} + +impl EntryLocation { + pub(crate) fn from_partition_sector(sector: u32, fs: &mut FileSystem) -> Self + where + S: Read + Write + Seek, + { + if sector < fs.first_data_sector() { + EntryLocation::RootDirSector((sector - fs.props.first_root_dir_sector as u32) as u16) + } else { + EntryLocation::DataCluster(fs.partition_sector_to_data_cluster(sector)) + } + } +} + +/// The location of a chain of [`FATDirEntry`] +#[derive(Debug)] +pub(crate) struct DirEntryChain { + /// the location of the first corresponding entry + pub(crate) location: EntryLocation, + /// the first entry's index/offset from the start of the sector + pub(crate) index: u32, + /// how many (contiguous) entries this entry chain has + pub(crate) len: u32, +} + +/// A resolved file/directory entry (for internal usage only) +#[derive(Debug)] +pub(crate) struct RawProperties { + pub(crate) name: String, + pub(crate) is_dir: bool, + pub(crate) attributes: RawAttributes, + pub(crate) created: PrimitiveDateTime, + pub(crate) modified: PrimitiveDateTime, + pub(crate) accessed: Date, + pub(crate) file_size: u32, + pub(crate) data_cluster: u32, + + pub(crate) chain_props: DirEntryChain, +} + +/// A thin wrapper for [`Properties`] represing a directory entry +#[derive(Debug)] +pub struct DirEntry { + pub(crate) entry: Properties, +} + +impl ops::Deref for DirEntry { + type Target = Properties; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.entry + } +} diff --git a/src/fs/file.rs b/src/fs/file.rs new file mode 100644 index 0000000..b63a17c --- /dev/null +++ b/src/fs/file.rs @@ -0,0 +1,604 @@ +use super::*; + +use core::{cmp, ops}; + +#[cfg(not(feature = "std"))] +use alloc::{string::String, vec::Vec}; + +use crate::error::{IOError, IOErrorKind}; +use crate::io::prelude::*; + +#[derive(Debug)] +pub(crate) struct FileProps { + pub(crate) entry: Properties, + /// the byte offset of the R/W pointer + pub(crate) offset: u64, + pub(crate) current_cluster: u32, +} + +/// A read-only file within a FAT filesystem +#[derive(Debug)] +pub struct ROFile<'a, S> +where + S: Read + Write + Seek, +{ + pub(crate) fs: &'a mut FileSystem, + pub(crate) props: FileProps, +} + +impl ops::Deref for ROFile<'_, S> +where + S: Read + Write + Seek, +{ + type Target = Properties; + + fn deref(&self) -> &Self::Target { + &self.props.entry + } +} + +impl ops::DerefMut for ROFile<'_, S> +where + S: Read + Write + Seek, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.props.entry + } +} + +impl IOBase for ROFile<'_, S> +where + S: Read + Write + Seek, +{ + type Error = S::Error; +} + +/// A read-write file within a FAT filesystem +/// +/// The size of the file will be automatically adjusted +/// if the cursor goes beyond EOF. +/// +/// To reduce a file's size, use the [`truncate`](RWFile::truncate) method +#[derive(Debug)] +pub struct RWFile<'a, S> +where + S: Read + Write + Seek, +{ + pub(crate) ro_file: ROFile<'a, S>, +} + +impl<'a, S> ops::Deref for RWFile<'a, S> +where + S: Read + Write + Seek, +{ + type Target = ROFile<'a, S>; + + fn deref(&self) -> &Self::Target { + &self.ro_file + } +} + +impl ops::DerefMut for RWFile<'_, S> +where + S: Read + Write + Seek, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.ro_file + } +} + +impl IOBase for RWFile<'_, S> +where + S: Read + Write + Seek, +{ + type Error = S::Error; +} + +// Public functions +impl RWFile<'_, S> +where + S: Read + Write + Seek, +{ + /// Truncates the file to a given size, deleting everything past the new EOF + /// + /// If `size` is greater or equal to the current file size + /// till the end of the last cluster allocated, this has no effect + /// + /// Furthermore, if the cursor point is beyond the new EOF, it will be moved there + pub fn truncate(&mut self, size: u32) -> Result<(), ::Error> { + // looks like the new truncated size would be smaller than the current one, so we just return + if size.next_multiple_of(self.fs.props.cluster_size as u32) >= self.file_size { + if size < self.file_size { + self.file_size = size; + } + + return Ok(()); + } + + // we store the current offset for later use + let previous_offset = cmp::min(self.props.offset, size.into()); + + // we seek back to where the EOF will be + self.seek(SeekFrom::Start(size.into()))?; + + // set what the new filesize will be + let previous_size = self.file_size; + self.file_size = size; + + let mut next_cluster_option = self.get_next_cluster()?; + + // we set the new last cluster in the chain to be EOF + self.ro_file + .fs + .write_nth_FAT_entry(self.ro_file.props.current_cluster, FATEntry::EOF)?; + + // then, we set each cluster after the current one to EOF + while let Some(next_cluster) = next_cluster_option { + next_cluster_option = self.fs.get_next_cluster(next_cluster)?; + + self.fs.write_nth_FAT_entry(next_cluster, FATEntry::Free)?; + } + + // don't forget to seek back to where we started + self.seek(SeekFrom::Start(previous_offset))?; + + log::debug!( + "Successfully truncated file {} from {} to {} bytes", + self.path, + previous_size, + self.file_size + ); + + Ok(()) + } + + /// Remove the current file from the [`FileSystem`] + pub fn remove(mut self) -> Result<(), ::Error> { + // we begin by removing the corresponding entries... + let mut entries_freed = 0; + let mut current_offset = self.props.entry.chain_props.index; + + // current_cluster_option is `None` if we are dealing with a root directory entry + let (mut current_sector, current_cluster_option): (u32, Option) = + match self.props.entry.chain_props.location { + EntryLocation::RootDirSector(root_dir_sector) => ( + (root_dir_sector + self.fs.props.first_root_dir_sector).into(), + None, + ), + EntryLocation::DataCluster(data_cluster) => ( + self.fs.data_cluster_to_partition_sector(data_cluster), + Some(data_cluster), + ), + }; + + while entries_freed < self.props.entry.chain_props.len { + if current_sector as u64 != self.fs.stored_sector { + self.fs.read_nth_sector(current_sector.into())?; + } + + // we won't even bother zeroing the entire thing, just the first byte + let byte_offset = current_offset as usize * DIRENTRY_SIZE; + self.fs.sector_buffer[byte_offset] = UNUSED_ENTRY; + self.fs.buffer_modified = true; + + log::trace!( + "freed entry at sector {} with byte offset {}", + current_sector, + byte_offset + ); + + if current_offset + 1 >= (self.fs.sector_size() / DIRENTRY_SIZE as u32) { + // we have moved to a new sector + current_sector += 1; + + match current_cluster_option { + // data region + Some(mut current_cluster) => { + if self.fs.partition_sector_to_data_cluster(current_sector) + != current_cluster + { + current_cluster = self.fs.get_next_cluster(current_cluster)?.unwrap(); + current_sector = + self.fs.data_cluster_to_partition_sector(current_cluster); + } + } + None => (), + } + + current_offset = 0; + } else { + current_offset += 1 + } + + entries_freed += 1; + } + + // ... and then we free the data clusters + + // rewind back to the start of the file + self.rewind()?; + + loop { + let current_cluster = self.props.current_cluster; + let next_cluster_option = self.get_next_cluster()?; + + // free the current cluster + self.fs + .write_nth_FAT_entry(current_cluster, FATEntry::Free)?; + + // proceed to the next one, otherwise break + match next_cluster_option { + Some(next_cluster) => self.props.current_cluster = next_cluster, + None => break, + } + } + + Ok(()) + } +} + +// Internal functions +impl ROFile<'_, S> +where + S: Read + Write + Seek, +{ + #[inline] + /// Panics if the current cluser doesn't point to another clluster + fn next_cluster(&mut self) -> Result<(), ::Error> { + // when a `ROFile` is created, `cluster_chain_is_healthy` is called, if it fails, that ROFile is dropped + self.props.current_cluster = self.get_next_cluster()?.unwrap(); + + Ok(()) + } + + #[inline] + /// Non-[`panic`]king version of [`next_cluster()`](ROFile::next_cluster) + fn get_next_cluster(&mut self) -> Result, ::Error> { + Ok(self.fs.get_next_cluster(self.props.current_cluster)?) + } + + /// Returns that last cluster in the file's cluster chain + fn last_cluster_in_chain(&mut self) -> Result::Error> { + // we begin from the current cluster to save some time + let mut current_cluster = self.props.current_cluster; + + loop { + match self.fs.read_nth_FAT_entry(current_cluster)? { + FATEntry::Allocated(next_cluster) => current_cluster = next_cluster, + FATEntry::EOF => break, + _ => unreachable!(), + } + } + + Ok(current_cluster) + } + + /// Checks whether the cluster chain of this file is healthy or malformed + pub(crate) fn cluster_chain_is_healthy(&mut self) -> Result { + let mut current_cluster = self.data_cluster; + let mut cluster_count = 0; + + loop { + cluster_count += 1; + + if cluster_count * self.fs.cluster_size() >= self.file_size.into() { + break; + } + + match self.fs.read_nth_FAT_entry(current_cluster)? { + FATEntry::Allocated(next_cluster) => current_cluster = next_cluster, + _ => return Ok(false), + }; + } + + Ok(true) + } + + fn offset_from_seekfrom(&self, seekfrom: SeekFrom) -> u64 { + match seekfrom { + SeekFrom::Start(offset) => offset, + SeekFrom::Current(offset) => { + let offset = self.props.offset as i64 + offset; + offset.try_into().unwrap_or(u64::MIN) + } + SeekFrom::End(offset) => { + let offset = self.file_size as i64 + offset; + offset.try_into().unwrap_or(u64::MIN) + } + } + } +} + +impl Read for ROFile<'_, S> +where + S: Read + Write + Seek, +{ + fn read(&mut self, buf: &mut [u8]) -> Result { + let mut bytes_read = 0; + // this is the maximum amount of bytes that can be read + let read_cap = cmp::min( + buf.len(), + self.file_size as usize - self.props.offset as usize, + ); + + 'outer: loop { + let sector_init_offset = u32::try_from(self.props.offset % self.fs.cluster_size()) + .unwrap() + / self.fs.sector_size(); + let first_sector_of_cluster = self + .fs + .data_cluster_to_partition_sector(self.props.current_cluster) + + sector_init_offset; + let last_sector_of_cluster = first_sector_of_cluster + + self.fs.sectors_per_cluster() as u32 + - sector_init_offset + - 1; + log::debug!( + "Reading cluster {} from sectors {} to {}", + self.props.current_cluster, + first_sector_of_cluster, + last_sector_of_cluster + ); + + for sector in first_sector_of_cluster..=last_sector_of_cluster { + self.fs.read_nth_sector(sector.into())?; + + let start_index = self.props.offset as usize % self.fs.sector_size() as usize; + let bytes_to_read = cmp::min( + read_cap - bytes_read, + self.fs.sector_size() as usize - start_index, + ); + log::debug!( + "Gonna read {} bytes from sector {} starting at byte {}", + bytes_to_read, + sector, + start_index + ); + + buf[bytes_read..bytes_read + bytes_to_read].copy_from_slice( + &self.fs.sector_buffer[start_index..start_index + bytes_to_read], + ); + + bytes_read += bytes_to_read; + self.props.offset += bytes_to_read as u64; + + // if we have read as many bytes as we want... + if bytes_read >= read_cap { + // ...but we must process get the next cluster for future uses, + // we do that before breaking + if self.props.offset % self.fs.cluster_size() == 0 + && self.props.offset < self.file_size.into() + { + self.next_cluster()?; + } + + break 'outer; + } + } + + self.next_cluster()?; + } + + Ok(bytes_read) + } + + // the default `read_to_end` implementation isn't efficient enough, so we just do this + fn read_to_end(&mut self, buf: &mut Vec) -> Result { + let bytes_to_read = self.file_size as usize - self.props.offset as usize; + let init_buf_len = buf.len(); + + // resize buffer to fit the file contents exactly + buf.resize(init_buf_len + bytes_to_read, 0); + + // this is guaranteed not to raise an EOF (although other error kinds might be raised...) + self.read_exact(&mut buf[init_buf_len..])?; + + Ok(bytes_to_read) + } +} +impl Read for RWFile<'_, S> +where + S: Read + Write + Seek, +{ + #[inline] + fn read(&mut self, buf: &mut [u8]) -> Result { + self.ro_file.read(buf) + } + + #[inline] + fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Self::Error> { + self.ro_file.read_exact(buf) + } + + #[inline] + fn read_to_end(&mut self, buf: &mut Vec) -> Result { + self.ro_file.read_to_end(buf) + } + + #[inline] + fn read_to_string(&mut self, string: &mut String) -> Result { + self.ro_file.read_to_string(string) + } +} + +impl Write for RWFile<'_, S> +where + S: Read + Write + Seek, +{ + fn write(&mut self, buf: &[u8]) -> Result { + // allocate clusters + self.seek(SeekFrom::Current(buf.len() as i64))?; + // rewind back to where we were + self.seek(SeekFrom::Current(-(buf.len() as i64)))?; + + let mut bytes_written = 0; + + 'outer: loop { + log::trace!( + "writing file data to cluster: {}", + self.props.current_cluster + ); + + let sector_init_offset = u32::try_from(self.props.offset % self.fs.cluster_size()) + .unwrap() + / self.fs.sector_size(); + let first_sector_of_cluster = self + .fs + .data_cluster_to_partition_sector(self.props.current_cluster) + + sector_init_offset; + let last_sector_of_cluster = first_sector_of_cluster + + self.fs.sectors_per_cluster() as u32 + - sector_init_offset + - 1; + for sector in first_sector_of_cluster..=last_sector_of_cluster { + self.fs.read_nth_sector(sector.into())?; + + let start_index = self.props.offset as usize % self.fs.sector_size() as usize; + + let bytes_to_write = cmp::min( + buf.len() - bytes_written, + self.fs.sector_size() as usize - start_index, + ); + + self.fs.sector_buffer[start_index..start_index + bytes_to_write] + .copy_from_slice(&buf[bytes_written..bytes_written + bytes_to_write]); + self.fs.buffer_modified = true; + + bytes_written += bytes_to_write; + self.props.offset += bytes_to_write as u64; + + // if we have written as many bytes as we want... + if bytes_written >= buf.len() { + // ...but we must process get the next cluster for future uses, + // we do that before breaking + if self.props.offset % self.fs.cluster_size() == 0 { + self.next_cluster()?; + } + + break 'outer; + } + } + + self.next_cluster()?; + } + + Ok(bytes_written) + } + + // everything is immediately written to the storage medium + fn flush(&mut self) -> Result<(), Self::Error> { + Ok(()) + } +} + +impl Seek for ROFile<'_, S> +where + S: Read + Write + Seek, +{ + fn seek(&mut self, pos: SeekFrom) -> Result { + let offset = self.offset_from_seekfrom(pos); + + // in case the cursor goes beyond the EOF, allocate more clusters + if offset > (self.file_size as u64).next_multiple_of(self.fs.cluster_size()) { + return Err(IOError::new( + ::Kind::new_unexpected_eof(), + "moved past eof in a RO file", + )); + } + + log::trace!( + "Previous cursor offset is {}, new cursor offset is {}", + self.props.offset, + offset + ); + + use cmp::Ordering; + match offset.cmp(&self.props.offset) { + Ordering::Less => { + // here, we basically "rewind" back to the start of the file and then seek to where we want + // this of course has performance issues, so TODO: find a solution that is both memory & time efficient + // (perhaps we could follow a similar approach to elm-chan's FATFS, by using a cluster link map table, perhaps as an optional feature) + self.props.offset = 0; + self.props.current_cluster = self.data_cluster; + self.seek(SeekFrom::Start(offset))?; + } + Ordering::Equal => (), + Ordering::Greater => { + for _ in self.props.offset / self.fs.cluster_size()..offset / self.fs.cluster_size() + { + self.next_cluster()?; + } + self.props.offset = offset; + } + } + + Ok(self.props.offset) + } +} + +impl Seek for RWFile<'_, S> +where + S: Read + Write + Seek, +{ + fn seek(&mut self, pos: SeekFrom) -> Result { + let offset = self.offset_from_seekfrom(pos); + + // in case the cursor goes beyond the EOF, allocate more clusters + if offset > (self.file_size as u64).next_multiple_of(self.fs.cluster_size()) { + let clusters_to_allocate = (offset + - (self.file_size as u64).next_multiple_of(self.fs.cluster_size())) + .div_ceil(self.fs.cluster_size()) + + 1; + log::debug!( + "Seeking beyond EOF, allocating {} more clusters", + clusters_to_allocate + ); + + let mut last_cluster_in_chain = self.last_cluster_in_chain()?; + + for clusters_allocated in 0..clusters_to_allocate { + match self.fs.next_free_cluster()? { + Some(next_free_cluster) => { + // we set the last allocated cluster to point to the next free one + self.fs.write_nth_FAT_entry( + last_cluster_in_chain, + FATEntry::Allocated(next_free_cluster), + )?; + // we also set the next free cluster to be EOF + self.fs + .write_nth_FAT_entry(next_free_cluster, FATEntry::EOF)?; + log::trace!( + "cluster {} now points to {}", + last_cluster_in_chain, + next_free_cluster + ); + // now the next free cluster i the last allocated one + last_cluster_in_chain = next_free_cluster; + } + None => { + self.file_size = (((self.file_size as u64) + .next_multiple_of(self.fs.cluster_size()) + - offset) + + clusters_allocated * self.fs.cluster_size()) + as u32; + self.props.offset = self.file_size.into(); + + log::error!("storage medium full while attempting to allocate more clusters for a ROFile"); + return Err(IOError::new( + ::Kind::new_unexpected_eof(), + "the storage medium is full, can't increase size of file", + )); + } + } + } + + self.file_size = offset as u32; + log::debug!( + "New file size after reallocation is {} bytes", + self.file_size + ); + } + + self.ro_file.seek(pos) + } +} diff --git a/src/fs/fs.rs b/src/fs/fs.rs new file mode 100644 index 0000000..9b74585 --- /dev/null +++ b/src/fs/fs.rs @@ -0,0 +1,1140 @@ +use super::*; + +use crate::{error::*, io::prelude::*, path::PathBuf, utils}; + +use core::{cmp, ops}; + +#[cfg(not(feature = "std"))] +use alloc::{ + format, + string::{String, ToString}, + vec, + vec::Vec, +}; + +use bincode::Options as _; + +use ::time; +use time::{Date, PrimitiveDateTime}; + +/// An enum representing different versions of the FAT filesystem +#[derive(Debug, Clone, Copy, PartialEq)] +// no need for enum variant documentation here +#[allow(missing_docs)] +pub enum FATType { + FAT12, + FAT16, + FAT32, + ExFAT, +} + +impl FATType { + #[inline] + /// How many bits this [`FATType`] uses to address clusters in the disk + fn bits_per_entry(&self) -> u8 { + match self { + FATType::FAT12 => 12, + FATType::FAT16 => 16, + // the high 4 bits are ignored, but are still part of the entry + FATType::FAT32 => 32, + FATType::ExFAT => 32, + } + } + + #[inline] + /// How many bytes this [`FATType`] spans across + fn entry_size(&self) -> u32 { + self.bits_per_entry().next_power_of_two() as u32 / 8 + } +} + +// the first 2 entries are reserved +const RESERVED_FAT_ENTRIES: u32 = 2; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum FATEntry { + /// This cluster is free + Free, + /// This cluster is allocated and the next cluster is the contained value + Allocated(u32), + /// This cluster is reserved + Reserved, + /// This is a bad (defective) cluster + Bad, + /// This cluster is allocated and is the final cluster of the file + EOF, +} + +impl From for u32 { + fn from(value: FATEntry) -> Self { + Self::from(&value) + } +} + +impl From<&FATEntry> for u32 { + fn from(value: &FATEntry) -> Self { + match value { + FATEntry::Free => u32::MIN, + FATEntry::Allocated(cluster) => *cluster, + FATEntry::Reserved => 0xFFFFFF6, + FATEntry::Bad => 0xFFFFFF7, + FATEntry::EOF => u32::MAX, + } + } +} + +/// A container for file/directory properties +#[derive(Debug)] +pub struct Properties { + pub(crate) path: PathBuf, + pub(crate) attributes: Attributes, + pub(crate) created: PrimitiveDateTime, + pub(crate) modified: PrimitiveDateTime, + pub(crate) accessed: Date, + pub(crate) file_size: u32, + pub(crate) data_cluster: u32, + + // internal fields + pub(crate) chain_props: DirEntryChain, +} + +/// Getter methods +impl Properties { + #[inline] + /// Get the corresponding [`PathBuf`] to this entry + pub fn path(&self) -> &PathBuf { + &self.path + } + + #[inline] + /// Get the corresponding [`Attributes`] to this entry + pub fn attributes(&self) -> &Attributes { + &self.attributes + } + + #[inline] + /// Find out when this entry was created (max resolution: 1ms) + /// + /// Returns a [`PrimitiveDateTime`] from the [`time`] crate + pub fn creation_time(&self) -> &PrimitiveDateTime { + &self.created + } + + #[inline] + /// Find out when this entry was last modified (max resolution: 2 secs) + /// + /// Returns a [`PrimitiveDateTime`] from the [`time`] crate + pub fn modification_time(&self) -> &PrimitiveDateTime { + &self.modified + } + + #[inline] + /// Find out when this entry was last accessed (max resolution: 1 day) + /// + /// Returns a [`Date`] from the [`time`] crate + pub fn last_accessed_date(&self) -> &Date { + &self.accessed + } + + #[inline] + /// Find out the size of this entry + /// + /// Always returns `0` for directories + pub fn file_size(&self) -> u32 { + self.file_size + } +} + +/// Serialization methods +impl Properties { + #[inline] + fn from_raw(raw: RawProperties, path: PathBuf) -> Self { + Properties { + path, + attributes: raw.attributes.into(), + created: raw.created, + modified: raw.modified, + accessed: raw.accessed, + file_size: raw.file_size, + data_cluster: raw.data_cluster, + chain_props: raw.chain_props, + } + } +} + +pub(crate) trait OffsetConversions { + fn sector_size(&self) -> u32; + fn cluster_size(&self) -> u64; + fn first_data_sector(&self) -> u32; + + #[inline] + fn cluster_to_sector(&self, cluster: u64) -> u32 { + (cluster * self.cluster_size() / self.sector_size() as u64) + .try_into() + .unwrap() + } + + #[inline] + fn sectors_per_cluster(&self) -> u64 { + self.cluster_size() / self.sector_size() as u64 + } + + #[inline] + fn sector_to_partition_offset(&self, sector: u32) -> u32 { + sector * self.sector_size() + } + + #[inline] + fn data_cluster_to_partition_sector(&self, cluster: u32) -> u32 { + self.cluster_to_sector((cluster - RESERVED_FAT_ENTRIES).into()) + self.first_data_sector() + } + + #[inline] + fn partition_sector_to_data_cluster(&self, sector: u32) -> u32 { + (sector - self.first_data_sector()) / self.sectors_per_cluster() as u32 + + RESERVED_FAT_ENTRIES + } +} + +/// Some generic properties common across all FAT versions, like a sector's size, are cached here +#[derive(Debug)] +pub(crate) struct FSProperties { + pub(crate) sector_size: u32, + pub(crate) cluster_size: u64, + pub(crate) total_sectors: u32, + pub(crate) total_clusters: u32, + /// sector offset of the FAT + pub(crate) fat_table_count: u8, + pub(crate) first_root_dir_sector: u16, + pub(crate) first_data_sector: u32, +} + +/// Filter (or not) things like hidden files/directories +/// for FileSystem operations +#[derive(Debug)] +struct FileFilter { + show_hidden: bool, + show_systen: bool, +} + +impl FileFilter { + fn filter(&self, item: &RawProperties) -> bool { + let is_hidden = item.attributes.contains(RawAttributes::HIDDEN); + let is_system = item.attributes.contains(RawAttributes::SYSTEM); + let should_filter = !self.show_hidden && is_hidden || !self.show_systen && is_system; + + !should_filter + } +} + +impl Default for FileFilter { + fn default() -> Self { + // The FAT spec says to filter everything by default + FileFilter { + show_hidden: false, + show_systen: false, + } + } +} + +/// An API to process a FAT filesystem +#[derive(Debug)] +pub struct FileSystem +where + S: Read + Write + Seek, +{ + /// Any struct that implements the [`Read`], [`Write`] & [`Seek`] traits + storage: S, + + /// The length of this will be the sector size of the FS for all FAT types except FAT12, in that case, it will be double that value + pub(crate) sector_buffer: Vec, + /// ANY CHANGES TO THE SECTOR BUFFER SHOULD ALSO SET THIS TO TRUE + pub(crate) buffer_modified: bool, + pub(crate) stored_sector: u64, + + pub(crate) boot_record: BootRecord, + // since `self.boot_record.fat_type()` calls like 5 nested functions, we keep this cached and expose it with a public getter function + fat_type: FATType, + pub(crate) props: FSProperties, + + filter: FileFilter, +} + +impl OffsetConversions for FileSystem +where + S: Read + Write + Seek, +{ + #[inline] + fn sector_size(&self) -> u32 { + self.props.sector_size + } + + #[inline] + fn cluster_size(&self) -> u64 { + self.props.cluster_size + } + + #[inline] + fn first_data_sector(&self) -> u32 { + self.props.first_data_sector + } +} + +/// Getter functions +impl FileSystem +where + S: Read + Write + Seek, +{ + /// What is the [`FATType`] of the filesystem + pub fn fat_type(&self) -> FATType { + self.fat_type + } +} + +/// Setter functions +impl FileSystem +where + S: Read + Write + Seek, +{ + /// Whether or not to list hidden files + /// + /// Off by default + #[inline] + pub fn show_hidden(&mut self, show: bool) { + self.filter.show_hidden = show; + } + + /// Whether or not to list system files + /// + /// Off by default + #[inline] + pub fn show_system(&mut self, show: bool) { + self.filter.show_systen = show; + } +} + +/// Constructors +impl FileSystem +where + S: Read + Write + Seek, +{ + /// Create a [`FileSystem`] from a storage object that implements [`Read`], [`Write`] & [`Seek`] + /// + /// Fails if the storage is way too small to support a FAT filesystem. + /// For most use cases, that shouldn't be an issue, you can just call [`.unwrap()`](Result::unwrap) + pub fn from_storage(mut storage: S) -> FSResult { + use utils::bincode::bincode_config; + + // Begin by reading the boot record + // We don't know the sector size yet, so we just go with the biggest possible one for now + let mut buffer = [0u8; SECTOR_SIZE_MAX]; + + let bytes_read = storage.read(&mut buffer)?; + let mut stored_sector = 0; + + if bytes_read < 512 { + return Err(FSError::InternalFSError(InternalFSError::StorageTooSmall)); + } + + let bpb: BPBFAT = bincode_config().deserialize(&buffer[..BPBFAT_SIZE])?; + + let ebr = if bpb.table_size_16 == 0 { + let ebr_fat32 = bincode_config() + .deserialize::(&buffer[BPBFAT_SIZE..BPBFAT_SIZE + EBR_SIZE])?; + + storage.seek(SeekFrom::Start( + ebr_fat32.fat_info as u64 * bpb.bytes_per_sector as u64, + ))?; + stored_sector = ebr_fat32.fat_info.into(); + storage.read_exact(&mut buffer[..bpb.bytes_per_sector as usize])?; + let fsinfo = bincode_config() + .deserialize::(&buffer[..bpb.bytes_per_sector as usize])?; + + if !fsinfo.verify_signature() { + log::error!("FAT32 FSInfo has invalid signature(s)"); + return Err(FSError::InternalFSError(InternalFSError::InvalidFSInfoSig)); + } + + EBR::FAT32(ebr_fat32, fsinfo) + } else { + EBR::FAT12_16( + bincode_config() + .deserialize::(&buffer[BPBFAT_SIZE..BPBFAT_SIZE + EBR_SIZE])?, + ) + }; + + // TODO: see how we will handle this for exfat + let boot_record = BootRecord::FAT(BootRecordFAT { bpb, ebr }); + + // verify boot record signature + let fat_type = boot_record.fat_type(); + log::info!("The FAT type of the filesystem is {:?}", fat_type); + + match boot_record { + BootRecord::FAT(boot_record_fat) => { + if boot_record_fat.verify_signature() { + log::error!("FAT boot record has invalid signature(s)"); + return Err(FSError::InternalFSError(InternalFSError::InvalidBPBSig)); + } + } + BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT not yet implemented"), + }; + + let sector_size: u32 = match boot_record { + BootRecord::FAT(boot_record_fat) => boot_record_fat.bpb.bytes_per_sector.into(), + BootRecord::ExFAT(boot_record_exfat) => 1 << boot_record_exfat.sector_shift, + }; + let cluster_size: u64 = match boot_record { + BootRecord::FAT(boot_record_fat) => { + (boot_record_fat.bpb.sectors_per_cluster as u32 * sector_size).into() + } + BootRecord::ExFAT(boot_record_exfat) => { + 1 << (boot_record_exfat.sector_shift + boot_record_exfat.cluster_shift) + } + }; + + let first_root_dir_sector = match boot_record { + BootRecord::FAT(boot_record_fat) => boot_record_fat.first_root_dir_sector().into(), + BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), + }; + + let first_data_sector = match boot_record { + BootRecord::FAT(boot_record_fat) => boot_record_fat.first_data_sector().into(), + BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), + }; + + let fat_table_count = match boot_record { + BootRecord::FAT(boot_record_fat) => boot_record_fat.bpb.table_count, + BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), + }; + + let total_sectors = match boot_record { + BootRecord::FAT(boot_record_fat) => boot_record_fat.total_sectors(), + BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), + }; + + let total_clusters = match boot_record { + BootRecord::FAT(boot_record_fat) => boot_record_fat.total_clusters(), + BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), + }; + + let props = FSProperties { + sector_size, + cluster_size, + fat_table_count, + total_sectors, + total_clusters, + first_root_dir_sector, + first_data_sector, + }; + + let mut fs = Self { + storage, + sector_buffer: buffer[..sector_size as usize].to_vec(), + buffer_modified: false, + stored_sector, + boot_record, + fat_type, + props, + filter: FileFilter::default(), + }; + + if !fs.FAT_tables_are_identical()? { + return Err(FSError::InternalFSError( + InternalFSError::MismatchingFATTables, + )); + } + + Ok(fs) + } +} + +#[derive(Debug)] +struct EntryParser { + entries: Vec, + lfn_buf: Vec, + lfn_checksum: Option, + current_chain: Option, +} + +impl Default for EntryParser { + fn default() -> Self { + EntryParser { + entries: Vec::new(), + lfn_buf: Vec::new(), + lfn_checksum: None, + current_chain: None, + } + } +} + +pub(crate) const UNUSED_ENTRY: u8 = 0xE5; +pub(crate) const LAST_AND_UNUSED_ENTRY: u8 = 0x00; + +impl EntryParser { + #[inline] + fn _decrement_parsed_entries_counter(&mut self) { + if let Some(current_chain) = &mut self.current_chain { + current_chain.len -= 1 + } + } + + /// Parses a sector of 8.3 & LFN entries + /// + /// Returns a [`Result`] indicating whether or not + /// this sector was the last one in the chain containing entries + fn parse_sector( + &mut self, + sector: u32, + fs: &mut FileSystem, + ) -> Result::Error> + where + S: Read + Write + Seek, + { + use utils::bincode::bincode_config; + + let entry_location = EntryLocation::from_partition_sector(sector, fs); + + for (index, chunk) in fs + .read_nth_sector(sector.into())? + .chunks(DIRENTRY_SIZE) + .enumerate() + { + match chunk[0] { + LAST_AND_UNUSED_ENTRY => return Ok(true), + UNUSED_ENTRY => continue, + _ => (), + }; + + let Ok(entry) = bincode_config().deserialize::(&chunk) else { + continue; + }; + + // update current entry chain data + match &mut self.current_chain { + Some(current_chain) => current_chain.len += 1, + None => { + self.current_chain = Some(DirEntryChain { + location: entry_location.clone(), + index: index as u32, + len: 1, + }) + } + } + + if entry.attributes.contains(RawAttributes::LFN) { + // TODO: perhaps there is a way to utilize the `order` field? + let Ok(lfn_entry) = bincode_config().deserialize::(&chunk) else { + self._decrement_parsed_entries_counter(); + continue; + }; + + // If the signature verification fails, consider this entry corrupted + if !lfn_entry.verify_signature() { + self._decrement_parsed_entries_counter(); + continue; + } + + match self.lfn_checksum { + Some(checksum) => { + if checksum != lfn_entry.checksum { + self.lfn_checksum = None; + self.lfn_buf.clear(); + self.current_chain = None; + continue; + } + } + None => self.lfn_checksum = Some(lfn_entry.checksum), + } + + let char_arr = lfn_entry.get_byte_slice().to_vec(); + if let Ok(temp_str) = utils::string::string_from_lfn(&char_arr) { + self.lfn_buf.push(temp_str); + } + + continue; + } + + let filename = if !self.lfn_buf.is_empty() + && self + .lfn_checksum + .is_some_and(|checksum| checksum == entry.sfn.gen_checksum()) + { + // for efficiency reasons, we store the LFN string sequences as we read them + let parsed_str: String = self.lfn_buf.iter().cloned().rev().collect(); + self.lfn_buf.clear(); + self.lfn_checksum = None; + parsed_str + } else { + entry.sfn.to_string() + }; + + if let (Ok(created), Ok(modified), Ok(accessed)) = ( + entry.created.try_into(), + entry.modified.try_into(), + entry.accessed.try_into(), + ) { + self.entries.push(RawProperties { + name: filename, + is_dir: entry.attributes.contains(RawAttributes::DIRECTORY), + attributes: entry.attributes, + created, + modified, + accessed, + file_size: entry.file_size, + data_cluster: ((entry.cluster_high as u32) << 16) + entry.cluster_low as u32, + chain_props: self + .current_chain + .take() + .expect("at this point, this shouldn't be None"), + }) + } + } + + Ok(false) + } + + /// Consumes [`Self`](EntryParser) & returns a `Vec` of [`RawProperties`] + /// of the parsed entries + fn finish(self) -> Vec { + self.entries + } +} + +/// Internal [`Read`]-related low-level functions +impl FileSystem +where + S: Read + Write + Seek, +{ + fn process_root_dir(&mut self) -> FSResult, S::Error> { + match self.boot_record { + BootRecord::FAT(boot_record_fat) => match boot_record_fat.ebr { + EBR::FAT12_16(_ebr_fat12_16) => { + let mut entry_parser = EntryParser::default(); + + let root_dir_sector = boot_record_fat.first_root_dir_sector(); + let sector_count = boot_record_fat.root_dir_sectors(); + + for sector in root_dir_sector..(root_dir_sector + sector_count) { + if entry_parser.parse_sector(sector.into(), self)? { + break; + } + } + + Ok(entry_parser.finish()) + } + EBR::FAT32(ebr_fat32, _) => { + let cluster = ebr_fat32.root_cluster; + self.process_normal_dir(cluster) + } + }, + BootRecord::ExFAT(_boot_record_exfat) => todo!(), + } + } + + fn process_normal_dir( + &mut self, + mut data_cluster: u32, + ) -> FSResult, S::Error> { + let mut entry_parser = EntryParser::default(); + + 'outer: loop { + // FAT specification, section 6.7 + let first_sector_of_cluster = self.data_cluster_to_partition_sector(data_cluster); + for sector in first_sector_of_cluster + ..(first_sector_of_cluster + self.sectors_per_cluster() as u32) + { + if entry_parser.parse_sector(sector.into(), self)? { + break 'outer; + } + } + + // Read corresponding FAT entry + let current_fat_entry = self.read_nth_FAT_entry(data_cluster)?; + + match current_fat_entry { + // we are done here, break the loop + FATEntry::EOF => break, + // this cluster chain goes on, follow it + FATEntry::Allocated(next_cluster) => data_cluster = next_cluster, + // any other case (whether a bad, reserved or free cluster) is invalid, consider this cluster chain malformed + _ => { + log::error!("Cluster chain of directory is malformed"); + return Err(FSError::InternalFSError( + InternalFSError::MalformedClusterChain, + )); + } + } + } + + Ok(entry_parser.finish()) + } + + /// Gets the next free cluster. Returns an IO [`Result`] + /// If the [`Result`] returns [`Ok`] that contains a [`None`], the drive is full + pub(crate) fn next_free_cluster(&mut self) -> Result, S::Error> { + let start_cluster = match self.boot_record { + BootRecord::FAT(boot_record_fat) => { + let mut first_free_cluster = RESERVED_FAT_ENTRIES; + + if let EBR::FAT32(_, fsinfo) = boot_record_fat.ebr { + // a value of u32::MAX denotes unawareness of the first free cluster + // we also do a bit of range checking + // TODO: if this is unknown, figure it out and write it to the FSInfo structure + if fsinfo.first_free_cluster != u32::MAX + && fsinfo.first_free_cluster <= self.props.total_sectors + { + first_free_cluster = fsinfo.first_free_cluster + } + } + + first_free_cluster + } + BootRecord::ExFAT(_) => todo!("ExFAT not yet implemented"), + }; + + let mut current_cluster = start_cluster; + + while current_cluster < self.props.total_clusters { + match self.read_nth_FAT_entry(current_cluster)? { + FATEntry::Free => return Ok(Some(current_cluster)), + _ => (), + } + current_cluster += 1; + } + + Ok(None) + } + + /// Get the next cluster in a cluster chain, otherwise return [`None`] + pub(crate) fn get_next_cluster(&mut self, cluster: u32) -> Result, S::Error> { + Ok(match self.read_nth_FAT_entry(cluster)? { + FATEntry::Allocated(next_cluster) => Some(next_cluster), + // when a `ROFile` is created, `cluster_chain_is_healthy` is called, if it fails, that ROFile is dropped + _ => None, + }) + } + + #[allow(non_snake_case)] + /// Check whether or not the all the FAT tables of the storage medium are identical to each other + pub(crate) fn FAT_tables_are_identical(&mut self) -> Result { + // we could make it work, but we are only testing regular FAT filesystems (for now) + assert_ne!( + self.fat_type, + FATType::ExFAT, + "this function doesn't work with ExFAT" + ); + + /// How many bytes to probe at max for each FAT per iteration (must be a multiple of [`SECTOR_SIZE_MAX`]) + const MAX_PROBE_SIZE: u32 = 1 << 20; + + let fat_byte_size = match self.boot_record { + BootRecord::FAT(boot_record_fat) => boot_record_fat.fat_sector_size(), + BootRecord::ExFAT(_) => unreachable!(), + }; + + for nth_iteration in 0..fat_byte_size.div_ceil(MAX_PROBE_SIZE) { + let mut tables: Vec> = Vec::new(); + + for i in 0..self.props.fat_table_count { + let fat_start = + self.sector_to_partition_offset(self.boot_record.nth_FAT_table_sector(i)); + let current_offset = fat_start + nth_iteration * MAX_PROBE_SIZE; + let bytes_left = fat_byte_size - nth_iteration * MAX_PROBE_SIZE; + + self.storage.seek(SeekFrom::Start(current_offset.into()))?; + let mut buf = vec![0_u8; cmp::min(MAX_PROBE_SIZE, bytes_left) as usize]; + self.storage.read_exact(buf.as_mut_slice())?; + tables.push(buf); + } + + // we check each table with the first one (except the first one ofc) + if !tables.iter().skip(1).all(|buf| buf == &tables[0]) { + return Ok(false); + } + } + + Ok(true) + } + + /// Read the nth sector from the partition's beginning and store it in [`self.sector_buffer`](Self::sector_buffer) + /// + /// This function also returns an immutable reference to [`self.sector_buffer`](Self::sector_buffer) + pub(crate) fn read_nth_sector(&mut self, n: u64) -> Result<&Vec, S::Error> { + // nothing to do if the sector we wanna read is already cached + if n != self.stored_sector { + // let's sync the current sector first + self.sync_sector_buffer()?; + self.storage.seek(SeekFrom::Start( + self.sector_to_partition_offset(n as u32).into(), + ))?; + self.storage.read_exact(&mut self.sector_buffer)?; + self.storage + .seek(SeekFrom::Current(-i64::from(self.props.sector_size)))?; + + self.stored_sector = n; + } + + Ok(&self.sector_buffer) + } + + #[allow(non_snake_case)] + pub(crate) fn read_nth_FAT_entry(&mut self, n: u32) -> Result { + // the size of an entry rounded up to bytes + let entry_size = self.fat_type.entry_size(); + let entry_props = FATEntryProps::new(n, &self); + + self.read_nth_sector(entry_props.fat_sectors[0].into())?; + + let mut value_bytes = [0_u8; 4]; + let bytes_to_read: usize = cmp::min( + entry_props.sector_offset + entry_size as usize, + self.sector_size() as usize, + ) - entry_props.sector_offset; + value_bytes[..bytes_to_read].copy_from_slice( + &self.sector_buffer + [entry_props.sector_offset..entry_props.sector_offset + bytes_to_read], + ); // this shouldn't panic + + // in FAT12, FAT entries may be split between two different sectors + if self.fat_type == FATType::FAT12 && (bytes_to_read as u32) < entry_size { + self.read_nth_sector((entry_props.fat_sectors[0] + 1).into())?; + + value_bytes[bytes_to_read..entry_size as usize] + .copy_from_slice(&self.sector_buffer[..(entry_size as usize - bytes_to_read)]); + }; + + let mut value = u32::from_le_bytes(value_bytes); + match self.fat_type { + // FAT12 entries are split between different bytes + FATType::FAT12 => { + if n & 1 != 0 { + value >>= 4 + } else { + value &= 0xFFF + } + } + // ignore the high 4 bits if this is FAT32 + FATType::FAT32 => value &= 0x0FFFFFFF, + _ => (), + } + + /* + // pad unused bytes with 1s + let padding: u32 = u32::MAX.to_be() << self.fat_type.bits_per_entry(); + value |= padding.to_le(); + */ + + // TODO: perhaps byte padding can replace some redundant code here? + Ok(match self.fat_type { + FATType::FAT12 => match value { + 0x000 => FATEntry::Free, + 0xFF7 => FATEntry::Bad, + 0xFF8..=0xFFE | 0xFFF => FATEntry::EOF, + _ => { + if (0x002..(self.props.total_clusters + 1)).contains(&value.into()) { + FATEntry::Allocated(value.into()) + } else { + FATEntry::Reserved + } + } + }, + FATType::FAT16 => match value { + 0x0000 => FATEntry::Free, + 0xFFF7 => FATEntry::Bad, + 0xFFF8..=0xFFFE | 0xFFFF => FATEntry::EOF, + _ => { + if (0x0002..(self.props.total_clusters + 1)).contains(&value.into()) { + FATEntry::Allocated(value.into()) + } else { + FATEntry::Reserved + } + } + }, + FATType::FAT32 => match value { + 0x00000000 => FATEntry::Free, + 0x0FFFFFF7 => FATEntry::Bad, + 0x0FFFFFF8..=0xFFFFFFE | 0x0FFFFFFF => FATEntry::EOF, + _ => { + if (0x00000002..(self.props.total_clusters + 1)).contains(&value.into()) { + FATEntry::Allocated(value.into()) + } else { + FATEntry::Reserved + } + } + }, + FATType::ExFAT => todo!("ExFAT not yet implemented"), + }) + } +} + +/// Internal [`Write`]-related low-level functions +impl FileSystem +where + S: Read + Write + Seek, +{ + #[allow(non_snake_case)] + pub(crate) fn write_nth_FAT_entry(&mut self, n: u32, entry: FATEntry) -> Result<(), S::Error> { + // the size of an entry rounded up to bytes + let entry_size = self.fat_type.entry_size(); + let entry_props = FATEntryProps::new(n, &self); + + // the previous solution would overflow, here's a correct implementation + let mask = utils::bits::setbits_u32(self.fat_type.bits_per_entry()); + let mut value: u32 = u32::from(entry.clone()) & mask; + + if self.fat_type == FATType::FAT32 { + // in FAT32, the high 4 bits are unused + value &= 0x0FFFFFFF; + } + + match self.fat_type { + FATType::FAT12 => { + let should_shift = n & 1 != 0; + if should_shift { + // FAT12 entries are split between different bytes + value <<= 4; + } + + // we update all the FAT copies + for fat_sector in entry_props.fat_sectors { + self.read_nth_sector(fat_sector.into())?; + + let value_bytes = value.to_le_bytes(); + + let mut first_byte = value_bytes[0]; + + if should_shift { + let mut old_byte = self.sector_buffer[entry_props.sector_offset]; + // ignore the high 4 bytes of the old entry + old_byte &= 0x0F; + // OR it with the new value + first_byte |= old_byte; + } + + self.sector_buffer[entry_props.sector_offset] = first_byte; // this shouldn't panic + self.buffer_modified = true; + + let bytes_left_on_sector: usize = cmp::min( + entry_size as usize, + self.sector_size() as usize - entry_props.sector_offset, + ); + + if bytes_left_on_sector < entry_size as usize { + // looks like this FAT12 entry spans multiple sectors, we must also update the other one + self.read_nth_sector((fat_sector + 1).into())?; + } + + let mut second_byte = value_bytes[1]; + let second_byte_index = + (entry_props.sector_offset + 1) % self.sector_size() as usize; + if !should_shift { + let mut old_byte = self.sector_buffer[second_byte_index]; + // ignore the low 4 bytes of the old entry + old_byte &= 0xF0; + // OR it with the new value + second_byte |= old_byte; + } + + self.sector_buffer[second_byte_index] = second_byte; // this shouldn't panic + self.buffer_modified = true; + } + } + FATType::FAT16 | FATType::FAT32 => { + // we update all the FAT copies + for fat_sector in entry_props.fat_sectors { + self.read_nth_sector(fat_sector.into())?; + + let value_bytes = value.to_le_bytes(); + + self.sector_buffer[entry_props.sector_offset + ..entry_props.sector_offset + entry_size as usize] + .copy_from_slice(&value_bytes[..entry_size as usize]); // this shouldn't panic + self.buffer_modified = true; + } + } + FATType::ExFAT => todo!("ExFAT not yet implemented"), + }; + + Ok(()) + } + + pub(crate) fn sync_sector_buffer(&mut self) -> Result<(), S::Error> { + if self.buffer_modified { + log::trace!("syncing sector {:?}", self.stored_sector); + self.storage.write_all(&self.sector_buffer)?; + self.storage + .seek(SeekFrom::Current(-i64::from(self.props.sector_size)))?; + } + self.buffer_modified = false; + + Ok(()) + } +} + +/// Public [`Read`]-related functions +impl FileSystem +where + S: Read + Write + Seek, +{ + /// Read all the entries of a directory ([`PathBuf`]) into [`Vec`] + /// + /// Fails if `path` doesn't represent a directory, or if that directory doesn't exist + pub fn read_dir(&mut self, path: PathBuf) -> FSResult, S::Error> { + if path.is_malformed() { + return Err(FSError::MalformedPath); + } + if !path.is_dir() { + log::error!("Not a directory"); + return Err(FSError::NotADirectory); + } + + let mut entries = self.process_root_dir()?; + + for dir_name in path.clone().into_iter() { + let dir_cluster = match entries.iter().find(|entry| { + entry.name == dir_name && entry.attributes.contains(RawAttributes::DIRECTORY) + }) { + Some(entry) => entry.data_cluster, + None => { + log::error!("Directory {} not found", path); + return Err(FSError::NotFound); + } + }; + + entries = self.process_normal_dir(dir_cluster)?; + } + + // if we haven't returned by now, that means that the entries vector + // contains what we want, let's map it to DirEntries and return + Ok(entries + .into_iter() + .filter(|x| self.filter.filter(x)) + .map(|rawentry| { + let mut entry_path = path.clone(); + + entry_path.push(format!( + "{}{}", + rawentry.name, + if rawentry.is_dir { "/" } else { "" } + )); + DirEntry { + entry: Properties::from_raw(rawentry, entry_path), + } + }) + .collect()) + } + + /// Get a corresponding [`ROFile`] object from a [`PathBuf`] + /// + /// Borrows `&mut self` until that [`ROFile`] object is dropped, effectively locking `self` until that file closed + /// + /// Fails if `path` doesn't represent a file, or if that file doesn't exist + pub fn get_ro_file(&mut self, path: PathBuf) -> FSResult, S::Error> { + if path.is_malformed() { + return Err(FSError::MalformedPath); + } + + if let Some(file_name) = path.file_name() { + let parent_dir = self.read_dir(path.parent())?; + match parent_dir.into_iter().find(|direntry| { + direntry + .path() + .file_name() + .is_some_and(|entry_name| entry_name == file_name) + }) { + Some(direntry) => { + let mut file = ROFile { + fs: self, + props: FileProps { + offset: 0, + current_cluster: direntry.entry.data_cluster, + entry: direntry.entry, + }, + }; + + if file.cluster_chain_is_healthy()? { + Ok(file) + } else { + log::error!("The cluster chain of a file is malformed"); + Err(FSError::InternalFSError( + InternalFSError::MalformedClusterChain, + )) + } + } + None => { + log::error!("ROFile {} not found", path); + Err(FSError::NotFound) + } + } + } else { + log::error!("Is a directory (not a file)"); + Err(FSError::IsADirectory) + } + } +} + +/// [`Write`]-related functions +impl FileSystem +where + S: Read + Write + Seek, +{ + /// Get a corresponding [`RWFile`] object from a [`PathBuf`] + /// + /// Borrows `&mut self` until that [`RWFile`] object is dropped, effectively locking `self` until that file closed + /// + /// Fails if `path` doesn't represent a file, or if that file doesn't exist + pub fn get_rw_file(&mut self, path: PathBuf) -> FSResult, S::Error> { + // we first write an empty array to the storage medium + // if the storage has Write functionality, this shouldn't error, + // otherwise it should return an error. + self.storage.write_all(&[])?; + + let ro_file = self.get_ro_file(path)?; + if ro_file.attributes.read_only { + return Err(FSError::ReadOnlyFile); + }; + + Ok(RWFile { ro_file }) + } +} + +/// Properties about the position of a [`FATEntry`] inside the FAT region +struct FATEntryProps { + /// Each `n`th element of the vector points at the corrensponding sector at the `n+1`th FAT table + fat_sectors: Vec, + sector_offset: usize, +} + +impl FATEntryProps { + /// Get the [`FATEntryProps`] of the `n`-th [`FATEntry`] of a [`ROFileSystem`] (`fs`) + pub fn new(n: u32, fs: &FileSystem) -> Self + where + S: Read + Write + Seek, + { + let fat_byte_offset: u32 = n * fs.fat_type.bits_per_entry() as u32 / 8; + let mut fat_sectors = Vec::new(); + for nth_table in 0..fs.props.fat_table_count { + let table_sector_offset = fs.boot_record.nth_FAT_table_sector(nth_table); + let fat_sector = table_sector_offset + fat_byte_offset / fs.props.sector_size; + fat_sectors.push(fat_sector); + } + let sector_offset: usize = (fat_byte_offset % fs.props.sector_size) as usize; + + FATEntryProps { + fat_sectors, + sector_offset, + } + } +} + +impl ops::Drop for FileSystem +where + S: Read + Write + Seek, +{ + fn drop(&mut self) { + // nothing to do if these error out while dropping + let _ = self.sync_sector_buffer(); + let _ = self.storage.flush(); + } +} diff --git a/src/fs/mod.rs b/src/fs/mod.rs new file mode 100644 index 0000000..917836e --- /dev/null +++ b/src/fs/mod.rs @@ -0,0 +1,13 @@ +mod bpb; +mod consts; +mod direntry; +mod file; +mod fs; +#[cfg(all(test, feature = "std"))] +mod tests; + +use bpb::*; +pub use consts::*; +pub use direntry::*; +pub use file::*; +pub use fs::*; diff --git a/src/fs/tests.rs b/src/fs/tests.rs new file mode 100644 index 0000000..6cf4593 --- /dev/null +++ b/src/fs/tests.rs @@ -0,0 +1,486 @@ +use crate::io::prelude::*; +use crate::*; + +use test_log::test; + +static MINFS: &[u8] = include_bytes!("../../imgs/minfs.img"); +static FAT12: &[u8] = include_bytes!("../../imgs/fat12.img"); +static FAT16: &[u8] = include_bytes!("../../imgs/fat16.img"); +static FAT32: &[u8] = include_bytes!("../../imgs/fat32.img"); + +#[test] +#[allow(non_snake_case)] +fn check_FAT_offset() { + use crate::fs::BootRecord; + + use std::io::Cursor; + + let mut storage = Cursor::new(FAT16.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let fat_offset = match fs.boot_record { + BootRecord::FAT(boot_record_fat) => boot_record_fat.first_fat_sector(), + BootRecord::ExFAT(_boot_record_exfat) => unreachable!(), + }; + + // we manually read the first and second entry of the FAT table + fs.read_nth_sector(fat_offset.into()).unwrap(); + + let first_entry = u16::from_le_bytes(fs.sector_buffer[0..2].try_into().unwrap()); + let media_type = if let BootRecord::FAT(boot_record_fat) = fs.boot_record { + boot_record_fat.bpb._media_type + } else { + unreachable!("this should be a FAT16 filesystem") + }; + assert_eq!(u16::MAX << 8 | media_type as u16, first_entry); + + let second_entry = u16::from_le_bytes(fs.sector_buffer[2..4].try_into().unwrap()); + assert_eq!(u16::MAX, second_entry); +} + +#[test] +fn read_file_in_root_dir() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT16.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let mut file = fs.get_ro_file(PathBuf::from("/root.txt")).unwrap(); + + let mut file_string = String::new(); + file.read_to_string(&mut file_string).unwrap(); + const EXPECTED_STR: &str = "I am in the filesystem's root!!!\n\n"; + assert_eq!(file_string, EXPECTED_STR); +} + +static BEE_MOVIE_SCRIPT: &str = include_str!("../../tests/bee movie script.txt"); +fn assert_vec_is_bee_movie_script(buf: &Vec) { + let string = std::str::from_utf8(&buf).unwrap(); + let expected_size = BEE_MOVIE_SCRIPT.len(); + assert_eq!(buf.len(), expected_size); + + assert_eq!(string, BEE_MOVIE_SCRIPT); +} +fn assert_file_is_bee_movie_script(file: &mut ROFile<'_, S>) +where + S: Read + Write + Seek, +{ + let mut buf = Vec::new(); + file.read_to_end(&mut buf).unwrap(); + + assert_vec_is_bee_movie_script(&buf); +} + +#[test] +fn read_huge_file() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT16.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let mut file = fs + .get_ro_file(PathBuf::from("/bee movie script.txt")) + .unwrap(); + assert_file_is_bee_movie_script(&mut file); +} + +#[test] +fn seek_n_read() { + // this uses the famous "I'd like to interject for a moment" copypasta as a test file + // you can find it online by just searching this term + + use std::io::Cursor; + + let mut storage = Cursor::new(FAT16.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let mut file = fs + .get_ro_file(PathBuf::from("/GNU ⁄ Linux copypasta.txt")) + .unwrap(); + let mut file_bytes = [0_u8; 4096]; + + // we first perform a forward seek... + const EXPECTED_STR1: &str = "Linux is the kernel"; + file.seek(SeekFrom::Start(792)).unwrap(); + let bytes_read = file.read(&mut file_bytes[..EXPECTED_STR1.len()]).unwrap(); + assert_eq!( + String::from_utf8_lossy(&file_bytes[..bytes_read]), + EXPECTED_STR1 + ); + + // ...then a backward one + const EXPECTED_STR2: &str = "What you're referring to as Linux, is in fact, GNU/Linux"; + file.seek(SeekFrom::Start(39)).unwrap(); + let bytes_read = file.read(&mut file_bytes[..EXPECTED_STR2.len()]).unwrap(); + assert_eq!( + String::from_utf8_lossy(&file_bytes[..bytes_read]), + EXPECTED_STR2 + ); +} + +#[test] +// this won't actually modify the .img file or the static slices, +// since we run .to_owned(), which basically clones the data in the static slices, +// in order to make the Cursor readable/writable +fn write_to_file() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT12.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let mut file = fs.get_rw_file(PathBuf::from("/root.txt")).unwrap(); + + file.write_all(BEE_MOVIE_SCRIPT.as_bytes()).unwrap(); + file.rewind().unwrap(); + + assert_file_is_bee_movie_script(&mut file); + + // now let's do something else + // this write operations will happen between 2 clusters + const TEXT_OFFSET: u64 = 4598; + const TEXT: &str = "Hello from the other side"; + + file.seek(SeekFrom::Start(TEXT_OFFSET)).unwrap(); + file.write_all(TEXT.as_bytes()).unwrap(); + + // seek back to the start of where we wrote our text + file.seek(SeekFrom::Current(-(TEXT.len() as i64))).unwrap(); + let mut buf = [0_u8; TEXT.len()]; + file.read_exact(&mut buf).unwrap(); + let stored_text = std::str::from_utf8(&buf).unwrap(); + + assert_eq!(TEXT, stored_text); + + // we are also gonna write the bee movie ten more times to see if FAT12 can correctly handle split entries + for i in 0..10 { + log::debug!("Writing the bee movie script for the {i} consecutive time",); + + let start_offset = file.seek(SeekFrom::End(0)).unwrap(); + + file.write_all(BEE_MOVIE_SCRIPT.as_bytes()).unwrap(); + file.seek(SeekFrom::Start(start_offset)).unwrap(); + + let mut buf = vec![0_u8; BEE_MOVIE_SCRIPT.len()]; + file.read_exact(buf.as_mut_slice()).unwrap(); + + assert_vec_is_bee_movie_script(&buf); + } +} + +#[test] +fn remove_root_dir_file() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT16.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + // the bee movie script (here) is in the root directory region + let file_path = PathBuf::from("/bee movie script.txt"); + let file = fs.get_rw_file(file_path.clone()).unwrap(); + file.remove().unwrap(); + + // the file should now be gone + let file_result = fs.get_ro_file(file_path); + match file_result { + Err(err) => match err { + FSError::NotFound => (), + _ => panic!("unexpected IOError: {:?}", err), + }, + _ => panic!("file should have been deleted by now"), + } +} + +#[test] +fn remove_data_region_file() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT12.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + // the bee movie script (here) is in the data region + let file_path = PathBuf::from("/test/bee movie script.txt"); + let file = fs.get_rw_file(file_path.clone()).unwrap(); + file.remove().unwrap(); + + // the file should now be gone + let file_result = fs.get_ro_file(file_path); + match file_result { + Err(err) => match err { + FSError::NotFound => (), + _ => panic!("unexpected IOError: {:?}", err), + }, + _ => panic!("file should have been deleted by now"), + } +} + +#[test] +#[allow(non_snake_case)] +fn FAT_tables_after_write_are_identical() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT16.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + assert!( + fs.FAT_tables_are_identical().unwrap(), + concat!( + "this should pass. ", + "if it doesn't, either the corresponding .img file's FAT tables aren't identical", + "or the tables_are_identical function doesn't work correctly" + ) + ); + + // let's write the bee movie script to root.txt (why not), check, truncate the file, then check again + let mut file = fs.get_rw_file(PathBuf::from("root.txt")).unwrap(); + + file.write_all(BEE_MOVIE_SCRIPT.as_bytes()).unwrap(); + assert!(file.fs.FAT_tables_are_identical().unwrap()); + + file.truncate(10_000).unwrap(); + assert!(file.fs.FAT_tables_are_identical().unwrap()); +} + +#[test] +fn truncate_file() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT16.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let mut file = fs + .get_rw_file(PathBuf::from("/bee movie script.txt")) + .unwrap(); + + // we are gonna truncate the bee movie script down to 20 000 bytes + const NEW_SIZE: u32 = 20_000; + file.truncate(NEW_SIZE).unwrap(); + + let mut file_string = String::new(); + file.read_to_string(&mut file_string).unwrap(); + let mut expected_string = BEE_MOVIE_SCRIPT.to_string(); + expected_string.truncate(NEW_SIZE as usize); + + assert_eq!(file_string, expected_string); +} + +#[test] +fn read_only_file() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT16.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let file_result = fs.get_rw_file(PathBuf::from("/rootdir/example.txt")); + + match file_result { + Err(err) => match err { + FSError::ReadOnlyFile => (), + _ => panic!("unexpected IOError"), + }, + _ => panic!("file is marked read-only, yet somehow we got a RWFile for it"), + } +} + +#[test] +fn get_hidden_file() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT12.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let file_path = PathBuf::from("/hidden"); + let file_result = fs.get_ro_file(file_path.clone()); + match file_result { + Err(err) => match err { + FSError::NotFound => (), + _ => panic!("unexpected IOError"), + }, + _ => panic!("file should be hidden by default"), + } + + // let's now allow the filesystem to list hidden files + fs.show_hidden(true); + let file = fs.get_ro_file(file_path).unwrap(); + assert!(file.attributes.hidden); +} + +#[test] +fn read_file_in_subdir() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT16.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let mut file = fs + .get_ro_file(PathBuf::from("/rootdir/example.txt")) + .unwrap(); + + let mut file_string = String::new(); + file.read_to_string(&mut file_string).unwrap(); + const EXPECTED_STR: &str = "I am not in the root directory :(\n\n"; + assert_eq!(file_string, EXPECTED_STR); +} + +#[test] +fn check_file_timestamps() { + use time::macros::*; + + use std::io::Cursor; + + let mut storage = Cursor::new(FAT16.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let file = fs + .get_ro_file(PathBuf::from("/rootdir/example.txt")) + .unwrap(); + + assert_eq!(datetime!(2024-07-11 13:02:38.15), file.created); + assert_eq!(datetime!(2024-07-11 13:02:38.0), file.modified); + assert_eq!(date!(2024 - 07 - 11), file.accessed); +} + +#[test] +fn read_file_fat12() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT12.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let mut file = fs.get_ro_file(PathBuf::from("/foo/bar.txt")).unwrap(); + let mut file_string = String::new(); + file.read_to_string(&mut file_string).unwrap(); + const EXPECTED_STR: &str = "Hello, World!\n"; + assert_eq!(file_string, EXPECTED_STR); + + // please not that the FAT12 image has been modified so that + // one FAT entry of the file we are reading is split between different sectors + // this way, we also test for this case + let mut file = fs + .get_ro_file(PathBuf::from("/test/bee movie script.txt")) + .unwrap(); + assert_file_is_bee_movie_script(&mut file); +} + +#[test] +fn read_file_fat32() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT32.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let mut file = fs + .get_ro_file(PathBuf::from("/secret/bee movie script.txt")) + .unwrap(); + + assert_file_is_bee_movie_script(&mut file); +} + +#[test] +fn seek_n_read_fat32() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT32.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let mut file = fs.get_ro_file(PathBuf::from("/hello.txt")).unwrap(); + file.seek(SeekFrom::Start(13)).unwrap(); + + let mut string = String::new(); + file.read_to_string(&mut string).unwrap(); + const EXPECTED_STR: &str = "FAT32 filesystem!!!\n"; + + assert_eq!(string, EXPECTED_STR); +} + +#[test] +fn write_to_fat32_file() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT32.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let mut file = fs.get_rw_file(PathBuf::from("/hello.txt")).unwrap(); + // an arbitrary offset to seek to + const START_OFFSET: u64 = 1436; + file.seek(SeekFrom::Start(START_OFFSET)).unwrap(); + + file.write_all(BEE_MOVIE_SCRIPT.as_bytes()).unwrap(); + + // seek back + file.seek(SeekFrom::Current(-(BEE_MOVIE_SCRIPT.len() as i64))) + .unwrap(); + + // read back what we wrote + let mut string = String::new(); + file.read_to_string(&mut string).unwrap(); + assert_eq!(string, BEE_MOVIE_SCRIPT); + + // let's also read back what was (and hopefully still is) + // at the start of the file + const EXPECTED_STR: &str = "Hello from a FAT32 filesystem!!!\n"; + file.rewind().unwrap(); + let mut buf = [0_u8; EXPECTED_STR.len()]; + file.read_exact(&mut buf).unwrap(); + + let stored_text = std::str::from_utf8(&buf).unwrap(); + assert_eq!(stored_text, EXPECTED_STR) +} + +#[test] +fn truncate_fat32_file() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT32.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + const EXPECTED_STR: &str = "Hello fr"; + + let mut file = fs.get_rw_file(PathBuf::from("/hello.txt")).unwrap(); + file.truncate(EXPECTED_STR.len() as u32).unwrap(); + + let mut string = String::new(); + file.read_to_string(&mut string).unwrap(); + assert_eq!(string, EXPECTED_STR); +} + +#[test] +fn remove_fat32_file() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT32.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let file_path = PathBuf::from("/secret/bee movie script.txt"); + + let file = fs.get_rw_file(file_path.clone()).unwrap(); + file.remove().unwrap(); + + // the file should now be gone + let file_result = fs.get_ro_file(file_path); + match file_result { + Err(err) => match err { + FSError::NotFound => (), + _ => panic!("unexpected IOError: {:?}", err), + }, + _ => panic!("file should have been deleted by now"), + } +} + +#[test] +fn assert_img_fat_type() { + static TEST_CASES: &[(&[u8], FATType)] = &[ + (MINFS, FATType::FAT12), + (FAT12, FATType::FAT12), + (FAT16, FATType::FAT16), + (FAT32, FATType::FAT32), + ]; + + for case in TEST_CASES { + use std::io::Cursor; + + let mut storage = Cursor::new(case.0.to_owned()); + let fs = FileSystem::from_storage(&mut storage).unwrap(); + + assert_eq!(fs.fat_type(), case.1) + } +} diff --git a/src/io.rs b/src/io.rs index e017ad2..b0b8f2c 100644 --- a/src/io.rs +++ b/src/io.rs @@ -12,12 +12,10 @@ //! - [`Write`] allows for writing bytes to a sink. //! - [`Seek`] provides a cursor which can be moved within a stream of bytes -#[cfg(not(feature = "std"))] -use core::*; -#[cfg(feature = "std")] -use std::*; +use core::str; -use ::alloc::{string::String, vec::Vec}; +#[cfg(not(feature = "std"))] +use alloc::{string::String, vec::Vec}; use crate::error::{IOError, IOErrorKind}; diff --git a/src/lib.rs b/src/lib.rs index 9eaf088..48ad60d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,6 +87,8 @@ #![deny(missing_debug_implementations)] #![deny(missing_docs)] #![deny(non_ascii_idents)] +#![deny(private_bounds)] +#![deny(private_interfaces)] #![deny(trivial_numeric_casts)] #![deny(single_use_lifetimes)] #![deny(unsafe_op_in_unsafe_fn)] diff --git a/src/path.rs b/src/path.rs index c5de452..b53b1a7 100644 --- a/src/path.rs +++ b/src/path.rs @@ -5,13 +5,10 @@ //! return a [`MalformedPath`](crate::error::FSError::MalformedPath) error //! -#[cfg(not(feature = "std"))] -use core::*; -#[cfg(feature = "std")] -use std::*; +use core::{fmt, iter}; #[cfg(not(feature = "std"))] -use ::alloc::{ +use alloc::{ borrow::ToOwned, collections::vec_deque::VecDeque, string::{String, ToString}, @@ -218,7 +215,7 @@ mod tests { #[test] fn catch_invalid_path() { #[cfg(not(feature = "std"))] - use ::alloc::format; + use alloc::format; let mut pathbuf = PathBuf::new(); @@ -239,7 +236,7 @@ mod tests { #[test] fn catch_non_control_forbidden_chars() { #[cfg(not(feature = "std"))] - use ::alloc::format; + use alloc::format; let mut pathbuf = PathBuf::new(); diff --git a/src/utils/bincode.rs b/src/utils/bincode.rs new file mode 100644 index 0000000..ba0835c --- /dev/null +++ b/src/utils/bincode.rs @@ -0,0 +1,11 @@ +use bincode::{DefaultOptions, Options}; + +#[inline] +// an easy way to universally use the same bincode (de)serialization options +pub(crate) fn bincode_config() -> impl Options + Copy { + // also check https://docs.rs/bincode/1.3.3/bincode/config/index.html#options-struct-vs-bincode-functions + DefaultOptions::new() + .with_fixint_encoding() + .allow_trailing_bytes() + .with_little_endian() +} diff --git a/src/utils/bits.rs b/src/utils/bits.rs index 787a352..d2f171c 100644 --- a/src/utils/bits.rs +++ b/src/utils/bits.rs @@ -1,6 +1,6 @@ /// Sets the low `n` bits of a [`u32`] to `1` /// /// https://users.rust-lang.org/t/how-to-make-an-integer-with-n-bits-set-without-overflow/63078/3 -pub fn setbits_u32(n: u8) -> u32 { +pub(crate) fn setbits_u32(n: u8) -> u32 { u32::MAX >> (u32::BITS - u32::from(n)) } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 7909b54..26f85f5 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1 +1,3 @@ +pub(crate) mod bincode; pub(crate) mod bits; +pub(crate) mod string; diff --git a/src/utils/string.rs b/src/utils/string.rs new file mode 100644 index 0000000..16e005a --- /dev/null +++ b/src/utils/string.rs @@ -0,0 +1,14 @@ +#[cfg(not(feature = "std"))] +use alloc::string::String; + +use alloc::string::FromUtf16Error; + +/// variation of https://stackoverflow.com/a/42067321/19247098 for processing LFNs +pub(crate) fn string_from_lfn(utf16_src: &[u16]) -> Result { + let nul_range_end = utf16_src + .iter() + .position(|c| *c == 0x0000) + .unwrap_or(utf16_src.len()); // default to length if no `\0` present + + String::from_utf16(&utf16_src[0..nul_range_end]) +} From a4503de2e1b436cd3efb1c3189bfde0868db89b7 Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sun, 1 Sep 2024 15:26:34 +0300 Subject: [PATCH 16/40] chore: various aesthetic changes Consider this a followup to 8b61fea --- src/fs/bpb.rs | 53 +++--- src/fs/consts.rs | 4 +- src/fs/direntry.rs | 124 +++++-------- src/fs/file.rs | 413 ++++++++++++++++++++--------------------- src/fs/fs.rs | 438 +++++++++++++++++++++++--------------------- src/fs/tests.rs | 2 +- src/utils/string.rs | 2 +- 7 files changed, 514 insertions(+), 522 deletions(-) diff --git a/src/fs/bpb.rs b/src/fs/bpb.rs index bdaa882..28b8217 100644 --- a/src/fs/bpb.rs +++ b/src/fs/bpb.rs @@ -5,26 +5,6 @@ use core::fmt; use serde::{Deserialize, Serialize}; use serde_big_array::BigArray; -pub(crate) const BPBFAT_SIZE: usize = 36; -#[derive(Serialize, Deserialize, Debug, Clone, Copy)] -pub(crate) struct BPBFAT { - pub _jmpboot: [u8; 3], - pub _oem_identifier: [u8; 8], - pub bytes_per_sector: u16, - pub sectors_per_cluster: u8, - pub reserved_sector_count: u16, - pub table_count: u8, - pub root_entry_count: u16, - // If this is 0, check `total_sectors_32` - pub total_sectors_16: u16, - pub _media_type: u8, - pub table_size_16: u16, - pub _sectors_per_track: u16, - pub _head_side_count: u16, - pub hidden_sector_count: u32, - pub total_sectors_32: u32, -} - #[derive(Debug)] pub(crate) enum BootRecord { FAT(BootRecordFAT), @@ -36,16 +16,7 @@ impl BootRecord { /// The FAT type of this file system pub(crate) fn fat_type(&self) -> FATType { match self { - BootRecord::FAT(boot_record_fat) => { - let total_clusters = boot_record_fat.total_clusters(); - if total_clusters < 4085 { - FATType::FAT12 - } else if total_clusters < 65525 { - FATType::FAT16 - } else { - FATType::FAT32 - } - } + BootRecord::FAT(boot_record_fat) => boot_record_fat.fat_type(), BootRecord::ExFAT(_boot_record_exfat) => { todo!("ExFAT not yet implemented"); FATType::ExFAT @@ -197,7 +168,27 @@ pub(crate) struct BootRecordExFAT { pub _reserved: [u8; 7], } -pub(crate) const EBR_SIZE: usize = 512 - BPBFAT_SIZE; +pub(crate) const BPBFAT_SIZE: usize = 36; +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +pub(crate) struct BPBFAT { + pub _jmpboot: [u8; 3], + pub _oem_identifier: [u8; 8], + pub bytes_per_sector: u16, + pub sectors_per_cluster: u8, + pub reserved_sector_count: u16, + pub table_count: u8, + pub root_entry_count: u16, + // If this is 0, check `total_sectors_32` + pub total_sectors_16: u16, + pub _media_type: u8, + pub table_size_16: u16, + pub _sectors_per_track: u16, + pub _head_side_count: u16, + pub hidden_sector_count: u32, + pub total_sectors_32: u32, +} + +pub(crate) const EBR_SIZE: usize = MIN_SECTOR_SIZE - BPBFAT_SIZE; #[derive(Clone, Copy)] pub(crate) enum EBR { FAT12_16(EBRFAT12_16), diff --git a/src/fs/consts.rs b/src/fs/consts.rs index fe93055..18b4b0b 100644 --- a/src/fs/consts.rs +++ b/src/fs/consts.rs @@ -1,7 +1,7 @@ /// The minimum size (in bytes) a sector is allowed to have -pub const SECTOR_SIZE_MIN: usize = 512; +pub const MIN_SECTOR_SIZE: usize = 512; /// The maximum size (in bytes) a sector is allowed to have -pub const SECTOR_SIZE_MAX: usize = 4096; +pub const MAX_SECTOR_SIZE: usize = 4096; /// Place this in the BPB _jmpboot field to hang if a computer attempts to boot this partition /// The first two bytes jump to 0 on all bit modes and the third byte is just a NOP diff --git a/src/fs/direntry.rs b/src/fs/direntry.rs index 7a0b642..e396efe 100644 --- a/src/fs/direntry.rs +++ b/src/fs/direntry.rs @@ -2,7 +2,7 @@ use super::*; use crate::io::prelude::*; -use core::{fmt, mem, num, ops}; +use core::{fmt, mem, num}; #[cfg(not(feature = "std"))] use alloc::{borrow::ToOwned, string::String}; @@ -12,52 +12,6 @@ use bitflags::bitflags; use serde::{Deserialize, Serialize}; use time::{Date, PrimitiveDateTime, Time}; -#[derive(Serialize, Deserialize, Debug, Clone, Copy)] -pub(crate) struct SFN { - name: [u8; 8], - ext: [u8; 3], -} - -impl SFN { - fn get_byte_slice(&self) -> [u8; 11] { - let mut slice = [0; 11]; - - slice[..8].copy_from_slice(&self.name); - slice[8..].copy_from_slice(&self.ext); - - slice - } - - pub(crate) fn gen_checksum(&self) -> u8 { - let mut sum = 0; - - for c in self.get_byte_slice() { - sum = (if (sum & 1) != 0 { 0x80_u8 } else { 0_u8 }) - .wrapping_add(sum >> 1) - .wrapping_add(c) - } - - log::debug!("SFN checksum: {:X}", sum); - - sum - } -} - -impl fmt::Display for SFN { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // we begin by writing the name (even if it is padded with spaces, they will be trimmed, so we don't care) - write!(f, "{}", String::from_utf8_lossy(&self.name).trim())?; - - // then, if the extension isn't empty (padded with zeroes), we write it too - let ext = String::from_utf8_lossy(&self.ext).trim().to_owned(); - if !ext.is_empty() { - write!(f, ".{}", ext)?; - }; - - Ok(()) - } -} - bitflags! { /// A list of the various (raw) attributes specified for a file/directory /// @@ -230,6 +184,52 @@ pub(crate) struct FATDirEntry { pub(crate) file_size: u32, } +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +pub(crate) struct SFN { + name: [u8; 8], + ext: [u8; 3], +} + +impl SFN { + fn get_byte_slice(&self) -> [u8; 11] { + let mut slice = [0; 11]; + + slice[..8].copy_from_slice(&self.name); + slice[8..].copy_from_slice(&self.ext); + + slice + } + + pub(crate) fn gen_checksum(&self) -> u8 { + let mut sum = 0; + + for c in self.get_byte_slice() { + sum = (if (sum & 1) != 0 { 0x80_u8 } else { 0_u8 }) + .wrapping_add(sum >> 1) + .wrapping_add(c) + } + + log::debug!("SFN checksum: {:X}", sum); + + sum + } +} + +impl fmt::Display for SFN { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // we begin by writing the name (even if it is padded with spaces, they will be trimmed, so we don't care) + write!(f, "{}", String::from_utf8_lossy(&self.name).trim())?; + + // then, if the extension isn't empty (padded with zeroes), we write it too + let ext = String::from_utf8_lossy(&self.ext).trim().to_owned(); + if !ext.is_empty() { + write!(f, ".{}", ext)?; + }; + + Ok(()) + } +} + #[derive(Debug, Deserialize, Serialize)] pub(crate) struct LFNEntry { /// masked with 0x40 if this is the last entry @@ -304,33 +304,3 @@ pub(crate) struct DirEntryChain { /// how many (contiguous) entries this entry chain has pub(crate) len: u32, } - -/// A resolved file/directory entry (for internal usage only) -#[derive(Debug)] -pub(crate) struct RawProperties { - pub(crate) name: String, - pub(crate) is_dir: bool, - pub(crate) attributes: RawAttributes, - pub(crate) created: PrimitiveDateTime, - pub(crate) modified: PrimitiveDateTime, - pub(crate) accessed: Date, - pub(crate) file_size: u32, - pub(crate) data_cluster: u32, - - pub(crate) chain_props: DirEntryChain, -} - -/// A thin wrapper for [`Properties`] represing a directory entry -#[derive(Debug)] -pub struct DirEntry { - pub(crate) entry: Properties, -} - -impl ops::Deref for DirEntry { - type Target = Properties; - - #[inline] - fn deref(&self) -> &Self::Target { - &self.entry - } -} diff --git a/src/fs/file.rs b/src/fs/file.rs index b63a17c..0e888d1 100644 --- a/src/fs/file.rs +++ b/src/fs/file.rs @@ -46,6 +46,78 @@ where } } +// Internal functions +impl ROFile<'_, S> +where + S: Read + Write + Seek, +{ + #[inline] + /// Panics if the current cluser doesn't point to another clluster + fn next_cluster(&mut self) -> Result<(), ::Error> { + // when a `ROFile` is created, `cluster_chain_is_healthy` is called, if it fails, that ROFile is dropped + self.props.current_cluster = self.get_next_cluster()?.unwrap(); + + Ok(()) + } + + #[inline] + /// Non-[`panic`]king version of [`next_cluster()`](ROFile::next_cluster) + fn get_next_cluster(&mut self) -> Result, ::Error> { + Ok(self.fs.get_next_cluster(self.props.current_cluster)?) + } + + /// Returns that last cluster in the file's cluster chain + fn last_cluster_in_chain(&mut self) -> Result::Error> { + // we begin from the current cluster to save some time + let mut current_cluster = self.props.current_cluster; + + loop { + match self.fs.read_nth_FAT_entry(current_cluster)? { + FATEntry::Allocated(next_cluster) => current_cluster = next_cluster, + FATEntry::EOF => break, + _ => unreachable!(), + } + } + + Ok(current_cluster) + } + + /// Checks whether the cluster chain of this file is healthy or malformed + pub(crate) fn cluster_chain_is_healthy(&mut self) -> Result { + let mut current_cluster = self.data_cluster; + let mut cluster_count = 0; + + loop { + cluster_count += 1; + + if cluster_count * self.fs.cluster_size() >= self.file_size.into() { + break; + } + + match self.fs.read_nth_FAT_entry(current_cluster)? { + FATEntry::Allocated(next_cluster) => current_cluster = next_cluster, + _ => return Ok(false), + }; + } + + Ok(true) + } + + fn offset_from_seekfrom(&self, seekfrom: SeekFrom) -> u64 { + match seekfrom { + SeekFrom::Start(offset) => offset, + SeekFrom::Current(offset) => { + let offset = self.props.offset as i64 + offset; + offset.try_into().unwrap_or(u64::MIN) + } + SeekFrom::End(offset) => { + let offset = self.file_size as i64 + offset; + offset.try_into().unwrap_or(u64::MIN) + } + } + } +} + impl IOBase for ROFile<'_, S> where S: Read + Write + Seek, @@ -53,6 +125,139 @@ where type Error = S::Error; } +impl Read for ROFile<'_, S> +where + S: Read + Write + Seek, +{ + fn read(&mut self, buf: &mut [u8]) -> Result { + let mut bytes_read = 0; + // this is the maximum amount of bytes that can be read + let read_cap = cmp::min( + buf.len(), + self.file_size as usize - self.props.offset as usize, + ); + + 'outer: loop { + let sector_init_offset = u32::try_from(self.props.offset % self.fs.cluster_size()) + .unwrap() + / self.fs.sector_size(); + let first_sector_of_cluster = self + .fs + .data_cluster_to_partition_sector(self.props.current_cluster) + + sector_init_offset; + let last_sector_of_cluster = first_sector_of_cluster + + self.fs.sectors_per_cluster() as u32 + - sector_init_offset + - 1; + log::debug!( + "Reading cluster {} from sectors {} to {}", + self.props.current_cluster, + first_sector_of_cluster, + last_sector_of_cluster + ); + + for sector in first_sector_of_cluster..=last_sector_of_cluster { + self.fs.read_nth_sector(sector.into())?; + + let start_index = self.props.offset as usize % self.fs.sector_size() as usize; + let bytes_to_read = cmp::min( + read_cap - bytes_read, + self.fs.sector_size() as usize - start_index, + ); + log::debug!( + "Gonna read {} bytes from sector {} starting at byte {}", + bytes_to_read, + sector, + start_index + ); + + buf[bytes_read..bytes_read + bytes_to_read].copy_from_slice( + &self.fs.sector_buffer[start_index..start_index + bytes_to_read], + ); + + bytes_read += bytes_to_read; + self.props.offset += bytes_to_read as u64; + + // if we have read as many bytes as we want... + if bytes_read >= read_cap { + // ...but we must process get the next cluster for future uses, + // we do that before breaking + if self.props.offset % self.fs.cluster_size() == 0 + && self.props.offset < self.file_size.into() + { + self.next_cluster()?; + } + + break 'outer; + } + } + + self.next_cluster()?; + } + + Ok(bytes_read) + } + + // the default `read_to_end` implementation isn't efficient enough, so we just do this + fn read_to_end(&mut self, buf: &mut Vec) -> Result { + let bytes_to_read = self.file_size as usize - self.props.offset as usize; + let init_buf_len = buf.len(); + + // resize buffer to fit the file contents exactly + buf.resize(init_buf_len + bytes_to_read, 0); + + // this is guaranteed not to raise an EOF (although other error kinds might be raised...) + self.read_exact(&mut buf[init_buf_len..])?; + + Ok(bytes_to_read) + } +} + +impl Seek for ROFile<'_, S> +where + S: Read + Write + Seek, +{ + fn seek(&mut self, pos: SeekFrom) -> Result { + let offset = self.offset_from_seekfrom(pos); + + // in case the cursor goes beyond the EOF, allocate more clusters + if offset > (self.file_size as u64).next_multiple_of(self.fs.cluster_size()) { + return Err(IOError::new( + ::Kind::new_unexpected_eof(), + "moved past eof in a RO file", + )); + } + + log::trace!( + "Previous cursor offset is {}, new cursor offset is {}", + self.props.offset, + offset + ); + + use cmp::Ordering; + match offset.cmp(&self.props.offset) { + Ordering::Less => { + // here, we basically "rewind" back to the start of the file and then seek to where we want + // this of course has performance issues, so TODO: find a solution that is both memory & time efficient + // (perhaps we could follow a similar approach to elm-chan's FATFS, by using a cluster link map table, perhaps as an optional feature) + self.props.offset = 0; + self.props.current_cluster = self.data_cluster; + self.seek(SeekFrom::Start(offset))?; + } + Ordering::Equal => (), + Ordering::Greater => { + for _ in self.props.offset / self.fs.cluster_size()..offset / self.fs.cluster_size() + { + self.next_cluster()?; + } + self.props.offset = offset; + } + } + + Ok(self.props.offset) + } +} + /// A read-write file within a FAT filesystem /// /// The size of the file will be automatically adjusted @@ -87,13 +292,6 @@ where } } -impl IOBase for RWFile<'_, S> -where - S: Read + Write + Seek, -{ - type Error = S::Error; -} - // Public functions impl RWFile<'_, S> where @@ -237,165 +435,13 @@ where } } -// Internal functions -impl ROFile<'_, S> +impl IOBase for RWFile<'_, S> where S: Read + Write + Seek, { - #[inline] - /// Panics if the current cluser doesn't point to another clluster - fn next_cluster(&mut self) -> Result<(), ::Error> { - // when a `ROFile` is created, `cluster_chain_is_healthy` is called, if it fails, that ROFile is dropped - self.props.current_cluster = self.get_next_cluster()?.unwrap(); - - Ok(()) - } - - #[inline] - /// Non-[`panic`]king version of [`next_cluster()`](ROFile::next_cluster) - fn get_next_cluster(&mut self) -> Result, ::Error> { - Ok(self.fs.get_next_cluster(self.props.current_cluster)?) - } - - /// Returns that last cluster in the file's cluster chain - fn last_cluster_in_chain(&mut self) -> Result::Error> { - // we begin from the current cluster to save some time - let mut current_cluster = self.props.current_cluster; - - loop { - match self.fs.read_nth_FAT_entry(current_cluster)? { - FATEntry::Allocated(next_cluster) => current_cluster = next_cluster, - FATEntry::EOF => break, - _ => unreachable!(), - } - } - - Ok(current_cluster) - } - - /// Checks whether the cluster chain of this file is healthy or malformed - pub(crate) fn cluster_chain_is_healthy(&mut self) -> Result { - let mut current_cluster = self.data_cluster; - let mut cluster_count = 0; - - loop { - cluster_count += 1; - - if cluster_count * self.fs.cluster_size() >= self.file_size.into() { - break; - } - - match self.fs.read_nth_FAT_entry(current_cluster)? { - FATEntry::Allocated(next_cluster) => current_cluster = next_cluster, - _ => return Ok(false), - }; - } - - Ok(true) - } - - fn offset_from_seekfrom(&self, seekfrom: SeekFrom) -> u64 { - match seekfrom { - SeekFrom::Start(offset) => offset, - SeekFrom::Current(offset) => { - let offset = self.props.offset as i64 + offset; - offset.try_into().unwrap_or(u64::MIN) - } - SeekFrom::End(offset) => { - let offset = self.file_size as i64 + offset; - offset.try_into().unwrap_or(u64::MIN) - } - } - } + type Error = S::Error; } -impl Read for ROFile<'_, S> -where - S: Read + Write + Seek, -{ - fn read(&mut self, buf: &mut [u8]) -> Result { - let mut bytes_read = 0; - // this is the maximum amount of bytes that can be read - let read_cap = cmp::min( - buf.len(), - self.file_size as usize - self.props.offset as usize, - ); - - 'outer: loop { - let sector_init_offset = u32::try_from(self.props.offset % self.fs.cluster_size()) - .unwrap() - / self.fs.sector_size(); - let first_sector_of_cluster = self - .fs - .data_cluster_to_partition_sector(self.props.current_cluster) - + sector_init_offset; - let last_sector_of_cluster = first_sector_of_cluster - + self.fs.sectors_per_cluster() as u32 - - sector_init_offset - - 1; - log::debug!( - "Reading cluster {} from sectors {} to {}", - self.props.current_cluster, - first_sector_of_cluster, - last_sector_of_cluster - ); - - for sector in first_sector_of_cluster..=last_sector_of_cluster { - self.fs.read_nth_sector(sector.into())?; - - let start_index = self.props.offset as usize % self.fs.sector_size() as usize; - let bytes_to_read = cmp::min( - read_cap - bytes_read, - self.fs.sector_size() as usize - start_index, - ); - log::debug!( - "Gonna read {} bytes from sector {} starting at byte {}", - bytes_to_read, - sector, - start_index - ); - - buf[bytes_read..bytes_read + bytes_to_read].copy_from_slice( - &self.fs.sector_buffer[start_index..start_index + bytes_to_read], - ); - - bytes_read += bytes_to_read; - self.props.offset += bytes_to_read as u64; - - // if we have read as many bytes as we want... - if bytes_read >= read_cap { - // ...but we must process get the next cluster for future uses, - // we do that before breaking - if self.props.offset % self.fs.cluster_size() == 0 - && self.props.offset < self.file_size.into() - { - self.next_cluster()?; - } - - break 'outer; - } - } - - self.next_cluster()?; - } - - Ok(bytes_read) - } - - // the default `read_to_end` implementation isn't efficient enough, so we just do this - fn read_to_end(&mut self, buf: &mut Vec) -> Result { - let bytes_to_read = self.file_size as usize - self.props.offset as usize; - let init_buf_len = buf.len(); - - // resize buffer to fit the file contents exactly - buf.resize(init_buf_len + bytes_to_read, 0); - - // this is guaranteed not to raise an EOF (although other error kinds might be raised...) - self.read_exact(&mut buf[init_buf_len..])?; - - Ok(bytes_to_read) - } -} impl Read for RWFile<'_, S> where S: Read + Write + Seek, @@ -491,51 +537,6 @@ where } } -impl Seek for ROFile<'_, S> -where - S: Read + Write + Seek, -{ - fn seek(&mut self, pos: SeekFrom) -> Result { - let offset = self.offset_from_seekfrom(pos); - - // in case the cursor goes beyond the EOF, allocate more clusters - if offset > (self.file_size as u64).next_multiple_of(self.fs.cluster_size()) { - return Err(IOError::new( - ::Kind::new_unexpected_eof(), - "moved past eof in a RO file", - )); - } - - log::trace!( - "Previous cursor offset is {}, new cursor offset is {}", - self.props.offset, - offset - ); - - use cmp::Ordering; - match offset.cmp(&self.props.offset) { - Ordering::Less => { - // here, we basically "rewind" back to the start of the file and then seek to where we want - // this of course has performance issues, so TODO: find a solution that is both memory & time efficient - // (perhaps we could follow a similar approach to elm-chan's FATFS, by using a cluster link map table, perhaps as an optional feature) - self.props.offset = 0; - self.props.current_cluster = self.data_cluster; - self.seek(SeekFrom::Start(offset))?; - } - Ordering::Equal => (), - Ordering::Greater => { - for _ in self.props.offset / self.fs.cluster_size()..offset / self.fs.cluster_size() - { - self.next_cluster()?; - } - self.props.offset = offset; - } - } - - Ok(self.props.offset) - } -} - impl Seek for RWFile<'_, S> where S: Read + Write + Seek, diff --git a/src/fs/fs.rs b/src/fs/fs.rs index 9b74585..e9ce95b 100644 --- a/src/fs/fs.rs +++ b/src/fs/fs.rs @@ -83,6 +83,50 @@ impl From<&FATEntry> for u32 { } } +/// Properties about the position of a [`FATEntry`] inside the FAT region +struct FATEntryProps { + /// Each `n`th element of the vector points at the corrensponding sector at the `n+1`th FAT table + fat_sectors: Vec, + sector_offset: usize, +} + +impl FATEntryProps { + /// Get the [`FATEntryProps`] of the `n`-th [`FATEntry`] of a [`ROFileSystem`] (`fs`) + pub fn new(n: u32, fs: &FileSystem) -> Self + where + S: Read + Write + Seek, + { + let fat_byte_offset: u32 = n * fs.fat_type.bits_per_entry() as u32 / 8; + let mut fat_sectors = Vec::new(); + for nth_table in 0..fs.props.fat_table_count { + let table_sector_offset = fs.boot_record.nth_FAT_table_sector(nth_table); + let fat_sector = table_sector_offset + fat_byte_offset / fs.props.sector_size; + fat_sectors.push(fat_sector); + } + let sector_offset: usize = (fat_byte_offset % fs.props.sector_size) as usize; + + FATEntryProps { + fat_sectors, + sector_offset, + } + } +} + +/// A resolved file/directory entry (for internal usage only) +#[derive(Debug)] +pub(crate) struct RawProperties { + pub(crate) name: String, + pub(crate) is_dir: bool, + pub(crate) attributes: RawAttributes, + pub(crate) created: PrimitiveDateTime, + pub(crate) modified: PrimitiveDateTime, + pub(crate) accessed: Date, + pub(crate) file_size: u32, + pub(crate) data_cluster: u32, + + pub(crate) chain_props: DirEntryChain, +} + /// A container for file/directory properties #[derive(Debug)] pub struct Properties { @@ -162,6 +206,173 @@ impl Properties { } } +/// A thin wrapper for [`Properties`] represing a directory entry +#[derive(Debug)] +pub struct DirEntry { + pub(crate) entry: Properties, +} + +impl ops::Deref for DirEntry { + type Target = Properties; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.entry + } +} + +pub(crate) const UNUSED_ENTRY: u8 = 0xE5; +pub(crate) const LAST_AND_UNUSED_ENTRY: u8 = 0x00; + +#[derive(Debug)] +struct EntryParser { + entries: Vec, + lfn_buf: Vec, + lfn_checksum: Option, + current_chain: Option, +} + +impl Default for EntryParser { + fn default() -> Self { + EntryParser { + entries: Vec::new(), + lfn_buf: Vec::new(), + lfn_checksum: None, + current_chain: None, + } + } +} + +impl EntryParser { + #[inline] + fn _decrement_parsed_entries_counter(&mut self) { + if let Some(current_chain) = &mut self.current_chain { + current_chain.len -= 1 + } + } + + /// Parses a sector of 8.3 & LFN entries + /// + /// Returns a [`Result`] indicating whether or not + /// this sector was the last one in the chain containing entries + fn parse_sector( + &mut self, + sector: u32, + fs: &mut FileSystem, + ) -> Result::Error> + where + S: Read + Write + Seek, + { + use utils::bincode::bincode_config; + + let entry_location = EntryLocation::from_partition_sector(sector, fs); + + for (index, chunk) in fs + .read_nth_sector(sector.into())? + .chunks(DIRENTRY_SIZE) + .enumerate() + { + match chunk[0] { + LAST_AND_UNUSED_ENTRY => return Ok(true), + UNUSED_ENTRY => continue, + _ => (), + }; + + let Ok(entry) = bincode_config().deserialize::(&chunk) else { + continue; + }; + + // update current entry chain data + match &mut self.current_chain { + Some(current_chain) => current_chain.len += 1, + None => { + self.current_chain = Some(DirEntryChain { + location: entry_location.clone(), + index: index as u32, + len: 1, + }) + } + } + + if entry.attributes.contains(RawAttributes::LFN) { + // TODO: perhaps there is a way to utilize the `order` field? + let Ok(lfn_entry) = bincode_config().deserialize::(&chunk) else { + self._decrement_parsed_entries_counter(); + continue; + }; + + // If the signature verification fails, consider this entry corrupted + if !lfn_entry.verify_signature() { + self._decrement_parsed_entries_counter(); + continue; + } + + match self.lfn_checksum { + Some(checksum) => { + if checksum != lfn_entry.checksum { + self.lfn_checksum = None; + self.lfn_buf.clear(); + self.current_chain = None; + continue; + } + } + None => self.lfn_checksum = Some(lfn_entry.checksum), + } + + let char_arr = lfn_entry.get_byte_slice(); + if let Ok(temp_str) = utils::string::string_from_lfn(&char_arr) { + self.lfn_buf.push(temp_str); + } + + continue; + } + + let filename = if !self.lfn_buf.is_empty() + && self + .lfn_checksum + .is_some_and(|checksum| checksum == entry.sfn.gen_checksum()) + { + // for efficiency reasons, we store the LFN string sequences as we read them + let parsed_str: String = self.lfn_buf.iter().cloned().rev().collect(); + self.lfn_buf.clear(); + self.lfn_checksum = None; + parsed_str + } else { + entry.sfn.to_string() + }; + + if let (Ok(created), Ok(modified), Ok(accessed)) = ( + entry.created.try_into(), + entry.modified.try_into(), + entry.accessed.try_into(), + ) { + self.entries.push(RawProperties { + name: filename, + is_dir: entry.attributes.contains(RawAttributes::DIRECTORY), + attributes: entry.attributes, + created, + modified, + accessed, + file_size: entry.file_size, + data_cluster: ((entry.cluster_high as u32) << 16) + entry.cluster_low as u32, + chain_props: self + .current_chain + .take() + .expect("at this point, this shouldn't be None"), + }) + } + } + + Ok(false) + } + + /// Consumes [`Self`](EntryParser) & returns a `Vec` of [`RawProperties`] + /// of the parsed entries + fn finish(self) -> Vec { + self.entries + } +} + pub(crate) trait OffsetConversions { fn sector_size(&self) -> u32; fn cluster_size(&self) -> u64; @@ -196,6 +407,26 @@ pub(crate) trait OffsetConversions { } } +impl OffsetConversions for FileSystem +where + S: Read + Write + Seek, +{ + #[inline] + fn sector_size(&self) -> u32 { + self.props.sector_size + } + + #[inline] + fn cluster_size(&self) -> u64 { + self.props.cluster_size + } + + #[inline] + fn first_data_sector(&self) -> u32 { + self.props.first_data_sector + } +} + /// Some generic properties common across all FAT versions, like a sector's size, are cached here #[derive(Debug)] pub(crate) struct FSProperties { @@ -260,26 +491,6 @@ where filter: FileFilter, } -impl OffsetConversions for FileSystem -where - S: Read + Write + Seek, -{ - #[inline] - fn sector_size(&self) -> u32 { - self.props.sector_size - } - - #[inline] - fn cluster_size(&self) -> u64 { - self.props.cluster_size - } - - #[inline] - fn first_data_sector(&self) -> u32 { - self.props.first_data_sector - } -} - /// Getter functions impl FileSystem where @@ -327,12 +538,12 @@ where // Begin by reading the boot record // We don't know the sector size yet, so we just go with the biggest possible one for now - let mut buffer = [0u8; SECTOR_SIZE_MAX]; + let mut buffer = [0u8; MAX_SECTOR_SIZE]; let bytes_read = storage.read(&mut buffer)?; let mut stored_sector = 0; - if bytes_read < 512 { + if bytes_read < MIN_SECTOR_SIZE { return Err(FSError::InternalFSError(InternalFSError::StorageTooSmall)); } @@ -449,158 +660,6 @@ where } } -#[derive(Debug)] -struct EntryParser { - entries: Vec, - lfn_buf: Vec, - lfn_checksum: Option, - current_chain: Option, -} - -impl Default for EntryParser { - fn default() -> Self { - EntryParser { - entries: Vec::new(), - lfn_buf: Vec::new(), - lfn_checksum: None, - current_chain: None, - } - } -} - -pub(crate) const UNUSED_ENTRY: u8 = 0xE5; -pub(crate) const LAST_AND_UNUSED_ENTRY: u8 = 0x00; - -impl EntryParser { - #[inline] - fn _decrement_parsed_entries_counter(&mut self) { - if let Some(current_chain) = &mut self.current_chain { - current_chain.len -= 1 - } - } - - /// Parses a sector of 8.3 & LFN entries - /// - /// Returns a [`Result`] indicating whether or not - /// this sector was the last one in the chain containing entries - fn parse_sector( - &mut self, - sector: u32, - fs: &mut FileSystem, - ) -> Result::Error> - where - S: Read + Write + Seek, - { - use utils::bincode::bincode_config; - - let entry_location = EntryLocation::from_partition_sector(sector, fs); - - for (index, chunk) in fs - .read_nth_sector(sector.into())? - .chunks(DIRENTRY_SIZE) - .enumerate() - { - match chunk[0] { - LAST_AND_UNUSED_ENTRY => return Ok(true), - UNUSED_ENTRY => continue, - _ => (), - }; - - let Ok(entry) = bincode_config().deserialize::(&chunk) else { - continue; - }; - - // update current entry chain data - match &mut self.current_chain { - Some(current_chain) => current_chain.len += 1, - None => { - self.current_chain = Some(DirEntryChain { - location: entry_location.clone(), - index: index as u32, - len: 1, - }) - } - } - - if entry.attributes.contains(RawAttributes::LFN) { - // TODO: perhaps there is a way to utilize the `order` field? - let Ok(lfn_entry) = bincode_config().deserialize::(&chunk) else { - self._decrement_parsed_entries_counter(); - continue; - }; - - // If the signature verification fails, consider this entry corrupted - if !lfn_entry.verify_signature() { - self._decrement_parsed_entries_counter(); - continue; - } - - match self.lfn_checksum { - Some(checksum) => { - if checksum != lfn_entry.checksum { - self.lfn_checksum = None; - self.lfn_buf.clear(); - self.current_chain = None; - continue; - } - } - None => self.lfn_checksum = Some(lfn_entry.checksum), - } - - let char_arr = lfn_entry.get_byte_slice().to_vec(); - if let Ok(temp_str) = utils::string::string_from_lfn(&char_arr) { - self.lfn_buf.push(temp_str); - } - - continue; - } - - let filename = if !self.lfn_buf.is_empty() - && self - .lfn_checksum - .is_some_and(|checksum| checksum == entry.sfn.gen_checksum()) - { - // for efficiency reasons, we store the LFN string sequences as we read them - let parsed_str: String = self.lfn_buf.iter().cloned().rev().collect(); - self.lfn_buf.clear(); - self.lfn_checksum = None; - parsed_str - } else { - entry.sfn.to_string() - }; - - if let (Ok(created), Ok(modified), Ok(accessed)) = ( - entry.created.try_into(), - entry.modified.try_into(), - entry.accessed.try_into(), - ) { - self.entries.push(RawProperties { - name: filename, - is_dir: entry.attributes.contains(RawAttributes::DIRECTORY), - attributes: entry.attributes, - created, - modified, - accessed, - file_size: entry.file_size, - data_cluster: ((entry.cluster_high as u32) << 16) + entry.cluster_low as u32, - chain_props: self - .current_chain - .take() - .expect("at this point, this shouldn't be None"), - }) - } - } - - Ok(false) - } - - /// Consumes [`Self`](EntryParser) & returns a `Vec` of [`RawProperties`] - /// of the parsed entries - fn finish(self) -> Vec { - self.entries - } -} - /// Internal [`Read`]-related low-level functions impl FileSystem where @@ -725,7 +784,7 @@ where "this function doesn't work with ExFAT" ); - /// How many bytes to probe at max for each FAT per iteration (must be a multiple of [`SECTOR_SIZE_MAX`]) + /// How many bytes to probe at max for each FAT per iteration (must be a multiple of [`MAX_SECTOR_SIZE`]) const MAX_PROBE_SIZE: u32 = 1 << 20; let fat_byte_size = match self.boot_record { @@ -1099,35 +1158,6 @@ where } } -/// Properties about the position of a [`FATEntry`] inside the FAT region -struct FATEntryProps { - /// Each `n`th element of the vector points at the corrensponding sector at the `n+1`th FAT table - fat_sectors: Vec, - sector_offset: usize, -} - -impl FATEntryProps { - /// Get the [`FATEntryProps`] of the `n`-th [`FATEntry`] of a [`ROFileSystem`] (`fs`) - pub fn new(n: u32, fs: &FileSystem) -> Self - where - S: Read + Write + Seek, - { - let fat_byte_offset: u32 = n * fs.fat_type.bits_per_entry() as u32 / 8; - let mut fat_sectors = Vec::new(); - for nth_table in 0..fs.props.fat_table_count { - let table_sector_offset = fs.boot_record.nth_FAT_table_sector(nth_table); - let fat_sector = table_sector_offset + fat_byte_offset / fs.props.sector_size; - fat_sectors.push(fat_sector); - } - let sector_offset: usize = (fat_byte_offset % fs.props.sector_size) as usize; - - FATEntryProps { - fat_sectors, - sector_offset, - } - } -} - impl ops::Drop for FileSystem where S: Read + Write + Seek, diff --git a/src/fs/tests.rs b/src/fs/tests.rs index 6cf4593..ca2c072 100644 --- a/src/fs/tests.rs +++ b/src/fs/tests.rs @@ -26,7 +26,7 @@ fn check_FAT_offset() { // we manually read the first and second entry of the FAT table fs.read_nth_sector(fat_offset.into()).unwrap(); - let first_entry = u16::from_le_bytes(fs.sector_buffer[0..2].try_into().unwrap()); + let first_entry = u16::from_le_bytes(fs.sector_buffer[..2].try_into().unwrap()); let media_type = if let BootRecord::FAT(boot_record_fat) = fs.boot_record { boot_record_fat.bpb._media_type } else { diff --git a/src/utils/string.rs b/src/utils/string.rs index 16e005a..7804c46 100644 --- a/src/utils/string.rs +++ b/src/utils/string.rs @@ -10,5 +10,5 @@ pub(crate) fn string_from_lfn(utf16_src: &[u16]) -> Result Date: Tue, 3 Sep 2024 17:11:48 +0300 Subject: [PATCH 17/40] fix: properly handle FATs Mirroring is now properly implemented & sector writes are limited near the theoretical minimum --- src/fs/bpb.rs | 17 ++- src/fs/file.rs | 5 + src/fs/fs.rs | 272 +++++++++++++++++++++++++++++++++++++----------- src/fs/tests.rs | 40 +++++++ 4 files changed, 271 insertions(+), 63 deletions(-) diff --git a/src/fs/bpb.rs b/src/fs/bpb.rs index 28b8217..d234699 100644 --- a/src/fs/bpb.rs +++ b/src/fs/bpb.rs @@ -2,6 +2,7 @@ use super::*; use core::fmt; +use bitfield_struct::bitfield; use serde::{Deserialize, Serialize}; use serde_big_array::BigArray; @@ -215,6 +216,20 @@ pub(crate) struct EBRFAT12_16 { pub signature: u16, } +#[bitfield(u16, order = Lsb)] +#[derive(Deserialize, Serialize)] +pub(crate) struct FAT32ExtendedFlags { + #[bits(4)] + #[allow(non_snake_case)] + pub(crate) active_FAT: u8, + #[bits(3)] + _reserved: _, + #[bits(1)] + pub(crate) mirroring_disabled: bool, + #[bits(8)] + _reserved: _, +} + // FIXME: these might be the other way around #[derive(Deserialize, Serialize, Debug, Clone, Copy)] pub(crate) struct FATVersion { @@ -225,7 +240,7 @@ pub(crate) struct FATVersion { #[derive(Deserialize, Serialize, Clone, Copy)] pub(crate) struct EBRFAT32 { pub table_size_32: u32, - pub _extended_flags: u16, + pub extended_flags: FAT32ExtendedFlags, pub fat_version: FATVersion, pub root_cluster: u32, pub fat_info: u16, diff --git a/src/fs/file.rs b/src/fs/file.rs index 0e888d1..b415bcf 100644 --- a/src/fs/file.rs +++ b/src/fs/file.rs @@ -560,6 +560,11 @@ where for clusters_allocated in 0..clusters_to_allocate { match self.fs.next_free_cluster()? { Some(next_free_cluster) => { + // FIXME: in FAT12 filesystems, this can cause a sector + // to be updated up to 4 times for seeminly no reason + // Similar behavour is observed in FAT16/32, with 2 sync operations + // THis number should be halved for both cases + // we set the last allocated cluster to point to the next free one self.fs.write_nth_FAT_entry( last_cluster_in_chain, diff --git a/src/fs/fs.rs b/src/fs/fs.rs index e9ce95b..5843d83 100644 --- a/src/fs/fs.rs +++ b/src/fs/fs.rs @@ -85,8 +85,8 @@ impl From<&FATEntry> for u32 { /// Properties about the position of a [`FATEntry`] inside the FAT region struct FATEntryProps { - /// Each `n`th element of the vector points at the corrensponding sector at the `n+1`th FAT table - fat_sectors: Vec, + /// Each `n`th element of the vector points at the corrensponding sector at the (first) active FAT table + fat_sector: u32, sector_offset: usize, } @@ -97,18 +97,64 @@ impl FATEntryProps { S: Read + Write + Seek, { let fat_byte_offset: u32 = n * fs.fat_type.bits_per_entry() as u32 / 8; - let mut fat_sectors = Vec::new(); - for nth_table in 0..fs.props.fat_table_count { - let table_sector_offset = fs.boot_record.nth_FAT_table_sector(nth_table); - let fat_sector = table_sector_offset + fat_byte_offset / fs.props.sector_size; - fat_sectors.push(fat_sector); - } + let fat_sector = + u32::from(fs.props.first_fat_sector) + fat_byte_offset / fs.props.sector_size; let sector_offset: usize = (fat_byte_offset % fs.props.sector_size) as usize; FATEntryProps { - fat_sectors, + fat_sector, + sector_offset, + } + } +} + +/// Properties about the position of a sector within the FAT +struct FATSectorProps { + /// the sector belongs to this FAT copy + #[allow(unused)] + fat_offset: u8, + /// the sector is that many away from the start of the FAT copy + sector_offset: u32, +} + +impl FATSectorProps { + /// Returns [`None`] if this sector doesn't belong to a FAT table + pub fn new(sector: u64, fs: &FileSystem) -> Option + where + S: Read + Write + Seek, + { + if !fs.sector_belongs_to_FAT(sector) { + return None; + } + + let sector_offset_from_first_fat: u64 = sector - u64::from(fs.props.first_fat_sector); + let fat_offset = (sector_offset_from_first_fat / u64::from(fs.props.fat_sector_size)) as u8; + let sector_offset = + (sector_offset_from_first_fat % u64::from(fs.props.fat_sector_size)) as u32; + + Some(FATSectorProps { + fat_offset, sector_offset, + }) + } + + #[allow(non_snake_case)] + pub fn get_corresponding_FAT_sectors(&self, fs: &FileSystem) -> Vec + where + S: Read + Write + Seek, + { + let mut vec = Vec::new(); + + for i in 0..fs.props.fat_table_count { + vec.push( + (u32::from(fs.props.first_fat_sector) + + u32::from(i) * fs.props.fat_sector_size + + self.sector_offset) + .into(), + ) } + + vec } } @@ -436,6 +482,8 @@ pub(crate) struct FSProperties { pub(crate) total_clusters: u32, /// sector offset of the FAT pub(crate) fat_table_count: u8, + pub(crate) fat_sector_size: u32, + pub(crate) first_fat_sector: u16, pub(crate) first_root_dir_sector: u16, pub(crate) first_data_sector: u32, } @@ -487,6 +535,9 @@ where // since `self.boot_record.fat_type()` calls like 5 nested functions, we keep this cached and expose it with a public getter function fat_type: FATType, pub(crate) props: FSProperties, + // this doesn't mean that this is the first free cluster, it just means + // that if we want to figure that out, we should start from this cluster + first_free_cluster: u32, filter: FileFilter, } @@ -604,6 +655,16 @@ where } }; + let first_fat_sector = match boot_record { + BootRecord::FAT(boot_record_fat) => boot_record_fat.first_fat_sector().into(), + BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT not yet implemented"), + }; + + let fat_sector_size = match boot_record { + BootRecord::FAT(boot_record_fat) => boot_record_fat.fat_sector_size().into(), + BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT not yet implemented"), + }; + let first_root_dir_sector = match boot_record { BootRecord::FAT(boot_record_fat) => boot_record_fat.first_root_dir_sector().into(), BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), @@ -633,6 +694,8 @@ where sector_size, cluster_size, fat_table_count, + fat_sector_size, + first_fat_sector, total_sectors, total_clusters, first_root_dir_sector, @@ -647,6 +710,7 @@ where boot_record, fat_type, props, + first_free_cluster: RESERVED_FAT_ENTRIES, filter: FileFilter::default(), }; @@ -734,7 +798,7 @@ where pub(crate) fn next_free_cluster(&mut self) -> Result, S::Error> { let start_cluster = match self.boot_record { BootRecord::FAT(boot_record_fat) => { - let mut first_free_cluster = RESERVED_FAT_ENTRIES; + let mut first_free_cluster = self.first_free_cluster; if let EBR::FAT32(_, fsinfo) = boot_record_fat.ebr { // a value of u32::MAX denotes unawareness of the first free cluster @@ -743,7 +807,8 @@ where if fsinfo.first_free_cluster != u32::MAX && fsinfo.first_free_cluster <= self.props.total_sectors { - first_free_cluster = fsinfo.first_free_cluster + first_free_cluster = + cmp::min(self.first_free_cluster, fsinfo.first_free_cluster); } } @@ -756,12 +821,26 @@ where while current_cluster < self.props.total_clusters { match self.read_nth_FAT_entry(current_cluster)? { - FATEntry::Free => return Ok(Some(current_cluster)), + FATEntry::Free => { + self.first_free_cluster = current_cluster; + + match &mut self.boot_record { + BootRecord::FAT(boot_record_fat) => { + if let EBR::FAT32(_, fsinfo) = &mut boot_record_fat.ebr { + fsinfo.first_free_cluster = current_cluster; + } + } + BootRecord::ExFAT(_) => todo!("ExFAT not yet implemented"), + } + + return Ok(Some(current_cluster)); + } _ => (), } current_cluster += 1; } + self.first_free_cluster = self.props.total_clusters - 1; Ok(None) } @@ -816,6 +895,16 @@ where Ok(true) } + #[allow(non_snake_case)] + pub(crate) fn sector_belongs_to_FAT(&self, sector: u64) -> bool { + match self.boot_record { + BootRecord::FAT(boot_record_fat) => (boot_record_fat.first_fat_sector().into() + ..boot_record_fat.first_root_dir_sector().into()) + .contains(§or), + BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT not yet implemented"), + } + } + /// Read the nth sector from the partition's beginning and store it in [`self.sector_buffer`](Self::sector_buffer) /// /// This function also returns an immutable reference to [`self.sector_buffer`](Self::sector_buffer) @@ -843,7 +932,7 @@ where let entry_size = self.fat_type.entry_size(); let entry_props = FATEntryProps::new(n, &self); - self.read_nth_sector(entry_props.fat_sectors[0].into())?; + self.read_nth_sector(entry_props.fat_sector.into())?; let mut value_bytes = [0_u8; 4]; let bytes_to_read: usize = cmp::min( @@ -857,7 +946,7 @@ where // in FAT12, FAT entries may be split between two different sectors if self.fat_type == FATType::FAT12 && (bytes_to_read as u32) < entry_size { - self.read_nth_sector((entry_props.fat_sectors[0] + 1).into())?; + self.read_nth_sector((entry_props.fat_sector + 1).into())?; value_bytes[bytes_to_read..entry_size as usize] .copy_from_slice(&self.sector_buffer[..(entry_size as usize - bytes_to_read)]); @@ -955,75 +1044,134 @@ where value <<= 4; } - // we update all the FAT copies - for fat_sector in entry_props.fat_sectors { - self.read_nth_sector(fat_sector.into())?; - - let value_bytes = value.to_le_bytes(); + self.read_nth_sector(entry_props.fat_sector.into())?; - let mut first_byte = value_bytes[0]; + let value_bytes = value.to_le_bytes(); - if should_shift { - let mut old_byte = self.sector_buffer[entry_props.sector_offset]; - // ignore the high 4 bytes of the old entry - old_byte &= 0x0F; - // OR it with the new value - first_byte |= old_byte; - } + let mut first_byte = value_bytes[0]; - self.sector_buffer[entry_props.sector_offset] = first_byte; // this shouldn't panic - self.buffer_modified = true; + if should_shift { + let mut old_byte = self.sector_buffer[entry_props.sector_offset]; + // ignore the high 4 bytes of the old entry + old_byte &= 0x0F; + // OR it with the new value + first_byte |= old_byte; + } - let bytes_left_on_sector: usize = cmp::min( - entry_size as usize, - self.sector_size() as usize - entry_props.sector_offset, - ); + self.sector_buffer[entry_props.sector_offset] = first_byte; // this shouldn't panic + self.buffer_modified = true; - if bytes_left_on_sector < entry_size as usize { - // looks like this FAT12 entry spans multiple sectors, we must also update the other one - self.read_nth_sector((fat_sector + 1).into())?; - } + let bytes_left_on_sector: usize = cmp::min( + entry_size as usize, + self.sector_size() as usize - entry_props.sector_offset, + ); - let mut second_byte = value_bytes[1]; - let second_byte_index = - (entry_props.sector_offset + 1) % self.sector_size() as usize; - if !should_shift { - let mut old_byte = self.sector_buffer[second_byte_index]; - // ignore the low 4 bytes of the old entry - old_byte &= 0xF0; - // OR it with the new value - second_byte |= old_byte; - } + if bytes_left_on_sector < entry_size as usize { + // looks like this FAT12 entry spans multiple sectors, we must also update the other one + self.read_nth_sector((entry_props.fat_sector + 1).into())?; + } - self.sector_buffer[second_byte_index] = second_byte; // this shouldn't panic - self.buffer_modified = true; + let mut second_byte = value_bytes[1]; + let second_byte_index = + (entry_props.sector_offset + 1) % self.sector_size() as usize; + if !should_shift { + let mut old_byte = self.sector_buffer[second_byte_index]; + // ignore the low 4 bytes of the old entry + old_byte &= 0xF0; + // OR it with the new value + second_byte |= old_byte; } + + self.sector_buffer[second_byte_index] = second_byte; // this shouldn't panic + self.buffer_modified = true; } FATType::FAT16 | FATType::FAT32 => { - // we update all the FAT copies - for fat_sector in entry_props.fat_sectors { - self.read_nth_sector(fat_sector.into())?; + self.read_nth_sector(entry_props.fat_sector.into())?; - let value_bytes = value.to_le_bytes(); + let value_bytes = value.to_le_bytes(); - self.sector_buffer[entry_props.sector_offset - ..entry_props.sector_offset + entry_size as usize] - .copy_from_slice(&value_bytes[..entry_size as usize]); // this shouldn't panic - self.buffer_modified = true; - } + self.sector_buffer + [entry_props.sector_offset..entry_props.sector_offset + entry_size as usize] + .copy_from_slice(&value_bytes[..entry_size as usize]); // this shouldn't panic + self.buffer_modified = true; } FATType::ExFAT => todo!("ExFAT not yet implemented"), }; + if entry == FATEntry::Free && n < self.first_free_cluster { + self.first_free_cluster = n; + } + + // lastly, update the FSInfoFAT32 structure is it is available + match &mut self.boot_record { + BootRecord::FAT(boot_record_fat) => match &mut boot_record_fat.ebr { + EBR::FAT32(_, fsinfo) => { + match entry { + FATEntry::Free => { + fsinfo.free_cluster_count += 1; + if n < fsinfo.first_free_cluster { + fsinfo.first_free_cluster = n; + } + } + _ => fsinfo.free_cluster_count -= 1, + }; + } + _ => (), + }, + _ => (), + } + + Ok(()) + } + + /// Syncs `self.sector_buffer` back to the storage + fn _sync_current_sector(&mut self) -> Result<(), S::Error> { + self.storage.write_all(&self.sector_buffer)?; + self.storage + .seek(SeekFrom::Current(-i64::from(self.props.sector_size)))?; + + Ok(()) + } + + /// Syncs a FAT sector to ALL OTHER FAT COPIES on the device medium + #[allow(non_snake_case)] + fn _sync_FAT_sector(&mut self, fat_sector_props: &FATSectorProps) -> Result<(), S::Error> { + let current_offset = self.storage.stream_position()?; + + for sector in fat_sector_props.get_corresponding_FAT_sectors(self) { + self.storage + .seek(SeekFrom::Start(sector * u64::from(self.props.sector_size)))?; + self.storage.write_all(&self.sector_buffer)?; + } + + self.storage.seek(SeekFrom::Start(current_offset))?; + Ok(()) } pub(crate) fn sync_sector_buffer(&mut self) -> Result<(), S::Error> { if self.buffer_modified { - log::trace!("syncing sector {:?}", self.stored_sector); - self.storage.write_all(&self.sector_buffer)?; - self.storage - .seek(SeekFrom::Current(-i64::from(self.props.sector_size)))?; + if let Some(fat_sector_props) = FATSectorProps::new(self.stored_sector, self) { + log::trace!("syncing FAT sector {}", fat_sector_props.sector_offset,); + match self.boot_record { + BootRecord::FAT(boot_record_fat) => match boot_record_fat.ebr { + EBR::FAT12_16(_) => { + self._sync_FAT_sector(&fat_sector_props)?; + } + EBR::FAT32(ebr_fat32, _) => { + if ebr_fat32.extended_flags.mirroring_disabled() { + self._sync_current_sector()?; + } else { + self._sync_FAT_sector(&fat_sector_props)?; + } + } + }, + BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT not yet implemented"), + } + } else { + log::trace!("syncing sector {}", self.stored_sector); + self._sync_current_sector()?; + } } self.buffer_modified = false; diff --git a/src/fs/tests.rs b/src/fs/tests.rs index ca2c072..4f3bf42 100644 --- a/src/fs/tests.rs +++ b/src/fs/tests.rs @@ -466,6 +466,46 @@ fn remove_fat32_file() { } } +#[test] +#[allow(non_snake_case)] +fn FAT_tables_after_fat32_write_are_identical() { + use crate::fs::{BootRecord, EBR}; + + use std::io::Cursor; + + let mut storage = Cursor::new(FAT32.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + match fs.boot_record { + BootRecord::FAT(boot_record_fat) => match boot_record_fat.ebr { + EBR::FAT32(ebr_fat32, _) => assert!( + !ebr_fat32.extended_flags.mirroring_disabled(), + "mirroring should be enabled for this .img file" + ), + _ => unreachable!(), + }, + _ => unreachable!(), + } + + assert!( + fs.FAT_tables_are_identical().unwrap(), + concat!( + "this should pass. ", + "if it doesn't, either the corresponding .img file's FAT tables aren't identical", + "or the tables_are_identical function doesn't work correctly" + ) + ); + + // let's write the bee movie script to root.txt (why not), check, truncate the file, then check again + let mut file = fs.get_rw_file(PathBuf::from("hello.txt")).unwrap(); + + file.write_all(BEE_MOVIE_SCRIPT.as_bytes()).unwrap(); + assert!(file.fs.FAT_tables_are_identical().unwrap()); + + file.truncate(10_000).unwrap(); + assert!(file.fs.FAT_tables_are_identical().unwrap()); +} + #[test] fn assert_img_fat_type() { static TEST_CASES: &[(&[u8], FATType)] = &[ From d43d06adec0dd2b307db9a93fa3fc070a5b539bf Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sat, 7 Sep 2024 17:48:49 +0300 Subject: [PATCH 18/40] chore: rename src/fs/ to src/fat/ --- src/{fs => fat}/bpb.rs | 0 src/{fs => fat}/consts.rs | 0 src/{fs => fat}/direntry.rs | 0 src/{fs => fat}/file.rs | 0 src/{fs => fat}/fs.rs | 0 src/{fs => fat}/mod.rs | 0 src/{fs => fat}/tests.rs | 4 ++-- src/lib.rs | 4 ++-- 8 files changed, 4 insertions(+), 4 deletions(-) rename src/{fs => fat}/bpb.rs (100%) rename src/{fs => fat}/consts.rs (100%) rename src/{fs => fat}/direntry.rs (100%) rename src/{fs => fat}/file.rs (100%) rename src/{fs => fat}/fs.rs (100%) rename src/{fs => fat}/mod.rs (100%) rename src/{fs => fat}/tests.rs (99%) diff --git a/src/fs/bpb.rs b/src/fat/bpb.rs similarity index 100% rename from src/fs/bpb.rs rename to src/fat/bpb.rs diff --git a/src/fs/consts.rs b/src/fat/consts.rs similarity index 100% rename from src/fs/consts.rs rename to src/fat/consts.rs diff --git a/src/fs/direntry.rs b/src/fat/direntry.rs similarity index 100% rename from src/fs/direntry.rs rename to src/fat/direntry.rs diff --git a/src/fs/file.rs b/src/fat/file.rs similarity index 100% rename from src/fs/file.rs rename to src/fat/file.rs diff --git a/src/fs/fs.rs b/src/fat/fs.rs similarity index 100% rename from src/fs/fs.rs rename to src/fat/fs.rs diff --git a/src/fs/mod.rs b/src/fat/mod.rs similarity index 100% rename from src/fs/mod.rs rename to src/fat/mod.rs diff --git a/src/fs/tests.rs b/src/fat/tests.rs similarity index 99% rename from src/fs/tests.rs rename to src/fat/tests.rs index 4f3bf42..1ea59e3 100644 --- a/src/fs/tests.rs +++ b/src/fat/tests.rs @@ -11,7 +11,7 @@ static FAT32: &[u8] = include_bytes!("../../imgs/fat32.img"); #[test] #[allow(non_snake_case)] fn check_FAT_offset() { - use crate::fs::BootRecord; + use crate::fat::BootRecord; use std::io::Cursor; @@ -469,7 +469,7 @@ fn remove_fat32_file() { #[test] #[allow(non_snake_case)] fn FAT_tables_after_fat32_write_are_identical() { - use crate::fs::{BootRecord, EBR}; + use crate::fat::{BootRecord, EBR}; use std::io::Cursor; diff --git a/src/lib.rs b/src/lib.rs index 48ad60d..d8e3e25 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,11 +98,11 @@ extern crate alloc; mod error; -mod fs; +mod fat; pub mod io; mod path; mod utils; pub use error::*; -pub use fs::*; +pub use fat::*; pub use path::*; From 66b1f780f18ffe803b322e6b20817ce29b0b7c1f Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sat, 7 Sep 2024 18:19:53 +0300 Subject: [PATCH 19/40] fix: correctly handle calling RW methods on RO storage mediums --- src/error.rs | 20 +++++++++----------- src/fat/fs.rs | 20 ++++++++++++++++---- src/io.rs | 2 +- src/utils/io.rs | 19 +++++++++++++++++++ src/utils/mod.rs | 1 + 5 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 src/utils/io.rs diff --git a/src/error.rs b/src/error.rs index a5d31b2..a4547a7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -48,8 +48,8 @@ pub trait IOErrorKind: PartialEq + Sized { fn new_interrupted() -> Self; /// Create a new `InvalidData` [`IOErrorKind`] fn new_invalid_data() -> Self; - /// Create a new `ReadOnlyFilesystem` [`IOErrorKind`] - fn new_readonly_filesystem() -> Self; + /// Create a new `Unsupported` [`IOErrorKind`] + fn new_unsupported() -> Self; #[inline] /// Check whether this [`IOErrorKind`] is of kind `UnexpectedEOF` @@ -66,12 +66,11 @@ pub trait IOErrorKind: PartialEq + Sized { fn is_invalid_data(&self) -> bool { self == &Self::new_invalid_data() } - // when the `io_error_more` feature gets merged, uncomment this - // /// Check whether this [`IOErrorKind`] is of kind `InvalidData` - // #[inline] - // fn is_readonly_filesystem(&self) -> bool { - // self == &Self::new_readonly_filesystem() - // } + /// Check whether this [`IOErrorKind`] is of kind `Unsupported` + #[inline] + fn is_unsupported(&self) -> bool { + self == &Self::new_unsupported() + } } #[cfg(feature = "std")] @@ -89,9 +88,8 @@ impl IOErrorKind for std::io::ErrorKind { std::io::ErrorKind::InvalidData } #[inline] - fn new_readonly_filesystem() -> Self { - // Unfortunately the ReadOnlyFilesystem ErrorKind is locked behind a feature flag (for now) - std::io::ErrorKind::Other + fn new_unsupported() -> Self { + std::io::ErrorKind::Unsupported } } diff --git a/src/fat/fs.rs b/src/fat/fs.rs index 5843d83..74a4786 100644 --- a/src/fat/fs.rs +++ b/src/fat/fs.rs @@ -1150,6 +1150,8 @@ where } pub(crate) fn sync_sector_buffer(&mut self) -> Result<(), S::Error> { + self._raise_io_rw_result()?; + if self.buffer_modified { if let Some(fat_sector_props) = FATSectorProps::new(self.stored_sector, self) { log::trace!("syncing FAT sector {}", fat_sector_props.sector_offset,); @@ -1177,6 +1179,19 @@ where Ok(()) } + + /// Returns an `Err` of `Unexpected [`IOErrorKind`] + /// if the device medium is read-only + fn _raise_io_rw_result(&mut self) -> Result<(), S::Error> { + if !utils::io::storage_medium_is_rw(&mut self.storage)? { + return Err(S::Error::new( + ::Kind::new_unsupported(), + "the storage medium is read-only", + )); + } + + Ok(()) + } } /// Public [`Read`]-related functions @@ -1292,10 +1307,7 @@ where /// /// Fails if `path` doesn't represent a file, or if that file doesn't exist pub fn get_rw_file(&mut self, path: PathBuf) -> FSResult, S::Error> { - // we first write an empty array to the storage medium - // if the storage has Write functionality, this shouldn't error, - // otherwise it should return an error. - self.storage.write_all(&[])?; + self._raise_io_rw_result()?; let ro_file = self.get_ro_file(path)?; if ro_file.attributes.read_only { diff --git a/src/io.rs b/src/io.rs index b0b8f2c..2d39e82 100644 --- a/src/io.rs +++ b/src/io.rs @@ -141,7 +141,7 @@ pub trait Read: IOBase { /// A simplified version of [`std::io::Write`] for use within a `no_std` context /// /// Even if the storage medium doesn't have [`Write`] functionality, it should implement -/// this trait and return an [`IOErrorKind`] of type `ReadOnlyFilesystem` for all methods. +/// this trait and return an [`IOErrorKind`] of type `Unsupported` for all methods. /// This way, in case a [`Write`]-related method is called for a [`FileSystem`](crate::FileSystem), /// it will return that error. pub trait Write: IOBase { diff --git a/src/utils/io.rs b/src/utils/io.rs new file mode 100644 index 0000000..f6b2310 --- /dev/null +++ b/src/utils/io.rs @@ -0,0 +1,19 @@ +use crate::error::{IOError, IOErrorKind}; +use crate::io::prelude::*; + +/// Returns a `Result`, where the `bool` indicates whether +/// the storage medium support write operations or not +pub(crate) fn storage_medium_is_rw(storage: &mut S) -> Result::Error> +where + S: Read + Write + Seek, +{ + // this zero-length write should return an error + // of `Unexpected` IOErrorKind is the storage medium is read-only + match storage.write_all(&[]) { + Ok(_) => Ok(true), + // in case this filesystem doesn't support + // write operations, don't error out + Err(ref e) if e.kind().is_unsupported() => Ok(false), + Err(e) => return Err(e), + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 26f85f5..71979c7 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,3 +1,4 @@ pub(crate) mod bincode; pub(crate) mod bits; +pub(crate) mod io; pub(crate) mod string; From 9e61bd88c3b8e5ae3966627dcd8f2492b563af5e Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sat, 7 Sep 2024 18:29:46 +0300 Subject: [PATCH 20/40] chore: create a proper constructor for the FSProperties struct --- src/fat/fs.rs | 119 +++++++++++++++++++++++++------------------------- 1 file changed, 59 insertions(+), 60 deletions(-) diff --git a/src/fat/fs.rs b/src/fat/fs.rs index 74a4786..d87c325 100644 --- a/src/fat/fs.rs +++ b/src/fat/fs.rs @@ -488,6 +488,63 @@ pub(crate) struct FSProperties { pub(crate) first_data_sector: u32, } +impl FSProperties { + fn from_boot_record(boot_record: &BootRecord) -> Self { + let sector_size = match boot_record { + BootRecord::FAT(boot_record_fat) => boot_record_fat.bpb.bytes_per_sector.into(), + BootRecord::ExFAT(boot_record_exfat) => 1 << boot_record_exfat.sector_shift, + }; + let cluster_size = match boot_record { + BootRecord::FAT(boot_record_fat) => { + (boot_record_fat.bpb.sectors_per_cluster as u32 * sector_size).into() + } + BootRecord::ExFAT(boot_record_exfat) => { + 1 << (boot_record_exfat.sector_shift + boot_record_exfat.cluster_shift) + } + }; + let total_sectors = match boot_record { + BootRecord::FAT(boot_record_fat) => boot_record_fat.total_sectors(), + BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), + }; + let total_clusters = match boot_record { + BootRecord::FAT(boot_record_fat) => boot_record_fat.total_clusters(), + BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), + }; + let fat_table_count = match boot_record { + BootRecord::FAT(boot_record_fat) => boot_record_fat.bpb.table_count, + BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), + }; + let fat_sector_size = match boot_record { + BootRecord::FAT(boot_record_fat) => boot_record_fat.fat_sector_size().into(), + BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT not yet implemented"), + }; + let first_fat_sector = match boot_record { + BootRecord::FAT(boot_record_fat) => boot_record_fat.first_fat_sector().into(), + BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT not yet implemented"), + }; + let first_root_dir_sector = match boot_record { + BootRecord::FAT(boot_record_fat) => boot_record_fat.first_root_dir_sector().into(), + BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), + }; + let first_data_sector = match boot_record { + BootRecord::FAT(boot_record_fat) => boot_record_fat.first_data_sector().into(), + BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), + }; + + FSProperties { + sector_size, + cluster_size, + fat_table_count, + fat_sector_size, + first_fat_sector, + total_sectors, + total_clusters, + first_root_dir_sector, + first_data_sector, + } + } +} + /// Filter (or not) things like hidden files/directories /// for FileSystem operations #[derive(Debug)] @@ -642,69 +699,11 @@ where BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT not yet implemented"), }; - let sector_size: u32 = match boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.bpb.bytes_per_sector.into(), - BootRecord::ExFAT(boot_record_exfat) => 1 << boot_record_exfat.sector_shift, - }; - let cluster_size: u64 = match boot_record { - BootRecord::FAT(boot_record_fat) => { - (boot_record_fat.bpb.sectors_per_cluster as u32 * sector_size).into() - } - BootRecord::ExFAT(boot_record_exfat) => { - 1 << (boot_record_exfat.sector_shift + boot_record_exfat.cluster_shift) - } - }; - - let first_fat_sector = match boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.first_fat_sector().into(), - BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT not yet implemented"), - }; - - let fat_sector_size = match boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.fat_sector_size().into(), - BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT not yet implemented"), - }; - - let first_root_dir_sector = match boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.first_root_dir_sector().into(), - BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), - }; - - let first_data_sector = match boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.first_data_sector().into(), - BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), - }; - - let fat_table_count = match boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.bpb.table_count, - BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), - }; - - let total_sectors = match boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.total_sectors(), - BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), - }; - - let total_clusters = match boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.total_clusters(), - BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), - }; - - let props = FSProperties { - sector_size, - cluster_size, - fat_table_count, - fat_sector_size, - first_fat_sector, - total_sectors, - total_clusters, - first_root_dir_sector, - first_data_sector, - }; + let props = FSProperties::from_boot_record(&boot_record); let mut fs = Self { storage, - sector_buffer: buffer[..sector_size as usize].to_vec(), + sector_buffer: buffer[..props.sector_size as usize].to_vec(), buffer_modified: false, stored_sector, boot_record, From fa7380d11f4a4a7005b1efc1d99fedb9a1a3216e Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sat, 7 Sep 2024 18:46:17 +0300 Subject: [PATCH 21/40] chore: Update README.md --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 07f01eb..bcbff14 100644 --- a/README.md +++ b/README.md @@ -29,13 +29,13 @@ A fully-working FAT driver that covers the following criteria: - [x] FAT12 support (just handle entries between 2 sectors) - [x] Distinguish between dirs and files in paths (this must also be verified by the filesystem, just like in the `std`) -- [ ] Check whether system endianness matters (FAT is little-endian) +- [x] Check whether system endianness matters (FAT is little-endian) + PS: it does in fact matter. [bincode](https://crates.io/crates/bincode), which we use for (de)serialization allows us to configure the default endianess - [ ] Handle non-printable characters in names of files and directories - [ ] ExFAT support -- [ ] when [feature(error_in_core)](https://github.com/rust-lang/rust/issues/103765) gets released to stable, bump MSRV & use the `core::error::Error` trait instead of our custom `error::Error` - -[crates.io]: https://crates.io -[rafalh's rust-fatfs]: https://github.com/rafalh/rust-fatfs +- [x] ~~when [feature(error_in_core)](https://github.com/rust-lang/rust/issues/103765) gets released to stable, bump MSRV & use the `core::error::Error` trait instead of our custom `error::Error`~~ + this feature is now stabilized. However, [after a couple of community recommendations](https://www.reddit.com/r/rust/comments/1ejukow/comment/lgg3dtb/), we will be switching to [embedded-io] in the near future, which has its own `Error` trait, so that means... +- [ ] replace custom `io` implementation with the [embedded-io] crate ## Acknowledgements @@ -44,3 +44,7 @@ This project adheres to [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) ## License [MIT](LICENSE) + +[crates.io]: https://crates.io +[rafalh's rust-fatfs]: https://github.com/rafalh/rust-fatfs +[embedded-io]: https://crates.io/crates/embedded-io From 5fb88c9a3fe304209ba9ce2315579ec54b0048df Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sun, 8 Sep 2024 15:02:16 +0300 Subject: [PATCH 22/40] refactor: minor entry chain-related changes --- src/fat/direntry.rs | 26 +++++++++++++++++--------- src/fat/file.rs | 10 +++++----- src/fat/fs.rs | 16 +++++++++------- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/fat/direntry.rs b/src/fat/direntry.rs index e396efe..335554e 100644 --- a/src/fat/direntry.rs +++ b/src/fat/direntry.rs @@ -271,36 +271,44 @@ impl LFNEntry { } } -/// The location of a [`FATDirEntry`] within a root directory sector -/// or a data region cluster -#[derive(Debug, Clone)] -pub(crate) enum EntryLocation { +/// The root directory sector or data cluster a [`FATDirEntry`] belongs too +#[derive(Debug, Clone, Copy)] +pub(crate) enum EntryLocationUnit { /// Sector offset from the start of the root directory region (FAT12/16) RootDirSector(u16), /// Cluster offset from the start of the data region DataCluster(u32), } -impl EntryLocation { +impl EntryLocationUnit { pub(crate) fn from_partition_sector(sector: u32, fs: &mut FileSystem) -> Self where S: Read + Write + Seek, { if sector < fs.first_data_sector() { - EntryLocation::RootDirSector((sector - fs.props.first_root_dir_sector as u32) as u16) + EntryLocationUnit::RootDirSector( + (sector - fs.props.first_root_dir_sector as u32) as u16, + ) } else { - EntryLocation::DataCluster(fs.partition_sector_to_data_cluster(sector)) + EntryLocationUnit::DataCluster(fs.partition_sector_to_data_cluster(sector)) } } } +/// The location of a [`FATDirEntry`] +#[derive(Debug)] +pub(crate) struct EntryLocation { + /// the location of the first corresponding entry's data unit + pub(crate) unit: EntryLocationUnit, + /// the first entry's index/offset from the start of the data unit + pub(crate) index: u32, +} + /// The location of a chain of [`FATDirEntry`] #[derive(Debug)] pub(crate) struct DirEntryChain { /// the location of the first corresponding entry pub(crate) location: EntryLocation, - /// the first entry's index/offset from the start of the sector - pub(crate) index: u32, /// how many (contiguous) entries this entry chain has pub(crate) len: u32, } diff --git a/src/fat/file.rs b/src/fat/file.rs index b415bcf..dc4bfcf 100644 --- a/src/fat/file.rs +++ b/src/fat/file.rs @@ -354,22 +354,22 @@ where pub fn remove(mut self) -> Result<(), ::Error> { // we begin by removing the corresponding entries... let mut entries_freed = 0; - let mut current_offset = self.props.entry.chain_props.index; + let mut current_offset = self.props.entry.chain.location.index; // current_cluster_option is `None` if we are dealing with a root directory entry let (mut current_sector, current_cluster_option): (u32, Option) = - match self.props.entry.chain_props.location { - EntryLocation::RootDirSector(root_dir_sector) => ( + match self.props.entry.chain.location.unit { + EntryLocationUnit::RootDirSector(root_dir_sector) => ( (root_dir_sector + self.fs.props.first_root_dir_sector).into(), None, ), - EntryLocation::DataCluster(data_cluster) => ( + EntryLocationUnit::DataCluster(data_cluster) => ( self.fs.data_cluster_to_partition_sector(data_cluster), Some(data_cluster), ), }; - while entries_freed < self.props.entry.chain_props.len { + while entries_freed < self.props.entry.chain.len { if current_sector as u64 != self.fs.stored_sector { self.fs.read_nth_sector(current_sector.into())?; } diff --git a/src/fat/fs.rs b/src/fat/fs.rs index d87c325..c5d8ccf 100644 --- a/src/fat/fs.rs +++ b/src/fat/fs.rs @@ -170,7 +170,7 @@ pub(crate) struct RawProperties { pub(crate) file_size: u32, pub(crate) data_cluster: u32, - pub(crate) chain_props: DirEntryChain, + pub(crate) chain: DirEntryChain, } /// A container for file/directory properties @@ -185,7 +185,7 @@ pub struct Properties { pub(crate) data_cluster: u32, // internal fields - pub(crate) chain_props: DirEntryChain, + pub(crate) chain: DirEntryChain, } /// Getter methods @@ -247,7 +247,7 @@ impl Properties { accessed: raw.accessed, file_size: raw.file_size, data_cluster: raw.data_cluster, - chain_props: raw.chain_props, + chain: raw.chain, } } } @@ -311,7 +311,7 @@ impl EntryParser { { use utils::bincode::bincode_config; - let entry_location = EntryLocation::from_partition_sector(sector, fs); + let entry_location_unit = EntryLocationUnit::from_partition_sector(sector, fs); for (index, chunk) in fs .read_nth_sector(sector.into())? @@ -333,8 +333,10 @@ impl EntryParser { Some(current_chain) => current_chain.len += 1, None => { self.current_chain = Some(DirEntryChain { - location: entry_location.clone(), - index: index as u32, + location: EntryLocation { + index: index as u32, + unit: entry_location_unit, + }, len: 1, }) } @@ -401,7 +403,7 @@ impl EntryParser { accessed, file_size: entry.file_size, data_cluster: ((entry.cluster_high as u32) << 16) + entry.cluster_low as u32, - chain_props: self + chain: self .current_chain .take() .expect("at this point, this shouldn't be None"), From de8560925e53dc2fab27afcc84048712798d17ce Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sun, 8 Sep 2024 15:19:34 +0300 Subject: [PATCH 23/40] refactor: create modularized internal remove-related methods There is no need for these things to be part of the truncate method and not of the Filesystem struct --- src/fat/file.rs | 77 ++++----------------------------------------- src/fat/fs.rs | 84 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 71 deletions(-) diff --git a/src/fat/file.rs b/src/fat/file.rs index dc4bfcf..959c925 100644 --- a/src/fat/file.rs +++ b/src/fat/file.rs @@ -353,83 +353,18 @@ where /// Remove the current file from the [`FileSystem`] pub fn remove(mut self) -> Result<(), ::Error> { // we begin by removing the corresponding entries... - let mut entries_freed = 0; - let mut current_offset = self.props.entry.chain.location.index; - - // current_cluster_option is `None` if we are dealing with a root directory entry - let (mut current_sector, current_cluster_option): (u32, Option) = - match self.props.entry.chain.location.unit { - EntryLocationUnit::RootDirSector(root_dir_sector) => ( - (root_dir_sector + self.fs.props.first_root_dir_sector).into(), - None, - ), - EntryLocationUnit::DataCluster(data_cluster) => ( - self.fs.data_cluster_to_partition_sector(data_cluster), - Some(data_cluster), - ), - }; - - while entries_freed < self.props.entry.chain.len { - if current_sector as u64 != self.fs.stored_sector { - self.fs.read_nth_sector(current_sector.into())?; - } - - // we won't even bother zeroing the entire thing, just the first byte - let byte_offset = current_offset as usize * DIRENTRY_SIZE; - self.fs.sector_buffer[byte_offset] = UNUSED_ENTRY; - self.fs.buffer_modified = true; - - log::trace!( - "freed entry at sector {} with byte offset {}", - current_sector, - byte_offset - ); - - if current_offset + 1 >= (self.fs.sector_size() / DIRENTRY_SIZE as u32) { - // we have moved to a new sector - current_sector += 1; - - match current_cluster_option { - // data region - Some(mut current_cluster) => { - if self.fs.partition_sector_to_data_cluster(current_sector) - != current_cluster - { - current_cluster = self.fs.get_next_cluster(current_cluster)?.unwrap(); - current_sector = - self.fs.data_cluster_to_partition_sector(current_cluster); - } - } - None => (), - } - - current_offset = 0; - } else { - current_offset += 1 - } - - entries_freed += 1; - } + self.ro_file + .fs + .remove_entry_chain(&self.ro_file.props.entry.chain)?; // ... and then we free the data clusters // rewind back to the start of the file self.rewind()?; - loop { - let current_cluster = self.props.current_cluster; - let next_cluster_option = self.get_next_cluster()?; - - // free the current cluster - self.fs - .write_nth_FAT_entry(current_cluster, FATEntry::Free)?; - - // proceed to the next one, otherwise break - match next_cluster_option { - Some(next_cluster) => self.props.current_cluster = next_cluster, - None => break, - } - } + self.ro_file + .fs + .free_cluster_chain(self.props.current_cluster)?; Ok(()) } diff --git a/src/fat/fs.rs b/src/fat/fs.rs index c5d8ccf..8e67db0 100644 --- a/src/fat/fs.rs +++ b/src/fat/fs.rs @@ -1125,6 +1125,90 @@ where Ok(()) } + /// Mark the individual entries of a contiguous FAT entry chain as unused + /// + /// Note: No validation is done to check whether or not the chain is valid + pub(crate) fn remove_entry_chain(&mut self, chain: &DirEntryChain) -> Result<(), S::Error> { + // we begin by removing the corresponding entries... + let mut entries_freed = 0; + let mut current_offset = chain.location.index; + + // current_cluster_option is `None` if we are dealing with a root directory entry + let (mut current_sector, current_cluster_option): (u32, Option) = + match chain.location.unit { + EntryLocationUnit::RootDirSector(root_dir_sector) => ( + (root_dir_sector + self.props.first_root_dir_sector).into(), + None, + ), + EntryLocationUnit::DataCluster(data_cluster) => ( + self.data_cluster_to_partition_sector(data_cluster), + Some(data_cluster), + ), + }; + + while entries_freed < chain.len { + if current_sector as u64 != self.stored_sector { + self.read_nth_sector(current_sector.into())?; + } + + // we won't even bother zeroing the entire thing, just the first byte + let byte_offset = current_offset as usize * DIRENTRY_SIZE; + self.sector_buffer[byte_offset] = UNUSED_ENTRY; + self.buffer_modified = true; + + log::trace!( + "freed entry at sector {} with byte offset {}", + current_sector, + byte_offset + ); + + if current_offset + 1 >= (self.sector_size() / DIRENTRY_SIZE as u32) { + // we have moved to a new sector + current_sector += 1; + + match current_cluster_option { + // data region + Some(mut current_cluster) => { + if self.partition_sector_to_data_cluster(current_sector) != current_cluster + { + current_cluster = self.get_next_cluster(current_cluster)?.unwrap(); + current_sector = self.data_cluster_to_partition_sector(current_cluster); + } + } + None => (), + } + + current_offset = 0; + } else { + current_offset += 1 + } + + entries_freed += 1; + } + + Ok(()) + } + + /// Frees all the cluster in a cluster chain starting with `first_cluster` + pub(crate) fn free_cluster_chain(&mut self, first_cluster: u32) -> Result<(), S::Error> { + let mut current_cluster = first_cluster; + + loop { + let next_cluster_option = self.get_next_cluster(current_cluster)?; + + // free the current cluster + self.write_nth_FAT_entry(current_cluster, FATEntry::Free)?; + + // proceed to the next one, otherwise break + match next_cluster_option { + Some(next_cluster) => current_cluster = next_cluster, + None => break, + } + } + + Ok(()) + } + /// Syncs `self.sector_buffer` back to the storage fn _sync_current_sector(&mut self) -> Result<(), S::Error> { self.storage.write_all(&self.sector_buffer)?; From 61c83eda8ecdbebc780e78228db521ebaf816f34 Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sun, 8 Sep 2024 19:43:46 +0300 Subject: [PATCH 24/40] feat(time): Add a Clock trait that will be used for generating file timestamps --- Cargo.toml | 2 +- src/fat/direntry.rs | 5 ++--- src/fat/file.rs | 36 ++++++++++++++++++------------------ src/fat/fs.rs | 42 +++++++++++++++++++++++++++++------------- src/fat/tests.rs | 4 ++-- src/lib.rs | 3 ++- src/time.rs | 45 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 99 insertions(+), 38 deletions(-) create mode 100644 src/time.rs diff --git a/Cargo.toml b/Cargo.toml index 613312d..a84d169 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ time = { version = "0.3.36", default-features = false, features = [ "alloc", "pa [features] default = ["std"] -std = [] +std = ["time/std", "time/local-offset"] [dev-dependencies] test-log = "0.2.16" diff --git a/src/fat/direntry.rs b/src/fat/direntry.rs index 335554e..43f7e82 100644 --- a/src/fat/direntry.rs +++ b/src/fat/direntry.rs @@ -1,6 +1,7 @@ use super::*; use crate::io::prelude::*; +use crate::time::EPOCH; use core::{fmt, mem, num}; @@ -71,8 +72,6 @@ impl From for Attributes { } } -const START_YEAR: i32 = 1980; - #[bitfield(u16)] #[derive(Serialize, Deserialize)] pub(crate) struct TimeAttribute { @@ -115,7 +114,7 @@ impl TryFrom for Date { fn try_from(value: DateAttribute) -> Result { time::parsing::Parsed::new() - .with_year(i32::from(value.year()) + START_YEAR) + .with_year(i32::from(value.year()) + EPOCH.year()) .and_then(|parsed| parsed.with_month(value.month().try_into().ok()?)) .and_then(|parsed| parsed.with_day(num::NonZeroU8::new(value.day())?)) .map(|parsed| parsed.try_into().ok()) diff --git a/src/fat/file.rs b/src/fat/file.rs index 959c925..c4d5eaa 100644 --- a/src/fat/file.rs +++ b/src/fat/file.rs @@ -18,15 +18,15 @@ pub(crate) struct FileProps { /// A read-only file within a FAT filesystem #[derive(Debug)] -pub struct ROFile<'a, S> +pub struct ROFile<'a, 'b: 'a, S> where S: Read + Write + Seek, { - pub(crate) fs: &'a mut FileSystem, + pub(crate) fs: &'a mut FileSystem<'b, S>, pub(crate) props: FileProps, } -impl ops::Deref for ROFile<'_, S> +impl ops::Deref for ROFile<'_, '_, S> where S: Read + Write + Seek, { @@ -37,7 +37,7 @@ where } } -impl ops::DerefMut for ROFile<'_, S> +impl ops::DerefMut for ROFile<'_, '_, S> where S: Read + Write + Seek, { @@ -47,7 +47,7 @@ where } // Internal functions -impl ROFile<'_, S> +impl ROFile<'_, '_, S> where S: Read + Write + Seek, { @@ -118,14 +118,14 @@ where } } -impl IOBase for ROFile<'_, S> +impl IOBase for ROFile<'_, '_, S> where S: Read + Write + Seek, { type Error = S::Error; } -impl Read for ROFile<'_, S> +impl Read for ROFile<'_, '_, S> where S: Read + Write + Seek, { @@ -213,7 +213,7 @@ where } } -impl Seek for ROFile<'_, S> +impl Seek for ROFile<'_, '_, S> where S: Read + Write + Seek, { @@ -265,25 +265,25 @@ where /// /// To reduce a file's size, use the [`truncate`](RWFile::truncate) method #[derive(Debug)] -pub struct RWFile<'a, S> +pub struct RWFile<'a, 'b, S> where S: Read + Write + Seek, { - pub(crate) ro_file: ROFile<'a, S>, + pub(crate) ro_file: ROFile<'a, 'b, S>, } -impl<'a, S> ops::Deref for RWFile<'a, S> +impl<'a, 'b, S> ops::Deref for RWFile<'a, 'b, S> where S: Read + Write + Seek, { - type Target = ROFile<'a, S>; + type Target = ROFile<'a, 'b, S>; fn deref(&self) -> &Self::Target { &self.ro_file } } -impl ops::DerefMut for RWFile<'_, S> +impl ops::DerefMut for RWFile<'_, '_, S> where S: Read + Write + Seek, { @@ -293,7 +293,7 @@ where } // Public functions -impl RWFile<'_, S> +impl RWFile<'_, '_, S> where S: Read + Write + Seek, { @@ -370,14 +370,14 @@ where } } -impl IOBase for RWFile<'_, S> +impl IOBase for RWFile<'_, '_, S> where S: Read + Write + Seek, { type Error = S::Error; } -impl Read for RWFile<'_, S> +impl Read for RWFile<'_, '_, S> where S: Read + Write + Seek, { @@ -402,7 +402,7 @@ where } } -impl Write for RWFile<'_, S> +impl Write for RWFile<'_, '_, S> where S: Read + Write + Seek, { @@ -472,7 +472,7 @@ where } } -impl Seek for RWFile<'_, S> +impl Seek for RWFile<'_, '_, S> where S: Read + Write + Seek, { diff --git a/src/fat/fs.rs b/src/fat/fs.rs index 8e67db0..fa568e4 100644 --- a/src/fat/fs.rs +++ b/src/fat/fs.rs @@ -1,6 +1,6 @@ use super::*; -use crate::{error::*, io::prelude::*, path::PathBuf, utils}; +use crate::{error::*, io::prelude::*, path::PathBuf, time::*, utils}; use core::{cmp, ops}; @@ -455,7 +455,7 @@ pub(crate) trait OffsetConversions { } } -impl OffsetConversions for FileSystem +impl OffsetConversions for FileSystem<'_, S> where S: Read + Write + Seek, { @@ -577,7 +577,7 @@ impl Default for FileFilter { /// An API to process a FAT filesystem #[derive(Debug)] -pub struct FileSystem +pub struct FileSystem<'a, S> where S: Read + Write + Seek, { @@ -590,6 +590,8 @@ where pub(crate) buffer_modified: bool, pub(crate) stored_sector: u64, + clock: &'a dyn Clock, + pub(crate) boot_record: BootRecord, // since `self.boot_record.fat_type()` calls like 5 nested functions, we keep this cached and expose it with a public getter function fat_type: FATType, @@ -602,7 +604,7 @@ where } /// Getter functions -impl FileSystem +impl FileSystem<'_, S> where S: Read + Write + Seek, { @@ -613,7 +615,20 @@ where } /// Setter functions -impl FileSystem +impl<'a, S> FileSystem<'a, S> +where + S: Read + Write + Seek, +{ + /// Replace the internal [`Clock`] with a different one + /// + /// Use this in `no-std` contexts to replace the [`DefaultClock`] used + pub fn with_clock(&mut self, clock: &'a dyn Clock) { + self.clock = clock; + } +} + +/// Setter functions +impl FileSystem<'_, S> where S: Read + Write + Seek, { @@ -635,7 +650,7 @@ where } /// Constructors -impl FileSystem +impl FileSystem<'_, S> where S: Read + Write + Seek, { @@ -708,6 +723,7 @@ where sector_buffer: buffer[..props.sector_size as usize].to_vec(), buffer_modified: false, stored_sector, + clock: &STATIC_DEFAULT_CLOCK, boot_record, fat_type, props, @@ -726,7 +742,7 @@ where } /// Internal [`Read`]-related low-level functions -impl FileSystem +impl FileSystem<'_, S> where S: Read + Write + Seek, { @@ -1018,7 +1034,7 @@ where } /// Internal [`Write`]-related low-level functions -impl FileSystem +impl FileSystem<'_, S> where S: Read + Write + Seek, { @@ -1280,7 +1296,7 @@ where } /// Public [`Read`]-related functions -impl FileSystem +impl<'a, S> FileSystem<'a, S> where S: Read + Write + Seek, { @@ -1337,7 +1353,7 @@ where /// Borrows `&mut self` until that [`ROFile`] object is dropped, effectively locking `self` until that file closed /// /// Fails if `path` doesn't represent a file, or if that file doesn't exist - pub fn get_ro_file(&mut self, path: PathBuf) -> FSResult, S::Error> { + pub fn get_ro_file(&mut self, path: PathBuf) -> FSResult, S::Error> { if path.is_malformed() { return Err(FSError::MalformedPath); } @@ -1382,7 +1398,7 @@ where } /// [`Write`]-related functions -impl FileSystem +impl<'a, S> FileSystem<'a, S> where S: Read + Write + Seek, { @@ -1391,7 +1407,7 @@ where /// Borrows `&mut self` until that [`RWFile`] object is dropped, effectively locking `self` until that file closed /// /// Fails if `path` doesn't represent a file, or if that file doesn't exist - pub fn get_rw_file(&mut self, path: PathBuf) -> FSResult, S::Error> { + pub fn get_rw_file(&mut self, path: PathBuf) -> FSResult, S::Error> { self._raise_io_rw_result()?; let ro_file = self.get_ro_file(path)?; @@ -1403,7 +1419,7 @@ where } } -impl ops::Drop for FileSystem +impl ops::Drop for FileSystem<'_, S> where S: Read + Write + Seek, { diff --git a/src/fat/tests.rs b/src/fat/tests.rs index 1ea59e3..3327c03 100644 --- a/src/fat/tests.rs +++ b/src/fat/tests.rs @@ -61,7 +61,7 @@ fn assert_vec_is_bee_movie_script(buf: &Vec) { assert_eq!(string, BEE_MOVIE_SCRIPT); } -fn assert_file_is_bee_movie_script(file: &mut ROFile<'_, S>) +fn assert_file_is_bee_movie_script(file: &mut ROFile<'_, '_, S>) where S: Read + Write + Seek, { @@ -323,7 +323,7 @@ fn read_file_in_subdir() { #[test] fn check_file_timestamps() { - use time::macros::*; + use ::time::macros::*; use std::io::Cursor; diff --git a/src/lib.rs b/src/lib.rs index d8e3e25..ca03fc1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -81,7 +81,6 @@ #![cfg_attr(not(feature = "std"), no_std)] // Even inside unsafe functions, we must acknowlegde the usage of unsafe code #![deny(deprecated)] -#![deny(elided_lifetimes_in_paths)] #![deny(macro_use_extern_crate)] #![deny(missing_copy_implementations)] #![deny(missing_debug_implementations)] @@ -101,8 +100,10 @@ mod error; mod fat; pub mod io; mod path; +mod time; mod utils; pub use error::*; pub use fat::*; pub use path::*; +pub use time::*; diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 0000000..e805c5c --- /dev/null +++ b/src/time.rs @@ -0,0 +1,45 @@ +use core::fmt; + +use time::{macros::date, OffsetDateTime, PrimitiveDateTime}; + +pub(crate) const EPOCH: PrimitiveDateTime = date!(1980 - 01 - 01).midnight(); + +/// An object that can measure and return the current time +pub trait Clock: fmt::Debug { + /// Returns the current date and time in your local timezone + /// (https://learn.microsoft.com/en-us/windows/win32/sysinfo/file-times) + fn now(&self) -> PrimitiveDateTime; +} + +/// The default [`Clock`] component +/// +/// Returns the current local time in a `std` environment. +/// In a `no-std` environment, it just returns the [`EPOCH`] +#[derive(Debug)] +#[allow(missing_copy_implementations)] +pub struct DefaultClock; + +pub(crate) static STATIC_DEFAULT_CLOCK: DefaultClock = DefaultClock {}; + +impl Default for DefaultClock { + fn default() -> Self { + DefaultClock {} + } +} + +impl Clock for DefaultClock { + fn now(&self) -> PrimitiveDateTime { + #[cfg(feature = "std")] + { + // https://stackoverflow.com/a/76149536/ + + // TODO: make the trait return an error to handle such cases + let now_odt = OffsetDateTime::now_local().unwrap(); + let now_pdt = PrimitiveDateTime::new(now_odt.date(), now_odt.time()); + + now_pdt + } + #[cfg(not(feature = "std"))] + EPOCH + } +} From dbeaf4bdb1e4af1d8fe04597635f5612c3db3a07 Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Thu, 12 Sep 2024 20:25:05 +0300 Subject: [PATCH 25/40] feat: create alias method `remove_file` --- src/fat/fs.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/fat/fs.rs b/src/fat/fs.rs index fa568e4..5f44e15 100644 --- a/src/fat/fs.rs +++ b/src/fat/fs.rs @@ -1402,6 +1402,16 @@ impl<'a, S> FileSystem<'a, S> where S: Read + Write + Seek, { + /// Remove a [`RWFile`] from the filesystem + /// + /// This is an alias to `self.get_rw_file(path)?.remove()?` + #[inline] + pub fn remove_file(&mut self, path: PathBuf) -> FSResult<(), S::Error> { + self.get_rw_file(path)?.remove()?; + + Ok(()) + } + /// Get a corresponding [`RWFile`] object from a [`PathBuf`] /// /// Borrows `&mut self` until that [`RWFile`] object is dropped, effectively locking `self` until that file closed From 7a2bc0ce4f0c218f70c4d4d9f9b409bb20f1fedf Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sat, 14 Sep 2024 15:54:07 +0300 Subject: [PATCH 26/40] fix: PathBuf's `.parent()` method wouldn't behave as expected --- src/path.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/path.rs b/src/path.rs index b53b1a7..d2b78aa 100644 --- a/src/path.rs +++ b/src/path.rs @@ -125,7 +125,12 @@ impl PathBuf { pub fn parent(&self) -> PathBuf { if self.inner.len() > 1 { let mut pathbuf = self.clone(); + + if pathbuf.is_dir() { + pathbuf.inner.pop_back(); + } pathbuf.inner.back_mut().unwrap().clear(); + pathbuf } else { PathBuf::new() From 63b0f9b08f3071abcd9f56f978a075bfc930abfb Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sat, 14 Sep 2024 15:54:50 +0300 Subject: [PATCH 27/40] feat: Add the ability to remove empty directories --- src/error.rs | 2 ++ src/fat/direntry.rs | 5 +++++ src/fat/fs.rs | 46 +++++++++++++++++++++++++++++++++++++++++++++ src/fat/tests.rs | 44 +++++++++++++++++++++++++++++++++++++++++++ src/path.rs | 2 +- 5 files changed, 98 insertions(+), 1 deletion(-) diff --git a/src/error.rs b/src/error.rs index a4547a7..9f1cb63 100644 --- a/src/error.rs +++ b/src/error.rs @@ -144,6 +144,8 @@ where NotADirectory, /// Found a directory when we expected a file IsADirectory, + /// Expected an empty directory + DirectoryNotEmpty, /// This file cannot be modified, as it is read-only ReadOnlyFile, /// A file or directory wasn't found diff --git a/src/fat/direntry.rs b/src/fat/direntry.rs index 43f7e82..98ffcec 100644 --- a/src/fat/direntry.rs +++ b/src/fat/direntry.rs @@ -170,6 +170,11 @@ impl TryFrom for PrimitiveDateTime { // a directory entry occupies 32 bytes pub(crate) const DIRENTRY_SIZE: usize = 32; +// each directory other than the root directory must have +// at least the `.` and `..` entries +// TODO: actually check this on runtime +pub(crate) const NONROOT_MIN_DIRENTRIES: usize = 2; + #[derive(Serialize, Deserialize, Debug, Clone, Copy)] pub(crate) struct FATDirEntry { pub(crate) sfn: SFN, diff --git a/src/fat/fs.rs b/src/fat/fs.rs index 5f44e15..7bb1bbf 100644 --- a/src/fat/fs.rs +++ b/src/fat/fs.rs @@ -1412,6 +1412,52 @@ where Ok(()) } + /// Remove an empty directory from the filesystem + /// + /// Errors if the path provided points to the root directory + pub fn remove_empty_dir(&mut self, path: PathBuf) -> FSResult<(), S::Error> { + if path.is_malformed() { + return Err(FSError::MalformedPath); + } + + if !path.is_dir() { + log::error!("Not a directory"); + return Err(FSError::NotADirectory); + } + + if path == PathBuf::new() { + // we are in the root directory, we can't remove it + return Err(S::Error::new( + ::Kind::new_unsupported(), + "We can't remove the root directory", + ) + .into()); + } + + let dir_entries = self.read_dir(path.clone())?; + + if dir_entries.len() > NONROOT_MIN_DIRENTRIES { + return Err(FSError::DirectoryNotEmpty); + } + + let parent_path = path.parent(); + + let parent_dir_entries = self.read_dir(parent_path)?; + + let entry = parent_dir_entries + .iter() + .find(|entry| entry.path == path) + .ok_or(FSError::NotFound)?; + + // we first clear the corresponding entry chain in the parent directory + self.remove_entry_chain(&entry.chain)?; + + // then we remove the allocated cluster chain + self.free_cluster_chain(entry.data_cluster)?; + + Ok(()) + } + /// Get a corresponding [`RWFile`] object from a [`PathBuf`] /// /// Borrows `&mut self` until that [`RWFile`] object is dropped, effectively locking `self` until that file closed diff --git a/src/fat/tests.rs b/src/fat/tests.rs index 3327c03..92a23ef 100644 --- a/src/fat/tests.rs +++ b/src/fat/tests.rs @@ -213,6 +213,28 @@ fn remove_data_region_file() { } } +#[test] +fn remove_empty_dir() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT16.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let dir_path = PathBuf::from("/another root directory/"); + + fs.remove_empty_dir(dir_path.clone()).unwrap(); + + // the directory should now be gone + let dir_result = fs.read_dir(dir_path); + match dir_result { + Err(err) => match err { + FSError::NotFound => (), + _ => panic!("unexpected IOError: {:?}", err), + }, + _ => panic!("the directory should have been deleted by now"), + } +} + #[test] #[allow(non_snake_case)] fn FAT_tables_after_write_are_identical() { @@ -466,6 +488,28 @@ fn remove_fat32_file() { } } +#[test] +fn remove_fat32_dir() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT32.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let dir_path = PathBuf::from("/emptydir/"); + + fs.remove_empty_dir(dir_path.clone()).unwrap(); + + // the directory should now be gone + let dir_result = fs.read_dir(dir_path); + match dir_result { + Err(err) => match err { + FSError::NotFound => (), + _ => panic!("unexpected IOError: {:?}", err), + }, + _ => panic!("the directory should have been deleted by now"), + } +} + #[test] #[allow(non_snake_case)] fn FAT_tables_after_fat32_write_are_identical() { diff --git a/src/path.rs b/src/path.rs index d2b78aa..5335f40 100644 --- a/src/path.rs +++ b/src/path.rs @@ -58,7 +58,7 @@ fn is_forbidden(pathbuf: &PathBuf) -> bool { // TODO: pushing an absolute path should replace a pathbuf /// Represents an owned, mutable path -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct PathBuf { inner: VecDeque, } From 842fce118f19f119699cc793345a4875f9f907d2 Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sat, 14 Sep 2024 16:17:30 +0300 Subject: [PATCH 28/40] chore: enable clippy lints --- .vscode/settings.json | 3 +- src/error.rs | 2 +- src/fat/bpb.rs | 26 +++---- src/fat/direntry.rs | 20 +++--- src/fat/file.rs | 8 +-- src/fat/fs.rs | 162 +++++++++++++++++++----------------------- src/fat/tests.rs | 14 ++-- src/lib.rs | 2 + src/path.rs | 14 +--- src/time.rs | 11 +-- src/utils/io.rs | 2 +- tests/checksums.rs | 2 +- 12 files changed, 118 insertions(+), 148 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 0196fd7..1d02abb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,6 @@ "[rust]": { "editor.defaultFormatter": "rust-lang.rust-analyzer", "editor.formatOnSave": true - } + }, + "rust-analyzer.check.command": "clippy", } \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 9f1cb63..03f6756 100644 --- a/src/error.rs +++ b/src/error.rs @@ -103,7 +103,7 @@ pub enum InternalFSError { InvalidBPBSig, /** Invalid FAT32 FSInfo signature. - Perhaps the FSInfo structure or the FAT32 EBR's fat_info field is malformed? + Perhaps the FSInfo structure or the FAT32 Ebr's fat_info field is malformed? */ InvalidFSInfoSig, /** diff --git a/src/fat/bpb.rs b/src/fat/bpb.rs index d234699..262d136 100644 --- a/src/fat/bpb.rs +++ b/src/fat/bpb.rs @@ -7,8 +7,9 @@ use serde::{Deserialize, Serialize}; use serde_big_array::BigArray; #[derive(Debug)] +#[allow(clippy::large_enum_variant)] pub(crate) enum BootRecord { - FAT(BootRecordFAT), + Fat(BootRecordFAT), ExFAT(BootRecordExFAT), } @@ -17,7 +18,7 @@ impl BootRecord { /// The FAT type of this file system pub(crate) fn fat_type(&self) -> FATType { match self { - BootRecord::FAT(boot_record_fat) => boot_record_fat.fat_type(), + BootRecord::Fat(boot_record_fat) => boot_record_fat.fat_type(), BootRecord::ExFAT(_boot_record_exfat) => { todo!("ExFAT not yet implemented"); FATType::ExFAT @@ -28,7 +29,7 @@ impl BootRecord { #[allow(non_snake_case)] pub(crate) fn nth_FAT_table_sector(&self, n: u8) -> u32 { match self { - BootRecord::FAT(boot_record_fat) => { + BootRecord::Fat(boot_record_fat) => { boot_record_fat.first_fat_sector() as u32 + n as u32 * boot_record_fat.fat_sector_size() } @@ -46,8 +47,8 @@ pub(crate) const FAT_SIGNATURE: u16 = 0x55AA; #[derive(Debug, Clone, Copy)] pub(crate) struct BootRecordFAT { - pub bpb: BPBFAT, - pub ebr: EBR, + pub bpb: BpbFat, + pub ebr: Ebr, } impl BootRecordFAT { @@ -55,11 +56,11 @@ impl BootRecordFAT { pub(crate) fn verify_signature(&self) -> bool { match self.fat_type() { FATType::FAT12 | FATType::FAT16 | FATType::FAT32 => match self.ebr { - EBR::FAT12_16(ebr_fat12_16) => { + Ebr::FAT12_16(ebr_fat12_16) => { ebr_fat12_16.boot_signature == BOOT_SIGNATURE && ebr_fat12_16.signature == FAT_SIGNATURE } - EBR::FAT32(ebr_fat32, _) => { + Ebr::FAT32(ebr_fat32, _) => { ebr_fat32.boot_signature == BOOT_SIGNATURE && ebr_fat32.signature == FAT_SIGNATURE } @@ -82,8 +83,8 @@ impl BootRecordFAT { /// FAT size in sectors pub(crate) fn fat_sector_size(&self) -> u32 { match self.ebr { - EBR::FAT12_16(_ebr_fat12_16) => self.bpb.table_size_16.into(), - EBR::FAT32(ebr_fat32, _) => ebr_fat32.table_size_32, + Ebr::FAT12_16(_ebr_fat12_16) => self.bpb.table_size_16.into(), + Ebr::FAT32(ebr_fat32, _) => ebr_fat32.table_size_32, } } @@ -171,7 +172,7 @@ pub(crate) struct BootRecordExFAT { pub(crate) const BPBFAT_SIZE: usize = 36; #[derive(Serialize, Deserialize, Debug, Clone, Copy)] -pub(crate) struct BPBFAT { +pub(crate) struct BpbFat { pub _jmpboot: [u8; 3], pub _oem_identifier: [u8; 8], pub bytes_per_sector: u16, @@ -191,12 +192,13 @@ pub(crate) struct BPBFAT { pub(crate) const EBR_SIZE: usize = MIN_SECTOR_SIZE - BPBFAT_SIZE; #[derive(Clone, Copy)] -pub(crate) enum EBR { +#[allow(clippy::large_enum_variant)] +pub(crate) enum Ebr { FAT12_16(EBRFAT12_16), FAT32(EBRFAT32, FSInfoFAT32), } -impl fmt::Debug for EBR { +impl fmt::Debug for Ebr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // TODO: find a good way of printing this write!(f, "FAT12-16/32 Extended boot record...") diff --git a/src/fat/direntry.rs b/src/fat/direntry.rs index 98ffcec..baf33d8 100644 --- a/src/fat/direntry.rs +++ b/src/fat/direntry.rs @@ -103,8 +103,7 @@ impl TryFrom for Time { .with_hour_24(value.hour()) .and_then(|parsed| parsed.with_minute(value.minutes())) .and_then(|parsed| parsed.with_second(value.seconds() * 2)) - .map(|parsed| parsed.try_into().ok()) - .flatten() + .and_then(|parsed| parsed.try_into().ok()) .ok_or(()) } } @@ -117,8 +116,7 @@ impl TryFrom for Date { .with_year(i32::from(value.year()) + EPOCH.year()) .and_then(|parsed| parsed.with_month(value.month().try_into().ok()?)) .and_then(|parsed| parsed.with_day(num::NonZeroU8::new(value.day())?)) - .map(|parsed| parsed.try_into().ok()) - .flatten() + .and_then(|parsed| parsed.try_into().ok()) .ok_or(()) } } @@ -177,7 +175,7 @@ pub(crate) const NONROOT_MIN_DIRENTRIES: usize = 2; #[derive(Serialize, Deserialize, Debug, Clone, Copy)] pub(crate) struct FATDirEntry { - pub(crate) sfn: SFN, + pub(crate) sfn: Sfn, pub(crate) attributes: RawAttributes, pub(crate) _reserved: [u8; 1], pub(crate) created: EntryCreationTime, @@ -189,12 +187,12 @@ pub(crate) struct FATDirEntry { } #[derive(Serialize, Deserialize, Debug, Clone, Copy)] -pub(crate) struct SFN { +pub(crate) struct Sfn { name: [u8; 8], ext: [u8; 3], } -impl SFN { +impl Sfn { fn get_byte_slice(&self) -> [u8; 11] { let mut slice = [0; 11]; @@ -213,13 +211,13 @@ impl SFN { .wrapping_add(c) } - log::debug!("SFN checksum: {:X}", sum); + log::debug!("Sfn checksum: {:X}", sum); sum } } -impl fmt::Display for SFN { +impl fmt::Display for Sfn { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // we begin by writing the name (even if it is padded with spaces, they will be trimmed, so we don't care) write!(f, "{}", String::from_utf8_lossy(&self.name).trim())?; @@ -245,8 +243,8 @@ pub(crate) struct LFNEntry { pub(crate) _long_entry_type: u8, /// If this doesn't match with the computed cksum, then the set of LFNs is considered corrupt /// - /// A [`LFNEntry`] will be marked as corrupt even if it isn't, if the SFN is modifed by a legacy system, - /// since the new SFN's signature and the one on this field won't (probably) match + /// A [`LFNEntry`] will be marked as corrupt even if it isn't, if the Sfn is modifed by a legacy system, + /// since the new Sfn's signature and the one on this field won't (probably) match pub(crate) checksum: u8, pub(crate) mid_chars: [u8; 12], pub(crate) _zeroed: [u8; 2], diff --git a/src/fat/file.rs b/src/fat/file.rs index c4d5eaa..338bd19 100644 --- a/src/fat/file.rs +++ b/src/fat/file.rs @@ -63,7 +63,7 @@ where #[inline] /// Non-[`panic`]king version of [`next_cluster()`](ROFile::next_cluster) fn get_next_cluster(&mut self) -> Result, ::Error> { - Ok(self.fs.get_next_cluster(self.props.current_cluster)?) + self.fs.get_next_cluster(self.props.current_cluster) } /// Returns that last cluster in the file's cluster chain @@ -74,7 +74,7 @@ where loop { match self.fs.read_nth_FAT_entry(current_cluster)? { FATEntry::Allocated(next_cluster) => current_cluster = next_cluster, - FATEntry::EOF => break, + FATEntry::Eof => break, _ => unreachable!(), } } @@ -328,7 +328,7 @@ where // we set the new last cluster in the chain to be EOF self.ro_file .fs - .write_nth_FAT_entry(self.ro_file.props.current_cluster, FATEntry::EOF)?; + .write_nth_FAT_entry(self.ro_file.props.current_cluster, FATEntry::Eof)?; // then, we set each cluster after the current one to EOF while let Some(next_cluster) = next_cluster_option { @@ -507,7 +507,7 @@ where )?; // we also set the next free cluster to be EOF self.fs - .write_nth_FAT_entry(next_free_cluster, FATEntry::EOF)?; + .write_nth_FAT_entry(next_free_cluster, FATEntry::Eof)?; log::trace!( "cluster {} now points to {}", last_cluster_in_chain, diff --git a/src/fat/fs.rs b/src/fat/fs.rs index 7bb1bbf..0d1e283 100644 --- a/src/fat/fs.rs +++ b/src/fat/fs.rs @@ -62,7 +62,7 @@ pub(crate) enum FATEntry { /// This is a bad (defective) cluster Bad, /// This cluster is allocated and is the final cluster of the file - EOF, + Eof, } impl From for u32 { @@ -78,7 +78,7 @@ impl From<&FATEntry> for u32 { FATEntry::Allocated(cluster) => *cluster, FATEntry::Reserved => 0xFFFFFF6, FATEntry::Bad => 0xFFFFFF7, - FATEntry::EOF => u32::MAX, + FATEntry::Eof => u32::MAX, } } } @@ -270,7 +270,7 @@ impl ops::Deref for DirEntry { pub(crate) const UNUSED_ENTRY: u8 = 0xE5; pub(crate) const LAST_AND_UNUSED_ENTRY: u8 = 0x00; -#[derive(Debug)] +#[derive(Debug, Default)] struct EntryParser { entries: Vec, lfn_buf: Vec, @@ -278,17 +278,6 @@ struct EntryParser { current_chain: Option, } -impl Default for EntryParser { - fn default() -> Self { - EntryParser { - entries: Vec::new(), - lfn_buf: Vec::new(), - lfn_checksum: None, - current_chain: None, - } - } -} - impl EntryParser { #[inline] fn _decrement_parsed_entries_counter(&mut self) { @@ -324,7 +313,7 @@ impl EntryParser { _ => (), }; - let Ok(entry) = bincode_config().deserialize::(&chunk) else { + let Ok(entry) = bincode_config().deserialize::(chunk) else { continue; }; @@ -344,7 +333,7 @@ impl EntryParser { if entry.attributes.contains(RawAttributes::LFN) { // TODO: perhaps there is a way to utilize the `order` field? - let Ok(lfn_entry) = bincode_config().deserialize::(&chunk) else { + let Ok(lfn_entry) = bincode_config().deserialize::(chunk) else { self._decrement_parsed_entries_counter(); continue; }; @@ -493,11 +482,11 @@ pub(crate) struct FSProperties { impl FSProperties { fn from_boot_record(boot_record: &BootRecord) -> Self { let sector_size = match boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.bpb.bytes_per_sector.into(), + BootRecord::Fat(boot_record_fat) => boot_record_fat.bpb.bytes_per_sector.into(), BootRecord::ExFAT(boot_record_exfat) => 1 << boot_record_exfat.sector_shift, }; let cluster_size = match boot_record { - BootRecord::FAT(boot_record_fat) => { + BootRecord::Fat(boot_record_fat) => { (boot_record_fat.bpb.sectors_per_cluster as u32 * sector_size).into() } BootRecord::ExFAT(boot_record_exfat) => { @@ -505,31 +494,31 @@ impl FSProperties { } }; let total_sectors = match boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.total_sectors(), + BootRecord::Fat(boot_record_fat) => boot_record_fat.total_sectors(), BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), }; let total_clusters = match boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.total_clusters(), + BootRecord::Fat(boot_record_fat) => boot_record_fat.total_clusters(), BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), }; let fat_table_count = match boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.bpb.table_count, + BootRecord::Fat(boot_record_fat) => boot_record_fat.bpb.table_count, BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), }; let fat_sector_size = match boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.fat_sector_size().into(), + BootRecord::Fat(boot_record_fat) => boot_record_fat.fat_sector_size(), BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT not yet implemented"), }; let first_fat_sector = match boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.first_fat_sector().into(), + BootRecord::Fat(boot_record_fat) => boot_record_fat.first_fat_sector(), BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT not yet implemented"), }; let first_root_dir_sector = match boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.first_root_dir_sector().into(), + BootRecord::Fat(boot_record_fat) => boot_record_fat.first_root_dir_sector(), BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), }; let first_data_sector = match boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.first_data_sector().into(), + BootRecord::Fat(boot_record_fat) => boot_record_fat.first_data_sector().into(), BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT is not yet implemented"), }; @@ -565,6 +554,7 @@ impl FileFilter { } } +#[allow(clippy::derivable_impls)] impl Default for FileFilter { fn default() -> Self { // The FAT spec says to filter everything by default @@ -672,7 +662,7 @@ where return Err(FSError::InternalFSError(InternalFSError::StorageTooSmall)); } - let bpb: BPBFAT = bincode_config().deserialize(&buffer[..BPBFAT_SIZE])?; + let bpb: BpbFat = bincode_config().deserialize(&buffer[..BPBFAT_SIZE])?; let ebr = if bpb.table_size_16 == 0 { let ebr_fat32 = bincode_config() @@ -691,23 +681,23 @@ where return Err(FSError::InternalFSError(InternalFSError::InvalidFSInfoSig)); } - EBR::FAT32(ebr_fat32, fsinfo) + Ebr::FAT32(ebr_fat32, fsinfo) } else { - EBR::FAT12_16( + Ebr::FAT12_16( bincode_config() .deserialize::(&buffer[BPBFAT_SIZE..BPBFAT_SIZE + EBR_SIZE])?, ) }; // TODO: see how we will handle this for exfat - let boot_record = BootRecord::FAT(BootRecordFAT { bpb, ebr }); + let boot_record = BootRecord::Fat(BootRecordFAT { bpb, ebr }); // verify boot record signature let fat_type = boot_record.fat_type(); log::info!("The FAT type of the filesystem is {:?}", fat_type); match boot_record { - BootRecord::FAT(boot_record_fat) => { + BootRecord::Fat(boot_record_fat) => { if boot_record_fat.verify_signature() { log::error!("FAT boot record has invalid signature(s)"); return Err(FSError::InternalFSError(InternalFSError::InvalidBPBSig)); @@ -748,8 +738,8 @@ where { fn process_root_dir(&mut self) -> FSResult, S::Error> { match self.boot_record { - BootRecord::FAT(boot_record_fat) => match boot_record_fat.ebr { - EBR::FAT12_16(_ebr_fat12_16) => { + BootRecord::Fat(boot_record_fat) => match boot_record_fat.ebr { + Ebr::FAT12_16(_ebr_fat12_16) => { let mut entry_parser = EntryParser::default(); let root_dir_sector = boot_record_fat.first_root_dir_sector(); @@ -763,7 +753,7 @@ where Ok(entry_parser.finish()) } - EBR::FAT32(ebr_fat32, _) => { + Ebr::FAT32(ebr_fat32, _) => { let cluster = ebr_fat32.root_cluster; self.process_normal_dir(cluster) } @@ -784,7 +774,7 @@ where for sector in first_sector_of_cluster ..(first_sector_of_cluster + self.sectors_per_cluster() as u32) { - if entry_parser.parse_sector(sector.into(), self)? { + if entry_parser.parse_sector(sector, self)? { break 'outer; } } @@ -794,7 +784,7 @@ where match current_fat_entry { // we are done here, break the loop - FATEntry::EOF => break, + FATEntry::Eof => break, // this cluster chain goes on, follow it FATEntry::Allocated(next_cluster) => data_cluster = next_cluster, // any other case (whether a bad, reserved or free cluster) is invalid, consider this cluster chain malformed @@ -814,10 +804,10 @@ where /// If the [`Result`] returns [`Ok`] that contains a [`None`], the drive is full pub(crate) fn next_free_cluster(&mut self) -> Result, S::Error> { let start_cluster = match self.boot_record { - BootRecord::FAT(boot_record_fat) => { + BootRecord::Fat(boot_record_fat) => { let mut first_free_cluster = self.first_free_cluster; - if let EBR::FAT32(_, fsinfo) = boot_record_fat.ebr { + if let Ebr::FAT32(_, fsinfo) = boot_record_fat.ebr { // a value of u32::MAX denotes unawareness of the first free cluster // we also do a bit of range checking // TODO: if this is unknown, figure it out and write it to the FSInfo structure @@ -837,22 +827,19 @@ where let mut current_cluster = start_cluster; while current_cluster < self.props.total_clusters { - match self.read_nth_FAT_entry(current_cluster)? { - FATEntry::Free => { - self.first_free_cluster = current_cluster; - - match &mut self.boot_record { - BootRecord::FAT(boot_record_fat) => { - if let EBR::FAT32(_, fsinfo) = &mut boot_record_fat.ebr { - fsinfo.first_free_cluster = current_cluster; - } + if self.read_nth_FAT_entry(current_cluster)? == FATEntry::Free { + self.first_free_cluster = current_cluster; + + match &mut self.boot_record { + BootRecord::Fat(boot_record_fat) => { + if let Ebr::FAT32(_, fsinfo) = &mut boot_record_fat.ebr { + fsinfo.first_free_cluster = current_cluster; } - BootRecord::ExFAT(_) => todo!("ExFAT not yet implemented"), } - - return Ok(Some(current_cluster)); + BootRecord::ExFAT(_) => todo!("ExFAT not yet implemented"), } - _ => (), + + return Ok(Some(current_cluster)); } current_cluster += 1; } @@ -884,7 +871,7 @@ where const MAX_PROBE_SIZE: u32 = 1 << 20; let fat_byte_size = match self.boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.fat_sector_size(), + BootRecord::Fat(boot_record_fat) => boot_record_fat.fat_sector_size(), BootRecord::ExFAT(_) => unreachable!(), }; @@ -915,7 +902,7 @@ where #[allow(non_snake_case)] pub(crate) fn sector_belongs_to_FAT(&self, sector: u64) -> bool { match self.boot_record { - BootRecord::FAT(boot_record_fat) => (boot_record_fat.first_fat_sector().into() + BootRecord::Fat(boot_record_fat) => (boot_record_fat.first_fat_sector().into() ..boot_record_fat.first_root_dir_sector().into()) .contains(§or), BootRecord::ExFAT(_boot_record_exfat) => todo!("ExFAT not yet implemented"), @@ -947,7 +934,7 @@ where pub(crate) fn read_nth_FAT_entry(&mut self, n: u32) -> Result { // the size of an entry rounded up to bytes let entry_size = self.fat_type.entry_size(); - let entry_props = FATEntryProps::new(n, &self); + let entry_props = FATEntryProps::new(n, self); self.read_nth_sector(entry_props.fat_sector.into())?; @@ -995,10 +982,11 @@ where FATType::FAT12 => match value { 0x000 => FATEntry::Free, 0xFF7 => FATEntry::Bad, - 0xFF8..=0xFFE | 0xFFF => FATEntry::EOF, + #[allow(clippy::manual_range_patterns)] + 0xFF8..=0xFFE | 0xFFF => FATEntry::Eof, _ => { - if (0x002..(self.props.total_clusters + 1)).contains(&value.into()) { - FATEntry::Allocated(value.into()) + if (0x002..(self.props.total_clusters + 1)).contains(&value) { + FATEntry::Allocated(value) } else { FATEntry::Reserved } @@ -1007,10 +995,11 @@ where FATType::FAT16 => match value { 0x0000 => FATEntry::Free, 0xFFF7 => FATEntry::Bad, - 0xFFF8..=0xFFFE | 0xFFFF => FATEntry::EOF, + #[allow(clippy::manual_range_patterns)] + 0xFFF8..=0xFFFE | 0xFFFF => FATEntry::Eof, _ => { - if (0x0002..(self.props.total_clusters + 1)).contains(&value.into()) { - FATEntry::Allocated(value.into()) + if (0x0002..(self.props.total_clusters + 1)).contains(&value) { + FATEntry::Allocated(value) } else { FATEntry::Reserved } @@ -1019,10 +1008,11 @@ where FATType::FAT32 => match value { 0x00000000 => FATEntry::Free, 0x0FFFFFF7 => FATEntry::Bad, - 0x0FFFFFF8..=0xFFFFFFE | 0x0FFFFFFF => FATEntry::EOF, + #[allow(clippy::manual_range_patterns)] + 0x0FFFFFF8..=0xFFFFFFE | 0x0FFFFFFF => FATEntry::Eof, _ => { - if (0x00000002..(self.props.total_clusters + 1)).contains(&value.into()) { - FATEntry::Allocated(value.into()) + if (0x00000002..(self.props.total_clusters + 1)).contains(&value) { + FATEntry::Allocated(value) } else { FATEntry::Reserved } @@ -1042,7 +1032,7 @@ where pub(crate) fn write_nth_FAT_entry(&mut self, n: u32, entry: FATEntry) -> Result<(), S::Error> { // the size of an entry rounded up to bytes let entry_size = self.fat_type.entry_size(); - let entry_props = FATEntryProps::new(n, &self); + let entry_props = FATEntryProps::new(n, self); // the previous solution would overflow, here's a correct implementation let mask = utils::bits::setbits_u32(self.fat_type.bits_per_entry()); @@ -1120,22 +1110,18 @@ where } // lastly, update the FSInfoFAT32 structure is it is available - match &mut self.boot_record { - BootRecord::FAT(boot_record_fat) => match &mut boot_record_fat.ebr { - EBR::FAT32(_, fsinfo) => { - match entry { - FATEntry::Free => { - fsinfo.free_cluster_count += 1; - if n < fsinfo.first_free_cluster { - fsinfo.first_free_cluster = n; - } + if let BootRecord::Fat(boot_record_fat) = &mut self.boot_record { + if let Ebr::FAT32(_, fsinfo) = &mut boot_record_fat.ebr { + match entry { + FATEntry::Free => { + fsinfo.free_cluster_count += 1; + if n < fsinfo.first_free_cluster { + fsinfo.first_free_cluster = n; } - _ => fsinfo.free_cluster_count -= 1, - }; - } - _ => (), - }, - _ => (), + } + _ => fsinfo.free_cluster_count -= 1, + }; + } } Ok(()) @@ -1182,16 +1168,12 @@ where // we have moved to a new sector current_sector += 1; - match current_cluster_option { + if let Some(mut current_cluster) = current_cluster_option { // data region - Some(mut current_cluster) => { - if self.partition_sector_to_data_cluster(current_sector) != current_cluster - { - current_cluster = self.get_next_cluster(current_cluster)?.unwrap(); - current_sector = self.data_cluster_to_partition_sector(current_cluster); - } + if self.partition_sector_to_data_cluster(current_sector) != current_cluster { + current_cluster = self.get_next_cluster(current_cluster)?.unwrap(); + current_sector = self.data_cluster_to_partition_sector(current_cluster); } - None => (), } current_offset = 0; @@ -1257,11 +1239,11 @@ where if let Some(fat_sector_props) = FATSectorProps::new(self.stored_sector, self) { log::trace!("syncing FAT sector {}", fat_sector_props.sector_offset,); match self.boot_record { - BootRecord::FAT(boot_record_fat) => match boot_record_fat.ebr { - EBR::FAT12_16(_) => { + BootRecord::Fat(boot_record_fat) => match boot_record_fat.ebr { + Ebr::FAT12_16(_) => { self._sync_FAT_sector(&fat_sector_props)?; } - EBR::FAT32(ebr_fat32, _) => { + Ebr::FAT32(ebr_fat32, _) => { if ebr_fat32.extended_flags.mirroring_disabled() { self._sync_current_sector()?; } else { @@ -1314,7 +1296,7 @@ where let mut entries = self.process_root_dir()?; - for dir_name in path.clone().into_iter() { + for dir_name in path.clone() { let dir_cluster = match entries.iter().find(|entry| { entry.name == dir_name && entry.attributes.contains(RawAttributes::DIRECTORY) }) { diff --git a/src/fat/tests.rs b/src/fat/tests.rs index 92a23ef..598e6aa 100644 --- a/src/fat/tests.rs +++ b/src/fat/tests.rs @@ -19,7 +19,7 @@ fn check_FAT_offset() { let mut fs = FileSystem::from_storage(&mut storage).unwrap(); let fat_offset = match fs.boot_record { - BootRecord::FAT(boot_record_fat) => boot_record_fat.first_fat_sector(), + BootRecord::Fat(boot_record_fat) => boot_record_fat.first_fat_sector(), BootRecord::ExFAT(_boot_record_exfat) => unreachable!(), }; @@ -27,7 +27,7 @@ fn check_FAT_offset() { fs.read_nth_sector(fat_offset.into()).unwrap(); let first_entry = u16::from_le_bytes(fs.sector_buffer[..2].try_into().unwrap()); - let media_type = if let BootRecord::FAT(boot_record_fat) = fs.boot_record { + let media_type = if let BootRecord::Fat(boot_record_fat) = fs.boot_record { boot_record_fat.bpb._media_type } else { unreachable!("this should be a FAT16 filesystem") @@ -54,8 +54,8 @@ fn read_file_in_root_dir() { } static BEE_MOVIE_SCRIPT: &str = include_str!("../../tests/bee movie script.txt"); -fn assert_vec_is_bee_movie_script(buf: &Vec) { - let string = std::str::from_utf8(&buf).unwrap(); +fn assert_vec_is_bee_movie_script(buf: &[u8]) { + let string = std::str::from_utf8(buf).unwrap(); let expected_size = BEE_MOVIE_SCRIPT.len(); assert_eq!(buf.len(), expected_size); @@ -513,7 +513,7 @@ fn remove_fat32_dir() { #[test] #[allow(non_snake_case)] fn FAT_tables_after_fat32_write_are_identical() { - use crate::fat::{BootRecord, EBR}; + use crate::fat::{BootRecord, Ebr}; use std::io::Cursor; @@ -521,8 +521,8 @@ fn FAT_tables_after_fat32_write_are_identical() { let mut fs = FileSystem::from_storage(&mut storage).unwrap(); match fs.boot_record { - BootRecord::FAT(boot_record_fat) => match boot_record_fat.ebr { - EBR::FAT32(ebr_fat32, _) => assert!( + BootRecord::Fat(boot_record_fat) => match boot_record_fat.ebr { + Ebr::FAT32(ebr_fat32, _) => assert!( !ebr_fat32.extended_flags.mirroring_disabled(), "mirroring should be enabled for this .img file" ), diff --git a/src/lib.rs b/src/lib.rs index ca03fc1..2cd0382 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,6 +93,8 @@ #![deny(unsafe_op_in_unsafe_fn)] #![deny(unused_import_braces)] #![deny(unused_lifetimes)] +// clippy attributes +#![deny(clippy::redundant_clone)] extern crate alloc; diff --git a/src/path.rs b/src/path.rs index 5335f40..f9b6bf2 100644 --- a/src/path.rs +++ b/src/path.rs @@ -45,7 +45,7 @@ fn is_forbidden(pathbuf: &PathBuf) -> bool { if RESERVED_FILENAMES .iter() // remove extension - .map(|file_name| file_name.split_once(".").map(|s| s.0).unwrap_or(file_name)) + .map(|file_name| file_name.split_once('.').map(|s| s.0).unwrap_or(file_name)) .any(|reserved| file_name == reserved) { return true; @@ -58,7 +58,7 @@ fn is_forbidden(pathbuf: &PathBuf) -> bool { // TODO: pushing an absolute path should replace a pathbuf /// Represents an owned, mutable path -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct PathBuf { inner: VecDeque, } @@ -158,7 +158,7 @@ impl PathBuf { /// Checks whether `self` is malformed (as if someone pushed a string with many consecutive slashes) pub(crate) fn is_malformed(&self) -> bool { - is_forbidden(&self) + is_forbidden(self) } } @@ -187,14 +187,6 @@ impl fmt::Display for PathBuf { } } -impl Default for PathBuf { - fn default() -> Self { - Self { - inner: VecDeque::new(), - } - } -} - impl iter::Iterator for PathBuf { type Item = String; diff --git a/src/time.rs b/src/time.rs index e805c5c..368b1a4 100644 --- a/src/time.rs +++ b/src/time.rs @@ -15,18 +15,12 @@ pub trait Clock: fmt::Debug { /// /// Returns the current local time in a `std` environment. /// In a `no-std` environment, it just returns the [`EPOCH`] -#[derive(Debug)] +#[derive(Debug, Default)] #[allow(missing_copy_implementations)] pub struct DefaultClock; pub(crate) static STATIC_DEFAULT_CLOCK: DefaultClock = DefaultClock {}; -impl Default for DefaultClock { - fn default() -> Self { - DefaultClock {} - } -} - impl Clock for DefaultClock { fn now(&self) -> PrimitiveDateTime { #[cfg(feature = "std")] @@ -35,9 +29,8 @@ impl Clock for DefaultClock { // TODO: make the trait return an error to handle such cases let now_odt = OffsetDateTime::now_local().unwrap(); - let now_pdt = PrimitiveDateTime::new(now_odt.date(), now_odt.time()); - now_pdt + PrimitiveDateTime::new(now_odt.date(), now_odt.time()) } #[cfg(not(feature = "std"))] EPOCH diff --git a/src/utils/io.rs b/src/utils/io.rs index f6b2310..6079361 100644 --- a/src/utils/io.rs +++ b/src/utils/io.rs @@ -14,6 +14,6 @@ where // in case this filesystem doesn't support // write operations, don't error out Err(ref e) if e.kind().is_unsupported() => Ok(false), - Err(e) => return Err(e), + Err(e) => Err(e), } } diff --git a/tests/checksums.rs b/tests/checksums.rs index c6b5691..4b25713 100644 --- a/tests/checksums.rs +++ b/tests/checksums.rs @@ -31,7 +31,7 @@ fn verify_checksums() { } } -fn sha256sum(file: &std::path::PathBuf) -> std::process::ExitStatus { +fn sha256sum(file: &std::path::Path) -> std::process::ExitStatus { Command::new("sha256sum") .args(["--status", "-c", file.as_os_str().to_str().unwrap()]) .status() From 02eb2132aca8d7814fdaacbddc8b8373e110f0b7 Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sat, 14 Sep 2024 17:28:29 +0300 Subject: [PATCH 29/40] fix: don't expose the `.` & `..` entries to the end user --- src/fat/fs.rs | 4 +++- src/path.rs | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/fat/fs.rs b/src/fat/fs.rs index 0d1e283..c964f43 100644 --- a/src/fat/fs.rs +++ b/src/fat/fs.rs @@ -1,6 +1,6 @@ use super::*; -use crate::{error::*, io::prelude::*, path::PathBuf, time::*, utils}; +use crate::{error::*, io::prelude::*, path::PathBuf, time::*, utils, SPECIAL_ENTRIES}; use core::{cmp, ops}; @@ -1315,6 +1315,8 @@ where Ok(entries .into_iter() .filter(|x| self.filter.filter(x)) + // we shouldn't expose the special entries to the user + .filter(|x| !SPECIAL_ENTRIES.contains(&x.name.as_str())) .map(|rawentry| { let mut entry_path = path.clone(); diff --git a/src/path.rs b/src/path.rs index f9b6bf2..2d0aa34 100644 --- a/src/path.rs +++ b/src/path.rs @@ -29,6 +29,9 @@ pub const RESERVED_FILENAMES: &[&str] = &[ "COM8", "COM9", "COM¹", "COM²", "COM³", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "LPT¹", "LPT²", "LPT³", ]; +/// Except for the root directory, each directory must contain +/// the following (two) entries at the beginning of the directory +pub(crate) const SPECIAL_ENTRIES: &[&str] = &[".", ".."]; /// Check whether a [`PathBuf`] is forbidden for use in filenames or directory names fn is_forbidden(pathbuf: &PathBuf) -> bool { From d19a04e2e1a8bbded115efe14aa40a58b77aee71 Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sun, 15 Sep 2024 19:25:47 +0300 Subject: [PATCH 30/40] fix: sync the FSInfo struct on FAT32 filesystems --- src/fat/fs.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/fat/fs.rs b/src/fat/fs.rs index c964f43..55de27d 100644 --- a/src/fat/fs.rs +++ b/src/fat/fs.rs @@ -1263,6 +1263,23 @@ where Ok(()) } + /// Sync the [`FSInfoFAT32`] back to the storage medium + /// if this is FAT32 + pub(crate) fn sync_fsinfo(&mut self) -> FSResult<(), S::Error> { + use utils::bincode::bincode_config; + + if let BootRecord::Fat(boot_record_fat) = self.boot_record { + if let Ebr::FAT32(ebr_fat32, fsinfo) = boot_record_fat.ebr { + self.read_nth_sector(ebr_fat32.fat_info.into())?; + + let bytes = bincode_config().serialize(&fsinfo)?; + self.sector_buffer.copy_from_slice(&bytes); + } + } + + Ok(()) + } + /// Returns an `Err` of `Unexpected [`IOErrorKind`] /// if the device medium is read-only fn _raise_io_rw_result(&mut self) -> Result<(), S::Error> { @@ -1465,6 +1482,7 @@ where { fn drop(&mut self) { // nothing to do if these error out while dropping + let _ = self.sync_fsinfo(); let _ = self.sync_sector_buffer(); let _ = self.storage.flush(); } From 7803ab239a5ce7c731b17e6d70cc88d919b06ddd Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sun, 15 Sep 2024 19:38:42 +0300 Subject: [PATCH 31/40] feat: add a proper unmount method --- src/fat/fs.rs | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/fat/fs.rs b/src/fat/fs.rs index 55de27d..d5d6fdb 100644 --- a/src/fat/fs.rs +++ b/src/fat/fs.rs @@ -578,6 +578,7 @@ where pub(crate) sector_buffer: Vec, /// ANY CHANGES TO THE SECTOR BUFFER SHOULD ALSO SET THIS TO TRUE pub(crate) buffer_modified: bool, + fsinfo_modified: bool, pub(crate) stored_sector: u64, clock: &'a dyn Clock, @@ -712,6 +713,7 @@ where storage, sector_buffer: buffer[..props.sector_size as usize].to_vec(), buffer_modified: false, + fsinfo_modified: false, stored_sector, clock: &STATIC_DEFAULT_CLOCK, boot_record, @@ -834,6 +836,7 @@ where BootRecord::Fat(boot_record_fat) => { if let Ebr::FAT32(_, fsinfo) = &mut boot_record_fat.ebr { fsinfo.first_free_cluster = current_cluster; + self.fsinfo_modified = true; } } BootRecord::ExFAT(_) => todo!("ExFAT not yet implemented"), @@ -1121,6 +1124,7 @@ where } _ => fsinfo.free_cluster_count -= 1, }; + self.fsinfo_modified = true; } } @@ -1268,13 +1272,17 @@ where pub(crate) fn sync_fsinfo(&mut self) -> FSResult<(), S::Error> { use utils::bincode::bincode_config; - if let BootRecord::Fat(boot_record_fat) = self.boot_record { - if let Ebr::FAT32(ebr_fat32, fsinfo) = boot_record_fat.ebr { - self.read_nth_sector(ebr_fat32.fat_info.into())?; + if self.fsinfo_modified { + if let BootRecord::Fat(boot_record_fat) = self.boot_record { + if let Ebr::FAT32(ebr_fat32, fsinfo) = boot_record_fat.ebr { + self.read_nth_sector(ebr_fat32.fat_info.into())?; - let bytes = bincode_config().serialize(&fsinfo)?; - self.sector_buffer.copy_from_slice(&bytes); + let bytes = bincode_config().serialize(&fsinfo)?; + self.sector_buffer.copy_from_slice(&bytes); + } } + + self.fsinfo_modified = false; } Ok(()) @@ -1474,6 +1482,18 @@ where Ok(RWFile { ro_file }) } + + /// Sync any pending changes back to the storage medium and drop + /// + /// Use this to catch any IO errors that might be rejected silently + /// while [`Drop`]ping + pub fn unmount(&mut self) -> FSResult<(), S::Error> { + self.sync_fsinfo()?; + self.sync_sector_buffer()?; + self.storage.flush()?; + + Ok(()) + } } impl ops::Drop for FileSystem<'_, S> @@ -1481,9 +1501,7 @@ where S: Read + Write + Seek, { fn drop(&mut self) { - // nothing to do if these error out while dropping - let _ = self.sync_fsinfo(); - let _ = self.sync_sector_buffer(); - let _ = self.storage.flush(); + // nothing to do if this errors out while dropping + let _ = self.unmount(); } } From faf1be26143cedc2507edb1f1052b4e84e1bd5d4 Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sat, 21 Sep 2024 17:36:18 +0300 Subject: [PATCH 32/40] feat: add the ability to remove non-empty directories This is essentially a followup to `63b0f9b` --- src/fat/fs.rs | 101 ++++++++++++++++++++++++++++++++++++++++++++--- src/fat/tests.rs | 57 +++++++++++++++++++++++++- 2 files changed, 151 insertions(+), 7 deletions(-) diff --git a/src/fat/fs.rs b/src/fat/fs.rs index d5d6fdb..4b8c417 100644 --- a/src/fat/fs.rs +++ b/src/fat/fs.rs @@ -1027,7 +1027,7 @@ where } /// Internal [`Write`]-related low-level functions -impl FileSystem<'_, S> +impl<'a, S> FileSystem<'a, S> where S: Read + Write + Seek, { @@ -1300,6 +1300,17 @@ where Ok(()) } + + /// Like [`Self::get_rw_file`], but will ignore the read-only flag (if it is present) + /// + /// This is a private function for obvious reasons + fn get_rw_file_unchecked(&mut self, path: PathBuf) -> FSResult, S::Error> { + self._raise_io_rw_result()?; + + let ro_file = self.get_ro_file(path)?; + + Ok(RWFile { ro_file }) + } } /// Public [`Read`]-related functions @@ -1421,6 +1432,16 @@ where Ok(()) } + /// Remove a file from the filesystem, even if it is read-only + /// + /// **USE WITH EXTREME CAUTION!** + #[inline] + pub fn remove_file_unchecked(&mut self, path: PathBuf) -> FSResult<(), S::Error> { + self.get_rw_file_unchecked(path)?.remove()?; + + Ok(()) + } + /// Remove an empty directory from the filesystem /// /// Errors if the path provided points to the root directory @@ -1467,20 +1488,88 @@ where Ok(()) } + /// Removes a directory at this path, after removing all its contents. + /// + /// Use with caution! + /// + /// This will fail if there is at least 1 (one) read-only file + /// in this directory or in any subdirectory. To avoid this behaviour, + /// use [`remove_dir_all_unchecked()`](FileSystem::remove_dir_all_unchecked) + pub fn remove_dir_all(&mut self, path: PathBuf) -> FSResult<(), S::Error> { + // before we actually start removing stuff, + // let's make sure there are no read-only files + + if self.check_for_readonly_files(path.clone())? { + log::error!(concat!( + "A read-only file has been found ", + "in a directory pending deletion." + )); + return Err(FSError::ReadOnlyFile); + } + + // we have checked everything, this is safe to use + self.remove_dir_all_unchecked(path)?; + + Ok(()) + } + + /// Like [`remove_dir_all()`](FileSystem::remove_dir_all), + /// but also removes read-only files. + /// + /// **USE WITH EXTREME CAUTION!** + pub fn remove_dir_all_unchecked(&mut self, path: PathBuf) -> FSResult<(), S::Error> { + for entry in self.read_dir(path.clone())? { + if entry.path.is_dir() { + self.remove_dir_all(entry.path.to_owned())?; + } else if entry.path.is_file() { + self.remove_file_unchecked(entry.path.to_owned())?; + } else { + unreachable!() + } + } + + self.remove_empty_dir(path)?; + + Ok(()) + } + + /// Check `path` recursively to see if there are any read-only files in it + /// + /// If successful, the `bool` returned indicates + /// whether or not at least 1 (one) read-only file has been found + pub fn check_for_readonly_files(&mut self, path: PathBuf) -> FSResult { + for entry in self.read_dir(path)? { + let read_only_found = if entry.path.is_dir() { + self.check_for_readonly_files(entry.path.to_owned())? + } else if entry.path.is_file() { + entry.attributes.read_only + } else { + unreachable!() + }; + + if read_only_found { + // we have found at least 1 read-only file, + // no need to search any further + return Ok(true); + } + } + + Ok(false) + } + /// Get a corresponding [`RWFile`] object from a [`PathBuf`] /// /// Borrows `&mut self` until that [`RWFile`] object is dropped, effectively locking `self` until that file closed /// /// Fails if `path` doesn't represent a file, or if that file doesn't exist pub fn get_rw_file(&mut self, path: PathBuf) -> FSResult, S::Error> { - self._raise_io_rw_result()?; + let rw_file = self.get_rw_file_unchecked(path)?; - let ro_file = self.get_ro_file(path)?; - if ro_file.attributes.read_only { + if rw_file.attributes.read_only { return Err(FSError::ReadOnlyFile); - }; + } - Ok(RWFile { ro_file }) + Ok(rw_file) } /// Sync any pending changes back to the storage medium and drop diff --git a/src/fat/tests.rs b/src/fat/tests.rs index 598e6aa..1266d4f 100644 --- a/src/fat/tests.rs +++ b/src/fat/tests.rs @@ -235,6 +235,39 @@ fn remove_empty_dir() { } } +#[test] +fn remove_nonempty_dir_with_readonly_file() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT16.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let dir_path = PathBuf::from("/rootdir/"); + + // the directory should contain a read-only file (example.txt) + let del_result = fs.remove_dir_all(dir_path.clone()); + match del_result { + Err(err) => match err { + FSError::ReadOnlyFile => (), + _ => panic!("unexpected IOError: {:?}", err), + }, + _ => panic!("the directory shouldn't have been removed already"), + } + + // this should now remove the directory + fs.remove_dir_all_unchecked(dir_path.clone()).unwrap(); + + // the directory should now be gone + let dir_result = fs.read_dir(dir_path); + match dir_result { + Err(err) => match err { + FSError::NotFound => (), + _ => panic!("unexpected IOError: {:?}", err), + }, + _ => panic!("the directory should have been deleted by now"), + } +} + #[test] #[allow(non_snake_case)] fn FAT_tables_after_write_are_identical() { @@ -489,7 +522,7 @@ fn remove_fat32_file() { } #[test] -fn remove_fat32_dir() { +fn remove_empty_fat32_dir() { use std::io::Cursor; let mut storage = Cursor::new(FAT32.to_owned()); @@ -510,6 +543,28 @@ fn remove_fat32_dir() { } } +#[test] +fn remove_nonempty_fat32_dir() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT32.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let dir_path = PathBuf::from("/secret/"); + + fs.remove_dir_all(dir_path.clone()).unwrap(); + + // the directory should now be gone + let dir_result = fs.read_dir(dir_path); + match dir_result { + Err(err) => match err { + FSError::NotFound => (), + _ => panic!("unexpected IOError: {:?}", err), + }, + _ => panic!("the directory should have been deleted by now"), + } +} + #[test] #[allow(non_snake_case)] fn FAT_tables_after_fat32_write_are_identical() { From d1c1015fa3a5371c5596cdc6958bc05a8094fce4 Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sat, 21 Sep 2024 17:45:58 +0300 Subject: [PATCH 33/40] chore: fix `faf1be2` for `no_std` contexts --- src/fat/fs.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fat/fs.rs b/src/fat/fs.rs index 4b8c417..356f39c 100644 --- a/src/fat/fs.rs +++ b/src/fat/fs.rs @@ -6,6 +6,7 @@ use core::{cmp, ops}; #[cfg(not(feature = "std"))] use alloc::{ + borrow::ToOwned, format, string::{String, ToString}, vec, From 38c46c532f8b49a8c18fbfc9c9c36439b09b784a Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Sun, 22 Dec 2024 15:50:24 +0200 Subject: [PATCH 34/40] refactor: split FileSystem::remove_entry_chain logic to other functions --- src/fat/direntry.rs | 96 ++++++++++++++++++++++++++++++++++++++++++++- src/fat/fs.rs | 58 ++++++--------------------- 2 files changed, 108 insertions(+), 46 deletions(-) diff --git a/src/fat/direntry.rs b/src/fat/direntry.rs index baf33d8..708cc1e 100644 --- a/src/fat/direntry.rs +++ b/src/fat/direntry.rs @@ -295,10 +295,65 @@ impl EntryLocationUnit { EntryLocationUnit::DataCluster(fs.partition_sector_to_data_cluster(sector)) } } + + pub(crate) fn get_max_offset(&self, fs: &mut FileSystem) -> u64 + where + S: Read + Write + Seek, + { + match self { + EntryLocationUnit::DataCluster(_) => fs.props.cluster_size, + EntryLocationUnit::RootDirSector(_) => fs.props.sector_size.into(), + } + } + + pub(crate) fn get_entry_sector(&self, fs: &mut FileSystem) -> u64 + where + S: Read + Write + Seek, + { + match self { + EntryLocationUnit::RootDirSector(root_dir_sector) => { + (root_dir_sector + fs.props.first_root_dir_sector).into() + } + EntryLocationUnit::DataCluster(data_cluster) => { + fs.data_cluster_to_partition_sector(*data_cluster).into() + } + } + } + + pub(crate) fn get_next_unit( + &self, + fs: &mut FileSystem, + ) -> Result, S::Error> + where + S: Read + Write + Seek, + { + match self { + EntryLocationUnit::RootDirSector(sector) => match fs.boot_record { + BootRecord::Fat(boot_record_fat) => { + if boot_record_fat.root_dir_sectors() == 0 { + unreachable!(concat!("This should be zero iff the FAT type if FAT32, ", + "in which case we won't even be reading root directory sectors, since it doesn't exist")) + } + + if *sector + >= fs.props.first_root_dir_sector + boot_record_fat.root_dir_sectors() + { + Ok(None) + } else { + Ok(Some(EntryLocationUnit::RootDirSector(sector + 1))) + } + } + BootRecord::ExFAT(_) => todo!("ExFAT is not implemented yet"), + }, + EntryLocationUnit::DataCluster(cluster) => Ok(fs + .get_next_cluster(*cluster)? + .map(EntryLocationUnit::DataCluster)), + } + } } /// The location of a [`FATDirEntry`] -#[derive(Debug)] +#[derive(Clone, Debug)] pub(crate) struct EntryLocation { /// the location of the first corresponding entry's data unit pub(crate) unit: EntryLocationUnit, @@ -306,6 +361,45 @@ pub(crate) struct EntryLocation { pub(crate) index: u32, } +impl EntryLocation { + pub(crate) fn free_entry(&self, fs: &mut FileSystem) -> Result<(), S::Error> + where + S: Read + Write + Seek, + { + let entry_sector = self.unit.get_entry_sector(fs); + fs.read_nth_sector(entry_sector)?; + + let byte_offset = self.index as usize * DIRENTRY_SIZE; + fs.sector_buffer[byte_offset] = UNUSED_ENTRY; + fs.buffer_modified = true; + + Ok(()) + } + + pub(crate) fn next_entry( + mut self, + fs: &mut FileSystem, + ) -> Result, S::Error> + where + S: Read + Write + Seek, + { + self.index += 1; + + // we haven't advanced to a new unit, we return immediately + if u64::from(self.index) < self.unit.get_max_offset(fs) { + return Ok(Some(self)); + } + + // we try to advance to the next entry unit (if it exists) + Ok(self.unit.get_next_unit(fs)?.map(|unit| { + self.unit = unit; + self.index = 0; + + self + })) + } +} + /// The location of a chain of [`FATDirEntry`] #[derive(Debug)] pub(crate) struct DirEntryChain { diff --git a/src/fat/fs.rs b/src/fat/fs.rs index 356f39c..d8de297 100644 --- a/src/fat/fs.rs +++ b/src/fat/fs.rs @@ -1136,57 +1136,25 @@ where /// /// Note: No validation is done to check whether or not the chain is valid pub(crate) fn remove_entry_chain(&mut self, chain: &DirEntryChain) -> Result<(), S::Error> { - // we begin by removing the corresponding entries... let mut entries_freed = 0; - let mut current_offset = chain.location.index; - - // current_cluster_option is `None` if we are dealing with a root directory entry - let (mut current_sector, current_cluster_option): (u32, Option) = - match chain.location.unit { - EntryLocationUnit::RootDirSector(root_dir_sector) => ( - (root_dir_sector + self.props.first_root_dir_sector).into(), - None, - ), - EntryLocationUnit::DataCluster(data_cluster) => ( - self.data_cluster_to_partition_sector(data_cluster), - Some(data_cluster), - ), - }; + let mut current_entry = chain.location.clone(); - while entries_freed < chain.len { - if current_sector as u64 != self.stored_sector { - self.read_nth_sector(current_sector.into())?; - } + loop { + current_entry.free_entry(self)?; - // we won't even bother zeroing the entire thing, just the first byte - let byte_offset = current_offset as usize * DIRENTRY_SIZE; - self.sector_buffer[byte_offset] = UNUSED_ENTRY; - self.buffer_modified = true; - - log::trace!( - "freed entry at sector {} with byte offset {}", - current_sector, - byte_offset - ); - - if current_offset + 1 >= (self.sector_size() / DIRENTRY_SIZE as u32) { - // we have moved to a new sector - current_sector += 1; - - if let Some(mut current_cluster) = current_cluster_option { - // data region - if self.partition_sector_to_data_cluster(current_sector) != current_cluster { - current_cluster = self.get_next_cluster(current_cluster)?.unwrap(); - current_sector = self.data_cluster_to_partition_sector(current_cluster); - } - } + entries_freed += 1; - current_offset = 0; - } else { - current_offset += 1 + if entries_freed >= chain.len { + break; } - entries_freed += 1; + current_entry = match current_entry.next_entry(self)? { + Some(current_entry) => current_entry, + None => unreachable!( + concat!("It is guaranteed that at least as many entries ", + "as there are in chain exist, since we counted them when initializing the struct") + ), + }; } Ok(()) From 4552d6ff38cd53a01a13f9f9e7238d4265192a0c Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Mon, 23 Dec 2024 17:12:22 +0200 Subject: [PATCH 35/40] feat: change expression to comply with clippy's manual_div_ceil --- src/fat/bpb.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/fat/bpb.rs b/src/fat/bpb.rs index 262d136..5566731 100644 --- a/src/fat/bpb.rs +++ b/src/fat/bpb.rs @@ -92,8 +92,7 @@ impl BootRecordFAT { /// The size of the root directory (unless we have FAT32, in which case the size will be 0) /// This calculation will round up pub(crate) fn root_dir_sectors(&self) -> u16 { - ((self.bpb.root_entry_count * DIRENTRY_SIZE as u16) + (self.bpb.bytes_per_sector - 1)) - / self.bpb.bytes_per_sector + (self.bpb.root_entry_count * DIRENTRY_SIZE as u16).div_ceil(self.bpb.bytes_per_sector) } #[inline] From b36897504cf71a476a2f920abf1089a5e68154d2 Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Wed, 25 Dec 2024 16:46:57 +0200 Subject: [PATCH 36/40] chore: replace the path module with a proper path library --- Cargo.toml | 1 + src/error.rs | 2 +- src/fat/fs.rs | 137 +++++++++++++++--------- src/lib.rs | 6 +- src/path.rs | 290 +------------------------------------------------- 5 files changed, 93 insertions(+), 343 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a84d169..2d876f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ log = "0.4.22" serde = { version = "1.0.204", default-features = false, features = [ "alloc", "derive" ] } serde-big-array = "0.5.1" time = { version = "0.3.36", default-features = false, features = [ "alloc", "parsing", "macros" ]} +typed-path = { version = "0.10.0", default-features = false } [features] default = ["std"] diff --git a/src/error.rs b/src/error.rs index 03f6756..d38e1ac 100644 --- a/src/error.rs +++ b/src/error.rs @@ -127,7 +127,7 @@ where #[displaydoc("An internal FS error occured: {0}")] InternalFSError(InternalFSError), /** - The [PathBuf](`crate::path::PathBuf`) provided is malformed. + The [Path](`crate::Path`) provided is malformed. This is mostly an error variant used for internal testing. If you get this error, open an issue: diff --git a/src/fat/fs.rs b/src/fat/fs.rs index d8de297..841ae72 100644 --- a/src/fat/fs.rs +++ b/src/fat/fs.rs @@ -1,6 +1,6 @@ use super::*; -use crate::{error::*, io::prelude::*, path::PathBuf, time::*, utils, SPECIAL_ENTRIES}; +use crate::{error::*, io::prelude::*, path::*, time::*, utils}; use core::{cmp, ops}; @@ -178,6 +178,7 @@ pub(crate) struct RawProperties { #[derive(Debug)] pub struct Properties { pub(crate) path: PathBuf, + pub(crate) is_dir: bool, pub(crate) attributes: Attributes, pub(crate) created: PrimitiveDateTime, pub(crate) modified: PrimitiveDateTime, @@ -197,6 +198,18 @@ impl Properties { &self.path } + #[inline] + /// Check whether this entry belongs to a directory + pub fn is_dir(&self) -> bool { + self.is_dir + } + + #[inline] + /// Check whether this entry belongs to a file + pub fn is_file(&self) -> bool { + !self.is_dir() + } + #[inline] /// Get the corresponding [`Attributes`] to this entry pub fn attributes(&self) -> &Attributes { @@ -242,6 +255,7 @@ impl Properties { fn from_raw(raw: RawProperties, path: PathBuf) -> Self { Properties { path, + is_dir: raw.is_dir, attributes: raw.attributes.into(), created: raw.created, modified: raw.modified, @@ -1273,7 +1287,10 @@ where /// Like [`Self::get_rw_file`], but will ignore the read-only flag (if it is present) /// /// This is a private function for obvious reasons - fn get_rw_file_unchecked(&mut self, path: PathBuf) -> FSResult, S::Error> { + fn get_rw_file_unchecked>( + &mut self, + path: P, + ) -> FSResult, S::Error> { self._raise_io_rw_result()?; let ro_file = self.get_ro_file(path)?; @@ -1290,20 +1307,20 @@ where /// Read all the entries of a directory ([`PathBuf`]) into [`Vec`] /// /// Fails if `path` doesn't represent a directory, or if that directory doesn't exist - pub fn read_dir(&mut self, path: PathBuf) -> FSResult, S::Error> { - if path.is_malformed() { - return Err(FSError::MalformedPath); - } - if !path.is_dir() { - log::error!("Not a directory"); - return Err(FSError::NotADirectory); - } + pub fn read_dir>(&mut self, path: P) -> FSResult, S::Error> { + // normalize the given path + let path = path.as_ref().normalize(); let mut entries = self.process_root_dir()?; - for dir_name in path.clone() { + for dir_name in path + .components() + // the path is normalized, so this essentially only filters prefixes and the root directory + .filter(|component| matches!(component, WindowsComponent::Normal(_))) + { let dir_cluster = match entries.iter().find(|entry| { - entry.name == dir_name && entry.attributes.contains(RawAttributes::DIRECTORY) + entry.name == dir_name.as_str() + && entry.attributes.contains(RawAttributes::DIRECTORY) }) { Some(entry) => entry.data_cluster, None => { @@ -1321,15 +1338,15 @@ where .into_iter() .filter(|x| self.filter.filter(x)) // we shouldn't expose the special entries to the user - .filter(|x| !SPECIAL_ENTRIES.contains(&x.name.as_str())) + .filter(|x| { + ![path_consts::CURRENT_DIR_STR, path_consts::PARENT_DIR_STR] + .contains(&x.name.as_str()) + }) .map(|rawentry| { - let mut entry_path = path.clone(); + let mut entry_path = path.to_path_buf(); + + entry_path.push(PathBuf::from(&rawentry.name)); - entry_path.push(format!( - "{}{}", - rawentry.name, - if rawentry.is_dir { "/" } else { "" } - )); DirEntry { entry: Properties::from_raw(rawentry, entry_path), } @@ -1342,13 +1359,17 @@ where /// Borrows `&mut self` until that [`ROFile`] object is dropped, effectively locking `self` until that file closed /// /// Fails if `path` doesn't represent a file, or if that file doesn't exist - pub fn get_ro_file(&mut self, path: PathBuf) -> FSResult, S::Error> { - if path.is_malformed() { - return Err(FSError::MalformedPath); - } + pub fn get_ro_file>( + &mut self, + path: P, + ) -> FSResult, S::Error> { + let path = path.as_ref(); if let Some(file_name) = path.file_name() { - let parent_dir = self.read_dir(path.parent())?; + let parent_dir = self.read_dir( + path.parent() + .expect("we aren't in the root directory, this shouldn't panic"), + )?; match parent_dir.into_iter().find(|direntry| { direntry .path() @@ -1395,7 +1416,7 @@ where /// /// This is an alias to `self.get_rw_file(path)?.remove()?` #[inline] - pub fn remove_file(&mut self, path: PathBuf) -> FSResult<(), S::Error> { + pub fn remove_file>(&mut self, path: P) -> FSResult<(), S::Error> { self.get_rw_file(path)?.remove()?; Ok(()) @@ -1405,7 +1426,7 @@ where /// /// **USE WITH EXTREME CAUTION!** #[inline] - pub fn remove_file_unchecked(&mut self, path: PathBuf) -> FSResult<(), S::Error> { + pub fn remove_file_unchecked>(&mut self, path: P) -> FSResult<(), S::Error> { self.get_rw_file_unchecked(path)?.remove()?; Ok(()) @@ -1414,17 +1435,15 @@ where /// Remove an empty directory from the filesystem /// /// Errors if the path provided points to the root directory - pub fn remove_empty_dir(&mut self, path: PathBuf) -> FSResult<(), S::Error> { - if path.is_malformed() { - return Err(FSError::MalformedPath); - } - - if !path.is_dir() { - log::error!("Not a directory"); - return Err(FSError::NotADirectory); - } - - if path == PathBuf::new() { + pub fn remove_empty_dir>(&mut self, path: P) -> FSResult<(), S::Error> { + let path = path.as_ref(); + + if path + .components() + .last() + .expect("this iterator will always yield at least the root directory") + == WindowsComponent::root() + { // we are in the root directory, we can't remove it return Err(S::Error::new( ::Kind::new_unsupported(), @@ -1433,13 +1452,15 @@ where .into()); } - let dir_entries = self.read_dir(path.clone())?; + let dir_entries = self.read_dir(path)?; if dir_entries.len() > NONROOT_MIN_DIRENTRIES { return Err(FSError::DirectoryNotEmpty); } - let parent_path = path.parent(); + let parent_path = path + .parent() + .expect("we aren't in the root directory, this shouldn't panic"); let parent_dir_entries = self.read_dir(parent_path)?; @@ -1464,11 +1485,11 @@ where /// This will fail if there is at least 1 (one) read-only file /// in this directory or in any subdirectory. To avoid this behaviour, /// use [`remove_dir_all_unchecked()`](FileSystem::remove_dir_all_unchecked) - pub fn remove_dir_all(&mut self, path: PathBuf) -> FSResult<(), S::Error> { + pub fn remove_dir_all>(&mut self, path: P) -> FSResult<(), S::Error> { // before we actually start removing stuff, // let's make sure there are no read-only files - if self.check_for_readonly_files(path.clone())? { + if self.check_for_readonly_files(&path)? { log::error!(concat!( "A read-only file has been found ", "in a directory pending deletion." @@ -1477,7 +1498,7 @@ where } // we have checked everything, this is safe to use - self.remove_dir_all_unchecked(path)?; + self.remove_dir_all_unchecked(&path)?; Ok(()) } @@ -1486,12 +1507,14 @@ where /// but also removes read-only files. /// /// **USE WITH EXTREME CAUTION!** - pub fn remove_dir_all_unchecked(&mut self, path: PathBuf) -> FSResult<(), S::Error> { - for entry in self.read_dir(path.clone())? { - if entry.path.is_dir() { - self.remove_dir_all(entry.path.to_owned())?; - } else if entry.path.is_file() { - self.remove_file_unchecked(entry.path.to_owned())?; + pub fn remove_dir_all_unchecked>(&mut self, path: P) -> FSResult<(), S::Error> { + let path = path.as_ref(); + + for entry in self.read_dir(path)? { + if entry.is_dir() { + self.remove_dir_all(&entry.path)?; + } else if entry.is_file() { + self.remove_file_unchecked(&entry.path)?; } else { unreachable!() } @@ -1506,11 +1529,16 @@ where /// /// If successful, the `bool` returned indicates /// whether or not at least 1 (one) read-only file has been found - pub fn check_for_readonly_files(&mut self, path: PathBuf) -> FSResult { + pub fn check_for_readonly_files>( + &mut self, + path: P, + ) -> FSResult { + let path = path.as_ref(); + for entry in self.read_dir(path)? { - let read_only_found = if entry.path.is_dir() { - self.check_for_readonly_files(entry.path.to_owned())? - } else if entry.path.is_file() { + let read_only_found = if entry.is_dir() { + self.check_for_readonly_files(&entry.path)? + } else if entry.is_file() { entry.attributes.read_only } else { unreachable!() @@ -1531,7 +1559,10 @@ where /// Borrows `&mut self` until that [`RWFile`] object is dropped, effectively locking `self` until that file closed /// /// Fails if `path` doesn't represent a file, or if that file doesn't exist - pub fn get_rw_file(&mut self, path: PathBuf) -> FSResult, S::Error> { + pub fn get_rw_file>( + &mut self, + path: P, + ) -> FSResult, S::Error> { let rw_file = self.get_rw_file_unchecked(path)?; if rw_file.attributes.read_only { diff --git a/src/lib.rs b/src/lib.rs index 2cd0382..6c552c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,11 +25,11 @@ //! // We can either pass by value of by (mutable) reference //! let mut fs = FileSystem::from_storage(&mut cursor).unwrap(); //! -//! // Let's see what entries are in the root directory +//! // Let's see what entries there are in the root directory //! for entry in fs.read_dir(PathBuf::from("/")).unwrap() { -//! if entry.path().is_dir() { +//! if entry.is_dir() { //! println!("Directory: {}", entry.path()) -//! } else if entry.path().is_file() { +//! } else if entry.is_file() { //! println!("File: {}", entry.path()) //! } else { //! unreachable!() diff --git a/src/path.rs b/src/path.rs index 2d0aa34..3dead50 100644 --- a/src/path.rs +++ b/src/path.rs @@ -1,289 +1,7 @@ -//! Note: Paths here are Unix-like when converted to [`String`]s (the root directory is `/`, and the directory separator is `/`), -//! but DOS-like paths are also accepted and converted to Unix-like when pushed -//! -//! It is possible to push forbidden characters to a [`PathBuf`], doing so however will make most functions -//! return a [`MalformedPath`](crate::error::FSError::MalformedPath) error -//! +//! Re-exports for [`typed_path`] -use core::{fmt, iter}; - -#[cfg(not(feature = "std"))] -use alloc::{ - borrow::ToOwned, - collections::vec_deque::VecDeque, - string::{String, ToString}, - vec::Vec, +pub(crate) use typed_path::{ + constants::windows as path_consts, Utf8Component as _, Utf8WindowsComponent as WindowsComponent, }; -#[cfg(feature = "std")] -use std::collections::VecDeque; - -// A (incomplete) list of all the forbidden filename/directory name characters -// See https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN - -/// A list of all the characters that are forbidden to exist in a filename -pub const FORBIDDEN_CHARS: &[char] = &['<', '>', ':', '"', '/', '\\', ',', '?', '*']; -/// A list of all filenames that are reserved in Windows (and should probably also not be used by FAT) -pub const RESERVED_FILENAMES: &[&str] = &[ - "CON", "PRN", "AUX", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", - "COM8", "COM9", "COM¹", "COM²", "COM³", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", - "LPT7", "LPT8", "LPT9", "LPT¹", "LPT²", "LPT³", -]; -/// Except for the root directory, each directory must contain -/// the following (two) entries at the beginning of the directory -pub(crate) const SPECIAL_ENTRIES: &[&str] = &[".", ".."]; - -/// Check whether a [`PathBuf`] is forbidden for use in filenames or directory names -fn is_forbidden(pathbuf: &PathBuf) -> bool { - for subpath in pathbuf.clone() { - if subpath - .chars() - .any(|c| c.is_control() || FORBIDDEN_CHARS.contains(&c)) - { - return true; - } - } - - if let Some(file_name) = pathbuf.file_name() { - if RESERVED_FILENAMES - .iter() - // remove extension - .map(|file_name| file_name.split_once('.').map(|s| s.0).unwrap_or(file_name)) - .any(|reserved| file_name == reserved) - { - return true; - } - } - - false -} - -// TODO: pushing an absolute path should replace a pathbuf - -/// Represents an owned, mutable path -#[derive(Debug, Default, Clone, PartialEq, Eq)] -pub struct PathBuf { - inner: VecDeque, -} - -impl PathBuf { - /// Create a new, empty [`PathBuf`] pointing to the root directory ("/") - pub fn new() -> Self { - log::debug!("New PathBuf created"); - Self::default() - } - - /// Extends `self` with `subpath` - /// - /// Doesn't replace the current path if the `path` is absolute - pub fn push

(&mut self, subpath: P) - where - P: ToString, - { - let subpath = subpath.to_string(); - - // if this is an absolute path, clear `self` - if ['\\', '/'].contains(&subpath.chars().next().unwrap_or_default()) { - self.clear() - } - - let mut split_subpath: VecDeque<&str> = - subpath.split(|c| ['\\', '/'].contains(&c)).collect(); - - // remove trailing slash in the beginning of split_subpath.. - if split_subpath.front().map(|s| s.is_empty()).unwrap_or(false) { - split_subpath.pop_front(); - }; - // ...as well in the beginning of the inner deque - if self.inner.back().map(|s| s.is_empty()).unwrap_or(false) { - self.inner.pop_back(); - }; - - for p in split_subpath { - match p { - "." => continue, - ".." => { - self.inner.pop_back(); - continue; - } - _ => self.inner.push_back(p.to_string()), - } - } - - log::debug!("Pushed {} into PathBuf", subpath); - } - - /// If `self` is a file, returns `Ok(file_name)`, otherwise `None` - pub fn file_name(&self) -> Option { - if let Some(file_name) = self.inner.back() { - if !file_name.is_empty() { - return Some(file_name.to_owned()); - } - } - - None - } - - /// Returns the parent directory of the current [`PathBuf`] - pub fn parent(&self) -> PathBuf { - if self.inner.len() > 1 { - let mut pathbuf = self.clone(); - - if pathbuf.is_dir() { - pathbuf.inner.pop_back(); - } - pathbuf.inner.back_mut().unwrap().clear(); - - pathbuf - } else { - PathBuf::new() - } - } - - /// Whether or not `self` represents a directory - pub fn is_dir(&self) -> bool { - match self.inner.len() { - // root directory - 0 => true, - n => self.inner[n - 1].is_empty(), - } - } - - /// Whether or not `self` represents a file - pub fn is_file(&self) -> bool { - !self.is_dir() - } - - /// Resets `self` - pub fn clear(&mut self) { - *self = Self::new(); - } - - /// Checks whether `self` is malformed (as if someone pushed a string with many consecutive slashes) - pub(crate) fn is_malformed(&self) -> bool { - is_forbidden(self) - } -} - -impl From for PathBuf { - fn from(value: String) -> Self { - let mut pathbuf = PathBuf::new(); - pathbuf.push(value); - - pathbuf - } -} - -impl From<&str> for PathBuf { - fn from(value: &str) -> Self { - Self::from(value.to_string()) - } -} - -impl fmt::Display for PathBuf { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "/{}", - self.inner.iter().cloned().collect::>().join("/") - ) - } -} - -impl iter::Iterator for PathBuf { - type Item = String; - - fn next(&mut self) -> Option { - // for this to work correctly, the PathBuf mustn't be malformed - self.inner.pop_front().filter(|s| !s.is_empty()) - } -} - -#[cfg(all(test, feature = "std"))] -mod tests { - use super::*; - use test_log::test; - - #[test] - fn empty_pathbuf_tostring() { - let pathbuf = PathBuf::new(); - - assert_eq!(pathbuf.to_string(), "/"); - assert!(!pathbuf.is_malformed()); - } - - #[test] - fn catch_invalid_path() { - #[cfg(not(feature = "std"))] - use alloc::format; - - let mut pathbuf = PathBuf::new(); - - // ignore path separators - for filename in RESERVED_FILENAMES { - pathbuf.push(format!("/{filename}")); - - assert!( - pathbuf.is_malformed(), - "unable to detect invalid filename {}", - pathbuf - ); - - pathbuf.clear(); - } - } - - #[test] - fn catch_non_control_forbidden_chars() { - #[cfg(not(feature = "std"))] - use alloc::format; - - let mut pathbuf = PathBuf::new(); - - // ignore path separators - const PATH_SEPARATORS: &[char] = &['/', '\\']; - for c in FORBIDDEN_CHARS - .iter() - .filter(|c| !PATH_SEPARATORS.contains(c)) - { - pathbuf.push(format!("/{c}")); - - assert!( - pathbuf.is_malformed(), - "unable to detect character {} (hex: {:#02x}) as forbidden {:?}", - c, - (*c as usize), - pathbuf - ); - - pathbuf.clear(); - } - } - - #[test] - fn push_to_pathbuf() { - let mut pathbuf = PathBuf::new(); - - pathbuf.push("foo"); - pathbuf.push("bar/test"); - pathbuf.push("bar2\\test2"); - pathbuf.push("ignored\\../."); - pathbuf.push("fintest1"); - pathbuf.push("fintest2/"); - pathbuf.push("last"); - - assert_eq!( - pathbuf.to_string(), - "/foo/bar/test/bar2/test2/fintest1/fintest2/last" - ) - } - - #[test] - fn push_absolute_path() { - let mut pathbuf = PathBuf::from("/foo/bar.txt"); - - pathbuf.push("\\test"); - - assert_eq!(pathbuf.to_string(), "/test") - } -} +pub use typed_path::{Utf8WindowsPath as Path, Utf8WindowsPathBuf as PathBuf}; From 9cea9f044e837e7e59b0c159f75ec4a3a11370fe Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Tue, 7 Jan 2025 17:48:25 +0200 Subject: [PATCH 37/40] refactor: implement current directory location caching Up until this commit, each time we wanted to go to a directory we would start from the root. This change will both make navigation faster, but it will also make it easier to implement file and directory renaming and movie later on --- src/error.rs | 2 + src/fat/fs.rs | 206 ++++++++++++++++++++++++++++++++++++++++++-------- src/path.rs | 31 ++++++++ 3 files changed, 206 insertions(+), 33 deletions(-) diff --git a/src/error.rs b/src/error.rs index d38e1ac..4d2bc78 100644 --- a/src/error.rs +++ b/src/error.rs @@ -115,6 +115,8 @@ pub enum InternalFSError { MismatchingFATTables, /// Encountered a malformed cluster chain MalformedClusterChain, + /// Encountered a malformed directory entry chain + MalformedEntryChain, } /// An error indicating that a filesystem-related operation has failed diff --git a/src/fat/fs.rs b/src/fat/fs.rs index 841ae72..02b3ff9 100644 --- a/src/fat/fs.rs +++ b/src/fat/fs.rs @@ -174,6 +174,21 @@ pub(crate) struct RawProperties { pub(crate) chain: DirEntryChain, } +impl RawProperties { + pub(crate) fn into_dir_entry

(self, path: P) -> DirEntry + where + P: AsRef, + { + let mut entry_path = path.as_ref().to_path_buf(); + + entry_path.push(PathBuf::from(&self.name)); + + DirEntry { + entry: Properties::from_raw(self, entry_path), + } + } +} + /// A container for file/directory properties #[derive(Debug)] pub struct Properties { @@ -267,6 +282,31 @@ impl Properties { } } +#[derive(Debug, Clone)] +struct DirInfo { + path: PathBuf, + chain_start: EntryLocationUnit, +} + +impl DirInfo { + fn at_root_dir(boot_record: &BootRecord) -> Self { + DirInfo { + // this is basically the root directory + path: PathBuf::from(path_consts::SEPARATOR_STR), + chain_start: match boot_record { + BootRecord::Fat(boot_record_fat) => match boot_record_fat.ebr { + // it doesn't really matter what value we put in here, since we won't be using it + Ebr::FAT12_16(_ebr_fat12_16) => EntryLocationUnit::RootDirSector(0), + Ebr::FAT32(ebr_fat32, _) => { + EntryLocationUnit::DataCluster(ebr_fat32.root_cluster) + } + }, + BootRecord::ExFAT(_boot_record_exfat) => todo!(), + }, + } + } +} + /// A thin wrapper for [`Properties`] represing a directory entry #[derive(Debug)] pub struct DirEntry { @@ -596,6 +636,8 @@ where fsinfo_modified: bool, pub(crate) stored_sector: u64, + dir_info: DirInfo, + clock: &'a dyn Clock, pub(crate) boot_record: BootRecord, @@ -731,6 +773,7 @@ where fsinfo_modified: false, stored_sector, clock: &STATIC_DEFAULT_CLOCK, + dir_info: DirInfo::at_root_dir(&boot_record), boot_record, fat_type, props, @@ -753,7 +796,7 @@ impl FileSystem<'_, S> where S: Read + Write + Seek, { - fn process_root_dir(&mut self) -> FSResult, S::Error> { + fn _process_root_dir(&mut self) -> FSResult, S::Error> { match self.boot_record { BootRecord::Fat(boot_record_fat) => match boot_record_fat.ebr { Ebr::FAT12_16(_ebr_fat12_16) => { @@ -772,14 +815,14 @@ where } Ebr::FAT32(ebr_fat32, _) => { let cluster = ebr_fat32.root_cluster; - self.process_normal_dir(cluster) + self._process_normal_dir(cluster) } }, BootRecord::ExFAT(_boot_record_exfat) => todo!(), } } - fn process_normal_dir( + fn _process_normal_dir( &mut self, mut data_cluster: u32, ) -> FSResult, S::Error> { @@ -817,6 +860,128 @@ where Ok(entry_parser.finish()) } + fn process_current_dir(&mut self) -> FSResult, S::Error> { + match self.dir_info.chain_start { + EntryLocationUnit::RootDirSector(_) => self._process_root_dir(), + EntryLocationUnit::DataCluster(data_cluster) => self._process_normal_dir(data_cluster), + } + } + + /// Goes to the parent directory. + /// + /// If this is the root directory, it does nothing + fn _go_to_parent_dir(&mut self) -> FSResult<(), S::Error> { + if let Some(parent_path) = self.dir_info.path.parent() { + let parent_pathbuf = parent_path.to_path_buf(); + + let entries = self.process_current_dir()?; + + let parent_entry = entries + .iter() + .filter(|entry| entry.is_dir) + .find(|entry| entry.name == path_consts::PARENT_DIR_STR) + .ok_or(FSError::InternalFSError( + InternalFSError::MalformedEntryChain, + ))?; + + self.dir_info.path = parent_pathbuf; + self.dir_info.chain_start = EntryLocationUnit::DataCluster(parent_entry.data_cluster); + } else { + self._go_to_root_directory(); + } + + Ok(()) + } + + /// Goes to the given child directory + /// + /// If it doesn't exist, the encapsulated [`Option`] will be `None` + fn _go_to_child_dir(&mut self, name: &str) -> FSResult<(), S::Error> { + let entries = self.process_current_dir()?; + + let child_entry = entries + .iter() + .find(|entry| entry.name == name) + .ok_or(FSError::NotFound)?; + + if !child_entry.is_dir { + return Err(FSError::NotADirectory); + } + + self.dir_info.path.push(&child_entry.name); + self.dir_info.chain_start = EntryLocationUnit::DataCluster(child_entry.data_cluster); + + Ok(()) + } + + fn _go_to_root_directory(&mut self) { + self.dir_info = DirInfo::at_root_dir(&self.boot_record); + } + + // This is a helper function for `go_to_dir` + fn _go_up_till_target

(&mut self, target: P) -> FSResult<(), S::Error> + where + P: AsRef, + { + let target = target.as_ref(); + + while self.dir_info.path != target { + self._go_to_parent_dir()?; + } + + Ok(()) + } + + // This is a helper function for `go_to_dir` + fn _go_down_till_target

(&mut self, target: P) -> FSResult<(), S::Error> + where + P: AsRef, + { + for dir_name in target.as_ref().components().filter(keep_path_normals) { + self._go_to_child_dir(dir_name.as_str())?; + } + + Ok(()) + } + + // There are many ways this can be achieved. That's how we'll do it: + // Firstly, we find the common path prefix of the `current_path` and the `target` + // Then, we check whether it is faster to start from the root directory + // and get down to the target or whether we should start from where we are + // now, go up till we find the common prefix path and then go down to the `target` + + /// Navigates to the `target` [`Path`] + pub(crate) fn go_to_dir

(&mut self, target: P) -> FSResult<(), S::Error> + where + P: AsRef, + { + let target = target.as_ref(); + + if self.dir_info.path == target { + // we are already where we want to be + return Ok(()); + } + + let common_path_prefix = find_common_path_prefix(&self.dir_info.path, target); + + // Note: these are the distances to the common prefix, not the target path + let distance_from_root = common_path_prefix.ancestors().count() - 1; + let distance_from_current_path = + (self.dir_info.path.ancestors().count() - 1) - distance_from_root; + + if distance_from_root <= distance_from_current_path { + self._go_to_root_directory(); + + self._go_down_till_target(target)?; + } else { + self._go_up_till_target(target)?; + + self._go_down_till_target(target)?; + } + + Ok(()) + } + /// Gets the next free cluster. Returns an IO [`Result`] /// If the [`Result`] returns [`Ok`] that contains a [`None`], the drive is full pub(crate) fn next_free_cluster(&mut self) -> Result, S::Error> { @@ -1311,29 +1476,11 @@ where // normalize the given path let path = path.as_ref().normalize(); - let mut entries = self.process_root_dir()?; + self.go_to_dir(&path)?; - for dir_name in path - .components() - // the path is normalized, so this essentially only filters prefixes and the root directory - .filter(|component| matches!(component, WindowsComponent::Normal(_))) - { - let dir_cluster = match entries.iter().find(|entry| { - entry.name == dir_name.as_str() - && entry.attributes.contains(RawAttributes::DIRECTORY) - }) { - Some(entry) => entry.data_cluster, - None => { - log::error!("Directory {} not found", path); - return Err(FSError::NotFound); - } - }; - - entries = self.process_normal_dir(dir_cluster)?; - } + let entries = self.process_current_dir()?; - // if we haven't returned by now, that means that the entries vector - // contains what we want, let's map it to DirEntries and return + // let's map the entries vector to DirEntries and return Ok(entries .into_iter() .filter(|x| self.filter.filter(x)) @@ -1342,15 +1489,7 @@ where ![path_consts::CURRENT_DIR_STR, path_consts::PARENT_DIR_STR] .contains(&x.name.as_str()) }) - .map(|rawentry| { - let mut entry_path = path.to_path_buf(); - - entry_path.push(PathBuf::from(&rawentry.name)); - - DirEntry { - entry: Properties::from_raw(rawentry, entry_path), - } - }) + .map(|rawentry| rawentry.into_dir_entry(&path)) .collect()) } @@ -1370,6 +1509,7 @@ where path.parent() .expect("we aren't in the root directory, this shouldn't panic"), )?; + match parent_dir.into_iter().find(|direntry| { direntry .path() diff --git a/src/path.rs b/src/path.rs index 3dead50..b30422e 100644 --- a/src/path.rs +++ b/src/path.rs @@ -5,3 +5,34 @@ pub(crate) use typed_path::{ }; pub use typed_path::{Utf8WindowsPath as Path, Utf8WindowsPathBuf as PathBuf}; + +// if the path is normalized, so this essentially only filters prefixes and the root directory +pub(crate) fn keep_path_normals(component: &WindowsComponent) -> bool { + matches!(component, WindowsComponent::Normal(_)) +} + +pub(crate) fn find_common_path_prefix(path1: P1, path2: P2) -> PathBuf +where + P1: AsRef, + P2: AsRef, +{ + let path1 = path1.as_ref().normalize(); + let path2 = path2.as_ref().normalize(); + + let mut common_prefix_pathbuf = PathBuf::new(); + + for (component1, component2) in path1 + .components() + .filter(keep_path_normals) + .zip(path2.components().filter(keep_path_normals)) + { + if component1 != component2 { + break; + } + + // it doesn't matter whether we push component1 or component2 + common_prefix_pathbuf.push(component1); + } + + common_prefix_pathbuf +} From 6aa47d6b654b6287c7d4e2fa0aa92f6aaaea7a74 Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Tue, 7 Jan 2025 17:49:04 +0200 Subject: [PATCH 38/40] test: add a new test to check whether a file can be interpreted as a directory --- src/fat/tests.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/fat/tests.rs b/src/fat/tests.rs index 1266d4f..b379c85 100644 --- a/src/fat/tests.rs +++ b/src/fat/tests.rs @@ -565,6 +565,26 @@ fn remove_nonempty_fat32_dir() { } } +#[test] +fn attempt_to_remove_file_as_directory() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT32.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let dir_path = PathBuf::from("/hello.txt"); + + let fs_result = fs.remove_dir_all(dir_path); + + match fs_result { + Err(err) => match err { + FSError::NotADirectory => (), + _ => panic!("unexpected IOError: {:?}", err), + }, + _ => panic!("the filesystem struct should have detected that this isn't a directory"), + } +} + #[test] #[allow(non_snake_case)] fn FAT_tables_after_fat32_write_are_identical() { From 9e7255bbc2a1b8a2abbf0fb0c958b532f5fc6f90 Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Tue, 7 Jan 2025 17:50:57 +0200 Subject: [PATCH 39/40] chore: change clippy::redundant_clone level to warn from deny --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 6c552c5..98dacfc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -94,7 +94,7 @@ #![deny(unused_import_braces)] #![deny(unused_lifetimes)] // clippy attributes -#![deny(clippy::redundant_clone)] +#![warn(clippy::redundant_clone)] extern crate alloc; From c57952fca285acae16e34e3a0adf278dca9e01ed Mon Sep 17 00:00:00 2001 From: Oakchris1955 <80592203+Oakchris1955@users.noreply.github.com> Date: Tue, 7 Jan 2025 17:53:18 +0200 Subject: [PATCH 40/40] chore: remove redundant path clones in tests --- src/fat/tests.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/fat/tests.rs b/src/fat/tests.rs index b379c85..44a09cd 100644 --- a/src/fat/tests.rs +++ b/src/fat/tests.rs @@ -176,7 +176,7 @@ fn remove_root_dir_file() { // the bee movie script (here) is in the root directory region let file_path = PathBuf::from("/bee movie script.txt"); - let file = fs.get_rw_file(file_path.clone()).unwrap(); + let file = fs.get_rw_file(&file_path).unwrap(); file.remove().unwrap(); // the file should now be gone @@ -199,7 +199,7 @@ fn remove_data_region_file() { // the bee movie script (here) is in the data region let file_path = PathBuf::from("/test/bee movie script.txt"); - let file = fs.get_rw_file(file_path.clone()).unwrap(); + let file = fs.get_rw_file(&file_path).unwrap(); file.remove().unwrap(); // the file should now be gone @@ -222,7 +222,7 @@ fn remove_empty_dir() { let dir_path = PathBuf::from("/another root directory/"); - fs.remove_empty_dir(dir_path.clone()).unwrap(); + fs.remove_empty_dir(&dir_path).unwrap(); // the directory should now be gone let dir_result = fs.read_dir(dir_path); @@ -245,7 +245,7 @@ fn remove_nonempty_dir_with_readonly_file() { let dir_path = PathBuf::from("/rootdir/"); // the directory should contain a read-only file (example.txt) - let del_result = fs.remove_dir_all(dir_path.clone()); + let del_result = fs.remove_dir_all(&dir_path); match del_result { Err(err) => match err { FSError::ReadOnlyFile => (), @@ -344,7 +344,7 @@ fn get_hidden_file() { let mut fs = FileSystem::from_storage(&mut storage).unwrap(); let file_path = PathBuf::from("/hidden"); - let file_result = fs.get_ro_file(file_path.clone()); + let file_result = fs.get_ro_file(&file_path); match file_result { Err(err) => match err { FSError::NotFound => (), @@ -507,7 +507,7 @@ fn remove_fat32_file() { let file_path = PathBuf::from("/secret/bee movie script.txt"); - let file = fs.get_rw_file(file_path.clone()).unwrap(); + let file = fs.get_rw_file(&file_path).unwrap(); file.remove().unwrap(); // the file should now be gone @@ -530,7 +530,7 @@ fn remove_empty_fat32_dir() { let dir_path = PathBuf::from("/emptydir/"); - fs.remove_empty_dir(dir_path.clone()).unwrap(); + fs.remove_empty_dir(&dir_path).unwrap(); // the directory should now be gone let dir_result = fs.read_dir(dir_path); @@ -552,7 +552,7 @@ fn remove_nonempty_fat32_dir() { let dir_path = PathBuf::from("/secret/"); - fs.remove_dir_all(dir_path.clone()).unwrap(); + fs.remove_dir_all(&dir_path).unwrap(); // the directory should now be gone let dir_result = fs.read_dir(dir_path);