diff --git a/CHANGELOG.md b/CHANGELOG.md index 24357bc..5fe70e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,12 @@ The format is based on [Keep a Changelog] and this project adheres to [Semantic * `Volume`, `Directory` and `File` are now smart! They hold references to the thing they were made from, and will clean themselves up when dropped. The trade-off is you can can't open multiple volumes, directories or files at the same time. * Renamed the old types to `RawVolume`, `RawDirectory` and `RawFile` * New method `make_dir_in_dir` -* Fixed long-standing bug that caused an integer overflow when a FAT32 directory - was longer than one cluster ([#74]) +* Fixed long-standing bug that caused an integer overflow when a FAT32 directory was longer than one cluster ([#74]) +* Updated 'shell' example to support `mkdir`, `tree` and relative/absolute paths +* Renamed `Error::FileNotFound` to `Error::NotFound` +* New API `change_dir` which changes a directory to point to some child directory (or the parent) without opening a new directory. +* Empty strings and `"."` convert to `ShortFileName::this_dir()` +* You can now open directories multiple times without error [#74]: https://github.com/rust-embedded-community/embedded-sdmmc-rs/issues/74 diff --git a/examples/shell.rs b/examples/shell.rs index 433fb3a..0e41ce1 100644 --- a/examples/shell.rs +++ b/examples/shell.rs @@ -1,15 +1,180 @@ //! A simple shell demo for embedded-sdmmc //! -//! Presents a basic command prompt which implements some basic MS-DOS style shell commands. +//! Presents a basic command prompt which implements some basic MS-DOS style +//! shell commands. +//! +//! Note that `embedded_sdmmc` itself does not care about 'paths' - only +//! accessing files and directories on on disk, relative to some previously +//! opened directory. A 'path' is an operating-system level construct, and can +//! vary greatly (see MS-DOS paths vs POSIX paths). This example, however, +//! implements an MS-DOS style Path API over the top of embedded-sdmmc. Feel +//! free to copy it if it suits your particular application. +//! +//! The four primary partitions are scanned on the given disk image on start-up. +//! Any valid FAT16 or FAT32 volumes are mounted, and given volume labels from +//! `A:` to `D:`, like MS-DOS. Also like MS-DOS, file and directory names use +//! the `8.3` format, like `FILENAME.TXT`. Long filenames are not supported. +//! +//! Unlike MS-DOS, this application uses the POSIX `/` as the directory +//! separator. +//! +//! Every volume has its own *current working directory*. The shell has one +//! *current volume* selected but it remembers the *current working directory* +//! for the unselected volumes. +//! +//! A path comprises: +//! +//! * An optional volume specifier, like `A:` +//! * If the volume specifier is not given, the current volume is used. +//! * An optional `/` to indicate this is an absolute path, not a relative path +//! * If this is a relative path, traversal starts at the Current Working +//! Directory for the volume +//! * An optional sequence of directory names, each followed by a `/` +//! * An optional final filename +//! * If this is missing, then `.` is the default (which selects the +//! containing directory) +//! +//! An *expanded path* has all optional components, and works independently of +//! whichever volume is currently selected, or the current working directory +//! within that volume. The empty path (`""`) is invalid, but commands may +//! assume that in the absence of a path argument they are to use the current +//! working directory on the current volume. +//! +//! As an example, imagine that volume `A:` is the current volume, and we have +//! these current working directories: +//! +//! * `A:` has a CWD of `/CATS` +//! * `B:` has a CWD of `/DOGS` +//! +//! The following path expansions would occur: +//! +//! | Given Path | Volume | Absolute | Directory Names | Final Filename | Expanded Path | +//! | --------------------------- | ------- | -------- | ------------------ | -------------- | ------------------------------ | +//! | `NAMES.CSV` | Current | No | `[]` | `NAMES.CSV` | `A:/CATS/NAMES.CSV` | +//! | `./NAMES.CSV` | Current | No | `[.]` | `NAMES.CSV` | `A:/CATS/NAMES.CSV` | +//! | `BACKUP.000/` | Current | No | `[BACKUP.000]` | None | `A:/CATS/BACKUP.000/.` | +//! | `BACKUP.000/NAMES.CSV` | Current | No | `[BACKUP.000]` | `NAMES.CSV` | `A:/CATS/BACKUP.000/NAMES.CSV` | +//! | `/BACKUP.000/NAMES.CSV` | Current | Yes | `[BACKUP.000]` | `NAMES.CSV` | `A:/BACKUP.000/NAMES.CSV` | +//! | `../BACKUP.000/NAMES.CSV` | Current | No | `[.., BACKUP.000]` | `NAMES.CSV` | `A:/BACKUP.000/NAMES.CSV` | +//! | `A:NAMES.CSV` | `A:` | No | `[]` | `NAMES.CSV` | `A:/CATS/NAMES.CSV` | +//! | `A:./NAMES.CSV` | `A:` | No | `[.]` | `NAMES.CSV` | `A:/CATS/NAMES.CSV` | +//! | `A:BACKUP.000/` | `A:` | No | `[BACKUP.000]` | None | `A:/CATS/BACKUP.000/.` | +//! | `A:BACKUP.000/NAMES.CSV` | `A:` | No | `[BACKUP.000]` | `NAMES.CSV` | `A:/CATS/BACKUP.000/NAMES.CSV` | +//! | `A:/BACKUP.000/NAMES.CSV` | `A:` | Yes | `[BACKUP.000]` | `NAMES.CSV` | `A:/BACKUP.000/NAMES.CSV` | +//! | `A:../BACKUP.000/NAMES.CSV` | `A:` | No | `[.., BACKUP.000]` | `NAMES.CSV` | `A:/BACKUP.000/NAMES.CSV` | +//! | `B:NAMES.CSV` | `B:` | No | `[]` | `NAMES.CSV` | `B:/DOGS/NAMES.CSV` | +//! | `B:./NAMES.CSV` | `B:` | No | `[.]` | `NAMES.CSV` | `B:/DOGS/NAMES.CSV` | +//! | `B:BACKUP.000/` | `B:` | No | `[BACKUP.000]` | None | `B:/DOGS/BACKUP.000/.` | +//! | `B:BACKUP.000/NAMES.CSV` | `B:` | No | `[BACKUP.000]` | `NAMES.CSV` | `B:/DOGS/BACKUP.000/NAMES.CSV` | +//! | `B:/BACKUP.000/NAMES.CSV` | `B:` | Yes | `[BACKUP.000]` | `NAMES.CSV` | `B:/BACKUP.000/NAMES.CSV` | +//! | `B:../BACKUP.000/NAMES.CSV` | `B:` | No | `[.., BACKUP.000]` | `NAMES.CSV` | `B:/BACKUP.000/NAMES.CSV` | use std::io::prelude::*; -use embedded_sdmmc::{Error, RawDirectory, RawVolume, VolumeIdx, VolumeManager}; +use embedded_sdmmc::{ + Error as EsError, RawDirectory, RawVolume, ShortFileName, VolumeIdx, VolumeManager, +}; use crate::linux::{Clock, LinuxBlockDevice}; +type Error = EsError; + mod linux; +/// Represents a path on a volume within `embedded_sdmmc`. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +#[repr(transparent)] +struct Path(str); + +impl std::ops::Deref for Path { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Path { + /// Create a new Path from a string slice. + /// + /// The `Path` borrows the string slice. No validation is performed on the + /// path. + fn new + ?Sized>(s: &S) -> &Path { + unsafe { &*(s.as_ref() as *const str as *const Path) } + } + + /// Does this path specify a volume? + fn volume(&self) -> Option { + let mut char_iter = self.chars(); + match (char_iter.next(), char_iter.next()) { + (Some(volume), Some(':')) => Some(volume), + _ => None, + } + } + + /// Is this an absolute path? + fn is_absolute(&self) -> bool { + let tail = self.without_volume(); + tail.starts_with('/') + } + + /// Iterate through the directory components. + /// + /// This will exclude the final path component (i.e. it will not include the + /// 'basename'). + fn iterate_dirs(&self) -> impl Iterator { + let path = self.without_volume(); + let path = path.strip_prefix('/').unwrap_or(path); + if let Some((directories, _basename)) = path.rsplit_once('/') { + directories.split('/') + } else { + "".split('/') + } + } + + /// Iterate through all the components. + /// + /// This will include the final path component (i.e. it will include the + /// 'basename'). + fn iterate_components(&self) -> impl Iterator { + let path = self.without_volume(); + let path = path.strip_prefix('/').unwrap_or(path); + path.split('/') + } + + /// Get the final component of this path (the 'basename'). + fn basename(&self) -> Option<&str> { + if let Some((_, basename)) = self.rsplit_once('/') { + if basename.is_empty() { + None + } else { + Some(basename) + } + } else { + let path = self.without_volume(); + Some(path) + } + } + + /// Return this [`Path`], but without a leading volume. + fn without_volume(&self) -> &Path { + if let Some((volume, tail)) = self.split_once(':') { + // only support single char drive letters + if volume.chars().count() == 1 { + return Path::new(tail); + } + } + self + } +} + +impl PartialEq for Path { + fn eq(&self, other: &str) -> bool { + let s: &str = self; + s == other + } +} + struct VolumeState { directory: RawDirectory, volume: RawVolume, @@ -30,134 +195,305 @@ impl Context { s.path.clone() } - fn process_line(&mut self, line: &str) -> Result<(), Error> { - if line == "help" { - println!("Commands:"); - println!("\thelp -> this help text"); - println!("\t: -> change volume/partition"); - println!("\tdir -> do a directory listing"); - println!("\tstat -> print volume manager status"); - println!("\tcat -> print a text file"); - 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; - } else if line == "1:" { - self.current_volume = 1; - } else if line == "2:" { - self.current_volume = 2; - } else if line == "3:" { - self.current_volume = 3; - } else if line == "stat" { - println!("Status:\n{:#?}", self.volume_mgr); - } else if line == "dir" { - let Some(s) = &self.volumes[self.current_volume] else { - println!("That volume isn't available"); - return Ok(()); - }; - self.volume_mgr.iterate_dir(s.directory, |entry| { - println!( - "{: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(()); - }; - let d = self.volume_mgr.open_dir(s.directory, arg)?; - self.volume_mgr.close_dir(s.directory)?; - s.directory = d; - if arg == ".." { - s.path.pop(); - } else { - s.path.push(arg.to_owned()); + /// Print some help text + fn help(&mut self) -> Result<(), Error> { + println!("Commands:"); + println!("\thelp -> this help text"); + println!("\t: -> change volume/partition"); + println!("\tstat -> print volume manager status"); + println!("\tdir [] -> do a directory listing"); + println!("\ttree [] -> do a recursive directory listing"); + println!("\tcd .. -> go up a level"); + println!("\tcd -> change into directory "); + println!("\tcat -> print a text file"); + println!("\thexdump -> print a binary file"); + println!("\tmkdir -> create an empty directory"); + println!("\tquit -> exits the program"); + println!(); + println!("Paths can be:"); + println!(); + println!("\t* Bare names, like `FILE.DAT`"); + println!("\t* Relative, like `../SOMEDIR/FILE.DAT` or `./FILE.DAT`"); + println!("\t* Absolute, like `B:/SOMEDIR/FILE.DAT`"); + Ok(()) + } + + /// Print volume manager status + fn stat(&mut self) -> Result<(), Error> { + println!("Status:\n{:#?}", self.volume_mgr); + Ok(()) + } + + /// Print a directory listing + fn dir(&mut self, path: &Path) -> Result<(), Error> { + println!("Directory listing of {:?}", path); + 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 + ); + })?; + Ok(()) + } + + /// Print a recursive directory listing for the given path + fn tree(&mut self, path: &Path) -> Result<(), Error> { + println!("Directory listing of {:?}", path); + let dir = self.resolve_existing_directory(path)?; + // tree_dir will close this directory, always + self.tree_dir(dir) + } + + /// Print a recursive directory listing for the given open directory. + /// + /// Will close the given directory. + fn tree_dir(&mut self, dir: RawDirectory) -> Result<(), Error> { + let mut dir = dir.to_directory(&mut self.volume_mgr); + let mut children = Vec::new(); + dir.iterate_dir(|entry| { + println!( + "{:12} {:9} {} {} {:08X?} {:?}", + entry.name, entry.size, entry.ctime, entry.mtime, entry.cluster, entry.attributes + ); + if entry.attributes.is_directory() + && entry.name != ShortFileName::this_dir() + && entry.name != ShortFileName::parent_dir() + { + children.push(entry.name.clone()); } - } 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(()); + })?; + // Be sure to close this, no matter what happens + let dir = dir.to_raw_directory(); + for child in children { + println!("Entering {}", child); + let child_dir = match self.volume_mgr.open_dir(dir, &child) { + Ok(child_dir) => child_dir, + Err(e) => { + self.volume_mgr.close_dir(dir).expect("close open dir"); + return Err(e); + } }; - let mut f = self - .volume_mgr - .open_file_in_dir(s.directory, arg, embedded_sdmmc::Mode::ReadOnly)? - .to_file(&mut self.volume_mgr); - let mut data = Vec::new(); - while !f.is_eof() { - let mut buffer = vec![0u8; 65536]; - let n = f.read(&mut buffer)?; - // read n bytes - data.extend_from_slice(&buffer[0..n]); - println!("Read {} bytes, making {} total", n, data.len()); + let result = self.tree_dir(child_dir); + println!("Returning from {}", child); + if let Err(e) = result { + self.volume_mgr.close_dir(dir).expect("close open dir"); + return Err(e); } - if let Ok(s) = std::str::from_utf8(&data) { - println!("{}", s); + } + self.volume_mgr.close_dir(dir).expect("close open dir"); + Ok(()) + } + + /// Change into `` + /// + /// * An arg of `..` goes up one level + /// * A relative arg like `../FOO` goes up a level and then into the `FOO` + /// sub-folder, starting from the current directory on the current volume + /// * An absolute path like `B:/FOO` changes the CWD on Volume 1 to path + /// `/FOO` + fn cd(&mut self, full_path: &Path) -> Result<(), Error> { + let volume_idx = self.resolve_volume(full_path)?; + let d = self.resolve_existing_directory(full_path)?; + let Some(s) = &mut self.volumes[volume_idx] else { + self.volume_mgr.close_dir(d).expect("close open dir"); + return Err(Error::NoSuchVolume); + }; + self.volume_mgr + .close_dir(s.directory) + .expect("close open dir"); + s.directory = d; + if full_path.is_absolute() { + s.path.clear(); + } + for fragment in full_path.iterate_components().filter(|s| !s.is_empty()) { + if fragment == ".." { + s.path.pop(); } else { - println!("I'm afraid that file isn't UTF-8 encoded"); + s.path.push(fragment.to_owned()); } - } 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(()); - }; - let mut f = self - .volume_mgr - .open_file_in_dir(s.directory, arg, embedded_sdmmc::Mode::ReadOnly)? - .to_file(&mut self.volume_mgr); - let mut data = Vec::new(); - while !f.is_eof() { - let mut buffer = vec![0u8; 65536]; - let n = f.read(&mut buffer)?; - // read n bytes - data.extend_from_slice(&buffer[0..n]); - println!("Read {} bytes, making {} total", n, data.len()); + } + Ok(()) + } + + /// print a text file + fn cat(&mut self, filename: &Path) -> Result<(), Error> { + let (dir, filename) = self.resolve_filename(filename)?; + let mut dir = dir.to_directory(&mut self.volume_mgr); + let mut f = dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadOnly)?; + let mut data = Vec::new(); + while !f.is_eof() { + let mut buffer = vec![0u8; 65536]; + let n = f.read(&mut buffer)?; + // read n bytes + data.extend_from_slice(&buffer[0..n]); + println!("Read {} bytes, making {} total", n, data.len()); + } + if let Ok(s) = std::str::from_utf8(&data) { + println!("{}", s); + } else { + println!("I'm afraid that file isn't UTF-8 encoded"); + } + Ok(()) + } + + /// print a binary file + fn hexdump(&mut self, filename: &Path) -> Result<(), Error> { + let (dir, filename) = self.resolve_filename(filename)?; + let mut dir = dir.to_directory(&mut self.volume_mgr); + let mut f = dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadOnly)?; + let mut data = Vec::new(); + while !f.is_eof() { + let mut buffer = vec![0u8; 65536]; + let n = f.read(&mut buffer)?; + // read n bytes + data.extend_from_slice(&buffer[0..n]); + println!("Read {} bytes, making {} total", n, data.len()); + } + for (idx, chunk) in data.chunks(16).enumerate() { + print!("{:08x} | ", idx * 16); + for b in chunk { + print!("{:02x} ", b); } - for (idx, chunk) in data.chunks(16).enumerate() { - print!("{:08x} | ", idx * 16); - for b in chunk { - print!("{:02x} ", b); - } - for _padding in 0..(16 - chunk.len()) { - print!(" "); - } - print!("| "); - for b in chunk { - print!( - "{}", - if b.is_ascii_graphic() { - *b as char - } else { - '.' - } - ); - } - println!(); + for _padding in 0..(16 - chunk.len()) { + print!(" "); } - } 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)?; + print!("| "); + for b in chunk { + print!( + "{}", + if b.is_ascii_graphic() { + *b as char + } else { + '.' + } + ); + } + println!(); + } + Ok(()) + } + + /// create a directory + fn mkdir(&mut self, dir_name: &Path) -> Result<(), Error> { + let (dir, filename) = self.resolve_filename(dir_name)?; + let mut dir = dir.to_directory(&mut self.volume_mgr); + dir.make_dir_in_dir(filename) + } + + fn process_line(&mut self, line: &str) -> Result<(), Error> { + if line == "help" { + self.help()?; + } else if line == "A:" || line == "a:" { + self.current_volume = 0; + } else if line == "B:" || line == "b:" { + self.current_volume = 1; + } else if line == "C:" || line == "c:" { + self.current_volume = 2; + } else if line == "D:" || line == "d:" { + self.current_volume = 3; + } else if line == "dir" { + self.dir(Path::new("."))?; + } else if let Some(path) = line.strip_prefix("dir ") { + self.dir(Path::new(path.trim()))?; + } else if line == "tree" { + self.tree(Path::new("."))?; + } else if let Some(path) = line.strip_prefix("tree ") { + self.tree(Path::new(path.trim()))?; + } else if line == "stat" { + self.stat()?; + } else if let Some(path) = line.strip_prefix("cd ") { + self.cd(Path::new(path.trim()))?; + } else if let Some(path) = line.strip_prefix("cat ") { + self.cat(Path::new(path.trim()))?; + } else if let Some(path) = line.strip_prefix("hexdump ") { + self.hexdump(Path::new(path.trim()))?; + } else if let Some(path) = line.strip_prefix("mkdir ") { + self.mkdir(Path::new(path.trim()))?; } else { println!("Unknown command {line:?} - try 'help' for help"); } Ok(()) } + + /// Resolves an existing directory. + /// + /// Converts a string path into a directory handle. + /// + /// * Bare names (no leading `.`, `/` or `N:/`) are mapped to the current + /// directory in the current volume. + /// * Relative names, like `../SOMEDIR` or `./SOMEDIR`, traverse + /// starting at the current volume and directory. + /// * Absolute, like `B:/SOMEDIR/OTHERDIR` start at the given volume. + fn resolve_existing_directory(&mut self, full_path: &Path) -> Result { + let (dir, fragment) = self.resolve_filename(full_path)?; + let mut work_dir = dir.to_directory(&mut self.volume_mgr); + work_dir.change_dir(fragment)?; + Ok(work_dir.to_raw_directory()) + } + + /// Either get the volume from the path, or pick the current volume. + fn resolve_volume(&self, path: &Path) -> Result { + match path.volume() { + None => Ok(self.current_volume), + Some('A' | 'a') => Ok(0), + Some('B' | 'b') => Ok(1), + Some('C' | 'c') => Ok(2), + Some('D' | 'd') => Ok(3), + Some(_) => Err(Error::NoSuchVolume), + } + } + + /// Resolves a filename. + /// + /// Converts a string path into a directory handle and a name within that + /// directory (that may or may not exist). + /// + /// * Bare names (no leading `.`, `/` or `N:/`) are mapped to the current + /// directory in the current volume. + /// * Relative names, like `../SOMEDIR/SOMEFILE` or `./SOMEDIR/SOMEFILE`, traverse + /// starting at the current volume and directory. + /// * Absolute, like `B:/SOMEDIR/SOMEFILE` start at the given volume. + fn resolve_filename<'path>( + &mut self, + full_path: &'path Path, + ) -> Result<(RawDirectory, &'path str), Error> { + let volume_idx = self.resolve_volume(full_path)?; + let Some(s) = &mut self.volumes[volume_idx] else { + return Err(Error::NoSuchVolume); + }; + let mut work_dir = if full_path.is_absolute() { + // relative to root + self.volume_mgr + .open_root_dir(s.volume)? + .to_directory(&mut self.volume_mgr) + } else { + // relative to CWD + self.volume_mgr + .open_dir(s.directory, ".")? + .to_directory(&mut self.volume_mgr) + }; + + for fragment in full_path.iterate_dirs() { + work_dir.change_dir(fragment)?; + } + Ok(( + work_dir.to_raw_directory(), + full_path.basename().unwrap_or("."), + )) + } + + /// Convert a volume index to a letter + fn volume_to_letter(volume: usize) -> char { + match volume { + 0 => 'A', + 1 => 'B', + 2 => 'C', + 3 => 'D', + _ => panic!("Invalid volume ID"), + } + } } impl Drop for Context { @@ -178,7 +514,7 @@ impl Drop for Context { } } -fn main() -> Result<(), Error> { +fn main() -> Result<(), Error> { env_logger::init(); let mut args = std::env::args().skip(1); let filename = args.next().unwrap_or_else(|| "/dev/mmcblk0".into()); @@ -197,7 +533,7 @@ 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", volume_no,); + println!("Volume # {}: found", Context::volume_to_letter(volume_no)); match ctx.volume_mgr.open_root_dir(volume) { Ok(root_dir) => { ctx.volumes[volume_no] = Some(VolumeState { @@ -233,7 +569,7 @@ fn main() -> Result<(), Error> { }; loop { - print!("{}:/", ctx.current_volume); + print!("{}:/", Context::volume_to_letter(ctx.current_volume)); print!("{}", ctx.current_path().join("/")); print!("> "); std::io::stdout().flush().unwrap(); diff --git a/src/fat/volume.rs b/src/fat/volume.rs index c41591f..05ffc0e 100644 --- a/src/fat/volume.rs +++ b/src/fat/volume.rs @@ -575,7 +575,7 @@ impl FatVolume { match_name, block, ) { - Err(Error::FileNotFound) => continue, + Err(Error::NotFound) => continue, x => return x, } } @@ -592,7 +592,7 @@ impl FatVolume { current_cluster = None; } } - Err(Error::FileNotFound) + Err(Error::NotFound) } FatSpecificInfo::Fat32(fat32_info) => { let mut current_cluster = match dir.cluster { @@ -609,7 +609,7 @@ impl FatVolume { match_name, block, ) { - Err(Error::FileNotFound) => continue, + Err(Error::NotFound) => continue, x => return x, } } @@ -619,7 +619,7 @@ impl FatVolume { _ => None, } } - Err(Error::FileNotFound) + Err(Error::NotFound) } } } @@ -653,7 +653,7 @@ impl FatVolume { return Ok(dir_entry.get_entry(fat_type, block, start)); } } - Err(Error::FileNotFound) + Err(Error::NotFound) } /// Delete an entry from the given directory @@ -691,7 +691,7 @@ impl FatVolume { // Scan the cluster / root dir a block at a time for block in first_dir_block_num.range(dir_size) { match self.delete_entry_in_block(block_device, match_name, block) { - Err(Error::FileNotFound) => { + Err(Error::NotFound) => { // Carry on } x => { @@ -731,7 +731,7 @@ impl FatVolume { let block_idx = self.cluster_to_block(cluster); for block in block_idx.range(BlockCount(u32::from(self.blocks_per_cluster))) { match self.delete_entry_in_block(block_device, match_name, block) { - Err(Error::FileNotFound) => { + Err(Error::NotFound) => { // Carry on continue; } @@ -755,7 +755,7 @@ impl FatVolume { } // If we get here we never found the right entry in any of the // blocks that made up the directory - Err(Error::FileNotFound) + Err(Error::NotFound) } /// Deletes a directory entry from a block of directory entries. @@ -790,7 +790,7 @@ impl FatVolume { .map_err(Error::DeviceError); } } - Err(Error::FileNotFound) + Err(Error::NotFound) } /// Finds the next free cluster after the start_cluster and before end_cluster diff --git a/src/filesystem/directory.rs b/src/filesystem/directory.rs index 684fc47..f0a7bb0 100644 --- a/src/filesystem/directory.rs +++ b/src/filesystem/directory.rs @@ -121,6 +121,19 @@ where Ok(d.to_directory(self.volume_mgr)) } + /// Change to a directory, mutating this object. + /// + /// You can then read the directory entries with `iterate_dir` and `open_file_in_dir`. + pub fn change_dir(&mut self, name: N) -> Result<(), Error> + where + N: ToShortFileName, + { + let d = self.volume_mgr.open_dir(self.raw_directory, name)?; + self.volume_mgr.close_dir(self.raw_directory).unwrap(); + self.raw_directory = d; + Ok(()) + } + /// Look in a directory for a named file. pub fn find_directory_entry(&mut self, name: N) -> Result> where @@ -161,6 +174,14 @@ where self.volume_mgr.delete_file_in_dir(self.raw_directory, name) } + /// Make a directory inside this directory + pub fn make_dir_in_dir(&mut self, name: N) -> Result<(), Error> + where + N: ToShortFileName, + { + self.volume_mgr.make_dir_in_dir(self.raw_directory, name) + } + /// Convert back to a raw directory pub fn to_raw_directory(self) -> RawDirectory { let d = self.raw_directory; diff --git a/src/filesystem/filename.rs b/src/filesystem/filename.rs index 120eaa5..a8327ea 100644 --- a/src/filesystem/filename.rs +++ b/src/filesystem/filename.rs @@ -91,6 +91,11 @@ impl ShortFileName { return Ok(ShortFileName::parent_dir()); } + // Special case `.` (or blank), which means "this directory". + if name.is_empty() || name == "." { + return Ok(ShortFileName::this_dir()); + } + let mut idx = 0; let mut seen_dot = false; for ch in name.bytes() { @@ -318,9 +323,16 @@ mod test { assert_eq!(sfn, ShortFileName::create_from_str("1.C").unwrap()); } + #[test] + fn filename_empty() { + assert_eq!( + ShortFileName::create_from_str("").unwrap(), + ShortFileName::this_dir() + ); + } + #[test] fn filename_bad() { - assert!(ShortFileName::create_from_str("").is_err()); assert!(ShortFileName::create_from_str(" ").is_err()); assert!(ShortFileName::create_from_str("123456789").is_err()); assert!(ShortFileName::create_from_str("12345678.ABCD").is_err()); diff --git a/src/lib.rs b/src/lib.rs index 11db6e2..ebf9a4d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -159,8 +159,8 @@ where TooManyOpenFiles, /// Bad handle given BadHandle, - /// That file doesn't exist - FileNotFound, + /// That file or directory doesn't exist + NotFound, /// You can't open a file twice or delete an open file FileAlreadyOpen, /// You can't open a directory twice diff --git a/src/volume_mgr.rs b/src/volume_mgr.rs index 95c09ed..54d1a5f 100644 --- a/src/volume_mgr.rs +++ b/src/volume_mgr.rs @@ -12,9 +12,9 @@ use crate::filesystem::{ SearchIdGenerator, TimeSource, ToShortFileName, MAX_FILE_SIZE, }; use crate::{ - debug, Block, BlockCount, BlockDevice, BlockIdx, Error, RawVolume, Volume, VolumeIdx, - VolumeInfo, VolumeType, PARTITION_ID_FAT16, PARTITION_ID_FAT16_LBA, PARTITION_ID_FAT32_CHS_LBA, - PARTITION_ID_FAT32_LBA, + debug, Block, BlockCount, BlockDevice, BlockIdx, Error, RawVolume, ShortFileName, Volume, + VolumeIdx, VolumeInfo, VolumeType, PARTITION_ID_FAT16, PARTITION_ID_FAT16_LBA, + PARTITION_ID_FAT32_CHS_LBA, PARTITION_ID_FAT32_LBA, }; use heapless::Vec; @@ -206,11 +206,7 @@ where /// You can then read the directory entries with `iterate_dir`, or you can /// use `open_file_in_dir`. pub fn open_root_dir(&mut self, volume: RawVolume) -> Result> { - for dir in self.open_dirs.iter() { - if dir.cluster == ClusterId::ROOT_DIR && dir.volume_id == volume { - return Err(Error::DirAlreadyOpen); - } - } + // Opening a root directory twice is OK let directory_id = RawDirectory(self.id_generator.get()); let dir_info = DirectoryInfo { @@ -229,6 +225,8 @@ where /// Open a directory. /// /// You can then read the directory entries with `iterate_dir` and `open_file_in_dir`. + /// + /// Passing "." as the name results in opening the `parent_dir` a second time. pub fn open_dir( &mut self, parent_dir: RawDirectory, @@ -245,9 +243,25 @@ where let parent_dir_idx = self.get_dir_by_id(parent_dir)?; let volume_idx = self.get_volume_by_id(self.open_dirs[parent_dir_idx].volume_id)?; let short_file_name = name.to_short_filename().map_err(Error::FilenameError)?; + let parent_dir_info = &self.open_dirs[parent_dir_idx]; // Open the directory - let parent_dir_info = &self.open_dirs[parent_dir_idx]; + if short_file_name == ShortFileName::this_dir() { + // short-cut (root dir doesn't have ".") + let directory_id = RawDirectory(self.id_generator.get()); + let dir_info = DirectoryInfo { + directory_id, + volume_id: self.open_volumes[volume_idx].volume_id, + cluster: parent_dir_info.cluster, + }; + + self.open_dirs + .push(dir_info) + .map_err(|_| Error::TooManyOpenDirs)?; + + return Ok(directory_id); + } + let dir_entry = match &self.open_volumes[volume_idx].volume_type { VolumeType::Fat(fat) => { fat.find_directory_entry(&self.block_device, parent_dir_info, &short_file_name)? @@ -260,14 +274,8 @@ where return Err(Error::OpenedFileAsDir); } - // Check it's not already open - for d in self.open_dirs.iter() { - if d.volume_id == self.open_volumes[volume_idx].volume_id - && d.cluster == dir_entry.cluster - { - return Err(Error::DirAlreadyOpen); - } - } + // We don't check if the directory is already open - directories hold + // no cached state and so opening a directory twice is allowable. // Remember this open directory. let directory_id = RawDirectory(self.id_generator.get()); @@ -493,7 +501,7 @@ where } _ => { // We are opening a non-existant file, and that's not OK. - return Err(Error::FileNotFound); + return Err(Error::NotFound); } }; @@ -905,7 +913,7 @@ where Ok(_entry) => { return Err(Error::FileAlreadyExists); } - Err(Error::FileNotFound) => { + Err(Error::NotFound) => { // perfect, let's make it } Err(e) => { diff --git a/tests/directories.rs b/tests/directories.rs index af1cf87..df9154f 100644 --- a/tests/directories.rs +++ b/tests/directories.rs @@ -237,10 +237,9 @@ fn open_dir_twice() { .open_root_dir(fat32_volume) .expect("open root dir"); - assert!(matches!( - volume_mgr.open_root_dir(fat32_volume), - Err(embedded_sdmmc::Error::DirAlreadyOpen) - )); + let root_dir2 = volume_mgr + .open_root_dir(fat32_volume) + .expect("open it again"); assert!(matches!( volume_mgr.open_dir(root_dir, "README.TXT"), @@ -251,13 +250,12 @@ fn open_dir_twice() { .open_dir(root_dir, "TEST") .expect("open test dir"); - assert!(matches!( - volume_mgr.open_dir(root_dir, "TEST"), - Err(embedded_sdmmc::Error::DirAlreadyOpen) - )); + let test_dir2 = volume_mgr.open_dir(root_dir, "TEST").unwrap(); volume_mgr.close_dir(root_dir).expect("close root dir"); volume_mgr.close_dir(test_dir).expect("close test dir"); + volume_mgr.close_dir(test_dir2).expect("close test dir"); + volume_mgr.close_dir(root_dir2).expect("close test dir"); assert!(matches!( volume_mgr.close_dir(test_dir), @@ -316,7 +314,7 @@ fn find_dir_entry() { assert!(matches!( volume_mgr.find_directory_entry(root_dir, "README.TXS"), - Err(embedded_sdmmc::Error::FileNotFound) + Err(embedded_sdmmc::Error::NotFound) )); } @@ -345,7 +343,7 @@ fn delete_file() { assert!(matches!( volume_mgr.delete_file_in_dir(root_dir, "README2.TXT"), - Err(embedded_sdmmc::Error::FileNotFound) + Err(embedded_sdmmc::Error::NotFound) )); volume_mgr.close_file(file).unwrap(); @@ -356,12 +354,12 @@ fn delete_file() { assert!(matches!( volume_mgr.delete_file_in_dir(root_dir, "README.TXT"), - Err(embedded_sdmmc::Error::FileNotFound) + Err(embedded_sdmmc::Error::NotFound) )); assert!(matches!( volume_mgr.open_file_in_dir(root_dir, "README.TXT", Mode::ReadOnly), - Err(embedded_sdmmc::Error::FileNotFound) + Err(embedded_sdmmc::Error::NotFound) )); } diff --git a/tests/open_files.rs b/tests/open_files.rs index 9608a15..dcdaadf 100644 --- a/tests/open_files.rs +++ b/tests/open_files.rs @@ -67,7 +67,7 @@ fn open_files() { assert!(matches!( volume_mgr.open_file_in_dir(root_dir, "README.TXS", Mode::ReadOnly), - Err(Error::FileNotFound) + Err(Error::NotFound) )); // Create a new file