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 f06f23eb..c7e83b62 100644 --- a/src/fat/bpb.rs +++ b/src/fat/bpb.rs @@ -85,12 +85,13 @@ impl<'a> Bpb<'a> { // FAT16/FAT32 functions /// 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] + pub fn volume_label(&self) -> [u8; 11] { + let mut result = [0u8; 11]; + match self.fat_type { + FatType::Fat16 => result.copy_from_slice(&self.data[43..=53]), + FatType::Fat32 => result.copy_from_slice(&self.data[71..=81]), } + result } // FAT32 only functions @@ -98,10 +99,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()))), } } 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..c453adb9 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) } } @@ -491,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); } @@ -546,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); } @@ -673,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)); } } @@ -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/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 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, 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)),