From 054af872b3bb51c40753d7e215ea1c9b8e1c811f Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sun, 17 Dec 2023 20:45:45 +0000 Subject: [PATCH] Support making new directories. --- examples/shell.rs | 21 +++++- src/fat/volume.rs | 44 +++++++++-- src/lib.rs | 2 + src/volume_mgr.rs | 171 ++++++++++++++++++++++++++++++++++++------- tests/directories.rs | 113 ++++++++++++++++++++++++++++ tests/utils/mod.rs | 8 ++ 6 files changed, 322 insertions(+), 37 deletions(-) diff --git a/examples/shell.rs b/examples/shell.rs index ddea8de..433fb3a 100644 --- a/examples/shell.rs +++ b/examples/shell.rs @@ -41,6 +41,7 @@ impl Context { println!("\thexdump -> print a binary file"); println!("\tcd .. -> go up a level"); println!("\tcd -> change into "); + println!("\tmkdir -> create a directory called "); println!("\tquit -> exits the program"); } else if line == "0:" { self.current_volume = 0; @@ -59,11 +60,17 @@ impl Context { }; self.volume_mgr.iterate_dir(s.directory, |entry| { println!( - "{:12} {:9} {} {:?}", - entry.name, entry.size, entry.mtime, entry.attributes + "{:12} {:9} {} {} {:X?} {:?}", + entry.name, + entry.size, + entry.ctime, + entry.mtime, + entry.cluster, + entry.attributes ); })?; } else if let Some(arg) = line.strip_prefix("cd ") { + let arg = arg.trim(); let Some(s) = &mut self.volumes[self.current_volume] else { println!("This volume isn't available"); return Ok(()); @@ -77,6 +84,7 @@ impl Context { s.path.push(arg.to_owned()); } } else if let Some(arg) = line.strip_prefix("cat ") { + let arg = arg.trim(); let Some(s) = &mut self.volumes[self.current_volume] else { println!("This volume isn't available"); return Ok(()); @@ -99,6 +107,7 @@ impl Context { println!("I'm afraid that file isn't UTF-8 encoded"); } } else if let Some(arg) = line.strip_prefix("hexdump ") { + let arg = arg.trim(); let Some(s) = &mut self.volumes[self.current_volume] else { println!("This volume isn't available"); return Ok(()); @@ -136,6 +145,14 @@ impl Context { } println!(); } + } else if let Some(arg) = line.strip_prefix("mkdir ") { + let arg = arg.trim(); + let Some(s) = &mut self.volumes[self.current_volume] else { + println!("This volume isn't available"); + return Ok(()); + }; + // make the dir + self.volume_mgr.make_dir_in_dir(s.directory, arg)?; } else { println!("Unknown command {line:?} - try 'help' for help"); } diff --git a/src/fat/volume.rs b/src/fat/volume.rs index b6dd510..c41591f 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -278,7 +278,7 @@ impl FatVolume { &mut self, block_device: &D, time_source: &T, - dir: &DirectoryInfo, + dir_cluster: ClusterId, name: ShortFileName, attributes: Attributes, ) -> Result> @@ -292,12 +292,12 @@ impl FatVolume { // a specially reserved space on disk (see // `first_root_dir_block`). Other directories can have any size // as they are made of regular clusters. - let mut current_cluster = Some(dir.cluster); - let mut first_dir_block_num = match dir.cluster { + let mut current_cluster = Some(dir_cluster); + let mut first_dir_block_num = match dir_cluster { ClusterId::ROOT_DIR => self.lba_start + fat16_info.first_root_dir_block, - _ => self.cluster_to_block(dir.cluster), + _ => self.cluster_to_block(dir_cluster), }; - let dir_size = match dir.cluster { + let dir_size = match dir_cluster { ClusterId::ROOT_DIR => { let len_bytes = u32::from(fat16_info.root_entries_count) * OnDiskDirEntry::LEN_U32; @@ -363,11 +363,11 @@ impl FatVolume { FatSpecificInfo::Fat32(fat32_info) => { // All directories on FAT32 have a cluster chain but the root // dir starts in a specified cluster. - let mut current_cluster = match dir.cluster { + let mut current_cluster = match dir_cluster { ClusterId::ROOT_DIR => Some(fat32_info.first_root_dir_cluster), - _ => Some(dir.cluster), + _ => Some(dir_cluster), }; - let mut first_dir_block_num = self.cluster_to_block(dir.cluster); + let mut first_dir_block_num = self.cluster_to_block(dir_cluster); let mut blocks = [Block::new()]; let dir_size = BlockCount(u32::from(self.blocks_per_cluster)); @@ -1006,6 +1006,34 @@ impl FatVolume { } Ok(()) } + + /// Writes a Directory Entry to the disk + pub(crate) fn write_entry_to_disk( + &self, + block_device: &D, + entry: &DirEntry, + ) -> Result<(), Error> + where + D: BlockDevice, + { + let fat_type = match self.fat_specific_info { + FatSpecificInfo::Fat16(_) => FatType::Fat16, + FatSpecificInfo::Fat32(_) => FatType::Fat32, + }; + let mut blocks = [Block::new()]; + block_device + .read(&mut blocks, entry.entry_block, "read") + .map_err(Error::DeviceError)?; + let block = &mut blocks[0]; + + let start = usize::try_from(entry.entry_offset).map_err(|_| Error::ConversionError)?; + block[start..start + 32].copy_from_slice(&entry.serialize(fat_type)[..]); + + block_device + .write(&blocks, entry.entry_block) + .map_err(Error::DeviceError)?; + Ok(()) + } } /// Load the boot parameter block from the start of the given partition and diff --git a/src/lib.rs b/src/lib.rs index 7aa9622..630d09f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -222,6 +222,8 @@ where InvalidOffset, /// Disk is full DiskFull, + /// A directory with that name already exists + DirAlreadyExists, } impl From for Error diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 9e95d0f..95c09ed 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -5,7 +5,7 @@ use byteorder::{ByteOrder, LittleEndian}; use core::convert::TryFrom; -use crate::fat::{self, BlockCache, RESERVED_ENTRIES}; +use crate::fat::{self, BlockCache, FatType, OnDiskDirEntry, RESERVED_ENTRIES}; use crate::filesystem::{ Attributes, ClusterId, DirEntry, DirectoryInfo, FileInfo, Mode, RawDirectory, RawFile, @@ -432,8 +432,7 @@ where match &self.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => { file.entry.mtime = self.time_source.get_timestamp(); - let fat_type = fat.get_fat_type(); - self.write_entry_to_disk(fat_type, &file.entry)?; + fat.write_entry_to_disk(&self.block_device, &file.entry)?; } }; @@ -518,7 +517,7 @@ where VolumeType::Fat(fat) => fat.write_new_directory_entry( &self.block_device, &self.time_source, - directory_info, + directory_info.cluster, sfn, att, )?, @@ -789,8 +788,7 @@ where // If you have a length, you must have a cluster assert!(file_info.entry.cluster.0 != 0); } - let fat_type = fat.get_fat_type(); - self.write_entry_to_disk(fat_type, &file_info.entry)?; + fat.write_entry_to_disk(&self.block_device, &file_info.entry)?; } }; } @@ -866,6 +864,146 @@ where Ok(self.open_files[file_idx].current_offset) } + /// Create a directory in a given directory. + pub fn make_dir_in_dir( + &mut self, + directory: RawDirectory, + name: N, + ) -> Result<(), Error> + where + N: ToShortFileName, + { + // This check is load-bearing - we do an unchecked push later. + if self.open_dirs.is_full() { + return Err(Error::TooManyOpenDirs); + } + + let parent_directory_idx = self.get_dir_by_id(directory)?; + let parent_directory_info = &self.open_dirs[parent_directory_idx]; + let volume_id = self.open_dirs[parent_directory_idx].volume_id; + let volume_idx = self.get_volume_by_id(volume_id)?; + let volume_info = &self.open_volumes[volume_idx]; + let sfn = name.to_short_filename().map_err(Error::FilenameError)?; + + debug!("Creating directory '{}'", sfn); + debug!( + "Parent dir is in cluster {:?}", + parent_directory_info.cluster + ); + + // Does an entry exist with this name? + let maybe_dir_entry = match &volume_info.volume_type { + VolumeType::Fat(fat) => { + fat.find_directory_entry(&self.block_device, parent_directory_info, &sfn) + } + }; + + match maybe_dir_entry { + Ok(entry) if entry.attributes.is_directory() => { + return Err(Error::DirAlreadyExists); + } + Ok(_entry) => { + return Err(Error::FileAlreadyExists); + } + Err(Error::FileNotFound) => { + // perfect, let's make it + } + Err(e) => { + // Some other error - tell them about it + return Err(e); + } + }; + + let att = Attributes::create_from_fat(Attributes::DIRECTORY); + + // Need mutable access for this + match &mut self.open_volumes[volume_idx].volume_type { + VolumeType::Fat(fat) => { + debug!("Making dir entry"); + let mut new_dir_entry_in_parent = fat.write_new_directory_entry( + &self.block_device, + &self.time_source, + parent_directory_info.cluster, + sfn, + att, + )?; + if new_dir_entry_in_parent.cluster == ClusterId::EMPTY { + new_dir_entry_in_parent.cluster = + fat.alloc_cluster(&self.block_device, None, false)?; + // update the parent dir with the cluster of the new dir + fat.write_entry_to_disk(&self.block_device, &new_dir_entry_in_parent)?; + } + let new_dir_start_block = fat.cluster_to_block(new_dir_entry_in_parent.cluster); + debug!("Made new dir entry {:?}", new_dir_entry_in_parent); + let now = self.time_source.get_timestamp(); + let fat_type = fat.get_fat_type(); + // A blank block + let mut blocks = [Block::new()]; + // make the "." entry + let dot_entry_in_child = DirEntry { + name: crate::ShortFileName::this_dir(), + mtime: now, + ctime: now, + attributes: att, + // point at ourselves + cluster: new_dir_entry_in_parent.cluster, + size: 0, + entry_block: new_dir_start_block, + entry_offset: 0, + }; + debug!("New dir has {:?}", dot_entry_in_child); + let mut offset = 0; + blocks[0][offset..offset + OnDiskDirEntry::LEN] + .copy_from_slice(&dot_entry_in_child.serialize(fat_type)[..]); + offset += OnDiskDirEntry::LEN; + // make the ".." entry + let dot_dot_entry_in_child = DirEntry { + name: crate::ShortFileName::parent_dir(), + mtime: now, + ctime: now, + attributes: att, + // point at our parent + cluster: match fat_type { + FatType::Fat16 => { + // On FAT16, indicate parent is root using Cluster(0) + if parent_directory_info.cluster == ClusterId::ROOT_DIR { + ClusterId::EMPTY + } else { + parent_directory_info.cluster + } + } + FatType::Fat32 => parent_directory_info.cluster, + }, + size: 0, + entry_block: new_dir_start_block, + entry_offset: OnDiskDirEntry::LEN_U32, + }; + debug!("New dir has {:?}", dot_dot_entry_in_child); + blocks[0][offset..offset + OnDiskDirEntry::LEN] + .copy_from_slice(&dot_dot_entry_in_child.serialize(fat_type)[..]); + + self.block_device + .write(&blocks, new_dir_start_block) + .map_err(Error::DeviceError)?; + + // Now zero the rest of the cluster + for b in blocks[0].iter_mut() { + *b = 0; + } + for block in new_dir_start_block + .range(BlockCount(u32::from(fat.blocks_per_cluster))) + .skip(1) + { + self.block_device + .write(&blocks, block) + .map_err(Error::DeviceError)?; + } + } + }; + + Ok(()) + } + fn get_volume_by_id(&self, volume: RawVolume) -> Result> { for (idx, v) in self.open_volumes.iter().enumerate() { if v.volume_id == volume { @@ -928,27 +1066,6 @@ where let available = Block::LEN - block_offset; Ok((block_idx, block_offset, available)) } - - /// Writes a Directory Entry to the disk - fn write_entry_to_disk( - &self, - fat_type: fat::FatType, - entry: &DirEntry, - ) -> Result<(), Error> { - let mut blocks = [Block::new()]; - self.block_device - .read(&mut blocks, entry.entry_block, "read") - .map_err(Error::DeviceError)?; - let block = &mut blocks[0]; - - let start = usize::try_from(entry.entry_offset).map_err(|_| Error::ConversionError)?; - block[start..start + 32].copy_from_slice(&entry.serialize(fat_type)[..]); - - self.block_device - .write(&blocks, entry.entry_block) - .map_err(Error::DeviceError)?; - Ok(()) - } } /// Transform mode variants (ReadWriteCreate_Or_Append) to simple modes ReadWriteAppend or diff --git a/tests/directories.rs b/tests/directories.rs index fee08f7..af1cf87 100644 --- a/tests/directories.rs +++ b/tests/directories.rs @@ -365,6 +365,119 @@ fn delete_file() { )); } +#[test] +fn make_directory() { + let time_source = utils::make_time_source(); + let disk = utils::make_block_device(utils::DISK_SOURCE).unwrap(); + let mut volume_mgr = embedded_sdmmc::VolumeManager::new(disk, time_source); + + let fat32_volume = volume_mgr + .open_raw_volume(embedded_sdmmc::VolumeIdx(1)) + .expect("open volume 1"); + + let root_dir = volume_mgr + .open_root_dir(fat32_volume) + .expect("open root dir"); + + let test_dir_name = ShortFileName::create_from_str("12345678.ABC").unwrap(); + let test_file_name = ShortFileName::create_from_str("ABC.TXT").unwrap(); + + volume_mgr + .make_dir_in_dir(root_dir, &test_dir_name) + .unwrap(); + + let new_dir = volume_mgr.open_dir(root_dir, &test_dir_name).unwrap(); + + let mut has_this = false; + let mut has_parent = false; + volume_mgr + .iterate_dir(new_dir, |item| { + if item.name == ShortFileName::parent_dir() { + has_parent = true; + assert!(item.attributes.is_directory()); + assert_eq!(item.size, 0); + assert_eq!(item.mtime.to_string(), utils::get_time_source_string()); + assert_eq!(item.ctime.to_string(), utils::get_time_source_string()); + } else if item.name == ShortFileName::this_dir() { + has_this = true; + assert!(item.attributes.is_directory()); + assert_eq!(item.size, 0); + assert_eq!(item.mtime.to_string(), utils::get_time_source_string()); + assert_eq!(item.ctime.to_string(), utils::get_time_source_string()); + } else { + panic!("Unexpected item in new dir"); + } + }) + .unwrap(); + assert!(has_this); + assert!(has_parent); + + let new_file = volume_mgr + .open_file_in_dir( + new_dir, + &test_file_name, + embedded_sdmmc::Mode::ReadWriteCreate, + ) + .expect("open new file"); + volume_mgr + .write(new_file, b"Hello") + .expect("write to new file"); + volume_mgr.close_file(new_file).expect("close new file"); + + let mut has_this = false; + let mut has_parent = false; + let mut has_new_file = false; + volume_mgr + .iterate_dir(new_dir, |item| { + if item.name == ShortFileName::parent_dir() { + has_parent = true; + assert!(item.attributes.is_directory()); + assert_eq!(item.size, 0); + assert_eq!(item.mtime.to_string(), utils::get_time_source_string()); + assert_eq!(item.ctime.to_string(), utils::get_time_source_string()); + } else if item.name == ShortFileName::this_dir() { + has_this = true; + assert!(item.attributes.is_directory()); + assert_eq!(item.size, 0); + assert_eq!(item.mtime.to_string(), utils::get_time_source_string()); + assert_eq!(item.ctime.to_string(), utils::get_time_source_string()); + } else if item.name == test_file_name { + has_new_file = true; + // We wrote "Hello" to it + assert_eq!(item.size, 5); + assert!(!item.attributes.is_directory()); + assert_eq!(item.mtime.to_string(), utils::get_time_source_string()); + assert_eq!(item.ctime.to_string(), utils::get_time_source_string()); + } else { + panic!("Unexpected item in new dir"); + } + }) + .unwrap(); + assert!(has_this); + assert!(has_parent); + assert!(has_new_file); + + // Close the root dir and look again + volume_mgr.close_dir(root_dir).expect("close root"); + volume_mgr.close_dir(new_dir).expect("close new_dir"); + let root_dir = volume_mgr + .open_root_dir(fat32_volume) + .expect("open root dir"); + // Check we can't make it again now it exists + assert!(volume_mgr + .make_dir_in_dir(root_dir, &test_dir_name) + .is_err()); + let new_dir = volume_mgr + .open_dir(root_dir, &test_dir_name) + .expect("find new dir"); + let new_file = volume_mgr + .open_file_in_dir(new_dir, &test_file_name, embedded_sdmmc::Mode::ReadOnly) + .expect("re-open new file"); + volume_mgr.close_dir(root_dir).expect("close root"); + volume_mgr.close_dir(new_dir).expect("close new dir"); + volume_mgr.close_file(new_file).expect("close file"); +} + // **************************************************************************** // // End Of File diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs index b74db45..9976975 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -179,6 +179,14 @@ pub fn make_time_source() -> TestTimeSource { } } +/// Get the test time source time, as a string. +/// +/// We apply the FAT 2-second rounding here. +#[allow(unused)] +pub fn get_time_source_string() -> &'static str { + "2003-04-04 13:30:04" +} + // **************************************************************************** // // End Of File