From 7951b76258a3683f0610aa007e1dd1cadf72ed92 Mon Sep 17 00:00:00 2001 From: Andrew Walbran Date: Wed, 7 Aug 2024 17:30:53 +0100 Subject: [PATCH 01/62] Implement embedded-io traits for File. --- CHANGELOG.md | 2 +- Cargo.toml | 1 + src/filesystem/files.rs | 78 ++++++++++++++++++++++++++++++++++++++++- src/lib.rs | 36 +++++++++++++++++++ 4 files changed, 115 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecb40f3a..3361d1a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ The format is based on [Keep a Changelog] and this project adheres to [Semantic ### Added -- None +- `File` now implements the `embedded-io` `Read`, `Write` and `Seek` traits. ### Removed diff --git a/Cargo.toml b/Cargo.toml index 88412f76..45276c1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ version = "0.8.0" byteorder = {version = "1", default-features = false} defmt = {version = "0.3", optional = true} embedded-hal = "1.0.0" +embedded-io = "0.6.1" heapless = "^0.8" log = {version = "0.4", default-features = false, optional = true} diff --git a/src/filesystem/files.rs b/src/filesystem/files.rs index 5e524775..1b13e096 100644 --- a/src/filesystem/files.rs +++ b/src/filesystem/files.rs @@ -1,7 +1,9 @@ +use super::TimeSource; use crate::{ filesystem::{ClusterId, DirEntry, SearchId}, - Error, RawVolume, VolumeManager, + BlockDevice, Error, RawVolume, VolumeManager, }; +use embedded_io::{ErrorType, Read, Seek, SeekFrom, Write}; /// A handle for an open file on disk. /// @@ -165,6 +167,80 @@ where } } +impl< + D: BlockDevice, + T: TimeSource, + const MAX_DIRS: usize, + const MAX_FILES: usize, + const MAX_VOLUMES: usize, + > ErrorType for File<'_, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +{ + type Error = crate::Error; +} + +impl< + D: BlockDevice, + T: TimeSource, + const MAX_DIRS: usize, + const MAX_FILES: usize, + const MAX_VOLUMES: usize, + > Read for File<'_, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +{ + fn read(&mut self, buf: &mut [u8]) -> Result { + if buf.is_empty() { + Ok(0) + } else { + self.read(buf) + } + } +} + +impl< + D: BlockDevice, + T: TimeSource, + const MAX_DIRS: usize, + const MAX_FILES: usize, + const MAX_VOLUMES: usize, + > Write for File<'_, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +{ + fn write(&mut self, buf: &[u8]) -> Result { + if buf.is_empty() { + Ok(0) + } else { + self.write(buf)?; + Ok(buf.len()) + } + } + + fn flush(&mut self) -> Result<(), Self::Error> { + self.flush() + } +} + +impl< + D: BlockDevice, + T: TimeSource, + const MAX_DIRS: usize, + const MAX_FILES: usize, + const MAX_VOLUMES: usize, + > Seek for File<'_, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> +{ + fn seek(&mut self, pos: SeekFrom) -> Result { + match pos { + SeekFrom::Start(offset) => { + self.seek_from_start(offset.try_into().map_err(|_| Error::InvalidOffset)?)? + } + SeekFrom::End(offset) => { + self.seek_from_end((-offset).try_into().map_err(|_| Error::InvalidOffset)?)? + } + SeekFrom::Current(offset) => { + self.seek_from_current(offset.try_into().map_err(|_| Error::InvalidOffset)?)? + } + } + Ok(self.offset().into()) + } +} + #[cfg(feature = "defmt-log")] impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> defmt::Format for File<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> diff --git a/src/lib.rs b/src/lib.rs index f1c4e029..f5105717 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -73,6 +73,8 @@ pub mod fat; pub mod filesystem; pub mod sdcard; +use core::fmt::Debug; +use embedded_io::ErrorKind; use filesystem::SearchId; #[doc(inline)] @@ -202,6 +204,40 @@ where DirAlreadyExists, } +impl embedded_io::Error for Error { + fn kind(&self) -> ErrorKind { + match self { + Error::DeviceError(_) + | Error::FormatError(_) + | Error::FileAlreadyOpen + | Error::DirAlreadyOpen + | Error::VolumeStillInUse + | Error::VolumeAlreadyOpen + | Error::EndOfFile + | Error::DiskFull + | Error::NotEnoughSpace + | Error::AllocationError => ErrorKind::Other, + Error::NoSuchVolume + | Error::FilenameError(_) + | Error::BadHandle + | Error::InvalidOffset => ErrorKind::InvalidInput, + Error::TooManyOpenVolumes | Error::TooManyOpenDirs | Error::TooManyOpenFiles => { + ErrorKind::OutOfMemory + } + Error::NotFound => ErrorKind::NotFound, + Error::OpenedDirAsFile + | Error::OpenedFileAsDir + | Error::DeleteDirAsFile + | Error::BadCluster + | Error::ConversionError + | Error::UnterminatedFatChain => ErrorKind::InvalidData, + Error::Unsupported | Error::BadBlockSize(_) => ErrorKind::Unsupported, + Error::ReadOnly => ErrorKind::PermissionDenied, + Error::FileAlreadyExists | Error::DirAlreadyExists => ErrorKind::AlreadyExists, + } + } +} + impl From for Error where E: core::fmt::Debug, From 3e672aacc9a6654d4fbd07eef7de3f97e0c83feb Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Fri, 11 Oct 2024 17:00:19 +0200 Subject: [PATCH 02/62] Prefer chunks_exact. --- src/fat/volume.rs | 59 ++++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/src/fat/volume.rs b/src/fat/volume.rs index 8f44c6d9..76a60f4d 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -313,11 +313,10 @@ impl FatVolume { block_device .read(&mut blocks, block, "read_dir") .map_err(Error::DeviceError)?; - let entries_per_block = Block::LEN / OnDiskDirEntry::LEN; - for entry in 0..entries_per_block { - let start = entry * OnDiskDirEntry::LEN; - let end = (entry + 1) * OnDiskDirEntry::LEN; - let dir_entry = OnDiskDirEntry::new(&blocks[0][start..end]); + for (i, dir_entry_bytes) in + blocks[0].chunks_exact_mut(OnDiskDirEntry::LEN).enumerate() + { + let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); // 0x00 or 0xE5 represents a free entry if !dir_entry.is_valid() { let ctime = time_source.get_timestamp(); @@ -327,9 +326,9 @@ impl FatVolume { ClusterId::EMPTY, ctime, block, - start as u32, + (i * OnDiskDirEntry::LEN) as u32, ); - blocks[0][start..start + 32] + dir_entry_bytes .copy_from_slice(&entry.serialize(FatType::Fat16)[..]); block_device .write(&blocks, block) @@ -381,10 +380,10 @@ impl FatVolume { .map_err(Error::DeviceError)?; // Are any entries in the block we just loaded blank? If so // we can use them. - for entry in 0..Block::LEN / OnDiskDirEntry::LEN { - let start = entry * OnDiskDirEntry::LEN; - let end = (entry + 1) * OnDiskDirEntry::LEN; - let dir_entry = OnDiskDirEntry::new(&blocks[0][start..end]); + for (i, dir_entry_bytes) in + blocks[0].chunks_exact_mut(OnDiskDirEntry::LEN).enumerate() + { + let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); // 0x00 or 0xE5 represents a free entry if !dir_entry.is_valid() { let ctime = time_source.get_timestamp(); @@ -394,9 +393,9 @@ impl FatVolume { ClusterId(0), ctime, block, - start as u32, + (i * OnDiskDirEntry::LEN) as u32, ); - blocks[0][start..start + 32] + dir_entry_bytes .copy_from_slice(&entry.serialize(FatType::Fat32)[..]); block_device .write(&blocks, block) @@ -483,16 +482,14 @@ impl FatVolume { while let Some(cluster) = current_cluster { for block_idx in first_dir_block_num.range(dir_size) { let block = block_cache.read(block_device, block_idx, "read_dir")?; - for entry in 0..Block::LEN / OnDiskDirEntry::LEN { - let start = entry * OnDiskDirEntry::LEN; - let end = (entry + 1) * OnDiskDirEntry::LEN; - let dir_entry = OnDiskDirEntry::new(&block[start..end]); + for (i, dir_entry_bytes) in block.chunks_exact(OnDiskDirEntry::LEN).enumerate() { + let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); if dir_entry.is_end() { // Can quit early return Ok(()); } else if dir_entry.is_valid() && !dir_entry.is_lfn() { // Safe, since Block::LEN always fits on a u32 - let start = u32::try_from(start).unwrap(); + let start = (i * OnDiskDirEntry::LEN) as u32; let entry = dir_entry.get_entry(FatType::Fat16, block_idx, start); func(&entry); } @@ -538,16 +535,16 @@ impl FatVolume { block_device .read(&mut blocks, block, "read_dir") .map_err(Error::DeviceError)?; - for entry in 0..Block::LEN / OnDiskDirEntry::LEN { - let start = entry * OnDiskDirEntry::LEN; - let end = (entry + 1) * OnDiskDirEntry::LEN; - let dir_entry = OnDiskDirEntry::new(&blocks[0][start..end]); + for (i, dir_entry_bytes) in + blocks[0].chunks_exact_mut(OnDiskDirEntry::LEN).enumerate() + { + let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); if dir_entry.is_end() { // Can quit early return Ok(()); } else if dir_entry.is_valid() && !dir_entry.is_lfn() { // Safe, since Block::LEN always fits on a u32 - let start = u32::try_from(start).unwrap(); + let start = (i * OnDiskDirEntry::LEN) as u32; let entry = dir_entry.get_entry(FatType::Fat32, block, start); func(&entry); } @@ -664,17 +661,15 @@ impl FatVolume { block_device .read(&mut blocks, block, "read_dir") .map_err(Error::DeviceError)?; - for entry in 0..Block::LEN / OnDiskDirEntry::LEN { - let start = entry * OnDiskDirEntry::LEN; - let end = (entry + 1) * OnDiskDirEntry::LEN; - let dir_entry = OnDiskDirEntry::new(&blocks[0][start..end]); + for (i, dir_entry_bytes) in blocks[0].chunks_exact_mut(OnDiskDirEntry::LEN).enumerate() { + let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); if dir_entry.is_end() { // Can quit early break; } else if dir_entry.matches(match_name) { // Found it // Safe, since Block::LEN always fits on a u32 - let start = u32::try_from(start).unwrap(); + let start = (i * OnDiskDirEntry::LEN) as u32; return Ok(dir_entry.get_entry(fat_type, block, start)); } } @@ -800,15 +795,15 @@ impl FatVolume { block_device .read(&mut blocks, block, "read_dir") .map_err(Error::DeviceError)?; - for entry in 0..Block::LEN / OnDiskDirEntry::LEN { - let start = entry * OnDiskDirEntry::LEN; - let end = (entry + 1) * OnDiskDirEntry::LEN; - let dir_entry = OnDiskDirEntry::new(&blocks[0][start..end]); + for (i, dir_entry_bytes) in blocks[0].chunks_exact_mut(OnDiskDirEntry::LEN).enumerate() { + let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); if dir_entry.is_end() { // Can quit early break; } else if dir_entry.matches(match_name) { let mut blocks = blocks; + let start = i * OnDiskDirEntry::LEN; + // set first byte to the 'unused' marker blocks[0].contents[start] = 0xE5; return block_device .write(&blocks, block) From 5e718c2eed7445bb5e79d94c2012d1b801611e76 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Fri, 11 Oct 2024 18:32:11 +0200 Subject: [PATCH 03/62] Remove the reason from the Block Device API. The caller of the API should do their own logging. Also, we didn't have it on write and the asymmetry was displeasing. --- examples/linux/mod.rs | 12 ++------- src/blockdevice.rs | 7 +---- src/fat/mod.rs | 3 +-- src/fat/volume.rs | 59 +++++++++++++++++++++++++++---------------- src/sdcard/mod.rs | 14 ++-------- src/volume_mgr.rs | 26 +++++++++---------- tests/utils/mod.rs | 7 +---- 7 files changed, 56 insertions(+), 72 deletions(-) diff --git a/examples/linux/mod.rs b/examples/linux/mod.rs index 5abb99f5..6eefe23d 100644 --- a/examples/linux/mod.rs +++ b/examples/linux/mod.rs @@ -34,22 +34,14 @@ impl LinuxBlockDevice { impl BlockDevice for LinuxBlockDevice { type Error = std::io::Error; - fn read( - &self, - blocks: &mut [Block], - start_block_idx: BlockIdx, - reason: &str, - ) -> Result<(), Self::Error> { + fn read(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { self.file .borrow_mut() .seek(SeekFrom::Start(start_block_idx.into_bytes()))?; for block in blocks.iter_mut() { self.file.borrow_mut().read_exact(&mut block.contents)?; if self.print_blocks { - println!( - "Read block ({}) {:?}: {:?}", - reason, start_block_idx, &block - ); + println!("Read block {:?}: {:?}", start_block_idx, &block); } } Ok(()) diff --git a/src/blockdevice.rs b/src/blockdevice.rs index 45644572..5460e3d2 100644 --- a/src/blockdevice.rs +++ b/src/blockdevice.rs @@ -43,12 +43,7 @@ pub trait BlockDevice { /// The errors that the `BlockDevice` can return. Must be debug formattable. type Error: core::fmt::Debug; /// Read one or more blocks, starting at the given block index. - fn read( - &self, - blocks: &mut [Block], - start_block_idx: BlockIdx, - reason: &str, - ) -> Result<(), Self::Error>; + fn read(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Self::Error>; /// Write one or more blocks, starting at the given block index. fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error>; /// Determine how many blocks this device can hold. diff --git a/src/fat/mod.rs b/src/fat/mod.rs index 35641cb6..02fe8598 100644 --- a/src/fat/mod.rs +++ b/src/fat/mod.rs @@ -29,7 +29,6 @@ impl BlockCache { &mut self, block_device: &D, block_idx: BlockIdx, - reason: &str, ) -> Result<&Block, Error> where D: BlockDevice, @@ -37,7 +36,7 @@ impl BlockCache { if Some(block_idx) != self.idx { self.idx = Some(block_idx); block_device - .read(core::slice::from_mut(&mut self.block), block_idx, reason) + .read(core::slice::from_mut(&mut self.block), block_idx) .map_err(Error::DeviceError)?; } Ok(&self.block) diff --git a/src/fat/volume.rs b/src/fat/volume.rs index 76a60f4d..86a1f746 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -80,8 +80,9 @@ impl FatVolume { return Ok(()); } let mut blocks = [Block::new()]; + trace!("Reading info sector"); block_device - .read(&mut blocks, fat32_info.info_location, "read_info_sector") + .read(&mut blocks, fat32_info.info_location) .map_err(Error::DeviceError)?; let block = &mut blocks[0]; if let Some(count) = self.free_clusters_count { @@ -90,6 +91,7 @@ impl FatVolume { if let Some(next_free_cluster) = self.next_free_cluster { block[492..496].copy_from_slice(&next_free_cluster.0.to_le_bytes()); } + trace!("Writing info sector"); block_device .write(&blocks, fat32_info.info_location) .map_err(Error::DeviceError)?; @@ -123,8 +125,9 @@ impl FatVolume { let fat_offset = cluster.0 * 2; this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; + trace!("Reading FAT"); block_device - .read(&mut blocks, this_fat_block_num, "read_fat") + .read(&mut blocks, this_fat_block_num) .map_err(Error::DeviceError)?; // See let entry = match new_value { @@ -144,8 +147,9 @@ impl FatVolume { let fat_offset = cluster.0 * 4; this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; + trace!("Reading FAT"); block_device - .read(&mut blocks, this_fat_block_num, "read_fat") + .read(&mut blocks, this_fat_block_num) .map_err(Error::DeviceError)?; let entry = match new_value { ClusterId::INVALID => 0x0FFF_FFF6, @@ -163,6 +167,7 @@ impl FatVolume { ); } } + trace!("Writing FAT"); block_device .write(&blocks, this_fat_block_num) .map_err(Error::DeviceError)?; @@ -187,8 +192,8 @@ impl FatVolume { let fat_offset = cluster.0 * 2; let this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; - let block = - fat_block_cache.read(block_device, this_fat_block_num, "next_cluster")?; + trace!("Reading FAT"); + let block = fat_block_cache.read(block_device, this_fat_block_num)?; let fat_entry = LittleEndian::read_u16(&block[this_fat_ent_offset..=this_fat_ent_offset + 1]); match fat_entry { @@ -210,8 +215,8 @@ impl FatVolume { let fat_offset = cluster.0 * 4; let this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; - let block = - fat_block_cache.read(block_device, this_fat_block_num, "next_cluster")?; + trace!("Reading FAT"); + let block = fat_block_cache.read(block_device, this_fat_block_num)?; let fat_entry = LittleEndian::read_u32(&block[this_fat_ent_offset..=this_fat_ent_offset + 3]) & 0x0FFF_FFFF; @@ -310,8 +315,9 @@ impl FatVolume { let mut blocks = [Block::new()]; while let Some(cluster) = current_cluster { for block in first_dir_block_num.range(dir_size) { + trace!("Reading directory"); block_device - .read(&mut blocks, block, "read_dir") + .read(&mut blocks, block) .map_err(Error::DeviceError)?; for (i, dir_entry_bytes) in blocks[0].chunks_exact_mut(OnDiskDirEntry::LEN).enumerate() @@ -330,6 +336,7 @@ impl FatVolume { ); dir_entry_bytes .copy_from_slice(&entry.serialize(FatType::Fat16)[..]); + trace!("Updating directory"); block_device .write(&blocks, block) .map_err(Error::DeviceError)?; @@ -375,8 +382,9 @@ impl FatVolume { // Loop through the blocks in the cluster for block in first_dir_block_num.range(dir_size) { // Read a block of directory entries + trace!("Reading directory"); block_device - .read(&mut blocks, block, "read_dir") + .read(&mut blocks, block) .map_err(Error::DeviceError)?; // Are any entries in the block we just loaded blank? If so // we can use them. @@ -397,6 +405,7 @@ impl FatVolume { ); dir_entry_bytes .copy_from_slice(&entry.serialize(FatType::Fat32)[..]); + trace!("Updating directory"); block_device .write(&blocks, block) .map_err(Error::DeviceError)?; @@ -481,7 +490,8 @@ impl FatVolume { let mut block_cache = BlockCache::empty(); while let Some(cluster) = current_cluster { for block_idx in first_dir_block_num.range(dir_size) { - let block = block_cache.read(block_device, block_idx, "read_dir")?; + trace!("Reading FAT"); + let block = block_cache.read(block_device, block_idx)?; for (i, dir_entry_bytes) in block.chunks_exact(OnDiskDirEntry::LEN).enumerate() { let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); if dir_entry.is_end() { @@ -532,8 +542,9 @@ impl FatVolume { while let Some(cluster) = current_cluster { let block_idx = self.cluster_to_block(cluster); for block in block_idx.range(BlockCount(u32::from(self.blocks_per_cluster))) { + trace!("Reading FAT"); block_device - .read(&mut blocks, block, "read_dir") + .read(&mut blocks, block) .map_err(Error::DeviceError)?; for (i, dir_entry_bytes) in blocks[0].chunks_exact_mut(OnDiskDirEntry::LEN).enumerate() @@ -658,8 +669,9 @@ impl FatVolume { D: BlockDevice, { let mut blocks = [Block::new()]; + trace!("Reading directory"); block_device - .read(&mut blocks, block, "read_dir") + .read(&mut blocks, block) .map_err(Error::DeviceError)?; for (i, dir_entry_bytes) in blocks[0].chunks_exact_mut(OnDiskDirEntry::LEN).enumerate() { let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); @@ -792,8 +804,9 @@ impl FatVolume { D: BlockDevice, { let mut blocks = [Block::new()]; + trace!("Reading directory"); block_device - .read(&mut blocks, block, "read_dir") + .read(&mut blocks, block) .map_err(Error::DeviceError)?; for (i, dir_entry_bytes) in blocks[0].chunks_exact_mut(OnDiskDirEntry::LEN).enumerate() { let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); @@ -805,6 +818,7 @@ impl FatVolume { let start = i * OnDiskDirEntry::LEN; // set first byte to the 'unused' marker blocks[0].contents[start] = 0xE5; + trace!("Updating directory"); return block_device .write(&blocks, block) .map_err(Error::DeviceError); @@ -842,7 +856,7 @@ impl FatVolume { .map_err(|_| Error::ConversionError)?; trace!("Reading block {:?}", this_fat_block_num); block_device - .read(&mut blocks, this_fat_block_num, "next_cluster") + .read(&mut blocks, this_fat_block_num) .map_err(Error::DeviceError)?; while this_fat_ent_offset <= Block::LEN - 2 { @@ -873,7 +887,7 @@ impl FatVolume { .map_err(|_| Error::ConversionError)?; trace!("Reading block {:?}", this_fat_block_num); block_device - .read(&mut blocks, this_fat_block_num, "next_cluster") + .read(&mut blocks, this_fat_block_num) .map_err(Error::DeviceError)?; while this_fat_ent_offset <= Block::LEN - 4 { @@ -969,6 +983,7 @@ impl FatVolume { let first_block = self.cluster_to_block(new_cluster); let num_blocks = BlockCount(u32::from(self.blocks_per_cluster)); for block in first_block.range(num_blocks) { + trace!("Zeroing cluster"); block_device .write(&blocks, block) .map_err(Error::DeviceError)?; @@ -1041,14 +1056,16 @@ impl FatVolume { FatSpecificInfo::Fat32(_) => FatType::Fat32, }; let mut blocks = [Block::new()]; + trace!("Reading directory for update"); block_device - .read(&mut blocks, entry.entry_block, "read") + .read(&mut blocks, entry.entry_block) .map_err(Error::DeviceError)?; let block = &mut blocks[0]; let start = usize::try_from(entry.entry_offset).map_err(|_| Error::ConversionError)?; block[start..start + 32].copy_from_slice(&entry.serialize(fat_type)[..]); + trace!("Updating directory"); block_device .write(&blocks, entry.entry_block) .map_err(Error::DeviceError)?; @@ -1068,8 +1085,9 @@ where D::Error: core::fmt::Debug, { let mut blocks = [Block::new()]; + trace!("Reading BPB"); block_device - .read(&mut blocks, lba_start, "read_bpb") + .read(&mut blocks, lba_start) .map_err(Error::DeviceError)?; let block = &blocks[0]; let bpb = Bpb::create_from_bytes(block).map_err(Error::FormatError)?; @@ -1112,12 +1130,9 @@ where // Safe to unwrap since this is a Fat32 Type let info_location = bpb.fs_info_block().unwrap(); let mut info_blocks = [Block::new()]; + trace!("Reading info block"); block_device - .read( - &mut info_blocks, - lba_start + info_location, - "read_info_sector", - ) + .read(&mut info_blocks, lba_start + info_location) .map_err(Error::DeviceError)?; let info_block = &info_blocks[0]; let info_sector = diff --git a/src/sdcard/mod.rs b/src/sdcard/mod.rs index 65930438..5c3c0381 100644 --- a/src/sdcard/mod.rs +++ b/src/sdcard/mod.rs @@ -162,19 +162,9 @@ where /// Read one or more blocks, starting at the given block index. /// /// This will trigger card (re-)initialisation. - fn read( - &self, - blocks: &mut [Block], - start_block_idx: BlockIdx, - _reason: &str, - ) -> Result<(), Self::Error> { + fn read(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { let mut inner = self.inner.borrow_mut(); - debug!( - "Read {} blocks @ {} for {}", - blocks.len(), - start_block_idx.0, - _reason - ); + debug!("Read {} blocks @ {}", blocks.len(), start_block_idx.0,); inner.check_init()?; inner.read(blocks, start_block_idx) } diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 98b77814..ebdcf86f 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -2,9 +2,11 @@ //! //! The volume manager handles partitions and open files on a block device. -use byteorder::{ByteOrder, LittleEndian}; use core::convert::TryFrom; +use byteorder::{ByteOrder, LittleEndian}; +use heapless::Vec; + use crate::fat::{self, BlockCache, FatType, OnDiskDirEntry, RESERVED_ENTRIES}; use crate::filesystem::{ @@ -12,11 +14,10 @@ use crate::filesystem::{ SearchIdGenerator, TimeSource, ToShortFileName, MAX_FILE_SIZE, }; use crate::{ - debug, Block, BlockCount, BlockDevice, BlockIdx, Error, RawVolume, ShortFileName, Volume, - VolumeIdx, VolumeInfo, VolumeType, PARTITION_ID_FAT16, PARTITION_ID_FAT16_LBA, + debug, trace, Block, BlockCount, BlockDevice, BlockIdx, Error, RawVolume, ShortFileName, + Volume, VolumeIdx, VolumeInfo, VolumeType, PARTITION_ID_FAT16, PARTITION_ID_FAT16_LBA, PARTITION_ID_FAT32_CHS_LBA, PARTITION_ID_FAT32_LBA, }; -use heapless::Vec; /// Wraps a block device and gives access to the FAT-formatted volumes within /// it. @@ -142,8 +143,9 @@ where let (part_type, lba_start, num_blocks) = { let mut blocks = [Block::new()]; + trace!("Reading partition table"); self.block_device - .read(&mut blocks, BlockIdx(0), "read_mbr") + .read(&mut blocks, BlockIdx(0)) .map_err(Error::DeviceError)?; let block = &blocks[0]; // We only support Master Boot Record (MBR) partitioned cards, not @@ -632,8 +634,9 @@ where )?; self.open_files[file_idx].current_cluster = current_cluster; let mut blocks = [Block::new()]; + trace!("Reading file ID {:?}", file); self.block_device - .read(&mut blocks, block_idx, "read") + .read(&mut blocks, block_idx) .map_err(Error::DeviceError)?; let block = &blocks[0]; let to_copy = block_avail @@ -747,9 +750,9 @@ where let mut blocks = [Block::new()]; let to_copy = core::cmp::min(block_avail, bytes_to_write - written); if block_offset != 0 { - debug!("Partial block write"); + debug!("Reading for partial block write"); self.block_device - .read(&mut blocks, block_idx, "read") + .read(&mut blocks, block_idx) .map_err(Error::DeviceError)?; } let block = &mut blocks[0]; @@ -1157,12 +1160,7 @@ mod tests { type Error = Error; /// Read one or more blocks, starting at the given block index. - fn read( - &self, - blocks: &mut [Block], - start_block_idx: BlockIdx, - _reason: &str, - ) -> Result<(), Self::Error> { + fn read(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { // Actual blocks taken from an SD card, except I've changed the start and length of partition 0. static BLOCKS: [Block; 3] = [ Block { diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs index 22ed2eab..3f89a0ea 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -85,12 +85,7 @@ where { type Error = Error; - fn read( - &self, - blocks: &mut [Block], - start_block_idx: BlockIdx, - _reason: &str, - ) -> Result<(), Self::Error> { + fn read(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Self::Error> { let borrow = self.contents.borrow(); let contents: &[u8] = borrow.as_ref(); let mut block_idx = start_block_idx; From fe05b9febb66715104f590054aff00cc19743aaf Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Fri, 11 Oct 2024 19:58:38 +0200 Subject: [PATCH 04/62] Rename SearchId to Handle. --- src/filesystem/directory.rs | 8 +-- src/filesystem/files.rs | 12 ++-- src/filesystem/{search_id.rs => handles.rs} | 20 +++--- src/filesystem/mod.rs | 4 +- src/lib.rs | 23 ++++--- src/volume_mgr.rs | 68 ++++++++++----------- 6 files changed, 73 insertions(+), 62 deletions(-) rename src/filesystem/{search_id.rs => handles.rs} (66%) diff --git a/src/filesystem/directory.rs b/src/filesystem/directory.rs index b965995f..c7ff0c9f 100644 --- a/src/filesystem/directory.rs +++ b/src/filesystem/directory.rs @@ -2,7 +2,7 @@ use core::convert::TryFrom; use crate::blockdevice::BlockIdx; use crate::fat::{FatType, OnDiskDirEntry}; -use crate::filesystem::{Attributes, ClusterId, SearchId, ShortFileName, Timestamp}; +use crate::filesystem::{Attributes, ClusterId, Handle, ShortFileName, Timestamp}; use crate::{Error, RawVolume, VolumeManager}; use super::ToShortFileName; @@ -47,7 +47,7 @@ pub struct DirEntry { /// and there's a reason we did it this way. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct RawDirectory(pub(crate) SearchId); +pub struct RawDirectory(pub(crate) Handle); impl RawDirectory { /// Convert a raw directory into a droppable [`Directory`] @@ -240,9 +240,9 @@ where #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] #[derive(Debug, Clone)] pub(crate) struct DirectoryInfo { - /// Unique ID for this directory. + /// The handle for this directory. pub(crate) directory_id: RawDirectory, - /// The unique ID for the volume this directory is on + /// The handle for the volume this directory is on pub(crate) volume_id: RawVolume, /// The starting point of the directory listing. pub(crate) cluster: ClusterId, diff --git a/src/filesystem/files.rs b/src/filesystem/files.rs index 1b13e096..c0159cfb 100644 --- a/src/filesystem/files.rs +++ b/src/filesystem/files.rs @@ -1,6 +1,6 @@ use super::TimeSource; use crate::{ - filesystem::{ClusterId, DirEntry, SearchId}, + filesystem::{ClusterId, DirEntry, Handle}, BlockDevice, Error, RawVolume, VolumeManager, }; use embedded_io::{ErrorType, Read, Seek, SeekFrom, Write}; @@ -23,7 +23,7 @@ use embedded_io::{ErrorType, Read, Seek, SeekFrom, Write}; /// reason we did it this way. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct RawFile(pub(crate) SearchId); +pub struct RawFile(pub(crate) Handle); impl RawFile { /// Convert a raw file into a droppable [`File`] @@ -283,10 +283,10 @@ pub enum Mode { #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] #[derive(Debug, Clone)] pub(crate) struct FileInfo { - /// Unique ID for this file - pub(crate) file_id: RawFile, - /// The unique ID for the volume this directory is on - pub(crate) volume_id: RawVolume, + /// Handle for this file + pub(crate) raw_file: RawFile, + /// The handle for the volume this directory is on + pub(crate) raw_volume: RawVolume, /// The last cluster we accessed, and how many bytes that short-cuts us. /// /// This saves us walking from the very start of the FAT chain when we move diff --git a/src/filesystem/search_id.rs b/src/filesystem/handles.rs similarity index 66% rename from src/filesystem/search_id.rs rename to src/filesystem/handles.rs index 30c10182..3fd96083 100644 --- a/src/filesystem/search_id.rs +++ b/src/filesystem/handles.rs @@ -1,11 +1,13 @@ +//! Contains the Handles and the HandleGenerator. + use core::num::Wrapping; #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] -/// Unique ID used to search for files and directories in the open Volume/File/Directory lists -pub struct SearchId(pub(crate) u32); +/// Unique ID used to identify things in the open Volume/File/Directory lists +pub struct Handle(pub(crate) u32); -/// A Search ID generator. +/// A Handle Generator. /// /// This object will always return a different ID. /// @@ -13,23 +15,23 @@ pub struct SearchId(pub(crate) u32); /// files, and if they do, they are unlikely to hold one file open and then /// open/close `2**32 - 1` others. #[derive(Debug)] -pub struct SearchIdGenerator { +pub struct HandleGenerator { next_id: Wrapping, } -impl SearchIdGenerator { - /// Create a new generator of Search IDs. +impl HandleGenerator { + /// Create a new generator of Handles. pub const fn new(offset: u32) -> Self { Self { next_id: Wrapping(offset), } } - /// Generate a new, unique [`SearchId`]. - pub fn get(&mut self) -> SearchId { + /// Generate a new, unique [`Handle`]. + pub fn generate(&mut self) -> Handle { let id = self.next_id; self.next_id += 1; - SearchId(id.0) + Handle(id.0) } } diff --git a/src/filesystem/mod.rs b/src/filesystem/mod.rs index 03baa678..f6cd28a4 100644 --- a/src/filesystem/mod.rs +++ b/src/filesystem/mod.rs @@ -11,7 +11,7 @@ mod cluster; mod directory; mod filename; mod files; -mod search_id; +mod handles; mod timestamp; pub use self::attributes::Attributes; @@ -19,7 +19,7 @@ pub use self::cluster::ClusterId; pub use self::directory::{DirEntry, Directory, RawDirectory}; pub use self::filename::{FilenameError, ShortFileName, ToShortFileName}; pub use self::files::{File, FileError, Mode, RawFile}; -pub use self::search_id::{SearchId, SearchIdGenerator}; +pub use self::handles::{Handle, HandleGenerator}; pub use self::timestamp::{TimeSource, Timestamp}; pub(crate) use self::directory::DirectoryInfo; diff --git a/src/lib.rs b/src/lib.rs index f5105717..a58a07cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,7 +75,7 @@ pub mod sdcard; use core::fmt::Debug; use embedded_io::ErrorKind; -use filesystem::SearchId; +use filesystem::Handle; #[doc(inline)] pub use crate::blockdevice::{Block, BlockCount, BlockDevice, BlockIdx}; @@ -247,10 +247,19 @@ where } } -/// A partition with a filesystem within it. +/// A handle to a volume. +/// +/// A volume is a partition with a filesystem within it. +/// +/// Do NOT drop this object! It doesn't hold a reference to the Volume Manager +/// it was created from and the VolumeManager will think you still have the +/// volume open if you just drop it, and it won't let you open the file again. +/// +/// Instead you must pass it to [`crate::VolumeManager::close_volume`] to close +/// it cleanly. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct RawVolume(SearchId); +pub struct RawVolume(Handle); impl RawVolume { /// Convert a raw volume into a droppable [`Volume`] @@ -272,7 +281,7 @@ impl RawVolume { } } -/// An open volume on disk, which closes on drop. +/// A handle for an open volume on disk, which closes on drop. /// /// In contrast to a `RawVolume`, a `Volume` holds a mutable reference to its /// parent `VolumeManager`, which restricts which operations you can perform. @@ -373,9 +382,9 @@ where #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] #[derive(Debug, PartialEq, Eq)] pub(crate) struct VolumeInfo { - /// Search ID for this volume. - volume_id: RawVolume, - /// TODO: some kind of index + /// Handle for this volume. + raw_volume: RawVolume, + /// Which volume (i.e. partition) we opened on the disk idx: VolumeIdx, /// What kind of volume this is volume_type: VolumeType, diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 98b77814..479cf636 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -8,8 +8,8 @@ use core::convert::TryFrom; use crate::fat::{self, BlockCache, FatType, OnDiskDirEntry, RESERVED_ENTRIES}; use crate::filesystem::{ - Attributes, ClusterId, DirEntry, DirectoryInfo, FileInfo, Mode, RawDirectory, RawFile, - SearchIdGenerator, TimeSource, ToShortFileName, MAX_FILE_SIZE, + Attributes, ClusterId, DirEntry, DirectoryInfo, FileInfo, HandleGenerator, Mode, RawDirectory, + RawFile, TimeSource, ToShortFileName, MAX_FILE_SIZE, }; use crate::{ debug, Block, BlockCount, BlockDevice, BlockIdx, Error, RawVolume, ShortFileName, Volume, @@ -37,7 +37,7 @@ pub struct VolumeManager< { pub(crate) block_device: D, pub(crate) time_source: T, - id_generator: SearchIdGenerator, + id_generator: HandleGenerator, open_volumes: Vec, open_dirs: Vec, open_files: Vec, @@ -86,7 +86,7 @@ where VolumeManager { block_device, time_source, - id_generator: SearchIdGenerator::new(id_offset), + id_generator: HandleGenerator::new(id_offset), open_volumes: Vec::new(), open_dirs: Vec::new(), open_files: Vec::new(), @@ -190,9 +190,9 @@ where | PARTITION_ID_FAT16_LBA | PARTITION_ID_FAT16 => { let volume = fat::parse_volume(&self.block_device, lba_start, num_blocks)?; - let id = RawVolume(self.id_generator.get()); + let id = RawVolume(self.id_generator.generate()); let info = VolumeInfo { - volume_id: id, + raw_volume: id, idx: volume_idx, volume_type: volume, }; @@ -211,7 +211,7 @@ where pub fn open_root_dir(&mut self, volume: RawVolume) -> Result> { // Opening a root directory twice is OK - let directory_id = RawDirectory(self.id_generator.get()); + let directory_id = RawDirectory(self.id_generator.generate()); let dir_info = DirectoryInfo { volume_id: volume, cluster: ClusterId::ROOT_DIR, @@ -251,10 +251,10 @@ where // Open the directory if short_file_name == ShortFileName::this_dir() { // short-cut (root dir doesn't have ".") - let directory_id = RawDirectory(self.id_generator.get()); + let directory_id = RawDirectory(self.id_generator.generate()); let dir_info = DirectoryInfo { directory_id, - volume_id: self.open_volumes[volume_idx].volume_id, + volume_id: self.open_volumes[volume_idx].raw_volume, cluster: parent_dir_info.cluster, }; @@ -281,10 +281,10 @@ where // no cached state and so opening a directory twice is allowable. // Remember this open directory. - let directory_id = RawDirectory(self.id_generator.get()); + let directory_id = RawDirectory(self.id_generator.generate()); let dir_info = DirectoryInfo { directory_id, - volume_id: self.open_volumes[volume_idx].volume_id, + volume_id: self.open_volumes[volume_idx].raw_volume, cluster: dir_entry.cluster, }; @@ -312,7 +312,7 @@ where /// You can't close it if there are any files or directories open on it. pub fn close_volume(&mut self, volume: RawVolume) -> Result<(), Error> { for f in self.open_files.iter() { - if f.volume_id == volume { + if f.raw_volume == volume { return Err(Error::VolumeStillInUse); } } @@ -397,12 +397,12 @@ where } let mode = solve_mode_variant(mode, true); - let file_id = RawFile(self.id_generator.get()); + let file_id = RawFile(self.id_generator.generate()); let file = match mode { Mode::ReadOnly => FileInfo { - file_id, - volume_id: volume, + raw_file: file_id, + raw_volume: volume, current_cluster: (0, dir_entry.cluster), current_offset: 0, mode, @@ -411,8 +411,8 @@ where }, Mode::ReadWriteAppend => { let mut file = FileInfo { - file_id, - volume_id: volume, + raw_file: file_id, + raw_volume: volume, current_cluster: (0, dir_entry.cluster), current_offset: 0, mode, @@ -425,8 +425,8 @@ where } Mode::ReadWriteTruncate => { let mut file = FileInfo { - file_id, - volume_id: volume, + raw_file: file_id, + raw_volume: volume, current_cluster: (0, dir_entry.cluster), current_offset: 0, mode, @@ -510,7 +510,7 @@ where // Check if it's open already if let Some(dir_entry) = &dir_entry { - if self.file_is_open(volume_info.volume_id, dir_entry) { + if self.file_is_open(volume_info.raw_volume, dir_entry) { return Err(Error::FileAlreadyOpen); } } @@ -534,11 +534,11 @@ where )?, }; - let file_id = RawFile(self.id_generator.get()); + let file_id = RawFile(self.id_generator.generate()); let file = FileInfo { - file_id, - volume_id, + raw_file: file_id, + raw_volume: volume_id, current_cluster: (0, entry.cluster), current_offset: 0, mode, @@ -603,7 +603,7 @@ where /// Returns `true` if it's open, `false`, otherwise. fn file_is_open(&self, volume: RawVolume, dir_entry: &DirEntry) -> bool { for f in self.open_files.iter() { - if f.volume_id == volume + if f.raw_volume == volume && f.entry.entry_block == dir_entry.entry_block && f.entry.entry_offset == dir_entry.entry_offset { @@ -616,7 +616,7 @@ where /// Read from an open file. pub fn read(&mut self, file: RawFile, buffer: &mut [u8]) -> Result> { let file_idx = self.get_file_by_id(file)?; - let volume_idx = self.get_volume_by_id(self.open_files[file_idx].volume_id)?; + let volume_idx = self.get_volume_by_id(self.open_files[file_idx].raw_volume)?; // Calculate which file block the current offset lies within // While there is more to read, read the block and copy in to the buffer. // If we need to find the next cluster, walk the FAT. @@ -662,7 +662,7 @@ where // Clone this so we can touch our other structures. Need to ensure we // write it back at the end. let file_idx = self.get_file_by_id(file)?; - let volume_idx = self.get_volume_by_id(self.open_files[file_idx].volume_id)?; + let volume_idx = self.get_volume_by_id(self.open_files[file_idx].raw_volume)?; if self.open_files[file_idx].mode == Mode::ReadOnly { return Err(Error::ReadOnly); @@ -685,7 +685,7 @@ where } // Clone this so we can touch our other structures. - let volume_idx = self.get_volume_by_id(self.open_files[file_idx].volume_id)?; + let volume_idx = self.get_volume_by_id(self.open_files[file_idx].raw_volume)?; if (self.open_files[file_idx].current_cluster.1) < self.open_files[file_idx].entry.cluster { debug!("Rewinding to start"); @@ -791,11 +791,11 @@ where let file_info = self .open_files .iter() - .find(|info| info.file_id == file) + .find(|info| info.raw_file == file) .ok_or(Error::BadHandle)?; if file_info.dirty { - let volume_idx = self.get_volume_by_id(file_info.volume_id)?; + let volume_idx = self.get_volume_by_id(file_info.raw_volume)?; match self.open_volumes[volume_idx].volume_type { VolumeType::Fat(ref mut fat) => { debug!("Updating FAT info sector"); @@ -1021,7 +1021,7 @@ where fn get_volume_by_id(&self, volume: RawVolume) -> Result> { for (idx, v) in self.open_volumes.iter().enumerate() { - if v.volume_id == volume { + if v.raw_volume == volume { return Ok(idx); } } @@ -1039,7 +1039,7 @@ where fn get_file_by_id(&self, file: RawFile) -> Result> { for (idx, f) in self.open_files.iter().enumerate() { - if f.file_id == file { + if f.raw_file == file { return Ok(idx); } } @@ -1127,7 +1127,7 @@ fn solve_mode_variant(mode: Mode, dir_entry_is_some: bool) -> Mode { #[cfg(test)] mod tests { use super::*; - use crate::filesystem::SearchId; + use crate::filesystem::Handle; use crate::Timestamp; struct DummyBlockDevice; @@ -1371,12 +1371,12 @@ mod tests { VolumeManager::new_with_limits(DummyBlockDevice, Clock, 0xAA00_0000); let v = c.open_raw_volume(VolumeIdx(0)).unwrap(); - let expected_id = RawVolume(SearchId(0xAA00_0000)); + let expected_id = RawVolume(Handle(0xAA00_0000)); assert_eq!(v, expected_id); assert_eq!( &c.open_volumes[0], &VolumeInfo { - volume_id: expected_id, + raw_volume: expected_id, idx: VolumeIdx(0), volume_type: VolumeType::Fat(crate::FatVolume { lba_start: BlockIdx(1), From cc62a8f6b5e7efc4b4e4aa9118dac15ea688528b Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Fri, 11 Oct 2024 20:06:01 +0200 Subject: [PATCH 05/62] Improve debug print for Handle. It's obvious it's a handle from context, so just print the inner value. And print it as hex because it might be large. --- src/filesystem/handles.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/filesystem/handles.rs b/src/filesystem/handles.rs index 3fd96083..dd379038 100644 --- a/src/filesystem/handles.rs +++ b/src/filesystem/handles.rs @@ -2,11 +2,17 @@ use core::num::Wrapping; -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] /// Unique ID used to identify things in the open Volume/File/Directory lists pub struct Handle(pub(crate) u32); +impl core::fmt::Debug for Handle { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#08x}", self.0) + } +} + /// A Handle Generator. /// /// This object will always return a different ID. From db06494c681e66c93ddba1a3acc42d06d6ff52f0 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sun, 6 Oct 2024 18:49:34 +0100 Subject: [PATCH 06/62] Minor tidy up match on enums, don't use if --- src/fat/bpb.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/fat/bpb.rs b/src/fat/bpb.rs index f06f23eb..d925d942 100644 --- a/src/fat/bpb.rs +++ b/src/fat/bpb.rs @@ -86,10 +86,9 @@ impl<'a> Bpb<'a> { /// Get the Volume Label string for this volume pub fn volume_label(&self) -> &[u8] { - if self.fat_type != FatType::Fat32 { - &self.data[43..=53] - } else { - &self.data[71..=81] + match self.fat_type { + FatType::Fat16 => &self.data[43..=53], + FatType::Fat32 => &self.data[71..=81], } } @@ -98,10 +97,9 @@ impl<'a> Bpb<'a> { /// On a FAT32 volume, return the free block count from the Info Block. On /// a FAT16 volume, returns None. pub fn fs_info_block(&self) -> Option { - if self.fat_type != FatType::Fat32 { - None - } else { - Some(BlockCount(u32::from(self.fs_info()))) + match self.fat_type { + FatType::Fat16 => None, + FatType::Fat32 => Some(BlockCount(u32::from(self.fs_info()))), } } From a340b4d76361e3c3429064b424894ecd0736e66d Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sun, 6 Oct 2024 20:29:34 +0100 Subject: [PATCH 07/62] Better support for volume labels. They can contain spaces! But not full-stops. --- examples/shell.rs | 23 ++++-- src/fat/bpb.rs | 8 +- src/fat/mod.rs | 8 +- src/fat/volume.rs | 145 ++++++++++++++++++++++++++++++---- src/filesystem/filename.rs | 155 ++++++++++++++----------------------- src/lib.rs | 2 +- src/volume_mgr.rs | 29 ++++++- 7 files changed, 246 insertions(+), 124 deletions(-) diff --git a/examples/shell.rs b/examples/shell.rs index 0e41ce1c..91b5c499 100644 --- a/examples/shell.rs +++ b/examples/shell.rs @@ -230,10 +230,17 @@ impl Context { let dir = self.resolve_existing_directory(path)?; let mut dir = dir.to_directory(&mut self.volume_mgr); dir.iterate_dir(|entry| { - println!( - "{:12} {:9} {} {} {:08X?} {:?}", - entry.name, entry.size, entry.ctime, entry.mtime, entry.cluster, entry.attributes - ); + if !entry.attributes.is_volume() && !entry.attributes.is_lfn() { + println!( + "{:12} {:9} {} {} {:08X?} {:?}", + entry.name, + entry.size, + entry.ctime, + entry.mtime, + entry.cluster, + entry.attributes + ); + } })?; Ok(()) } @@ -310,6 +317,8 @@ impl Context { for fragment in full_path.iterate_components().filter(|s| !s.is_empty()) { if fragment == ".." { s.path.pop(); + } else if fragment == "." { + // do nothing } else { s.path.push(fragment.to_owned()); } @@ -533,7 +542,11 @@ fn main() -> Result<(), Error> { for volume_no in 0..4 { match ctx.volume_mgr.open_raw_volume(VolumeIdx(volume_no)) { Ok(volume) => { - println!("Volume # {}: found", Context::volume_to_letter(volume_no)); + println!( + "Volume # {}: found, label: {:?}", + Context::volume_to_letter(volume_no), + ctx.volume_mgr.get_root_volume_label(volume)? + ); match ctx.volume_mgr.open_root_dir(volume) { Ok(root_dir) => { ctx.volumes[volume_no] = Some(VolumeState { diff --git a/src/fat/bpb.rs b/src/fat/bpb.rs index d925d942..c7e83b62 100644 --- a/src/fat/bpb.rs +++ b/src/fat/bpb.rs @@ -85,11 +85,13 @@ impl<'a> Bpb<'a> { // FAT16/FAT32 functions /// Get the Volume Label string for this volume - pub fn volume_label(&self) -> &[u8] { + pub fn volume_label(&self) -> [u8; 11] { + let mut result = [0u8; 11]; match self.fat_type { - FatType::Fat16 => &self.data[43..=53], - FatType::Fat32 => &self.data[71..=81], + FatType::Fat16 => result.copy_from_slice(&self.data[43..=53]), + FatType::Fat32 => result.copy_from_slice(&self.data[71..=81]), } + result } // FAT32 only functions diff --git a/src/fat/mod.rs b/src/fat/mod.rs index 35641cb6..5784bdc4 100644 --- a/src/fat/mod.rs +++ b/src/fat/mod.rs @@ -139,7 +139,11 @@ mod test { "#; let results = [ Expected::Short(DirEntry { - name: ShortFileName::create_from_str_mixed_case("boot").unwrap(), + name: unsafe { + VolumeName::create_from_str("boot") + .unwrap() + .to_short_filename() + }, mtime: Timestamp::from_calendar(2015, 11, 21, 19, 35, 18).unwrap(), ctime: Timestamp::from_calendar(2015, 11, 21, 19, 35, 18).unwrap(), attributes: Attributes::create_from_fat(Attributes::VOLUME), @@ -349,7 +353,7 @@ mod test { assert_eq!(bpb.fat_size16(), 32); assert_eq!(bpb.total_blocks32(), 122_880); assert_eq!(bpb.footer(), 0xAA55); - assert_eq!(bpb.volume_label(), b"boot "); + assert_eq!(bpb.volume_label(), *b"boot "); assert_eq!(bpb.fat_size(), 32); assert_eq!(bpb.total_blocks(), 122_880); assert_eq!(bpb.fat_type, FatType::Fat16); diff --git a/src/fat/volume.rs b/src/fat/volume.rs index 8f44c6d9..ebab5b7c 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -6,6 +6,7 @@ use crate::{ Bpb, Fat16Info, Fat32Info, FatSpecificInfo, FatType, InfoSector, OnDiskDirEntry, RESERVED_ENTRIES, }, + filesystem::FilenameError, trace, warn, Attributes, Block, BlockCount, BlockDevice, BlockIdx, ClusterId, DirEntry, DirectoryInfo, Error, ShortFileName, TimeSource, VolumeType, }; @@ -14,26 +15,121 @@ use core::convert::TryFrom; use super::BlockCache; -/// The name given to a particular FAT formatted volume. +/// An MS-DOS 11 character volume label. +/// +/// ISO-8859-1 encoding is assumed. Trailing spaces are trimmed. Reserved +/// characters are not allowed. There is no file extension, unlike with a +/// filename. +/// +/// Volume labels can be found in the BIOS Parameter Block, and in a root +/// directory entry with the 'Volume Label' bit set. Both places should have the +/// same contents, but they can get out of sync. +/// +/// MS-DOS FDISK would show you the one in the BPB, but DIR would show you the +/// one in the root directory. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] -#[derive(Clone, PartialEq, Eq)] +#[derive(PartialEq, Eq, Clone)] pub struct VolumeName { - data: [u8; 11], + pub(crate) contents: [u8; Self::TOTAL_LEN], } impl VolumeName { - /// Create a new VolumeName - pub fn new(data: [u8; 11]) -> VolumeName { - VolumeName { data } + const TOTAL_LEN: usize = 11; + + /// Get name + pub fn name(&self) -> &[u8] { + self.contents.trim_ascii_end() + } + + /// Create a new MS-DOS volume label. + pub fn create_from_str(name: &str) -> Result { + let mut sfn = VolumeName { + contents: [b' '; Self::TOTAL_LEN], + }; + + let mut idx = 0; + for ch in name.chars() { + match ch { + // Microsoft say these are the invalid characters + '\u{0000}'..='\u{001F}' + | '"' + | '*' + | '+' + | ',' + | '/' + | ':' + | ';' + | '<' + | '=' + | '>' + | '?' + | '[' + | '\\' + | ']' + | '.' + | '|' => { + return Err(FilenameError::InvalidCharacter); + } + x if x > '\u{00FF}' => { + // We only handle ISO-8859-1 which is Unicode Code Points + // \U+0000 to \U+00FF. This is above that. + return Err(FilenameError::InvalidCharacter); + } + _ => { + let b = ch as u8; + if idx < Self::TOTAL_LEN { + sfn.contents[idx] = b; + } else { + return Err(FilenameError::NameTooLong); + } + idx += 1; + } + } + } + if idx == 0 { + return Err(FilenameError::FilenameEmpty); + } + Ok(sfn) + } + + /// Convert to a Short File Name + /// + /// # Safety + /// + /// Volume Labels can contain things that Short File Names cannot, so only + /// do this conversion if you are creating the name of a directory entry + /// with the 'Volume Label' attribute. + pub unsafe fn to_short_filename(self) -> ShortFileName { + ShortFileName { + contents: self.contents, + } } } -impl core::fmt::Debug for VolumeName { - fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result { - match core::str::from_utf8(&self.data) { - Ok(s) => write!(fmt, "{:?}", s), - Err(_e) => write!(fmt, "{:?}", &self.data), +impl core::fmt::Display for VolumeName { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + let mut printed = 0; + for &c in self.name().iter() { + // converting a byte to a codepoint means you are assuming + // ISO-8859-1 encoding, because that's how Unicode was designed. + write!(f, "{}", c as char)?; + printed += 1; + } + if let Some(mut width) = f.width() { + if width > printed { + width -= printed; + for _ in 0..width { + write!(f, "{}", f.fill())?; + } + } } + Ok(()) + } +} + +impl core::fmt::Debug for VolumeName { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "VolumeName(\"{}\")", self) } } @@ -1091,10 +1187,12 @@ where let first_root_dir_block = fat_start + BlockCount(u32::from(bpb.num_fats()) * bpb.fat_size()); let first_data_block = first_root_dir_block + BlockCount(root_dir_blocks); - let mut volume = FatVolume { + let volume = FatVolume { lba_start, num_blocks, - name: VolumeName { data: [0u8; 11] }, + name: VolumeName { + contents: bpb.volume_label(), + }, blocks_per_cluster: bpb.blocks_per_cluster(), first_data_block: (first_data_block), fat_start: BlockCount(u32::from(bpb.reserved_block_count())), @@ -1106,7 +1204,6 @@ where first_root_dir_block, }), }; - volume.name.data[..].copy_from_slice(bpb.volume_label()); Ok(VolumeType::Fat(volume)) } FatType::Fat32 => { @@ -1128,10 +1225,12 @@ where let info_sector = InfoSector::create_from_bytes(info_block).map_err(Error::FormatError)?; - let mut volume = FatVolume { + let volume = FatVolume { lba_start, num_blocks, - name: VolumeName { data: [0u8; 11] }, + name: VolumeName { + contents: bpb.volume_label(), + }, blocks_per_cluster: bpb.blocks_per_cluster(), first_data_block: BlockCount(first_data_block), fat_start: BlockCount(u32::from(bpb.reserved_block_count())), @@ -1143,12 +1242,24 @@ where first_root_dir_cluster: ClusterId(bpb.first_root_dir_cluster()), }), }; - volume.name.data[..].copy_from_slice(bpb.volume_label()); Ok(VolumeType::Fat(volume)) } } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn volume_name() { + let sfn = VolumeName { + contents: *b"Hello \xA399 ", + }; + assert_eq!(sfn, VolumeName::create_from_str("Hello £99").unwrap()) + } +} + // **************************************************************************** // // End Of File diff --git a/src/filesystem/filename.rs b/src/filesystem/filename.rs index 4cb763f3..a8b34e4b 100644 --- a/src/filesystem/filename.rs +++ b/src/filesystem/filename.rs @@ -1,5 +1,7 @@ //! Filename related types +use crate::fat::VolumeName; + /// Various filename related errors that can occur. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] #[derive(Debug, Clone)] @@ -40,17 +42,19 @@ impl ToShortFileName for &str { } } -/// An MS-DOS 8.3 filename. 7-bit ASCII only. All lower-case is converted to -/// upper-case by default. +/// An MS-DOS 8.3 filename. +/// +/// ISO-8859-1 encoding is assumed. All lower-case is converted to upper-case by +/// default. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] #[derive(PartialEq, Eq, Clone)] pub struct ShortFileName { - pub(crate) contents: [u8; 11], + pub(crate) contents: [u8; Self::TOTAL_LEN], } impl ShortFileName { - const FILENAME_BASE_MAX_LEN: usize = 8; - const FILENAME_MAX_LEN: usize = 11; + const BASE_LEN: usize = 8; + const TOTAL_LEN: usize = 11; /// Get a short file name containing "..", which means "parent directory". pub const fn parent_dir() -> Self { @@ -68,22 +72,24 @@ impl ShortFileName { /// Get base name (without extension) of the file. pub fn base_name(&self) -> &[u8] { - Self::bytes_before_space(&self.contents[..Self::FILENAME_BASE_MAX_LEN]) + Self::bytes_before_space(&self.contents[..Self::BASE_LEN]) } /// Get extension of the file (without base name). pub fn extension(&self) -> &[u8] { - Self::bytes_before_space(&self.contents[Self::FILENAME_BASE_MAX_LEN..]) + Self::bytes_before_space(&self.contents[Self::BASE_LEN..]) } fn bytes_before_space(bytes: &[u8]) -> &[u8] { - bytes.split(|b| *b == b' ').next().unwrap_or(&bytes[0..0]) + bytes.split(|b| *b == b' ').next().unwrap_or(&[]) } /// Create a new MS-DOS 8.3 space-padded file name as stored in the directory entry. + /// + /// The output uses ISO-8859-1 encoding. pub fn create_from_str(name: &str) -> Result { let mut sfn = ShortFileName { - contents: [b' '; Self::FILENAME_MAX_LEN], + contents: [b' '; Self::TOTAL_LEN], }; // Special case `..`, which means "parent directory". @@ -98,47 +104,52 @@ impl ShortFileName { let mut idx = 0; let mut seen_dot = false; - for ch in name.bytes() { + for ch in name.chars() { match ch { // Microsoft say these are the invalid characters - 0x00..=0x1F - | 0x20 - | 0x22 - | 0x2A - | 0x2B - | 0x2C - | 0x2F - | 0x3A - | 0x3B - | 0x3C - | 0x3D - | 0x3E - | 0x3F - | 0x5B - | 0x5C - | 0x5D - | 0x7C => { + '\u{0000}'..='\u{001F}' + | '"' + | '*' + | '+' + | ',' + | '/' + | ':' + | ';' + | '<' + | '=' + | '>' + | '?' + | '[' + | '\\' + | ']' + | ' ' + | '|' => { + return Err(FilenameError::InvalidCharacter); + } + x if x > '\u{00FF}' => { + // We only handle ISO-8859-1 which is Unicode Code Points + // \U+0000 to \U+00FF. This is above that. return Err(FilenameError::InvalidCharacter); } - // Denotes the start of the file extension - b'.' => { - if (1..=Self::FILENAME_BASE_MAX_LEN).contains(&idx) { - idx = Self::FILENAME_BASE_MAX_LEN; + '.' => { + // Denotes the start of the file extension + if (1..=Self::BASE_LEN).contains(&idx) { + idx = Self::BASE_LEN; seen_dot = true; } else { return Err(FilenameError::MisplacedPeriod); } } _ => { - let ch = ch.to_ascii_uppercase(); + let b = ch.to_ascii_uppercase() as u8; if seen_dot { - if (Self::FILENAME_BASE_MAX_LEN..Self::FILENAME_MAX_LEN).contains(&idx) { - sfn.contents[idx] = ch; + if (Self::BASE_LEN..Self::TOTAL_LEN).contains(&idx) { + sfn.contents[idx] = b; } else { return Err(FilenameError::NameTooLong); } - } else if idx < Self::FILENAME_BASE_MAX_LEN { - sfn.contents[idx] = ch; + } else if idx < Self::BASE_LEN { + sfn.contents[idx] = b; } else { return Err(FilenameError::NameTooLong); } @@ -152,65 +163,17 @@ impl ShortFileName { Ok(sfn) } - /// Create a new MS-DOS 8.3 space-padded file name as stored in the directory entry. - /// Use this for volume labels with mixed case. - pub fn create_from_str_mixed_case(name: &str) -> Result { - let mut sfn = ShortFileName { - contents: [b' '; Self::FILENAME_MAX_LEN], - }; - let mut idx = 0; - let mut seen_dot = false; - for ch in name.bytes() { - match ch { - // Microsoft say these are the invalid characters - 0x00..=0x1F - | 0x20 - | 0x22 - | 0x2A - | 0x2B - | 0x2C - | 0x2F - | 0x3A - | 0x3B - | 0x3C - | 0x3D - | 0x3E - | 0x3F - | 0x5B - | 0x5C - | 0x5D - | 0x7C => { - return Err(FilenameError::InvalidCharacter); - } - // Denotes the start of the file extension - b'.' => { - if (1..=Self::FILENAME_BASE_MAX_LEN).contains(&idx) { - idx = Self::FILENAME_BASE_MAX_LEN; - seen_dot = true; - } else { - return Err(FilenameError::MisplacedPeriod); - } - } - _ => { - if seen_dot { - if (Self::FILENAME_BASE_MAX_LEN..Self::FILENAME_MAX_LEN).contains(&idx) { - sfn.contents[idx] = ch; - } else { - return Err(FilenameError::NameTooLong); - } - } else if idx < Self::FILENAME_BASE_MAX_LEN { - sfn.contents[idx] = ch; - } else { - return Err(FilenameError::NameTooLong); - } - idx += 1; - } - } + /// Convert a Short File Name to a Volume Label. + /// + /// # Safety + /// + /// Volume Labels can contain things that Short File Names cannot, so only + /// do this conversion if you have the name of a directory entry with the + /// 'Volume Label' attribute. + pub unsafe fn to_volume_label(self) -> VolumeName { + VolumeName { + contents: self.contents, } - if idx == 0 { - return Err(FilenameError::FilenameEmpty); - } - Ok(sfn) } } @@ -219,10 +182,12 @@ impl core::fmt::Display for ShortFileName { let mut printed = 0; for (i, &c) in self.contents.iter().enumerate() { if c != b' ' { - if i == Self::FILENAME_BASE_MAX_LEN { + if i == Self::BASE_LEN { write!(f, ".")?; printed += 1; } + // converting a byte to a codepoint means you are assuming + // ISO-8859-1 encoding, because that's how Unicode was designed. write!(f, "{}", c as char)?; printed += 1; } diff --git a/src/lib.rs b/src/lib.rs index a58a07cd..05938ecf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -81,7 +81,7 @@ use filesystem::Handle; pub use crate::blockdevice::{Block, BlockCount, BlockDevice, BlockIdx}; #[doc(inline)] -pub use crate::fat::FatVolume; +pub use crate::fat::{FatVolume, VolumeName}; #[doc(inline)] pub use crate::filesystem::{ diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 479cf636..3a5d04a9 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -598,6 +598,33 @@ where Ok(()) } + /// Search the root directory for a volume label + pub fn get_root_volume_label( + &mut self, + volume: RawVolume, + ) -> Result, Error> { + let directory = self.open_root_dir(volume)?; + // this can't fail - we literally just opened it + let inner = || -> Result, Error> { + let directory_idx = self.get_dir_by_id(directory).expect("Dir ID error"); + let volume_idx = self.get_volume_by_id(self.open_dirs[directory_idx].volume_id)?; + let mut maybe_volume_name = None; + match &self.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + fat.iterate_dir(&self.block_device, &self.open_dirs[directory_idx], |de| { + if de.attributes == Attributes::create_from_fat(Attributes::VOLUME) { + maybe_volume_name = Some(unsafe { de.name.clone().to_volume_label() }) + } + })?; + } + } + Ok(maybe_volume_name) + }; + let result = inner(); + self.close_dir(directory)?; + result + } + /// Check if a file is open /// /// Returns `true` if it's open, `false`, otherwise. @@ -1384,7 +1411,7 @@ mod tests { blocks_per_cluster: 8, first_data_block: BlockCount(15136), fat_start: BlockCount(32), - name: fat::VolumeName::new(*b"Pictures "), + name: fat::VolumeName::create_from_str("Pictures").unwrap(), free_clusters_count: None, next_free_cluster: None, cluster_count: 965_788, From 77b45be92c13c749f14a9cb9bd046d5dfd81d27a Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sun, 6 Oct 2024 20:33:10 +0100 Subject: [PATCH 08/62] Simplify some casts Avoid try_from ... unwrap. We can just use 'as', which is easier to understand. --- src/fat/volume.rs | 12 ++++++------ src/filesystem/directory.rs | 10 ++-------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/fat/volume.rs b/src/fat/volume.rs index ebab5b7c..c453adb9 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -587,8 +587,8 @@ impl FatVolume { // Can quit early return Ok(()); } else if dir_entry.is_valid() && !dir_entry.is_lfn() { - // Safe, since Block::LEN always fits on a u32 - let start = u32::try_from(start).unwrap(); + // Block::LEN always fits on a u32 + let start = start as u32; let entry = dir_entry.get_entry(FatType::Fat16, block_idx, start); func(&entry); } @@ -642,8 +642,8 @@ impl FatVolume { // Can quit early return Ok(()); } else if dir_entry.is_valid() && !dir_entry.is_lfn() { - // Safe, since Block::LEN always fits on a u32 - let start = u32::try_from(start).unwrap(); + // Block::LEN always fits on a u32 + let start = start as u32; let entry = dir_entry.get_entry(FatType::Fat32, block, start); func(&entry); } @@ -769,8 +769,8 @@ impl FatVolume { break; } else if dir_entry.matches(match_name) { // Found it - // Safe, since Block::LEN always fits on a u32 - let start = u32::try_from(start).unwrap(); + // Block::LEN always fits on a u32 + let start = start as u32; return Ok(dir_entry.get_entry(fat_type, block, start)); } } diff --git a/src/filesystem/directory.rs b/src/filesystem/directory.rs index c7ff0c9f..efd6e5e8 100644 --- a/src/filesystem/directory.rs +++ b/src/filesystem/directory.rs @@ -1,5 +1,3 @@ -use core::convert::TryFrom; - use crate::blockdevice::BlockIdx; use crate::fat::{FatType, OnDiskDirEntry}; use crate::filesystem::{Attributes, ClusterId, Handle, ShortFileName, Timestamp}; @@ -262,16 +260,12 @@ impl DirEntry { [0u8; 2] } else { // Safe due to the AND operation - u16::try_from((cluster_number >> 16) & 0x0000_FFFF) - .unwrap() - .to_le_bytes() + (((cluster_number >> 16) & 0x0000_FFFF) as u16).to_le_bytes() }; data[20..22].copy_from_slice(&cluster_hi[..]); data[22..26].copy_from_slice(&self.mtime.serialize_to_fat()[..]); // Safe due to the AND operation - let cluster_lo = u16::try_from(cluster_number & 0x0000_FFFF) - .unwrap() - .to_le_bytes(); + let cluster_lo = ((cluster_number & 0x0000_FFFF) as u16).to_le_bytes(); data[26..28].copy_from_slice(&cluster_lo[..]); data[28..32].copy_from_slice(&self.size.to_le_bytes()[..]); data From 3548e145a856b5b13b35d744b80305399ebdac79 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Fri, 11 Oct 2024 14:33:10 +0200 Subject: [PATCH 09/62] Add extra test opening volumes. --- tests/volume.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/volume.rs b/tests/volume.rs index 633a8d25..6ef05dff 100644 --- a/tests/volume.rs +++ b/tests/volume.rs @@ -58,6 +58,12 @@ fn open_all_volumes() { Err(embedded_sdmmc::Error::FormatError(_e)) )); + // This isn't a valid volume + assert!(matches!( + volume_mgr.open_raw_volume(embedded_sdmmc::VolumeIdx(3)), + Err(embedded_sdmmc::Error::FormatError(_e)) + )); + // This isn't a valid volume assert!(matches!( volume_mgr.open_raw_volume(embedded_sdmmc::VolumeIdx(9)), From be37965c37ae4bd7a0805d12f98ab423a1a8af4d Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sun, 6 Oct 2024 16:54:26 +0100 Subject: [PATCH 10/62] Use a RefCell to provide interior mutability for VolumeManager. --- README.md | 10 +- examples/append_file.rs | 8 +- examples/big_dir.rs | 8 +- examples/create_file.rs | 8 +- examples/delete_file.rs | 6 +- examples/list_dir.rs | 6 +- examples/read_file.rs | 11 +- examples/readme_test.rs | 8 +- examples/shell.rs | 14 +- src/filesystem/directory.rs | 22 +- src/filesystem/files.rs | 20 +- src/filesystem/mod.rs | 2 +- src/lib.rs | 18 +- src/volume_mgr.rs | 669 +++++++++++++++++++----------------- tests/directories.rs | 16 +- tests/open_files.rs | 10 +- tests/read_file.rs | 16 +- tests/volume.rs | 4 +- tests/write_file.rs | 4 +- 19 files changed, 453 insertions(+), 407 deletions(-) diff --git a/README.md b/README.md index 41985d25..eace6f8c 100644 --- a/README.md +++ b/README.md @@ -17,16 +17,16 @@ let sdcard = embedded_sdmmc::SdCard::new(sdmmc_spi, delay); println!("Card size is {} bytes", sdcard.num_bytes()?); // Now let's look for volumes (also known as partitions) on our block device. // To do this we need a Volume Manager. It will take ownership of the block device. -let mut volume_mgr = embedded_sdmmc::VolumeManager::new(sdcard, time_source); +let volume_mgr = embedded_sdmmc::VolumeManager::new(sdcard, time_source); // Try and access Volume 0 (i.e. the first partition). // The volume object holds information about the filesystem on that volume. -let mut volume0 = volume_mgr.open_volume(embedded_sdmmc::VolumeIdx(0))?; +let volume0 = volume_mgr.open_volume(embedded_sdmmc::VolumeIdx(0))?; println!("Volume 0: {:?}", volume0); // Open the root directory (mutably borrows from the volume). -let mut root_dir = volume0.open_root_dir()?; +let root_dir = volume0.open_root_dir()?; // Open a file called "MY_FILE.TXT" in the root directory // This mutably borrows the directory. -let mut my_file = root_dir.open_file_in_dir("MY_FILE.TXT", embedded_sdmmc::Mode::ReadOnly)?; +let my_file = root_dir.open_file_in_dir("MY_FILE.TXT", embedded_sdmmc::Mode::ReadOnly)?; // Print the contents of the file, assuming it's in ISO-8859-1 encoding while !my_file.is_eof() { let mut buffer = [0u8; 32]; @@ -43,7 +43,7 @@ By default the `VolumeManager` will initialize with a maximum number of `4` open ```rust // Create a volume manager with a maximum of 6 open directories, 12 open files, and 4 volumes (or partitions) -let mut cont: VolumeManager<_, _, 6, 12, 4> = VolumeManager::new_with_limits(block, time_source); +let cont: VolumeManager<_, _, 6, 12, 4> = VolumeManager::new_with_limits(block, time_source); ``` ## Supported features diff --git a/examples/append_file.rs b/examples/append_file.rs index f5a3bc43..bbda0098 100644 --- a/examples/append_file.rs +++ b/examples/append_file.rs @@ -30,12 +30,12 @@ fn main() -> Result<(), embedded_sdmmc::Error> { let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; - let mut volume_mgr: VolumeManager = + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); - let mut volume = volume_mgr.open_volume(VolumeIdx(0))?; - let mut root_dir = volume.open_root_dir()?; + let volume = volume_mgr.open_volume(VolumeIdx(0))?; + let root_dir = volume.open_root_dir()?; println!("\nCreating file {}...", FILE_TO_APPEND); - let mut f = root_dir.open_file_in_dir(FILE_TO_APPEND, Mode::ReadWriteAppend)?; + let f = root_dir.open_file_in_dir(FILE_TO_APPEND, Mode::ReadWriteAppend)?; f.write(b"\r\n\r\nThis has been added to your file.\r\n")?; Ok(()) } diff --git a/examples/big_dir.rs b/examples/big_dir.rs index b355c3c4..98ad90a3 100644 --- a/examples/big_dir.rs +++ b/examples/big_dir.rs @@ -11,20 +11,20 @@ fn main() -> Result<(), embedded_sdmmc::Error> { let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; - let mut volume_mgr: VolumeManager = + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); - let mut volume = volume_mgr + let volume = volume_mgr .open_volume(embedded_sdmmc::VolumeIdx(1)) .unwrap(); println!("Volume: {:?}", volume); - let mut root_dir = volume.open_root_dir().unwrap(); + let root_dir = volume.open_root_dir().unwrap(); let mut file_num = 0; loop { file_num += 1; let file_name = format!("{}.da", file_num); println!("opening file {file_name} for writing"); - let mut file = root_dir + let file = root_dir .open_file_in_dir( file_name.as_str(), embedded_sdmmc::Mode::ReadWriteCreateOrTruncate, diff --git a/examples/create_file.rs b/examples/create_file.rs index 81263ceb..fa96d075 100644 --- a/examples/create_file.rs +++ b/examples/create_file.rs @@ -30,15 +30,15 @@ fn main() -> Result<(), embedded_sdmmc::Error> { let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; - let mut volume_mgr: VolumeManager = + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); - let mut volume = volume_mgr.open_volume(VolumeIdx(0))?; - let mut root_dir = volume.open_root_dir()?; + let volume = volume_mgr.open_volume(VolumeIdx(0))?; + let root_dir = volume.open_root_dir()?; println!("\nCreating file {}...", FILE_TO_CREATE); // This will panic if the file already exists: use ReadWriteCreateOrAppend // or ReadWriteCreateOrTruncate instead if you want to modify an existing // file. - let mut f = root_dir.open_file_in_dir(FILE_TO_CREATE, Mode::ReadWriteCreate)?; + let f = root_dir.open_file_in_dir(FILE_TO_CREATE, Mode::ReadWriteCreate)?; f.write(b"Hello, this is a new file on disk\r\n")?; Ok(()) } diff --git a/examples/delete_file.rs b/examples/delete_file.rs index 743b2d54..f76d3601 100644 --- a/examples/delete_file.rs +++ b/examples/delete_file.rs @@ -33,10 +33,10 @@ fn main() -> Result<(), embedded_sdmmc::Error> { let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; - let mut volume_mgr: VolumeManager = + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); - let mut volume = volume_mgr.open_volume(VolumeIdx(0))?; - let mut root_dir = volume.open_root_dir()?; + let volume = volume_mgr.open_volume(VolumeIdx(0))?; + let root_dir = volume.open_root_dir()?; println!("Deleting file {}...", FILE_TO_DELETE); root_dir.delete_file_in_dir(FILE_TO_DELETE)?; println!("Deleted!"); diff --git a/examples/list_dir.rs b/examples/list_dir.rs index 18c121d4..60d72943 100644 --- a/examples/list_dir.rs +++ b/examples/list_dir.rs @@ -47,9 +47,9 @@ fn main() -> Result<(), Error> { let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; - let mut volume_mgr: VolumeManager = + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); - let mut volume = volume_mgr.open_volume(VolumeIdx(0))?; + let volume = volume_mgr.open_volume(VolumeIdx(0))?; let root_dir = volume.open_root_dir()?; list_dir(root_dir, "/")?; Ok(()) @@ -59,7 +59,7 @@ fn main() -> Result<(), Error> { /// /// The path is for display purposes only. fn list_dir( - mut directory: Directory, + directory: Directory, path: &str, ) -> Result<(), Error> { println!("Listing {}", path); diff --git a/examples/read_file.rs b/examples/read_file.rs index 1a958c1c..f962b75a 100644 --- a/examples/read_file.rs +++ b/examples/read_file.rs @@ -47,12 +47,15 @@ fn main() -> Result<(), embedded_sdmmc::Error> { let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; - let mut volume_mgr: VolumeManager = + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); - let mut volume = volume_mgr.open_volume(VolumeIdx(0))?; - let mut root_dir = volume.open_root_dir()?; + let volume = volume_mgr.open_volume(VolumeIdx(0))?; + let root_dir = volume.open_root_dir()?; println!("\nReading file {}...", FILE_TO_READ); - let mut f = root_dir.open_file_in_dir(FILE_TO_READ, Mode::ReadOnly)?; + let f = root_dir.open_file_in_dir(FILE_TO_READ, Mode::ReadOnly)?; + // Proves we can open two files at once now (or try to - this file doesn't exist) + let f2 = root_dir.open_file_in_dir("MISSING.DAT", Mode::ReadOnly); + assert!(f2.is_err()); while !f.is_eof() { let mut buffer = [0u8; 16]; let offset = f.offset(); diff --git a/examples/readme_test.rs b/examples/readme_test.rs index 7ae7384d..fd867802 100644 --- a/examples/readme_test.rs +++ b/examples/readme_test.rs @@ -125,16 +125,16 @@ fn main() -> Result<(), Error> { println!("Card size is {} bytes", sdcard.num_bytes()?); // Now let's look for volumes (also known as partitions) on our block device. // To do this we need a Volume Manager. It will take ownership of the block device. - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(sdcard, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(sdcard, time_source); // Try and access Volume 0 (i.e. the first partition). // The volume object holds information about the filesystem on that volume. - let mut volume0 = volume_mgr.open_volume(embedded_sdmmc::VolumeIdx(0))?; + let volume0 = volume_mgr.open_volume(embedded_sdmmc::VolumeIdx(0))?; println!("Volume 0: {:?}", volume0); // Open the root directory (mutably borrows from the volume). - let mut root_dir = volume0.open_root_dir()?; + let root_dir = volume0.open_root_dir()?; // Open a file called "MY_FILE.TXT" in the root directory // This mutably borrows the directory. - let mut my_file = root_dir.open_file_in_dir("MY_FILE.TXT", embedded_sdmmc::Mode::ReadOnly)?; + let my_file = root_dir.open_file_in_dir("MY_FILE.TXT", embedded_sdmmc::Mode::ReadOnly)?; // Print the contents of the file, assuming it's in ISO-8859-1 encoding while !my_file.is_eof() { let mut buffer = [0u8; 32]; diff --git a/examples/shell.rs b/examples/shell.rs index 91b5c499..7822dde8 100644 --- a/examples/shell.rs +++ b/examples/shell.rs @@ -228,7 +228,7 @@ impl Context { fn dir(&mut self, path: &Path) -> Result<(), Error> { println!("Directory listing of {:?}", path); let dir = self.resolve_existing_directory(path)?; - let mut dir = dir.to_directory(&mut self.volume_mgr); + let dir = dir.to_directory(&mut self.volume_mgr); dir.iterate_dir(|entry| { if !entry.attributes.is_volume() && !entry.attributes.is_lfn() { println!( @@ -257,7 +257,7 @@ impl Context { /// /// Will close the given directory. fn tree_dir(&mut self, dir: RawDirectory) -> Result<(), Error> { - let mut dir = dir.to_directory(&mut self.volume_mgr); + let dir = dir.to_directory(&mut self.volume_mgr); let mut children = Vec::new(); dir.iterate_dir(|entry| { println!( @@ -329,8 +329,8 @@ impl Context { /// print a text file fn cat(&mut self, filename: &Path) -> Result<(), Error> { let (dir, filename) = self.resolve_filename(filename)?; - let mut dir = dir.to_directory(&mut self.volume_mgr); - let mut f = dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadOnly)?; + let dir = dir.to_directory(&mut self.volume_mgr); + let f = dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadOnly)?; let mut data = Vec::new(); while !f.is_eof() { let mut buffer = vec![0u8; 65536]; @@ -350,8 +350,8 @@ impl Context { /// print a binary file fn hexdump(&mut self, filename: &Path) -> Result<(), Error> { let (dir, filename) = self.resolve_filename(filename)?; - let mut dir = dir.to_directory(&mut self.volume_mgr); - let mut f = dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadOnly)?; + let dir = dir.to_directory(&mut self.volume_mgr); + let f = dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadOnly)?; let mut data = Vec::new(); while !f.is_eof() { let mut buffer = vec![0u8; 65536]; @@ -387,7 +387,7 @@ impl Context { /// create a directory fn mkdir(&mut self, dir_name: &Path) -> Result<(), Error> { let (dir, filename) = self.resolve_filename(dir_name)?; - let mut dir = dir.to_directory(&mut self.volume_mgr); + let dir = dir.to_directory(&mut self.volume_mgr); dir.make_dir_in_dir(filename) } diff --git a/src/filesystem/directory.rs b/src/filesystem/directory.rs index efd6e5e8..46dad489 100644 --- a/src/filesystem/directory.rs +++ b/src/filesystem/directory.rs @@ -57,7 +57,7 @@ impl RawDirectory { const MAX_VOLUMES: usize, >( self, - volume_mgr: &mut VolumeManager, + volume_mgr: &VolumeManager, ) -> Directory where D: crate::BlockDevice, @@ -87,7 +87,7 @@ pub struct Directory< T: crate::TimeSource, { raw_directory: RawDirectory, - volume_mgr: &'a mut VolumeManager, + volume_mgr: &'a VolumeManager, } impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> @@ -99,7 +99,7 @@ where /// Create a new `Directory` from a `RawDirectory` pub fn new( raw_directory: RawDirectory, - volume_mgr: &'a mut VolumeManager, + volume_mgr: &'a VolumeManager, ) -> Directory<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> { Directory { raw_directory, @@ -111,7 +111,7 @@ where /// /// You can then read the directory entries with `iterate_dir` and `open_file_in_dir`. pub fn open_dir( - &mut self, + &self, name: N, ) -> Result, Error> where @@ -135,7 +135,7 @@ where } /// Look in a directory for a named file. - pub fn find_directory_entry(&mut self, name: N) -> Result> + pub fn find_directory_entry(&self, name: N) -> Result> where N: ToShortFileName, { @@ -144,7 +144,7 @@ where } /// Call a callback function for each directory entry in a directory. - pub fn iterate_dir(&mut self, func: F) -> Result<(), Error> + pub fn iterate_dir(&self, func: F) -> Result<(), Error> where F: FnMut(&DirEntry), { @@ -153,7 +153,7 @@ where /// Open a file with the given full path. A file can only be opened once. pub fn open_file_in_dir( - &mut self, + &self, name: N, mode: crate::Mode, ) -> Result, crate::Error> @@ -167,7 +167,7 @@ where } /// Delete a closed file with the given filename, if it exists. - pub fn delete_file_in_dir(&mut self, name: N) -> Result<(), Error> + pub fn delete_file_in_dir(&self, name: N) -> Result<(), Error> where N: ToShortFileName, { @@ -175,7 +175,7 @@ where } /// Make a directory inside this directory - pub fn make_dir_in_dir(&mut self, name: N) -> Result<(), Error> + pub fn make_dir_in_dir(&self, name: N) -> Result<(), Error> where N: ToShortFileName, { @@ -239,9 +239,9 @@ where #[derive(Debug, Clone)] pub(crate) struct DirectoryInfo { /// The handle for this directory. - pub(crate) directory_id: RawDirectory, + pub(crate) raw_directory: RawDirectory, /// The handle for the volume this directory is on - pub(crate) volume_id: RawVolume, + pub(crate) raw_volume: RawVolume, /// The starting point of the directory listing. pub(crate) cluster: ClusterId, } diff --git a/src/filesystem/files.rs b/src/filesystem/files.rs index c0159cfb..870d85df 100644 --- a/src/filesystem/files.rs +++ b/src/filesystem/files.rs @@ -29,7 +29,7 @@ impl RawFile { /// Convert a raw file into a droppable [`File`] pub fn to_file( self, - volume_mgr: &mut VolumeManager, + volume_mgr: &VolumeManager, ) -> File where D: crate::BlockDevice, @@ -53,7 +53,7 @@ where T: crate::TimeSource, { raw_file: RawFile, - volume_mgr: &'a mut VolumeManager, + volume_mgr: &'a VolumeManager, } impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> @@ -65,7 +65,7 @@ where /// Create a new `File` from a `RawFile` pub fn new( raw_file: RawFile, - volume_mgr: &'a mut VolumeManager, + volume_mgr: &'a VolumeManager, ) -> File<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> { File { raw_file, @@ -76,12 +76,12 @@ where /// Read from the file /// /// Returns how many bytes were read, or an error. - pub fn read(&mut self, buffer: &mut [u8]) -> Result> { + pub fn read(&self, buffer: &mut [u8]) -> Result> { self.volume_mgr.read(self.raw_file, buffer) } /// Write to the file - pub fn write(&mut self, buffer: &[u8]) -> Result<(), crate::Error> { + pub fn write(&self, buffer: &[u8]) -> Result<(), crate::Error> { self.volume_mgr.write(self.raw_file, buffer) } @@ -93,18 +93,18 @@ where } /// Seek a file with an offset from the current position. - pub fn seek_from_current(&mut self, offset: i32) -> Result<(), crate::Error> { + pub fn seek_from_current(&self, offset: i32) -> Result<(), crate::Error> { self.volume_mgr .file_seek_from_current(self.raw_file, offset) } /// Seek a file with an offset from the start of the file. - pub fn seek_from_start(&mut self, offset: u32) -> Result<(), crate::Error> { + pub fn seek_from_start(&self, offset: u32) -> Result<(), crate::Error> { self.volume_mgr.file_seek_from_start(self.raw_file, offset) } /// Seek a file with an offset back from the end of the file. - pub fn seek_from_end(&mut self, offset: u32) -> Result<(), crate::Error> { + pub fn seek_from_end(&self, offset: u32) -> Result<(), crate::Error> { self.volume_mgr.file_seek_from_end(self.raw_file, offset) } @@ -130,7 +130,7 @@ where } /// Flush any written data by updating the directory entry. - pub fn flush(&mut self) -> Result<(), Error> { + pub fn flush(&self) -> Result<(), Error> { self.volume_mgr.flush_file(self.raw_file) } @@ -213,7 +213,7 @@ impl< } fn flush(&mut self) -> Result<(), Self::Error> { - self.flush() + Self::flush(self) } } diff --git a/src/filesystem/mod.rs b/src/filesystem/mod.rs index f6cd28a4..92c94a1e 100644 --- a/src/filesystem/mod.rs +++ b/src/filesystem/mod.rs @@ -4,7 +4,7 @@ //! most (if not all) supported filesystems. /// Maximum file size supported by this library -pub const MAX_FILE_SIZE: u32 = core::u32::MAX; +pub const MAX_FILE_SIZE: u32 = u32::MAX; mod attributes; mod cluster; diff --git a/src/lib.rs b/src/lib.rs index 05938ecf..27d54f6c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,10 +27,10 @@ //! { //! let sdcard = SdCard::new(spi, delay); //! println!("Card size is {} bytes", sdcard.num_bytes()?); -//! let mut volume_mgr = VolumeManager::new(sdcard, ts); -//! let mut volume0 = volume_mgr.open_volume(VolumeIdx(0))?; +//! let volume_mgr = VolumeManager::new(sdcard, ts); +//! let volume0 = volume_mgr.open_volume(VolumeIdx(0))?; //! println!("Volume 0: {:?}", volume0); -//! let mut root_dir = volume0.open_root_dir()?; +//! let root_dir = volume0.open_root_dir()?; //! let mut my_file = root_dir.open_file_in_dir("MY_FILE.TXT", Mode::ReadOnly)?; //! while !my_file.is_eof() { //! let mut buffer = [0u8; 32]; @@ -47,8 +47,8 @@ //! //! * `log`: Enabled by default. Generates log messages using the `log` crate. //! * `defmt-log`: By turning off the default features and enabling the -//! `defmt-log` feature you can configure this crate to log messages over defmt -//! instead. +//! `defmt-log` feature you can configure this crate to log messages over defmt +//! instead. //! //! You cannot enable both the `log` feature and the `defmt-log` feature. @@ -271,7 +271,7 @@ impl RawVolume { const MAX_VOLUMES: usize, >( self, - volume_mgr: &mut VolumeManager, + volume_mgr: &VolumeManager, ) -> Volume where D: crate::BlockDevice, @@ -295,7 +295,7 @@ where T: crate::TimeSource, { raw_volume: RawVolume, - volume_mgr: &'a mut VolumeManager, + volume_mgr: &'a VolumeManager, } impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> @@ -307,7 +307,7 @@ where /// Create a new `Volume` from a `RawVolume` pub fn new( raw_volume: RawVolume, - volume_mgr: &'a mut VolumeManager, + volume_mgr: &'a VolumeManager, ) -> Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> { Volume { raw_volume, @@ -320,7 +320,7 @@ where /// You can then read the directory entries with `iterate_dir`, or you can /// use `open_file_in_dir`. pub fn open_root_dir( - &mut self, + &self, ) -> Result, Error> { let d = self.volume_mgr.open_root_dir(self.raw_volume)?; Ok(d.to_directory(self.volume_mgr)) diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 928f410d..4e5f56d4 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -2,6 +2,7 @@ //! //! The volume manager handles partitions and open files on a block device. +use core::cell::RefCell; use core::convert::TryFrom; use byteorder::{ByteOrder, LittleEndian}; @@ -38,10 +39,7 @@ pub struct VolumeManager< { pub(crate) block_device: D, pub(crate) time_source: T, - id_generator: HandleGenerator, - open_volumes: Vec, - open_dirs: Vec, - open_files: Vec, + data: RefCell>, } impl VolumeManager @@ -87,15 +85,22 @@ where VolumeManager { block_device, time_source, - id_generator: HandleGenerator::new(id_offset), - open_volumes: Vec::new(), - open_dirs: Vec::new(), - open_files: Vec::new(), + data: RefCell::new(VolumeManagerData { + id_generator: HandleGenerator::new(id_offset), + open_volumes: Vec::new(), + open_dirs: Vec::new(), + open_files: Vec::new(), + }), } } /// Temporarily get access to the underlying block device. - pub fn device(&mut self) -> &mut D { + pub fn device(&self) -> &D { + &self.block_device + } + + /// Temporarily get access to the underlying block device. + pub fn device_mut(&mut self) -> &mut D { &mut self.block_device } @@ -104,7 +109,7 @@ where /// We do not support GUID Partition Table disks. Nor do we support any /// concept of drive letters - that is for a higher layer to handle. pub fn open_volume( - &mut self, + &self, volume_idx: VolumeIdx, ) -> Result, Error> { let v = self.open_raw_volume(volume_idx)?; @@ -118,7 +123,7 @@ where /// /// This function gives you a `RawVolume` and you must close the volume by /// calling `VolumeManager::close_volume`. - pub fn open_raw_volume(&mut self, volume_idx: VolumeIdx) -> Result> { + pub fn open_raw_volume(&self, volume_idx: VolumeIdx) -> Result> { const PARTITION1_START: usize = 446; const PARTITION2_START: usize = PARTITION1_START + PARTITION_INFO_LENGTH; const PARTITION3_START: usize = PARTITION2_START + PARTITION_INFO_LENGTH; @@ -131,11 +136,13 @@ where const PARTITION_INFO_LBA_START_INDEX: usize = 8; const PARTITION_INFO_NUM_BLOCKS_INDEX: usize = 12; - if self.open_volumes.is_full() { + let mut data = self.data.borrow_mut(); + + if data.open_volumes.is_full() { return Err(Error::TooManyOpenVolumes); } - for v in self.open_volumes.iter() { + for v in data.open_volumes.iter() { if v.idx == volume_idx { return Err(Error::VolumeAlreadyOpen); } @@ -192,14 +199,14 @@ where | PARTITION_ID_FAT16_LBA | PARTITION_ID_FAT16 => { let volume = fat::parse_volume(&self.block_device, lba_start, num_blocks)?; - let id = RawVolume(self.id_generator.generate()); + let id = RawVolume(data.id_generator.generate()); let info = VolumeInfo { raw_volume: id, idx: volume_idx, volume_type: volume, }; // We already checked for space - self.open_volumes.push(info).unwrap(); + data.open_volumes.push(info).unwrap(); Ok(id) } _ => Err(Error::FormatError("Partition type not supported")), @@ -210,17 +217,19 @@ where /// /// You can then read the directory entries with `iterate_dir`, or you can /// use `open_file_in_dir`. - pub fn open_root_dir(&mut self, volume: RawVolume) -> Result> { + pub fn open_root_dir(&self, volume: RawVolume) -> Result> { // Opening a root directory twice is OK - let directory_id = RawDirectory(self.id_generator.generate()); + let mut data = self.data.borrow_mut(); + + let directory_id = RawDirectory(data.id_generator.generate()); let dir_info = DirectoryInfo { - volume_id: volume, + raw_volume: volume, cluster: ClusterId::ROOT_DIR, - directory_id, + raw_directory: directory_id, }; - self.open_dirs + data.open_dirs .push(dir_info) .map_err(|_| Error::TooManyOpenDirs)?; @@ -233,44 +242,47 @@ where /// /// Passing "." as the name results in opening the `parent_dir` a second time. pub fn open_dir( - &mut self, + &self, parent_dir: RawDirectory, name: N, ) -> Result> where N: ToShortFileName, { - if self.open_dirs.is_full() { + let mut data = self.data.borrow_mut(); + + if data.open_dirs.is_full() { return Err(Error::TooManyOpenDirs); } // Find dir by ID - let parent_dir_idx = self.get_dir_by_id(parent_dir)?; - let volume_idx = self.get_volume_by_id(self.open_dirs[parent_dir_idx].volume_id)?; + let parent_dir_idx = data.get_dir_by_id(parent_dir)?; + let volume_idx = data.get_volume_by_id(data.open_dirs[parent_dir_idx].raw_volume)?; let short_file_name = name.to_short_filename().map_err(Error::FilenameError)?; - let parent_dir_info = &self.open_dirs[parent_dir_idx]; // Open the directory if short_file_name == ShortFileName::this_dir() { // short-cut (root dir doesn't have ".") - let directory_id = RawDirectory(self.id_generator.generate()); + let directory_id = RawDirectory(data.id_generator.generate()); let dir_info = DirectoryInfo { - directory_id, - volume_id: self.open_volumes[volume_idx].raw_volume, - cluster: parent_dir_info.cluster, + raw_directory: directory_id, + raw_volume: data.open_volumes[volume_idx].raw_volume, + cluster: data.open_dirs[parent_dir_idx].cluster, }; - self.open_dirs + data.open_dirs .push(dir_info) .map_err(|_| Error::TooManyOpenDirs)?; return Ok(directory_id); } - let dir_entry = match &self.open_volumes[volume_idx].volume_type { - VolumeType::Fat(fat) => { - fat.find_directory_entry(&self.block_device, parent_dir_info, &short_file_name)? - } + let dir_entry = match &data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => fat.find_directory_entry( + &self.block_device, + &data.open_dirs[parent_dir_idx], + &short_file_name, + )?, }; debug!("Found dir entry: {:?}", dir_entry); @@ -283,14 +295,14 @@ where // no cached state and so opening a directory twice is allowable. // Remember this open directory. - let directory_id = RawDirectory(self.id_generator.generate()); + let directory_id = RawDirectory(data.id_generator.generate()); let dir_info = DirectoryInfo { - directory_id, - volume_id: self.open_volumes[volume_idx].raw_volume, + raw_directory: directory_id, + raw_volume: data.open_volumes[volume_idx].raw_volume, cluster: dir_entry.cluster, }; - self.open_dirs + data.open_dirs .push(dir_info) .map_err(|_| Error::TooManyOpenDirs)?; @@ -299,10 +311,12 @@ where /// Close a directory. You cannot perform operations on an open directory /// and so must close it if you want to do something with it. - pub fn close_dir(&mut self, directory: RawDirectory) -> Result<(), Error> { - for (idx, info) in self.open_dirs.iter().enumerate() { - if directory == info.directory_id { - self.open_dirs.swap_remove(idx); + pub fn close_dir(&self, directory: RawDirectory) -> Result<(), Error> { + let mut data = self.data.borrow_mut(); + + for (idx, info) in data.open_dirs.iter().enumerate() { + if directory == info.raw_directory { + data.open_dirs.swap_remove(idx); return Ok(()); } } @@ -312,159 +326,68 @@ where /// Close a volume /// /// You can't close it if there are any files or directories open on it. - pub fn close_volume(&mut self, volume: RawVolume) -> Result<(), Error> { - for f in self.open_files.iter() { + pub fn close_volume(&self, volume: RawVolume) -> Result<(), Error> { + let mut data = self.data.borrow_mut(); + + for f in data.open_files.iter() { if f.raw_volume == volume { return Err(Error::VolumeStillInUse); } } - for d in self.open_dirs.iter() { - if d.volume_id == volume { + for d in data.open_dirs.iter() { + if d.raw_volume == volume { return Err(Error::VolumeStillInUse); } } - let volume_idx = self.get_volume_by_id(volume)?; - self.open_volumes.swap_remove(volume_idx); + let volume_idx = data.get_volume_by_id(volume)?; + + data.open_volumes.swap_remove(volume_idx); Ok(()) } /// Look in a directory for a named file. pub fn find_directory_entry( - &mut self, + &self, directory: RawDirectory, name: N, ) -> Result> where N: ToShortFileName, { - let directory_idx = self.get_dir_by_id(directory)?; - let volume_idx = self.get_volume_by_id(self.open_dirs[directory_idx].volume_id)?; - match &self.open_volumes[volume_idx].volume_type { + let data = self.data.borrow(); + + let directory_idx = data.get_dir_by_id(directory)?; + let volume_idx = data.get_volume_by_id(data.open_dirs[directory_idx].raw_volume)?; + match &data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => { let sfn = name.to_short_filename().map_err(Error::FilenameError)?; - fat.find_directory_entry(&self.block_device, &self.open_dirs[directory_idx], &sfn) + fat.find_directory_entry(&self.block_device, &data.open_dirs[directory_idx], &sfn) } } } /// Call a callback function for each directory entry in a directory. - pub fn iterate_dir( - &mut self, - directory: RawDirectory, - func: F, - ) -> Result<(), Error> + pub fn iterate_dir(&self, directory: RawDirectory, func: F) -> Result<(), Error> where F: FnMut(&DirEntry), { - let directory_idx = self.get_dir_by_id(directory)?; - let volume_idx = self.get_volume_by_id(self.open_dirs[directory_idx].volume_id)?; - match &self.open_volumes[volume_idx].volume_type { - VolumeType::Fat(fat) => { - fat.iterate_dir(&self.block_device, &self.open_dirs[directory_idx], func) - } - } - } + let data = self.data.borrow(); - /// Open a file from a DirEntry. This is obtained by calling iterate_dir. - /// - /// # Safety - /// - /// The DirEntry must be a valid DirEntry read from disk, and not just - /// random numbers. - unsafe fn open_dir_entry( - &mut self, - volume: RawVolume, - dir_entry: DirEntry, - mode: Mode, - ) -> Result> { - // This check is load-bearing - we do an unchecked push later. - if self.open_files.is_full() { - return Err(Error::TooManyOpenFiles); - } - - if dir_entry.attributes.is_read_only() && mode != Mode::ReadOnly { - return Err(Error::ReadOnly); - } - - if dir_entry.attributes.is_directory() { - return Err(Error::OpenedDirAsFile); - } - - // Check it's not already open - if self.file_is_open(volume, &dir_entry) { - return Err(Error::FileAlreadyOpen); - } - - let mode = solve_mode_variant(mode, true); - let file_id = RawFile(self.id_generator.generate()); - - let file = match mode { - Mode::ReadOnly => FileInfo { - raw_file: file_id, - raw_volume: volume, - current_cluster: (0, dir_entry.cluster), - current_offset: 0, - mode, - entry: dir_entry, - dirty: false, - }, - Mode::ReadWriteAppend => { - let mut file = FileInfo { - raw_file: file_id, - raw_volume: volume, - current_cluster: (0, dir_entry.cluster), - current_offset: 0, - mode, - entry: dir_entry, - dirty: false, - }; - // seek_from_end with 0 can't fail - file.seek_from_end(0).ok(); - file - } - Mode::ReadWriteTruncate => { - let mut file = FileInfo { - raw_file: file_id, - raw_volume: volume, - current_cluster: (0, dir_entry.cluster), - current_offset: 0, - mode, - entry: dir_entry, - dirty: false, - }; - let volume_idx = self.get_volume_by_id(volume)?; - match &mut self.open_volumes[volume_idx].volume_type { - VolumeType::Fat(fat) => { - fat.truncate_cluster_chain(&self.block_device, file.entry.cluster)? - } - }; - file.update_length(0); - match &self.open_volumes[volume_idx].volume_type { - VolumeType::Fat(fat) => { - file.entry.mtime = self.time_source.get_timestamp(); - fat.write_entry_to_disk(&self.block_device, &file.entry)?; - } - }; - - file + let directory_idx = data.get_dir_by_id(directory)?; + let volume_idx = data.get_volume_by_id(data.open_dirs[directory_idx].raw_volume)?; + match &data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + fat.iterate_dir(&self.block_device, &data.open_dirs[directory_idx], func) } - _ => return Err(Error::Unsupported), - }; - - // Remember this open file - can't be full as we checked already - unsafe { - self.open_files.push_unchecked(file); } - - Ok(file_id) } /// Open a file with the given full path. A file can only be opened once. pub fn open_file_in_dir( - &mut self, + &self, directory: RawDirectory, name: N, mode: Mode, @@ -472,21 +395,22 @@ where where N: ToShortFileName, { + let mut data = self.data.borrow_mut(); + // This check is load-bearing - we do an unchecked push later. - if self.open_files.is_full() { + if data.open_files.is_full() { return Err(Error::TooManyOpenFiles); } - let directory_idx = self.get_dir_by_id(directory)?; - let directory_info = &self.open_dirs[directory_idx]; - let volume_id = self.open_dirs[directory_idx].volume_id; - let volume_idx = self.get_volume_by_id(volume_id)?; - let volume_info = &self.open_volumes[volume_idx]; + let directory_idx = data.get_dir_by_id(directory)?; + let volume_id = data.open_dirs[directory_idx].raw_volume; + let volume_idx = data.get_volume_by_id(volume_id)?; + let volume_info = &data.open_volumes[volume_idx]; let sfn = name.to_short_filename().map_err(Error::FilenameError)?; let dir_entry = match &volume_info.volume_type { VolumeType::Fat(fat) => { - fat.find_directory_entry(&self.block_device, directory_info, &sfn) + fat.find_directory_entry(&self.block_device, &data.open_dirs[directory_idx], &sfn) } }; @@ -512,7 +436,7 @@ where // Check if it's open already if let Some(dir_entry) = &dir_entry { - if self.file_is_open(volume_info.raw_volume, dir_entry) { + if data.file_is_open(volume_info.raw_volume, dir_entry) { return Err(Error::FileAlreadyOpen); } } @@ -524,19 +448,20 @@ where if dir_entry.is_some() { return Err(Error::FileAlreadyExists); } + let cluster = data.open_dirs[directory_idx].cluster; let att = Attributes::create_from_fat(0); - let volume_idx = self.get_volume_by_id(volume_id)?; - let entry = match &mut self.open_volumes[volume_idx].volume_type { + let volume_idx = data.get_volume_by_id(volume_id)?; + let entry = match &mut data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => fat.write_new_directory_entry( &self.block_device, &self.time_source, - directory_info.cluster, + cluster, sfn, att, )?, }; - let file_id = RawFile(self.id_generator.generate()); + let file_id = RawFile(data.id_generator.generate()); let file = FileInfo { raw_file: file_id, @@ -550,7 +475,7 @@ where // Remember this open file - can't be full as we checked already unsafe { - self.open_files.push_unchecked(file); + data.open_files.push_unchecked(file); } Ok(file_id) @@ -558,27 +483,102 @@ where _ => { // Safe to unwrap, since we actually have an entry if we got here let dir_entry = dir_entry.unwrap(); - // Safety: We read this dir entry off disk and didn't change it - unsafe { self.open_dir_entry(volume_id, dir_entry, mode) } + + if dir_entry.attributes.is_read_only() && mode != Mode::ReadOnly { + return Err(Error::ReadOnly); + } + + if dir_entry.attributes.is_directory() { + return Err(Error::OpenedDirAsFile); + } + + // Check it's not already open + if data.file_is_open(volume_id, &dir_entry) { + return Err(Error::FileAlreadyOpen); + } + + let mode = solve_mode_variant(mode, true); + let raw_file = RawFile(data.id_generator.generate()); + + let file = match mode { + Mode::ReadOnly => FileInfo { + raw_file, + raw_volume: volume_id, + current_cluster: (0, dir_entry.cluster), + current_offset: 0, + mode, + entry: dir_entry, + dirty: false, + }, + Mode::ReadWriteAppend => { + let mut file = FileInfo { + raw_file, + raw_volume: volume_id, + current_cluster: (0, dir_entry.cluster), + current_offset: 0, + mode, + entry: dir_entry, + dirty: false, + }; + // seek_from_end with 0 can't fail + file.seek_from_end(0).ok(); + file + } + Mode::ReadWriteTruncate => { + let mut file = FileInfo { + raw_file, + raw_volume: volume_id, + current_cluster: (0, dir_entry.cluster), + current_offset: 0, + mode, + entry: dir_entry, + dirty: false, + }; + match &mut data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + fat.truncate_cluster_chain(&self.block_device, file.entry.cluster)? + } + }; + file.update_length(0); + match &data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + file.entry.mtime = self.time_source.get_timestamp(); + fat.write_entry_to_disk(&self.block_device, &file.entry)?; + } + }; + + file + } + _ => return Err(Error::Unsupported), + }; + + // Remember this open file - can't be full as we checked already + unsafe { + data.open_files.push_unchecked(file); + } + + Ok(raw_file) } } } /// Delete a closed file with the given filename, if it exists. pub fn delete_file_in_dir( - &mut self, + &self, directory: RawDirectory, name: N, ) -> Result<(), Error> where N: ToShortFileName, { - let dir_idx = self.get_dir_by_id(directory)?; - let dir_info = &self.open_dirs[dir_idx]; - let volume_idx = self.get_volume_by_id(dir_info.volume_id)?; + let data = self.data.borrow(); + + let dir_idx = data.get_dir_by_id(directory)?; + let dir_info = &data.open_dirs[dir_idx]; + let volume_idx = data.get_volume_by_id(dir_info.raw_volume)?; let sfn = name.to_short_filename().map_err(Error::FilenameError)?; - let dir_entry = match &self.open_volumes[volume_idx].volume_type { + let dir_entry = match &data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => fat.find_directory_entry(&self.block_device, dir_info, &sfn), }?; @@ -586,12 +586,12 @@ where return Err(Error::DeleteDirAsFile); } - if self.file_is_open(dir_info.volume_id, &dir_entry) { + if data.file_is_open(dir_info.raw_volume, &dir_entry) { return Err(Error::FileAlreadyOpen); } - let volume_idx = self.get_volume_by_id(dir_info.volume_id)?; - match &self.open_volumes[volume_idx].volume_type { + let volume_idx = data.get_volume_by_id(dir_info.raw_volume)?; + match &data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => { fat.delete_directory_entry(&self.block_device, dir_info, &sfn)? } @@ -602,64 +602,51 @@ where /// Search the root directory for a volume label pub fn get_root_volume_label( - &mut self, + &self, volume: RawVolume, ) -> Result, Error> { let directory = self.open_root_dir(volume)?; + let data = self.data.borrow(); // this can't fail - we literally just opened it - let inner = || -> Result, Error> { - let directory_idx = self.get_dir_by_id(directory).expect("Dir ID error"); - let volume_idx = self.get_volume_by_id(self.open_dirs[directory_idx].volume_id)?; - let mut maybe_volume_name = None; - match &self.open_volumes[volume_idx].volume_type { - VolumeType::Fat(fat) => { - fat.iterate_dir(&self.block_device, &self.open_dirs[directory_idx], |de| { - if de.attributes == Attributes::create_from_fat(Attributes::VOLUME) { - maybe_volume_name = Some(unsafe { de.name.clone().to_volume_label() }) - } - })?; - } - } - Ok(maybe_volume_name) - }; - let result = inner(); - self.close_dir(directory)?; - result - } - - /// Check if a file is open - /// - /// Returns `true` if it's open, `false`, otherwise. - fn file_is_open(&self, volume: RawVolume, dir_entry: &DirEntry) -> bool { - for f in self.open_files.iter() { - if f.raw_volume == volume - && f.entry.entry_block == dir_entry.entry_block - && f.entry.entry_offset == dir_entry.entry_offset - { - return true; + let directory_idx = data + .get_dir_by_id::(directory) + .expect("Dir ID error"); + let volume_idx = data.get_volume_by_id(data.open_dirs[directory_idx].raw_volume)?; + let mut maybe_volume_name = None; + match &data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + fat.iterate_dir(&self.block_device, &data.open_dirs[directory_idx], |de| { + if de.attributes == Attributes::create_from_fat(Attributes::VOLUME) { + maybe_volume_name = Some(unsafe { de.name.clone().to_volume_label() }) + } + })?; } } - false + Ok(maybe_volume_name) } /// Read from an open file. - pub fn read(&mut self, file: RawFile, buffer: &mut [u8]) -> Result> { - let file_idx = self.get_file_by_id(file)?; - let volume_idx = self.get_volume_by_id(self.open_files[file_idx].raw_volume)?; + pub fn read(&self, file: RawFile, buffer: &mut [u8]) -> Result> { + let mut data = self.data.borrow_mut(); + + let file_idx = data.get_file_by_id(file)?; + let volume_idx = data.get_volume_by_id(data.open_files[file_idx].raw_volume)?; + // Calculate which file block the current offset lies within // While there is more to read, read the block and copy in to the buffer. // If we need to find the next cluster, walk the FAT. let mut space = buffer.len(); let mut read = 0; - while space > 0 && !self.open_files[file_idx].eof() { - let mut current_cluster = self.open_files[file_idx].current_cluster; - let (block_idx, block_offset, block_avail) = self.find_data_on_disk( + while space > 0 && !data.open_files[file_idx].eof() { + let mut current_cluster = data.open_files[file_idx].current_cluster; + let (block_idx, block_offset, block_avail) = data.find_data_on_disk( + &self.block_device, volume_idx, &mut current_cluster, - self.open_files[file_idx].entry.cluster, - self.open_files[file_idx].current_offset, + data.open_files[file_idx].entry.cluster, + data.open_files[file_idx].current_offset, )?; - self.open_files[file_idx].current_cluster = current_cluster; + data.open_files[file_idx].current_cluster = current_cluster; let mut blocks = [Block::new()]; trace!("Reading file ID {:?}", file); self.block_device @@ -668,13 +655,13 @@ where let block = &blocks[0]; let to_copy = block_avail .min(space) - .min(self.open_files[file_idx].left() as usize); + .min(data.open_files[file_idx].left() as usize); assert!(to_copy != 0); buffer[read..read + to_copy] .copy_from_slice(&block[block_offset..block_offset + to_copy]); read += to_copy; space -= to_copy; - self.open_files[file_idx] + data.open_files[file_idx] .seek_from_current(to_copy as i32) .unwrap(); } @@ -682,63 +669,66 @@ where } /// Write to a open file. - pub fn write(&mut self, file: RawFile, buffer: &[u8]) -> Result<(), Error> { + pub fn write(&self, file: RawFile, buffer: &[u8]) -> Result<(), Error> { #[cfg(feature = "defmt-log")] debug!("write(file={:?}, buffer={:x}", file, buffer); #[cfg(feature = "log")] debug!("write(file={:?}, buffer={:x?}", file, buffer); + let mut data = self.data.borrow_mut(); + // Clone this so we can touch our other structures. Need to ensure we // write it back at the end. - let file_idx = self.get_file_by_id(file)?; - let volume_idx = self.get_volume_by_id(self.open_files[file_idx].raw_volume)?; + let file_idx = data.get_file_by_id(file)?; + let volume_idx = data.get_volume_by_id(data.open_files[file_idx].raw_volume)?; - if self.open_files[file_idx].mode == Mode::ReadOnly { + if data.open_files[file_idx].mode == Mode::ReadOnly { return Err(Error::ReadOnly); } - self.open_files[file_idx].dirty = true; + data.open_files[file_idx].dirty = true; - if self.open_files[file_idx].entry.cluster.0 < RESERVED_ENTRIES { + if data.open_files[file_idx].entry.cluster.0 < RESERVED_ENTRIES { // file doesn't have a valid allocated cluster (possible zero-length file), allocate one - self.open_files[file_idx].entry.cluster = - match self.open_volumes[volume_idx].volume_type { + data.open_files[file_idx].entry.cluster = + match data.open_volumes[volume_idx].volume_type { VolumeType::Fat(ref mut fat) => { fat.alloc_cluster(&self.block_device, None, false)? } }; debug!( "Alloc first cluster {:?}", - self.open_files[file_idx].entry.cluster + data.open_files[file_idx].entry.cluster ); } // Clone this so we can touch our other structures. - let volume_idx = self.get_volume_by_id(self.open_files[file_idx].raw_volume)?; + let volume_idx = data.get_volume_by_id(data.open_files[file_idx].raw_volume)?; - if (self.open_files[file_idx].current_cluster.1) < self.open_files[file_idx].entry.cluster { + if (data.open_files[file_idx].current_cluster.1) < data.open_files[file_idx].entry.cluster { debug!("Rewinding to start"); - self.open_files[file_idx].current_cluster = - (0, self.open_files[file_idx].entry.cluster); + data.open_files[file_idx].current_cluster = + (0, data.open_files[file_idx].entry.cluster); } let bytes_until_max = - usize::try_from(MAX_FILE_SIZE - self.open_files[file_idx].current_offset) + usize::try_from(MAX_FILE_SIZE - data.open_files[file_idx].current_offset) .map_err(|_| Error::ConversionError)?; let bytes_to_write = core::cmp::min(buffer.len(), bytes_until_max); let mut written = 0; while written < bytes_to_write { - let mut current_cluster = self.open_files[file_idx].current_cluster; + let mut current_cluster = data.open_files[file_idx].current_cluster; debug!( "Have written bytes {}/{}, finding cluster {:?}", written, bytes_to_write, current_cluster ); - let current_offset = self.open_files[file_idx].current_offset; - let (block_idx, block_offset, block_avail) = match self.find_data_on_disk( + let current_offset = data.open_files[file_idx].current_offset; + let (block_idx, block_offset, block_avail) = match data.find_data_on_disk( + &self.block_device, volume_idx, &mut current_cluster, - self.open_files[file_idx].entry.cluster, + data.open_files[file_idx].entry.cluster, current_offset, ) { Ok(vars) => { @@ -750,7 +740,7 @@ where } Err(Error::EndOfFile) => { debug!("Extending file"); - match self.open_volumes[volume_idx].volume_type { + match data.open_volumes[volume_idx].volume_type { VolumeType::Fat(ref mut fat) => { if fat .alloc_cluster(&self.block_device, Some(current_cluster.1), false) @@ -759,12 +749,13 @@ where return Err(Error::DiskFull); } debug!("Allocated new FAT cluster, finding offsets..."); - let new_offset = self + let new_offset = data .find_data_on_disk( + &self.block_device, volume_idx, &mut current_cluster, - self.open_files[file_idx].entry.cluster, - self.open_files[file_idx].current_offset, + data.open_files[file_idx].entry.cluster, + data.open_files[file_idx].current_offset, ) .map_err(|_| Error::AllocationError)?; debug!("New offset {:?}", new_offset); @@ -790,52 +781,53 @@ where .write(&blocks, block_idx) .map_err(Error::DeviceError)?; written += to_copy; - self.open_files[file_idx].current_cluster = current_cluster; + data.open_files[file_idx].current_cluster = current_cluster; let to_copy = to_copy as u32; - let new_offset = self.open_files[file_idx].current_offset + to_copy; - if new_offset > self.open_files[file_idx].entry.size { + let new_offset = data.open_files[file_idx].current_offset + to_copy; + if new_offset > data.open_files[file_idx].entry.size { // We made it longer - self.open_files[file_idx].update_length(new_offset); + data.open_files[file_idx].update_length(new_offset); } - self.open_files[file_idx] + data.open_files[file_idx] .seek_from_start(new_offset) .unwrap(); // Entry update deferred to file close, for performance. } - self.open_files[file_idx].entry.attributes.set_archive(true); - self.open_files[file_idx].entry.mtime = self.time_source.get_timestamp(); + data.open_files[file_idx].entry.attributes.set_archive(true); + data.open_files[file_idx].entry.mtime = self.time_source.get_timestamp(); Ok(()) } /// Close a file with the given raw file handle. - pub fn close_file(&mut self, file: RawFile) -> Result<(), Error> { + pub fn close_file(&self, file: RawFile) -> Result<(), Error> { let flush_result = self.flush_file(file); - let file_idx = self.get_file_by_id(file)?; - self.open_files.swap_remove(file_idx); + let mut data = self.data.borrow_mut(); + let file_idx = data.get_file_by_id(file)?; + data.open_files.swap_remove(file_idx); flush_result } /// Flush (update the entry) for a file with the given raw file handle. - pub fn flush_file(&mut self, file: RawFile) -> Result<(), Error> { - let file_info = self - .open_files - .iter() - .find(|info| info.raw_file == file) - .ok_or(Error::BadHandle)?; - - if file_info.dirty { - let volume_idx = self.get_volume_by_id(file_info.raw_volume)?; - match self.open_volumes[volume_idx].volume_type { - VolumeType::Fat(ref mut fat) => { + pub fn flush_file(&self, file: RawFile) -> Result<(), Error> { + use core::ops::DerefMut; + let mut data = self.data.borrow_mut(); + let data = data.deref_mut(); + + let file_id = data.get_file_by_id(file)?; + + if data.open_files[file_id].dirty { + let volume_idx = data.get_volume_by_id(data.open_files[file_id].raw_volume)?; + match &mut data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { debug!("Updating FAT info sector"); fat.update_info_sector(&self.block_device)?; - debug!("Updating dir entry {:?}", file_info.entry); - if file_info.entry.size != 0 { + debug!("Updating dir entry {:?}", data.open_files[file_id].entry); + if data.open_files[file_id].entry.size != 0 { // If you have a length, you must have a cluster - assert!(file_info.entry.cluster.0 != 0); + assert!(data.open_files[file_id].entry.cluster.0 != 0); } - fat.write_entry_to_disk(&self.block_device, &file_info.entry)?; + fat.write_entry_to_disk(&self.block_device, &data.open_files[file_id].entry)?; } }; } @@ -844,7 +836,8 @@ where /// Check if any files or folders are open. pub fn has_open_handles(&self) -> bool { - !(self.open_dirs.is_empty() || self.open_files.is_empty()) + let data = self.data.borrow(); + !(data.open_dirs.is_empty() || data.open_files.is_empty()) } /// Consume self and return BlockDevice and TimeSource @@ -854,18 +847,16 @@ where /// Check if a file is at End Of File. pub fn file_eof(&self, file: RawFile) -> Result> { - let file_idx = self.get_file_by_id(file)?; - Ok(self.open_files[file_idx].eof()) + let data = self.data.borrow(); + let file_idx = data.get_file_by_id(file)?; + Ok(data.open_files[file_idx].eof()) } /// Seek a file with an offset from the start of the file. - pub fn file_seek_from_start( - &mut self, - file: RawFile, - offset: u32, - ) -> Result<(), Error> { - let file_idx = self.get_file_by_id(file)?; - self.open_files[file_idx] + pub fn file_seek_from_start(&self, file: RawFile, offset: u32) -> Result<(), Error> { + let mut data = self.data.borrow_mut(); + let file_idx = data.get_file_by_id(file)?; + data.open_files[file_idx] .seek_from_start(offset) .map_err(|_| Error::InvalidOffset)?; Ok(()) @@ -873,25 +864,23 @@ where /// Seek a file with an offset from the current position. pub fn file_seek_from_current( - &mut self, + &self, file: RawFile, offset: i32, ) -> Result<(), Error> { - let file_idx = self.get_file_by_id(file)?; - self.open_files[file_idx] + let mut data = self.data.borrow_mut(); + let file_idx = data.get_file_by_id(file)?; + data.open_files[file_idx] .seek_from_current(offset) .map_err(|_| Error::InvalidOffset)?; Ok(()) } /// Seek a file with an offset back from the end of the file. - pub fn file_seek_from_end( - &mut self, - file: RawFile, - offset: u32, - ) -> Result<(), Error> { - let file_idx = self.get_file_by_id(file)?; - self.open_files[file_idx] + pub fn file_seek_from_end(&self, file: RawFile, offset: u32) -> Result<(), Error> { + let mut data = self.data.borrow_mut(); + let file_idx = data.get_file_by_id(file)?; + data.open_files[file_idx] .seek_from_end(offset) .map_err(|_| Error::InvalidOffset)?; Ok(()) @@ -899,35 +888,41 @@ where /// Get the length of a file pub fn file_length(&self, file: RawFile) -> Result> { - let file_idx = self.get_file_by_id(file)?; - Ok(self.open_files[file_idx].length()) + let data = self.data.borrow(); + let file_idx = data.get_file_by_id(file)?; + Ok(data.open_files[file_idx].length()) } /// Get the current offset of a file pub fn file_offset(&self, file: RawFile) -> Result> { - let file_idx = self.get_file_by_id(file)?; - Ok(self.open_files[file_idx].current_offset) + let data = self.data.borrow(); + let file_idx = data.get_file_by_id(file)?; + Ok(data.open_files[file_idx].current_offset) } /// Create a directory in a given directory. pub fn make_dir_in_dir( - &mut self, + &self, directory: RawDirectory, name: N, ) -> Result<(), Error> where N: ToShortFileName, { + use core::ops::DerefMut; + let mut data = self.data.borrow_mut(); + let data = data.deref_mut(); + // This check is load-bearing - we do an unchecked push later. - if self.open_dirs.is_full() { + if data.open_dirs.is_full() { return Err(Error::TooManyOpenDirs); } - let parent_directory_idx = self.get_dir_by_id(directory)?; - let parent_directory_info = &self.open_dirs[parent_directory_idx]; - let volume_id = self.open_dirs[parent_directory_idx].volume_id; - let volume_idx = self.get_volume_by_id(volume_id)?; - let volume_info = &self.open_volumes[volume_idx]; + let parent_directory_idx = data.get_dir_by_id(directory)?; + let parent_directory_info = &data.open_dirs[parent_directory_idx]; + let volume_id = data.open_dirs[parent_directory_idx].raw_volume; + let volume_idx = data.get_volume_by_id(volume_id)?; + let volume_info = &data.open_volumes[volume_idx]; let sfn = name.to_short_filename().map_err(Error::FilenameError)?; debug!("Creating directory '{}'", sfn); @@ -962,7 +957,7 @@ where let att = Attributes::create_from_fat(Attributes::DIRECTORY); // Need mutable access for this - match &mut self.open_volumes[volume_idx].volume_type { + match &mut data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => { debug!("Making dir entry"); let mut new_dir_entry_in_parent = fat.write_new_directory_entry( @@ -1048,28 +1043,72 @@ where Ok(()) } +} + +/// The mutable data the VolumeManager needs to hold +/// +/// Kept separate so its easier to wrap it in a RefCell +#[derive(Debug)] + +struct VolumeManagerData< + const MAX_DIRS: usize = 4, + const MAX_FILES: usize = 4, + const MAX_VOLUMES: usize = 1, +> { + id_generator: HandleGenerator, + open_volumes: Vec, + open_dirs: Vec, + open_files: Vec, +} + +impl + VolumeManagerData +{ + /// Check if a file is open + /// + /// Returns `true` if it's open, `false`, otherwise. + fn file_is_open(&self, raw_volume: RawVolume, dir_entry: &DirEntry) -> bool { + for f in self.open_files.iter() { + if f.raw_volume == raw_volume + && f.entry.entry_block == dir_entry.entry_block + && f.entry.entry_offset == dir_entry.entry_offset + { + return true; + } + } + false + } - fn get_volume_by_id(&self, volume: RawVolume) -> Result> { + fn get_volume_by_id(&self, raw_volume: RawVolume) -> Result> + where + E: core::fmt::Debug, + { for (idx, v) in self.open_volumes.iter().enumerate() { - if v.raw_volume == volume { + if v.raw_volume == raw_volume { return Ok(idx); } } Err(Error::BadHandle) } - fn get_dir_by_id(&self, directory: RawDirectory) -> Result> { + fn get_dir_by_id(&self, raw_directory: RawDirectory) -> Result> + where + E: core::fmt::Debug, + { for (idx, d) in self.open_dirs.iter().enumerate() { - if d.directory_id == directory { + if d.raw_directory == raw_directory { return Ok(idx); } } Err(Error::BadHandle) } - fn get_file_by_id(&self, file: RawFile) -> Result> { + fn get_file_by_id(&self, raw_file: RawFile) -> Result> + where + E: core::fmt::Debug, + { for (idx, f) in self.open_files.iter().enumerate() { - if f.raw_file == file { + if f.raw_file == raw_file { return Ok(idx); } } @@ -1085,13 +1124,17 @@ where /// * the index for the block on the disk that contains the data we want, /// * the byte offset into that block for the data we want, and /// * how many bytes remain in that block. - fn find_data_on_disk( + fn find_data_on_disk( &self, + block_device: &D, volume_idx: usize, start: &mut (u32, ClusterId), file_start: ClusterId, desired_offset: u32, - ) -> Result<(BlockIdx, usize, usize), Error> { + ) -> Result<(BlockIdx, usize, usize), Error> + where + D: BlockDevice, + { let bytes_per_cluster = match &self.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => fat.bytes_per_cluster(), }; @@ -1110,7 +1153,7 @@ where for _ in 0..num_clusters { start.1 = match &self.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => { - fat.next_cluster(&self.block_device, start.1, &mut block_cache)? + fat.next_cluster(block_device, start.1, &mut block_cache)? } }; start.0 += bytes_per_cluster; @@ -1392,14 +1435,14 @@ mod tests { #[test] fn partition0() { - let mut c: VolumeManager = + let c: VolumeManager = VolumeManager::new_with_limits(DummyBlockDevice, Clock, 0xAA00_0000); let v = c.open_raw_volume(VolumeIdx(0)).unwrap(); let expected_id = RawVolume(Handle(0xAA00_0000)); assert_eq!(v, expected_id); assert_eq!( - &c.open_volumes[0], + &c.data.borrow().open_volumes[0], &VolumeInfo { raw_volume: expected_id, idx: VolumeIdx(0), diff --git a/tests/directories.rs b/tests/directories.rs index df9154f4..1acfb370 100644 --- a/tests/directories.rs +++ b/tests/directories.rs @@ -38,7 +38,7 @@ impl PartialEq for ExpectedDirEntry { fn fat16_root_directory_listing() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let fat16_volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) @@ -100,7 +100,7 @@ fn fat16_root_directory_listing() { fn fat16_sub_directory_listing() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let fat16_volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) @@ -165,7 +165,7 @@ fn fat16_sub_directory_listing() { fn fat32_root_directory_listing() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let fat32_volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) @@ -227,7 +227,7 @@ fn fat32_root_directory_listing() { fn open_dir_twice() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let fat32_volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) @@ -267,7 +267,7 @@ fn open_dir_twice() { fn open_too_many_dirs() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr: embedded_sdmmc::VolumeManager< + let volume_mgr: embedded_sdmmc::VolumeManager< utils::RamDisk>, utils::TestTimeSource, 1, @@ -292,7 +292,7 @@ fn open_too_many_dirs() { fn find_dir_entry() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let fat32_volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) @@ -322,7 +322,7 @@ fn find_dir_entry() { fn delete_file() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let fat32_volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) @@ -367,7 +367,7 @@ fn delete_file() { fn make_directory() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let fat32_volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) diff --git a/tests/open_files.rs b/tests/open_files.rs index 10e21818..6b927bc0 100644 --- a/tests/open_files.rs +++ b/tests/open_files.rs @@ -8,7 +8,7 @@ mod utils; fn open_files() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = + let volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = VolumeManager::new_with_limits(disk, time_source, 0xAA00_0000); let volume = volume_mgr .open_raw_volume(VolumeIdx(0)) @@ -95,11 +95,11 @@ fn open_files() { fn open_non_raw() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = + let volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = VolumeManager::new_with_limits(disk, time_source, 0xAA00_0000); - let mut volume = volume_mgr.open_volume(VolumeIdx(0)).expect("open volume"); - let mut root_dir = volume.open_root_dir().expect("open root dir"); - let mut f = root_dir + let volume = volume_mgr.open_volume(VolumeIdx(0)).expect("open volume"); + let root_dir = volume.open_root_dir().expect("open root dir"); + let f = root_dir .open_file_in_dir("README.TXT", Mode::ReadOnly) .expect("open file"); diff --git a/tests/read_file.rs b/tests/read_file.rs index b7c80c95..904964f3 100644 --- a/tests/read_file.rs +++ b/tests/read_file.rs @@ -11,7 +11,7 @@ static TEST_DAT_SHA256_SUM: &[u8] = fn read_file_512_blocks() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let fat16_volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) @@ -53,7 +53,7 @@ fn read_file_512_blocks() { fn read_file_all() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let fat16_volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) @@ -87,7 +87,7 @@ fn read_file_all() { fn read_file_prime_blocks() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let fat16_volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) @@ -130,7 +130,7 @@ fn read_file_prime_blocks() { fn read_file_backwards() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let fat16_volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) @@ -188,13 +188,13 @@ fn read_file_backwards() { fn read_file_with_odd_seek() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); - let mut volume = volume_mgr + let volume = volume_mgr .open_volume(embedded_sdmmc::VolumeIdx(0)) .unwrap(); - let mut root_dir = volume.open_root_dir().unwrap(); - let mut f = root_dir + let root_dir = volume.open_root_dir().unwrap(); + let f = root_dir .open_file_in_dir("64MB.DAT", embedded_sdmmc::Mode::ReadOnly) .unwrap(); f.seek_from_start(0x2c).unwrap(); diff --git a/tests/volume.rs b/tests/volume.rs index 6ef05dff..c2ea49b9 100644 --- a/tests/volume.rs +++ b/tests/volume.rs @@ -6,7 +6,7 @@ mod utils; fn open_all_volumes() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr: embedded_sdmmc::VolumeManager< + let volume_mgr: embedded_sdmmc::VolumeManager< utils::RamDisk>, utils::TestTimeSource, 4, @@ -82,7 +82,7 @@ fn open_all_volumes() { fn close_volume_too_early() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + let volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); let volume = volume_mgr .open_raw_volume(embedded_sdmmc::VolumeIdx(0)) diff --git a/tests/write_file.rs b/tests/write_file.rs index 97e7bcdc..af178145 100644 --- a/tests/write_file.rs +++ b/tests/write_file.rs @@ -8,7 +8,7 @@ mod utils; fn append_file() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = + let volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = VolumeManager::new_with_limits(disk, time_source, 0xAA00_0000); let volume = volume_mgr .open_raw_volume(VolumeIdx(0)) @@ -59,7 +59,7 @@ fn append_file() { fn flush_file() { let time_source = utils::make_time_source(); let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); - let mut volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = + let volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = VolumeManager::new_with_limits(disk, time_source, 0xAA00_0000); let volume = volume_mgr .open_raw_volume(VolumeIdx(0)) From e9ec42331c44f1b14163d161b91ffe10211d3ee2 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Fri, 11 Oct 2024 22:26:49 +0200 Subject: [PATCH 11/62] Return an error on lock failure. --- src/lib.rs | 7 ++++++- src/volume_mgr.rs | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 27d54f6c..c1a610f9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -202,6 +202,10 @@ where DiskFull, /// A directory with that name already exists DirAlreadyExists, + /// The filesystem tried to gain a lock whilst already locked. + /// + /// This is a bug in the filesystem. Please open an issue. + LockError, } impl embedded_io::Error for Error { @@ -216,7 +220,8 @@ impl embedded_io::Error for Error { | Error::EndOfFile | Error::DiskFull | Error::NotEnoughSpace - | Error::AllocationError => ErrorKind::Other, + | Error::AllocationError + | Error::LockError => ErrorKind::Other, Error::NoSuchVolume | Error::FilenameError(_) | Error::BadHandle diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 4e5f56d4..34227bc7 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -136,7 +136,7 @@ where const PARTITION_INFO_LBA_START_INDEX: usize = 8; const PARTITION_INFO_NUM_BLOCKS_INDEX: usize = 12; - let mut data = self.data.borrow_mut(); + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; if data.open_volumes.is_full() { return Err(Error::TooManyOpenVolumes); @@ -220,7 +220,7 @@ where pub fn open_root_dir(&self, volume: RawVolume) -> Result> { // Opening a root directory twice is OK - let mut data = self.data.borrow_mut(); + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; let directory_id = RawDirectory(data.id_generator.generate()); let dir_info = DirectoryInfo { @@ -249,7 +249,7 @@ where where N: ToShortFileName, { - let mut data = self.data.borrow_mut(); + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; if data.open_dirs.is_full() { return Err(Error::TooManyOpenDirs); @@ -312,7 +312,7 @@ where /// Close a directory. You cannot perform operations on an open directory /// and so must close it if you want to do something with it. pub fn close_dir(&self, directory: RawDirectory) -> Result<(), Error> { - let mut data = self.data.borrow_mut(); + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; for (idx, info) in data.open_dirs.iter().enumerate() { if directory == info.raw_directory { @@ -327,7 +327,7 @@ where /// /// You can't close it if there are any files or directories open on it. pub fn close_volume(&self, volume: RawVolume) -> Result<(), Error> { - let mut data = self.data.borrow_mut(); + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; for f in data.open_files.iter() { if f.raw_volume == volume { @@ -395,7 +395,7 @@ where where N: ToShortFileName, { - let mut data = self.data.borrow_mut(); + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; // This check is load-bearing - we do an unchecked push later. if data.open_files.is_full() { @@ -627,7 +627,7 @@ where /// Read from an open file. pub fn read(&self, file: RawFile, buffer: &mut [u8]) -> Result> { - let mut data = self.data.borrow_mut(); + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; let file_idx = data.get_file_by_id(file)?; let volume_idx = data.get_volume_by_id(data.open_files[file_idx].raw_volume)?; @@ -676,7 +676,7 @@ where #[cfg(feature = "log")] debug!("write(file={:?}, buffer={:x?}", file, buffer); - let mut data = self.data.borrow_mut(); + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; // Clone this so we can touch our other structures. Need to ensure we // write it back at the end. @@ -802,7 +802,7 @@ where /// Close a file with the given raw file handle. pub fn close_file(&self, file: RawFile) -> Result<(), Error> { let flush_result = self.flush_file(file); - let mut data = self.data.borrow_mut(); + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; let file_idx = data.get_file_by_id(file)?; data.open_files.swap_remove(file_idx); flush_result @@ -811,7 +811,7 @@ where /// Flush (update the entry) for a file with the given raw file handle. pub fn flush_file(&self, file: RawFile) -> Result<(), Error> { use core::ops::DerefMut; - let mut data = self.data.borrow_mut(); + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; let data = data.deref_mut(); let file_id = data.get_file_by_id(file)?; @@ -854,7 +854,7 @@ where /// Seek a file with an offset from the start of the file. pub fn file_seek_from_start(&self, file: RawFile, offset: u32) -> Result<(), Error> { - let mut data = self.data.borrow_mut(); + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; let file_idx = data.get_file_by_id(file)?; data.open_files[file_idx] .seek_from_start(offset) @@ -868,7 +868,7 @@ where file: RawFile, offset: i32, ) -> Result<(), Error> { - let mut data = self.data.borrow_mut(); + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; let file_idx = data.get_file_by_id(file)?; data.open_files[file_idx] .seek_from_current(offset) @@ -878,7 +878,7 @@ where /// Seek a file with an offset back from the end of the file. pub fn file_seek_from_end(&self, file: RawFile, offset: u32) -> Result<(), Error> { - let mut data = self.data.borrow_mut(); + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; let file_idx = data.get_file_by_id(file)?; data.open_files[file_idx] .seek_from_end(offset) @@ -910,7 +910,7 @@ where N: ToShortFileName, { use core::ops::DerefMut; - let mut data = self.data.borrow_mut(); + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; let data = data.deref_mut(); // This check is load-bearing - we do an unchecked push later. From 870ed18d2adb9620d5fecc209551a6a11dcf620a Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Fri, 11 Oct 2024 23:36:05 +0200 Subject: [PATCH 12/62] Simplify the shell example. Now we can open multiple files at once, we don't need to be so careful in our tree code. --- examples/shell.rs | 85 ++++++++++++++++++++--------------------------- 1 file changed, 36 insertions(+), 49 deletions(-) diff --git a/examples/shell.rs b/examples/shell.rs index 7822dde8..54f713de 100644 --- a/examples/shell.rs +++ b/examples/shell.rs @@ -69,11 +69,12 @@ //! | `B:/BACKUP.000/NAMES.CSV` | `B:` | Yes | `[BACKUP.000]` | `NAMES.CSV` | `B:/BACKUP.000/NAMES.CSV` | //! | `B:../BACKUP.000/NAMES.CSV` | `B:` | No | `[.., BACKUP.000]` | `NAMES.CSV` | `B:/BACKUP.000/NAMES.CSV` | -use std::io::prelude::*; +use std::{cell::RefCell, io::prelude::*}; -use embedded_sdmmc::{ - Error as EsError, RawDirectory, RawVolume, ShortFileName, VolumeIdx, VolumeManager, -}; +use embedded_sdmmc::{Error as EsError, RawDirectory, RawVolume, ShortFileName, VolumeIdx}; + +type VolumeManager = embedded_sdmmc::VolumeManager; +type Directory<'a> = embedded_sdmmc::Directory<'a, LinuxBlockDevice, Clock, 8, 8, 4>; use crate::linux::{Clock, LinuxBlockDevice}; @@ -182,21 +183,21 @@ struct VolumeState { } struct Context { - volume_mgr: VolumeManager, - volumes: [Option; 4], + volume_mgr: VolumeManager, + volumes: RefCell<[Option; 4]>, current_volume: usize, } impl Context { fn current_path(&self) -> Vec { - let Some(s) = &self.volumes[self.current_volume] else { + let Some(s) = &self.volumes.borrow()[self.current_volume] else { return vec![]; }; s.path.clone() } /// Print some help text - fn help(&mut self) -> Result<(), Error> { + fn help(&self) -> Result<(), Error> { println!("Commands:"); println!("\thelp -> this help text"); println!("\t: -> change volume/partition"); @@ -219,16 +220,15 @@ impl Context { } /// Print volume manager status - fn stat(&mut self) -> Result<(), Error> { + fn stat(&self) -> Result<(), Error> { println!("Status:\n{:#?}", self.volume_mgr); Ok(()) } /// Print a directory listing - fn dir(&mut self, path: &Path) -> Result<(), Error> { + fn dir(&self, path: &Path) -> Result<(), Error> { println!("Directory listing of {:?}", path); let dir = self.resolve_existing_directory(path)?; - let dir = dir.to_directory(&mut self.volume_mgr); dir.iterate_dir(|entry| { if !entry.attributes.is_volume() && !entry.attributes.is_lfn() { println!( @@ -246,7 +246,7 @@ impl Context { } /// Print a recursive directory listing for the given path - fn tree(&mut self, path: &Path) -> Result<(), Error> { + fn tree(&self, path: &Path) -> Result<(), Error> { println!("Directory listing of {:?}", path); let dir = self.resolve_existing_directory(path)?; // tree_dir will close this directory, always @@ -256,8 +256,7 @@ impl Context { /// Print a recursive directory listing for the given open directory. /// /// Will close the given directory. - fn tree_dir(&mut self, dir: RawDirectory) -> Result<(), Error> { - let dir = dir.to_directory(&mut self.volume_mgr); + fn tree_dir<'a>(&'a self, dir: Directory<'a>) -> Result<(), Error> { let mut children = Vec::new(); dir.iterate_dir(|entry| { println!( @@ -271,25 +270,20 @@ impl Context { children.push(entry.name.clone()); } })?; - // Be sure to close this, no matter what happens - let dir = dir.to_raw_directory(); for child in children { println!("Entering {}", child); - let child_dir = match self.volume_mgr.open_dir(dir, &child) { + let child_dir = match dir.open_dir(&child) { Ok(child_dir) => child_dir, Err(e) => { - self.volume_mgr.close_dir(dir).expect("close open dir"); return Err(e); } }; let result = self.tree_dir(child_dir); println!("Returning from {}", child); if let Err(e) = result { - self.volume_mgr.close_dir(dir).expect("close open dir"); return Err(e); } } - self.volume_mgr.close_dir(dir).expect("close open dir"); Ok(()) } @@ -300,17 +294,17 @@ impl Context { /// sub-folder, starting from the current directory on the current volume /// * An absolute path like `B:/FOO` changes the CWD on Volume 1 to path /// `/FOO` - fn cd(&mut self, full_path: &Path) -> Result<(), Error> { + fn cd(&self, full_path: &Path) -> Result<(), Error> { let volume_idx = self.resolve_volume(full_path)?; - let d = self.resolve_existing_directory(full_path)?; - let Some(s) = &mut self.volumes[volume_idx] else { - self.volume_mgr.close_dir(d).expect("close open dir"); + let (mut d, fragment) = self.resolve_filename(full_path)?; + d.change_dir(fragment)?; + let Some(s) = &mut self.volumes.borrow_mut()[volume_idx] else { return Err(Error::NoSuchVolume); }; self.volume_mgr .close_dir(s.directory) .expect("close open dir"); - s.directory = d; + s.directory = d.to_raw_directory(); if full_path.is_absolute() { s.path.clear(); } @@ -327,9 +321,8 @@ impl Context { } /// print a text file - fn cat(&mut self, filename: &Path) -> Result<(), Error> { + fn cat(&self, filename: &Path) -> Result<(), Error> { let (dir, filename) = self.resolve_filename(filename)?; - let dir = dir.to_directory(&mut self.volume_mgr); let f = dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadOnly)?; let mut data = Vec::new(); while !f.is_eof() { @@ -348,9 +341,8 @@ impl Context { } /// print a binary file - fn hexdump(&mut self, filename: &Path) -> Result<(), Error> { + fn hexdump(&self, filename: &Path) -> Result<(), Error> { let (dir, filename) = self.resolve_filename(filename)?; - let dir = dir.to_directory(&mut self.volume_mgr); let f = dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadOnly)?; let mut data = Vec::new(); while !f.is_eof() { @@ -385,9 +377,8 @@ impl Context { } /// create a directory - fn mkdir(&mut self, dir_name: &Path) -> Result<(), Error> { + fn mkdir(&self, dir_name: &Path) -> Result<(), Error> { let (dir, filename) = self.resolve_filename(dir_name)?; - let dir = dir.to_directory(&mut self.volume_mgr); dir.make_dir_in_dir(filename) } @@ -435,11 +426,10 @@ impl Context { /// * Relative names, like `../SOMEDIR` or `./SOMEDIR`, traverse /// starting at the current volume and directory. /// * Absolute, like `B:/SOMEDIR/OTHERDIR` start at the given volume. - fn resolve_existing_directory(&mut self, full_path: &Path) -> Result { - let (dir, fragment) = self.resolve_filename(full_path)?; - let mut work_dir = dir.to_directory(&mut self.volume_mgr); - work_dir.change_dir(fragment)?; - Ok(work_dir.to_raw_directory()) + fn resolve_existing_directory<'a>(&'a self, full_path: &Path) -> Result, Error> { + let (mut dir, fragment) = self.resolve_filename(full_path)?; + dir.change_dir(fragment)?; + Ok(dir) } /// Either get the volume from the path, or pick the current volume. @@ -464,33 +454,30 @@ impl Context { /// * Relative names, like `../SOMEDIR/SOMEFILE` or `./SOMEDIR/SOMEFILE`, traverse /// starting at the current volume and directory. /// * Absolute, like `B:/SOMEDIR/SOMEFILE` start at the given volume. - fn resolve_filename<'path>( - &mut self, + fn resolve_filename<'a, 'path>( + &'a self, full_path: &'path Path, - ) -> Result<(RawDirectory, &'path str), Error> { + ) -> Result<(Directory<'a>, &'path str), Error> { let volume_idx = self.resolve_volume(full_path)?; - let Some(s) = &mut self.volumes[volume_idx] else { + let Some(s) = &self.volumes.borrow()[volume_idx] else { return Err(Error::NoSuchVolume); }; let mut work_dir = if full_path.is_absolute() { // relative to root self.volume_mgr .open_root_dir(s.volume)? - .to_directory(&mut self.volume_mgr) + .to_directory(&self.volume_mgr) } else { // relative to CWD self.volume_mgr .open_dir(s.directory, ".")? - .to_directory(&mut self.volume_mgr) + .to_directory(&self.volume_mgr) }; for fragment in full_path.iterate_dirs() { work_dir.change_dir(fragment)?; } - Ok(( - work_dir.to_raw_directory(), - full_path.basename().unwrap_or("."), - )) + Ok((work_dir, full_path.basename().unwrap_or("."))) } /// Convert a volume index to a letter @@ -507,7 +494,7 @@ impl Context { impl Drop for Context { fn drop(&mut self) { - for v in self.volumes.iter_mut() { + for v in self.volumes.borrow_mut().iter_mut() { if let Some(v) = v { println!("Closing directory {:?}", v.directory); self.volume_mgr @@ -534,7 +521,7 @@ fn main() -> Result<(), Error> { let mut ctx = Context { volume_mgr: VolumeManager::new_with_limits(lbd, Clock, 100), - volumes: [None, None, None, None], + volumes: RefCell::new([None, None, None, None]), current_volume: 0, }; @@ -549,7 +536,7 @@ fn main() -> Result<(), Error> { ); match ctx.volume_mgr.open_root_dir(volume) { Ok(root_dir) => { - ctx.volumes[volume_no] = Some(VolumeState { + ctx.volumes.borrow_mut()[volume_no] = Some(VolumeState { directory: root_dir, volume, path: vec![], From b6b5bd52b59f18048042f9c91c7f69c4cf26ce23 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 12 Oct 2024 17:39:04 +0100 Subject: [PATCH 13/62] Fixed resource leak reading volume labels. --- src/volume_mgr.rs | 53 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 34227bc7..9f9f218d 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -218,8 +218,9 @@ where /// You can then read the directory entries with `iterate_dir`, or you can /// use `open_file_in_dir`. pub fn open_root_dir(&self, volume: RawVolume) -> Result> { - // Opening a root directory twice is OK + debug!("Opening root on {:?}", volume); + // Opening a root directory twice is OK let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; let directory_id = RawDirectory(data.id_generator.generate()); @@ -233,6 +234,8 @@ where .push(dir_info) .map_err(|_| Error::TooManyOpenDirs)?; + debug!("Opened root on {:?}, got {:?}", volume, directory_id); + Ok(directory_id) } @@ -312,6 +315,7 @@ where /// Close a directory. You cannot perform operations on an open directory /// and so must close it if you want to do something with it. pub fn close_dir(&self, directory: RawDirectory) -> Result<(), Error> { + debug!("Closing {:?}", directory); let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; for (idx, info) in data.open_dirs.iter().enumerate() { @@ -600,28 +604,47 @@ where Ok(()) } - /// Search the root directory for a volume label + /// Get the volume label + /// + /// Will look in the BPB for a volume label, and if nothing is found, will + /// search the root directory for a volume label. pub fn get_root_volume_label( &self, - volume: RawVolume, + raw_volume: RawVolume, ) -> Result, Error> { - let directory = self.open_root_dir(volume)?; + debug!("Reading volume label for {:?}", raw_volume); + // prefer the one in the BPB - it's easier to get let data = self.data.borrow(); - // this can't fail - we literally just opened it - let directory_idx = data - .get_dir_by_id::(directory) - .expect("Dir ID error"); - let volume_idx = data.get_volume_by_id(data.open_dirs[directory_idx].raw_volume)?; - let mut maybe_volume_name = None; + let volume_idx = data.get_volume_by_id(raw_volume)?; match &data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => { - fat.iterate_dir(&self.block_device, &data.open_dirs[directory_idx], |de| { - if de.attributes == Attributes::create_from_fat(Attributes::VOLUME) { - maybe_volume_name = Some(unsafe { de.name.clone().to_volume_label() }) - } - })?; + if !fat.name.name().is_empty() { + debug!( + "Got volume label {:?} for {:?} from BPB", + fat.name, raw_volume + ); + return Ok(Some(fat.name.clone())); + } } } + drop(data); + + // Nothing in the BPB, let's do it the slow way + let root_dir = self.open_root_dir(raw_volume)?.to_directory(self); + let mut maybe_volume_name = None; + root_dir.iterate_dir(|de| { + if maybe_volume_name.is_none() + && de.attributes == Attributes::create_from_fat(Attributes::VOLUME) + { + maybe_volume_name = Some(unsafe { de.name.clone().to_volume_label() }) + } + })?; + + debug!( + "Got volume label {:?} for {:?} from root", + maybe_volume_name, raw_volume + ); + Ok(maybe_volume_name) } From 41cbb6250bb22864846144b88bae8d8754cedb4f Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 12 Oct 2024 17:39:13 +0100 Subject: [PATCH 14/62] Make ClusterId print nicer. --- src/filesystem/cluster.rs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/filesystem/cluster.rs b/src/filesystem/cluster.rs index 34d85909..bcf6eb0b 100644 --- a/src/filesystem/cluster.rs +++ b/src/filesystem/cluster.rs @@ -3,7 +3,7 @@ /// A cluster is a consecutive group of blocks. Each cluster has a a numeric ID. /// Some numeric IDs are reserved for special purposes. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] -#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] +#[derive(Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] pub struct ClusterId(pub(crate) u32); impl ClusterId { @@ -33,6 +33,34 @@ impl core::ops::AddAssign for ClusterId { } } +impl core::fmt::Debug for ClusterId { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "ClusterId(")?; + match *self { + Self::INVALID => { + write!(f, "INVALID")?; + } + Self::BAD => { + write!(f, "BAD")?; + } + Self::EMPTY => { + write!(f, "EMPTY")?; + } + Self::ROOT_DIR => { + write!(f, "ROOT_DIR")?; + } + Self::END_OF_FILE => { + write!(f, "END_OF_FILE")?; + } + ClusterId(value) => { + write!(f, "{:#08x}", value)?; + } + } + write!(f, ")")?; + Ok(()) + } +} + // **************************************************************************** // // End Of File From f5bafa51937fb5d905e829012cada42d0f1521a9 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 12 Oct 2024 17:41:08 +0100 Subject: [PATCH 15/62] Ensure DirectoryInfo values are called dir_info --- src/fat/volume.rs | 50 +++++++++++++++++++++++------------------------ src/volume_mgr.rs | 5 ++++- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/fat/volume.rs b/src/fat/volume.rs index 580c9bba..a05b6f67 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -538,7 +538,7 @@ impl FatVolume { pub(crate) fn iterate_dir( &self, block_device: &D, - dir: &DirectoryInfo, + dir_info: &DirectoryInfo, func: F, ) -> Result<(), Error> where @@ -547,17 +547,17 @@ impl FatVolume { { match &self.fat_specific_info { FatSpecificInfo::Fat16(fat16_info) => { - self.iterate_fat16(dir, fat16_info, block_device, func) + self.iterate_fat16(dir_info, fat16_info, block_device, func) } FatSpecificInfo::Fat32(fat32_info) => { - self.iterate_fat32(dir, fat32_info, block_device, func) + self.iterate_fat32(dir_info, fat32_info, block_device, func) } } } fn iterate_fat16( &self, - dir: &DirectoryInfo, + dir_info: &DirectoryInfo, fat16_info: &Fat16Info, block_device: &D, mut func: F, @@ -570,12 +570,12 @@ impl FatVolume { // a specially reserved space on disk (see // `first_root_dir_block`). Other directories can have any size // as they are made of regular clusters. - let mut current_cluster = Some(dir.cluster); - let mut first_dir_block_num = match dir.cluster { + let mut current_cluster = Some(dir_info.cluster); + let mut first_dir_block_num = match dir_info.cluster { ClusterId::ROOT_DIR => self.lba_start + fat16_info.first_root_dir_block, - _ => self.cluster_to_block(dir.cluster), + _ => self.cluster_to_block(dir_info.cluster), }; - let dir_size = match dir.cluster { + let dir_size = match dir_info.cluster { ClusterId::ROOT_DIR => { let len_bytes = u32::from(fat16_info.root_entries_count) * OnDiskDirEntry::LEN_U32; BlockCount::from_bytes(len_bytes) @@ -618,7 +618,7 @@ impl FatVolume { fn iterate_fat32( &self, - dir: &DirectoryInfo, + dir_info: &DirectoryInfo, fat32_info: &Fat32Info, block_device: &D, mut func: F, @@ -629,9 +629,9 @@ impl FatVolume { { // All directories on FAT32 have a cluster chain but the root // dir starts in a specified cluster. - let mut current_cluster = match dir.cluster { + let mut current_cluster = match dir_info.cluster { ClusterId::ROOT_DIR => Some(fat32_info.first_root_dir_cluster), - _ => Some(dir.cluster), + _ => Some(dir_info.cluster), }; let mut blocks = [Block::new()]; let mut block_cache = BlockCache::empty(); @@ -669,7 +669,7 @@ impl FatVolume { pub(crate) fn find_directory_entry( &self, block_device: &D, - dir: &DirectoryInfo, + dir_info: &DirectoryInfo, match_name: &ShortFileName, ) -> Result> where @@ -681,12 +681,12 @@ impl FatVolume { // a specially reserved space on disk (see // `first_root_dir_block`). Other directories can have any size // as they are made of regular clusters. - let mut current_cluster = Some(dir.cluster); - let mut first_dir_block_num = match dir.cluster { + let mut current_cluster = Some(dir_info.cluster); + let mut first_dir_block_num = match dir_info.cluster { ClusterId::ROOT_DIR => self.lba_start + fat16_info.first_root_dir_block, - _ => self.cluster_to_block(dir.cluster), + _ => self.cluster_to_block(dir_info.cluster), }; - let dir_size = match dir.cluster { + let dir_size = match dir_info.cluster { ClusterId::ROOT_DIR => { let len_bytes = u32::from(fat16_info.root_entries_count) * OnDiskDirEntry::LEN_U32; @@ -724,9 +724,9 @@ impl FatVolume { Err(Error::NotFound) } FatSpecificInfo::Fat32(fat32_info) => { - let mut current_cluster = match dir.cluster { + let mut current_cluster = match dir_info.cluster { ClusterId::ROOT_DIR => Some(fat32_info.first_root_dir_cluster), - _ => Some(dir.cluster), + _ => Some(dir_info.cluster), }; let mut block_cache = BlockCache::empty(); while let Some(cluster) = current_cluster { @@ -788,7 +788,7 @@ impl FatVolume { pub(crate) fn delete_directory_entry( &self, block_device: &D, - dir: &DirectoryInfo, + dir_info: &DirectoryInfo, match_name: &ShortFileName, ) -> Result<(), Error> where @@ -800,12 +800,12 @@ impl FatVolume { // a specially reserved space on disk (see // `first_root_dir_block`). Other directories can have any size // as they are made of regular clusters. - let mut current_cluster = Some(dir.cluster); - let mut first_dir_block_num = match dir.cluster { + let mut current_cluster = Some(dir_info.cluster); + let mut first_dir_block_num = match dir_info.cluster { ClusterId::ROOT_DIR => self.lba_start + fat16_info.first_root_dir_block, - _ => self.cluster_to_block(dir.cluster), + _ => self.cluster_to_block(dir_info.cluster), }; - let dir_size = match dir.cluster { + let dir_size = match dir_info.cluster { ClusterId::ROOT_DIR => { let len_bytes = u32::from(fat16_info.root_entries_count) * OnDiskDirEntry::LEN_U32; @@ -849,9 +849,9 @@ impl FatVolume { FatSpecificInfo::Fat32(fat32_info) => { // Root directories on FAT32 start at a specified cluster, but // they can have any length. - let mut current_cluster = match dir.cluster { + let mut current_cluster = match dir_info.cluster { ClusterId::ROOT_DIR => Some(fat32_info.first_root_dir_cluster), - _ => Some(dir.cluster), + _ => Some(dir_info.cluster), }; // Walk the directory while let Some(cluster) = current_cluster { diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 9f9f218d..3098d690 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -264,8 +264,9 @@ where let short_file_name = name.to_short_filename().map_err(Error::FilenameError)?; // Open the directory + + // Should we short-cut? (root dir doesn't have ".") if short_file_name == ShortFileName::this_dir() { - // short-cut (root dir doesn't have ".") let directory_id = RawDirectory(data.id_generator.generate()); let dir_info = DirectoryInfo { raw_directory: directory_id, @@ -280,6 +281,8 @@ where return Ok(directory_id); } + // ok we'll actually look for the directory then + let dir_entry = match &data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => fat.find_directory_entry( &self.block_device, From d2039d45fbb8e795212f328c1ab5f107f51719a5 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 12 Oct 2024 17:41:25 +0100 Subject: [PATCH 16/62] Clean up shell example Makes clippy happy --- examples/shell.rs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/examples/shell.rs b/examples/shell.rs index 54f713de..341bb51a 100644 --- a/examples/shell.rs +++ b/examples/shell.rs @@ -250,13 +250,13 @@ impl Context { println!("Directory listing of {:?}", path); let dir = self.resolve_existing_directory(path)?; // tree_dir will close this directory, always - self.tree_dir(dir) + Self::tree_dir(dir) } /// Print a recursive directory listing for the given open directory. /// /// Will close the given directory. - fn tree_dir<'a>(&'a self, dir: Directory<'a>) -> Result<(), Error> { + fn tree_dir(dir: Directory) -> Result<(), Error> { let mut children = Vec::new(); dir.iterate_dir(|entry| { println!( @@ -272,17 +272,9 @@ impl Context { })?; for child in children { println!("Entering {}", child); - let child_dir = match dir.open_dir(&child) { - Ok(child_dir) => child_dir, - Err(e) => { - return Err(e); - } - }; - let result = self.tree_dir(child_dir); + let child_dir = dir.open_dir(&child)?; + Self::tree_dir(child_dir)?; println!("Returning from {}", child); - if let Err(e) = result { - return Err(e); - } } Ok(()) } From 413d81459715ef8b060db02a0f79fd6c7625bfb0 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 12 Oct 2024 17:48:42 +0100 Subject: [PATCH 17/62] Better notes about double locking. --- src/filesystem/directory.rs | 8 ++++++++ src/lib.rs | 3 ++- src/volume_mgr.rs | 8 ++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/filesystem/directory.rs b/src/filesystem/directory.rs index 46dad489..5cfc5f07 100644 --- a/src/filesystem/directory.rs +++ b/src/filesystem/directory.rs @@ -144,6 +144,14 @@ where } /// Call a callback function for each directory entry in a directory. + /// + ///
+ /// + /// Do not attempt to call any methods on the VolumeManager or any of its + /// handles from inside the callback. You will get a lock error because the + /// object is already locked in order to do the iteration. + /// + ///
pub fn iterate_dir(&self, func: F) -> Result<(), Error> where F: FnMut(&DirEntry), diff --git a/src/lib.rs b/src/lib.rs index c1a610f9..901101df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -204,7 +204,8 @@ where DirAlreadyExists, /// The filesystem tried to gain a lock whilst already locked. /// - /// This is a bug in the filesystem. Please open an issue. + /// This is either a bug in the filesystem, or you tried to access the + /// filesystem API from inside a directory iterator (that isn't allowed). LockError, } diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 3098d690..130e3092 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -377,6 +377,14 @@ where } /// Call a callback function for each directory entry in a directory. + /// + ///
+ /// + /// Do not attempt to call any methods on the VolumeManager or any of its + /// handles from inside the callback. You will get a lock error because the + /// object is already locked in order to do the iteration. + /// + ///
pub fn iterate_dir(&self, directory: RawDirectory, func: F) -> Result<(), Error> where F: FnMut(&DirEntry), From c12c7ee9f2b1dbcd42dba1c8ac550c854a204991 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 12 Oct 2024 18:13:37 +0100 Subject: [PATCH 18/62] Better trace logs. --- src/fat/volume.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/fat/volume.rs b/src/fat/volume.rs index a05b6f67..77067514 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -221,7 +221,7 @@ impl FatVolume { let fat_offset = cluster.0 * 2; this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; - trace!("Reading FAT"); + trace!("Reading FAT for update"); block_device .read(&mut blocks, this_fat_block_num) .map_err(Error::DeviceError)?; @@ -243,7 +243,7 @@ impl FatVolume { let fat_offset = cluster.0 * 4; this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; - trace!("Reading FAT"); + trace!("Reading FAT for update"); block_device .read(&mut blocks, this_fat_block_num) .map_err(Error::DeviceError)?; @@ -263,7 +263,7 @@ impl FatVolume { ); } } - trace!("Writing FAT"); + trace!("Updating FAT"); block_device .write(&blocks, this_fat_block_num) .map_err(Error::DeviceError)?; @@ -288,7 +288,7 @@ impl FatVolume { let fat_offset = cluster.0 * 2; let this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; - trace!("Reading FAT"); + trace!("Walkng FAT"); let block = fat_block_cache.read(block_device, this_fat_block_num)?; let fat_entry = LittleEndian::read_u16(&block[this_fat_ent_offset..=this_fat_ent_offset + 1]); @@ -311,7 +311,7 @@ impl FatVolume { let fat_offset = cluster.0 * 4; let this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; - trace!("Reading FAT"); + trace!("Walking FAT"); let block = fat_block_cache.read(block_device, this_fat_block_num)?; let fat_entry = LittleEndian::read_u32(&block[this_fat_ent_offset..=this_fat_ent_offset + 3]) @@ -586,7 +586,7 @@ impl FatVolume { let mut block_cache = BlockCache::empty(); while let Some(cluster) = current_cluster { for block_idx in first_dir_block_num.range(dir_size) { - trace!("Reading FAT"); + trace!("Reading directory"); let block = block_cache.read(block_device, block_idx)?; for (i, dir_entry_bytes) in block.chunks_exact(OnDiskDirEntry::LEN).enumerate() { let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); @@ -638,7 +638,7 @@ impl FatVolume { while let Some(cluster) = current_cluster { let block_idx = self.cluster_to_block(cluster); for block in block_idx.range(BlockCount(u32::from(self.blocks_per_cluster))) { - trace!("Reading FAT"); + trace!("Reading directory"); block_device .read(&mut blocks, block) .map_err(Error::DeviceError)?; From 92b0c6a706714817ba65934baf8e0b0ada1b7cb4 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Wed, 16 Oct 2024 21:45:40 +0100 Subject: [PATCH 19/62] Re-organise the blockdevice.rs file. --- src/blockdevice.rs | 184 ++++++++++++++++++++++----------------------- 1 file changed, 92 insertions(+), 92 deletions(-) diff --git a/src/blockdevice.rs b/src/blockdevice.rs index 5460e3d2..e151d1b1 100644 --- a/src/blockdevice.rs +++ b/src/blockdevice.rs @@ -16,40 +16,6 @@ pub struct Block { pub contents: [u8; Block::LEN], } -/// The linear numeric address of a block (or sector). -/// -/// The first block on a disk gets `BlockIdx(0)` (which usually contains the -/// Master Boot Record). -#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct BlockIdx(pub u32); - -/// The a number of blocks (or sectors). -/// -/// Add this to a `BlockIdx` to get an actual address on disk. -#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct BlockCount(pub u32); - -/// An iterator returned from `Block::range`. -pub struct BlockIter { - inclusive_end: BlockIdx, - current: BlockIdx, -} - -/// A block device - a device which can read and write blocks (or -/// sectors). Only supports devices which are <= 2 TiB in size. -pub trait BlockDevice { - /// The errors that the `BlockDevice` can return. Must be debug formattable. - type Error: core::fmt::Debug; - /// Read one or more blocks, starting at the given block index. - fn read(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Self::Error>; - /// Write one or more blocks, starting at the given block index. - fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error>; - /// Determine how many blocks this device can hold. - fn num_blocks(&self) -> Result; -} - impl Block { /// All our blocks are a fixed length of 512 bytes. We do not support /// 'Advanced Format' Hard Drives with 4 KiB blocks, nor weird old @@ -67,64 +33,6 @@ impl Block { } } -impl Default for Block { - fn default() -> Self { - Self::new() - } -} - -impl core::ops::Add for BlockIdx { - type Output = BlockIdx; - fn add(self, rhs: BlockCount) -> BlockIdx { - BlockIdx(self.0 + rhs.0) - } -} - -impl core::ops::AddAssign for BlockIdx { - fn add_assign(&mut self, rhs: BlockCount) { - self.0 += rhs.0 - } -} - -impl core::ops::Add for BlockCount { - type Output = BlockCount; - fn add(self, rhs: BlockCount) -> BlockCount { - BlockCount(self.0 + rhs.0) - } -} - -impl core::ops::AddAssign for BlockCount { - fn add_assign(&mut self, rhs: BlockCount) { - self.0 += rhs.0 - } -} - -impl core::ops::Sub for BlockIdx { - type Output = BlockIdx; - fn sub(self, rhs: BlockCount) -> BlockIdx { - BlockIdx(self.0 - rhs.0) - } -} - -impl core::ops::SubAssign for BlockIdx { - fn sub_assign(&mut self, rhs: BlockCount) { - self.0 -= rhs.0 - } -} - -impl core::ops::Sub for BlockCount { - type Output = BlockCount; - fn sub(self, rhs: BlockCount) -> BlockCount { - BlockCount(self.0 - rhs.0) - } -} - -impl core::ops::SubAssign for BlockCount { - fn sub_assign(&mut self, rhs: BlockCount) { - self.0 -= rhs.0 - } -} - impl core::ops::Deref for Block { type Target = [u8; 512]; fn deref(&self) -> &[u8; 512] { @@ -159,6 +67,33 @@ impl core::fmt::Debug for Block { } } +impl Default for Block { + fn default() -> Self { + Self::new() + } +} + +/// A block device - a device which can read and write blocks (or +/// sectors). Only supports devices which are <= 2 TiB in size. +pub trait BlockDevice { + /// The errors that the `BlockDevice` can return. Must be debug formattable. + type Error: core::fmt::Debug; + /// Read one or more blocks, starting at the given block index. + fn read(&self, blocks: &mut [Block], start_block_idx: BlockIdx) -> Result<(), Self::Error>; + /// Write one or more blocks, starting at the given block index. + fn write(&self, blocks: &[Block], start_block_idx: BlockIdx) -> Result<(), Self::Error>; + /// Determine how many blocks this device can hold. + fn num_blocks(&self) -> Result; +} + +/// The linear numeric address of a block (or sector). +/// +/// The first block on a disk gets `BlockIdx(0)` (which usually contains the +/// Master Boot Record). +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct BlockIdx(pub u32); + impl BlockIdx { /// Convert a block index into a 64-bit byte offset from the start of the /// volume. Useful if your underlying block device actually works in @@ -174,6 +109,65 @@ impl BlockIdx { } } +impl core::ops::Add for BlockIdx { + type Output = BlockIdx; + fn add(self, rhs: BlockCount) -> BlockIdx { + BlockIdx(self.0 + rhs.0) + } +} + +impl core::ops::AddAssign for BlockIdx { + fn add_assign(&mut self, rhs: BlockCount) { + self.0 += rhs.0 + } +} + +impl core::ops::Sub for BlockIdx { + type Output = BlockIdx; + fn sub(self, rhs: BlockCount) -> BlockIdx { + BlockIdx(self.0 - rhs.0) + } +} + +impl core::ops::SubAssign for BlockIdx { + fn sub_assign(&mut self, rhs: BlockCount) { + self.0 -= rhs.0 + } +} + +/// The a number of blocks (or sectors). +/// +/// Add this to a `BlockIdx` to get an actual address on disk. +#[cfg_attr(feature = "defmt-log", derive(defmt::Format))] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct BlockCount(pub u32); + +impl core::ops::Add for BlockCount { + type Output = BlockCount; + fn add(self, rhs: BlockCount) -> BlockCount { + BlockCount(self.0 + rhs.0) + } +} + +impl core::ops::AddAssign for BlockCount { + fn add_assign(&mut self, rhs: BlockCount) { + self.0 += rhs.0 + } +} + +impl core::ops::Sub for BlockCount { + type Output = BlockCount; + fn sub(self, rhs: BlockCount) -> BlockCount { + BlockCount(self.0 - rhs.0) + } +} + +impl core::ops::SubAssign for BlockCount { + fn sub_assign(&mut self, rhs: BlockCount) { + self.0 -= rhs.0 + } +} + impl BlockCount { /// How many blocks are required to hold this many bytes. /// @@ -200,6 +194,12 @@ impl BlockCount { } } +/// An iterator returned from `Block::range`. +pub struct BlockIter { + inclusive_end: BlockIdx, + current: BlockIdx, +} + impl BlockIter { /// Create a new `BlockIter`, from the given start block, through (and /// including) the given end block. From 2a604683107ed5f9f16591eccf19d62834b9bc83 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Wed, 16 Oct 2024 21:46:50 +0100 Subject: [PATCH 20/62] Add a general BlockCache. Avoids needing to stack allocate 512 byte blocks. --- src/blockdevice.rs | 72 ++++++++ src/fat/mod.rs | 31 ---- src/fat/volume.rs | 409 ++++++++++++++++++++------------------------- src/lib.rs | 2 +- src/volume_mgr.rs | 196 ++++++++++++---------- 5 files changed, 358 insertions(+), 352 deletions(-) diff --git a/src/blockdevice.rs b/src/blockdevice.rs index e151d1b1..4895ade2 100644 --- a/src/blockdevice.rs +++ b/src/blockdevice.rs @@ -86,6 +86,78 @@ pub trait BlockDevice { fn num_blocks(&self) -> Result; } +/// A caching layer for block devices +/// +/// Caches a single block. +#[derive(Debug)] +pub struct BlockCache { + block_device: D, + block: [Block; 1], + block_idx: Option, +} + +impl BlockCache +where + D: BlockDevice, +{ + /// Create a new block cache + pub fn new(block_device: D) -> BlockCache { + BlockCache { + block_device, + block: [Block::new()], + block_idx: None, + } + } + + /// Read a block, and return a reference to it. + pub fn read(&mut self, block_idx: BlockIdx) -> Result<&Block, D::Error> { + if self.block_idx != Some(block_idx) { + self.block_device.read(&mut self.block, block_idx)?; + self.block_idx = Some(block_idx); + } + Ok(&self.block[0]) + } + + /// Read a block, and return a reference to it. + pub fn read_mut(&mut self, block_idx: BlockIdx) -> Result<&mut Block, D::Error> { + if self.block_idx != Some(block_idx) { + self.block_device.read(&mut self.block, block_idx)?; + self.block_idx = Some(block_idx); + } + Ok(&mut self.block[0]) + } + + /// Write back a block you read with [`Self::read_mut`] and then modified. + pub fn write_back(&mut self) -> Result<(), D::Error> { + self.block_device.write( + &self.block, + self.block_idx.expect("write_back with no read"), + ) + } + + /// Access a blank sector + pub fn blank_mut(&mut self, block_idx: BlockIdx) -> &mut Block { + self.block_idx = Some(block_idx); + for b in self.block[0].iter_mut() { + *b = 0; + } + &mut self.block[0] + } + + /// Access the block device + pub fn block_device(&mut self) -> &mut D { + // invalidate the cache + self.block_idx = None; + // give them the block device + &mut self.block_device + } + + /// Get the block device back + pub fn free(self) -> D { + self.block_device + } +} + /// The linear numeric address of a block (or sector). /// /// The first block on a disk gets `BlockIdx(0)` (which usually contains the diff --git a/src/fat/mod.rs b/src/fat/mod.rs index dfd90be7..504f67fc 100644 --- a/src/fat/mod.rs +++ b/src/fat/mod.rs @@ -14,35 +14,6 @@ pub enum FatType { Fat32, } -pub(crate) struct BlockCache { - block: Block, - idx: Option, -} -impl BlockCache { - pub fn empty() -> Self { - BlockCache { - block: Block::new(), - idx: None, - } - } - pub(crate) fn read( - &mut self, - block_device: &D, - block_idx: BlockIdx, - ) -> Result<&Block, Error> - where - D: BlockDevice, - { - if Some(block_idx) != self.idx { - self.idx = Some(block_idx); - block_device - .read(core::slice::from_mut(&mut self.block), block_idx) - .map_err(Error::DeviceError)?; - } - Ok(&self.block) - } -} - mod bpb; mod info; mod ondiskdirentry; @@ -53,8 +24,6 @@ pub use info::{Fat16Info, Fat32Info, FatSpecificInfo, InfoSector}; pub use ondiskdirentry::OnDiskDirEntry; pub use volume::{parse_volume, FatVolume, VolumeName}; -use crate::{Block, BlockDevice, BlockIdx, Error}; - // **************************************************************************** // // Unit Tests diff --git a/src/fat/volume.rs b/src/fat/volume.rs index 77067514..fbbedf9e 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -7,14 +7,12 @@ use crate::{ RESERVED_ENTRIES, }, filesystem::FilenameError, - trace, warn, Attributes, Block, BlockCount, BlockDevice, BlockIdx, ClusterId, DirEntry, - DirectoryInfo, Error, ShortFileName, TimeSource, VolumeType, + trace, warn, Attributes, Block, BlockCache, BlockCount, BlockDevice, BlockIdx, ClusterId, + DirEntry, DirectoryInfo, Error, ShortFileName, TimeSource, VolumeType, }; use byteorder::{ByteOrder, LittleEndian}; use core::convert::TryFrom; -use super::BlockCache; - /// An MS-DOS 11 character volume label. /// /// ISO-8859-1 encoding is assumed. Trailing spaces are trimmed. Reserved @@ -163,7 +161,10 @@ pub struct FatVolume { impl FatVolume { /// Write a new entry in the FAT - pub fn update_info_sector(&mut self, block_device: &D) -> Result<(), Error> + pub fn update_info_sector( + &mut self, + block_cache: &mut BlockCache, + ) -> Result<(), Error> where D: BlockDevice, { @@ -175,12 +176,10 @@ impl FatVolume { if self.free_clusters_count.is_none() && self.next_free_cluster.is_none() { return Ok(()); } - let mut blocks = [Block::new()]; trace!("Reading info sector"); - block_device - .read(&mut blocks, fat32_info.info_location) + let block = block_cache + .read_mut(fat32_info.info_location) .map_err(Error::DeviceError)?; - let block = &mut blocks[0]; if let Some(count) = self.free_clusters_count { block[488..492].copy_from_slice(&count.to_le_bytes()); } @@ -188,9 +187,7 @@ impl FatVolume { block[492..496].copy_from_slice(&next_free_cluster.0.to_le_bytes()); } trace!("Writing info sector"); - block_device - .write(&blocks, fat32_info.info_location) - .map_err(Error::DeviceError)?; + block_cache.write_back()?; } } Ok(()) @@ -207,14 +204,13 @@ impl FatVolume { /// Write a new entry in the FAT fn update_fat( &mut self, - block_device: &D, + block_cache: &mut BlockCache, cluster: ClusterId, new_value: ClusterId, ) -> Result<(), Error> where D: BlockDevice, { - let mut blocks = [Block::new()]; let this_fat_block_num; match &self.fat_specific_info { FatSpecificInfo::Fat16(_fat16_info) => { @@ -222,8 +218,8 @@ impl FatVolume { this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; trace!("Reading FAT for update"); - block_device - .read(&mut blocks, this_fat_block_num) + let block = block_cache + .read_mut(this_fat_block_num) .map_err(Error::DeviceError)?; // See let entry = match new_value { @@ -234,7 +230,7 @@ impl FatVolume { _ => new_value.0 as u16, }; LittleEndian::write_u16( - &mut blocks[0][this_fat_ent_offset..=this_fat_ent_offset + 1], + &mut block[this_fat_ent_offset..=this_fat_ent_offset + 1], entry, ); } @@ -244,8 +240,8 @@ impl FatVolume { this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; trace!("Reading FAT for update"); - block_device - .read(&mut blocks, this_fat_block_num) + let block = block_cache + .read_mut(this_fat_block_num) .map_err(Error::DeviceError)?; let entry = match new_value { ClusterId::INVALID => 0x0FFF_FFF6, @@ -253,29 +249,25 @@ impl FatVolume { ClusterId::EMPTY => 0x0000_0000, _ => new_value.0, }; - let existing = LittleEndian::read_u32( - &blocks[0][this_fat_ent_offset..=this_fat_ent_offset + 3], - ); + let existing = + LittleEndian::read_u32(&block[this_fat_ent_offset..=this_fat_ent_offset + 3]); let new = (existing & 0xF000_0000) | (entry & 0x0FFF_FFFF); LittleEndian::write_u32( - &mut blocks[0][this_fat_ent_offset..=this_fat_ent_offset + 3], + &mut block[this_fat_ent_offset..=this_fat_ent_offset + 3], new, ); } } trace!("Updating FAT"); - block_device - .write(&blocks, this_fat_block_num) - .map_err(Error::DeviceError)?; + block_cache.write_back()?; Ok(()) } /// Look in the FAT to see which cluster comes next. pub(crate) fn next_cluster( &self, - block_device: &D, + block_cache: &mut BlockCache, cluster: ClusterId, - fat_block_cache: &mut BlockCache, ) -> Result> where D: BlockDevice, @@ -288,8 +280,8 @@ impl FatVolume { let fat_offset = cluster.0 * 2; let this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; - trace!("Walkng FAT"); - let block = fat_block_cache.read(block_device, this_fat_block_num)?; + trace!("Walking FAT"); + let block = block_cache.read(this_fat_block_num)?; let fat_entry = LittleEndian::read_u16(&block[this_fat_ent_offset..=this_fat_ent_offset + 1]); match fat_entry { @@ -312,7 +304,7 @@ impl FatVolume { let this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; trace!("Walking FAT"); - let block = fat_block_cache.read(block_device, this_fat_block_num)?; + let block = block_cache.read(this_fat_block_num)?; let fat_entry = LittleEndian::read_u32(&block[this_fat_ent_offset..=this_fat_ent_offset + 3]) & 0x0FFF_FFFF; @@ -377,7 +369,7 @@ impl FatVolume { /// needed pub(crate) fn write_new_directory_entry( &mut self, - block_device: &D, + block_cache: &mut BlockCache, time_source: &T, dir_cluster: ClusterId, name: ShortFileName, @@ -408,15 +400,14 @@ impl FatVolume { }; // Walk the directory - let mut blocks = [Block::new()]; while let Some(cluster) = current_cluster { - for block in first_dir_block_num.range(dir_size) { + for block_idx in first_dir_block_num.range(dir_size) { trace!("Reading directory"); - block_device - .read(&mut blocks, block) + let block = block_cache + .read_mut(block_idx) .map_err(Error::DeviceError)?; for (i, dir_entry_bytes) in - blocks[0].chunks_exact_mut(OnDiskDirEntry::LEN).enumerate() + block.chunks_exact_mut(OnDiskDirEntry::LEN).enumerate() { let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); // 0x00 or 0xE5 represents a free entry @@ -427,35 +418,30 @@ impl FatVolume { attributes, ClusterId::EMPTY, ctime, - block, + block_idx, (i * OnDiskDirEntry::LEN) as u32, ); dir_entry_bytes .copy_from_slice(&entry.serialize(FatType::Fat16)[..]); trace!("Updating directory"); - block_device - .write(&blocks, block) - .map_err(Error::DeviceError)?; + block_cache.write_back()?; return Ok(entry); } } } if cluster != ClusterId::ROOT_DIR { - let mut block_cache = BlockCache::empty(); - current_cluster = - match self.next_cluster(block_device, cluster, &mut block_cache) { - Ok(n) => { - first_dir_block_num = self.cluster_to_block(n); - Some(n) - } - Err(Error::EndOfFile) => { - let c = - self.alloc_cluster(block_device, Some(cluster), true)?; - first_dir_block_num = self.cluster_to_block(c); - Some(c) - } - _ => None, - }; + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => { + first_dir_block_num = self.cluster_to_block(n); + Some(n) + } + Err(Error::EndOfFile) => { + let c = self.alloc_cluster(block_cache, Some(cluster), true)?; + first_dir_block_num = self.cluster_to_block(c); + Some(c) + } + _ => None, + }; } else { current_cluster = None; } @@ -470,22 +456,21 @@ impl FatVolume { _ => Some(dir_cluster), }; let mut first_dir_block_num = self.cluster_to_block(dir_cluster); - let mut blocks = [Block::new()]; let dir_size = BlockCount(u32::from(self.blocks_per_cluster)); // Walk the cluster chain until we run out of clusters while let Some(cluster) = current_cluster { // Loop through the blocks in the cluster - for block in first_dir_block_num.range(dir_size) { + for block_idx in first_dir_block_num.range(dir_size) { // Read a block of directory entries trace!("Reading directory"); - block_device - .read(&mut blocks, block) + let block = block_cache + .read_mut(block_idx) .map_err(Error::DeviceError)?; // Are any entries in the block we just loaded blank? If so // we can use them. for (i, dir_entry_bytes) in - blocks[0].chunks_exact_mut(OnDiskDirEntry::LEN).enumerate() + block.chunks_exact_mut(OnDiskDirEntry::LEN).enumerate() { let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); // 0x00 or 0xE5 represents a free entry @@ -496,35 +481,31 @@ impl FatVolume { attributes, ClusterId(0), ctime, - block, + block_idx, (i * OnDiskDirEntry::LEN) as u32, ); dir_entry_bytes .copy_from_slice(&entry.serialize(FatType::Fat32)[..]); trace!("Updating directory"); - block_device - .write(&blocks, block) - .map_err(Error::DeviceError)?; + block_cache.write_back()?; return Ok(entry); } } } // Well none of the blocks in that cluster had any space in // them, let's fetch another one. - let mut block_cache = BlockCache::empty(); - current_cluster = - match self.next_cluster(block_device, cluster, &mut block_cache) { - Ok(n) => { - first_dir_block_num = self.cluster_to_block(n); - Some(n) - } - Err(Error::EndOfFile) => { - let c = self.alloc_cluster(block_device, Some(cluster), true)?; - first_dir_block_num = self.cluster_to_block(c); - Some(c) - } - _ => None, - }; + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => { + first_dir_block_num = self.cluster_to_block(n); + Some(n) + } + Err(Error::EndOfFile) => { + let c = self.alloc_cluster(block_cache, Some(cluster), true)?; + first_dir_block_num = self.cluster_to_block(c); + Some(c) + } + _ => None, + }; } // We ran out of clusters in the chain, and apparently we weren't // able to make the chain longer, so the disk must be full. @@ -537,7 +518,7 @@ impl FatVolume { /// Useful for performing directory listings. pub(crate) fn iterate_dir( &self, - block_device: &D, + block_cache: &mut BlockCache, dir_info: &DirectoryInfo, func: F, ) -> Result<(), Error> @@ -547,10 +528,10 @@ impl FatVolume { { match &self.fat_specific_info { FatSpecificInfo::Fat16(fat16_info) => { - self.iterate_fat16(dir_info, fat16_info, block_device, func) + self.iterate_fat16(dir_info, fat16_info, block_cache, func) } FatSpecificInfo::Fat32(fat32_info) => { - self.iterate_fat32(dir_info, fat32_info, block_device, func) + self.iterate_fat32(dir_info, fat32_info, block_cache, func) } } } @@ -559,7 +540,7 @@ impl FatVolume { &self, dir_info: &DirectoryInfo, fat16_info: &Fat16Info, - block_device: &D, + block_cache: &mut BlockCache, mut func: F, ) -> Result<(), Error> where @@ -583,18 +564,17 @@ impl FatVolume { _ => BlockCount(u32::from(self.blocks_per_cluster)), }; - let mut block_cache = BlockCache::empty(); while let Some(cluster) = current_cluster { for block_idx in first_dir_block_num.range(dir_size) { - trace!("Reading directory"); - let block = block_cache.read(block_device, block_idx)?; + trace!("Reading FAT"); + let block = block_cache.read(block_idx)?; for (i, dir_entry_bytes) in block.chunks_exact(OnDiskDirEntry::LEN).enumerate() { let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); if dir_entry.is_end() { // Can quit early return Ok(()); } else if dir_entry.is_valid() && !dir_entry.is_lfn() { - // Block::LEN always fits on a u32 + // Safe, since Block::LEN always fits on a u32 let start = (i * OnDiskDirEntry::LEN) as u32; let entry = dir_entry.get_entry(FatType::Fat16, block_idx, start); func(&entry); @@ -602,7 +582,7 @@ impl FatVolume { } } if cluster != ClusterId::ROOT_DIR { - current_cluster = match self.next_cluster(block_device, cluster, &mut block_cache) { + current_cluster = match self.next_cluster(block_cache, cluster) { Ok(n) => { first_dir_block_num = self.cluster_to_block(n); Some(n) @@ -620,7 +600,7 @@ impl FatVolume { &self, dir_info: &DirectoryInfo, fat32_info: &Fat32Info, - block_device: &D, + block_cache: &mut BlockCache, mut func: F, ) -> Result<(), Error> where @@ -633,31 +613,25 @@ impl FatVolume { ClusterId::ROOT_DIR => Some(fat32_info.first_root_dir_cluster), _ => Some(dir_info.cluster), }; - let mut blocks = [Block::new()]; - let mut block_cache = BlockCache::empty(); while let Some(cluster) = current_cluster { - let block_idx = self.cluster_to_block(cluster); - for block in block_idx.range(BlockCount(u32::from(self.blocks_per_cluster))) { - trace!("Reading directory"); - block_device - .read(&mut blocks, block) - .map_err(Error::DeviceError)?; - for (i, dir_entry_bytes) in - blocks[0].chunks_exact_mut(OnDiskDirEntry::LEN).enumerate() - { + let start_block_idx = self.cluster_to_block(cluster); + for block_idx in start_block_idx.range(BlockCount(u32::from(self.blocks_per_cluster))) { + trace!("Reading FAT"); + let block = block_cache.read(block_idx).map_err(Error::DeviceError)?; + for (i, dir_entry_bytes) in block.chunks_exact(OnDiskDirEntry::LEN).enumerate() { let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); if dir_entry.is_end() { // Can quit early return Ok(()); } else if dir_entry.is_valid() && !dir_entry.is_lfn() { - // Block::LEN always fits on a u32 + // Safe, since Block::LEN always fits on a u32 let start = (i * OnDiskDirEntry::LEN) as u32; - let entry = dir_entry.get_entry(FatType::Fat32, block, start); + let entry = dir_entry.get_entry(FatType::Fat32, block_idx, start); func(&entry); } } } - current_cluster = match self.next_cluster(block_device, cluster, &mut block_cache) { + current_cluster = match self.next_cluster(block_cache, cluster) { Ok(n) => Some(n), _ => None, }; @@ -668,7 +642,7 @@ impl FatVolume { /// Get an entry from the given directory pub(crate) fn find_directory_entry( &self, - block_device: &D, + block_cache: &mut BlockCache, dir_info: &DirectoryInfo, match_name: &ShortFileName, ) -> Result> @@ -695,11 +669,10 @@ impl FatVolume { _ => BlockCount(u32::from(self.blocks_per_cluster)), }; - let mut block_cache = BlockCache::empty(); while let Some(cluster) = current_cluster { for block in first_dir_block_num.range(dir_size) { match self.find_entry_in_block( - block_device, + block_cache, FatType::Fat16, match_name, block, @@ -709,14 +682,13 @@ impl FatVolume { } } if cluster != ClusterId::ROOT_DIR { - current_cluster = - match self.next_cluster(block_device, cluster, &mut block_cache) { - Ok(n) => { - first_dir_block_num = self.cluster_to_block(n); - Some(n) - } - _ => None, - }; + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => { + first_dir_block_num = self.cluster_to_block(n); + Some(n) + } + _ => None, + }; } else { current_cluster = None; } @@ -728,12 +700,11 @@ impl FatVolume { ClusterId::ROOT_DIR => Some(fat32_info.first_root_dir_cluster), _ => Some(dir_info.cluster), }; - let mut block_cache = BlockCache::empty(); while let Some(cluster) = current_cluster { let block_idx = self.cluster_to_block(cluster); for block in block_idx.range(BlockCount(u32::from(self.blocks_per_cluster))) { match self.find_entry_in_block( - block_device, + block_cache, FatType::Fat32, match_name, block, @@ -742,11 +713,10 @@ impl FatVolume { x => return x, } } - current_cluster = - match self.next_cluster(block_device, cluster, &mut block_cache) { - Ok(n) => Some(n), - _ => None, - } + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => Some(n), + _ => None, + } } Err(Error::NotFound) } @@ -756,20 +726,17 @@ impl FatVolume { /// Finds an entry in a given block of directory entries. fn find_entry_in_block( &self, - block_device: &D, + block_cache: &mut BlockCache, fat_type: FatType, match_name: &ShortFileName, - block: BlockIdx, + block_idx: BlockIdx, ) -> Result> where D: BlockDevice, { - let mut blocks = [Block::new()]; trace!("Reading directory"); - block_device - .read(&mut blocks, block) - .map_err(Error::DeviceError)?; - for (i, dir_entry_bytes) in blocks[0].chunks_exact_mut(OnDiskDirEntry::LEN).enumerate() { + let block = block_cache.read(block_idx).map_err(Error::DeviceError)?; + for (i, dir_entry_bytes) in block.chunks_exact(OnDiskDirEntry::LEN).enumerate() { let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); if dir_entry.is_end() { // Can quit early @@ -778,7 +745,7 @@ impl FatVolume { // Found it // Block::LEN always fits on a u32 let start = (i * OnDiskDirEntry::LEN) as u32; - return Ok(dir_entry.get_entry(fat_type, block, start)); + return Ok(dir_entry.get_entry(fat_type, block_idx, start)); } } Err(Error::NotFound) @@ -787,7 +754,7 @@ impl FatVolume { /// Delete an entry from the given directory pub(crate) fn delete_directory_entry( &self, - block_device: &D, + block_cache: &mut BlockCache, dir_info: &DirectoryInfo, match_name: &ShortFileName, ) -> Result<(), Error> @@ -817,8 +784,8 @@ impl FatVolume { // Walk the directory while let Some(cluster) = current_cluster { // Scan the cluster / root dir a block at a time - for block in first_dir_block_num.range(dir_size) { - match self.delete_entry_in_block(block_device, match_name, block) { + for block_idx in first_dir_block_num.range(dir_size) { + match self.delete_entry_in_block(block_cache, match_name, block_idx) { Err(Error::NotFound) => { // Carry on } @@ -831,15 +798,13 @@ impl FatVolume { } // if it's not the root dir, find the next cluster so we can keep looking if cluster != ClusterId::ROOT_DIR { - let mut block_cache = BlockCache::empty(); - current_cluster = - match self.next_cluster(block_device, cluster, &mut block_cache) { - Ok(n) => { - first_dir_block_num = self.cluster_to_block(n); - Some(n) - } - _ => None, - }; + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => { + first_dir_block_num = self.cluster_to_block(n); + Some(n) + } + _ => None, + }; } else { current_cluster = None; } @@ -856,9 +821,11 @@ impl FatVolume { // Walk the directory while let Some(cluster) = current_cluster { // Scan the cluster a block at a time - let block_idx = self.cluster_to_block(cluster); - for block in block_idx.range(BlockCount(u32::from(self.blocks_per_cluster))) { - match self.delete_entry_in_block(block_device, match_name, block) { + let start_block_idx = self.cluster_to_block(cluster); + for block_idx in + start_block_idx.range(BlockCount(u32::from(self.blocks_per_cluster))) + { + match self.delete_entry_in_block(block_cache, match_name, block_idx) { Err(Error::NotFound) => { // Carry on continue; @@ -871,12 +838,10 @@ impl FatVolume { } } // Find the next cluster - let mut block_cache = BlockCache::empty(); - current_cluster = - match self.next_cluster(block_device, cluster, &mut block_cache) { - Ok(n) => Some(n), - _ => None, - } + current_cluster = match self.next_cluster(block_cache, cluster) { + Ok(n) => Some(n), + _ => None, + } } // Ok, give up } @@ -892,32 +857,28 @@ impl FatVolume { /// to a special value. fn delete_entry_in_block( &self, - block_device: &D, + block_cache: &mut BlockCache, match_name: &ShortFileName, - block: BlockIdx, + block_idx: BlockIdx, ) -> Result<(), Error> where D: BlockDevice, { - let mut blocks = [Block::new()]; trace!("Reading directory"); - block_device - .read(&mut blocks, block) + let block = block_cache + .read_mut(block_idx) .map_err(Error::DeviceError)?; - for (i, dir_entry_bytes) in blocks[0].chunks_exact_mut(OnDiskDirEntry::LEN).enumerate() { + for (i, dir_entry_bytes) in block.chunks_exact_mut(OnDiskDirEntry::LEN).enumerate() { let dir_entry = OnDiskDirEntry::new(dir_entry_bytes); if dir_entry.is_end() { // Can quit early break; } else if dir_entry.matches(match_name) { - let mut blocks = blocks; let start = i * OnDiskDirEntry::LEN; // set first byte to the 'unused' marker - blocks[0].contents[start] = 0xE5; + block[start] = 0xE5; trace!("Updating directory"); - return block_device - .write(&blocks, block) - .map_err(Error::DeviceError); + return block_cache.write_back().map_err(Error::DeviceError); } } Err(Error::NotFound) @@ -926,14 +887,13 @@ impl FatVolume { /// Finds the next free cluster after the start_cluster and before end_cluster pub(crate) fn find_next_free_cluster( &self, - block_device: &D, + block_cache: &mut BlockCache, start_cluster: ClusterId, end_cluster: ClusterId, ) -> Result> where D: BlockDevice, { - let mut blocks = [Block::new()]; let mut current_cluster = start_cluster; match &self.fat_specific_info { FatSpecificInfo::Fat16(_fat16_info) => { @@ -951,13 +911,12 @@ impl FatVolume { let mut this_fat_ent_offset = usize::try_from(fat_offset % Block::LEN_U32) .map_err(|_| Error::ConversionError)?; trace!("Reading block {:?}", this_fat_block_num); - block_device - .read(&mut blocks, this_fat_block_num) + let block = block_cache + .read(this_fat_block_num) .map_err(Error::DeviceError)?; - while this_fat_ent_offset <= Block::LEN - 2 { let fat_entry = LittleEndian::read_u16( - &blocks[0][this_fat_ent_offset..=this_fat_ent_offset + 1], + &block[this_fat_ent_offset..=this_fat_ent_offset + 1], ); if fat_entry == 0 { return Ok(current_cluster); @@ -982,13 +941,12 @@ impl FatVolume { let mut this_fat_ent_offset = usize::try_from(fat_offset % Block::LEN_U32) .map_err(|_| Error::ConversionError)?; trace!("Reading block {:?}", this_fat_block_num); - block_device - .read(&mut blocks, this_fat_block_num) + let block = block_cache + .read(this_fat_block_num) .map_err(Error::DeviceError)?; - while this_fat_ent_offset <= Block::LEN - 4 { let fat_entry = LittleEndian::read_u32( - &blocks[0][this_fat_ent_offset..=this_fat_ent_offset + 3], + &block[this_fat_ent_offset..=this_fat_ent_offset + 3], ) & 0x0FFF_FFFF; if fat_entry == 0 { return Ok(current_cluster); @@ -1006,7 +964,7 @@ impl FatVolume { /// Tries to allocate a cluster pub(crate) fn alloc_cluster( &mut self, - block_device: &D, + block_cache: &mut BlockCache, prev_cluster: Option, zero: bool, ) -> Result> @@ -1024,31 +982,27 @@ impl FatVolume { start_cluster, end_cluster ); - let new_cluster = - match self.find_next_free_cluster(block_device, start_cluster, end_cluster) { - Ok(cluster) => cluster, - Err(_) if start_cluster.0 > RESERVED_ENTRIES => { - debug!( - "Retrying, finding next free between {:?}..={:?}", - ClusterId(RESERVED_ENTRIES), - end_cluster - ); - self.find_next_free_cluster( - block_device, - ClusterId(RESERVED_ENTRIES), - end_cluster, - )? - } - Err(e) => return Err(e), - }; - self.update_fat(block_device, new_cluster, ClusterId::END_OF_FILE)?; + let new_cluster = match self.find_next_free_cluster(block_cache, start_cluster, end_cluster) + { + Ok(cluster) => cluster, + Err(_) if start_cluster.0 > RESERVED_ENTRIES => { + debug!( + "Retrying, finding next free between {:?}..={:?}", + ClusterId(RESERVED_ENTRIES), + end_cluster + ); + self.find_next_free_cluster(block_cache, ClusterId(RESERVED_ENTRIES), end_cluster)? + } + Err(e) => return Err(e), + }; + self.update_fat(block_cache, new_cluster, ClusterId::END_OF_FILE)?; if let Some(cluster) = prev_cluster { trace!( "Updating old cluster {:?} to {:?} in FAT", cluster, new_cluster ); - self.update_fat(block_device, cluster, new_cluster)?; + self.update_fat(block_cache, cluster, new_cluster)?; } trace!( "Finding next free between {:?}..={:?}", @@ -1056,11 +1010,11 @@ impl FatVolume { end_cluster ); self.next_free_cluster = - match self.find_next_free_cluster(block_device, new_cluster, end_cluster) { + match self.find_next_free_cluster(block_cache, new_cluster, end_cluster) { Ok(cluster) => Some(cluster), Err(_) if new_cluster.0 > RESERVED_ENTRIES => { match self.find_next_free_cluster( - block_device, + block_cache, ClusterId(RESERVED_ENTRIES), end_cluster, ) { @@ -1076,12 +1030,13 @@ impl FatVolume { }; if zero { let blocks = [Block::new()]; - let first_block = self.cluster_to_block(new_cluster); + let start_block_idx = self.cluster_to_block(new_cluster); let num_blocks = BlockCount(u32::from(self.blocks_per_cluster)); - for block in first_block.range(num_blocks) { + for block_idx in start_block_idx.range(num_blocks) { trace!("Zeroing cluster"); - block_device - .write(&blocks, block) + block_cache + .block_device() + .write(&blocks, block_idx) .map_err(Error::DeviceError)?; } } @@ -1092,7 +1047,7 @@ impl FatVolume { /// Marks the input cluster as an EOF and all the subsequent clusters in the chain as free pub(crate) fn truncate_cluster_chain( &mut self, - block_device: &D, + block_cache: &mut BlockCache, cluster: ClusterId, ) -> Result<(), Error> where @@ -1103,8 +1058,7 @@ impl FatVolume { return Ok(()); } let mut next = { - let mut block_cache = BlockCache::empty(); - match self.next_cluster(block_device, cluster, &mut block_cache) { + match self.next_cluster(block_cache, cluster) { Ok(n) => n, Err(Error::EndOfFile) => return Ok(()), Err(e) => return Err(e), @@ -1117,16 +1071,15 @@ impl FatVolume { } else { self.next_free_cluster = Some(next); } - self.update_fat(block_device, cluster, ClusterId::END_OF_FILE)?; + self.update_fat(block_cache, cluster, ClusterId::END_OF_FILE)?; loop { - let mut block_cache = BlockCache::empty(); - match self.next_cluster(block_device, next, &mut block_cache) { + match self.next_cluster(block_cache, next) { Ok(n) => { - self.update_fat(block_device, next, ClusterId::EMPTY)?; + self.update_fat(block_cache, next, ClusterId::EMPTY)?; next = n; } Err(Error::EndOfFile) => { - self.update_fat(block_device, next, ClusterId::EMPTY)?; + self.update_fat(block_cache, next, ClusterId::EMPTY)?; break; } Err(e) => return Err(e), @@ -1141,7 +1094,7 @@ impl FatVolume { /// Writes a Directory Entry to the disk pub(crate) fn write_entry_to_disk( &self, - block_device: &D, + block_cache: &mut BlockCache, entry: &DirEntry, ) -> Result<(), Error> where @@ -1151,20 +1104,16 @@ impl FatVolume { FatSpecificInfo::Fat16(_) => FatType::Fat16, FatSpecificInfo::Fat32(_) => FatType::Fat32, }; - let mut blocks = [Block::new()]; trace!("Reading directory for update"); - block_device - .read(&mut blocks, entry.entry_block) + let block = block_cache + .read_mut(entry.entry_block) .map_err(Error::DeviceError)?; - let block = &mut blocks[0]; let start = usize::try_from(entry.entry_offset).map_err(|_| Error::ConversionError)?; block[start..start + 32].copy_from_slice(&entry.serialize(fat_type)[..]); trace!("Updating directory"); - block_device - .write(&blocks, entry.entry_block) - .map_err(Error::DeviceError)?; + block_cache.write_back().map_err(Error::DeviceError)?; Ok(()) } } @@ -1172,7 +1121,7 @@ impl FatVolume { /// Load the boot parameter block from the start of the given partition and /// determine if the partition contains a valid FAT16 or FAT32 file system. pub fn parse_volume( - block_device: &D, + block_cache: &mut BlockCache, lba_start: BlockIdx, num_blocks: BlockCount, ) -> Result> @@ -1180,12 +1129,8 @@ where D: BlockDevice, D::Error: core::fmt::Debug, { - let mut blocks = [Block::new()]; trace!("Reading BPB"); - block_device - .read(&mut blocks, lba_start) - .map_err(Error::DeviceError)?; - let block = &blocks[0]; + let block = block_cache.read(lba_start).map_err(Error::DeviceError)?; let bpb = Bpb::create_from_bytes(block).map_err(Error::FormatError)?; match bpb.fat_type { FatType::Fat16 => { @@ -1226,16 +1171,7 @@ where // Safe to unwrap since this is a Fat32 Type let info_location = bpb.fs_info_block().unwrap(); - let mut info_blocks = [Block::new()]; - trace!("Reading info block"); - block_device - .read(&mut info_blocks, lba_start + info_location) - .map_err(Error::DeviceError)?; - let info_block = &info_blocks[0]; - let info_sector = - InfoSector::create_from_bytes(info_block).map_err(Error::FormatError)?; - - let volume = FatVolume { + let mut volume = FatVolume { lba_start, num_blocks, name: VolumeName { @@ -1244,14 +1180,25 @@ where blocks_per_cluster: bpb.blocks_per_cluster(), first_data_block: BlockCount(first_data_block), fat_start: BlockCount(u32::from(bpb.reserved_block_count())), - free_clusters_count: info_sector.free_clusters_count(), - next_free_cluster: info_sector.next_free_cluster(), + free_clusters_count: None, + next_free_cluster: None, cluster_count: bpb.total_clusters(), fat_specific_info: FatSpecificInfo::Fat32(Fat32Info { info_location: lba_start + info_location, first_root_dir_cluster: ClusterId(bpb.first_root_dir_cluster()), }), }; + + // Now we don't need the BPB, update the volume with data from the info sector + trace!("Reading info block"); + let info_block = block_cache + .read(lba_start + info_location) + .map_err(Error::DeviceError)?; + let info_sector = + InfoSector::create_from_bytes(info_block).map_err(Error::FormatError)?; + volume.free_clusters_count = info_sector.free_clusters_count(); + volume.next_free_cluster = info_sector.next_free_cluster(); + Ok(VolumeType::Fat(volume)) } } diff --git a/src/lib.rs b/src/lib.rs index 901101df..bdd56305 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,7 +78,7 @@ use embedded_io::ErrorKind; use filesystem::Handle; #[doc(inline)] -pub use crate::blockdevice::{Block, BlockCount, BlockDevice, BlockIdx}; +pub use crate::blockdevice::{Block, BlockCache, BlockCount, BlockDevice, BlockIdx}; #[doc(inline)] pub use crate::fat::{FatVolume, VolumeName}; diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 130e3092..ab417246 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -8,16 +8,16 @@ use core::convert::TryFrom; use byteorder::{ByteOrder, LittleEndian}; use heapless::Vec; -use crate::fat::{self, BlockCache, FatType, OnDiskDirEntry, RESERVED_ENTRIES}; +use crate::fat::{self, FatType, OnDiskDirEntry, RESERVED_ENTRIES}; use crate::filesystem::{ Attributes, ClusterId, DirEntry, DirectoryInfo, FileInfo, HandleGenerator, Mode, RawDirectory, RawFile, TimeSource, ToShortFileName, MAX_FILE_SIZE, }; use crate::{ - debug, trace, Block, BlockCount, BlockDevice, BlockIdx, Error, RawVolume, ShortFileName, - Volume, VolumeIdx, VolumeInfo, VolumeType, PARTITION_ID_FAT16, PARTITION_ID_FAT16_LBA, - PARTITION_ID_FAT32_CHS_LBA, PARTITION_ID_FAT32_LBA, + debug, trace, Block, BlockCache, BlockCount, BlockDevice, BlockIdx, Error, RawVolume, + ShortFileName, Volume, VolumeIdx, VolumeInfo, VolumeType, PARTITION_ID_FAT16, + PARTITION_ID_FAT16_LBA, PARTITION_ID_FAT32_CHS_LBA, PARTITION_ID_FAT32_LBA, }; /// Wraps a block device and gives access to the FAT-formatted volumes within @@ -37,8 +37,8 @@ pub struct VolumeManager< T: TimeSource, ::Error: core::fmt::Debug, { - pub(crate) block_device: D, - pub(crate) time_source: T, + time_source: T, + block_cache: RefCell>, data: RefCell>, } @@ -83,7 +83,7 @@ where ) -> VolumeManager { debug!("Creating new embedded-sdmmc::VolumeManager"); VolumeManager { - block_device, + block_cache: RefCell::new(BlockCache::new(block_device)), time_source, data: RefCell::new(VolumeManagerData { id_generator: HandleGenerator::new(id_offset), @@ -95,13 +95,13 @@ where } /// Temporarily get access to the underlying block device. - pub fn device(&self) -> &D { - &self.block_device - } - - /// Temporarily get access to the underlying block device. - pub fn device_mut(&mut self) -> &mut D { - &mut self.block_device + pub fn device(&self, f: F) -> T + where + F: FnOnce(&mut D) -> T, + { + let mut block_cache = self.block_cache.borrow_mut(); + let result = f(block_cache.block_device()); + result } /// Get a volume (or partition) based on entries in the Master Boot Record. @@ -137,6 +137,10 @@ where const PARTITION_INFO_NUM_BLOCKS_INDEX: usize = 12; let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let mut block_cache = self + .block_cache + .try_borrow_mut() + .map_err(|_| Error::LockError)?; if data.open_volumes.is_full() { return Err(Error::TooManyOpenVolumes); @@ -149,12 +153,8 @@ where } let (part_type, lba_start, num_blocks) = { - let mut blocks = [Block::new()]; trace!("Reading partition table"); - self.block_device - .read(&mut blocks, BlockIdx(0)) - .map_err(Error::DeviceError)?; - let block = &blocks[0]; + let block = block_cache.read(BlockIdx(0)).map_err(Error::DeviceError)?; // We only support Master Boot Record (MBR) partitioned cards, not // GUID Partition Table (GPT) if LittleEndian::read_u16(&block[FOOTER_START..FOOTER_START + 2]) != FOOTER_VALUE { @@ -198,7 +198,7 @@ where | PARTITION_ID_FAT32_LBA | PARTITION_ID_FAT16_LBA | PARTITION_ID_FAT16 => { - let volume = fat::parse_volume(&self.block_device, lba_start, num_blocks)?; + let volume = fat::parse_volume(&mut block_cache, lba_start, num_blocks)?; let id = RawVolume(data.id_generator.generate()); let info = VolumeInfo { raw_volume: id, @@ -253,6 +253,10 @@ where N: ToShortFileName, { let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let mut block_cache = self + .block_cache + .try_borrow_mut() + .map_err(|_| Error::LockError)?; if data.open_dirs.is_full() { return Err(Error::TooManyOpenDirs); @@ -285,7 +289,7 @@ where let dir_entry = match &data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => fat.find_directory_entry( - &self.block_device, + &mut block_cache, &data.open_dirs[parent_dir_idx], &short_file_name, )?, @@ -364,14 +368,18 @@ where where N: ToShortFileName, { - let data = self.data.borrow(); + let data = self.data.try_borrow().map_err(|_| Error::LockError)?; + let mut block_cache = self + .block_cache + .try_borrow_mut() + .map_err(|_| Error::LockError)?; let directory_idx = data.get_dir_by_id(directory)?; let volume_idx = data.get_volume_by_id(data.open_dirs[directory_idx].raw_volume)?; match &data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => { let sfn = name.to_short_filename().map_err(Error::FilenameError)?; - fat.find_directory_entry(&self.block_device, &data.open_dirs[directory_idx], &sfn) + fat.find_directory_entry(&mut block_cache, &data.open_dirs[directory_idx], &sfn) } } } @@ -389,13 +397,17 @@ where where F: FnMut(&DirEntry), { - let data = self.data.borrow(); + let data = self.data.try_borrow().map_err(|_| Error::LockError)?; + let mut block_cache = self + .block_cache + .try_borrow_mut() + .map_err(|_| Error::LockError)?; let directory_idx = data.get_dir_by_id(directory)?; let volume_idx = data.get_volume_by_id(data.open_dirs[directory_idx].raw_volume)?; match &data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => { - fat.iterate_dir(&self.block_device, &data.open_dirs[directory_idx], func) + fat.iterate_dir(&mut block_cache, &data.open_dirs[directory_idx], func) } } } @@ -411,6 +423,10 @@ where N: ToShortFileName, { let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let mut block_cache = self + .block_cache + .try_borrow_mut() + .map_err(|_| Error::LockError)?; // This check is load-bearing - we do an unchecked push later. if data.open_files.is_full() { @@ -425,7 +441,7 @@ where let dir_entry = match &volume_info.volume_type { VolumeType::Fat(fat) => { - fat.find_directory_entry(&self.block_device, &data.open_dirs[directory_idx], &sfn) + fat.find_directory_entry(&mut block_cache, &data.open_dirs[directory_idx], &sfn) } }; @@ -468,7 +484,7 @@ where let volume_idx = data.get_volume_by_id(volume_id)?; let entry = match &mut data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => fat.write_new_directory_entry( - &self.block_device, + &mut block_cache, &self.time_source, cluster, sfn, @@ -551,14 +567,14 @@ where }; match &mut data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => { - fat.truncate_cluster_chain(&self.block_device, file.entry.cluster)? + fat.truncate_cluster_chain(&mut block_cache, file.entry.cluster)? } }; file.update_length(0); match &data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => { file.entry.mtime = self.time_source.get_timestamp(); - fat.write_entry_to_disk(&self.block_device, &file.entry)?; + fat.write_entry_to_disk(&mut block_cache, &file.entry)?; } }; @@ -586,7 +602,11 @@ where where N: ToShortFileName, { - let data = self.data.borrow(); + let data = self.data.try_borrow().map_err(|_| Error::LockError)?; + let mut block_cache = self + .block_cache + .try_borrow_mut() + .map_err(|_| Error::LockError)?; let dir_idx = data.get_dir_by_id(directory)?; let dir_info = &data.open_dirs[dir_idx]; @@ -594,7 +614,7 @@ where let sfn = name.to_short_filename().map_err(Error::FilenameError)?; let dir_entry = match &data.open_volumes[volume_idx].volume_type { - VolumeType::Fat(fat) => fat.find_directory_entry(&self.block_device, dir_info, &sfn), + VolumeType::Fat(fat) => fat.find_directory_entry(&mut block_cache, dir_info, &sfn), }?; if dir_entry.attributes.is_directory() { @@ -607,9 +627,7 @@ where let volume_idx = data.get_volume_by_id(dir_info.raw_volume)?; match &data.open_volumes[volume_idx].volume_type { - VolumeType::Fat(fat) => { - fat.delete_directory_entry(&self.block_device, dir_info, &sfn)? - } + VolumeType::Fat(fat) => fat.delete_directory_entry(&mut block_cache, dir_info, &sfn)?, } Ok(()) @@ -625,7 +643,7 @@ where ) -> Result, Error> { debug!("Reading volume label for {:?}", raw_volume); // prefer the one in the BPB - it's easier to get - let data = self.data.borrow(); + let data = self.data.try_borrow().map_err(|_| Error::LockError)?; let volume_idx = data.get_volume_by_id(raw_volume)?; match &data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => { @@ -662,6 +680,10 @@ where /// Read from an open file. pub fn read(&self, file: RawFile, buffer: &mut [u8]) -> Result> { let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let mut block_cache = self + .block_cache + .try_borrow_mut() + .map_err(|_| Error::LockError)?; let file_idx = data.get_file_by_id(file)?; let volume_idx = data.get_volume_by_id(data.open_files[file_idx].raw_volume)?; @@ -674,19 +696,15 @@ where while space > 0 && !data.open_files[file_idx].eof() { let mut current_cluster = data.open_files[file_idx].current_cluster; let (block_idx, block_offset, block_avail) = data.find_data_on_disk( - &self.block_device, + &mut block_cache, volume_idx, &mut current_cluster, data.open_files[file_idx].entry.cluster, data.open_files[file_idx].current_offset, )?; data.open_files[file_idx].current_cluster = current_cluster; - let mut blocks = [Block::new()]; trace!("Reading file ID {:?}", file); - self.block_device - .read(&mut blocks, block_idx) - .map_err(Error::DeviceError)?; - let block = &blocks[0]; + let block = block_cache.read(block_idx).map_err(Error::DeviceError)?; let to_copy = block_avail .min(space) .min(data.open_files[file_idx].left() as usize); @@ -711,6 +729,10 @@ where debug!("write(file={:?}, buffer={:x?}", file, buffer); let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let mut block_cache = self + .block_cache + .try_borrow_mut() + .map_err(|_| Error::LockError)?; // Clone this so we can touch our other structures. Need to ensure we // write it back at the end. @@ -725,12 +747,11 @@ where if data.open_files[file_idx].entry.cluster.0 < RESERVED_ENTRIES { // file doesn't have a valid allocated cluster (possible zero-length file), allocate one - data.open_files[file_idx].entry.cluster = - match data.open_volumes[volume_idx].volume_type { - VolumeType::Fat(ref mut fat) => { - fat.alloc_cluster(&self.block_device, None, false)? - } - }; + data.open_files[file_idx].entry.cluster = match data.open_volumes[volume_idx] + .volume_type + { + VolumeType::Fat(ref mut fat) => fat.alloc_cluster(&mut block_cache, None, false)?, + }; debug!( "Alloc first cluster {:?}", data.open_files[file_idx].entry.cluster @@ -759,7 +780,7 @@ where ); let current_offset = data.open_files[file_idx].current_offset; let (block_idx, block_offset, block_avail) = match data.find_data_on_disk( - &self.block_device, + &mut block_cache, volume_idx, &mut current_cluster, data.open_files[file_idx].entry.cluster, @@ -777,7 +798,7 @@ where match data.open_volumes[volume_idx].volume_type { VolumeType::Fat(ref mut fat) => { if fat - .alloc_cluster(&self.block_device, Some(current_cluster.1), false) + .alloc_cluster(&mut block_cache, Some(current_cluster.1), false) .is_err() { return Err(Error::DiskFull); @@ -785,7 +806,7 @@ where debug!("Allocated new FAT cluster, finding offsets..."); let new_offset = data .find_data_on_disk( - &self.block_device, + &mut block_cache, volume_idx, &mut current_cluster, data.open_files[file_idx].entry.cluster, @@ -799,21 +820,19 @@ where } Err(e) => return Err(e), }; - let mut blocks = [Block::new()]; let to_copy = core::cmp::min(block_avail, bytes_to_write - written); - if block_offset != 0 { + let block = if block_offset != 0 { debug!("Reading for partial block write"); - self.block_device - .read(&mut blocks, block_idx) - .map_err(Error::DeviceError)?; - } - let block = &mut blocks[0]; + block_cache + .read_mut(block_idx) + .map_err(Error::DeviceError)? + } else { + block_cache.blank_mut(block_idx) + }; block[block_offset..block_offset + to_copy] .copy_from_slice(&buffer[written..written + to_copy]); debug!("Writing block {:?}", block_idx); - self.block_device - .write(&blocks, block_idx) - .map_err(Error::DeviceError)?; + block_cache.write_back()?; written += to_copy; data.open_files[file_idx].current_cluster = current_cluster; @@ -846,6 +865,10 @@ where pub fn flush_file(&self, file: RawFile) -> Result<(), Error> { use core::ops::DerefMut; let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let mut block_cache = self + .block_cache + .try_borrow_mut() + .map_err(|_| Error::LockError)?; let data = data.deref_mut(); let file_id = data.get_file_by_id(file)?; @@ -855,13 +878,13 @@ where match &mut data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => { debug!("Updating FAT info sector"); - fat.update_info_sector(&self.block_device)?; + fat.update_info_sector(&mut block_cache)?; debug!("Updating dir entry {:?}", data.open_files[file_id].entry); if data.open_files[file_id].entry.size != 0 { // If you have a length, you must have a cluster assert!(data.open_files[file_id].entry.cluster.0 != 0); } - fat.write_entry_to_disk(&self.block_device, &data.open_files[file_id].entry)?; + fat.write_entry_to_disk(&mut block_cache, &data.open_files[file_id].entry)?; } }; } @@ -876,12 +899,13 @@ where /// Consume self and return BlockDevice and TimeSource pub fn free(self) -> (D, T) { - (self.block_device, self.time_source) + let block_cache = self.block_cache.into_inner(); + (block_cache.free(), self.time_source) } /// Check if a file is at End Of File. pub fn file_eof(&self, file: RawFile) -> Result> { - let data = self.data.borrow(); + let data = self.data.try_borrow().map_err(|_| Error::LockError)?; let file_idx = data.get_file_by_id(file)?; Ok(data.open_files[file_idx].eof()) } @@ -922,14 +946,14 @@ where /// Get the length of a file pub fn file_length(&self, file: RawFile) -> Result> { - let data = self.data.borrow(); + let data = self.data.try_borrow().map_err(|_| Error::LockError)?; let file_idx = data.get_file_by_id(file)?; Ok(data.open_files[file_idx].length()) } /// Get the current offset of a file pub fn file_offset(&self, file: RawFile) -> Result> { - let data = self.data.borrow(); + let data = self.data.try_borrow().map_err(|_| Error::LockError)?; let file_idx = data.get_file_by_id(file)?; Ok(data.open_files[file_idx].current_offset) } @@ -945,6 +969,10 @@ where { use core::ops::DerefMut; let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let mut block_cache = self + .block_cache + .try_borrow_mut() + .map_err(|_| Error::LockError)?; let data = data.deref_mut(); // This check is load-bearing - we do an unchecked push later. @@ -968,7 +996,7 @@ where // Does an entry exist with this name? let maybe_dir_entry = match &volume_info.volume_type { VolumeType::Fat(fat) => { - fat.find_directory_entry(&self.block_device, parent_directory_info, &sfn) + fat.find_directory_entry(&mut block_cache, parent_directory_info, &sfn) } }; @@ -995,7 +1023,7 @@ where VolumeType::Fat(fat) => { debug!("Making dir entry"); let mut new_dir_entry_in_parent = fat.write_new_directory_entry( - &self.block_device, + &mut block_cache, &self.time_source, parent_directory_info.cluster, sfn, @@ -1003,16 +1031,16 @@ where )?; if new_dir_entry_in_parent.cluster == ClusterId::EMPTY { new_dir_entry_in_parent.cluster = - fat.alloc_cluster(&self.block_device, None, false)?; + fat.alloc_cluster(&mut block_cache, None, false)?; // update the parent dir with the cluster of the new dir - fat.write_entry_to_disk(&self.block_device, &new_dir_entry_in_parent)?; + fat.write_entry_to_disk(&mut block_cache, &new_dir_entry_in_parent)?; } let new_dir_start_block = fat.cluster_to_block(new_dir_entry_in_parent.cluster); debug!("Made new dir entry {:?}", new_dir_entry_in_parent); let now = self.time_source.get_timestamp(); let fat_type = fat.get_fat_type(); // A blank block - let mut blocks = [Block::new()]; + let block = block_cache.blank_mut(new_dir_start_block); // make the "." entry let dot_entry_in_child = DirEntry { name: crate::ShortFileName::this_dir(), @@ -1027,7 +1055,7 @@ where }; debug!("New dir has {:?}", dot_entry_in_child); let mut offset = 0; - blocks[0][offset..offset + OnDiskDirEntry::LEN] + block[offset..offset + OnDiskDirEntry::LEN] .copy_from_slice(&dot_entry_in_child.serialize(fat_type)[..]); offset += OnDiskDirEntry::LEN; // make the ".." entry @@ -1053,24 +1081,17 @@ where entry_offset: OnDiskDirEntry::LEN_U32, }; debug!("New dir has {:?}", dot_dot_entry_in_child); - blocks[0][offset..offset + OnDiskDirEntry::LEN] + block[offset..offset + OnDiskDirEntry::LEN] .copy_from_slice(&dot_dot_entry_in_child.serialize(fat_type)[..]); - self.block_device - .write(&blocks, new_dir_start_block) - .map_err(Error::DeviceError)?; + block_cache.write_back()?; - // Now zero the rest of the cluster - for b in blocks[0].iter_mut() { - *b = 0; - } - for block in new_dir_start_block + for block_idx in new_dir_start_block .range(BlockCount(u32::from(fat.blocks_per_cluster))) .skip(1) { - self.block_device - .write(&blocks, block) - .map_err(Error::DeviceError)?; + let _block = block_cache.blank_mut(block_idx); + block_cache.write_back()?; } } }; @@ -1160,7 +1181,7 @@ impl /// * how many bytes remain in that block. fn find_data_on_disk( &self, - block_device: &D, + block_cache: &mut BlockCache, volume_idx: usize, start: &mut (u32, ClusterId), file_start: ClusterId, @@ -1183,12 +1204,9 @@ impl let offset_from_cluster = desired_offset - start.0; // walk through the FAT chain let num_clusters = offset_from_cluster / bytes_per_cluster; - let mut block_cache = BlockCache::empty(); for _ in 0..num_clusters { start.1 = match &self.open_volumes[volume_idx].volume_type { - VolumeType::Fat(fat) => { - fat.next_cluster(block_device, start.1, &mut block_cache)? - } + VolumeType::Fat(fat) => fat.next_cluster(block_cache, start.1)?, }; start.0 += bytes_per_cluster; } From 2fbd6cb945a4474c88c592dcc6ab10771d54fd83 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Wed, 16 Oct 2024 22:05:59 +0100 Subject: [PATCH 21/62] Pack the block cache into the existing RefCell. --- src/volume_mgr.rs | 215 ++++++++++++++++++++++------------------------ 1 file changed, 102 insertions(+), 113 deletions(-) diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index ab417246..75140d07 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -4,20 +4,20 @@ use core::cell::RefCell; use core::convert::TryFrom; +use core::ops::DerefMut; use byteorder::{ByteOrder, LittleEndian}; use heapless::Vec; -use crate::fat::{self, FatType, OnDiskDirEntry, RESERVED_ENTRIES}; - -use crate::filesystem::{ - Attributes, ClusterId, DirEntry, DirectoryInfo, FileInfo, HandleGenerator, Mode, RawDirectory, - RawFile, TimeSource, ToShortFileName, MAX_FILE_SIZE, -}; use crate::{ - debug, trace, Block, BlockCache, BlockCount, BlockDevice, BlockIdx, Error, RawVolume, - ShortFileName, Volume, VolumeIdx, VolumeInfo, VolumeType, PARTITION_ID_FAT16, - PARTITION_ID_FAT16_LBA, PARTITION_ID_FAT32_CHS_LBA, PARTITION_ID_FAT32_LBA, + debug, fat, + filesystem::{ + Attributes, ClusterId, DirEntry, DirectoryInfo, FileInfo, HandleGenerator, Mode, + RawDirectory, RawFile, TimeSource, ToShortFileName, MAX_FILE_SIZE, + }, + trace, Block, BlockCache, BlockCount, BlockDevice, BlockIdx, Error, RawVolume, ShortFileName, + Volume, VolumeIdx, VolumeInfo, VolumeType, PARTITION_ID_FAT16, PARTITION_ID_FAT16_LBA, + PARTITION_ID_FAT32_CHS_LBA, PARTITION_ID_FAT32_LBA, }; /// Wraps a block device and gives access to the FAT-formatted volumes within @@ -38,8 +38,7 @@ pub struct VolumeManager< ::Error: core::fmt::Debug, { time_source: T, - block_cache: RefCell>, - data: RefCell>, + data: RefCell>, } impl VolumeManager @@ -83,9 +82,9 @@ where ) -> VolumeManager { debug!("Creating new embedded-sdmmc::VolumeManager"); VolumeManager { - block_cache: RefCell::new(BlockCache::new(block_device)), time_source, data: RefCell::new(VolumeManagerData { + block_cache: BlockCache::new(block_device), id_generator: HandleGenerator::new(id_offset), open_volumes: Vec::new(), open_dirs: Vec::new(), @@ -99,8 +98,8 @@ where where F: FnOnce(&mut D) -> T, { - let mut block_cache = self.block_cache.borrow_mut(); - let result = f(block_cache.block_device()); + let mut data = self.data.borrow_mut(); + let result = f(data.block_cache.block_device()); result } @@ -137,10 +136,6 @@ where const PARTITION_INFO_NUM_BLOCKS_INDEX: usize = 12; let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; - let mut block_cache = self - .block_cache - .try_borrow_mut() - .map_err(|_| Error::LockError)?; if data.open_volumes.is_full() { return Err(Error::TooManyOpenVolumes); @@ -154,7 +149,10 @@ where let (part_type, lba_start, num_blocks) = { trace!("Reading partition table"); - let block = block_cache.read(BlockIdx(0)).map_err(Error::DeviceError)?; + let block = data + .block_cache + .read(BlockIdx(0)) + .map_err(Error::DeviceError)?; // We only support Master Boot Record (MBR) partitioned cards, not // GUID Partition Table (GPT) if LittleEndian::read_u16(&block[FOOTER_START..FOOTER_START + 2]) != FOOTER_VALUE { @@ -198,7 +196,7 @@ where | PARTITION_ID_FAT32_LBA | PARTITION_ID_FAT16_LBA | PARTITION_ID_FAT16 => { - let volume = fat::parse_volume(&mut block_cache, lba_start, num_blocks)?; + let volume = fat::parse_volume(&mut data.block_cache, lba_start, num_blocks)?; let id = RawVolume(data.id_generator.generate()); let info = VolumeInfo { raw_volume: id, @@ -253,10 +251,7 @@ where N: ToShortFileName, { let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; - let mut block_cache = self - .block_cache - .try_borrow_mut() - .map_err(|_| Error::LockError)?; + let data = data.deref_mut(); if data.open_dirs.is_full() { return Err(Error::TooManyOpenDirs); @@ -289,7 +284,7 @@ where let dir_entry = match &data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => fat.find_directory_entry( - &mut block_cache, + &mut data.block_cache, &data.open_dirs[parent_dir_idx], &short_file_name, )?, @@ -368,18 +363,19 @@ where where N: ToShortFileName, { - let data = self.data.try_borrow().map_err(|_| Error::LockError)?; - let mut block_cache = self - .block_cache - .try_borrow_mut() - .map_err(|_| Error::LockError)?; + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); let directory_idx = data.get_dir_by_id(directory)?; let volume_idx = data.get_volume_by_id(data.open_dirs[directory_idx].raw_volume)?; match &data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => { let sfn = name.to_short_filename().map_err(Error::FilenameError)?; - fat.find_directory_entry(&mut block_cache, &data.open_dirs[directory_idx], &sfn) + fat.find_directory_entry( + &mut data.block_cache, + &data.open_dirs[directory_idx], + &sfn, + ) } } } @@ -397,17 +393,14 @@ where where F: FnMut(&DirEntry), { - let data = self.data.try_borrow().map_err(|_| Error::LockError)?; - let mut block_cache = self - .block_cache - .try_borrow_mut() - .map_err(|_| Error::LockError)?; + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); let directory_idx = data.get_dir_by_id(directory)?; let volume_idx = data.get_volume_by_id(data.open_dirs[directory_idx].raw_volume)?; match &data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => { - fat.iterate_dir(&mut block_cache, &data.open_dirs[directory_idx], func) + fat.iterate_dir(&mut data.block_cache, &data.open_dirs[directory_idx], func) } } } @@ -423,10 +416,7 @@ where N: ToShortFileName, { let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; - let mut block_cache = self - .block_cache - .try_borrow_mut() - .map_err(|_| Error::LockError)?; + let data = data.deref_mut(); // This check is load-bearing - we do an unchecked push later. if data.open_files.is_full() { @@ -440,9 +430,11 @@ where let sfn = name.to_short_filename().map_err(Error::FilenameError)?; let dir_entry = match &volume_info.volume_type { - VolumeType::Fat(fat) => { - fat.find_directory_entry(&mut block_cache, &data.open_dirs[directory_idx], &sfn) - } + VolumeType::Fat(fat) => fat.find_directory_entry( + &mut data.block_cache, + &data.open_dirs[directory_idx], + &sfn, + ), }; let dir_entry = match dir_entry { @@ -484,7 +476,7 @@ where let volume_idx = data.get_volume_by_id(volume_id)?; let entry = match &mut data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => fat.write_new_directory_entry( - &mut block_cache, + &mut data.block_cache, &self.time_source, cluster, sfn, @@ -566,15 +558,16 @@ where dirty: false, }; match &mut data.open_volumes[volume_idx].volume_type { - VolumeType::Fat(fat) => { - fat.truncate_cluster_chain(&mut block_cache, file.entry.cluster)? - } + VolumeType::Fat(fat) => fat.truncate_cluster_chain( + &mut data.block_cache, + file.entry.cluster, + )?, }; file.update_length(0); match &data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => { file.entry.mtime = self.time_source.get_timestamp(); - fat.write_entry_to_disk(&mut block_cache, &file.entry)?; + fat.write_entry_to_disk(&mut data.block_cache, &file.entry)?; } }; @@ -602,11 +595,8 @@ where where N: ToShortFileName, { - let data = self.data.try_borrow().map_err(|_| Error::LockError)?; - let mut block_cache = self - .block_cache - .try_borrow_mut() - .map_err(|_| Error::LockError)?; + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); let dir_idx = data.get_dir_by_id(directory)?; let dir_info = &data.open_dirs[dir_idx]; @@ -614,7 +604,7 @@ where let sfn = name.to_short_filename().map_err(Error::FilenameError)?; let dir_entry = match &data.open_volumes[volume_idx].volume_type { - VolumeType::Fat(fat) => fat.find_directory_entry(&mut block_cache, dir_info, &sfn), + VolumeType::Fat(fat) => fat.find_directory_entry(&mut data.block_cache, dir_info, &sfn), }?; if dir_entry.attributes.is_directory() { @@ -627,7 +617,9 @@ where let volume_idx = data.get_volume_by_id(dir_info.raw_volume)?; match &data.open_volumes[volume_idx].volume_type { - VolumeType::Fat(fat) => fat.delete_directory_entry(&mut block_cache, dir_info, &sfn)?, + VolumeType::Fat(fat) => { + fat.delete_directory_entry(&mut data.block_cache, dir_info, &sfn)? + } } Ok(()) @@ -680,10 +672,7 @@ where /// Read from an open file. pub fn read(&self, file: RawFile, buffer: &mut [u8]) -> Result> { let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; - let mut block_cache = self - .block_cache - .try_borrow_mut() - .map_err(|_| Error::LockError)?; + let data = data.deref_mut(); let file_idx = data.get_file_by_id(file)?; let volume_idx = data.get_volume_by_id(data.open_files[file_idx].raw_volume)?; @@ -696,7 +685,6 @@ where while space > 0 && !data.open_files[file_idx].eof() { let mut current_cluster = data.open_files[file_idx].current_cluster; let (block_idx, block_offset, block_avail) = data.find_data_on_disk( - &mut block_cache, volume_idx, &mut current_cluster, data.open_files[file_idx].entry.cluster, @@ -704,7 +692,10 @@ where )?; data.open_files[file_idx].current_cluster = current_cluster; trace!("Reading file ID {:?}", file); - let block = block_cache.read(block_idx).map_err(Error::DeviceError)?; + let block = data + .block_cache + .read(block_idx) + .map_err(Error::DeviceError)?; let to_copy = block_avail .min(space) .min(data.open_files[file_idx].left() as usize); @@ -729,10 +720,7 @@ where debug!("write(file={:?}, buffer={:x?}", file, buffer); let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; - let mut block_cache = self - .block_cache - .try_borrow_mut() - .map_err(|_| Error::LockError)?; + let data = data.deref_mut(); // Clone this so we can touch our other structures. Need to ensure we // write it back at the end. @@ -745,13 +733,14 @@ where data.open_files[file_idx].dirty = true; - if data.open_files[file_idx].entry.cluster.0 < RESERVED_ENTRIES { + if data.open_files[file_idx].entry.cluster.0 < fat::RESERVED_ENTRIES { // file doesn't have a valid allocated cluster (possible zero-length file), allocate one - data.open_files[file_idx].entry.cluster = match data.open_volumes[volume_idx] - .volume_type - { - VolumeType::Fat(ref mut fat) => fat.alloc_cluster(&mut block_cache, None, false)?, - }; + data.open_files[file_idx].entry.cluster = + match data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(ref mut fat) => { + fat.alloc_cluster(&mut data.block_cache, None, false)? + } + }; debug!( "Alloc first cluster {:?}", data.open_files[file_idx].entry.cluster @@ -780,7 +769,6 @@ where ); let current_offset = data.open_files[file_idx].current_offset; let (block_idx, block_offset, block_avail) = match data.find_data_on_disk( - &mut block_cache, volume_idx, &mut current_cluster, data.open_files[file_idx].entry.cluster, @@ -798,7 +786,11 @@ where match data.open_volumes[volume_idx].volume_type { VolumeType::Fat(ref mut fat) => { if fat - .alloc_cluster(&mut block_cache, Some(current_cluster.1), false) + .alloc_cluster( + &mut data.block_cache, + Some(current_cluster.1), + false, + ) .is_err() { return Err(Error::DiskFull); @@ -806,7 +798,6 @@ where debug!("Allocated new FAT cluster, finding offsets..."); let new_offset = data .find_data_on_disk( - &mut block_cache, volume_idx, &mut current_cluster, data.open_files[file_idx].entry.cluster, @@ -823,16 +814,16 @@ where let to_copy = core::cmp::min(block_avail, bytes_to_write - written); let block = if block_offset != 0 { debug!("Reading for partial block write"); - block_cache + data.block_cache .read_mut(block_idx) .map_err(Error::DeviceError)? } else { - block_cache.blank_mut(block_idx) + data.block_cache.blank_mut(block_idx) }; block[block_offset..block_offset + to_copy] .copy_from_slice(&buffer[written..written + to_copy]); debug!("Writing block {:?}", block_idx); - block_cache.write_back()?; + data.block_cache.write_back()?; written += to_copy; data.open_files[file_idx].current_cluster = current_cluster; @@ -863,12 +854,7 @@ where /// Flush (update the entry) for a file with the given raw file handle. pub fn flush_file(&self, file: RawFile) -> Result<(), Error> { - use core::ops::DerefMut; let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; - let mut block_cache = self - .block_cache - .try_borrow_mut() - .map_err(|_| Error::LockError)?; let data = data.deref_mut(); let file_id = data.get_file_by_id(file)?; @@ -878,13 +864,16 @@ where match &mut data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => { debug!("Updating FAT info sector"); - fat.update_info_sector(&mut block_cache)?; + fat.update_info_sector(&mut data.block_cache)?; debug!("Updating dir entry {:?}", data.open_files[file_id].entry); if data.open_files[file_id].entry.size != 0 { // If you have a length, you must have a cluster assert!(data.open_files[file_id].entry.cluster.0 != 0); } - fat.write_entry_to_disk(&mut block_cache, &data.open_files[file_id].entry)?; + fat.write_entry_to_disk( + &mut data.block_cache, + &data.open_files[file_id].entry, + )?; } }; } @@ -899,8 +888,8 @@ where /// Consume self and return BlockDevice and TimeSource pub fn free(self) -> (D, T) { - let block_cache = self.block_cache.into_inner(); - (block_cache.free(), self.time_source) + let data = self.data.into_inner(); + (data.block_cache.free(), self.time_source) } /// Check if a file is at End Of File. @@ -967,12 +956,7 @@ where where N: ToShortFileName, { - use core::ops::DerefMut; let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; - let mut block_cache = self - .block_cache - .try_borrow_mut() - .map_err(|_| Error::LockError)?; let data = data.deref_mut(); // This check is load-bearing - we do an unchecked push later. @@ -996,7 +980,7 @@ where // Does an entry exist with this name? let maybe_dir_entry = match &volume_info.volume_type { VolumeType::Fat(fat) => { - fat.find_directory_entry(&mut block_cache, parent_directory_info, &sfn) + fat.find_directory_entry(&mut data.block_cache, parent_directory_info, &sfn) } }; @@ -1023,7 +1007,7 @@ where VolumeType::Fat(fat) => { debug!("Making dir entry"); let mut new_dir_entry_in_parent = fat.write_new_directory_entry( - &mut block_cache, + &mut data.block_cache, &self.time_source, parent_directory_info.cluster, sfn, @@ -1031,16 +1015,16 @@ where )?; if new_dir_entry_in_parent.cluster == ClusterId::EMPTY { new_dir_entry_in_parent.cluster = - fat.alloc_cluster(&mut block_cache, None, false)?; + fat.alloc_cluster(&mut data.block_cache, None, false)?; // update the parent dir with the cluster of the new dir - fat.write_entry_to_disk(&mut block_cache, &new_dir_entry_in_parent)?; + fat.write_entry_to_disk(&mut data.block_cache, &new_dir_entry_in_parent)?; } let new_dir_start_block = fat.cluster_to_block(new_dir_entry_in_parent.cluster); debug!("Made new dir entry {:?}", new_dir_entry_in_parent); let now = self.time_source.get_timestamp(); let fat_type = fat.get_fat_type(); // A blank block - let block = block_cache.blank_mut(new_dir_start_block); + let block = data.block_cache.blank_mut(new_dir_start_block); // make the "." entry let dot_entry_in_child = DirEntry { name: crate::ShortFileName::this_dir(), @@ -1055,9 +1039,9 @@ where }; debug!("New dir has {:?}", dot_entry_in_child); let mut offset = 0; - block[offset..offset + OnDiskDirEntry::LEN] + block[offset..offset + fat::OnDiskDirEntry::LEN] .copy_from_slice(&dot_entry_in_child.serialize(fat_type)[..]); - offset += OnDiskDirEntry::LEN; + offset += fat::OnDiskDirEntry::LEN; // make the ".." entry let dot_dot_entry_in_child = DirEntry { name: crate::ShortFileName::parent_dir(), @@ -1066,7 +1050,7 @@ where attributes: att, // point at our parent cluster: match fat_type { - FatType::Fat16 => { + fat::FatType::Fat16 => { // On FAT16, indicate parent is root using Cluster(0) if parent_directory_info.cluster == ClusterId::ROOT_DIR { ClusterId::EMPTY @@ -1074,24 +1058,24 @@ where parent_directory_info.cluster } } - FatType::Fat32 => parent_directory_info.cluster, + fat::FatType::Fat32 => parent_directory_info.cluster, }, size: 0, entry_block: new_dir_start_block, - entry_offset: OnDiskDirEntry::LEN_U32, + entry_offset: fat::OnDiskDirEntry::LEN_U32, }; debug!("New dir has {:?}", dot_dot_entry_in_child); - block[offset..offset + OnDiskDirEntry::LEN] + block[offset..offset + fat::OnDiskDirEntry::LEN] .copy_from_slice(&dot_dot_entry_in_child.serialize(fat_type)[..]); - block_cache.write_back()?; + data.block_cache.write_back()?; for block_idx in new_dir_start_block .range(BlockCount(u32::from(fat.blocks_per_cluster))) .skip(1) { - let _block = block_cache.blank_mut(block_idx); - block_cache.write_back()?; + let _block = data.block_cache.blank_mut(block_idx); + data.block_cache.write_back()?; } } }; @@ -1106,18 +1090,24 @@ where #[derive(Debug)] struct VolumeManagerData< + D, const MAX_DIRS: usize = 4, const MAX_FILES: usize = 4, const MAX_VOLUMES: usize = 1, -> { +> where + D: BlockDevice, +{ id_generator: HandleGenerator, + block_cache: BlockCache, open_volumes: Vec, open_dirs: Vec, open_files: Vec, } -impl - VolumeManagerData +impl + VolumeManagerData +where + D: BlockDevice, { /// Check if a file is open /// @@ -1179,9 +1169,8 @@ impl /// * the index for the block on the disk that contains the data we want, /// * the byte offset into that block for the data we want, and /// * how many bytes remain in that block. - fn find_data_on_disk( - &self, - block_cache: &mut BlockCache, + fn find_data_on_disk( + &mut self, volume_idx: usize, start: &mut (u32, ClusterId), file_start: ClusterId, @@ -1206,7 +1195,7 @@ impl let num_clusters = offset_from_cluster / bytes_per_cluster; for _ in 0..num_clusters { start.1 = match &self.open_volumes[volume_idx].volume_type { - VolumeType::Fat(fat) => fat.next_cluster(block_cache, start.1)?, + VolumeType::Fat(fat) => fat.next_cluster(&mut self.block_cache, start.1)?, }; start.0 += bytes_per_cluster; } From 242e05f9d0a58c12aa3c08591408ad2d9bdf5686 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Wed, 16 Oct 2024 22:08:13 +0100 Subject: [PATCH 22/62] Invalidate cache if read fails. --- src/blockdevice.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/blockdevice.rs b/src/blockdevice.rs index 4895ade2..618eda23 100644 --- a/src/blockdevice.rs +++ b/src/blockdevice.rs @@ -112,6 +112,7 @@ where /// Read a block, and return a reference to it. pub fn read(&mut self, block_idx: BlockIdx) -> Result<&Block, D::Error> { if self.block_idx != Some(block_idx) { + self.block_idx = None; self.block_device.read(&mut self.block, block_idx)?; self.block_idx = Some(block_idx); } @@ -121,6 +122,7 @@ where /// Read a block, and return a reference to it. pub fn read_mut(&mut self, block_idx: BlockIdx) -> Result<&mut Block, D::Error> { if self.block_idx != Some(block_idx) { + self.block_idx = None; self.block_device.read(&mut self.block, block_idx)?; self.block_idx = Some(block_idx); } From c89b01b27d923b789b647fa53b6904c13d2d9989 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 5 Oct 2024 15:10:43 +0100 Subject: [PATCH 23/62] Create a VolumeManager type alias Makes the example code a bit simple. --- examples/append_file.rs | 7 ++++--- examples/big_dir.rs | 7 ++++--- examples/create_file.rs | 7 ++++--- examples/delete_file.rs | 7 ++++--- examples/list_dir.rs | 20 +++++++++----------- examples/read_file.rs | 7 ++++--- 6 files changed, 29 insertions(+), 26 deletions(-) diff --git a/examples/append_file.rs b/examples/append_file.rs index bbda0098..2c7dd8e2 100644 --- a/examples/append_file.rs +++ b/examples/append_file.rs @@ -22,7 +22,9 @@ use linux::*; const FILE_TO_APPEND: &str = "README.TXT"; -use embedded_sdmmc::{Error, Mode, VolumeIdx, VolumeManager}; +use embedded_sdmmc::{Error, Mode, VolumeIdx}; + +type VolumeManager = embedded_sdmmc::VolumeManager; fn main() -> Result<(), embedded_sdmmc::Error> { env_logger::init(); @@ -30,8 +32,7 @@ fn main() -> Result<(), embedded_sdmmc::Error> { let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; - let volume_mgr: VolumeManager = - VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); let volume = volume_mgr.open_volume(VolumeIdx(0))?; let root_dir = volume.open_root_dir()?; println!("\nCreating file {}...", FILE_TO_APPEND); diff --git a/examples/big_dir.rs b/examples/big_dir.rs index 98ad90a3..a0170267 100644 --- a/examples/big_dir.rs +++ b/examples/big_dir.rs @@ -3,7 +3,9 @@ extern crate embedded_sdmmc; mod linux; use linux::*; -use embedded_sdmmc::{Error, VolumeManager}; +use embedded_sdmmc::Error; + +type VolumeManager = embedded_sdmmc::VolumeManager; fn main() -> Result<(), embedded_sdmmc::Error> { env_logger::init(); @@ -11,8 +13,7 @@ fn main() -> Result<(), embedded_sdmmc::Error> { let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; - let volume_mgr: VolumeManager = - VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); let volume = volume_mgr .open_volume(embedded_sdmmc::VolumeIdx(1)) .unwrap(); diff --git a/examples/create_file.rs b/examples/create_file.rs index fa96d075..cc8b1935 100644 --- a/examples/create_file.rs +++ b/examples/create_file.rs @@ -22,7 +22,9 @@ use linux::*; const FILE_TO_CREATE: &str = "CREATE.TXT"; -use embedded_sdmmc::{Error, Mode, VolumeIdx, VolumeManager}; +use embedded_sdmmc::{Error, Mode, VolumeIdx}; + +type VolumeManager = embedded_sdmmc::VolumeManager; fn main() -> Result<(), embedded_sdmmc::Error> { env_logger::init(); @@ -30,8 +32,7 @@ fn main() -> Result<(), embedded_sdmmc::Error> { let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; - let volume_mgr: VolumeManager = - VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); let volume = volume_mgr.open_volume(VolumeIdx(0))?; let root_dir = volume.open_root_dir()?; println!("\nCreating file {}...", FILE_TO_CREATE); diff --git a/examples/delete_file.rs b/examples/delete_file.rs index f76d3601..4d882132 100644 --- a/examples/delete_file.rs +++ b/examples/delete_file.rs @@ -25,7 +25,9 @@ use linux::*; const FILE_TO_DELETE: &str = "README.TXT"; -use embedded_sdmmc::{Error, VolumeIdx, VolumeManager}; +use embedded_sdmmc::{Error, VolumeIdx}; + +type VolumeManager = embedded_sdmmc::VolumeManager; fn main() -> Result<(), embedded_sdmmc::Error> { env_logger::init(); @@ -33,8 +35,7 @@ fn main() -> Result<(), embedded_sdmmc::Error> { let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; - let volume_mgr: VolumeManager = - VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); let volume = volume_mgr.open_volume(VolumeIdx(0))?; let root_dir = volume.open_root_dir()?; println!("Deleting file {}...", FILE_TO_DELETE); diff --git a/examples/list_dir.rs b/examples/list_dir.rs index 60d72943..00578494 100644 --- a/examples/list_dir.rs +++ b/examples/list_dir.rs @@ -32,23 +32,24 @@ //! $ cargo run --example list_dir -- ./disk.img //! ``` -extern crate embedded_sdmmc; - mod linux; use linux::*; -use embedded_sdmmc::{Directory, VolumeIdx, VolumeManager}; +use embedded_sdmmc::{ShortFileName, VolumeIdx}; type Error = embedded_sdmmc::Error; +type Directory<'a> = embedded_sdmmc::Directory<'a, LinuxBlockDevice, Clock, 8, 4, 4>; +type VolumeManager = embedded_sdmmc::VolumeManager; + fn main() -> Result<(), Error> { env_logger::init(); let mut args = std::env::args().skip(1); let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); + let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; - let volume_mgr: VolumeManager = - VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); let volume = volume_mgr.open_volume(VolumeIdx(0))?; let root_dir = volume.open_root_dir()?; list_dir(root_dir, "/")?; @@ -58,10 +59,7 @@ fn main() -> Result<(), Error> { /// Recursively print a directory listing for the open directory given. /// /// The path is for display purposes only. -fn list_dir( - directory: Directory, - path: &str, -) -> Result<(), Error> { +fn list_dir(directory: Directory<'_>, path: &str) -> Result<(), Error> { println!("Listing {}", path); let mut children = Vec::new(); directory.iterate_dir(|entry| { @@ -77,8 +75,8 @@ fn list_dir( } ); if entry.attributes.is_directory() - && entry.name != embedded_sdmmc::ShortFileName::parent_dir() - && entry.name != embedded_sdmmc::ShortFileName::this_dir() + && entry.name != ShortFileName::parent_dir() + && entry.name != ShortFileName::this_dir() { children.push(entry.name.clone()); } diff --git a/examples/read_file.rs b/examples/read_file.rs index f962b75a..e8d900cc 100644 --- a/examples/read_file.rs +++ b/examples/read_file.rs @@ -39,7 +39,9 @@ use linux::*; const FILE_TO_READ: &str = "README.TXT"; -use embedded_sdmmc::{Error, Mode, VolumeIdx, VolumeManager}; +use embedded_sdmmc::{Error, Mode, VolumeIdx}; + +type VolumeManager = embedded_sdmmc::VolumeManager; fn main() -> Result<(), embedded_sdmmc::Error> { env_logger::init(); @@ -47,8 +49,7 @@ fn main() -> Result<(), embedded_sdmmc::Error> { let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; - let volume_mgr: VolumeManager = - VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); let volume = volume_mgr.open_volume(VolumeIdx(0))?; let root_dir = volume.open_root_dir()?; println!("\nReading file {}...", FILE_TO_READ); From 7478430e022a22fff2b4f74b2731f3be6cc0709b Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Fri, 25 Oct 2024 16:54:54 +0100 Subject: [PATCH 24/62] Add a csum() method to ShortFileName. Long File Names carry this checksum of their matching Short File Name. --- src/filesystem/filename.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/filesystem/filename.rs b/src/filesystem/filename.rs index a8b34e4b..ad3dd049 100644 --- a/src/filesystem/filename.rs +++ b/src/filesystem/filename.rs @@ -175,6 +175,15 @@ impl ShortFileName { contents: self.contents, } } + + /// Get the LFN checksum for this short filename + pub fn csum(&self) -> u8 { + let mut result = 0u8; + for b in self.contents.iter() { + result = result.rotate_right(1).wrapping_add(*b); + } + result + } } impl core::fmt::Display for ShortFileName { @@ -302,6 +311,16 @@ mod test { assert!(ShortFileName::create_from_str("123456789").is_err()); assert!(ShortFileName::create_from_str("12345678.ABCD").is_err()); } + + #[test] + fn checksum() { + assert_eq!( + 0xB3, + ShortFileName::create_from_str("UNARCH~1.DAT") + .unwrap() + .csum() + ); + } } // **************************************************************************** From f2daa2e05fdeb9d79fdc1e2a00ccdffb00024f9d Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Fri, 25 Oct 2024 16:55:23 +0100 Subject: [PATCH 25/62] Ensure cluster IDs print with a fixed width. --- src/filesystem/cluster.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/filesystem/cluster.rs b/src/filesystem/cluster.rs index bcf6eb0b..14f11263 100644 --- a/src/filesystem/cluster.rs +++ b/src/filesystem/cluster.rs @@ -38,22 +38,22 @@ impl core::fmt::Debug for ClusterId { write!(f, "ClusterId(")?; match *self { Self::INVALID => { - write!(f, "INVALID")?; + write!(f, "{:08}", "INVALID")?; } Self::BAD => { - write!(f, "BAD")?; + write!(f, "{:08}", "BAD")?; } Self::EMPTY => { - write!(f, "EMPTY")?; + write!(f, "{:08}", "EMPTY")?; } Self::ROOT_DIR => { - write!(f, "ROOT_DIR")?; + write!(f, "{:08}", "ROOT")?; } Self::END_OF_FILE => { - write!(f, "END_OF_FILE")?; + write!(f, "{:08}", "EOF")?; } ClusterId(value) => { - write!(f, "{:#08x}", value)?; + write!(f, "{:08x}", value)?; } } write!(f, ")")?; From 20e57022535b569447124d7767f9b443878abb62 Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Fri, 25 Oct 2024 16:56:23 +0100 Subject: [PATCH 26/62] Ensure attributes print with a fixed width. --- src/filesystem/attributes.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/filesystem/attributes.rs b/src/filesystem/attributes.rs index a6df7578..e22dcd18 100644 --- a/src/filesystem/attributes.rs +++ b/src/filesystem/attributes.rs @@ -71,31 +71,33 @@ impl Attributes { impl core::fmt::Debug for Attributes { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + // Worst case is "DRHSVA" + let mut output = heapless::String::<7>::new(); if self.is_lfn() { - write!(f, "LFN")?; + output.push_str("LFN").unwrap(); } else { if self.is_directory() { - write!(f, "D")?; + output.push_str("D").unwrap(); } else { - write!(f, "F")?; + output.push_str("F").unwrap(); } if self.is_read_only() { - write!(f, "R")?; + output.push_str("R").unwrap(); } if self.is_hidden() { - write!(f, "H")?; + output.push_str("H").unwrap(); } if self.is_system() { - write!(f, "S")?; + output.push_str("S").unwrap(); } if self.is_volume() { - write!(f, "V")?; + output.push_str("V").unwrap(); } if self.is_archive() { - write!(f, "A")?; + output.push_str("A").unwrap(); } } - Ok(()) + f.pad(&output) } } From fe2bdff045412e26f64f0aaeea19d2d01ac8f322 Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Fri, 25 Oct 2024 17:07:16 +0100 Subject: [PATCH 27/62] Add an LfnBuffer type. Used to buffer Long File Names. Designed to be fed 16-bit UCS-2 chunks in reverse, like you find in a directory. --- src/filesystem/filename.rs | 102 +++++++++++++++++++++++++++++++++++++ src/lib.rs | 4 +- 2 files changed, 104 insertions(+), 2 deletions(-) diff --git a/src/filesystem/filename.rs b/src/filesystem/filename.rs index ad3dd049..e3f4f6e4 100644 --- a/src/filesystem/filename.rs +++ b/src/filesystem/filename.rs @@ -1,6 +1,7 @@ //! Filename related types use crate::fat::VolumeName; +use crate::trace; /// Various filename related errors that can occur. #[cfg_attr(feature = "defmt-log", derive(defmt::Format))] @@ -219,6 +220,83 @@ impl core::fmt::Debug for ShortFileName { } } +/// Used to store a Long File Name +/// +/// The const generic specifies the maximum capacity in bytes. +pub struct LfnBuffer { + /// We fill this buffer in from the back + inner: [u8; N], + /// How many bytes are free. + /// + /// This is also the byte index the string starts from. + free: usize, + /// Did we overflow? + overflow: bool, +} + +impl LfnBuffer { + /// Create a new, empty, LFN Buffer + pub fn new() -> LfnBuffer { + LfnBuffer { + inner: [0u8; N], + free: N, + overflow: false, + } + } + + /// Empty out this buffer + pub fn clear(&mut self) { + self.free = N; + self.overflow = false; + } + + /// Push the 13 UCS-2 characters into this string + /// + /// We assume they are pushed last-chunk-first, as you would find + /// them on disk. + pub fn push(&mut self, buffer: &[u16; 13]) { + // find the first null, if any + let null_idx = buffer + .iter() + .position(|&b| b == 0x0000) + .unwrap_or(buffer.len()); + // take all the wide chars, up to the null (or go to the end) + let buffer = &buffer[0..null_idx]; + for ch in buffer.iter().rev() { + let ch = char::from_u32(*ch as u32).unwrap_or('?'); + trace!("LFN push {:?}", ch); + let mut ch_bytes = [0u8; 4]; + // a buffer of length 4 is always enough + let ch_str = ch.encode_utf8(&mut ch_bytes); + if self.free < ch_str.len() { + self.overflow = true; + return; + } + // store the encoded character in the buffer, working backwards + for b in ch_str.bytes().rev() { + self.free -= 1; + self.inner[self.free] = b; + } + } + } + + /// View this LFN buffer as a string-slice + pub fn as_str(&self) -> &str { + if self.overflow { + "" + } else { + // we always only put UTF-8 encoded data in here + unsafe { core::str::from_utf8_unchecked(&self.inner[self.free..]) } + } + } +} + +impl core::default::Default for LfnBuffer { + fn default() -> Self { + LfnBuffer::new() + } +} + // **************************************************************************** // // Unit Tests @@ -321,6 +399,30 @@ mod test { .csum() ); } + + #[test] + fn one_piece() { + let mut buf: LfnBuffer<64> = LfnBuffer::new(); + buf.push(&[ + 0x0030, 0x0031, 0x0032, 0x0033, 0x2202, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, + 0xFFFF, 0xFFFF, + ]); + assert_eq!(buf.as_str(), "0123∂"); + } + + #[test] + fn two_piece() { + let mut buf: LfnBuffer<64> = LfnBuffer::new(); + buf.push(&[ + 0x0030, 0x0031, 0x0032, 0x0033, 0x2202, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, + 0xFFFF, 0xFFFF, + ]); + buf.push(&[ + 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, 0x0048, 0x0049, 0x004a, 0x004b, + 0x004c, 0x004d, + ]); + assert_eq!(buf.as_str(), "ABCDEFGHIJKLM0123∂"); + } } // **************************************************************************** diff --git a/src/lib.rs b/src/lib.rs index bdd56305..434808d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,8 +85,8 @@ pub use crate::fat::{FatVolume, VolumeName}; #[doc(inline)] pub use crate::filesystem::{ - Attributes, ClusterId, DirEntry, Directory, File, FilenameError, Mode, RawDirectory, RawFile, - ShortFileName, TimeSource, Timestamp, MAX_FILE_SIZE, + Attributes, ClusterId, DirEntry, Directory, File, FilenameError, LfnBuffer, Mode, RawDirectory, + RawFile, ShortFileName, TimeSource, Timestamp, MAX_FILE_SIZE, }; use filesystem::DirectoryInfo; From d89944cb1b6961591a15431502ed93134b5129e8 Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Fri, 25 Oct 2024 18:05:38 +0100 Subject: [PATCH 28/62] Add iterate_dir_lfn. Scoops up bits of LFN into the given LFN buffer and if it all looks OK, passes it to the user-supplied closure as a &str. --- examples/shell.rs | 20 +++- src/fat/mod.rs | 69 ++++++++--- src/fat/ondiskdirentry.rs | 56 +++------ src/fat/volume.rs | 125 ++++++++++++++++++-- src/filesystem/directory.rs | 31 ++++- src/filesystem/mod.rs | 2 +- src/volume_mgr.rs | 65 ++++++++++- tests/directories.rs | 226 +++++++++++++++++++++++++----------- tests/disk.img.gz | Bin 703296 -> 703414 bytes 9 files changed, 451 insertions(+), 143 deletions(-) diff --git a/examples/shell.rs b/examples/shell.rs index 341bb51a..d4bdd165 100644 --- a/examples/shell.rs +++ b/examples/shell.rs @@ -71,7 +71,9 @@ use std::{cell::RefCell, io::prelude::*}; -use embedded_sdmmc::{Error as EsError, RawDirectory, RawVolume, ShortFileName, VolumeIdx}; +use embedded_sdmmc::{ + Error as EsError, LfnBuffer, RawDirectory, RawVolume, ShortFileName, VolumeIdx, +}; type VolumeManager = embedded_sdmmc::VolumeManager; type Directory<'a> = embedded_sdmmc::Directory<'a, LinuxBlockDevice, Clock, 8, 8, 4>; @@ -229,17 +231,23 @@ impl Context { fn dir(&self, path: &Path) -> Result<(), Error> { println!("Directory listing of {:?}", path); let dir = self.resolve_existing_directory(path)?; - dir.iterate_dir(|entry| { - if !entry.attributes.is_volume() && !entry.attributes.is_lfn() { - println!( - "{:12} {:9} {} {} {:08X?} {:?}", + let mut lfn_buffer = LfnBuffer::<256>::new(); + dir.iterate_dir_lfn(&mut lfn_buffer, |entry, lfn| { + if !entry.attributes.is_volume() { + print!( + "{:12} {:9} {} {} {:08X?} {:5?}", entry.name, entry.size, entry.ctime, entry.mtime, entry.cluster, - entry.attributes + entry.attributes, ); + if let Some(lfn) = lfn { + println!(" {:?}", lfn); + } else { + println!(); + } } })?; Ok(()) diff --git a/src/fat/mod.rs b/src/fat/mod.rs index 504f67fc..c27a8cdd 100644 --- a/src/fat/mod.rs +++ b/src/fat/mod.rs @@ -84,7 +84,7 @@ mod test { fn test_dir_entries() { #[derive(Debug)] enum Expected { - Lfn(bool, u8, [char; 13]), + Lfn(bool, u8, u8, [u16; 13]), Short(DirEntry), } let raw_data = r#" @@ -105,6 +105,7 @@ mod test { 422e0064007400620000000f0059ffffffffffffffffffffffff0000ffffffff B..d.t.b.....Y.................. 01620063006d00320037000f0059300038002d0072007000690000002d006200 .b.c.m.2.7...Y0.8.-.r.p.i...-.b. "#; + let results = [ Expected::Short(DirEntry { name: unsafe { @@ -123,9 +124,10 @@ mod test { Expected::Lfn( true, 1, + 0x47, [ - 'o', 'v', 'e', 'r', 'l', 'a', 'y', 's', '\u{0000}', '\u{ffff}', '\u{ffff}', - '\u{ffff}', '\u{ffff}', + 'o' as u16, 'v' as u16, 'e' as u16, 'r' as u16, 'l' as u16, 'a' as u16, + 'y' as u16, 's' as u16, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, ], ), Expected::Short(DirEntry { @@ -141,16 +143,20 @@ mod test { Expected::Lfn( true, 2, + 0x79, [ - '-', 'p', 'l', 'u', 's', '.', 'd', 't', 'b', '\u{0000}', '\u{ffff}', - '\u{ffff}', '\u{ffff}', + '-' as u16, 'p' as u16, 'l' as u16, 'u' as u16, 's' as u16, '.' as u16, + 'd' as u16, 't' as u16, 'b' as u16, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, ], ), Expected::Lfn( false, 1, + 0x79, [ - 'b', 'c', 'm', '2', '7', '0', '8', '-', 'r', 'p', 'i', '-', 'b', + 'b' as u16, 'c' as u16, 'm' as u16, '2' as u16, '7' as u16, '0' as u16, + '8' as u16, '-' as u16, 'r' as u16, 'p' as u16, 'i' as u16, '-' as u16, + 'b' as u16, ], ), Expected::Short(DirEntry { @@ -166,8 +172,11 @@ mod test { Expected::Lfn( true, 1, + 0x12, [ - 'C', 'O', 'P', 'Y', 'I', 'N', 'G', '.', 'l', 'i', 'n', 'u', 'x', + 'C' as u16, 'O' as u16, 'P' as u16, 'Y' as u16, 'I' as u16, 'N' as u16, + 'G' as u16, '.' as u16, 'l' as u16, 'i' as u16, 'n' as u16, 'u' as u16, + 'x' as u16, ], ), Expected::Short(DirEntry { @@ -183,16 +192,31 @@ mod test { Expected::Lfn( true, 2, + 0x67, [ - 'c', 'o', 'm', '\u{0}', '\u{ffff}', '\u{ffff}', '\u{ffff}', '\u{ffff}', - '\u{ffff}', '\u{ffff}', '\u{ffff}', '\u{ffff}', '\u{ffff}', + 'c' as u16, + 'o' as u16, + 'm' as u16, + '\u{0}' as u16, + 0xFFFF, + 0xFFFF, + 0xFFFF, + 0xFFFF, + 0xFFFF, + 0xFFFF, + 0xFFFF, + 0xFFFF, + 0xFFFF, ], ), Expected::Lfn( false, 1, + 0x67, [ - 'L', 'I', 'C', 'E', 'N', 'C', 'E', '.', 'b', 'r', 'o', 'a', 'd', + 'L' as u16, 'I' as u16, 'C' as u16, 'E' as u16, 'N' as u16, 'C' as u16, + 'E' as u16, '.' as u16, 'b' as u16, 'r' as u16, 'o' as u16, 'a' as u16, + 'd' as u16, ], ), Expected::Short(DirEntry { @@ -208,16 +232,20 @@ mod test { Expected::Lfn( true, 2, + 0x19, [ - '-', 'b', '.', 'd', 't', 'b', '\u{0000}', '\u{ffff}', '\u{ffff}', '\u{ffff}', - '\u{ffff}', '\u{ffff}', '\u{ffff}', + '-' as u16, 'b' as u16, '.' as u16, 'd' as u16, 't' as u16, 'b' as u16, 0x0000, + 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, ], ), Expected::Lfn( false, 1, + 0x19, [ - 'b', 'c', 'm', '2', '7', '0', '9', '-', 'r', 'p', 'i', '-', '2', + 'b' as u16, 'c' as u16, 'm' as u16, '2' as u16, '7' as u16, '0' as u16, + '9' as u16, '-' as u16, 'r' as u16, 'p' as u16, 'i' as u16, '-' as u16, + '2' as u16, ], ), Expected::Short(DirEntry { @@ -233,16 +261,20 @@ mod test { Expected::Lfn( true, 2, + 0x59, [ - '.', 'd', 't', 'b', '\u{0000}', '\u{ffff}', '\u{ffff}', '\u{ffff}', '\u{ffff}', - '\u{ffff}', '\u{ffff}', '\u{ffff}', '\u{ffff}', + '.' as u16, 'd' as u16, 't' as u16, 'b' as u16, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, + 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, ], ), Expected::Lfn( false, 1, + 0x59, [ - 'b', 'c', 'm', '2', '7', '0', '8', '-', 'r', 'p', 'i', '-', 'b', + 'b' as u16, 'c' as u16, 'm' as u16, '2' as u16, '7' as u16, '0' as u16, + '8' as u16, '-' as u16, 'r' as u16, 'p' as u16, 'i' as u16, '-' as u16, + 'b' as u16, ], ), ]; @@ -251,12 +283,13 @@ mod test { for (part, expected) in data.chunks(OnDiskDirEntry::LEN).zip(results.iter()) { let on_disk_entry = OnDiskDirEntry::new(part); match expected { - Expected::Lfn(start, index, contents) if on_disk_entry.is_lfn() => { - let (calc_start, calc_index, calc_contents) = + Expected::Lfn(start, index, csum, contents) if on_disk_entry.is_lfn() => { + let (calc_start, calc_index, calc_csum, calc_contents) = on_disk_entry.lfn_contents().unwrap(); assert_eq!(*start, calc_start); assert_eq!(*index, calc_index); assert_eq!(*contents, calc_contents); + assert_eq!(*csum, calc_csum); } Expected::Short(expected_entry) if !on_disk_entry.is_lfn() => { let parsed_entry = on_disk_entry.get_entry(FatType::Fat32, BlockIdx(0), 0); diff --git a/src/fat/ondiskdirentry.rs b/src/fat/ondiskdirentry.rs index 49b8bb20..83707e47 100644 --- a/src/fat/ondiskdirentry.rs +++ b/src/fat/ondiskdirentry.rs @@ -78,47 +78,27 @@ impl<'a> OnDiskDirEntry<'a> { } /// If this is an LFN, get the contents so we can re-assemble the filename. - pub fn lfn_contents(&self) -> Option<(bool, u8, [char; 13])> { + pub fn lfn_contents(&self) -> Option<(bool, u8, u8, [u16; 13])> { if self.is_lfn() { - let mut buffer = [' '; 13]; let is_start = (self.data[0] & 0x40) != 0; let sequence = self.data[0] & 0x1F; - // LFNs store UCS-2, so we can map from 16-bit char to 32-bit char without problem. - buffer[0] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[1..=2]))).unwrap(); - buffer[1] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[3..=4]))).unwrap(); - buffer[2] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[5..=6]))).unwrap(); - buffer[3] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[7..=8]))).unwrap(); - buffer[4] = core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[9..=10]))) - .unwrap(); - buffer[5] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[14..=15]))) - .unwrap(); - buffer[6] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[16..=17]))) - .unwrap(); - buffer[7] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[18..=19]))) - .unwrap(); - buffer[8] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[20..=21]))) - .unwrap(); - buffer[9] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[22..=23]))) - .unwrap(); - buffer[10] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[24..=25]))) - .unwrap(); - buffer[11] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[28..=29]))) - .unwrap(); - buffer[12] = - core::char::from_u32(u32::from(LittleEndian::read_u16(&self.data[30..=31]))) - .unwrap(); - Some((is_start, sequence, buffer)) + let csum = self.data[13]; + let buffer = [ + LittleEndian::read_u16(&self.data[1..=2]), + LittleEndian::read_u16(&self.data[3..=4]), + LittleEndian::read_u16(&self.data[5..=6]), + LittleEndian::read_u16(&self.data[7..=8]), + LittleEndian::read_u16(&self.data[9..=10]), + LittleEndian::read_u16(&self.data[14..=15]), + LittleEndian::read_u16(&self.data[16..=17]), + LittleEndian::read_u16(&self.data[18..=19]), + LittleEndian::read_u16(&self.data[20..=21]), + LittleEndian::read_u16(&self.data[22..=23]), + LittleEndian::read_u16(&self.data[24..=25]), + LittleEndian::read_u16(&self.data[28..=29]), + LittleEndian::read_u16(&self.data[30..=31]), + ]; + Some((is_start, sequence, csum, buffer)) } else { None } diff --git a/src/fat/volume.rs b/src/fat/volume.rs index fbbedf9e..503a7e80 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -8,7 +8,7 @@ use crate::{ }, filesystem::FilenameError, trace, warn, Attributes, Block, BlockCache, BlockCount, BlockDevice, BlockIdx, ClusterId, - DirEntry, DirectoryInfo, Error, ShortFileName, TimeSource, VolumeType, + DirEntry, DirectoryInfo, Error, LfnBuffer, ShortFileName, TimeSource, VolumeType, }; use byteorder::{ByteOrder, LittleEndian}; use core::convert::TryFrom; @@ -520,7 +520,7 @@ impl FatVolume { &self, block_cache: &mut BlockCache, dir_info: &DirectoryInfo, - func: F, + mut func: F, ) -> Result<(), Error> where F: FnMut(&DirEntry), @@ -528,10 +528,115 @@ impl FatVolume { { match &self.fat_specific_info { FatSpecificInfo::Fat16(fat16_info) => { - self.iterate_fat16(dir_info, fat16_info, block_cache, func) + self.iterate_fat16(dir_info, fat16_info, block_cache, |de, _| func(de)) + } + FatSpecificInfo::Fat32(fat32_info) => { + self.iterate_fat32(dir_info, fat32_info, block_cache, |de, _| func(de)) + } + } + } + + /// Calls callback `func` with every valid entry in the given directory, + /// including the Long File Name. + /// + /// Useful for performing directory listings. + pub(crate) fn iterate_dir_lfn( + &self, + block_cache: &mut BlockCache, + lfn_buffer: &mut LfnBuffer, + dir_info: &DirectoryInfo, + mut func: F, + ) -> Result<(), Error> + where + F: FnMut(&DirEntry, Option<&str>), + D: BlockDevice, + { + #[derive(Clone, Copy)] + enum SeqState { + Waiting, + Remaining { csum: u8, next: u8 }, + Complete { csum: u8 }, + } + + impl SeqState { + fn update( + self, + lfn_buffer: &mut LfnBuffer, + start: bool, + sequence: u8, + csum: u8, + buffer: [u16; 13], + ) -> Self { + debug!("LFN Contents {start} {sequence} {csum:02x} {buffer:04x?}"); + match (start, sequence, self) { + (true, 0x01, _) => { + lfn_buffer.clear(); + lfn_buffer.push(&buffer); + SeqState::Complete { csum } + } + (true, 0x02..0x14, _) => { + lfn_buffer.clear(); + lfn_buffer.push(&buffer); + SeqState::Remaining { + csum, + next: sequence - 1, + } + } + (false, 0x01, SeqState::Remaining { csum, next }) if next == sequence => { + lfn_buffer.push(&buffer); + SeqState::Complete { csum } + } + (false, 0x01..0x13, SeqState::Remaining { csum, next }) if next == sequence => { + lfn_buffer.push(&buffer); + SeqState::Remaining { + csum, + next: sequence - 1, + } + } + _ => { + // this seems wrong + lfn_buffer.clear(); + SeqState::Waiting + } + } + } + } + + let mut seq_state = SeqState::Waiting; + match &self.fat_specific_info { + FatSpecificInfo::Fat16(fat16_info) => { + self.iterate_fat16(dir_info, fat16_info, block_cache, |de, odde| { + if let Some((start, this_seqno, csum, buffer)) = odde.lfn_contents() { + seq_state = seq_state.update(lfn_buffer, start, this_seqno, csum, buffer); + } else if let SeqState::Complete { csum } = seq_state { + if csum == de.name.csum() { + // Checksum is good, and all the pieces are there + func(de, Some(lfn_buffer.as_str())) + } else { + // Checksum was bad + func(de, None) + } + } else { + func(de, None) + } + }) } FatSpecificInfo::Fat32(fat32_info) => { - self.iterate_fat32(dir_info, fat32_info, block_cache, func) + self.iterate_fat32(dir_info, fat32_info, block_cache, |de, odde| { + if let Some((start, this_seqno, csum, buffer)) = odde.lfn_contents() { + seq_state = seq_state.update(lfn_buffer, start, this_seqno, csum, buffer); + } else if let SeqState::Complete { csum } = seq_state { + if csum == de.name.csum() { + // Checksum is good, and all the pieces are there + func(de, Some(lfn_buffer.as_str())) + } else { + // Checksum was bad + func(de, None) + } + } else { + func(de, None) + } + }) } } } @@ -544,7 +649,7 @@ impl FatVolume { mut func: F, ) -> Result<(), Error> where - F: FnMut(&DirEntry), + F: for<'odde> FnMut(&DirEntry, &OnDiskDirEntry<'odde>), D: BlockDevice, { // Root directories on FAT16 have a fixed size, because they use @@ -573,11 +678,11 @@ impl FatVolume { if dir_entry.is_end() { // Can quit early return Ok(()); - } else if dir_entry.is_valid() && !dir_entry.is_lfn() { + } else if dir_entry.is_valid() { // Safe, since Block::LEN always fits on a u32 let start = (i * OnDiskDirEntry::LEN) as u32; let entry = dir_entry.get_entry(FatType::Fat16, block_idx, start); - func(&entry); + func(&entry, &dir_entry); } } } @@ -604,7 +709,7 @@ impl FatVolume { mut func: F, ) -> Result<(), Error> where - F: FnMut(&DirEntry), + F: for<'odde> FnMut(&DirEntry, &OnDiskDirEntry<'odde>), D: BlockDevice, { // All directories on FAT32 have a cluster chain but the root @@ -623,11 +728,11 @@ impl FatVolume { if dir_entry.is_end() { // Can quit early return Ok(()); - } else if dir_entry.is_valid() && !dir_entry.is_lfn() { + } else if dir_entry.is_valid() { // Safe, since Block::LEN always fits on a u32 let start = (i * OnDiskDirEntry::LEN) as u32; let entry = dir_entry.get_entry(FatType::Fat32, block_idx, start); - func(&entry); + func(&entry, &dir_entry); } } } diff --git a/src/filesystem/directory.rs b/src/filesystem/directory.rs index 5cfc5f07..c0493529 100644 --- a/src/filesystem/directory.rs +++ b/src/filesystem/directory.rs @@ -1,6 +1,6 @@ use crate::blockdevice::BlockIdx; use crate::fat::{FatType, OnDiskDirEntry}; -use crate::filesystem::{Attributes, ClusterId, Handle, ShortFileName, Timestamp}; +use crate::filesystem::{Attributes, ClusterId, Handle, LfnBuffer, ShortFileName, Timestamp}; use crate::{Error, RawVolume, VolumeManager}; use super::ToShortFileName; @@ -145,6 +145,8 @@ where /// Call a callback function for each directory entry in a directory. /// + /// Long File Names will be ignored. + /// ///
/// /// Do not attempt to call any methods on the VolumeManager or any of its @@ -159,6 +161,33 @@ where self.volume_mgr.iterate_dir(self.raw_directory, func) } + /// Call a callback function for each directory entry in a directory, and + /// process Long File Names. + /// + /// You must supply a [`LfnBuffer`] this API can use to temporarily hold the + /// Long File Name. If you pass one that isn't large enough, any Long File + /// Names that don't fit will be ignored and presented as if they only had a + /// Short File Name. + /// + ///
+ /// + /// Do not attempt to call any methods on the VolumeManager or any of its + /// handles from inside the callback. You will get a lock error because the + /// object is already locked in order to do the iteration. + /// + ///
+ pub fn iterate_dir_lfn( + &self, + lfn_buffer: &mut LfnBuffer, + func: F, + ) -> Result<(), Error> + where + F: FnMut(&DirEntry, Option<&str>), + { + self.volume_mgr + .iterate_dir_lfn(self.raw_directory, lfn_buffer, func) + } + /// Open a file with the given full path. A file can only be opened once. pub fn open_file_in_dir( &self, diff --git a/src/filesystem/mod.rs b/src/filesystem/mod.rs index 92c94a1e..668ac86b 100644 --- a/src/filesystem/mod.rs +++ b/src/filesystem/mod.rs @@ -17,7 +17,7 @@ mod timestamp; pub use self::attributes::Attributes; pub use self::cluster::ClusterId; pub use self::directory::{DirEntry, Directory, RawDirectory}; -pub use self::filename::{FilenameError, ShortFileName, ToShortFileName}; +pub use self::filename::{FilenameError, LfnBuffer, ShortFileName, ToShortFileName}; pub use self::files::{File, FileError, Mode, RawFile}; pub use self::handles::{Handle, HandleGenerator}; pub use self::timestamp::{TimeSource, Timestamp}; diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 75140d07..dab2f024 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -12,7 +12,7 @@ use heapless::Vec; use crate::{ debug, fat, filesystem::{ - Attributes, ClusterId, DirEntry, DirectoryInfo, FileInfo, HandleGenerator, Mode, + Attributes, ClusterId, DirEntry, DirectoryInfo, FileInfo, HandleGenerator, LfnBuffer, Mode, RawDirectory, RawFile, TimeSource, ToShortFileName, MAX_FILE_SIZE, }, trace, Block, BlockCache, BlockCount, BlockDevice, BlockIdx, Error, RawVolume, ShortFileName, @@ -382,6 +382,8 @@ where /// Call a callback function for each directory entry in a directory. /// + /// Long File Names will be ignored. + /// ///
/// /// Do not attempt to call any methods on the VolumeManager or any of its @@ -389,7 +391,11 @@ where /// object is already locked in order to do the iteration. /// ///
- pub fn iterate_dir(&self, directory: RawDirectory, func: F) -> Result<(), Error> + pub fn iterate_dir( + &self, + directory: RawDirectory, + mut func: F, + ) -> Result<(), Error> where F: FnMut(&DirEntry), { @@ -400,7 +406,59 @@ where let volume_idx = data.get_volume_by_id(data.open_dirs[directory_idx].raw_volume)?; match &data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => { - fat.iterate_dir(&mut data.block_cache, &data.open_dirs[directory_idx], func) + fat.iterate_dir( + &mut data.block_cache, + &data.open_dirs[directory_idx], + |de| { + // Hide all the LFN directory entries + if !de.attributes.is_lfn() { + func(de); + } + }, + ) + } + } + } + + /// Call a callback function for each directory entry in a directory, and + /// process Long File Names. + /// + /// You must supply a [`LfnBuffer`] this API can use to temporarily hold the + /// Long File Name. If you pass one that isn't large enough, any Long File + /// Names that don't fit will be ignored and presented as if they only had a + /// Short File Name. + /// + ///
+ /// + /// Do not attempt to call any methods on the VolumeManager or any of its + /// handles from inside the callback. You will get a lock error because the + /// object is already locked in order to do the iteration. + /// + ///
+ pub fn iterate_dir_lfn( + &self, + directory: RawDirectory, + lfn_buffer: &mut LfnBuffer, + func: F, + ) -> Result<(), Error> + where + F: FnMut(&DirEntry, Option<&str>), + { + let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); + + let directory_idx = data.get_dir_by_id(directory)?; + let volume_idx = data.get_volume_by_id(data.open_dirs[directory_idx].raw_volume)?; + + match &data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + // This API doesn't care about the on-disk directory entry, so we discard it + fat.iterate_dir_lfn( + &mut data.block_cache, + lfn_buffer, + &data.open_dirs[directory_idx], + func, + ) } } } @@ -1005,6 +1063,7 @@ where // Need mutable access for this match &mut data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => { + // TODO: Move this into the FAT volume code debug!("Making dir entry"); let mut new_dir_entry_in_parent = fat.write_new_directory_entry( &mut data.block_cache, diff --git a/tests/directories.rs b/tests/directories.rs index 1acfb370..62aa72cd 100644 --- a/tests/directories.rs +++ b/tests/directories.rs @@ -1,6 +1,6 @@ //! Directory related tests -use embedded_sdmmc::{Mode, ShortFileName}; +use embedded_sdmmc::{LfnBuffer, Mode, ShortFileName}; mod utils; @@ -48,52 +48,86 @@ fn fat16_root_directory_listing() { .expect("open root dir"); let expected = [ - ExpectedDirEntry { - name: String::from("README.TXT"), - mtime: String::from("2018-12-09 19:22:34"), - ctime: String::from("2018-12-09 19:22:34"), - size: 258, - is_dir: false, - }, - ExpectedDirEntry { - name: String::from("EMPTY.DAT"), - mtime: String::from("2018-12-09 19:21:16"), - ctime: String::from("2018-12-09 19:21:16"), - size: 0, - is_dir: false, - }, - ExpectedDirEntry { - name: String::from("TEST"), - mtime: String::from("2018-12-09 19:23:16"), - ctime: String::from("2018-12-09 19:23:16"), - size: 0, - is_dir: true, - }, - ExpectedDirEntry { - name: String::from("64MB.DAT"), - mtime: String::from("2018-12-09 19:21:38"), - ctime: String::from("2018-12-09 19:21:38"), - size: 64 * 1024 * 1024, - is_dir: false, - }, + ( + ExpectedDirEntry { + name: String::from("README.TXT"), + mtime: String::from("2018-12-09 19:22:34"), + ctime: String::from("2018-12-09 19:22:34"), + size: 258, + is_dir: false, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("EMPTY.DAT"), + mtime: String::from("2018-12-09 19:21:16"), + ctime: String::from("2018-12-09 19:21:16"), + size: 0, + is_dir: false, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("TEST"), + mtime: String::from("2018-12-09 19:23:16"), + ctime: String::from("2018-12-09 19:23:16"), + size: 0, + is_dir: true, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("64MB.DAT"), + mtime: String::from("2018-12-09 19:21:38"), + ctime: String::from("2018-12-09 19:21:38"), + size: 64 * 1024 * 1024, + is_dir: false, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("FSEVEN~4"), + mtime: String::from("2024-10-25 16:30:42"), + ctime: String::from("2024-10-25 16:30:42"), + size: 0, + is_dir: true, + }, + Some(String::from(".fseventsd")), + ), ]; let mut listing = Vec::new(); + let mut lfn_buffer: LfnBuffer<128> = LfnBuffer::new(); volume_mgr - .iterate_dir(root_dir, |d| { - listing.push(d.clone()); + .iterate_dir_lfn(root_dir, &mut lfn_buffer, |d, opt_lfn| { + listing.push((d.clone(), opt_lfn.map(String::from))); }) .expect("iterate directory"); - assert_eq!(expected.len(), listing.len()); for (expected_entry, given_entry) in expected.iter().zip(listing.iter()) { assert_eq!( - expected_entry, given_entry, + expected_entry.0, given_entry.0, + "{:#?} does not match {:#?}", + given_entry, expected_entry + ); + assert_eq!( + expected_entry.1, given_entry.1, "{:#?} does not match {:#?}", given_entry, expected_entry ); } + assert_eq!( + expected.len(), + listing.len(), + "{:#?} != {:#?}", + expected, + listing + ); } #[test] @@ -151,7 +185,6 @@ fn fat16_sub_directory_listing() { }) .expect("iterate directory"); - assert_eq!(expected.len(), listing.len()); for (expected_entry, given_entry) in expected.iter().zip(listing.iter()) { assert_eq!( expected_entry, given_entry, @@ -159,6 +192,13 @@ fn fat16_sub_directory_listing() { given_entry, expected_entry ); } + assert_eq!( + expected.len(), + listing.len(), + "{:#?} != {:#?}", + expected, + listing + ); } #[test] @@ -175,52 +215,106 @@ fn fat32_root_directory_listing() { .expect("open root dir"); let expected = [ - ExpectedDirEntry { - name: String::from("64MB.DAT"), - mtime: String::from("2018-12-09 19:22:56"), - ctime: String::from("2018-12-09 19:22:56"), - size: 64 * 1024 * 1024, - is_dir: false, - }, - ExpectedDirEntry { - name: String::from("EMPTY.DAT"), - mtime: String::from("2018-12-09 19:22:56"), - ctime: String::from("2018-12-09 19:22:56"), - size: 0, - is_dir: false, - }, - ExpectedDirEntry { - name: String::from("README.TXT"), - mtime: String::from("2023-09-21 09:48:06"), - ctime: String::from("2018-12-09 19:22:56"), - size: 258, - is_dir: false, - }, - ExpectedDirEntry { - name: String::from("TEST"), - mtime: String::from("2018-12-09 19:23:20"), - ctime: String::from("2018-12-09 19:23:20"), - size: 0, - is_dir: true, - }, + ( + ExpectedDirEntry { + name: String::from("64MB.DAT"), + mtime: String::from("2018-12-09 19:22:56"), + ctime: String::from("2018-12-09 19:22:56"), + size: 64 * 1024 * 1024, + is_dir: false, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("EMPTY.DAT"), + mtime: String::from("2018-12-09 19:22:56"), + ctime: String::from("2018-12-09 19:22:56"), + size: 0, + is_dir: false, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("README.TXT"), + mtime: String::from("2023-09-21 09:48:06"), + ctime: String::from("2018-12-09 19:22:56"), + size: 258, + is_dir: false, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("TEST"), + mtime: String::from("2018-12-09 19:23:20"), + ctime: String::from("2018-12-09 19:23:20"), + size: 0, + is_dir: true, + }, + None, + ), + ( + ExpectedDirEntry { + name: String::from("FSEVEN~4"), + mtime: String::from("2024-10-25 16:30:42"), + ctime: String::from("2024-10-25 16:30:42"), + size: 0, + is_dir: true, + }, + Some(String::from(".fseventsd")), + ), + ( + ExpectedDirEntry { + name: String::from("THISIS~9"), + mtime: String::from("2024-10-25 16:30:54"), + ctime: String::from("2024-10-25 16:30:50"), + size: 0, + is_dir: true, + }, + Some(String::from("This is a long file name £99")), + ), + ( + ExpectedDirEntry { + name: String::from("COPYO~13.TXT"), + mtime: String::from("2024-10-25 16:31:14"), + ctime: String::from("2018-12-09 19:22:56"), + size: 258, + is_dir: false, + }, + Some(String::from("Copy of Readme.txt")), + ), ]; let mut listing = Vec::new(); + let mut lfn_buffer: LfnBuffer<128> = LfnBuffer::new(); volume_mgr - .iterate_dir(root_dir, |d| { - listing.push(d.clone()); + .iterate_dir_lfn(root_dir, &mut lfn_buffer, |d, opt_lfn| { + listing.push((d.clone(), opt_lfn.map(String::from))); }) .expect("iterate directory"); - assert_eq!(expected.len(), listing.len()); for (expected_entry, given_entry) in expected.iter().zip(listing.iter()) { assert_eq!( - expected_entry, given_entry, + expected_entry.0, given_entry.0, + "{:#?} does not match {:#?}", + given_entry, expected_entry + ); + assert_eq!( + expected_entry.1, given_entry.1, "{:#?} does not match {:#?}", given_entry, expected_entry ); } + assert_eq!( + expected.len(), + listing.len(), + "{:#?} != {:#?}", + expected, + listing + ); } #[test] diff --git a/tests/disk.img.gz b/tests/disk.img.gz index 8df362bcc05eb233a964c8d5252a1ef7450309fc..1ba2bdfd1f75fb2d39e1ffec210c8bd4d4312f0f 100644 GIT binary patch delta 83661 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 DDy6Pr delta 96624 zcmZsCcRbtg7jM-UEmc~pD5`4jO%b(g?>$l!wTs#!tvzCfhFUE#BVw-_wKo-eruM2D zF=F5R?(6<}{gpp`o^zga-s`D9=lONU17?YT@PLx!k;6U0_s%}90?zJE_x{c}Q2tiA z#$9;&bG|^MUnHcx^~e8(fBzhT&dL8IX_r6yC2lD$Hug)xz(wBA;EzfF#z#E={kxgZ znH&52?{A3X`G6AUL;FFjZ;8wPDQ|qrvH%f9NXkFoV6yn$Rrx5%z@ws^6EEseh zQQAXH!zgRpIjIod_n24W(`1@(dOXdZl;7-k?yk5526=~B@cP}*ShfcP!+i%>D z3UROFN^ZkjGA9~FQ8$ySQmQ!ZoNLTrj6y*#9m1-~TX^O&?`Pwk?zZ1H_%-3ZUw05* zQr*?{CkCuk-akI;>2bZXVxy+5pox2+5^1OC z^&>*hfQ#LVQ!I^k@PR|5w_@OrFg^W}S4OlIbY%~WBV81Ie?;jSmatFKdQk2@@Qn;o z1pWB(vEcV%J3|5VZ-fa0^zMWd_^g%SE2y8&q|s;35YfB`s-uL{H~_Cll;| zh~VXekHwYRjhc;?Fe+jK4sdY9L}zO!sPm?iurs_&4 zo~TuASc?dOE?7$SId39WUp!TJ?CLA8YAKa$9v!OUc+u|6RlXxfuX-u97L^v`Y~E3- zt@wsmvhK*$mtIu?KLes0m92OLs9^ED-RY}5UUiMCR=gEdGx3bw39B4lHH|8ZJj>i3 zR0mHFXdB{Xx^q?qyc!zS7J2_soyU`Ohp)08RR$=_@_?DClHxhLp{v|S%ty5WsDQkH zcHC#&cU*FSYKU2gvMeuv2fzUk0I&kM0qg*N02_c8z?n;xM+c+?Qjf(8#tO!xVo-6Y zSX8`cjAxu@tY`c|%t72i>_I$B3|%)xPf~YMk8Zba&scZ->eedVDs0tsb!Js^mHo)_ zNahF}d_;0ojVr)GaW%Mj90Hex`x&5et#oa8t$A&Et#)nN!b6(MrJl;ioyy~!DnOsg zZJx?6oyr@SDoA9&rDDLxZouPUAV8B$su2lyg?FOs44#yh7-{hcq$g`=6vFr5XaoAv zaw8ocLE~f|jdb`R`qY4?w9H7mOkgV6MN6X&K943hU@EOJ(k&AdPxeS-(2!J5fcF|u zm6lFwxd_xHTWBER(*}&C$VnX+LEq%p8aeP$1G-Ygr1q}BX>yQ8D}332r4%)(3)6o{ zApSh7a1Q-^hjz!rxk60$o8S@$e=@B`JiG_Jk7j~>eTZ-WPsO#`&6>@Yu;z#VYk&9z zx)lvV-=GQ6;bH>_(|GH?Fr1qQPTmK{N{Oi9SHn>`?6J?g;L9?y&3tcgkM%=kpCzm-m;I z4>Y^=7r718|Lw2%JJ9yL|Ht!zs_g#K?13h$Hv(LAsi+jPp_xII^?YyPrba780d80- z3aUBeuwLyg-PB^GP{C!I$~!bOWV~MHE#A~@rBuN^lj=Go+3&mFOye!r)VBDhP2YnH z^GY(6aY%c;$XgWl2TAaPYfX);92XqhAFmx#9Yc=|k0*{rk86&7j-@XG zFNiKwE?TZrudA=)uFI~!T{mAxT^C({y{^Abyso&;y>7ey5~6Uccl-9%>{j{K=vJ$Z z3oHO;1#^Sh!Tex0U}}4MJG31JP8J-JACw;e%MZ2pw-2@tv=5oC%dg9Cw6C{sfY-qr zi0bb#aq#}Q{F1n$?YJKqafP#S1&wjVIF0WS8t`t7e7Hu@x&}evcZ0W0s@&yP2x|Q} zo}RQ-jfaJB1C1sfZe*JNAkRkHu?A6LzJYpaxfRMtzl>)pZC8W5u*g8O6uJ0)Qh!&8 z=QQn7gK$H&@P{`P{noi$O#d6tNZO_bSz#es!x>qtKhLwDcBVm6Sb$dFDgUJ3%(ImC zPlKYc7_GU3Jkh`8xk-bSKPC`kYBJA64(og9dl+x>VAC!J-3RYG{HT6wUcOkd_yczI z7~k>!JPT<*(|)GIXebES!A)w?$VJ3r@ghq0yKIRpJX@dHgjs)-Cy6JCJBe49OP5EN zTbFl?Ym8@%dn;`#oi2?o9XqHwI5fyRh!}Jo+#6&Z%o?;DTo{xY{5cprcr{2eNVK7} z!M0()LA7DHA-a)&+;kjyTzH&*Tz8z{t$ty0p>tt#q0m%*U2+WvUzcC!<4ag{U16n< zFPPD-!L7-y{;hEZPZ&>GDi0#Atn6DEBFd%gs|zA=w=8!T@r9u*g#i)!tt|5!f@m_# zh4qx7t}+N|q0C=KYZ#AUpG7mYEnL5aN^H zT=?tigTgJ;kUEHI!#xD~B;!=nuC@zXUDdp@dzpk`7J`5Bs|#;k-Mp$hqTX-;!8DoZ z!dX`{ud-j}XZRB#H<{}qSl2MGwvRY7yh0F8e%XEDTlrcU`pJNs>D1A;{`W zV$^U8(NIb@8M*t?w<_poP&Bl{fEnDZvQ)NQ>S1_baA4AakeSTc74U6nR$D^+GdxF- zOos2Wo>m4a%at)3CLuT{p}X9t%%`-y z0tx|0Kmh;&D9$y^GX?4ajm8jyrGf}l391ZLib8mnc$RsVdLj-=4$5>6N)He$C3KS% zQ%RFaQ@WG7Q)83yyIZ?-yRco;-I-mA?QXc5?E3V0Ry<4-J;CFqJri(sY_(7Yi;Lzoy8MmiHR0(Mfz_I zm%`e;TC@c{vfM<67iIjL$0faXaQ@VS244|vM8(u^7ni!)`FU~+CS-+)E&?U~+hdZ! zMbbH;w%39RSvsxdT2c4g!Ub77ZNZ2{PV2a$e1E@o$*CQ+phF_2wf8Dct$zo(wALx_c<{-^4EAwH@=6?j;ghj1>vLRb2{d$LF89mrLj{qO^Z=xIk+M=Be?u*3POJ z`R(XZQ=3&cH~)N}cHhLkLP9qSwN%9ao7N@1wr75So(W2s`tpHaalQ6W&7YP~u~ht~ zsGXQ^od?a|%oEOs&nwN-@7wM_*)KmUImnHL|dDd6+KHE(3X4dC-p%xt*y%n2POV!nLX>1iXh3>pUX-IrRV72 zJGm4-{I{Xa z=FDW4C&q4rF2#=1N%X1ONe#YZ?Jw6ZsqlSk zcsX$?itk>ZOX-`y8=@PPo0i+u+v?l6+p^nlx6QXvw?(&KZ|iRpZ!2zdZ`*Fageu(W z-MzgtyHmb1y3=Ye0at*3fJ?!JU?jKzh;E;1pJ<-}{}$Yn-<971%kQ;sx9_&^wC|ao z$e+lcwx6`0f=|Gwcc&|KVUkD~8Nc+&gUK23NYHZD)_oXM+3cp<}n(QeUYSbUQno3H>O z>Q%H)shVwEYV3ky#$G=7Cst4IPw&qN>uNY*e+j-Eh5|4fo3gK$J)^PNwO z^yG8L$C7FpY$N)cAABIv)6bm}B{?&wM$|Xo`@~64WjpFfI%SBCm~W13y7?pq#s?y# zXR@6}B|SyyGRQ_$Htn!}*vBwEFn(9JOe{JsJ1o1yK7sMaF1P87wZQ_gp;!W}0+tmE zxv0E|37ig@3>gkt4CxF(hYW_yhxCR_hm6WP0X*B@wSCBy1d0G9fWkm=peRrhBZLvd zh+rfz!WeOkC`M9VNHBxt8%rijR$fNlx4cZck+BiMVZjm9FlxkeSaY*s(|_}PlVZ~W ztA^#of}OB*SW~Pd)*l;p5q(j70lR3pNV-4;PEqrXr}E7i@J*rlmPGg_s`(bY`DTv! zFr?+b$|ncQ7kS>%)RLOdm_;I8kuOd35gEpdJhthz8s;@^0yac&$e@G;2))JcVI+M_x;8 z9*$(4*29-*k-;{hR@J-!DLifFnk~Nk?VWZlhdC6H_?MSVoxuj(cQhJ(y z8aZv{nk$ZJ9b_r*3)voqMf>c@~m? z+GJ11HOm*~+fcVOZwub9^)vs8l$*A3&8x$_e#cb%*gP7^Hm$#x;k)?SHm6p{{M@V< z8H~Itze19flOUtZMW?%_*{03+^!J4Ky!IaMsqb;`Iq%W$neR#OWuLa3hMng7E(Z++ z%?0%YO$ChvEd_N2O#}@EEd=!i%><1FVJ?3)zXN>$y$890>;NDakfU`iprNg@t+uVY zt-h^Fz#Lg^>2aqKzq7+R9*b6%D$bS*Ut!Rw15N9306Ne>#*Ac<;^+G_%6g*0VW!@*a&OjmKgbXC^9!h8>37=z%r}g{L?k#&ZanwBZhi5r;lA_P zr{E~LiChPa+gSb9Y{P8Byw85%S;&<{@R!!#Htz-NJsS=xp@La2No4lj&#cdupaxkS zV2piU{XyM9%|Qc8B}*+!_1G-MLei|$zS@4@-@bj$znuHUe~b5>{yOb%{q;G+o{^o! zp9NmAUpZXSTp3@9U-@2(;u7Dt)4SHkaJ&0p*Yzm!Em-|ntPSnH%_`I z=c$f*ahgw#;XLZtN^z>;T+?#ts*!J*^i&Rmj&|`^!-b}f(zQt)-(Bg` z9OBUg9i=Uf;*^P5=T$M^Z_*<?#J9ux!$>cxn8-xxjv)P zV={tLg0Hu7VA-%7(`?h6ne3Sy$!y7-hU|tM|7`yp@Ok!m4n;QEX!NMsXx}L3Xz{4i zsLGbl7TK2O7Vnnp7UNbb7KzQlwqnD5<}SuB1}^36eMlP0`RPKVhD{ksfKJ$rx#*b;nS8m3iQ1TO-q#u=}uc)LyStHSs;y-Z@+jEPC z7|Th1z$UG~W+VHO!9QOQ=LU%7H2>CDf8Iu#B@GXU^Pq+}%PHihiN9;3_>zT(`#Et# zqU8*7YucZ&QL>qQDQgjGlxrf)*+{jd{_nkioXk|trjEZ;qv(?Pzmb1#{)xfy!3dd| zoUKuRPanEQvL%&&cISTQk6~jP_;KGlv9!6ovAhq%YW#29|8qXKIR~7Fo)eraoU@)o zt}3r$f~P@~pkdG=s1t++4T9!Dy`X8(sN5FdpzTlFcCJ6r8|Vl00{Q}dfc}_|m`@mQ zj334eX=3@A*O=Fw*F?88wj{VHxP)3nEqN|#{%iQ>|L^=C#XpC0 zwR6sMu+ur+x#_v&x&L|GRrFQy73`|vD(MOpJVhfr4waoVl%1N8T@sa@sF7Xpk)6TH zV#r#4$G6OCwoDGSEb{)LIUx<435aZUZGCB@kIFDzslFGUfWndLgkubT>mU>i=TKLLvI{pp-J=9w##Oo z*^$k%SN8xB$u*QMot_9C4*~PrI-1ak;_(|xssTlBbI;oPiO<Emx2yCV_}n@ns9ooQCCvKkg?IfmyBm%O(dpo>~4d{|0D*BM10Aptv=- z^{VBnm86BFHM&J~xoeqi+5ABNK={Dx;NgM#0r!FP0sVpbf%HN4dCPg&dA|R0@Idff za8K}5@JR4da98j|@KEqVa9{9D@K`YB>etUdpl#4P=r3p$vd5 z^~D@&V~*W1N7fiDNd8bs{@7mr$WR{Z)BYLwTW}q?Mt6ofjmN3n*oAtvM*z`+8^A5t z*$~d*#yd2yJq$RGS~opIog;9@H!h*R?NPu9)W*!2=iwgC7km>G3TpoXF2MY5AMjk4 zJj=pq-#CW)v`1plo*NBk3pn>1ztG_JaLo9@y8qcvob`=IsDFDjX5wJu{Ok$`x(N*h zgFnmvW?82=i^eJ4*xr5wN64dDHj>WzaP~J|w*lZV`SHAU-Lqnx;f?d{CvcSfMBWDG z@7UQEt^o&bxb(RRybZaN0Do!!4O|yI^E^K|gyA%A9B#eAk?m;U2I_1E=X&FN8w3t- zAIGeFo;BbsZ)9%VZ>?{aZvDW~?Gu=dgR^s7@D1oT^zJkGxBNQGSrSg|#{SmpE&_~} z-@vTpogJJWU=Pk&4p~lEj>m8mS4lW0Jgw~mU-#W`UUS|QUl-pvT|3=uUHjZ(Z^>@s zZv*ex?;P%E?u_rm?|kpb@3imu@7(X0?yT?R?m%~hcVTxw?o#fm?qcss?=r#TcZ+wO zcj&vpyZO7`_I21k6pRI*qi|px_!4{pzPiKRU7CVe!IXsD z$%O2Bg#5n=*@OsrD+xJ25(*xMzjzSN6%+pIZ8%?l_rc^l{ohg_o{EuwBN_>(RTzRn^avHXycG@$gN2)hnTF4V&4f#aNrpFs zHH7{p6bdn&z76n(CU-iabK^QK8=BbUpGqJ&J_ikC49~=lmvb z{Y?=r^r%4SF;s}WMu>tef~ZSzMcj7faK7@bnHV1}Ern(TdzZnAukB8ymYD<}-B$|U z2%0Vp+l|A^N)0pda#|A#*9h@03)}s}s7f6($w%dM(-e#ml2PPcS)H)N5ds#l?N+7c ztoUDAKZ@W8xh@;qgG$|5$-i`G6eJOZT?#9IKE9O_drte9LM?)=OMhkiqn4D!bGk$d z&Iqb5^_BIH8dBofv^o?{5u#n@Dfza8;z#vH zF)LaD3eyPQF5{KmkJ^nAR&*5tLtci6gz$vC4&exq2oaE_1F*DFwmr+G!233j@y-o7kPJwH zd4PF@A;mn#Jj9S=$S@T04+JAvqF5qXqVpp1qVgi?y2iQ$I|aK?ov1F)PR*5u75|m< z6^fOO!@k4fLx!zG*x}4!!{Pa%22Ksy2(e0d0qgkh4`yPr3{Mgucw$~4jHldI zVt{Xy+7J#EXaV>51|V)D)W7L@!DT9x>Mg#wv; z*bid@)(6|8A1ptae)5NP8{-Rdu3TIYTyT|tRX~zYQV^XlI^H$THa@pKzFoC_v>m%W zuwA;nvz@s;x81aDbrN!-cw+Yn5X2oM9mF0a9>gCc7sM7M8pInU6T}%L86+4afBB-B z67&p24SEiG3St1!T0;OyZ82?-wz#&$wpalbj4DP2qb9EmD_|+e%LnHb0P}$bn0!nD z?>NhN-Z*d^GcG^gK0fs}rpGL9!z^~nEdF#hW@I*Qe>Qe$HvUE`rb{YrO)7RmDjwVT z#!5(l8c0Q#h)Rg>R#~(2I>MV`A_6o(I#}XRx5JvZ*F~eEm2d^MX(B2K(QUlu;&qDm z$wVt?W)eMP_PTw+Ye8N&jS3<5i$ZPO9@LVFS>4)ej$T;2dnV#R(~!8(?Y`#cb=9b} zDEyDwKk;X`^_qv*Io?JS{YP`2c-0MB3-!7Scq1#sOihs(-L14{dvu6*)kK(Sk`nv6 z?bp1HZ~=<4!nxGCiN)Q9YtBa}0jjd1xipyPV~Jbc4c*{|UZ1tVqibA!fI^Fq4YgpR zXVO6otXp%<;pjL(rA5Ss29-F|?YicBbQz%3BD_rPnb^>6xhAvbeq?>LbaWP=hKC6> z2Z`t1!E2zSTilxvApkW?Vp6x-n*GrcPB}ybK!c&mOFT$8h(AbTiD7}T#Eo@R^d)sW zt*Nc`fmi$1I9EB>idT!*oK~IIwpM+Put#J^@kbZ9Lfi%}9j|!Q;ZAW0c-;eu+sEbL zmT;}O8(cU}5wC&hUwd7v-D6Z#mki!Hi%!Fv}e!FcAy^$AJ^U zvET%7JU9u}4ekc_f_uPycinfrruMAhCsf=iRP6dx{Nq$?!c@FfRGeN^f=AF7520MK z&{yhEz5(cc^$xh6x3+;)lLU7vwRRkP5A><}0Nlvi+Tb-eG);SueFI9W-UBx_K&FYc6pDB0*iWIv0}1L~>qT&Vw3KtInD#gJ5h$(t5EyQZe(emc z)t+bHhdx&CgBzlycTzuTH?uE6nbpVOrf8WR=!y0v`wf&};MaRS5h?oAN7_;BUC<{3 z{qK!LUeiM%+P&;+(1!!v?+rwxGgGy+i`XZi^aI21O}u?XWHOE|=eA-{x!9!4*fx-92)vwK=<=WHidrqg$X-mjY^n*^KW178FR z1#(G4WwT{dv$dJEnKhYpbKmA_WU+0}#Eei>L z*jHU%IL(BH^Tx?#t%ij-@~eriD_nfS!g>EBs#eEB5(%5uWrRx>kxyiyRq-5EeO}D{ ztAws4oPWZ^c@JM<33ph%t_+-MLdSWlR&!o_|Er%aEM%NO@%hNkU%auZ8RYs}G6y!Q z>#0cxC!0{&Sv@&BNra7#|BoYyE`422U(SIYkN=48#YL1-%4`fOtT!K^!0nkboQvkk^*nmXWKAw_kPe2COzv6R3-Mi_yYpV019* z7;TIuMpyo=Ae^O$rI4jK51v<)S4cN8HX(=>oIv5VaZkM4*0AHhbB_0BH%|Iaici2> zC$N*5lZKP?6OBu?OQ%cIOaDvVOV1#28WCZrh_s=I_=Jd@sEBBdh>VYjBvwS8tXe3( zT1vB8Y^Yk6H<@Ob)M4g*WQ}XhOY`w^GZR_fl=NW@hZ%>$8WHokaw`)6BH4JD$05CD zuZ9P&L|T~0AyTG>T^#Ca81ap|1TMERk#|jA$`Bv+7-VorsNpc5C^w&$bxo-ows1h! zNSH5_TTcV_l6{9?JLJ>|n9r13PRs43oDK&$wAKL3G3CH%dB$Y&VPc2y8rHerZe|j) zj427jstyG;!gI53RuX`$WbI)N2WSoV+@za@gj`n2$grb>q*G0e^c?>j7&*7-W+Ng0 zlixC#f0))GzGk6DrkZ^Y?PgvtYng%^HgQ1Ih|kTtS=R#=lHG?T9I|To=cfOTx>@>} z`uW$(Eu`!Z`#Joqk(*m~1J=vSBr^>^c8IQFn;ZXY<|iwYk~6I1aQ?oyCb;IR`l^Pc znxrPWT6C^!j%{x4@A%&;@ZY1qv402tmj2!OoB4O{Z_{6^vyd~zGdn*(Fn6$YFnh3g zFn_RIFk7%_FmJF-FlVr2uwbzK)r+6WpbStNC>xXt`Ud)HJq#FY>u(!w8*CeG8xU~7 zIARhp>3#TSzbs2&EfEjl{33VC@2rT9j=>1k-^q*yzx@ z1J*n6qD9e07?ox^iW)<#8)IDpPg_)NL{aH8qn`bH>%Q2az?&9@Q2S+}c5cr!$;fyl#kfzBma{1&HRQW3tCa zx7Hih8#aBgffv`;^&tvvLclb^QO~i1e%QJu*5Lwt9HP=D0!&AZ&aAs)eJ?IUl-h(b zX`Z7E>y}sHdzSkE-GE*|51_BDyREmar>!rv8ywmj+7sG$+kM-6+mrfEz3RQ0Hy_d0r<#$4 zz36w&RiDg)_z1r~8H#Y|t$t@-Jq-sCQMo&Ol&XwcukmqFe64i z1HzpVr|`&@p~#-t(T>*9{_&lYJ;$~eRrk#l$|+5r@rKd##My#- zs-Bvums6WC@P?=N4BBo~k>aCHPfh3%Va7dp@w|$`OdHQn7!l!9J-fE2Rm5gW^0D|)1l_8nQ3%B-E+!+L7VDE>Z^cf3``vo3Dr^|Y4J$Ucd8j8)oZ zFJVYif@ZM2RMeW(7pPR^gj9HqR5Z5nGoe*CyHziZRS#?8NAejjLP;;O1}_qSFN*V{ zdlW|x#uBzVu&Wn`*8v1AFKiwNLg+f;S9=au10J@#vLO{rfOQP5ZX6y55VgFtd5D6T zb|6-#4tE2{TR3dUPzf^~d#k61mjQ$=tjiBPA(G&Ztksdj%>XjIZba&t(9p54x_@{U zK+?jt{O|zc-|=&G>F{3wMGNOL*+Ig2$JOf1;Vtf7$P4CZicXR>rNaPO0+u+6uIM$k z!xS7>h!}vFB|fREZ;k3O7WWDd1c~zEbi0bzL=Q7@d?6A5lDzn_uB|n)!${oA5aG7} zfQ)g1U9dIY!*m=^hHtgkxFjliLwgAN! z_gJ4kWu+qh@ih5Ogz9#LLPmtjY=lx{gc?ryO@y-YI>#|(U~GtTX#PF@WG-cDjyOt{ zSUY8}{QvY*xm40P1}Pn4y_Ey=@9U?OC>wF$NmZVl-98x%KrHz& z>_tx|V(GiJ>}Z9vGL(kdH=kf*iMkc-o>XO+De^J0<8M_gO}B;}P1QGiQ?pM$*^4Fb zR<)x_c?=jDmj-OU=oi}P zm+oj*Ycv~3MV$Kg>PYhUVG z|Izw?-Y=zNv$b?si&H21ch(jbtyhvz%YNj#gttsn_@es1m;TV;{?>H>OxBtJ4u*7cWxwP~ z&FA`N$MtoUEB7hao7gnvfi%TZYB*(~{%Z$44hMaS4LyMkeE^LfD~*0rsjQI=4@-I; z&l?&zseWYXOMKAinQ=CcLONVSzpzxq$h_>iacpj~dMS^QaT$YgPMN|K+(o~x z^tF+78MASo%NubxgMLCOhk}tI-fYfxQK*Ak=p#!dj4aETrgC@R_`+Z7=adQr?ZHnp%{Tts3q)%K4Qp&%)_FlP0ne6|ZqbLsdXCRTv*|k^ z?^T5hVE?!YGzem0xpw%%B~(3&ulY~2np(5w(NOjCp_=<-W+~}rnZ{=6k$iUICQjtj z@A;=4nWi1&rkw~S-m^(a;s^cEraiWXXg17^wO+_Ff`5?KKE8&1Hr0*0UJO6?hrZem z)zHkwxv|%aSVkfTP3-XyarT=Vf4u~L4Gc}&@70jcM!B)o3oqzKxDQI$<00ehS2x~z z@r5Y&p?dp;8m8GqH_m#|g~!kR9fx)B;=fH9=Jg)`Z9P(uF6l5f>9_*vD0}I|HR&&e+5PI-!`#_} z3Xi=QihYQkylI_$9&dRwZ}||=c|U<=4Rvj79D7|f-N(l$DNQqYJ83rJyn33RTB+l8 zjc>eN=^KMy8%?BEDiu#nGZCG}8)aToO$_*pPivZm=$hKt^*U`L#wRYH%;4_8QZcT* z;#(FU`I}6vTr0?DzPk3+ZN2`;)TCqOR6#bAzSrx!74%WA$;QgFf?@`?*LS)_{E@Ip z%7aoegR!&6=i#EB$1}-n#*Tz_Rj-03;YBl#=aRWuzqHpmyr50oi$)#{k~vu&BkPV{ zHBHisRvyffd6vKU*J-`to7fi(7(D13vMoE1>n2`^Ch2drRI1J_c!D*fHN;X>F8M!_CMWYYF+H3vE%1s*!vU#DX@S{(}477*;R3yE%N1c z{^d=kST!WjtejdsrAfJ>sCisW$ zI3Cw?{WO%j9ReIh{V_d8;HIt_LiyVhfn%P3B#*Oj%CJg70tDhkk+&^SrC`nVGo)I+caSwYU zpDL>VNYk_6VoReLe7F%ws;KfKRnNTSr4dcp!>LFHMQ!L}gysuYooBBe(MCNuQVbI^ zddBgHPW1s>q<-fITZoX!GfoH{R4r2BzG6NsT$mu)=#4@0qgN!{Z#0rg;l$2wERr9? zN$B6GCO>>nZ2rb1nfyJ8^c$UIvhI7tAK%=2s`r`r(K`Z?fEV}fDZe0~=Xyg#`QkeP z)3aay(N=yTwE9E9^52X9p1@K_3HX)+6LoH&%Uq(XYS|p;sw1mipiG6-q2~iA_ko{7ue85Mn`OBd45g!Tf zFO5pla$?tC7L||7Nf>{rRz7?c2Kz`x5CQ)3%FY0f2CTGLi>M7_OMArZEQE71Y=&Pr zQj5lB_VC%6;K70QFzY6@Ppt40Dz6ku^^~X1DNoE(r~>sVzi`E+a>d7$#OW#LbL{4` ze#>W{&u44S=e)Fg5oPyklNK8j*b`zGTJ_mXkM9*NZG4PMkDZ-YRfHKn%KnvhDCU_% zkGEZ5RhXH6`70AzM2vBdi=A&(l$jyaE=H3DLPZiA-ot9wlJVj(O>}Hm&y&@+UTIPm z&)E`b`eN6h!!e5B3@#5^ysO;fzUt={-DtST{*U%NCb$Q*8hZ3OKu`7+Gi_3gT95sz z*HJ`(fh>D2?O2RckI!o0QCNUJ9=Ot?VoZBnSACD70u1p`l=dLTzbAMVbo2$M7xD@~ zn-`0iGJrEQM^f_pqy{g0w?hS%)3w0ALhxzFw? zcWIiIhCpGmfkr8O(trUUQP$BAbVwG_$b=6Y&~hX2GM>OjvbRPPJR?HAQTZ)gS-rOV zZF#qP%d59n4Wzmaq*x6=L%5R_xl_G01K}+O0Pc39H#}T4 zsid0Fmt;@r45Uk2j1siB%+csp92J2u-$hl3Ml*w4LT2VkWrfF#X_^Ct8zSE3P1#m96)#NFcn8uq zc)X39zFAr0AtXT>^Z`VZWo=#I_+4Pkb3x92r48HTkAV@g2F&b9U;Fy)H@uDmpxzKg zFy~;_2LG{1U{;IuGAmvi82Gs%cWe`w*8&8*z>5I`(Hm^X`WG1?RsdE!`yVLY5Ir`( z$PTe-dx59<1F#L=W8;f&A=YiIcuGIeupx77eUTdi41IxT?*mC2oX3V2nb%gKtnF#Q z0n~=%vE@b1wax7daGLx;-iF|@DflAm+WMCDE)6`;z9D}MyvWN|Rxf*Hn99qYmZrhO zUYDw>j({7YdFj(28XW93sVW0yc*T`hIxSm6fW7D)X$2)|sU9g(h!pXW6!m~qPg%v_ zy^6jO5zpIkYHML?!zyY^FKW{xYT&~(z1Xxx4Q6&!s_p=aT91?`GObgC`$=Jmffjc~ znw|zvVH#S4zOdXthZ|+2$HSAJHmE^USZ1JIS~02T!UJ7M&}8u%eBn?jq8w}bH0{Tr zkVB)0B7G^uK$X`%t!hxpp#`dvs0e83<9CHXw1)nz4Rq{eWUvI>T>Qj0R=>oP=88H(7YL>2lb ziH3u}h}KlIgni1u#ToCIneZ2XHRUa#nbN>(kZ~5q$RZO{*Anq5Xho7HixmDrju}&a zFXG{@o^Qr$E2bZbMrx%6Q^LDuzS*yBpwE&O!LhrJzVXe*OGO^0{v|R~*1I0QNzJB9 z#s5qxN=T-Z@FGP_kda&wv#D+g=ak{DGv06iOC+Z( zcim4DgG^hCmre0G;wiOV`_tG*U--zqSOBu749|8b9i`p16`C2wdHW4;snet1J=@jL4ArynGsXTK;_ zg(Qawv7I48@_hHmIu$F0`0mGd+Eq&N5r8#24JyU>2!}eoD`m^?@pfudij?0+bh=c& zE+=s9w5XIQC*11{ssy;*W9(F|6n49x)#+F%?M7hPX;LZfM!3-FS1I@Ro=m4srRd-L zKRZ1tW&RQbciL1+{w2KX46T%ZevhP6O6tMmD5ZC@TA!6xgruG)er8+Yl43~wV!t8< zmSWcVOttb#ieBf7;fk0P)97c>6+S7((JwwLvQjJppUGBUHa-*hqPZf{_`KpX?+Q;N zL&X=@mDi2T9-kRkI5HkccJY7YT_kSkV*1FrNaWuo_fc?>4>e|0mxic*uKA+}_1<$azf!?vg(gye5HuhjYK!300V$w(w`3~Wt8iZ7TFXv($O!WSq%|#c-`e-HQ&8vLMglF9^A0NGC zdUG%Kt)hrQa@-5Ww@R-?l4JSavyn9QD*;ILVqU@s%Lue#^dJI3QvK%)5ySsN)L5(l)1t9flX-X8h+RcWYn z18v7pw8HQsg`seqp`ePP_~_er0`19H+SAn9Q{SV+v?s)sM8!30#MOMnb+L7C$m-q> zAvea4T`p>`EeMd`P2TdB(~QTt^wh3eJVe4xGvQ+!ef&y7Qt+|Ih z9lvzBslA=Q=l-3xm>f;)s#F`muK*oSp;4l$jh%nxUM-=LRi-^rEvA$Gxsz)e?c6n095XO_=B4*N4?UB}lZceHCON zmzis&`=fZ$aHsTCEXYDGC)Z}|yVIoTPUa~e{!(YrNH#Oq|kRYJt82Eb|J+ zrnYvlr$m>Epp-TT?-*6Ber94hK%7 zwoOl6FMV&KLJivsG1CXz{-?p0pqnqZdSJNxbl$e^sS`fz6nSd^E^MF1YAP)wUMT1(TWQMhTMuWrkO4y}WKxxZ4Vg0f*7LoP z2ty`hSe2FykuuWnu`cAz&;-f&Lf@$H46Z<5+wrWtgT93oKb-#G!-Nknlte#u32AkS zxOE9zb%}>FA|oTYf7P#n1&=>3v`i7an}(4iWBUSc4q({%4;rJdok0C0xWX z0omz&WM3pa1kP|D7T#pQN8Wqy+yAKQH*@0?{>m@}+2|#;FRU6gb1xU3X4r$A_7d9{ zR1LfplVG%jAbTb3e^zzPI{y{%V+@A0_5$qNy?#l3crNmoQ4LbiE4Fqd(-Qe6QSE+tIIp5BNX;6C7`T}B`T)hoI7^Qa5wd@T~n2!^!x%Co@+o)Pe>40mfS zzS2U&wdUVzO|NS$Q)|78D|$azWLL%^mQtG8?3_{LoLRqjugw6TFzNB^_c{{A8HOXyo1c85q$jc+z@rjd89pO{n;|}5 zntofo7m$EuXpT5+diz8+p{*P$BxW*PM|?Mfe8QW?7vFPy%NpU|H1Wx5T3WRIC+45| zb3|^_#wV`{Bm0h7j3P67gl$tFn-RDuYnv;kn^`;}x@nHh4qR?|XCo$<2^-cV|Q*9|-o^2w)>u5{s@r2e{pVnQN*42yF^C4}evZhe=d(A2%GK>EI--Cxm6J}mi z`q-c+O(m&CWO=8_6GRErH-p2ru6*8v2~lA@R^};Q$zYLC-iv>eUW&JKJ?j1sZExWa z<+`tbOG|fmOLup7_t4!SsemvFBAo*aF@%JGfRX|ND$*@5ln5v(DIlPfzV}+|oVEA< zt+n5E_CEXk2{6w*bAP|r=lb0B5J+9^EC_1qX2!kzL6s0vSUYY`SyMAJw&MrNfRL`* zWpl=w=Q9&K{`mEnkbqex?|QM`#8#5p$FqU}Mt)1yoGqyHJV#{*S5?` zdv~>TzxVKMB&i#lrSPt48GG-yUmptTs9l<6@P6Jh@!p@lo&XY8+XMFJRUjnxE3DUt zRMt*|6sR_2T-x8Zo*x3Q9hoKHeHt?M(r>BW57Jt@I7`3#EM(%Pzg#^7B&~J;6zQu$ zqU_gIZwGnKSvvM-*tTyzy7^K$!kq5d6dOLGVA-&(Qe(e@=H(&gqEoCX%LVPEpep+yb`&72Fc zeXcXpheQ- zyzJoLEU}Pq<`$9EHM8vI-+E_WX5aSAFCwjLZrS}yE8#rWzRH>7Wqc%3-i@)HW#|#9V2*B@?%Uo84tgmghbYn)sBE1c z*xo$}x{yIbJkUpg5ql3WV6_n4y-H-1`XKqXx^x+0a-7P&78J_9=-%d#h9QQ=$=sic zjO87S-gc92LM)8axjz$`$os;3n?gDX(Kk-wUfncmbx?cTT$-c?F*8o%Ue`2k^~LA5 zxO4$R6}GjNW|P^bwdCR#C=pvQrMK}Ubh^!RDcCPuBBo#xY)wO(+N_p*{6Zz-3Z}<4 z`fW!0Y~Qlm4p-RryV{O#*mh$)`>g$JkNeqzRZA) ze!fqCy@Yb0C^1gIP7oT`cH|c=gQVXixTDd=x@2_V0WZ}?(SrJwb}THd`h~Q%FD}s^ z=-+{NEH14bgnVniTtYig`j!qJ*Hwm)wDy4|(gUq;SrPB$)`~(V+P9W)57a;(f1zvD zF64Rp+!F19-nX2H#jdsGkT31umaq;~zGYm_N3P0;6t_<-Q6A`g%f5Ucx%Mh#rTxni z;ep0C*yY0YY7{i&b^FQ^}<9Jk^>N+ zes(<;j~9<$hlB3E9m?|(i$R_}bn>Iba9DRSySc?k&Hl{MPWWl( zg&o@S$8&?;dtyiV;bWbfAfc_88}{CBIeH(y-+5+-^}K9uXm>B<=wdp z*?GN;@}=a?pv)fqQEK===Nd?N%kK=!>=z!5hi`R$UB>uQ3YI2&wnxvx=Q?+n@xMH| zGt#lYbaW8@t@Cmj?aSkDgAsdjM@8Wiom(IZuJ|?_vEOyH9R8*A+cMUdvTs9|dyz-4 zpy4Z>UzQ2KRDK(|+`m5h5&pB24V)G8ia!{9sys0{uf7(If+!RgFN5v#iO6~0HE)!; zLP7D;#HX#3pmQO{uK~q}TE)kn#Ya}fU&HJV9|&Az3S1^Wzj&wdYIFD1r-D}-i?7x? zUTs}QeT<7*-#YvHJ?#2kRMc-p0LnA;Us?dLtjFxoUkHD|_y30Xue|%;p#5oIvCdi7Mh@%+VS z6M)_i0*HPzbn^AO{n_*H7T4b3fqn#?e!a1B_T~E>=yk-8$e#~>zJsnSoE3jJxOV>$ z^fUJ76m;YD*~)kS>pMRpe#ZQq{J9Q2d;Q)1+W*I$pK(8@e{TFd`}rMu4UNiV@=WK| zWGKIBj9DFxRLW=aP3PBSbh#;vnHN2(l*8nm&Ns*al48vEXv_N;cNNiH6fuI|p!>YR zxW|OfM1&^z8`V!$qKJF)=H}=doV_>LAKu_zG2ML2gcFDr7Tu}ziV14Sbc>1%2M0?n zx)kPC29Dl1URWW~FO|AVqTVriP>JHuVCh9aQEDm)d&lBI-HZdl@{N9>)KL=oj`=fH zFpe~qZS*sx){=;Kte>e*aKf=7qhBe#E`bU%QBn0`#6)*@r!&cG-h71F6aA?>-oZtX zn~FXeyB~8c`l$P%gOeavE}bs+6U^!8?e1g;H$fg~F8wI>Cg#`Z^X_N|$NF2AbiCL( zm?O~}-H8sa_1u>9wb-+myV0lJj~txqxis;_v8;QGdYk z!@3jQ0qySk7P{RxEdS`%ZcwZ_Wd2Td4@Vx$KKglg z>&J*g*6-9;I8j*8=-1t^nY406btyOqO&)Ufm^$tV>g9^-Qa&LRc}U%(>$oYX1JF4N zH$rge?Xhv>?Nyd#Qx+~)mUdAVUsskVQzlA$e3NYWG09VAJR{hr6*98+9$Cj_LHk?< zULp!S+}PM27RO1LR0W5CcWg_KwBu5}ouz^gkvJZ0Y)TJ@<8-~%ysQuL3p@~X_J}(! z*4xg@?-7OK$;84t#!%>R*53XK|5Gt?z&=)xtKL?Y6he^c6I4Dl=>h!ZZFNZz1o=3@ z*2AEluuoUFmD&Vt$oL814|RH6KONszYZJC1e?~C((7Pw()5UF-HlYt>9FO24<0zp} zh1lXAflqUX*5Bo>h=1UL&P zDX_zcACIXXFj?YpTa+y^jimJPWWC2-Q5G8NltkDFmpHVU1sEn0%{9DBTP9z6L^d-6 z-$Nov+0UENSZ`iFxN)PT$+xVjtJnrH(zofe_+laS7p%;8QG9ukZjoe>W|3l%VUc)| zdXapQevx#Mc9C+CQHDr{N`_2^PKHE=MutL$L55g{T83PPUWQbLR)$iBv4g0Cs)MY9 zu7jk52HHWPhjzbM(mfF1JIW#&k z6@62V+YNX?a7CI!JreEO*XX#=Kxe5BS}C-Ke}agu);n1S)S{b{@Wf{K4?DOv1X_mG zqJoL!p;u_nk6lY5^TuJ;{{7WyCL$G{hZ ziZ&X*?~!9q1M$4351BX(ZA?mEzvEiNU0xKO-ao^~p#Ks-Ht*(x`+^*R0t2BAG7Yx# zem;aRC;=HTK;VGYpfc~ccQces<`MmXz5{223D^wag;L1G6%Oz_P&VkyyMnhBIVd#` z_&G>4SkL?J5rk59K;xDM7#s*2H0GW6u+PXqi+KP{01X!N-h23G6cKS<1M&`x4F>b> zd$?!hmyaR`q8t<&?C1UW2+t@ladaPKaGEN2n!pAu{VcVStz>~sUS@dg6!lS(F-NT!HLCIv~Rso1L{u#ygZ$fPHHyM)&yW>`S4nKXr31UVZ#+0<~fz`Fcqr0+2_c>Sp3}S=+8O8Qb zA+~!WuP{~;{d-#1WDX0z^WT$rg|(9OrRUok)?v(d<}1M{bcIC5p0qX2L)dq&E72%S zg{0!1i8ac@r0?utr-Jc1(Z1*Tn#5tjcfKp}D6H2>EAl;G)(8*dzO!Bl{XmB%D)bbu z34p>N&z0B@OlZ>Uo|QGm!?f?5S0XPEy2%8|bxse2znh|-9PIkL3F=9~lE0gwo)T>LyHRcv(V@n56W=DILmTMEzDc+TD47EXpcaz^Hw?Uih#=lMa0kyT8{x;dX%8tn(Y|zJ9TML|E$*f~B)^BY(oJ|sdWHJBoAHqH3Qa4%TveDwDkEA` ztDMD{)HA+WRhmUE*_)xj(&p<^aix+7~{7Y52Qu)dI z9-83&PO2CGL{+3zYBJhG3%uh=edAxK%9P4YKKQH|%py%{8~;pIqEu!w=CjrbOE_s{ z{3}(3QU#&=RGN4!m{3xc_{Y71PLe{=R9eX_>ZH!`4ZV_1vcv{#LEp1;1Wy(JT3Dq$!`2G_uB;K8nAqE6ZF&G7-Pki@VBx zlz3MUu&E?p;=lD`t+E~^eAfj?4arLUmtMkE&ZDI7dRNRpNPfn%f$9r&9zs{?37bhq z^^mBGAVioxuV_@!jZGvYZ;02$T$nbm5N!O|f->5NWL@lp8S{#H6)D(oGh&BWT#STM z;qok?SMduuiLDvUrj=N(^~gmlbzMsnhmA$en8nqY+3ZiqNfyvJ7?O6eZDg=2_E8jP z10{kX4v^(iFCcuBUa)~Gz>v6$btC;k(Vk)`8{qnf_(6?^cA;=j>5MHRqiaas#l8{1 z^>m5^Y*-mFs6))K6=`y4GAHSvY9Ojq20~SzqcYuO$a6h75K{_6{Tiqj2%uP*2q<2)0<@Ej}Y{zG@xBaYF|GXUAn@jBfe zG^pqA50mz1+vqI4aiGyjAN_bni4;~MXmuSW&@wnFO(=MnNkLs+9a7u1DVDV zSvfk65evJKaD^fFpHZSMRqsvdpYoffJv9(3p)pRW>Nh2Q$_3hz`Yk08e6%bwiOvUbn=OY4lS{ z)Q!ukCXrNLyke;|27M;jrYvF)@-_8JIZRBds*$2z(qb|BS|E)!394!bHEm1zO)RQvkYYR1Au%tt zmP-9h?o@SvUZ?!&{cugWQU())s!F5?uxmtzYju^{nfO<=B4u{uP9K2ys8rs>zUnzr zVn^mQ=0fYbG|B{8^%|+bja`_fug+O%@{ViUG}ucTzfdp-csD9V-cfJs2D?e)7G{sD zyH^IiliIck_LC+A{Uwd9O5ArC+seU?FK^mrrIx{Pu-?n+MHxN(c z_-V9O%DkiB)(>`niMy12pnj(^;+@>KU9kU4!X-G^_f%rNL)%t5fn{7#LD#m?r89~Y z4azwuVcT3Mrr|Uool}}9cb{z9mO8NsXOJrfj<9md$)s)e6XS5|E`*)Z^Ky&Hnr*QY zNH~30(Xyg{xx{3_Hs6VPIBi!U@O+iune5n>JFyD~0s|oSDkq$b+hzr7LaIoFyi#$w z!DQvO$cfbjU1ZTKMGO0Kfl2r_&xzRuO=RIKrIm93$<}R|6Wa@h$l@qP5UNe4ZF8QO z0B;o{N(rQBlh3y$POLBJuZw;tg4k@bcw6Aa;)3?N@P`t}$tGWKE1cM0Fc!ipt`$&w zg_Wd{xi?e<*;Ir?djxrVgk;gEA1EacY9^1~N*?|bEEk3imJSypXt@snUM2|U z!(lw zNLtFYP{CLT(~B8W|YQ_54fWyKw2YQkUIuuPM)Teg>ewi2zTTz zjWW0iG^H&}KtUiQ9GBZOO1yassH4Ua4*J5CxznT6o4i4(QWG`~{KD|ukx}wZ?x2+R z@edAu!mYWBqx75nL22z1AWRWX%N-ac-Q@b395xQp6XEB%bECAId|y+;CO}#uT%0>G zO1a7NHRXI9#3RD5b5}+gHwC_?X}tvvuZE!oGASQz(A>z^>v`fj-LSoY=c6u8T97yP z*3Gr4VPS#JM;{b93dVYX5USyP0TsO2iL7(^-cq=(&BN-I*hzoIh3~UlW>#3{yISUK zSQf>?p0+c3QCJt$^~}2NHk>YC`l!4hUd;7<3#woZV+#~MI^ejT@7wdjb+KW8fxt)O z1?ghm{#(!qYgk%f@G$_V_k#T%un}*#UO)lA9Yiv@^lw2$tf6Otcux}?=JV-$Qe6ic z))wxHKhSf@Hbj|F9^m+t893Co4)zJ`@I;-liceF4+ifQ{nEb5pgppC#PmO^KZF@EZ z{G9Nlk+F?W$ARZ}jV$~PdmxNNRR!7_yEyE-_9958t?&cCpG0fi;>iBmSrArR3^)M@-gzi9Ouv>I z#MuV>zy;#6M=~RYYvaHclLYWV;XAP%!?tVBf+X4sKJXog--+uO0sG=0!nU{%tOr8h z9!3m<1#yr-8yxtB#lAg?7y%pMAjY<|51a=g-(oL^!3tO^NTIFxgTR5rx46rZ>$S{7 zt#g-qfuNLIeqAP9(*rc9<4ZIqSt-1>Co!j=Uyo2sK6%rV2WzwD3Y+HP7YFM z+5R=zkJv@)Tt%DIMH^B@Tg))SE(Tb*nhqmlFX3j~p~|`A)opN;&t&XN+Z;G_J`cFM zD_>;rkrcMBrV3qo3rr$Mi|6!L`cagx3s!o7I^^i`9PLW!2N5(^;Vp2591Wb4UTOUx zgXV+t{Kmx5);TWt4J83F-di95Ihs4Cz0&(Z0WAQ>`;9M0-_EhFRDKZu%!R&H*eE`l zIH$bQ`9c0OADs3#R*t@$6JBZjApM#5^DWr+9j!pm8Ltd}Q2s3V*#q`_M_LZXxp*9L zTn^f~I8PpVIzVy>p2Sf*sOMt4J+g8z&&79(lXB3{#oc@q=3ti#{yGc?CCi%>kJKED zsIZc{SL@Mvv30tq0M3d#(tTWyT6>eHdl*oyI5pin^_aET&O|Yi=;`;_%9-$~2bBqq ziHwv{SZ*m}4;wzl|t4DPF|vJ7Ucfhej7VQqGfy#ws3)*J!(lK?kDp!C8k zJy2YQV{`9gx7|Shdo20ZO;i$9B@!1UVpAp3;6I4TTjf1bWKm#*W}M{hMhsb$8!G2U zD5gZ@%CSboreuAOVbC|MP^z~?sW(`N5^kxoVm8F9$VO`_-uy4PMzow2EpJd+JTN^; zcD?LI>@DRMLtj&@8MRZxNaPJM)f7W#B)XW|m|+kSwMDhg&>M-_qPA}sfkX*WEi`mT zVuYwI8QwvnovL;j`XjMU)vgVpqBrnWW8OK4qT{Reyt^%mYM7#0_Rd2TGevFf-CeRY z5<`~!`y8cO9>V$2Pf9&Kr1KxRm0Ed-=f`Z8hIzOU*pQ zAThJ0As%w??~9k}d5FN|%wypLm?-jA$)rgMeSIWOS`As25MI_C@{xp%J{%{thD=MV zS{8HinuM7?8Yev=4ks3V)W2tri4&vaRgJP#txQ$TkgB$#swT~<*1@V4C#v>%y@tuX zaK9=PD!2+33LktExXhuF=5(B?lxuzHLPiKCeKO7jSRdD_X+XZDEf^HY+!LDmq*iSL ziX`oAnE1&k6O#JaSB(Sm+HAnwKxUCp(XG1#PzAB|>&5 zp`%Z3)h?j8&0dxXC=n9k`dC+uj&kqXfT@DaAfd8PWYx;{2yxdA)aA$o65xG2t7b>} zcWuFxLFS*(+9$JWdsKAS{vH#M7$l_iaju#i<$bpSLkF3C!t*|fRqLa|?{-&AQDh1U z#eD*+7Dol&ZNUse22FU~r?6^&RGbU{>5JkfLU3{YD;A!Ao{GpcQ&-WffuL_PXAWpEbiG&Lkk;pfUSfhtjAfmvcC?UZ=eP$ zfnmUspM^4eZ5{4_2mHpVL%GQ#ihob$K5ArQwaQ|*$|1MP7PErmFJ$p8=KqJYgWWz0Cyi0*|0kH2}_l=DB_%eQx4*8s2w|nC3KqFaR|UcTDD=l;MX>P~)94;|PyPJ_i9W1w35QyY_KJQ7(2!+aYrz;jgd!Q_C4Fe`4Db)(@}HSDRZNfD1g z7JQgzqvcpVtftdsnTIheZJ2YT=~(Uj={=KIJPKLG!vY(v#~SA~S0WyW#mcF03% zWBQ6z^MNYonSh1ks0zG?U3x35Zkh6W^>iMGx$5|Awc&FW;V0d>ctuQ9sLmaOY5bw1G8_{ zmorc_P&-Tyw*)hsSz7hL4C#*6X%>JB%ZkhzzbX9jG=GqgK;r#a!) zU1iH=z&t#IwWD&HabXE2JhS5Ji5bcrozv_KNMzY7vz6*EGlV-Dr?3kvFz}hZu3nj8 z+%Y(XUszw4{V@Akt@YpFC-kq$&p_|6=1zz_-)nP4KuS;Hc&jy+LJ)s5VNd-(9_+uj zU<=_--^TZNsP2`e?w#E4h3NM-Bo!@B$S)ttFRjjhvYl`8I4{3qA-}9Wzw#VX5(_Dx zney@uX?_Wjt9v!8=v^f?MdPj4`~)IW*FLN4UEMMT@%C+g0gJ85P+9kTRJZ)wVRY0`FSI#TYPirkd}o{~os@t+(q=#lgp3xNHaFeWc8G;VC}=#S4~gL1K@L z*zP)&54Z0ET0O=RLgcRi>4jlruDlK7<20nkcG;;Act#NNcF?pr(i7FlXn7+rzs^>E zHaYdY2#$OtZ&F+_F}w9S=rru&UgU#UhG3eVg@4vLb-f6TjD2NnU-5i)?z8u4$VGT$ z%qt@>-2OR1`j-GH^#7J1{Y!xK79dDJs57uW&zKnWClkb>iA{d1PFz++>`o?+hmJ51+#O!q@fQFJuwQfBJL$5c-rQ@?bidg4g^ z;uF=gPt>o7sU8zkwfzTc2%)JiwUtD7A6 z@DCQ0rYTGwSEnqinH>A<2dcOrf)CU0hp#!XMe&56Sg6(*aEuG$>zc3E?;{>Ymp$Cbel3R3x*&cfJ91vLT&@7qvQ?appzp;8ZNK@^? zES>i=v59{yR~1>OyYH;u{X1sV6?~-F#*@eU=}+aA37=X19JpZrvhg-HN~1 z%(wW0ez8Svu@z0GnH6Tgp$@&?M>$jcjSeMjiCgH|CqC1R$dY@{;GEVnu&}m&SEgIP znRA|WU+c^zB2jLp$jzYDeO_eW>dYr1Rc@}xeWI0e9(P~u%qb$F3u)&j&}uQyvu}3h z6_L_4Yv=yFRbrlYU+WfvrH0i0{?xh>;rvFGpux&`NFG#R^ zo8XJ0#_e=g=}cCc#4_ppG8yP2XXzYgnbbAuqBWTZq|zBCBh~xJfF0>m#DzXu<>To= z&ppwAeCaX7rao?E#q_Y}esjQk>3zhRK2~KJ7#R10171omArABjD=Vi*w)am0E~Kv! zKgMskm;A<X&$UM@0By`LP=FTI91((i7HLy>wU1EU#AF7YExU1gwy|`bav+7T;;!w4G-pmhf>2sq~LcR^Rx|;+YV*h z_L4usfFoG!h=UmC32m<`=+3t0;Hi)?+c_zu+bLkrsUmfZY;zz%WB zcjHCqVw>+$s9!{D%;Mz!dT3}zo9&XH-yN`0oZjC!3%zWMT#E98-nkE=fpr2Xpioto z91m`T72_2BMr!CloAXk@!Ci@Ez1yTq*axb2GG!JDSM@_G+ozYP4|MP300H>gc*s`! z*CmVtz6 zh_KF1J9&ZE7H_}-^%y5Yt+Uh)VKK^ckbS5fUeP&aNBz9kVw~rT*&%m$cIPm#K|Zw@ zt2vlC^a>Xu{Cb=4?n906ERBfdfpEk?#5+>i^@O7Jp`z95qRs81PkBWf3q@<~!bMx> zb{}Ky)@P2rB0@S}+Q~hCHTMRbfRAY+^g5pa=U4mOTX3{JhD7*wzOVxq#>~M8Sg255 z!FX7xq3RLNoej&9{+*Ox*zYDRqx+Xi3>EC@9X$!3?%ZC+^M4{SQm{XEv>E=j^L!Z< zoP!6g_xO%-!bdtcfW)Yxb=Z2p?r1iAxASxv)4!~B=wD=Fq5oMZ_FraVp??p>wqMIe z*(nqkuK+>A3GR99HA|F{!nnXD+`y6@hI!pg)7TnKHfQzIWN5CkFo$O?WHfDzMVvzcU{Xz*}pDcSz-K0 zc=9VW3%Xyq3Lh9hrJZP;mt70}u!JHMmeanGUa9`67W)?|cF;8x2(kZLhyB+o?Ef4z z`#UrHe-F)mP%2{bPZt{F95sN)K6RK2Q5^TtEi#7{)7>hhQ zQK_ON(2%`~#uWD{mRNMY67L&S*&8Ut=_usyP)O@hC=XHa9^SxkV!{Y4LH8=b2zNkt zbHE7wi0=0hBZ>&!kqDzD`n^(NiJzfB6{F})8qAdFekE9m=Q~~xhUS|P%)01VrGgUQ zcl;iV!8fHb3!|aqO7IfzcYL22PHu){c115M6_@zG6Zp)CcM}uySFF~-Ly(t>A^D~{ zW?A%PcdmnvAU_pjFBZ0vlOS_0l`akkmPvGVce6vdAZsr5D2^LeP;`5Dr$badlO+`| z4h5D@bVYZgLs&hFC3P*1IhJ>HOE%53JBG(p#NE{_FtxI4gPJc_CU}eR{{i& zzqz#?KmFp>PR4}^9I^`wQaCX)M5o^zO2^qw$6iRsJtumk9Iq^tr7UHtEQVB;6IB*z zR+b4?mN-#Xz~R>D*DnbIAjlqxqUSyKJYG(}Za z7ge?3UR9r7wR`BS%*q*5S{Za)88n_54AdFaRvGkC8MI*;j2K2#YDRTcp{ATbyp^=0 zfnWN_sH`dg$>v2PmXxo-SDIr~Qx$^b@S<%=daprWnrc*86@+Bpp#fcc4Y|@Hqvut3 zkeoZTr%4wYXr&JTLJI{9Ee#ER(jyJl(u~O}k6=*_8rqbkevKm|Cj}B zhnFZ{KEqhNn|f2}!X(1OPgEeEkuRP?J*l)0sNX!Bc_9pS@#g9^r8ATH9=^@|5Jum4 zarJ^9r09e<%+XP9C!9}Bpl8-Pe7OZzm1HK1S!7?GUC<~CDs4YkNBs2E4vdc`s)lV z_%;!gt_ag0sj_E!jruV8E{Cj05k}g;UlE}Hw!`pO`V+lV3LFIzTHji)`5waXPRSe? zdW7f(uo1mnh<<8BAIkVAS^keK1OeFa01S(Ozljw!+L9vL(y;EvYy64k@J|U*(xZm& zlJ{g`cLJuM8Cja@Y@EhKTnVPCUM#7aD&_1`#M=pYs_HDH86X(!CACWMQ4M9m%76{3 z{b$XDzlaG@RZ-!uAt+4p@x)XQNPlM!?|-~mM07Lb{vX&wrnl^}Z#hv}*jWBSSqLNe zMhi{owREBrD#}&VW#b@*C2;kcI?(_Jks9)rBH$orlP4}t5a_j7r3HDN(iK}2@#_SI zUi($XjEB0~9IP-H*IV$z&D|j=NAc@+_q6>Gx|U_iFu)3n~V(8Q_#_D*pi&B;L+s z*oL7{XA&>O>}j2`M8F~kU%4p!rd%Xo!Gfs_J_gNoNvUW$tyC5bm@}aNc%`7JbO!oN zoSCq}p>?uAZXn{(A7^sUY#LlxrwinlN<$a`hfZeFVBb1PAXj^`jXoebGHV8B)@cIy z+EZ-|{Fo#%3kJv5DFS)gQ$FY)Fx|=Q7+hLs2oz{f`(VJpM3@;j*t1T2d@C$jR=!inA zAY0Q1;+)LqgLCV&$9y16GXSQX%;Ld`b;@HNke=!PV1j189$Z;xJQg@l(^Uo;nbA-+ znW;86d3Jtc*_5GYb+BnTH(GYau&V2mM#Cdhb8gz~!eJ%LjdI`y`i}}8fAKqJE5cpv zP?(@jWa^*(1`(D>CRUUbrpU;4aq6P(zt&Tcn>CVGI1d#(pJ%Ay%~t z(h*#Mo+YhLU#V}%S#5&!+`$8BlSW}BzaeF{4$^f82gFP2wv~Q{64lm7-yH&wCuuBI zG8ht8YapF>px7WlQkSc=GZd({Kzi@sgTzRqt5V*OvDyIXzJm)QBK62hpn9mbNBZv& zf=oz*{V{B6=<_=0TCo=<>gx?mdVX#WmX?+)d@!!*UMeze_4%$ePhrZq{#HrQRQq4p zEXDs-o#ih!OZ^hKqyzZuglNhFen$yu-qDRsqw-Cq{{bab?P%7x3|5Q_R{ayrWz6U9 z6CxnVN@mbcE76!L`#gCblU6n`P5L?Ege_cHF1AS9psaFQ41vnH0|e<6Ao}IXza2!m^Y_oJ{O+w zgKI$JmUX_AeVfMm9CN~aAsG2kUK`jtra3>uPPi^aLA;^^^c>TapOa45FN8s!q78f; z(-NNxPWUdwfr3M4rR>Wz;pey$)(fF)=))*&g|gylfzR+0o(nM`+0c1iwldB5Iqih= zLIhYfw4r6MrxiXIp9ow?0F8#u&oZzV&vVfY&o#OwZtn*h_Cz~J~*9m-fR53!5zw?P`RH>h+9}rXjfltJ9 zvgvoe$KjyN;q)gGPbB~!-W18A?#FXi7i7&d8uf6)gAFFT0=Y#V_Nb~GiAY}FTi_5C zn$DvJ?{o^0g1nwl*G-|Xc^J@9W$+7@YxS1bHAiS9k9<_yjl3z}s;Aabp?_fke-q>zUAH9vUFXiGp&a-U3mC&_EvPs1`8rG4Ek2qwMb|pj}lxoNe=uD zJ)N#VV4g=fsCiht)e#aa! zcn6Ma-z4(7u$#M)o4fwaib~PIzuO>BThC0}@JC1K9#g2G%tQKN0%xGRpU>F41w0*( z^jroS*XED*&_h{d5(-E49V;8B=eI#ii9;r-aBSRhtMTjn`5r1Xlo@CcM)@7#jU)3L zptZ!_k!U;i%yF)9cm8w_GnBO>VQJLQv9)n=ejk*WIDz_L?7;C`8Upl#^Sq~@Z%hgw!4LB4G-Y-FCv z%{|y13|W;9vTb{2Bk@dza!rt6>t^cut<+7b)QuYh0kWL8S+JxMU{bLrQYq6?F(Oh) zO;T|xh>8_ zd}I1(`yB6{x;$xd-o)F1um6a0nm|!k5dPi3)qhlXE`HDY75z%lmp6CTWsVBZ`2h+< zyHfa%*4O)6cZ(?RX2f;&jQ>FnR_>@Mi+TkPy%{oVQsnq06iecv`O44VrLe8AXSxPz39KFv+K?$<0QZiOcmcOPQOwg^079cnW9x;fXRPb6L99Ag>GY zYHY2tICDq3${^P|j6}l(%B$iSDpPu|JCp?Kb&j){N_ z3;K)6bk#5*SJH-#Ih9N!p{!5Hsq{BIku#pOPixf$)R)W<9Qu^*@gjXzt3Cm#k`@Tg zaZ1W~+&;BcC(vdx1>Y}9fKd0Dt$GEdw3*v*KBEMWX`kMz8z?uKecI|w-ruR7HDedD0`Ycwxk5ayy10ygcu>STLth$3{ zl-U(W6eToXq0fHR|0wOd#TDm2D*yVsMFsf?bg`y;@B~>$(M5WA1sPz^qZ!~%nkdq` zs6GVNZ*o8#4}bB^i+Xw=LYX(Ipp}Q$@#a}Q?H<7|TrV3FHkYOfW+Raz!Li|emWPJX z!lorSVpS?=`R}5|gPEfL8|#Ic#IU0zz*J4up#K|(-hV)$C;Y2JPgASH#W;{F7O-~g z6*;TrqQ(%e!Yn(W!ORlY%B$ct?#wa+NEwxS=B;aj%B~5|69q??e=x(Mv+1w0 zE0M6@(#quJ%H;OUBFX&S3=2v(6?Vq{)oDnJ9nMoYrY2g261X5EUeuI^ z10Sxg$y#AF<>8r}bt@&iUu&%bdrB2t4P}+jqL=+dYr2AZO4l<-)H^+Z%dCH~5%tNN&9d zOt!5bG!A%xuaTCJgMp)M~z@XbKtFE5~L*R?hs!aT=M`7lK$o4dGW=!z8|cB60i>T7 zxIczh*1sGRo>u}&pAX!bIs9{-P1~3~FVjZrak;56S9P`|drGFcRwY%rh43xvY+Y?P z_9T+$;DRo$g={))eRepE*w6$88!j&i9>{;R#omZDxvOTVitz^?H2QD0SO#r_3iy=u zb{Qxkab0Jl3@ZW$vL$sXp4lU=o^0Y_P4FdvP?xcBJ7)(B-*t@^Hp(m4HGgu;B-?X1 z*!7XHNnXXMIcVu*hYjCzg+6FBv?}K{&$*?O?K&Lj8rx`WRZ(j`d&@gJWH{V4rqO7j z+{gUIE$eLG;ZWDOMw5k#J@d0$5!sQ$Q5*LI4W-KI%u{b^WIF?Y{KG(FsS1So_$~MB zpy9BMXfXPh+n7JQWs&VY9J28!(4@WMgZaU&JJ}J#ksA+=4a3T1%^izw8DzT;2W`Y2 z8-vf&eEF7t_MPE~jToT!Ex%{}>Xv=B|L~oSIAH9pxHA873z`kpCK0R7*VTLW#L(nv zH4^EC0JC|`6O-v0YGkq(2Y^h}QY@ODE+F^7pkA4;16wyTK|zNTOM`#*KZ;}@3|Dt@ zSC!)Y7b&VYW2Pczrqg1kq4v8-7L4na?{)U;&WtXfUL$|(+yM1LP5N2_qqwI%$WLH; zFPGM)uS+!|ooN7dzD{YS!YX}Z&KeVB^^R!BOKFwD>TzTDnjmC5X!pH(301VM;y0$O z(Lq-12#2)4RJN^tW^7U8jcfsJzpj@`OI3cx5;fMyx*hS5j+ZJ+)d$9RY9f$bJMtl~ z!xiPK7>o&PG>~OGLZ@xv%5v33#s)R+$R^MQ>m3EL1c2W@VV^Vh;A%02c=74$lvT`3d2YAhft3d=$7-as&)cfS2NKE` zmt!(gu`zq>5!P}K5>S5O61qrEpWNuPZF*Cd45z-P4iL84tYY6UTo6YyoYjJ}_!~89nph=#&kGy}!OBL5d z?MOw9V*ZAg{uf=!JT@acHiycGWT}grHQB8=oxSjgY>9$EeeZ*XOgaASBM+4p$M?5= zABr2(R;0}KdnCNK^hJo<6_n^9IXq!4T<=YN^Tllo9*-g2Jeyji-rM*ViQ8MVmGB`c zJd;}3-y8epy|A%UfbG?q zsCA!QJFNw)hmIl@0Qyb^?S`FnhRe&n20g_p$|>AkwUbh?h-kA@7N~|#>Fm0mG=@uc zb%NSrmBkd#uGz^`u$Opgrvj+pDev8olb7LgU9Uiku}Wf!c31D@Nw|odlK->Tch);~ zC$r(S@T4#la>zii?m)=zSZYa-^mE*oRVsg=QZo*AiVk)kFzjS3s{qV+Dq=VCF=2^2&_WX;T`z&L^sHMKY9Gm>Yx;8ZRVRfrA zgwb-9gQ+Mtb#@B3x>;F1xb@T#ly0e8Ql!;w%ZiLvJRC$tfhHw|L*1k-FT+7p0z$=? zVy^C8mIBXKKp}@ZVY+?%8@eed86$zNB3kkXbaU`F{={L*o0}@>U7QnzW^&zhCBStf zDOUYH&Z$Ckx$dJ96l_$uDQf*r&It`jkp4)3+pSW}`n{Y}8fGosYbAg)Bt@^^4OVN7 zQ0dR}9Bp=!7U|0C2CEKXkpipHyCOAtW21hXfjH|nNZm^~E@r5kOOfVw9YjoL-vFZ83#GXHaAXa|NXgAo9 zR7s7}`K1bKR%(s`$f3>1)#CV>8Zqf{TlOJAh2Kx8xz)H`2D4IsS7MYalX z*+yeGS%Mncdu_bHyjs9BnholnE$#i_mJpG-0@~4(O^%=@7}OR4U9A;0hwv0;F9Wkn zv#?cO9j|$3_RO%GYippTRGp2vA5U8L+_3vbD~=^bowB(jxWNF)?MB|JZOL9|48C~D zX0zPx*{$4`fn-=gc+ol8@uIL z9R&~&XV1L%a)!&OfM+K+)~fAytPz8Ee3h@8gzs-y<9|_&6pc0`P4{@}m3bNgykBQ$ z?q8EOGY6#Ht(cZ*bxN}i-tl5c8aMn_bxYPdqgfB{$8jgwn{=6^`rsyVr}n&)XXfrI1-3_X0N=E=uhoLiuG{3kSx%x0=2#V(dkO z1xvkcFTWRMQ_>>)US=p)N3(4|zc;*TWRZL?5$szUZTp^iSv1uwitXix@^!qh9eCzF z*R;EczxN2NUmBKr{k$ZaNcT2FaXTuOhW&h78(?`3OJx5&De0ZtLka{c72U6#SsQOC4v@p-LN*0nR)mKQOuT`04@}6rgD`Z$|qYy`5 z4V+&Ws#i@zo=VLW(&+0|=9ny2uOUOg4`?B#zDi}r^t|V)Xh^;kwti4$?DUl9Msw(U zNtlll8I*I=f#BeG-0wzQ&|Qt&s#t%+{r-#gi=mr4qFcd6yh}hZ=o?hRrx&-^PC_oE zt_xAd6~Tddo_ZB8mDsKnYXSR`b5&C&~rT4{gKFY z-o|L?resiI*!Vqn_6JSxtk!u$b0l>NUB?66W1FU|HvT{6-ZCJ{eQn=n=#&oW?rubs z?hZ+j6bX@3KxAkb1|$X;QW{BNXbA&`25FE|hfqmDK`_wwx779QwVvhLd+lfM{on6b zKPfWzT=#XI=W*0rpOyE_4}22h7S%X0zvguvaqM&(noh%`HfTbePF1E_X~L`%v#%#mPBK23e7 z+GWBvO@F9%GJz#cL8cltK_N{;rq(k-C{2~5S~9_q9^Y5&d*$q3D@vaJyG==G?zF*6 z-Wz6Z0?X#$cm0XMYI)$){f%W(z$HNB&X2D2b^HdO`O{FOlececwC$wGdgk@vHrHi8 z@8H%*+X-MKx?bI8vFrhun6b7~f0ls6_Krl_>O$BZlI~~0JEHa4)Ac$Mhua^m+dZST zn~1r$Hgs>L>fX!EdzhSi>kIc*Ip+eE=5FA~1#ta58~Mj*By88}MxNp`MBSa|Q2JM8 z`;#FXod@>#;LtHRyzRQb()Ak<=1=2;5_N5s-9j4~Z*_0Fb~e~6)O7;w;A@Mwf;TNX ztL$a!+AVvkH~uUNDE8QMw|UPdq_%U`o(YK^51~EVdv>Hpshdk_s6?sHS?Pr@>l>mw zuT_lRx)|-78@&q{+tVL=>ovBY@+#!ctD6z@A*pjcTN{UOLvG&c{FWdj`6WT9eyv9C z3DX-yWZ`pY^1G>H_mhCIkn>Rm6Ju*`?;DTz|53{BVb-N3F%0rR??c&%WSI5Cyi3o3 z73o0eL&k|@c0R=~kHiNYe;3nv^1Cw?7H z^b`x%?h%11JHB`dwt_LX%s94^IJQ(hwxSOQNilLTSEWECG1q#f;|9TPAR z?M0|6x~9fKqsE|H#4K3E=uyPVSajN^h~*3i@$xvyi8x`8IPuMBr5=07H}ytO<+kbH zlM_Va^n9Zk%1blTog&wY=ISxQy!O%0Vz^D#AG3=1SoQ+yYm&o%HCMV~HEAa&4kXqL+|DD^@LtmhqfvLH(fDbg>W_IUT~KIRE#9AqzTzY;ki%>M zD@Q!ND+4_!I0@M-W?pE(V%}j;e5&C33xlvFa3%d4jIiZuhxhZ{f{71(S)L$@|EAe=*1>_a*lx_Zy*&`iy#w`p3{? zePg|2{bFdbKCxc0{yKDBUtMoq|26crzH2}U)&B%m%@3%ayI85s+@&SlM$)*H2? z7c_+s8S_I#HJ7!ut*M=r_3UWwS#aNJ zxNdv1rfc?`=fl89XRfd`oU+xbDFLU}hvjZt%9fQPTm;*bfdLgxjQShb^ov%6c-!pjcu>hw9YDSbsati zFTxGyY%kT+&B|?c9QJ&4KWPZFh1NWrJx|i;n_I7w+Yn)}m^iwSH@X-pvXCaSm_T-Q zX4Y$2*2^XL`Uv0-Hc`*Rb`S(P=aJ-*5MD$c)0*55Wyelz7Eh8?V+UA@mdt~ItU zDsJb$7roiq)&I=%P2TOH~I9p-Vqv0zbb8~I-7Q}an5cq%tO zUu4@(2B*8mlin{L(8h<*#q-+*@5Mf~p7ek5{Msl%aDy-|fS*9VsYkx1N1>BTzL-m) zp+tV%gk^MsrAOuV209PdP?awg@ll zzczkC|F84sXg-~x9>4}H00n*Eg2GPzDS&H2OwDhs1O&D#Ea(4jw)E}y^Xm7t>-P`k z^HII~Q0md?*Q9>i}cY!`bB)3H1dW!B8A%1ggO#PF3!x@ zEz8)g<#>-6yxz7JbQ2E;(l|5Ez!gfiU_nY+&N>-h1h17|4$M3Cwg(JY&%{Np;f zmQuksuSv0wW$U(oe+TsvY?u2+gJN&~CYYZ)q?WRGg>SSd_UCUs@qg7pvJ|t+c>~pf zzPKsskL(ay%GedW(cIB@aqFJ{TnE!q(k}0f#*W_QO;8Z&P+ZF26}zV(X4)UcqKsG>qLf_gv=U>nvwuA)R<>pU)x3(VmKkr~$O5WuI?PB zJI*f^?1~+=e(FEj`r;3jAU`5ZKHw)0YU&TF=@0JY3o7OdZnzsXZl?4LAzbawMsFuB z*xRHcpX#mf9Hs>icF}-5Nd|I3Z?)`jGI*nl)Lv5zft(*+aXoAZp6_C|Hxxr67l&8Z z58noV=)wj|o)l24TA@8m2=48oveyAF^#!5Tj6<+K?IL!u*QiSag#Hza!>Zt!E=GHU zx=hQ(>eZRU&EUf>e6SKqoqOuJB6FA%JlaJEk}DZ=3!baZhYP_wU8n4|>JW4DTPuNw z?ZL}ktoBBA$hpO>)x*P&!M@*yExWOIp<;Wg2oX+tgeIpf@_Ia^1WNvc*d1Z?DRJy$ z-q@$e@5FBMTQhT^%W|P>xnUzfJb6I<0d~Uu*Gb?%D?R*d9CQ8a^VmPtU|>5EdH$Z| z`JvMDBb@UGNQ@DL0sDL(QV9*26-Hr^$A^m7o`IT=nT5^EATN1`6LIfVW@qV>N|)V53ViiFl8*EBc2?(VC=iBal+b??XPw_8>Nr<#`&4P0@;&Lzn6934 zj?e2aF_71epUNL6>l!DI5-0r>htnN_DfID?=VoR+MG_as)t!o|@ClITWoEif(jQ0N z4a1cB_{sC+F&fH|+>Mj!&c@XET$ks|V|qgJDvqQ(22uRQUw9 z@!Bx~TW%avcM_)D#~(rGl|+O35(F&<==n|Zk6%B%-8m zM{k~gZH9hjNCbqheq4Ptw|*ZDsgq#24o^C2jg_phVkQot10iOm!=b{rt+SF8vf#m4$#_=rhyC~t zI4G5;4qk-do*zrruj#;rG6C&!Qdv5txTJnw2WphgQx{O}pv{dn=m!}ll=Z*FI)?u1 zu;YJ)bzFdr@kJW3Lg;AJ?0RMF`pK9vysSOcFNpTlgPhJH3}uC&-e<}0(2vEv>L+=D zx33!H*hvV`L$UPzdM|kPO@T~_)SgZ(4%sjCf_7gw$Q1zIY3pJw`#oRC?Arug?L1{q zUl%vm&-8+9Uo*(LlL&B%V&(d6Ux@5q2=eSCdq#IHuC-tB1t*src&yUM(uF4hA%}(J+CBX?vP0?28=1rr}fw`#rapI+>X={}7PExqT z0@On|aZ*zU)LnQRDE<$bcPKVssH>avR#8~MJk$e=@c}noW14r9A`lj$eybQVk#a>{ znm6Z{(7U>SF&?O7Xf*LIP+WywSHD?|Gm-jQJ%G2J;v&pj{l@R2?do~;gtSKW%kje+}1JADs%gUm>ubb*JogiTf}3M5ZEN6g4d0LJK5R+fo1-;tabRcdlpo)H_!86r z%tdLR22R!>;8Y%eR-ct4Ijw(Sb&cpPH-Ix4+~F8Ydo}QG4eu>yFsxJGo}(^pZeR<1 zd+`GNk^xYwrX3A@TEltE@gY7`AGlQ0#s*%kk-X*kkPvDBo~rqebojaSAj@Qfdp(C0|^>EUm7E#bZwn1vdf{~a^tv#u}AQ=Dt<9K*g((Q9fksTIpvVT!js_NUc)0D6cZMsPq7PU3?2fmBvgtka$*x znBS^`%%pgz%arFB8&qC4_pid6N%PQXE?+RdT6x|4W);p%DuDfzw;Nxq^ftdyMKFT^ z2%d7lQ>hFy&uie%_2Wmr>sJIi0-r6&}ljkq2{Co!=1wGrX3Z_)2tq1 z&B+T!EQO~`<0^WlDL4586VDjt7aE%0t(clVy(t`c`;5`WLQzv>#qc!kreI)FyWw)7 zw`ps|;xx;qXkc=?ky0V6X>vvXH1(zc@Q50A722EDRm@E@ZHfSKsL`#$bEX9qW7G7T zLWfBoMGQ|0!%QDmJfCLU6gy1r#yUl)j7^NCjDuevj&qhGoRFIx2T!>Wo9y)?EW)f- z2VzJDL^!d}J(AZw0bklcN?Olf+K8k{Tdzr9NV?R*rX+Bokn-xJ#2H9_ilI7RIRcTR zB#5{ZOv~d0I_bDTSge68A0Btz@CyiT$G(_)Pu;S-dfZ~e1H7EzUqm2CLNrD4TCU_? zzk+;!rQp=-lwBk8LW0Skg|$P3m2R^dArlXJ$+pqIT7Jq5S=m4B#wbpft*P%^{w)SA?Pen` zl6^tnvz%-y)4jjR?Tk1}wxPaTImuMUMqhy2@6yib({R-3IN#IB``O_{*}v zBQc3}#*9_Ij1@Y^E79N)uM0H_Hjg;3B7{)CMZ?>|V4ElMIMYQBRW>i_W!=JTd#Co% z1s5Sy#=M}HMGK>Ce(mE~7X<2-hA5Mp$n0g7Sy!c5Hyl})PmMTFBYv9cWf{Kh+~0^4 zcY%xxOMK6Lp7N3I_YwY{`{eVLyz7(lJ@=`WH`v>TYo`kOK3RIm9AO9(6q!BT?YiFJ zJAa?q&QO#dnLFIGzW&zt!+mT!m3tJHSwh{k>j}QS_o?i301_}qs3&6`WJvB40|qt~ zK(BOLtXKKY+-C$2Ksw9Z>YkbPP2a=&_;zadD1oA{TV_4Scl17;o!&j#xg5`)=Jf^N zo%^TkwC+*QWp8x{uD7%JF5hRhGrC7Vm%G(-xc<@C_sNRa6O7%H)lktFs>rJ)5GFj- z()Y;exbUe}-#agK?yNwJ-jR;IWgdHnE%ug5>>V+AaK>CvzFZJGH#E`c5#Jj1KI{Wx zO2%8=e);uEWWG5_UCu%4pG&aUF7{^mnjub#gFcxe(on--z1&Ww64_aHfOAh{2CM9q zcCwMk&$0)cV;U(~cCYY1N~of;tn2^DWpx|a-gJ6rN`qR|vQ#*jgr?G|A z_R2chg2tOY_+-$4M0>BOlO-zm>BZkgP#Ne&Cg@luXq6`DaU^Iy!Ew+LE9}1Y^6Jcj zr>NuLT=j|(7J02ap(oU@;3Vi6 zfYwK~soM#PQX}C)=!_LX-{v+=J0b8p4QE0p0n&bBo5qr$H+3sq5uLvx>f73;1z5S% zM{ojk6aeqn2dSSGWTj4q^Pv$d0{cxtnrDUXP>;ds(Qv@wZwS)p6tt(VgUg|FSD+&M zEkRnHLeHq*z{$~ZfXk0MRu2^fX=}I`8o460-+ZhYD)fN*Ih+lh3^4tT#~Pmn!>Av^ z&!Y=g#P(Z{wLS}dp_U**62j8?iL{&0+BImMPHydDZk>iwZDUg&gGnAW)iibi#AjL8oQ(b|KbQ=V#J~Q%uHN@A%DNvVzd2dW_gU1|1u*GgL_ z6*syLR6gy+2FunZ{jBfj+1Z0~U%Jp> z#=4-N#r+(+i(s%zM-V1zh%zI8%klj4+#{<{GNOMN8RrKXzaW8UNf=|!kH_EC&0o_k z(8^j}jC9Km>y* zae5&sEn(5OC^6@iZU%iU>TY#$34P?`HHaegn@{{ds}mPLC2~cLpnW{*9H|50ryht8 zJ2Ngp4~GvwGkzRG(cJLA9b<<65@Y@$wWqCU8mg%Io<;m7wKuk~_$7;|QB~PDjq9N* zlbT}%d;}JV%E1{L4_(kjv@s|tx0tHjm?8DhltJWJ13iHShyl$odl-UhqU~r2(9TwV zn8Ef?X-=`R0=5DRcxCSlm4^-}D%yyYWLgX(+>&X{Ku^)Owq(`<;1p-@J=B_0K@zT5 z)*`oZbcW7D57ZWIu9mb|ELQH!obu3WMl4tZm4QV^h2suuMxI;;OZ3x?kaHlsC~}H!PIz>d`PXP7qA!&l`Y#L1uQs-5 zELL5eYVlNT%3lz@+S;PESiLj#-jkpyYJmf|a@7^9Sf*}!@--nA1a_LPYbsXfPd)LZ zZ-OuI>@+~HYjjjyoVw>J*Oa>;veR;1tD|~(YS)vzDGub#QSa4nRw+&0@f2%9E(q;3 zzt_B3-8J>hldUOvfp4ery~d}iTT>4_&o>n;i0!n#*ZNd_G9}J3N0dJ;!1B83$?KXY z8=a!Bi$ymY?!7j)3^bSvRMTp&>_5N-vDMT&Rz}5qhzDs(Jn)U)Ln@%LNVm5E*@PM3 z$+mg8IUkIWDGGtm?1P7e18kk{7pO&0kyV9(uF`0p)689QR3QBlHn>$ ziH{lc9y3L9Fs5-ZB|yk##z~jONnNruN3icl;B;k@w5APL ze(JLU2N!9eph*(`zl0c}e?7%Gv2?|;NgAM3lVmk*OY$e4y>{5+nN9o$Dpg3?8IXu}oZvseSy;O%i`exae*h7x9ezM&X|v&9YfmERE2E6y{9;}@%35oe5yEq1&j&lsUuY;r}K zG3uWP=={BWP85{zM~E>(CpU30=X;1TO&~Xc$lyBz(k18e2ryYlQ)k0Qxi6c7qUe&o zW4W|hcIBw_WgDsd>}Q}`Dh>X4RQU1*&>LMIaeOHaTAe>pl7klMlCWc@v|e`EsN`kq z#ysn1HHel>Dzkr!ru?*=$doM0l#J4p9FCMUQv5!IQRgoODgR=QG6{-eVC8ahNlb`f zU~zWxN`&uAaPan(W-*&NiI?{^WwA;*@n7z9LgAvKQR*l(iW^mmazU-3Xi(`WbJR3S z8r6gfKpmh+QHdyh)Cfu#RfFlWHnZHDI za2Cfe>tYQn(&3o(zPAp+s8Fl08mtQx@P1;)7!;?Ki7*7Q>dkxE#_DR#Dc}T;o9SnC zAVXVlPL4cRQ6XIyjt83z`WAxO@W0Bv_{V1I~%63X7{S`9f6 zamW4HR$7?yhlvmAX$je}S=}26q62d;+1Tw@kNj5{Zm;4RuF#zFVRZAMd*#DQ??Z|7 zIc@1fJLkh9=R@7+hd8v_piq6bq7&) z^4|$ZwD|B|7U`4)7GF5XC#{n0#RhS8N&uW$f?QATs>ohukWi-#Ae6yddlpw&_HGC9 zbxHvdZ^Dh9j#Z_-J3(Tda=@(%KkE6kinAAS%pNL!4wo%~yeDpzYcKVfKUDfo{108- zFB{w)sIU5EPLkA&0ZkV^KMDn+KiTI->t2aAWQf+WiZ+nP<+j77WW`WJD5)qZx%?>a z!_KPPbF!rt@GH}BufSB*9g7J~(zV$WD0^X4>N>?VAl0G<+?kZ?Fk*F$Vk(pD3RRFz zp`3v+svEc!(*e1q8i4Uo9>Va|)ru)6vaYB~^W;#D!syiXifI8iN3DrxfpQ0SN?og% zdLsL^Y5-3=yN|*d z=8FzOQrd!lth}UDb_{58b2TCq2J*NySGZNuxHYiTcu%MCD5UY>nR4@(^3r}^*iLeb zO!Bf&rHR6{zwce+6I1UGEV-~sie)E{7}2qlr0u(KG>BVA3+wWx^bV}J5H)aHCkPv4 zatx=f54?53Yv8no)#w8dYTC@erVCjEFVq@dV=&9Hm3BDr(FLc0V?N$XUzQ^`ZFJzJ z3rPdde1exj3&&#G&cJ&Yf(EX6*tR|h+NLcJ?7EOQ@Xf=w4M3tH?PTE37Qnn|&I715 zdB3v?5qa8%oO}qgfl82(Kcf(prvsD#2!ABdxXeJAGqQ(NUCTh}rjps*khsJrp-?-a z7)!&9wAYlm7qr++sd&tY%S;)|%0HfD;RzsB)3rXBE613t_g2`Mdkd0hAcwTp z?-Cp1RR)3OdzPRoZ5b7yIQ?;X`iI>q^xv~Qg?eDgq%f+JmB$*ZRXR@lc;L#UF=`~0 zQ&mb#TX`_c+{sZkDCIRlR0>R+0}NDtj>>51Ws}BA1>~4L8g{0EQ-0xjSUB?RSVa6-SY!;3uuttNV{xX0lhS-&bMn@QlQCmOkWlP} zlVejv5~7twQ<&Bu!7Arc*l4iuRL=6_5=U_s@@r%DD*GB^r{k6>yBp)DmqInVP80s zBLg(J0d#a?aDyg5_Y7@*4oDQ2O-*cUkOpX;p=Qx?D+d|g4X=RfX9)5UeOK~$ucVb< zQJ#`kYn0Y{*Q5+@QXg(ot!~oXYEsE<(pYR#Q(Gvb+?hDXGS0y=A#sL2Klh2+tFm_! zA2zT9RLP?g(9tJaA@|E%3cmDW;_Qr(aRy+D@bJG;s^N);#aH$($0+nJX0fwH~y=0w-VxqwUUY|90# z%H*X46Hyx+2gV@`N_ng*$)#EoB^&C9qR&sEe2!_7gIQ|;499~=&>1atoRr+iI?%@l zSRNS6SyIY6t43dzR}m34IOst7go1R?nAG7)tfN(|bC9Nkx~7whrlYy0b3mViexH+9 zpCkLKWc)ru0Z~>8EQ$5(joSk}%#)();Bdb^++bmJwNxFlsWEmpm zCd;MftOb3Ra#6G7$}wpNM#-eCK7HzPfvH6ICru7ElC@d0`b^~_Q@7p60vygr=4U+t z?qs2U#B4k40VS0m-bBmN>R{n_SWx2+<`QdHn_fiB|QNXk)4%Ev-%+9x^__7cq0${?3RT$25ej-Jm z1s~ygDGBb@Lw3&hq~x-4M?_xA`p4ZLUUJ@*BF~B&;Q|{RDfKSq!%(R61E|#btb!4-mvZmoJ`8_$POI0Mck%L*#mh}I=q
  • YR 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 From 1ed2c1a1ad39788cb54d4074ae481dea4cb89b1b Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Fri, 25 Oct 2024 18:17:05 +0100 Subject: [PATCH 29/62] Fix defmt logs for LFNs. --- src/fat/volume.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/fat/volume.rs b/src/fat/volume.rs index 503a7e80..0e819211 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -567,7 +567,13 @@ impl FatVolume { csum: u8, buffer: [u16; 13], ) -> Self { + #[cfg(feature = "log")] debug!("LFN Contents {start} {sequence} {csum:02x} {buffer:04x?}"); + #[cfg(feature = "defmt-log")] + debug!( + "LFN Contents {=u8} {=u8} {=u8:02x} {=[?; 13]:#04x}", + start, sequence, csum, buffer + ); match (start, sequence, self) { (true, 0x01, _) => { lfn_buffer.clear(); From c8cc20fc9c5a21ae064e87c67a2398ab08ab49d4 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sun, 27 Oct 2024 12:23:30 +0000 Subject: [PATCH 30/62] LfnBuffer now borrows its contents. Removes a bunch of const generics from the API. --- examples/shell.rs | 3 ++- src/fat/volume.rs | 8 ++++---- src/filesystem/directory.rs | 4 ++-- src/filesystem/filename.rs | 31 +++++++++++++------------------ src/volume_mgr.rs | 4 ++-- tests/directories.rs | 6 ++++-- 6 files changed, 27 insertions(+), 29 deletions(-) diff --git a/examples/shell.rs b/examples/shell.rs index d4bdd165..5c6b0e50 100644 --- a/examples/shell.rs +++ b/examples/shell.rs @@ -231,7 +231,8 @@ impl Context { fn dir(&self, path: &Path) -> Result<(), Error> { println!("Directory listing of {:?}", path); let dir = self.resolve_existing_directory(path)?; - let mut lfn_buffer = LfnBuffer::<256>::new(); + let mut storage = [0u8; 128]; + let mut lfn_buffer = LfnBuffer::new(&mut storage); dir.iterate_dir_lfn(&mut lfn_buffer, |entry, lfn| { if !entry.attributes.is_volume() { print!( diff --git a/src/fat/volume.rs b/src/fat/volume.rs index 0e819211..5d8307d1 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -540,10 +540,10 @@ impl FatVolume { /// including the Long File Name. /// /// Useful for performing directory listings. - pub(crate) fn iterate_dir_lfn( + pub(crate) fn iterate_dir_lfn( &self, block_cache: &mut BlockCache, - lfn_buffer: &mut LfnBuffer, + lfn_buffer: &mut LfnBuffer<'_>, dir_info: &DirectoryInfo, mut func: F, ) -> Result<(), Error> @@ -559,9 +559,9 @@ impl FatVolume { } impl SeqState { - fn update( + fn update( self, - lfn_buffer: &mut LfnBuffer, + lfn_buffer: &mut LfnBuffer<'_>, start: bool, sequence: u8, csum: u8, diff --git a/src/filesystem/directory.rs b/src/filesystem/directory.rs index c0493529..527807bc 100644 --- a/src/filesystem/directory.rs +++ b/src/filesystem/directory.rs @@ -176,9 +176,9 @@ where /// object is already locked in order to do the iteration. /// ///
  • - pub fn iterate_dir_lfn( + pub fn iterate_dir_lfn( &self, - lfn_buffer: &mut LfnBuffer, + lfn_buffer: &mut LfnBuffer<'_>, func: F, ) -> Result<(), Error> where diff --git a/src/filesystem/filename.rs b/src/filesystem/filename.rs index e3f4f6e4..2f197c86 100644 --- a/src/filesystem/filename.rs +++ b/src/filesystem/filename.rs @@ -221,11 +221,9 @@ impl core::fmt::Debug for ShortFileName { } /// Used to store a Long File Name -/// -/// The const generic specifies the maximum capacity in bytes. -pub struct LfnBuffer { +pub struct LfnBuffer<'a> { /// We fill this buffer in from the back - inner: [u8; N], + inner: &'a mut [u8], /// How many bytes are free. /// /// This is also the byte index the string starts from. @@ -234,19 +232,20 @@ pub struct LfnBuffer { overflow: bool, } -impl LfnBuffer { - /// Create a new, empty, LFN Buffer - pub fn new() -> LfnBuffer { +impl<'a> LfnBuffer<'a> { + /// Create a new, empty, LFN Buffer using the given mutable slice as its storage. + pub fn new(storage: &'a mut [u8]) -> LfnBuffer<'a> { + let len = storage.len(); LfnBuffer { - inner: [0u8; N], - free: N, + inner: storage, + free: len, overflow: false, } } /// Empty out this buffer pub fn clear(&mut self) { - self.free = N; + self.free = self.inner.len(); self.overflow = false; } @@ -291,12 +290,6 @@ impl LfnBuffer { } } -impl core::default::Default for LfnBuffer { - fn default() -> Self { - LfnBuffer::new() - } -} - // **************************************************************************** // // Unit Tests @@ -402,7 +395,8 @@ mod test { #[test] fn one_piece() { - let mut buf: LfnBuffer<64> = LfnBuffer::new(); + let mut storage = [0u8; 64]; + let mut buf: LfnBuffer = LfnBuffer::new(&mut storage); buf.push(&[ 0x0030, 0x0031, 0x0032, 0x0033, 0x2202, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, @@ -412,7 +406,8 @@ mod test { #[test] fn two_piece() { - let mut buf: LfnBuffer<64> = LfnBuffer::new(); + let mut storage = [0u8; 64]; + let mut buf: LfnBuffer = LfnBuffer::new(&mut storage); buf.push(&[ 0x0030, 0x0031, 0x0032, 0x0033, 0x2202, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index dab2f024..f3594602 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -435,10 +435,10 @@ where /// object is already locked in order to do the iteration. /// /// - pub fn iterate_dir_lfn( + pub fn iterate_dir_lfn( &self, directory: RawDirectory, - lfn_buffer: &mut LfnBuffer, + lfn_buffer: &mut LfnBuffer<'_>, func: F, ) -> Result<(), Error> where diff --git a/tests/directories.rs b/tests/directories.rs index 62aa72cd..e2e20d29 100644 --- a/tests/directories.rs +++ b/tests/directories.rs @@ -101,7 +101,8 @@ fn fat16_root_directory_listing() { ]; let mut listing = Vec::new(); - let mut lfn_buffer: LfnBuffer<128> = LfnBuffer::new(); + let mut storage = [0u8; 128]; + let mut lfn_buffer: LfnBuffer = LfnBuffer::new(&mut storage); volume_mgr .iterate_dir_lfn(root_dir, &mut lfn_buffer, |d, opt_lfn| { @@ -288,7 +289,8 @@ fn fat32_root_directory_listing() { ]; let mut listing = Vec::new(); - let mut lfn_buffer: LfnBuffer<128> = LfnBuffer::new(); + let mut storage = [0u8; 128]; + let mut lfn_buffer: LfnBuffer = LfnBuffer::new(&mut storage); volume_mgr .iterate_dir_lfn(root_dir, &mut lfn_buffer, |d, opt_lfn| { From b77b9caf50daf01c62289c734818d48b95e6b461 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sun, 27 Oct 2024 13:43:33 +0000 Subject: [PATCH 31/62] Handle surrogate-pairs in LFNs. --- src/filesystem/filename.rs | 97 ++++++++++++++++++++++++++++++++++---- 1 file changed, 88 insertions(+), 9 deletions(-) diff --git a/src/filesystem/filename.rs b/src/filesystem/filename.rs index 2f197c86..31d28548 100644 --- a/src/filesystem/filename.rs +++ b/src/filesystem/filename.rs @@ -221,6 +221,7 @@ impl core::fmt::Debug for ShortFileName { } /// Used to store a Long File Name +#[derive(Debug)] pub struct LfnBuffer<'a> { /// We fill this buffer in from the back inner: &'a mut [u8], @@ -230,6 +231,8 @@ pub struct LfnBuffer<'a> { free: usize, /// Did we overflow? overflow: bool, + /// If a surrogate-pair is split over two directory entries, remember half of it here. + unpaired_surrogate: Option, } impl<'a> LfnBuffer<'a> { @@ -240,6 +243,7 @@ impl<'a> LfnBuffer<'a> { inner: storage, free: len, overflow: false, + unpaired_surrogate: None, } } @@ -247,12 +251,26 @@ impl<'a> LfnBuffer<'a> { pub fn clear(&mut self) { self.free = self.inner.len(); self.overflow = false; + self.unpaired_surrogate = None; } - /// Push the 13 UCS-2 characters into this string + /// Push the 13 UTF-16 codepoints into this string. /// /// We assume they are pushed last-chunk-first, as you would find /// them on disk. + /// + /// Any chunk starting with a half of a surrogate pair has that saved for the next call. + /// + /// ```text + /// [de00, 002e, 0074, 0078, 0074, 0000, ffff, ffff, ffff, ffff, ffff, ffff, ffff] + /// [0041, 0042, 0030, 0031, 0032, 0033, 0034, 0035, 0036, 0037, 0038, 0039, d83d] + /// + /// Would map to + /// + /// 0041 0042 0030 0031 0032 0033 0034 0035 0036 0037 0038 0039 1f600 002e 0074 0078 0074, or + /// + /// "AB0123456789😀.txt" + /// ``` pub fn push(&mut self, buffer: &[u16; 13]) { // find the first null, if any let null_idx = buffer @@ -261,18 +279,60 @@ impl<'a> LfnBuffer<'a> { .unwrap_or(buffer.len()); // take all the wide chars, up to the null (or go to the end) let buffer = &buffer[0..null_idx]; - for ch in buffer.iter().rev() { - let ch = char::from_u32(*ch as u32).unwrap_or('?'); + + // This next part will convert the 16-bit values into chars, noting that + // chars outside the Basic Multilingual Plane will require two 16-bit + // values to encode (see UTF-16 Surrogate Pairs). + // + // We cache the decoded chars into this array so we can iterate them + // backwards. It's 60 bytes, but it'll have to do. + let mut char_vec: heapless::Vec = heapless::Vec::new(); + // Now do the decode, including the unpaired surrogate (if any) from + // last time (maybe it has a pair now!) + let mut is_first = true; + for ch in char::decode_utf16( + buffer + .iter() + .cloned() + .chain(self.unpaired_surrogate.take().iter().cloned()), + ) { + match ch { + Ok(ch) => { + char_vec.push(ch).expect("Vec was full!?"); + } + Err(e) => { + // OK, so we found half a surrogate pair and nothing to go + // with it. Was this the first codepoint in the chunk? + if is_first { + // it was - the other half is probably in the next chunk + // so save this for next time + trace!("LFN saved {:?}", e.unpaired_surrogate()); + self.unpaired_surrogate = Some(e.unpaired_surrogate()); + } else { + // it wasn't - can't deal with it these mid-sequence, so + // replace it + trace!("LFN replaced {:?}", e.unpaired_surrogate()); + char_vec.push('\u{fffd}').expect("Vec was full?!"); + } + } + } + is_first = false; + } + + for ch in char_vec.iter().rev() { trace!("LFN push {:?}", ch); - let mut ch_bytes = [0u8; 4]; - // a buffer of length 4 is always enough - let ch_str = ch.encode_utf8(&mut ch_bytes); - if self.free < ch_str.len() { + // a buffer of length 4 is enough to encode any char + let mut encoded_ch = [0u8; 4]; + let encoded_ch = ch.encode_utf8(&mut encoded_ch); + if self.free < encoded_ch.len() { + // the LFN buffer they gave us was not long enough. Note for + // later, so we don't show them garbage. self.overflow = true; return; } - // store the encoded character in the buffer, working backwards - for b in ch_str.bytes().rev() { + // Store the encoded char in the buffer, working backwards. We + // already checked there was enough space. + for b in encoded_ch.bytes().rev() { self.free -= 1; self.inner[self.free] = b; } @@ -280,6 +340,9 @@ impl<'a> LfnBuffer<'a> { } /// View this LFN buffer as a string-slice + /// + /// If the buffer overflowed while parsing the LFN, or if this buffer is + /// empty, you get an empty string. pub fn as_str(&self) -> &str { if self.overflow { "" @@ -418,6 +481,22 @@ mod test { ]); assert_eq!(buf.as_str(), "ABCDEFGHIJKLM0123∂"); } + + #[test] + fn two_piece_split_surrogate() { + let mut storage = [0u8; 64]; + let mut buf: LfnBuffer = LfnBuffer::new(&mut storage); + + buf.push(&[ + 0xde00, 0x002e, 0x0074, 0x0078, 0x0074, 0x0000, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, + 0xffff, 0xffff, + ]); + buf.push(&[ + 0xd83d, 0xde00, 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, + 0x0039, 0xd83d, + ]); + assert_eq!(buf.as_str(), "😀0123456789😀.txt"); + } } // **************************************************************************** From 837ae27ac259f1c4c0f3a99c6ef0c050061689af Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Tue, 29 Oct 2024 11:47:03 +0000 Subject: [PATCH 32/62] Add missing items to CHANGELOG. --- CHANGELOG.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3361d1a4..3a5e92eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,15 +8,21 @@ The format is based on [Keep a Changelog] and this project adheres to [Semantic ### Changed -- None +- __Breaking Change__: `VolumeManager` now uses interior-mutability (with a `RefCell`) and so most methods are now `&self`. This also makes it easier to open multiple `File`, `Directory` or `Volume` objects at once. +- __Breaking Change__: The `VolumeManager`, `File`, `Directory` and `Volume` no longer implement `Send` or `Sync. +- `VolumeManager` uses an interior block cache of 512 bytes, increasing its size by about 520 bytes but hugely reducing stack space required at run-time. +- __Breaking Change__: The `VolumeManager::device` method now takes a callback rather than giving you a reference to the underlying `BlockDevice` +- __Breaking Change__: `Error:LockError` variant added. +- __Breaking Change__: `SearchId` was renamed to `Handle` ### Added - `File` now implements the `embedded-io` `Read`, `Write` and `Seek` traits. +- New `iterate_dir_lfn` method on `VolumeManager` and `Directory` - provides decoded Long File Names as `Option<&str>` ### Removed -- None +- __Breaking Change__: Removed the `reason: &str` argument from `BlockDevice` ## [Version 0.8.0] - 2024-07-12 From 2c836db226c06cb6cc799942d0547a8816a2c143 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Tue, 29 Oct 2024 12:10:01 +0000 Subject: [PATCH 33/62] Remove the last stack allocated block. Must have missed this before. --- src/fat/volume.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/fat/volume.rs b/src/fat/volume.rs index fbbedf9e..acf584b3 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -1029,15 +1029,12 @@ impl FatVolume { *number_free_cluster -= 1; }; if zero { - let blocks = [Block::new()]; let start_block_idx = self.cluster_to_block(new_cluster); let num_blocks = BlockCount(u32::from(self.blocks_per_cluster)); for block_idx in start_block_idx.range(num_blocks) { - trace!("Zeroing cluster"); - block_cache - .block_device() - .write(&blocks, block_idx) - .map_err(Error::DeviceError)?; + trace!("Zeroing cluster {:?}", block_idx); + let _block = block_cache.blank_mut(block_idx); + block_cache.write_back()?; } } debug!("All done, returning {:?}", new_cluster); From 83831c0ac57cba042021a62374733380726a2326 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Tue, 29 Oct 2024 12:11:36 +0000 Subject: [PATCH 34/62] Use libcore to memset a slice. The API exists, we might as well use it. --- src/blockdevice.rs | 4 +--- src/sdcard/mod.rs | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/blockdevice.rs b/src/blockdevice.rs index 618eda23..fcc1d43c 100644 --- a/src/blockdevice.rs +++ b/src/blockdevice.rs @@ -140,9 +140,7 @@ where /// Access a blank sector pub fn blank_mut(&mut self, block_idx: BlockIdx) -> &mut Block { self.block_idx = Some(block_idx); - for b in self.block[0].iter_mut() { - *b = 0; - } + self.block[0].fill(0); &mut self.block[0] } diff --git a/src/sdcard/mod.rs b/src/sdcard/mod.rs index 5c3c0381..553791f9 100644 --- a/src/sdcard/mod.rs +++ b/src/sdcard/mod.rs @@ -341,9 +341,7 @@ where return Err(Error::ReadError); } - for b in buffer.iter_mut() { - *b = 0xFF; - } + buffer.fill(0xFF); self.transfer_bytes(buffer)?; // These two bytes are always sent. They are either a valid CRC, or From dfa1c3e17f9129da7dc775fec167f6f10eb4573c Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 5 Oct 2024 15:10:43 +0100 Subject: [PATCH 35/62] Create a VolumeManager type alias Makes the example code a bit simple. --- examples/append_file.rs | 7 ++++--- examples/big_dir.rs | 7 ++++--- examples/create_file.rs | 7 ++++--- examples/delete_file.rs | 7 ++++--- examples/list_dir.rs | 20 +++++++++----------- examples/read_file.rs | 7 ++++--- 6 files changed, 29 insertions(+), 26 deletions(-) diff --git a/examples/append_file.rs b/examples/append_file.rs index bbda0098..2c7dd8e2 100644 --- a/examples/append_file.rs +++ b/examples/append_file.rs @@ -22,7 +22,9 @@ use linux::*; const FILE_TO_APPEND: &str = "README.TXT"; -use embedded_sdmmc::{Error, Mode, VolumeIdx, VolumeManager}; +use embedded_sdmmc::{Error, Mode, VolumeIdx}; + +type VolumeManager = embedded_sdmmc::VolumeManager; fn main() -> Result<(), embedded_sdmmc::Error> { env_logger::init(); @@ -30,8 +32,7 @@ fn main() -> Result<(), embedded_sdmmc::Error> { let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; - let volume_mgr: VolumeManager = - VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); let volume = volume_mgr.open_volume(VolumeIdx(0))?; let root_dir = volume.open_root_dir()?; println!("\nCreating file {}...", FILE_TO_APPEND); diff --git a/examples/big_dir.rs b/examples/big_dir.rs index 98ad90a3..a0170267 100644 --- a/examples/big_dir.rs +++ b/examples/big_dir.rs @@ -3,7 +3,9 @@ extern crate embedded_sdmmc; mod linux; use linux::*; -use embedded_sdmmc::{Error, VolumeManager}; +use embedded_sdmmc::Error; + +type VolumeManager = embedded_sdmmc::VolumeManager; fn main() -> Result<(), embedded_sdmmc::Error> { env_logger::init(); @@ -11,8 +13,7 @@ fn main() -> Result<(), embedded_sdmmc::Error> { let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; - let volume_mgr: VolumeManager = - VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); let volume = volume_mgr .open_volume(embedded_sdmmc::VolumeIdx(1)) .unwrap(); diff --git a/examples/create_file.rs b/examples/create_file.rs index fa96d075..cc8b1935 100644 --- a/examples/create_file.rs +++ b/examples/create_file.rs @@ -22,7 +22,9 @@ use linux::*; const FILE_TO_CREATE: &str = "CREATE.TXT"; -use embedded_sdmmc::{Error, Mode, VolumeIdx, VolumeManager}; +use embedded_sdmmc::{Error, Mode, VolumeIdx}; + +type VolumeManager = embedded_sdmmc::VolumeManager; fn main() -> Result<(), embedded_sdmmc::Error> { env_logger::init(); @@ -30,8 +32,7 @@ fn main() -> Result<(), embedded_sdmmc::Error> { let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; - let volume_mgr: VolumeManager = - VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); let volume = volume_mgr.open_volume(VolumeIdx(0))?; let root_dir = volume.open_root_dir()?; println!("\nCreating file {}...", FILE_TO_CREATE); diff --git a/examples/delete_file.rs b/examples/delete_file.rs index f76d3601..4d882132 100644 --- a/examples/delete_file.rs +++ b/examples/delete_file.rs @@ -25,7 +25,9 @@ use linux::*; const FILE_TO_DELETE: &str = "README.TXT"; -use embedded_sdmmc::{Error, VolumeIdx, VolumeManager}; +use embedded_sdmmc::{Error, VolumeIdx}; + +type VolumeManager = embedded_sdmmc::VolumeManager; fn main() -> Result<(), embedded_sdmmc::Error> { env_logger::init(); @@ -33,8 +35,7 @@ fn main() -> Result<(), embedded_sdmmc::Error> { let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; - let volume_mgr: VolumeManager = - VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); let volume = volume_mgr.open_volume(VolumeIdx(0))?; let root_dir = volume.open_root_dir()?; println!("Deleting file {}...", FILE_TO_DELETE); diff --git a/examples/list_dir.rs b/examples/list_dir.rs index 60d72943..00578494 100644 --- a/examples/list_dir.rs +++ b/examples/list_dir.rs @@ -32,23 +32,24 @@ //! $ cargo run --example list_dir -- ./disk.img //! ``` -extern crate embedded_sdmmc; - mod linux; use linux::*; -use embedded_sdmmc::{Directory, VolumeIdx, VolumeManager}; +use embedded_sdmmc::{ShortFileName, VolumeIdx}; type Error = embedded_sdmmc::Error; +type Directory<'a> = embedded_sdmmc::Directory<'a, LinuxBlockDevice, Clock, 8, 4, 4>; +type VolumeManager = embedded_sdmmc::VolumeManager; + fn main() -> Result<(), Error> { env_logger::init(); let mut args = std::env::args().skip(1); let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); + let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; - let volume_mgr: VolumeManager = - VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); let volume = volume_mgr.open_volume(VolumeIdx(0))?; let root_dir = volume.open_root_dir()?; list_dir(root_dir, "/")?; @@ -58,10 +59,7 @@ fn main() -> Result<(), Error> { /// Recursively print a directory listing for the open directory given. /// /// The path is for display purposes only. -fn list_dir( - directory: Directory, - path: &str, -) -> Result<(), Error> { +fn list_dir(directory: Directory<'_>, path: &str) -> Result<(), Error> { println!("Listing {}", path); let mut children = Vec::new(); directory.iterate_dir(|entry| { @@ -77,8 +75,8 @@ fn list_dir( } ); if entry.attributes.is_directory() - && entry.name != embedded_sdmmc::ShortFileName::parent_dir() - && entry.name != embedded_sdmmc::ShortFileName::this_dir() + && entry.name != ShortFileName::parent_dir() + && entry.name != ShortFileName::this_dir() { children.push(entry.name.clone()); } diff --git a/examples/read_file.rs b/examples/read_file.rs index f962b75a..e8d900cc 100644 --- a/examples/read_file.rs +++ b/examples/read_file.rs @@ -39,7 +39,9 @@ use linux::*; const FILE_TO_READ: &str = "README.TXT"; -use embedded_sdmmc::{Error, Mode, VolumeIdx, VolumeManager}; +use embedded_sdmmc::{Error, Mode, VolumeIdx}; + +type VolumeManager = embedded_sdmmc::VolumeManager; fn main() -> Result<(), embedded_sdmmc::Error> { env_logger::init(); @@ -47,8 +49,7 @@ fn main() -> Result<(), embedded_sdmmc::Error> { let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; - let volume_mgr: VolumeManager = - VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); + let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); let volume = volume_mgr.open_volume(VolumeIdx(0))?; let root_dir = volume.open_root_dir()?; println!("\nReading file {}...", FILE_TO_READ); From 7ed6189b5bf5f884a046c6987c82df5839bbe9cf Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Tue, 29 Oct 2024 12:49:06 +0000 Subject: [PATCH 36/62] Write to the second FAT, if it exists. --- src/blockdevice.rs | 12 ++++++++++++ src/fat/volume.rs | 47 ++++++++++++++++++++++++++++++++++------------ src/volume_mgr.rs | 1 + 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/src/blockdevice.rs b/src/blockdevice.rs index 618eda23..e0f38324 100644 --- a/src/blockdevice.rs +++ b/src/blockdevice.rs @@ -137,6 +137,18 @@ where ) } + /// Write back a block you read with [`Self::read_mut`] and then modified, but to two locations. + /// + /// This is useful for updating two File Allocation Tables. + pub fn write_back_with_duplicate(&mut self, duplicate: BlockIdx) -> Result<(), D::Error> { + self.block_device.write( + &self.block, + self.block_idx.expect("write_back with no read"), + )?; + self.block_device.write(&self.block, duplicate)?; + Ok(()) + } + /// Access a blank sector pub fn blank_mut(&mut self, block_idx: BlockIdx) -> &mut Block { self.block_idx = Some(block_idx); diff --git a/src/fat/volume.rs b/src/fat/volume.rs index fbbedf9e..321e1609 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -149,6 +149,9 @@ pub struct FatVolume { /// The block the FAT starts in. Relative to start of partition (so add /// `self.lba_offset` before passing to volume manager) pub(crate) fat_start: BlockCount, + /// The block the second FAT starts in. Relative to start of partition (so add + /// `self.lba_offset` before passing to volume manager) + pub(crate) second_fat_start: Option, /// Expected number of free clusters pub(crate) free_clusters_count: Option, /// Number of the next expected free cluster @@ -211,11 +214,15 @@ impl FatVolume { where D: BlockDevice, { - let this_fat_block_num; + let mut second_fat_block_num = None; match &self.fat_specific_info { FatSpecificInfo::Fat16(_fat16_info) => { let fat_offset = cluster.0 * 2; - this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); + let this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); + if let Some(second_fat_start) = self.second_fat_start { + second_fat_block_num = + Some(self.lba_start + second_fat_start.offset_bytes(fat_offset)); + } let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; trace!("Reading FAT for update"); let block = block_cache @@ -237,7 +244,11 @@ impl FatVolume { FatSpecificInfo::Fat32(_fat32_info) => { // FAT32 => 4 bytes per entry let fat_offset = cluster.0 * 4; - this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); + let this_fat_block_num = self.lba_start + self.fat_start.offset_bytes(fat_offset); + if let Some(second_fat_start) = self.second_fat_start { + second_fat_block_num = + Some(self.lba_start + second_fat_start.offset_bytes(fat_offset)); + } let this_fat_ent_offset = (fat_offset % Block::LEN_U32) as usize; trace!("Reading FAT for update"); let block = block_cache @@ -259,7 +270,11 @@ impl FatVolume { } } trace!("Updating FAT"); - block_cache.write_back()?; + if let Some(duplicate) = second_fat_block_num { + block_cache.write_back_with_duplicate(duplicate)?; + } else { + block_cache.write_back()?; + } Ok(()) } @@ -995,7 +1010,9 @@ impl FatVolume { } Err(e) => return Err(e), }; + // This new cluster is the end of the file's chain self.update_fat(block_cache, new_cluster, ClusterId::END_OF_FILE)?; + // If there's something before this new one, update the FAT to point it at us if let Some(cluster) = prev_cluster { trace!( "Updating old cluster {:?} to {:?} in FAT", @@ -1132,6 +1149,12 @@ where trace!("Reading BPB"); let block = block_cache.read(lba_start).map_err(Error::DeviceError)?; let bpb = Bpb::create_from_bytes(block).map_err(Error::FormatError)?; + let fat_start = BlockCount(u32::from(bpb.reserved_block_count())); + let second_fat_start = if bpb.num_fats() == 2 { + Some(fat_start + BlockCount(bpb.fat_size())) + } else { + None + }; match bpb.fat_type { FatType::Fat16 => { if bpb.bytes_per_block() as usize != Block::LEN { @@ -1141,7 +1164,6 @@ where let root_dir_blocks = ((u32::from(bpb.root_entries_count()) * OnDiskDirEntry::LEN_U32) + (Block::LEN_U32 - 1)) / Block::LEN_U32; - let fat_start = BlockCount(u32::from(bpb.reserved_block_count())); let first_root_dir_block = fat_start + BlockCount(u32::from(bpb.num_fats()) * bpb.fat_size()); let first_data_block = first_root_dir_block + BlockCount(root_dir_blocks); @@ -1152,8 +1174,9 @@ where contents: bpb.volume_label(), }, blocks_per_cluster: bpb.blocks_per_cluster(), - first_data_block: (first_data_block), - fat_start: BlockCount(u32::from(bpb.reserved_block_count())), + first_data_block, + fat_start, + second_fat_start, free_clusters_count: None, next_free_cluster: None, cluster_count: bpb.total_clusters(), @@ -1166,9 +1189,8 @@ where } FatType::Fat32 => { // FirstDataSector = BPB_ResvdSecCnt + (BPB_NumFATs * FATSz); - let first_data_block = u32::from(bpb.reserved_block_count()) - + (u32::from(bpb.num_fats()) * bpb.fat_size()); - + let first_data_block = + fat_start + BlockCount(u32::from(bpb.num_fats()) * bpb.fat_size()); // Safe to unwrap since this is a Fat32 Type let info_location = bpb.fs_info_block().unwrap(); let mut volume = FatVolume { @@ -1178,8 +1200,9 @@ where contents: bpb.volume_label(), }, blocks_per_cluster: bpb.blocks_per_cluster(), - first_data_block: BlockCount(first_data_block), - fat_start: BlockCount(u32::from(bpb.reserved_block_count())), + first_data_block, + fat_start, + second_fat_start, free_clusters_count: None, next_free_cluster: None, cluster_count: bpb.total_clusters(), diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 75140d07..d8311a6b 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -1493,6 +1493,7 @@ mod tests { blocks_per_cluster: 8, first_data_block: BlockCount(15136), fat_start: BlockCount(32), + second_fat_start: Some(BlockCount(32 + 0x0000_1D80)), name: fat::VolumeName::create_from_str("Pictures").unwrap(), free_clusters_count: None, next_free_cluster: None, From 60f502120aedc901d56c1b923a475b0bbff0f025 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Tue, 29 Oct 2024 12:54:08 +0000 Subject: [PATCH 37/62] Update the free cluster count when unmounting. Also decrement the free cluster count when allocating a cluster. --- src/fat/volume.rs | 1 + src/volume_mgr.rs | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/src/fat/volume.rs b/src/fat/volume.rs index 321e1609..aa6bc4d3 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -1042,6 +1042,7 @@ impl FatVolume { Err(e) => return Err(e), }; debug!("Next free cluster is {:?}", self.next_free_cluster); + // Record that we've allocated a cluster if let Some(ref mut number_free_cluster) = self.free_clusters_count { *number_free_cluster -= 1; }; diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index d8311a6b..1dfd9b01 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -334,6 +334,7 @@ where /// You can't close it if there are any files or directories open on it. pub fn close_volume(&self, volume: RawVolume) -> Result<(), Error> { let mut data = self.data.try_borrow_mut().map_err(|_| Error::LockError)?; + let data = data.deref_mut(); for f in data.open_files.iter() { if f.raw_volume == volume { @@ -349,6 +350,12 @@ where let volume_idx = data.get_volume_by_id(volume)?; + match &mut data.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + fat.update_info_sector(&mut data.block_cache)?; + } + } + data.open_volumes.swap_remove(volume_idx); Ok(()) From a6cc6cd9ebc22643cf6b1c6e1c41f525127e44e6 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Tue, 29 Oct 2024 12:03:18 +0000 Subject: [PATCH 38/62] Move FAT-specific code in make_dir_in_dir. This code belongs in the FAT implementation as it is all FAT-specific. --- src/fat/volume.rs | 86 +++++++++++++++++++++++++++++++++++++++++++++++ src/volume_mgr.rs | 66 +----------------------------------- 2 files changed, 87 insertions(+), 65 deletions(-) diff --git a/src/fat/volume.rs b/src/fat/volume.rs index 25f67ded..fa6a023e 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -1242,6 +1242,92 @@ impl FatVolume { block_cache.write_back().map_err(Error::DeviceError)?; Ok(()) } + + /// Create a new directory. + /// + /// 1) Creates the directory entry in the parent + /// 2) Allocates a new cluster to hold the new directory + /// 3) Writes out the `.` and `..` entries in the new directory + pub(crate) fn make_dir( + &mut self, + block_cache: &mut BlockCache, + time_source: &T, + parent: ClusterId, + sfn: ShortFileName, + att: Attributes, + ) -> Result<(), Error> + where + D: BlockDevice, + T: TimeSource, + { + let mut new_dir_entry_in_parent = + self.write_new_directory_entry(block_cache, time_source, parent, sfn, att)?; + if new_dir_entry_in_parent.cluster == ClusterId::EMPTY { + new_dir_entry_in_parent.cluster = self.alloc_cluster(block_cache, None, false)?; + // update the parent dir with the cluster of the new dir + self.write_entry_to_disk(block_cache, &new_dir_entry_in_parent)?; + } + let new_dir_start_block = self.cluster_to_block(new_dir_entry_in_parent.cluster); + debug!("Made new dir entry {:?}", new_dir_entry_in_parent); + let now = time_source.get_timestamp(); + let fat_type = self.get_fat_type(); + // A blank block + let block = block_cache.blank_mut(new_dir_start_block); + // make the "." entry + let dot_entry_in_child = DirEntry { + name: crate::ShortFileName::this_dir(), + mtime: now, + ctime: now, + attributes: att, + // point at ourselves + cluster: new_dir_entry_in_parent.cluster, + size: 0, + entry_block: new_dir_start_block, + entry_offset: 0, + }; + debug!("New dir has {:?}", dot_entry_in_child); + let mut offset = 0; + block[offset..offset + OnDiskDirEntry::LEN] + .copy_from_slice(&dot_entry_in_child.serialize(fat_type)[..]); + offset += OnDiskDirEntry::LEN; + // make the ".." entry + let dot_dot_entry_in_child = DirEntry { + name: crate::ShortFileName::parent_dir(), + mtime: now, + ctime: now, + attributes: att, + // point at our parent + cluster: match fat_type { + FatType::Fat16 => { + // On FAT16, indicate parent is root using Cluster(0) + if parent == ClusterId::ROOT_DIR { + ClusterId::EMPTY + } else { + parent + } + } + FatType::Fat32 => parent, + }, + size: 0, + entry_block: new_dir_start_block, + entry_offset: OnDiskDirEntry::LEN_U32, + }; + debug!("New dir has {:?}", dot_dot_entry_in_child); + block[offset..offset + OnDiskDirEntry::LEN] + .copy_from_slice(&dot_dot_entry_in_child.serialize(fat_type)[..]); + + block_cache.write_back()?; + + for block_idx in new_dir_start_block + .range(BlockCount(u32::from(self.blocks_per_cluster))) + .skip(1) + { + let _block = block_cache.blank_mut(block_idx); + block_cache.write_back()?; + } + + Ok(()) + } } /// Load the boot parameter block from the start of the given partition and diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 42770ae0..8ddbb62a 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -1072,77 +1072,13 @@ where VolumeType::Fat(fat) => { // TODO: Move this into the FAT volume code debug!("Making dir entry"); - let mut new_dir_entry_in_parent = fat.write_new_directory_entry( + fat.make_dir( &mut data.block_cache, &self.time_source, parent_directory_info.cluster, sfn, att, )?; - if new_dir_entry_in_parent.cluster == ClusterId::EMPTY { - new_dir_entry_in_parent.cluster = - fat.alloc_cluster(&mut data.block_cache, None, false)?; - // update the parent dir with the cluster of the new dir - fat.write_entry_to_disk(&mut data.block_cache, &new_dir_entry_in_parent)?; - } - let new_dir_start_block = fat.cluster_to_block(new_dir_entry_in_parent.cluster); - debug!("Made new dir entry {:?}", new_dir_entry_in_parent); - let now = self.time_source.get_timestamp(); - let fat_type = fat.get_fat_type(); - // A blank block - let block = data.block_cache.blank_mut(new_dir_start_block); - // make the "." entry - let dot_entry_in_child = DirEntry { - name: crate::ShortFileName::this_dir(), - mtime: now, - ctime: now, - attributes: att, - // point at ourselves - cluster: new_dir_entry_in_parent.cluster, - size: 0, - entry_block: new_dir_start_block, - entry_offset: 0, - }; - debug!("New dir has {:?}", dot_entry_in_child); - let mut offset = 0; - block[offset..offset + fat::OnDiskDirEntry::LEN] - .copy_from_slice(&dot_entry_in_child.serialize(fat_type)[..]); - offset += fat::OnDiskDirEntry::LEN; - // make the ".." entry - let dot_dot_entry_in_child = DirEntry { - name: crate::ShortFileName::parent_dir(), - mtime: now, - ctime: now, - attributes: att, - // point at our parent - cluster: match fat_type { - fat::FatType::Fat16 => { - // On FAT16, indicate parent is root using Cluster(0) - if parent_directory_info.cluster == ClusterId::ROOT_DIR { - ClusterId::EMPTY - } else { - parent_directory_info.cluster - } - } - fat::FatType::Fat32 => parent_directory_info.cluster, - }, - size: 0, - entry_block: new_dir_start_block, - entry_offset: fat::OnDiskDirEntry::LEN_U32, - }; - debug!("New dir has {:?}", dot_dot_entry_in_child); - block[offset..offset + fat::OnDiskDirEntry::LEN] - .copy_from_slice(&dot_dot_entry_in_child.serialize(fat_type)[..]); - - data.block_cache.write_back()?; - - for block_idx in new_dir_start_block - .range(BlockCount(u32::from(fat.blocks_per_cluster))) - .skip(1) - { - let _block = data.block_cache.blank_mut(block_idx); - data.block_cache.write_back()?; - } } }; From 567aa09ede6e4c3ffb0b8cf534ab2a5b8072f561 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Tue, 29 Oct 2024 13:29:22 +0000 Subject: [PATCH 39/62] Use cluster 0 when the parent dir is the root, for both FAT16 and FAT32. fsck was getting annoyed at this. --- src/fat/volume.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/fat/volume.rs b/src/fat/volume.rs index fa6a023e..4f085acb 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -1297,16 +1297,11 @@ impl FatVolume { ctime: now, attributes: att, // point at our parent - cluster: match fat_type { - FatType::Fat16 => { - // On FAT16, indicate parent is root using Cluster(0) - if parent == ClusterId::ROOT_DIR { - ClusterId::EMPTY - } else { - parent - } - } - FatType::Fat32 => parent, + cluster: if parent == ClusterId::ROOT_DIR { + // indicate parent is root using Cluster(0) + ClusterId::EMPTY + } else { + parent }, size: 0, entry_block: new_dir_start_block, From 2eed03b09eedadcb13152c7ca29673efefe836be Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Tue, 29 Oct 2024 13:32:25 +0000 Subject: [PATCH 40/62] Update disk image. I ran fsck on it and it wanted to ensure we had two copies of the Volume Label - one as a volume label file in the root and one in the filesystem metadata. --- tests/directories.rs | 20 ++++++++++++++++++++ tests/disk.img.gz | Bin 703414 -> 707976 bytes 2 files changed, 20 insertions(+) diff --git a/tests/directories.rs b/tests/directories.rs index e2e20d29..e10a9018 100644 --- a/tests/directories.rs +++ b/tests/directories.rs @@ -98,6 +98,16 @@ fn fat16_root_directory_listing() { }, Some(String::from(".fseventsd")), ), + ( + ExpectedDirEntry { + name: String::from("P-FAT16"), + mtime: String::from("2024-10-30 18:43:12"), + ctime: String::from("2024-10-30 18:43:12"), + size: 0, + is_dir: false, + }, + None, + ), ]; let mut listing = Vec::new(); @@ -266,6 +276,16 @@ fn fat32_root_directory_listing() { }, Some(String::from(".fseventsd")), ), + ( + ExpectedDirEntry { + name: String::from("P-FAT32"), + mtime: String::from("2024-10-30 18:43:16"), + ctime: String::from("2024-10-30 18:43:16"), + size: 0, + is_dir: false, + }, + None, + ), ( ExpectedDirEntry { name: String::from("THISIS~9"), diff --git a/tests/disk.img.gz b/tests/disk.img.gz index 1ba2bdfd1f75fb2d39e1ffec210c8bd4d4312f0f..507ec9487ddfcebab03baec2c837d3ebd3cbe629 100644 GIT binary patch literal 707976 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

    5Nfer%f9^mEhHrtwXao6t?u;16&D{0UBh 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 &+?~N#Vuby)^ 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�X(Tw?VnX8UAOj6cu~86Z>Z;)NvUy2kn(PJewb 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 From f497cc24715df9816c8028a534c761f2d037677b Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sun, 3 Nov 2024 14:26:06 +0000 Subject: [PATCH 41/62] Remove resolved TODO --- src/volume_mgr.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 8ddbb62a..5cc69c98 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -1070,7 +1070,6 @@ where // Need mutable access for this match &mut data.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => { - // TODO: Move this into the FAT volume code debug!("Making dir entry"); fat.make_dir( &mut data.block_cache, From ca9c9fa0c13c735fcc4deda9f9a2eb21371aa5d9 Mon Sep 17 00:00:00 2001 From: Graicc <33105645+Graicc@users.noreply.github.com> Date: Thu, 14 Nov 2024 16:28:58 -0500 Subject: [PATCH 42/62] Add support for partition id 04h --- src/lib.rs | 3 +++ src/volume_mgr.rs | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 434808d4..853a69b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -420,6 +420,9 @@ const PARTITION_ID_FAT16_LBA: u8 = 0x0E; /// Marker for a FAT16 partition. Seen on a card formatted with the official /// SD-Card formatter. const PARTITION_ID_FAT16: u8 = 0x06; +/// Marker for a FAT16 partition smaller than 32MB. Seen on the wowki simulated +/// microsd card +const PARTITION_ID_FAT16_SMALL: u8 = 0x04; /// Marker for a FAT32 partition. What Macosx disk utility (and also SD-Card formatter?) /// use. const PARTITION_ID_FAT32_CHS_LBA: u8 = 0x0B; diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 5cc69c98..de34a911 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -17,7 +17,7 @@ use crate::{ }, trace, Block, BlockCache, BlockCount, BlockDevice, BlockIdx, Error, RawVolume, ShortFileName, Volume, VolumeIdx, VolumeInfo, VolumeType, PARTITION_ID_FAT16, PARTITION_ID_FAT16_LBA, - PARTITION_ID_FAT32_CHS_LBA, PARTITION_ID_FAT32_LBA, + PARTITION_ID_FAT16_SMALL, PARTITION_ID_FAT32_CHS_LBA, PARTITION_ID_FAT32_LBA, }; /// Wraps a block device and gives access to the FAT-formatted volumes within @@ -195,7 +195,8 @@ where PARTITION_ID_FAT32_CHS_LBA | PARTITION_ID_FAT32_LBA | PARTITION_ID_FAT16_LBA - | PARTITION_ID_FAT16 => { + | PARTITION_ID_FAT16 + | PARTITION_ID_FAT16_SMALL => { let volume = fat::parse_volume(&mut data.block_cache, lba_start, num_blocks)?; let id = RawVolume(data.id_generator.generate()); let info = VolumeInfo { From dc223a14c0c6897c770b8958751450e177a65bec Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Fri, 22 Nov 2024 18:06:57 +0200 Subject: [PATCH 43/62] chore Cargo - Set rust-version & add matrix for building and testing in MSRV Signed-off-by: Lachezar Lechev --- .github/workflows/rust.yml | 12 ++++++++++-- Cargo.toml | 3 +++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 75b29b7a..64bcfde6 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -6,7 +6,10 @@ jobs: formatting: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt - name: Check formatting run: cargo fmt -- --check @@ -14,9 +17,14 @@ jobs: runs-on: ubuntu-latest strategy: matrix: + # Always run MSRV too! + rust: ["stable", "1.76"] features: ['log', 'defmt-log', '""'] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} - name: Build run: cargo build --no-default-features --features ${{matrix.features}} --verbose - name: Run Tests diff --git a/Cargo.toml b/Cargo.toml index 45276c1c..49b13171 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,9 @@ readme = "README.md" repository = "https://github.com/rust-embedded-community/embedded-sdmmc-rs" version = "0.8.0" +# Make sure to update the CI too! +rust-version = "1.76" + [dependencies] byteorder = {version = "1", default-features = false} defmt = {version = "0.3", optional = true} From c941b5a3224adcc6f25876885a8ad11607ecfa4c Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Sun, 24 Nov 2024 22:51:43 +0200 Subject: [PATCH 44/62] fix: MSRV - inclusive pattern matching Signed-off-by: Lachezar Lechev --- src/fat/volume.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/fat/volume.rs b/src/fat/volume.rs index 4f085acb..60bb365e 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -595,7 +595,7 @@ impl FatVolume { lfn_buffer.push(&buffer); SeqState::Complete { csum } } - (true, 0x02..0x14, _) => { + (true, sequence, _) if sequence >= 0x02 && sequence < 0x14 => { lfn_buffer.clear(); lfn_buffer.push(&buffer); SeqState::Remaining { @@ -607,7 +607,9 @@ impl FatVolume { lfn_buffer.push(&buffer); SeqState::Complete { csum } } - (false, 0x01..0x13, SeqState::Remaining { csum, next }) if next == sequence => { + (false, sequence, SeqState::Remaining { csum, next }) + if sequence >= 0x01 && sequence < 0x13 && next == sequence => + { lfn_buffer.push(&buffer); SeqState::Remaining { csum, From fb8e5d25c751ef2cab0a1700c8c406d9ede4d441 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Tue, 26 Nov 2024 15:22:27 +0200 Subject: [PATCH 45/62] fix: volume - name - replace trim_ascii_end fn call Signed-off-by: Lachezar Lechev --- src/fat/volume.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/fat/volume.rs b/src/fat/volume.rs index 60bb365e..78b88f7f 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -36,7 +36,15 @@ impl VolumeName { /// Get name pub fn name(&self) -> &[u8] { - self.contents.trim_ascii_end() + let mut bytes = &self.contents[..]; + while let [rest @ .., last] = bytes { + if last.is_ascii_whitespace() { + bytes = rest; + } else { + break; + } + } + bytes } /// Create a new MS-DOS volume label. From f7cdece97a5c61d6c1ac0e8d7fd2b7426a9a3424 Mon Sep 17 00:00:00 2001 From: Zach Grimaldi Date: Fri, 17 Jan 2025 20:18:46 -0500 Subject: [PATCH 46/62] docs: README write to file example; reminder to run `file.flush()` --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index eace6f8c..1524f546 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,18 @@ while !my_file.is_eof() { } ``` +For writing files: +```rust +let my_other_file = root_dir.open_file_in_dir("MY_OTHER_FILE.CSV", embedded_sdmmc::Mode::ReadWriteCreateOrAppend)?; +my_other_file.write(b"Timestamp,Signal,Value\n")?; +my_other_file.write(b"2025-01-01T00:00:00Z,TEMP,25.0\n")?; +my_other_file.write(b"2025-01-01T00:00:01Z,TEMP,25.1\n")?; +my_other_file.write(b"2025-01-01T00:00:02Z,TEMP,25.2\n")?; + +// Don't forget to flush the file so that the directory entry is updated +my_other_file.flush()?; +``` + ### Open directories and files By default the `VolumeManager` will initialize with a maximum number of `4` open directories, files and volumes. This can be customized by specifying the `MAX_DIR`, `MAX_FILES` and `MAX_VOLUMES` generic consts of the `VolumeManager`: From bc9d7fd1d538789f950df5240d0da97b83041623 Mon Sep 17 00:00:00 2001 From: Zach Grimaldi Date: Fri, 17 Jan 2025 20:51:16 -0500 Subject: [PATCH 47/62] fix: shorten file name --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1524f546..939b82f7 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ while !my_file.is_eof() { For writing files: ```rust -let my_other_file = root_dir.open_file_in_dir("MY_OTHER_FILE.CSV", embedded_sdmmc::Mode::ReadWriteCreateOrAppend)?; +let my_other_file = root_dir.open_file_in_dir("FILE2.CSV", embedded_sdmmc::Mode::ReadWriteCreateOrAppend)?; my_other_file.write(b"Timestamp,Signal,Value\n")?; my_other_file.write(b"2025-01-01T00:00:00Z,TEMP,25.0\n")?; my_other_file.write(b"2025-01-01T00:00:01Z,TEMP,25.1\n")?; From 9fdc5f62a61f4fa19be95e4c441f265fc7759442 Mon Sep 17 00:00:00 2001 From: Zach Grimaldi Date: Fri, 17 Jan 2025 20:52:18 -0500 Subject: [PATCH 48/62] fix: shorten filename to fit in "8.3" --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 939b82f7..95d1f66f 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ while !my_file.is_eof() { For writing files: ```rust -let my_other_file = root_dir.open_file_in_dir("FILE2.CSV", embedded_sdmmc::Mode::ReadWriteCreateOrAppend)?; +let my_other_file = root_dir.open_file_in_dir("MY_DATA.CSV", embedded_sdmmc::Mode::ReadWriteCreateOrAppend)?; my_other_file.write(b"Timestamp,Signal,Value\n")?; my_other_file.write(b"2025-01-01T00:00:00Z,TEMP,25.0\n")?; my_other_file.write(b"2025-01-01T00:00:01Z,TEMP,25.1\n")?; From 758c6bd98e8a67227e5edfa23c8a20bb453514f8 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 18 Jan 2025 15:29:16 +0000 Subject: [PATCH 49/62] Add blank line --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 95d1f66f..0c3a7352 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ while !my_file.is_eof() { ``` For writing files: + ```rust let my_other_file = root_dir.open_file_in_dir("MY_DATA.CSV", embedded_sdmmc::Mode::ReadWriteCreateOrAppend)?; my_other_file.write(b"Timestamp,Signal,Value\n")?; From 714d3ebd5c8a139ac83e1885fd0d5eb0b5e3b17a Mon Sep 17 00:00:00 2001 From: Zach Grimaldi Date: Sat, 18 Jan 2025 15:28:11 -0500 Subject: [PATCH 50/62] docs: adds same snippet of writing to files to `lib.rs` for crate doc --- src/lib.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 853a69b5..923c5f5f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,19 @@ //! } //! ``` //! +//! For writing files: +//! +//! ```rust +//! let my_other_file = root_dir.open_file_in_dir("MY_DATA.CSV", embedded_sdmmc::Mode::ReadWriteCreateOrAppend)?; +//! my_other_file.write(b"Timestamp,Signal,Value\n")?; +//! my_other_file.write(b"2025-01-01T00:00:00Z,TEMP,25.0\n")?; +//! my_other_file.write(b"2025-01-01T00:00:01Z,TEMP,25.1\n")?; +//! my_other_file.write(b"2025-01-01T00:00:02Z,TEMP,25.2\n")?; +//! +//! // Don't forget to flush the file so that the directory entry is updated +//! my_other_file.flush()?; +//! ``` +//! //! ## Features //! //! * `log`: Enabled by default. Generates log messages using the `log` crate. From 8a407ea59541351baeb6d437f93721aea50924d9 Mon Sep 17 00:00:00 2001 From: Zach Grimaldi Date: Sat, 18 Jan 2025 15:37:14 -0500 Subject: [PATCH 51/62] fix: `ignore` will pass ci/cd for known-to-be pseudocode --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 923c5f5f..95c6eceb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,7 +45,7 @@ //! //! For writing files: //! -//! ```rust +//! ```rust,,ignore //! let my_other_file = root_dir.open_file_in_dir("MY_DATA.CSV", embedded_sdmmc::Mode::ReadWriteCreateOrAppend)?; //! my_other_file.write(b"Timestamp,Signal,Value\n")?; //! my_other_file.write(b"2025-01-01T00:00:00Z,TEMP,25.0\n")?; From b3014cfb8d944be0f8a206ef4c58a67f7b26fe52 Mon Sep 17 00:00:00 2001 From: Zach Grimaldi Date: Sat, 18 Jan 2025 15:37:39 -0500 Subject: [PATCH 52/62] fix: extraneous comma --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 95c6eceb..9c766bae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,7 +45,7 @@ //! //! For writing files: //! -//! ```rust,,ignore +//! ```rust,ignore //! let my_other_file = root_dir.open_file_in_dir("MY_DATA.CSV", embedded_sdmmc::Mode::ReadWriteCreateOrAppend)?; //! my_other_file.write(b"Timestamp,Signal,Value\n")?; //! my_other_file.write(b"2025-01-01T00:00:00Z,TEMP,25.0\n")?; From a9fd5a420d95476a2c3ae221f9b3c8164826e648 Mon Sep 17 00:00:00 2001 From: Zach Grimaldi Date: Sun, 19 Jan 2025 12:56:30 -0500 Subject: [PATCH 53/62] Update src/lib.rs with suggestion Co-authored-by: Jonathan 'theJPster' Pallant --- src/lib.rs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9c766bae..c6af4e95 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,15 +45,21 @@ //! //! For writing files: //! -//! ```rust,ignore -//! let my_other_file = root_dir.open_file_in_dir("MY_DATA.CSV", embedded_sdmmc::Mode::ReadWriteCreateOrAppend)?; -//! my_other_file.write(b"Timestamp,Signal,Value\n")?; -//! my_other_file.write(b"2025-01-01T00:00:00Z,TEMP,25.0\n")?; -//! my_other_file.write(b"2025-01-01T00:00:01Z,TEMP,25.1\n")?; -//! my_other_file.write(b"2025-01-01T00:00:02Z,TEMP,25.2\n")?; -//! -//! // Don't forget to flush the file so that the directory entry is updated -//! my_other_file.flush()?; +//! ```rust +//! use embedded_sdmmc::{BlockDevice, Directory, Error, Mode, TimeSource}; +//! fn write_file( +//! root_dir: &mut Directory, +//! ) -> Result<(), Error> +//! { +//! let my_other_file = root_dir.open_file_in_dir("MY_DATA.CSV", Mode::ReadWriteCreateOrAppend)?; +//! my_other_file.write(b"Timestamp,Signal,Value\n")?; +//! my_other_file.write(b"2025-01-01T00:00:00Z,TEMP,25.0\n")?; +//! my_other_file.write(b"2025-01-01T00:00:01Z,TEMP,25.1\n")?; +//! my_other_file.write(b"2025-01-01T00:00:02Z,TEMP,25.2\n")?; +//! // Don't forget to flush the file so that the directory entry is updated +//! my_other_file.flush()?; +//! Ok(()) +//! } //! ``` //! //! ## Features From 8aceb574e994300528f51f10bbd449f2f6981bbd Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 8 Feb 2025 17:48:48 +0000 Subject: [PATCH 54/62] Clean up imports and help text for examples. --- README.md | 9 +++++---- examples/append_file.rs | 11 ++++------- examples/big_dir.rs | 33 +++++++++++++++++++++++---------- examples/create_file.rs | 11 ++++------- examples/delete_file.rs | 11 ++++------- examples/list_dir.rs | 7 +++---- examples/read_file.rs | 11 ++++------- examples/readme_test.rs | 38 +++++++++++++++++++++----------------- examples/shell.rs | 20 +++++++++++++++++--- 9 files changed, 85 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 0c3a7352..9bd06b1e 100644 --- a/README.md +++ b/README.md @@ -11,22 +11,23 @@ designed for readability and simplicity over performance. You will need something that implements the `BlockDevice` trait, which can read and write the 512-byte blocks (or sectors) from your card. If you were to implement this over USB Mass Storage, there's no reason this crate couldn't work with a USB Thumb Drive, but we only supply a `BlockDevice` suitable for reading SD and SDHC cards over SPI. ```rust +use embedded_sdmmc::{SdCard, VolumeManager, Mode, VolumeIdx}; // Build an SD Card interface out of an SPI device, a chip-select pin and the delay object -let sdcard = embedded_sdmmc::SdCard::new(sdmmc_spi, delay); +let sdcard = SdCard::new(sdmmc_spi, delay); // Get the card size (this also triggers card initialisation because it's not been done yet) println!("Card size is {} bytes", sdcard.num_bytes()?); // Now let's look for volumes (also known as partitions) on our block device. // To do this we need a Volume Manager. It will take ownership of the block device. -let volume_mgr = embedded_sdmmc::VolumeManager::new(sdcard, time_source); +let volume_mgr = VolumeManager::new(sdcard, time_source); // Try and access Volume 0 (i.e. the first partition). // The volume object holds information about the filesystem on that volume. -let volume0 = volume_mgr.open_volume(embedded_sdmmc::VolumeIdx(0))?; +let volume0 = volume_mgr.open_volume(VolumeIdx(0))?; println!("Volume 0: {:?}", volume0); // Open the root directory (mutably borrows from the volume). let root_dir = volume0.open_root_dir()?; // Open a file called "MY_FILE.TXT" in the root directory // This mutably borrows the directory. -let my_file = root_dir.open_file_in_dir("MY_FILE.TXT", embedded_sdmmc::Mode::ReadOnly)?; +let my_file = root_dir.open_file_in_dir("MY_FILE.TXT", Mode::ReadOnly)?; // Print the contents of the file, assuming it's in ISO-8859-1 encoding while !my_file.is_eof() { let mut buffer = [0u8; 32]; diff --git a/examples/append_file.rs b/examples/append_file.rs index 2c7dd8e2..54b7577e 100644 --- a/examples/append_file.rs +++ b/examples/append_file.rs @@ -5,18 +5,15 @@ //! $ cargo run --example append_file -- /dev/mmcblk0 //! ``` //! -//! If you pass a block device it should be unmounted. No testing has been -//! performed with Windows raw block devices - please report back if you try -//! this! There is a gzipped example disk image which you can gunzip and test -//! with if you don't have a suitable block device. +//! If you pass a block device it should be unmounted. There is a gzipped +//! example disk image which you can gunzip and test with if you don't have a +//! suitable block device. //! //! ```bash //! zcat ./tests/disk.img.gz > ./disk.img //! $ cargo run --example append_file -- ./disk.img //! ``` -extern crate embedded_sdmmc; - mod linux; use linux::*; @@ -26,7 +23,7 @@ use embedded_sdmmc::{Error, Mode, VolumeIdx}; type VolumeManager = embedded_sdmmc::VolumeManager; -fn main() -> Result<(), embedded_sdmmc::Error> { +fn main() -> Result<(), Error> { env_logger::init(); let mut args = std::env::args().skip(1); let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); diff --git a/examples/big_dir.rs b/examples/big_dir.rs index a0170267..bfc7e83c 100644 --- a/examples/big_dir.rs +++ b/examples/big_dir.rs @@ -1,22 +1,38 @@ -extern crate embedded_sdmmc; +//! Big Directory Example. +//! +//! Attempts to create an infinite number of files in the root directory of the +//! first volume of the given block device. This is basically to see what +//! happens when the root directory runs out of space. +//! +//! ```bash +//! $ cargo run --example big_dir -- ./disk.img +//! $ cargo run --example big_dir -- /dev/mmcblk0 +//! ``` +//! +//! If you pass a block device it should be unmounted. There is a gzipped +//! example disk image which you can gunzip and test with if you don't have a +//! suitable block device. +//! +//! ```bash +//! zcat ./tests/disk.img.gz > ./disk.img +//! $ cargo run --example big_dir -- ./disk.img +//! ``` mod linux; use linux::*; -use embedded_sdmmc::Error; +use embedded_sdmmc::{Error, Mode, VolumeIdx}; type VolumeManager = embedded_sdmmc::VolumeManager; -fn main() -> Result<(), embedded_sdmmc::Error> { +fn main() -> Result<(), Error> { env_logger::init(); let mut args = std::env::args().skip(1); let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); let print_blocks = args.find(|x| x == "-v").map(|_| true).unwrap_or(false); let lbd = LinuxBlockDevice::new(filename, print_blocks).map_err(Error::DeviceError)?; let volume_mgr: VolumeManager = VolumeManager::new_with_limits(lbd, Clock, 0xAA00_0000); - let volume = volume_mgr - .open_volume(embedded_sdmmc::VolumeIdx(1)) - .unwrap(); + let volume = volume_mgr.open_volume(VolumeIdx(0)).unwrap(); println!("Volume: {:?}", volume); let root_dir = volume.open_root_dir().unwrap(); @@ -26,10 +42,7 @@ fn main() -> Result<(), embedded_sdmmc::Error> { let file_name = format!("{}.da", file_num); println!("opening file {file_name} for writing"); let file = root_dir - .open_file_in_dir( - file_name.as_str(), - embedded_sdmmc::Mode::ReadWriteCreateOrTruncate, - ) + .open_file_in_dir(file_name.as_str(), Mode::ReadWriteCreateOrTruncate) .unwrap(); let buf = b"hello world, from rust"; println!("writing to file"); diff --git a/examples/create_file.rs b/examples/create_file.rs index cc8b1935..7f3cfb48 100644 --- a/examples/create_file.rs +++ b/examples/create_file.rs @@ -5,18 +5,15 @@ //! $ cargo run --example create_file -- /dev/mmcblk0 //! ``` //! -//! If you pass a block device it should be unmounted. No testing has been -//! performed with Windows raw block devices - please report back if you try -//! this! There is a gzipped example disk image which you can gunzip and test -//! with if you don't have a suitable block device. +//! If you pass a block device it should be unmounted. There is a gzipped +//! example disk image which you can gunzip and test with if you don't have a +//! suitable block device. //! //! ```bash //! zcat ./tests/disk.img.gz > ./disk.img //! $ cargo run --example create_file -- ./disk.img //! ``` -extern crate embedded_sdmmc; - mod linux; use linux::*; @@ -26,7 +23,7 @@ use embedded_sdmmc::{Error, Mode, VolumeIdx}; type VolumeManager = embedded_sdmmc::VolumeManager; -fn main() -> Result<(), embedded_sdmmc::Error> { +fn main() -> Result<(), Error> { env_logger::init(); let mut args = std::env::args().skip(1); let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); diff --git a/examples/delete_file.rs b/examples/delete_file.rs index 4d882132..3df1978e 100644 --- a/examples/delete_file.rs +++ b/examples/delete_file.rs @@ -8,18 +8,15 @@ //! NOTE: THIS EXAMPLE DELETES A FILE CALLED README.TXT. IF YOU DO NOT WANT THAT //! FILE DELETED FROM YOUR DISK IMAGE, DO NOT RUN THIS EXAMPLE. //! -//! If you pass a block device it should be unmounted. No testing has been -//! performed with Windows raw block devices - please report back if you try -//! this! There is a gzipped example disk image which you can gunzip and test -//! with if you don't have a suitable block device. +//! If you pass a block device it should be unmounted. There is a gzipped +//! example disk image which you can gunzip and test with if you don't have a +//! suitable block device. //! //! ```bash //! zcat ./tests/disk.img.gz > ./disk.img //! $ cargo run --example delete_file -- ./disk.img //! ``` -extern crate embedded_sdmmc; - mod linux; use linux::*; @@ -29,7 +26,7 @@ use embedded_sdmmc::{Error, VolumeIdx}; type VolumeManager = embedded_sdmmc::VolumeManager; -fn main() -> Result<(), embedded_sdmmc::Error> { +fn main() -> Result<(), Error> { env_logger::init(); let mut args = std::env::args().skip(1); let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); diff --git a/examples/list_dir.rs b/examples/list_dir.rs index 00578494..e12807af 100644 --- a/examples/list_dir.rs +++ b/examples/list_dir.rs @@ -22,10 +22,9 @@ //! $ //! ``` //! -//! If you pass a block device it should be unmounted. No testing has been -//! performed with Windows raw block devices - please report back if you try -//! this! There is a gzipped example disk image which you can gunzip and test -//! with if you don't have a suitable block device. +//! If you pass a block device it should be unmounted. There is a gzipped +//! example disk image which you can gunzip and test with if you don't have a +//! suitable block device. //! //! ```bash //! zcat ./tests/disk.img.gz > ./disk.img diff --git a/examples/read_file.rs b/examples/read_file.rs index e8d900cc..0800de94 100644 --- a/examples/read_file.rs +++ b/examples/read_file.rs @@ -22,18 +22,15 @@ //! 00000100 [54, 0a, 0d] |T...............| //! ``` //! -//! If you pass a block device it should be unmounted. No testing has been -//! performed with Windows raw block devices - please report back if you try -//! this! There is a gzipped example disk image which you can gunzip and test -//! with if you don't have a suitable block device. +//! If you pass a block device it should be unmounted. There is a gzipped +//! example disk image which you can gunzip and test with if you don't have a +//! suitable block device. //! //! ```bash //! zcat ./tests/disk.img.gz > ./disk.img //! $ cargo run --example read_file -- ./disk.img //! ``` -extern crate embedded_sdmmc; - mod linux; use linux::*; @@ -43,7 +40,7 @@ use embedded_sdmmc::{Error, Mode, VolumeIdx}; type VolumeManager = embedded_sdmmc::VolumeManager; -fn main() -> Result<(), embedded_sdmmc::Error> { +fn main() -> Result<(), Error> { env_logger::init(); let mut args = std::env::args().skip(1); let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); diff --git a/examples/readme_test.rs b/examples/readme_test.rs index fd867802..0d63d80b 100644 --- a/examples/readme_test.rs +++ b/examples/readme_test.rs @@ -7,6 +7,8 @@ use core::cell::RefCell; +use embedded_sdmmc::{Error, SdCardError, TimeSource, Timestamp}; + pub struct DummyCsPin; impl embedded_hal::digital::ErrorType for DummyCsPin { @@ -80,9 +82,9 @@ impl embedded_hal::delay::DelayNs for FakeDelayer { struct FakeTimesource(); -impl embedded_sdmmc::TimeSource for FakeTimesource { - fn get_timestamp(&self) -> embedded_sdmmc::Timestamp { - embedded_sdmmc::Timestamp { +impl TimeSource for FakeTimesource { + fn get_timestamp(&self) -> Timestamp { + Timestamp { year_since_1970: 0, zero_indexed_month: 0, zero_indexed_day: 0, @@ -94,24 +96,24 @@ impl embedded_sdmmc::TimeSource for FakeTimesource { } #[derive(Debug, Clone)] -enum Error { - Filesystem(embedded_sdmmc::Error), - Disk(embedded_sdmmc::SdCardError), +enum MyError { + Filesystem(Error), + Disk(SdCardError), } -impl From> for Error { - fn from(value: embedded_sdmmc::Error) -> Error { - Error::Filesystem(value) +impl From> for MyError { + fn from(value: Error) -> MyError { + MyError::Filesystem(value) } } -impl From for Error { - fn from(value: embedded_sdmmc::SdCardError) -> Error { - Error::Disk(value) +impl From for MyError { + fn from(value: SdCardError) -> MyError { + MyError::Disk(value) } } -fn main() -> Result<(), Error> { +fn main() -> Result<(), MyError> { // BEGIN Fake stuff that will be replaced with real peripherals let spi_bus = RefCell::new(FakeSpiBus()); let delay = FakeDelayer(); @@ -119,22 +121,23 @@ fn main() -> Result<(), Error> { let time_source = FakeTimesource(); // END Fake stuff that will be replaced with real peripherals + use embedded_sdmmc::{Mode, SdCard, VolumeIdx, VolumeManager}; // Build an SD Card interface out of an SPI device, a chip-select pin and the delay object - let sdcard = embedded_sdmmc::SdCard::new(sdmmc_spi, delay); + let sdcard = SdCard::new(sdmmc_spi, delay); // Get the card size (this also triggers card initialisation because it's not been done yet) println!("Card size is {} bytes", sdcard.num_bytes()?); // Now let's look for volumes (also known as partitions) on our block device. // To do this we need a Volume Manager. It will take ownership of the block device. - let volume_mgr = embedded_sdmmc::VolumeManager::new(sdcard, time_source); + let volume_mgr = VolumeManager::new(sdcard, time_source); // Try and access Volume 0 (i.e. the first partition). // The volume object holds information about the filesystem on that volume. - let volume0 = volume_mgr.open_volume(embedded_sdmmc::VolumeIdx(0))?; + let volume0 = volume_mgr.open_volume(VolumeIdx(0))?; println!("Volume 0: {:?}", volume0); // Open the root directory (mutably borrows from the volume). let root_dir = volume0.open_root_dir()?; // Open a file called "MY_FILE.TXT" in the root directory // This mutably borrows the directory. - let my_file = root_dir.open_file_in_dir("MY_FILE.TXT", embedded_sdmmc::Mode::ReadOnly)?; + let my_file = root_dir.open_file_in_dir("MY_FILE.TXT", Mode::ReadOnly)?; // Print the contents of the file, assuming it's in ISO-8859-1 encoding while !my_file.is_eof() { let mut buffer = [0u8; 32]; @@ -143,6 +146,7 @@ fn main() -> Result<(), Error> { print!("{}", *b as char); } } + Ok(()) } diff --git a/examples/shell.rs b/examples/shell.rs index 5c6b0e50..42682769 100644 --- a/examples/shell.rs +++ b/examples/shell.rs @@ -3,6 +3,20 @@ //! Presents a basic command prompt which implements some basic MS-DOS style //! shell commands. //! +//! ```bash +//! $ cargo run --example shell -- ./disk.img +//! $ cargo run --example shell -- /dev/mmcblk0 +//! ``` +//! +//! If you pass a block device it should be unmounted. There is a gzipped +//! example disk image which you can gunzip and test with if you don't have a +//! suitable block device. +//! +//! ```bash +//! zcat ./tests/disk.img.gz > ./disk.img +//! $ cargo run --example shell -- ./disk.img +//! ``` +//! //! Note that `embedded_sdmmc` itself does not care about 'paths' - only //! accessing files and directories on on disk, relative to some previously //! opened directory. A 'path' is an operating-system level construct, and can @@ -72,7 +86,7 @@ use std::{cell::RefCell, io::prelude::*}; use embedded_sdmmc::{ - Error as EsError, LfnBuffer, RawDirectory, RawVolume, ShortFileName, VolumeIdx, + Error as EsError, LfnBuffer, Mode, RawDirectory, RawVolume, ShortFileName, VolumeIdx, }; type VolumeManager = embedded_sdmmc::VolumeManager; @@ -324,7 +338,7 @@ impl Context { /// print a text file fn cat(&self, filename: &Path) -> Result<(), Error> { let (dir, filename) = self.resolve_filename(filename)?; - let f = dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadOnly)?; + let f = dir.open_file_in_dir(filename, Mode::ReadOnly)?; let mut data = Vec::new(); while !f.is_eof() { let mut buffer = vec![0u8; 65536]; @@ -344,7 +358,7 @@ impl Context { /// print a binary file fn hexdump(&self, filename: &Path) -> Result<(), Error> { let (dir, filename) = self.resolve_filename(filename)?; - let f = dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadOnly)?; + let f = dir.open_file_in_dir(filename, Mode::ReadOnly)?; let mut data = Vec::new(); while !f.is_eof() { let mut buffer = vec![0u8; 65536]; From e1f379f817f3e64fd1aca5fec85d8860ae710e26 Mon Sep 17 00:00:00 2001 From: Valentin Trophime <60969974+ValouBambou@users.noreply.github.com> Date: Fri, 28 Feb 2025 17:52:47 +0100 Subject: [PATCH 55/62] fix bad typing in defmt debug --- src/fat/volume.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fat/volume.rs b/src/fat/volume.rs index 78b88f7f..ed39b6a0 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -594,7 +594,7 @@ impl FatVolume { debug!("LFN Contents {start} {sequence} {csum:02x} {buffer:04x?}"); #[cfg(feature = "defmt-log")] debug!( - "LFN Contents {=u8} {=u8} {=u8:02x} {=[?; 13]:#04x}", + "LFN Contents {=bool} {=u8} {=u8:02x} {=[?; 13]:#04x}", start, sequence, csum, buffer ); match (start, sequence, self) { From 54bebcd91847ec328718e86b49090309aa852fa7 Mon Sep 17 00:00:00 2001 From: Valentin Trophime <60969974+ValouBambou@users.noreply.github.com> Date: Thu, 6 Mar 2025 15:04:06 +0100 Subject: [PATCH 56/62] ci: set DEFMT_LOG env var in build --- .github/workflows/rust.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 64bcfde6..45b6e5d3 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -27,5 +27,7 @@ jobs: toolchain: ${{ matrix.rust }} - name: Build run: cargo build --no-default-features --features ${{matrix.features}} --verbose + env: + DEFMT_LOG: debug - name: Run Tests run: cargo test --no-default-features --features ${{matrix.features}} --verbose From ef96021e5471766b36d93e1b035d00e0ba8369ec Mon Sep 17 00:00:00 2001 From: yanshay Date: Sat, 7 Jun 2025 12:49:11 +0300 Subject: [PATCH 57/62] fix seek and write to block start zeros rest of block after written data (#188) --- src/volume_mgr.rs | 2 +- tests/write_file.rs | 48 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index de34a911..edac23cd 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -878,7 +878,7 @@ where Err(e) => return Err(e), }; let to_copy = core::cmp::min(block_avail, bytes_to_write - written); - let block = if block_offset != 0 { + let block = if block_offset != 0 || data.open_files[file_idx].current_offset < data.open_files[file_idx].entry.size { debug!("Reading for partial block write"); data.block_cache .read_mut(block_idx) diff --git a/tests/write_file.rs b/tests/write_file.rs index af178145..30560b72 100644 --- a/tests/write_file.rs +++ b/tests/write_file.rs @@ -102,6 +102,54 @@ fn flush_file() { assert_eq!(entry.size, 64 * 3); } +#[test] +fn random_access_write_file() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let volume_mgr: VolumeManager>, utils::TestTimeSource, 4, 2, 1> = + VolumeManager::new_with_limits(disk, time_source, 0xAA00_0000); + let volume = volume_mgr + .open_raw_volume(VolumeIdx(0)) + .expect("open volume"); + let root_dir = volume_mgr.open_root_dir(volume).expect("open root dir"); + + // Open with string + let f = volume_mgr + .open_file_in_dir(root_dir, "README.TXT", Mode::ReadWriteTruncate) + .expect("open file"); + + let test_data = vec![0xCC; 1024]; + volume_mgr.write(f, &test_data).expect("file write"); + + let length = volume_mgr.file_length(f).expect("get length"); + assert_eq!(length, 1024); + + for seek_offset in [100, 0] { + let mut expected_buffer = [0u8;4]; + + // fetch some data at offset seek_offset + volume_mgr.file_seek_from_start(f, seek_offset).expect("Seeking"); + volume_mgr.read(f, &mut expected_buffer).expect("read file"); + + // modify first byte + expected_buffer[0] ^= 0xff; + + // write only first byte, expecting the rest to not change + volume_mgr.file_seek_from_start(f, seek_offset).expect("Seeking"); + volume_mgr.write(f, &expected_buffer[0..1]).expect("file write"); + volume_mgr.flush_file(f).expect("file flush"); + + // read and verify + volume_mgr.file_seek_from_start(f, seek_offset).expect("file seek"); + let mut read_buffer = [0xffu8, 0xff, 0xff, 0xff]; + volume_mgr.read(f, &mut read_buffer).expect("file read"); + assert_eq!(read_buffer, expected_buffer, "mismatch seek+write at offset {seek_offset} from start"); + } + + volume_mgr.close_file(f).expect("close file"); + volume_mgr.close_dir(root_dir).expect("close dir"); + volume_mgr.close_volume(volume).expect("close volume"); +} // **************************************************************************** // // End Of File From d94fe867448d1210f66513ea4cd47f1cf2854c0c Mon Sep 17 00:00:00 2001 From: yanshay Date: Sat, 7 Jun 2025 12:51:57 +0300 Subject: [PATCH 58/62] cargo fmt --- src/volume_mgr.rs | 4 +++- tests/write_file.rs | 23 +++++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index edac23cd..c2aec0a8 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -878,7 +878,9 @@ where Err(e) => return Err(e), }; let to_copy = core::cmp::min(block_avail, bytes_to_write - written); - let block = if block_offset != 0 || data.open_files[file_idx].current_offset < data.open_files[file_idx].entry.size { + let block = if block_offset != 0 + || data.open_files[file_idx].current_offset < data.open_files[file_idx].entry.size + { debug!("Reading for partial block write"); data.block_cache .read_mut(block_idx) diff --git a/tests/write_file.rs b/tests/write_file.rs index 30560b72..9a5e4ab9 100644 --- a/tests/write_file.rs +++ b/tests/write_file.rs @@ -125,25 +125,36 @@ fn random_access_write_file() { assert_eq!(length, 1024); for seek_offset in [100, 0] { - let mut expected_buffer = [0u8;4]; + let mut expected_buffer = [0u8; 4]; // fetch some data at offset seek_offset - volume_mgr.file_seek_from_start(f, seek_offset).expect("Seeking"); + volume_mgr + .file_seek_from_start(f, seek_offset) + .expect("Seeking"); volume_mgr.read(f, &mut expected_buffer).expect("read file"); // modify first byte expected_buffer[0] ^= 0xff; // write only first byte, expecting the rest to not change - volume_mgr.file_seek_from_start(f, seek_offset).expect("Seeking"); - volume_mgr.write(f, &expected_buffer[0..1]).expect("file write"); + volume_mgr + .file_seek_from_start(f, seek_offset) + .expect("Seeking"); + volume_mgr + .write(f, &expected_buffer[0..1]) + .expect("file write"); volume_mgr.flush_file(f).expect("file flush"); // read and verify - volume_mgr.file_seek_from_start(f, seek_offset).expect("file seek"); + volume_mgr + .file_seek_from_start(f, seek_offset) + .expect("file seek"); let mut read_buffer = [0xffu8, 0xff, 0xff, 0xff]; volume_mgr.read(f, &mut read_buffer).expect("file read"); - assert_eq!(read_buffer, expected_buffer, "mismatch seek+write at offset {seek_offset} from start"); + assert_eq!( + read_buffer, expected_buffer, + "mismatch seek+write at offset {seek_offset} from start" + ); } volume_mgr.close_file(f).expect("close file"); From 8ad3628046d96773375438073b3b45fce6f410ab Mon Sep 17 00:00:00 2001 From: yanshay Date: Sat, 7 Jun 2025 19:49:50 +0300 Subject: [PATCH 59/62] improve to avoid unneeded read before write when writing 512 bytes to a block --- src/volume_mgr.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index c2aec0a8..3f5f895c 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -878,15 +878,15 @@ where Err(e) => return Err(e), }; let to_copy = core::cmp::min(block_avail, bytes_to_write - written); - let block = if block_offset != 0 - || data.open_files[file_idx].current_offset < data.open_files[file_idx].entry.size - { + let block = if (block_offset == 0) && (to_copy == block_avail) { + // we're replacing the whole Block, so the previous contents + // are irrelevant + data.block_cache.blank_mut(block_idx) + } else { debug!("Reading for partial block write"); data.block_cache .read_mut(block_idx) .map_err(Error::DeviceError)? - } else { - data.block_cache.blank_mut(block_idx) }; block[block_offset..block_offset + to_copy] .copy_from_slice(&buffer[written..written + to_copy]); From 97b2471e626a92fd8b06bbd0dee13b588b509daa Mon Sep 17 00:00:00 2001 From: yanshay Date: Sat, 7 Jun 2025 20:02:58 +0300 Subject: [PATCH 60/62] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08b9b1fc..bc6bcd80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog] and this project adheres to [Semantic - __Breaking Change__: The `VolumeManager::device` method now takes a callback rather than giving you a reference to the underlying `BlockDevice` - __Breaking Change__: `Error:LockError` variant added. - __Breaking Change__: `SearchId` was renamed to `Handle` +- Fixed writing at block start mid-file (previously overwrote subsequent file data with zeros up to the end of the block) ### Added From 89ea59958825e45b26e77bf15b06da702676f871 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 7 Jun 2025 23:05:15 +0100 Subject: [PATCH 61/62] Prepare a release of 0.9 --- CHANGELOG.md | 5 ++++- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fab54e8..1ce4a40f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog] and this project adheres to [Semantic ## [Unreleased] +## [Version 0.9.0] - 2025-06-08 + ### Changed - __Breaking Change__: `VolumeManager` now uses interior-mutability (with a `RefCell`) and so most methods are now `&self`. This also makes it easier to open multiple `File`, `Directory` or `Volume` objects at once. @@ -187,7 +189,8 @@ The format is based on [Keep a Changelog] and this project adheres to [Semantic [Keep a Changelog]: http://keepachangelog.com/en/1.0.0/ [Semantic Versioning]: http://semver.org/spec/v2.0.0.html -[Unreleased]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.8.2...develop +[Unreleased]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.9.0...develop +[Version 0.9.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.8.2...v0.9.0 [Version 0.8.2]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.8.1...v0.8.2 [Version 0.8.1]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.8.0...v0.8.1 [Version 0.8.0]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/compare/v0.7.0...v0.8.0 diff --git a/Cargo.toml b/Cargo.toml index 4c3c8131..11b6ff6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" name = "embedded-sdmmc" readme = "README.md" repository = "https://github.com/rust-embedded-community/embedded-sdmmc-rs" -version = "0.8.2" +version = "0.9.0" # Make sure to update the CI too! rust-version = "1.76" From fe6d98c44b76bfa1cc529984f0150d912cbb1667 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 7 Jun 2025 23:07:15 +0100 Subject: [PATCH 62/62] Fix typo in CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ce4a40f..8ff7a2f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ The format is based on [Keep a Changelog] and this project adheres to [Semantic ### Changed - __Breaking Change__: `VolumeManager` now uses interior-mutability (with a `RefCell`) and so most methods are now `&self`. This also makes it easier to open multiple `File`, `Directory` or `Volume` objects at once. -- __Breaking Change__: The `VolumeManager`, `File`, `Directory` and `Volume` no longer implement `Send` or `Sync. +- __Breaking Change__: The `VolumeManager`, `File`, `Directory` and `Volume` no longer implement `Send` or `Sync`. - `VolumeManager` uses an interior block cache of 512 bytes, increasing its size by about 520 bytes but hugely reducing stack space required at run-time. - __Breaking Change__: The `VolumeManager::device` method now takes a callback rather than giving you a reference to the underlying `BlockDevice` - __Breaking Change__: `Error:LockError` variant added.