diff --git a/TODO.md b/TODO.md index 39df84d..9d84a62 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,5 @@ TODO ==== -* support for a volume label file in the root directory * format volume API * add method for getting `DirEntry` from a path (possible names: metadata, lookup) * do not create LFN entries if the name fits in a SFN entry diff --git a/src/dir.rs b/src/dir.rs index 25e518a..ed487cf 100644 --- a/src/dir.rs +++ b/src/dir.rs @@ -106,11 +106,7 @@ impl<'a, T: ReadWriteSeek + 'a> Dir<'a, T> { /// Creates directory entries iterator. pub fn iter(&self) -> DirIter<'a, T> { - DirIter { - stream: self.stream.clone(), - fs: self.fs, - err: false, - } + DirIter::new(self.stream.clone(), self.fs, true) } fn find_entry( @@ -138,6 +134,16 @@ impl<'a, T: ReadWriteSeek + 'a> Dir<'a, T> { Err(io::Error::new(ErrorKind::NotFound, "No such file or directory")) } + pub(crate) fn find_volume_entry(&self) -> io::Result>> { + for r in DirIter::new(self.stream.clone(), self.fs, false) { + let e = r?; + if e.data.is_volume() { + return Ok(Some(e)); + } + } + Ok(None) + } + fn check_for_existence(&self, name: &str, is_dir: Option) -> io::Result> { let mut short_name_gen = ShortNameGenerator::new(name); loop { @@ -453,10 +459,28 @@ impl<'a, T: ReadWriteSeek> Clone for Dir<'a, T> { pub struct DirIter<'a, T: ReadWriteSeek + 'a> { stream: DirRawStream<'a, T>, fs: &'a FileSystem, + skip_volume: bool, err: bool, } impl<'a, T: ReadWriteSeek> DirIter<'a, T> { + fn new(stream: DirRawStream<'a, T>, fs: &'a FileSystem, skip_volume: bool) -> Self { + DirIter { + stream, fs, skip_volume, + err: false, + } + } + + fn should_ship_entry(&self, raw_entry: &DirEntryData) -> bool { + if raw_entry.is_deleted() { + return true; + } + match raw_entry { + DirEntryData::File(sfn_entry) => self.skip_volume && sfn_entry.is_volume(), + _ => false, + } + } + fn read_dir_entry(&mut self) -> io::Result>> { #[cfg(feature = "alloc")] let mut lfn_buf = LongNameBuilder::new(); @@ -465,19 +489,19 @@ impl<'a, T: ReadWriteSeek> DirIter<'a, T> { loop { let raw_entry = DirEntryData::deserialize(&mut self.stream)?; offset += DIR_ENTRY_SIZE; + // Check if this is end of dir + if raw_entry.is_end() { + return Ok(None); + } + // Check if this is deleted or volume ID entry + if self.should_ship_entry(&raw_entry) { + #[cfg(feature = "alloc")] + lfn_buf.clear(); + begin_offset = offset; + continue; + } match raw_entry { DirEntryData::File(data) => { - // Check if this is end of dif - if data.is_end() { - return Ok(None); - } - // Check if this is deleted or volume ID entry - if data.is_deleted() || data.is_volume() { - #[cfg(feature = "alloc")] - lfn_buf.clear(); - begin_offset = offset; - continue; - } // Get entry position on volume let abs_pos = self.stream.abs_pos().map(|p| p - DIR_ENTRY_SIZE); // Check if LFN checksum is valid @@ -496,13 +520,6 @@ impl<'a, T: ReadWriteSeek> DirIter<'a, T> { })); }, DirEntryData::Lfn(data) => { - // Check if this is deleted entry - if data.is_deleted() { - #[cfg(feature = "alloc")] - lfn_buf.clear(); - begin_offset = offset; - continue; - } // Append to LFN buffer #[cfg(feature = "alloc")] lfn_buf.process(&data); @@ -519,6 +536,7 @@ impl<'a, T: ReadWriteSeek> Clone for DirIter<'a, T> { stream: self.stream.clone(), fs: self.fs, err: self.err, + skip_volume: self.skip_volume, } } } diff --git a/src/fs.rs b/src/fs.rs index 558eb9d..c68a68e 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -494,11 +494,44 @@ impl FileSystem { /// Note: File with `VOLUME_ID` attribute in root directory is ignored by this library. /// Only label from BPB is used. pub fn volume_label_as_bytes(&self) -> &[u8] { + const PADDING: u8 = 0x20; let full_label_slice = &self.bpb.volume_label; - let len = full_label_slice.iter().rposition(|b| *b != 0x20).map(|p| p + 1).unwrap_or(0); + let len = full_label_slice.iter().rposition(|b| *b != PADDING).map(|p| p + 1).unwrap_or(0); &full_label_slice[..len] } + /// Returns a volume label from root directory as `String`. + /// + /// It finds file with `VOLUME_ID` attribute and returns its short name. + #[cfg(feature = "alloc")] + pub fn read_volume_label_from_root_dir(&self) -> io::Result> { + // Note: DirEntry::file_short_name() cannot be used because it interprets name as 8.3 + // (adds dot before an extension) + let volume_label_opt = self.read_volume_label_from_root_dir_as_bytes()?; + if let Some(volume_label) = volume_label_opt { + const PADDING: u8 = 0x20; + // Strip label padding + let len = volume_label.iter().rposition(|b| *b != PADDING).map(|p| p + 1).unwrap_or(0); + let label_slice = &volume_label[..len]; + // Decode volume label from OEM codepage + let volume_label_iter = label_slice.iter().cloned(); + let char_iter = volume_label_iter.map(|c| self.options.oem_cp_converter.decode(c)); + // Build string from character iterator + Ok(Some(String::from_iter(char_iter))) + } else { + Ok(None) + } + } + + /// Returns a volume label from root directory as byte array. + /// + /// Label is encoded in the OEM codepage. + /// It finds file with `VOLUME_ID` attribute and returns its short name. + pub fn read_volume_label_from_root_dir_as_bytes(&self) -> io::Result> { + let entry_opt = self.root_dir().find_volume_entry()?; + Ok(entry_opt.map(|e| *e.raw_short_name())) + } + /// Returns a root directory object allowing for futher penetration of a filesystem structure. pub fn root_dir<'b>(&'b self) -> Dir<'b, T> { let root_rdr = { diff --git a/tests/read.rs b/tests/read.rs index 09c5f70..c8494de 100644 --- a/tests/read.rs +++ b/tests/read.rs @@ -187,6 +187,7 @@ fn test_get_file_by_path_fat32() { fn test_volume_metadata(fs: FileSystem, fat_type: FatType) { assert_eq!(fs.volume_id(), 0x12345678); assert_eq!(fs.volume_label(), "Test!"); + assert_eq!(&fs.read_volume_label_from_root_dir().unwrap().unwrap(), "Test!"); assert_eq!(fs.fat_type(), fat_type); }