diff --git a/CHANGELOG.md b/CHANGELOG.md index 3361d1a..3a5e92e 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 diff --git a/examples/shell.rs b/examples/shell.rs index 341bb51..5c6b0e5 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,24 @@ 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 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!( + "{: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 504f67f..c27a8cd 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 49b8bb2..83707e4 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 fbbedf9..5d8307d 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,121 @@ 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 { + #[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(); + 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 +655,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 +684,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 +715,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 +734,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/attributes.rs b/src/filesystem/attributes.rs index a6df757..e22dcd1 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) } } diff --git a/src/filesystem/cluster.rs b/src/filesystem/cluster.rs index bcf6eb0..14f1126 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, ")")?; diff --git a/src/filesystem/directory.rs b/src/filesystem/directory.rs index 5cfc5f0..527807b 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/filename.rs b/src/filesystem/filename.rs index a8b34e4..31d2854 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))] @@ -175,6 +176,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 { @@ -210,6 +220,139 @@ 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], + /// How many bytes are free. + /// + /// This is also the byte index the string starts from. + 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> { + /// 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: storage, + free: len, + overflow: false, + unpaired_surrogate: None, + } + } + + /// Empty out this buffer + pub fn clear(&mut self) { + self.free = self.inner.len(); + self.overflow = false; + self.unpaired_surrogate = None; + } + + /// 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 + .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]; + + // 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); + // 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 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; + } + } + } + + /// 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 { + "" + } else { + // we always only put UTF-8 encoded data in here + unsafe { core::str::from_utf8_unchecked(&self.inner[self.free..]) } + } + } +} + // **************************************************************************** // // Unit Tests @@ -302,6 +445,58 @@ 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() + ); + } + + #[test] + fn one_piece() { + 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, + ]); + assert_eq!(buf.as_str(), "0123∂"); + } + + #[test] + fn two_piece() { + 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, + ]); + buf.push(&[ + 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, 0x0048, 0x0049, 0x004a, 0x004b, + 0x004c, 0x004d, + ]); + 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"); + } } // **************************************************************************** diff --git a/src/filesystem/mod.rs b/src/filesystem/mod.rs index 92c94a1..668ac86 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/lib.rs b/src/lib.rs index bdd5630..434808d 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; diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 75140d0..f359460 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 1acfb37..e2e20d2 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,87 @@ 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 storage = [0u8; 128]; + let mut lfn_buffer: LfnBuffer = LfnBuffer::new(&mut storage); 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 +186,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 +193,13 @@ fn fat16_sub_directory_listing() { given_entry, expected_entry ); } + assert_eq!( + expected.len(), + listing.len(), + "{:#?} != {:#?}", + expected, + listing + ); } #[test] @@ -175,52 +216,107 @@ 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 storage = [0u8; 128]; + let mut lfn_buffer: LfnBuffer = LfnBuffer::new(&mut storage); 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 8df362b..1ba2bdf 100644 Binary files a/tests/disk.img.gz and b/tests/disk.img.gz differ