From ba5329edade9a588dfd156263147c1db63a17551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Harabie=C5=84?= Date: Sun, 15 Oct 2017 22:42:26 +0200 Subject: [PATCH] Support removing files and directories. --- src/dir.rs | 281 +++++++++++++++++++++++++++++++++---------------- src/file.rs | 2 +- tests/write.rs | 33 ++++++ 3 files changed, 225 insertions(+), 91 deletions(-) diff --git a/src/dir.rs b/src/dir.rs index 2d0991a..572b9c2 100644 --- a/src/dir.rs +++ b/src/dir.rs @@ -75,6 +75,10 @@ bitflags! { } } +const LFN_PART_LEN: usize = 13; +const DIR_ENTRY_SIZE: u64 = 32; +const DIR_ENTRY_REMOVED_FLAG: u8 = 0xE5; + #[allow(dead_code)] #[derive(Clone, Debug, Default)] pub(crate) struct DirFileEntryData { @@ -144,6 +148,14 @@ impl DirFileEntryData { wrt.write_u32::(self.size)?; Ok(()) } + + fn is_removed(&self) -> bool { + self.name[0] == DIR_ENTRY_REMOVED_FLAG + } + + fn is_end(&self) -> bool { + self.name[0] == 0 + } } #[allow(dead_code)] @@ -159,12 +171,81 @@ struct DirLfnEntryData { name_2: [u16; 2], } +impl DirLfnEntryData { + fn serialize(&self, wrt: &mut Write) -> io::Result<()> { + wrt.write_u8(self.order)?; + for ch in self.name_0.iter() { + wrt.write_u16::(*ch)?; + } + wrt.write_u8(self.attrs.bits())?; + wrt.write_u8(self.entry_type)?; + wrt.write_u8(self.checksum)?; + for ch in self.name_1.iter() { + wrt.write_u16::(*ch)?; + } + wrt.write_u16::(self.reserved_0)?; + for ch in self.name_2.iter() { + wrt.write_u16::(*ch)?; + } + Ok(()) + } + + fn is_removed(&self) -> bool { + self.order == DIR_ENTRY_REMOVED_FLAG + } +} + #[derive(Clone, Debug)] enum DirEntryData { File(DirFileEntryData), Lfn(DirLfnEntryData), } +impl DirEntryData { + fn serialize(&mut self, wrt: &mut Write) -> io::Result<()> { + match self { + &mut DirEntryData::File(ref mut file) => file.serialize(wrt), + &mut DirEntryData::Lfn(ref mut lfn) => lfn.serialize(wrt), + } + } + + fn deserialize(rdr: &mut Read) -> io::Result { + let mut name = [0; 11]; + rdr.read_exact(&mut name)?; + let attrs = FileAttributes::from_bits_truncate(rdr.read_u8()?); + if attrs == FileAttributes::LFN { + let mut data = DirLfnEntryData { + attrs, ..Default::default() + }; + let mut cur = Cursor::new(&name); + data.order = cur.read_u8()?; + cur.read_u16_into::(&mut data.name_0)?; + data.entry_type = rdr.read_u8()?; + data.checksum = rdr.read_u8()?; + rdr.read_u16_into::(&mut data.name_1)?; + data.reserved_0 = rdr.read_u16::()?; + rdr.read_u16_into::(&mut data.name_2)?; + Ok(DirEntryData::Lfn(data)) + } else { + let data = DirFileEntryData { + name, + attrs, + reserved_0: rdr.read_u8()?, + create_time_0: rdr.read_u8()?, + create_time_1: rdr.read_u16::()?, + create_date: rdr.read_u16::()?, + access_date: rdr.read_u16::()?, + first_cluster_hi: rdr.read_u16::()?, + modify_time: rdr.read_u16::()?, + modify_date: rdr.read_u16::()?, + first_cluster_lo: rdr.read_u16::()?, + size: rdr.read_u32::()?, + }; + Ok(DirEntryData::File(data)) + } + } +} + #[derive(Clone, Copy, Debug)] pub struct Date { pub year: u16, @@ -250,6 +331,7 @@ pub struct DirEntry<'a, 'b: 'a> { data: DirFileEntryData, lfn: Vec, entry_pos: u64, + offset_range: (u64, u64), fs: FileSystemRef<'a, 'b>, } @@ -390,6 +472,50 @@ impl <'a, 'b> Dir<'a, 'b> { None => Ok(e.to_file()) } } + + fn is_empty(&mut self) -> io::Result { + for r in self.iter() { + let e = r?; + let name = e.file_name(); + if name != "." && name != ".." { + return Ok(false); + } + } + Ok(true) + } + + pub fn remove(&mut self, path: &str) -> io::Result<()> { + let (name, rest_opt) = Self::split_path(path); + let e = self.find_entry(name)?; + match rest_opt { + Some(rest) => e.to_dir().remove(rest), + None => { + trace!("removing {}", path); + if e.is_dir() && !e.to_dir().is_empty()? { + return Err(io::Error::new(ErrorKind::NotFound, "removing non-empty directory is denied")); + } + match e.first_cluster() { + Some(n) => self.fs.cluster_iter(n).free()?, + _ => {}, + } + let mut stream = self.rdr.clone(); + stream.seek(SeekFrom::Start(e.offset_range.0 as u64))?; + let num = (e.offset_range.1 - e.offset_range.0) as usize / DIR_ENTRY_SIZE as usize; + for _ in 0..num { + let mut data = DirEntryData::deserialize(&mut stream)?; + trace!("removing dir entry {:?}", data); + match data { + DirEntryData::File(ref mut data) => + data.name[0] = DIR_ENTRY_REMOVED_FLAG, + DirEntryData::Lfn(ref mut data) => data.order = DIR_ENTRY_REMOVED_FLAG, + }; + stream.seek(SeekFrom::Current(-(DIR_ENTRY_SIZE as i64)))?; + data.serialize(&mut stream)?; + } + Ok(()) + } + } + } } #[derive(Clone)] @@ -400,39 +526,71 @@ pub struct DirIter<'a, 'b: 'a> { } impl <'a, 'b> DirIter<'a, 'b> { - fn read_dir_entry_data(&mut self) -> io::Result { - let mut name = [0; 11]; - self.rdr.read_exact(&mut name)?; - let attrs = FileAttributes::from_bits_truncate(self.rdr.read_u8()?); - if attrs == FileAttributes::LFN { - let mut data = DirLfnEntryData { - attrs, ..Default::default() - }; - let mut cur = Cursor::new(&name); - data.order = cur.read_u8()?; - cur.read_u16_into::(&mut data.name_0)?; - data.entry_type = self.rdr.read_u8()?; - data.checksum = self.rdr.read_u8()?; - self.rdr.read_u16_into::(&mut data.name_1)?; - data.reserved_0 = self.rdr.read_u16::()?; - self.rdr.read_u16_into::(&mut data.name_2)?; - Ok(DirEntryData::Lfn(data)) - } else { - let data = DirFileEntryData { - name, - attrs, - reserved_0: self.rdr.read_u8()?, - create_time_0: self.rdr.read_u8()?, - create_time_1: self.rdr.read_u16::()?, - create_date: self.rdr.read_u16::()?, - access_date: self.rdr.read_u16::()?, - first_cluster_hi: self.rdr.read_u16::()?, - modify_time: self.rdr.read_u16::()?, - modify_date: self.rdr.read_u16::()?, - first_cluster_lo: self.rdr.read_u16::()?, - size: self.rdr.read_u32::()?, - }; - Ok(DirEntryData::File(data)) + fn read_dir_entry_raw_data(&mut self) -> io::Result { + DirEntryData::deserialize(&mut self.rdr) + } + + fn read_dir_entry(&mut self) -> io::Result>> { + let mut lfn_buf = LongNameBuilder::new(); + let mut offset = self.rdr.seek(SeekFrom::Current(0))?; + let mut begin_offset = offset; + loop { + let raw_entry = self.read_dir_entry_raw_data()?; + offset += DIR_ENTRY_SIZE; + 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_removed() || data.attrs.contains(FileAttributes::VOLUME_ID) { + lfn_buf.clear(); + begin_offset = offset; + continue; + } + // Get entry position on volume + let entry_pos = self.rdr.global_pos().map(|p| p - DIR_ENTRY_SIZE); + // Check if LFN checksum is valid + lfn_buf.validate_chksum(&data.name); + return Ok(Some(DirEntry { + data, + lfn: lfn_buf.to_vec(), + fs: self.fs, + entry_pos: entry_pos.unwrap(), // safe + offset_range: (begin_offset, offset), + })); + }, + DirEntryData::Lfn(data) => { + // Check if this is deleted entry + if data.is_removed() { + lfn_buf.clear(); + begin_offset = offset; + continue; + } + // Append to LFN buffer + lfn_buf.process(&data); + } + } + } + } +} + +impl <'a, 'b> Iterator for DirIter<'a, 'b> { + type Item = io::Result>; + + fn next(&mut self) -> Option { + if self.err { + return None; + } + let r = self.read_dir_entry(); + match r { + Ok(Some(e)) => Some(Ok(e)), + Ok(None) => None, + Err(err) => { + self.err = true; + Some(Err(err)) + }, } } } @@ -443,8 +601,6 @@ struct LongNameBuilder { index: u8, } -const LFN_PART_LEN: usize = 13; - fn lfn_checksum(short_name: &[u8]) -> u8 { let mut chksum = 0u8; for i in 0..11 { @@ -530,58 +686,3 @@ impl LongNameBuilder { } } } - -const DIR_ENTRY_SIZE: u64 = 32; - -impl <'a, 'b> Iterator for DirIter<'a, 'b> { - type Item = io::Result>; - - fn next(&mut self) -> Option { - if self.err { - return None; - } - let mut lfn_buf = LongNameBuilder::new(); - loop { - let res = self.read_dir_entry_data(); - let data = match res { - Ok(data) => data, - Err(err) => { - self.err = true; - return Some(Err(err)); - }, - }; - match data { - DirEntryData::File(data) => { - // Check if this is end of dif - if data.name[0] == 0 { - return None; - } - // Check if this is deleted or volume ID entry - if data.name[0] == 0xE5 || data.attrs.contains(FileAttributes::VOLUME_ID) { - lfn_buf.clear(); - continue; - } - // Get entry position on volume - let entry_pos = self.rdr.global_pos().map(|p| p - DIR_ENTRY_SIZE); - // Check if LFN checksum is valid - lfn_buf.validate_chksum(&data.name); - return Some(Ok(DirEntry { - data, - lfn: lfn_buf.to_vec(), - fs: self.fs, - entry_pos: entry_pos.unwrap(), // safe - })); - }, - DirEntryData::Lfn(data) => { - // Check if this is deleted entry - if data.order == 0xE5 { - lfn_buf.clear(); - continue; - } - // Append to LFN buffer - lfn_buf.process(&data); - } - }; - } - } -} diff --git a/src/file.rs b/src/file.rs index 5e69490..eaa0b5e 100644 --- a/src/file.rs +++ b/src/file.rs @@ -273,7 +273,7 @@ impl<'a, 'b> Seek for File<'a, 'b> { // get number of clusters to seek (favoring previous cluster in corner case) let cluster_count = ((new_pos + cluster_size as i64 - 1) / cluster_size as i64 - 1) as isize; let old_cluster_count = ((self.offset as i64 + cluster_size as i64 - 1) / cluster_size as i64 - 1) as isize; - let new_cluster = if new_pos == 0 { + let new_cluster = if new_pos == 0 { None } else if cluster_count == old_cluster_count { self.current_cluster diff --git a/tests/write.rs b/tests/write.rs index 1eb7245..9751a57 100644 --- a/tests/write.rs +++ b/tests/write.rs @@ -89,3 +89,36 @@ fn test_write_long_file_fat16() { fn test_write_long_file_fat32() { call_with_fs(&test_write_long_file, FAT32_IMG, 2) } + +fn test_remove(fs: FileSystem) { + let mut root_dir = fs.root_dir(); + assert!(root_dir.remove("very/long/path").is_err()); + let dir = root_dir.open_dir("very/long/path").unwrap(); + let mut names = dir.iter().map(|r| r.unwrap().file_name()).collect::>(); + assert_eq!(names, [".", "..", "test.txt"]); + root_dir.remove("very/long/path/test.txt").unwrap(); + names = dir.iter().map(|r| r.unwrap().file_name()).collect::>(); + assert_eq!(names, [".", ".."]); + assert!(root_dir.remove("very/long/path").is_ok()); + + names = root_dir.iter().map(|r| r.unwrap().file_name()).collect::>(); + assert_eq!(names, ["long.txt", "short.txt", "very", "very-long-dir-name"]); + root_dir.remove("long.txt").unwrap(); + names = root_dir.iter().map(|r| r.unwrap().file_name()).collect::>(); + assert_eq!(names, ["short.txt", "very", "very-long-dir-name"]); +} + +#[test] +fn test_remove_fat12() { + call_with_fs(&test_remove, FAT12_IMG, 3) +} + +#[test] +fn test_remove_fat16() { + call_with_fs(&test_remove, FAT16_IMG, 3) +} + +#[test] +fn test_remove_fat32() { + call_with_fs(&test_remove, FAT32_IMG, 3) +}