diff --git a/src/error.rs b/src/error.rs index a4547a7..9f1cb63 100644 --- a/src/error.rs +++ b/src/error.rs @@ -144,6 +144,8 @@ where NotADirectory, /// Found a directory when we expected a file IsADirectory, + /// Expected an empty directory + DirectoryNotEmpty, /// This file cannot be modified, as it is read-only ReadOnlyFile, /// A file or directory wasn't found diff --git a/src/fat/direntry.rs b/src/fat/direntry.rs index 43f7e82..98ffcec 100644 --- a/src/fat/direntry.rs +++ b/src/fat/direntry.rs @@ -170,6 +170,11 @@ impl TryFrom for PrimitiveDateTime { // a directory entry occupies 32 bytes pub(crate) const DIRENTRY_SIZE: usize = 32; +// each directory other than the root directory must have +// at least the `.` and `..` entries +// TODO: actually check this on runtime +pub(crate) const NONROOT_MIN_DIRENTRIES: usize = 2; + #[derive(Serialize, Deserialize, Debug, Clone, Copy)] pub(crate) struct FATDirEntry { pub(crate) sfn: SFN, diff --git a/src/fat/fs.rs b/src/fat/fs.rs index 5f44e15..7bb1bbf 100644 --- a/src/fat/fs.rs +++ b/src/fat/fs.rs @@ -1412,6 +1412,52 @@ where Ok(()) } + /// Remove an empty directory from the filesystem + /// + /// Errors if the path provided points to the root directory + pub fn remove_empty_dir(&mut self, path: PathBuf) -> FSResult<(), S::Error> { + if path.is_malformed() { + return Err(FSError::MalformedPath); + } + + if !path.is_dir() { + log::error!("Not a directory"); + return Err(FSError::NotADirectory); + } + + if path == PathBuf::new() { + // we are in the root directory, we can't remove it + return Err(S::Error::new( + ::Kind::new_unsupported(), + "We can't remove the root directory", + ) + .into()); + } + + let dir_entries = self.read_dir(path.clone())?; + + if dir_entries.len() > NONROOT_MIN_DIRENTRIES { + return Err(FSError::DirectoryNotEmpty); + } + + let parent_path = path.parent(); + + let parent_dir_entries = self.read_dir(parent_path)?; + + let entry = parent_dir_entries + .iter() + .find(|entry| entry.path == path) + .ok_or(FSError::NotFound)?; + + // we first clear the corresponding entry chain in the parent directory + self.remove_entry_chain(&entry.chain)?; + + // then we remove the allocated cluster chain + self.free_cluster_chain(entry.data_cluster)?; + + Ok(()) + } + /// Get a corresponding [`RWFile`] object from a [`PathBuf`] /// /// Borrows `&mut self` until that [`RWFile`] object is dropped, effectively locking `self` until that file closed diff --git a/src/fat/tests.rs b/src/fat/tests.rs index 3327c03..92a23ef 100644 --- a/src/fat/tests.rs +++ b/src/fat/tests.rs @@ -213,6 +213,28 @@ fn remove_data_region_file() { } } +#[test] +fn remove_empty_dir() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT16.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let dir_path = PathBuf::from("/another root directory/"); + + fs.remove_empty_dir(dir_path.clone()).unwrap(); + + // the directory should now be gone + let dir_result = fs.read_dir(dir_path); + match dir_result { + Err(err) => match err { + FSError::NotFound => (), + _ => panic!("unexpected IOError: {:?}", err), + }, + _ => panic!("the directory should have been deleted by now"), + } +} + #[test] #[allow(non_snake_case)] fn FAT_tables_after_write_are_identical() { @@ -466,6 +488,28 @@ fn remove_fat32_file() { } } +#[test] +fn remove_fat32_dir() { + use std::io::Cursor; + + let mut storage = Cursor::new(FAT32.to_owned()); + let mut fs = FileSystem::from_storage(&mut storage).unwrap(); + + let dir_path = PathBuf::from("/emptydir/"); + + fs.remove_empty_dir(dir_path.clone()).unwrap(); + + // the directory should now be gone + let dir_result = fs.read_dir(dir_path); + match dir_result { + Err(err) => match err { + FSError::NotFound => (), + _ => panic!("unexpected IOError: {:?}", err), + }, + _ => panic!("the directory should have been deleted by now"), + } +} + #[test] #[allow(non_snake_case)] fn FAT_tables_after_fat32_write_are_identical() { diff --git a/src/path.rs b/src/path.rs index d2b78aa..5335f40 100644 --- a/src/path.rs +++ b/src/path.rs @@ -58,7 +58,7 @@ fn is_forbidden(pathbuf: &PathBuf) -> bool { // TODO: pushing an absolute path should replace a pathbuf /// Represents an owned, mutable path -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct PathBuf { inner: VecDeque, }